Initial Commit
This commit is contained in:
31
cmd/lassoctl/cmd/file.go
Normal file
31
cmd/lassoctl/cmd/file.go
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// fileCmd represents the file command
|
||||
var fileCmd = &cobra.Command{
|
||||
Use: "file",
|
||||
Short: "Send and retrieve files from a lasso node",
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(fileCmd)
|
||||
}
|
||||
41
cmd/lassoctl/cmd/list.go
Normal file
41
cmd/lassoctl/cmd/list.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// listCmd represents the list command
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Display a list of available nodes on the server",
|
||||
Aliases: []string{"ls"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
nodes := getNodeList()
|
||||
|
||||
for name, node := range nodes {
|
||||
fmt.Println(name+":", node.IP)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(listCmd)
|
||||
}
|
||||
361
cmd/lassoctl/cmd/mssh.go
Normal file
361
cmd/lassoctl/cmd/mssh.go
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
Copyright © 2021 NAME HERE <EMAIL ADDRESS>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
"github.com/melbahja/goph"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// msshCmd represents the mssh command
|
||||
var msshCmd = &cobra.Command{
|
||||
Use: "mssh <user@node...>",
|
||||
Short: "Make an SSH connection to multiple nodes simultaneously",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Create new shell
|
||||
shell := ishell.New()
|
||||
|
||||
var password string
|
||||
// If password prompt requested
|
||||
if viper.GetBool("password") {
|
||||
// Print prompt
|
||||
shell.Print("Password: ")
|
||||
// Read password into variable
|
||||
password = shell.ReadPassword()
|
||||
}
|
||||
|
||||
// Get node list from server
|
||||
nodes := getNodeList()
|
||||
|
||||
// Create new goph auth
|
||||
var auth goph.Auth
|
||||
|
||||
// If password prompt requested
|
||||
if viper.GetBool("password") {
|
||||
// Add password method to auth
|
||||
auth = append(auth, goph.Password(password)...)
|
||||
}
|
||||
|
||||
// If identity given
|
||||
if viper.GetString("identity") != "" {
|
||||
// Get identity as ssh key
|
||||
keyAuth, err := goph.Key(viper.GetString("identity"), "")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting SSH key:")
|
||||
}
|
||||
// Add key to auth
|
||||
auth = append(auth, keyAuth...)
|
||||
}
|
||||
|
||||
// Get all identities in default location
|
||||
keys, err := getAllIdentities()
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Error getting SSH identities")
|
||||
}
|
||||
// Add keys to auth
|
||||
auth = append(auth, keys...)
|
||||
|
||||
// If ssh agent exists
|
||||
if goph.HasAgent() {
|
||||
// Get ssh agent
|
||||
agent, err := goph.UseAgent()
|
||||
if err != nil {
|
||||
log.Fatal().Msg("Error getting SSH agent")
|
||||
}
|
||||
// Add ssh agent to auth
|
||||
auth = append(auth, agent...)
|
||||
}
|
||||
|
||||
// Create map to store goph clients
|
||||
clients := map[string]*goph.Client{}
|
||||
|
||||
// For every device
|
||||
for _, device := range args {
|
||||
// Split device by "@"
|
||||
splitArg := strings.Split(device, "@")
|
||||
|
||||
// If split device has less than two elements, it is invalid
|
||||
if len(splitArg) != 2 {
|
||||
log.Fatal().Msg("Invalid username/node argument")
|
||||
}
|
||||
|
||||
// Get variables from split device
|
||||
username, nodeName := splitArg[0], splitArg[1]
|
||||
|
||||
// Get node from list if it exists
|
||||
node, ok := nodes[nodeName]
|
||||
if !ok {
|
||||
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
|
||||
}
|
||||
|
||||
// Connect to node without verifying known hosts
|
||||
client, err := goph.NewUnknown(username, node.IP, auth)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error connecting to node")
|
||||
}
|
||||
// Add device to client list
|
||||
clients[device] = client
|
||||
}
|
||||
|
||||
// Clode all clients at the end of the function
|
||||
defer closeClients(clients)
|
||||
|
||||
// Add run command to shell
|
||||
shell.AddCmd(&ishell.Cmd{
|
||||
Name: "run",
|
||||
Help: "Run a shell command on all nodes simultaneously.",
|
||||
Func: func(c *ishell.Context) {
|
||||
// Create new wait group
|
||||
wg := &sync.WaitGroup{}
|
||||
// For every client
|
||||
for name, client := range clients {
|
||||
// Add goroutine to wait group
|
||||
wg.Add(1)
|
||||
go func(client *goph.Client, name string) {
|
||||
// Remove from wait group at the end of the function
|
||||
defer wg.Done()
|
||||
// Create command from given arguments
|
||||
cmd, err := client.Command(c.Args[0], c.Args[1:]...)
|
||||
if err != nil {
|
||||
cmdError(shell, name, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get command Stdout pipe
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
cmdError(shell, name, err)
|
||||
return
|
||||
}
|
||||
// Get command Stderr pipe
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
cmdError(shell, name, err)
|
||||
return
|
||||
}
|
||||
// Combine Stdout and Stderr
|
||||
combined := io.MultiReader(stdout, stderr)
|
||||
|
||||
// Start command without waiting for it to finish
|
||||
if err := cmd.Start(); err != nil {
|
||||
cmdError(shell, name, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new scanner for combined output
|
||||
scanner := bufio.NewScanner(combined)
|
||||
for scanner.Scan() {
|
||||
// Print command output
|
||||
cmdOut(shell, name, scanner.Text())
|
||||
}
|
||||
}(client, name)
|
||||
}
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
},
|
||||
})
|
||||
|
||||
// Create file command
|
||||
file := &ishell.Cmd{
|
||||
Name: "file",
|
||||
Help: "Transfer files to/from nodes",
|
||||
}
|
||||
|
||||
// Add send command to file command
|
||||
file.AddCmd(&ishell.Cmd{
|
||||
Name: "send",
|
||||
Help: "Send a file to all nodes. '~' will expand to device's home directory.",
|
||||
Func: func(c *ishell.Context) {
|
||||
// Get local and remote paths
|
||||
localPath := c.Args[0]
|
||||
remotePath := c.Args[1]
|
||||
|
||||
// Create new wait group
|
||||
wg := &sync.WaitGroup{}
|
||||
// For every client
|
||||
for name, client := range clients {
|
||||
// Set new remote to remote path
|
||||
newRemote := remotePath
|
||||
// If new remote starts with "~"
|
||||
if strings.HasPrefix(newRemote, "~") {
|
||||
// Replace ~ with "/home/user"
|
||||
newRemote = filepath.Join(
|
||||
"/home/"+client.User(),
|
||||
strings.TrimPrefix(newRemote, "~"),
|
||||
)
|
||||
}
|
||||
|
||||
// Add one goroutine to wait group
|
||||
wg.Add(1)
|
||||
go func(client *goph.Client, name string) {
|
||||
// Remove from wait group at the end of the function
|
||||
defer wg.Done()
|
||||
|
||||
// Upload file to remote
|
||||
err := client.Upload(localPath, newRemote)
|
||||
if err != nil {
|
||||
shell.Printf("%s [error]: %v\n", name, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print success message
|
||||
cmdSuccess(shell, name, "upload complete")
|
||||
}(client, name)
|
||||
|
||||
// Wait for all added goroutines to complete
|
||||
wg.Wait()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Add retrieve command to shell
|
||||
file.AddCmd(&ishell.Cmd{
|
||||
Name: "retrieve",
|
||||
Aliases: []string{"retr", "recv", "get"},
|
||||
Help: "Retrieve file from all nodes. File will be saved as 'file-user@node.ext'",
|
||||
Func: func(c *ishell.Context) {
|
||||
// Get remote and local paths
|
||||
remotePath := c.Args[0]
|
||||
localPath := c.Args[1]
|
||||
|
||||
// Create new wait group
|
||||
wg := &sync.WaitGroup{}
|
||||
// For every client
|
||||
for name, client := range clients {
|
||||
// Get extension, base, and directory of local path
|
||||
localExt := filepath.Ext(localPath)
|
||||
localBase := filepath.Base(localPath)
|
||||
localDir := filepath.Dir(localPath)
|
||||
|
||||
// Attempt to stat local path
|
||||
info, err := os.Stat(localPath)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting file info")
|
||||
}
|
||||
|
||||
// If local path is a directory
|
||||
if info.IsDir() {
|
||||
// Set local directory to local path
|
||||
localDir = localPath
|
||||
// Set local extention to remote extension
|
||||
localExt = filepath.Ext(remotePath)
|
||||
// Set local base to remote base
|
||||
localBase = filepath.Base(remotePath)
|
||||
}
|
||||
|
||||
// Remove extension from local base
|
||||
localBaseNoExt := strings.TrimSuffix(localBase, localExt)
|
||||
// Create new local, formatted as "filename-user@node.extension"
|
||||
newLocal := filepath.Join(localDir, localBaseNoExt+"-"+name+localExt)
|
||||
|
||||
// Set new remote to remote path
|
||||
newRemote := remotePath
|
||||
// If new remote starts with "~"
|
||||
if strings.HasPrefix(newRemote, "~") {
|
||||
// Replace ~ with "/home/user"
|
||||
newRemote = filepath.Join(
|
||||
"/home/"+client.User(),
|
||||
strings.TrimPrefix(newRemote, "~"),
|
||||
)
|
||||
}
|
||||
|
||||
// Add goroutine to wait group
|
||||
wg.Add(1)
|
||||
go func(client *goph.Client, name string) {
|
||||
// Remove from wait group at end of function
|
||||
defer wg.Done()
|
||||
|
||||
// Download file from remote
|
||||
err := client.Download(newRemote, newLocal)
|
||||
if err != nil {
|
||||
cmdError(shell, name, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print success message
|
||||
cmdSuccess(shell, name, "download complete")
|
||||
}(client, name)
|
||||
|
||||
// Wait for all added goroutines to complete
|
||||
wg.Wait()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Add file command to shell
|
||||
shell.AddCmd(file)
|
||||
|
||||
// Run shell
|
||||
shell.Run()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(msshCmd)
|
||||
msshCmd.Flags().BoolP("password", "p", false, "Prompt for SSH password on start")
|
||||
msshCmd.Flags().StringP("identity", "i", "", "SSH identity file to use")
|
||||
|
||||
viper.BindPFlags(msshCmd.Flags())
|
||||
}
|
||||
|
||||
func getAllIdentities() (goph.Auth, error) {
|
||||
// Get user's home directory
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get all ssh keys in "~/.ssh"
|
||||
matches, err := filepath.Glob(filepath.Join(home, ".ssh", "id_*.pub"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create goph auth for keys
|
||||
var out goph.Auth
|
||||
// For every glob match
|
||||
for _, match := range matches {
|
||||
// Get path of private key
|
||||
privKeyPath := strings.TrimSuffix(match, ".pub")
|
||||
|
||||
// Get SSH key as goph ket
|
||||
auth, err := goph.Key(privKeyPath, "")
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
// Add goph key to auth
|
||||
out = append(out, auth...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func closeClients(clients map[string]*goph.Client) {
|
||||
// For every client
|
||||
for _, client := range clients {
|
||||
// Close client
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
74
cmd/lassoctl/cmd/proxy.go
Normal file
74
cmd/lassoctl/cmd/proxy.go
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// proxyCmd represents the proxy command
|
||||
var proxyCmd = &cobra.Command{
|
||||
Use: "proxy [flags] <user@node> <local-port:host:remote-port>",
|
||||
Short: "Proxy a particular port from a node to a local port",
|
||||
Example: "proxy pi@raspberrypi 8080:localhost:80",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get node list from server
|
||||
nodes := getNodeList()
|
||||
|
||||
// Split first argument by "@"
|
||||
splitArg := strings.Split(args[0], "@")
|
||||
|
||||
// If less than two elements, argumen is invalid
|
||||
if len(splitArg) != 2 {
|
||||
log.Fatal().Msg("Invalid username/node argument")
|
||||
}
|
||||
|
||||
// Get variables from split argument
|
||||
username, nodeName := splitArg[0], splitArg[1]
|
||||
|
||||
// Get node from list if it exists
|
||||
node, ok := nodes[nodeName]
|
||||
if !ok {
|
||||
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
|
||||
}
|
||||
|
||||
// Create ssh command
|
||||
ssh := exec.Command("ssh", username+"@"+node.IP, "-L", args[1], "-N")
|
||||
ssh.Stdin = os.Stdin
|
||||
ssh.Stdout = os.Stdout
|
||||
ssh.Stderr = os.Stderr
|
||||
// Run ssh command
|
||||
if err := ssh.Run(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error received from ssh command")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(proxyCmd)
|
||||
}
|
||||
95
cmd/lassoctl/cmd/retrieve.go
Normal file
95
cmd/lassoctl/cmd/retrieve.go
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// retrieveCmd represents the retrieve command
|
||||
var retrieveCmd = &cobra.Command{
|
||||
Use: "retrieve",
|
||||
Short: "A brief description of your command",
|
||||
Aliases: []string{"retr", "recv", "get"},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get list of nodes from server
|
||||
nodes := getNodeList()
|
||||
|
||||
// Compile regular expression for target argument
|
||||
regex := regexp.MustCompile(`(.+)@(.+):(.+)`)
|
||||
|
||||
var sources []string
|
||||
// For every argument
|
||||
for index, arg := range args {
|
||||
// Stop if last argument
|
||||
if index == len(args)-1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Find submatches in current argument
|
||||
submatches := regex.FindStringSubmatch(arg)
|
||||
// If no submatches found
|
||||
if submatches == nil {
|
||||
log.Fatal().Msg("Invalid target argument")
|
||||
}
|
||||
|
||||
// Get variables from submatches
|
||||
username, nodeName, path := submatches[1], submatches[2], submatches[3]
|
||||
|
||||
// Get node from list if it exists
|
||||
node, ok := nodes[nodeName]
|
||||
if !ok {
|
||||
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
|
||||
}
|
||||
|
||||
// Add new source to sources slice
|
||||
sources = append(sources, username+"@"+node.IP+":"+path)
|
||||
}
|
||||
|
||||
// Get last argument
|
||||
file := args[len(args)-1]
|
||||
|
||||
// Create arguments for scp command
|
||||
var scpArgs []string
|
||||
scpArgs = append(scpArgs, sources...)
|
||||
scpArgs = append(scpArgs, file)
|
||||
|
||||
// Create scp command
|
||||
scp := exec.Command("scp", scpArgs...)
|
||||
scp.Stdin = os.Stdin
|
||||
scp.Stdout = os.Stdout
|
||||
scp.Stderr = os.Stderr
|
||||
// Run scp command
|
||||
if err := scp.Run(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error received from scp command")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
fileCmd.AddCommand(retrieveCmd)
|
||||
}
|
||||
126
cmd/lassoctl/cmd/root.go
Normal file
126
cmd/lassoctl/cmd/root.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/abiosoft/ishell"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/vmihailenco/msgpack/v5"
|
||||
"go.arsenm.dev/lasso/internal/types"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "lassoctl",
|
||||
Short: "Manage devices using IPs retrieved from a lasso server",
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
cobra.CheckErr(rootCmd.Execute())
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "Config file to use")
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" {
|
||||
// Use config file from the flag.
|
||||
viper.SetConfigFile(cfgFile)
|
||||
} else {
|
||||
// Find home directory.
|
||||
home, err := os.UserHomeDir()
|
||||
cobra.CheckErr(err)
|
||||
|
||||
// Search config in home directory with name ".lassoctl" (without extension).
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath(home)
|
||||
viper.SetConfigType("toml")
|
||||
viper.SetConfigName("lassoctl")
|
||||
}
|
||||
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
viper.ReadInConfig()
|
||||
}
|
||||
|
||||
// getNodeList gets a list of nodes from the lasso server
|
||||
func getNodeList() map[string]types.Node {
|
||||
// Get server address
|
||||
addr := net.JoinHostPort(viper.GetString("server.addr"), viper.GetString("server.port"))
|
||||
// Create base url for HTTPS server
|
||||
baseURL := "https://" + addr
|
||||
|
||||
// Get node list
|
||||
res, err := http.Get(baseURL + "/node/list")
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error getting node list from server")
|
||||
}
|
||||
|
||||
var resp types.Response
|
||||
// Decode server response
|
||||
err = msgpack.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error decoding server response")
|
||||
}
|
||||
|
||||
// If server response is an error
|
||||
if resp.Error {
|
||||
log.Fatal().Str("error", resp.Message).Msg("Server returned error")
|
||||
}
|
||||
|
||||
var nodes map[string]types.Node
|
||||
// Decode response data as node map
|
||||
err = mapstructure.Decode(resp.Data, &nodes)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("Error decoding nodes map")
|
||||
}
|
||||
|
||||
// Return node map
|
||||
return nodes
|
||||
}
|
||||
|
||||
// cmdError prints an error while running mssh
|
||||
func cmdError(shell *ishell.Shell, node string, err error) {
|
||||
shell.Printf("%s [error]: %v\n", node, err)
|
||||
}
|
||||
|
||||
// cmdOut prints output from a command while running mssh
|
||||
func cmdOut(shell *ishell.Shell, node, out string) {
|
||||
shell.Printf("%s [out]: %s\n", node, out)
|
||||
}
|
||||
|
||||
// cmdSucces prints a success message while running mssh
|
||||
func cmdSuccess(shell *ishell.Shell, node, msg string) {
|
||||
shell.Printf("%s [success]: %s\n", node, msg)
|
||||
}
|
||||
81
cmd/lassoctl/cmd/send.go
Normal file
81
cmd/lassoctl/cmd/send.go
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// sendCmd represents the send command
|
||||
var sendCmd = &cobra.Command{
|
||||
Use: "send <file...> <username@node:path>",
|
||||
Short: "Send a file to the node via ssh",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get list of nodes from server
|
||||
nodes := getNodeList()
|
||||
|
||||
// Compile regular expression for target argument
|
||||
regex := regexp.MustCompile(`(.+)@(.+):(.+)`)
|
||||
// Find submatches within last argument
|
||||
submatches := regex.FindStringSubmatch(args[len(args)-1])
|
||||
|
||||
// If no submatches found
|
||||
if submatches == nil {
|
||||
log.Fatal().Msg("Invalid target argument")
|
||||
}
|
||||
|
||||
// Files are all arguments except last one
|
||||
files := args[0 : len(args)-1]
|
||||
// Get variables from submatches
|
||||
username, nodeName, path := submatches[1], submatches[2], submatches[3]
|
||||
|
||||
// Get node from list if it exists
|
||||
node, ok := nodes[nodeName]
|
||||
if !ok {
|
||||
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
|
||||
}
|
||||
|
||||
// Create arguments for scp command
|
||||
var scpArgs []string
|
||||
scpArgs = append(scpArgs, files...)
|
||||
scpArgs = append(scpArgs, username+"@"+node.IP+":"+path)
|
||||
|
||||
// Create scp command
|
||||
scp := exec.Command("scp", scpArgs...)
|
||||
scp.Stdin = os.Stdin
|
||||
scp.Stdout = os.Stdout
|
||||
scp.Stderr = os.Stderr
|
||||
// Run scp command
|
||||
if err := scp.Run(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error received from scp command")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
fileCmd.AddCommand(sendCmd)
|
||||
}
|
||||
72
cmd/lassoctl/cmd/ssh.go
Normal file
72
cmd/lassoctl/cmd/ssh.go
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// sshCmd represents the ssh command
|
||||
var sshCmd = &cobra.Command{
|
||||
Use: "ssh <user@node>",
|
||||
Short: "Make an ssh connection to the specified node",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get list of nodes from server
|
||||
nodes := getNodeList()
|
||||
|
||||
// Split first argument by "@"
|
||||
splitArg := strings.Split(args[0], "@")
|
||||
|
||||
// If split string has less than two elements, argument is invalid
|
||||
if len(splitArg) != 2 {
|
||||
log.Fatal().Msg("Invalid username/node argument")
|
||||
}
|
||||
|
||||
// Get variables from split string
|
||||
username, nodeName := splitArg[0], splitArg[1]
|
||||
|
||||
// Get node from list if it exists
|
||||
node, ok := nodes[nodeName]
|
||||
if !ok {
|
||||
log.Fatal().Str("node", nodeName).Msg("Node does not exist on the server")
|
||||
}
|
||||
|
||||
// Create SSH command using node IP
|
||||
ssh := exec.Command("ssh", username+"@"+node.IP)
|
||||
ssh.Stdin = os.Stdin
|
||||
ssh.Stdout = os.Stdout
|
||||
ssh.Stderr = os.Stderr
|
||||
// Attempt to run command
|
||||
if err := ssh.Run(); err != nil {
|
||||
log.Fatal().Err(err).Msg("Error received from ssh command")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(sshCmd)
|
||||
}
|
||||
37
cmd/lassoctl/main.go
Normal file
37
cmd/lassoctl/main.go
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright © 2021 Arsen Musayelyan
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.arsenm.dev/lasso/cmd/lassoctl/cmd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Disable certificate verification as server uses self-signed key
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
}
|
||||
|
||||
func main() {
|
||||
cmd.Execute()
|
||||
}
|
||||
Reference in New Issue
Block a user