From 90acb7ecb36e565774329713bb70f13eb445a758 Mon Sep 17 00:00:00 2001 From: rparikh <2824564+roopakparikh@users.noreply.github.com> Date: Thu, 22 Oct 2020 17:25:10 -0700 Subject: [PATCH] Adding capability to use SSH for remote prep-node. This change introduces the use of SSHClient and simple library that uses golang ssh client to execute commands remotely. I have tested prep-node remotely, but nothing else. Will be doing more refactoring as more testing is performed. --- cmd/bootstrap.go | 2 +- cmd/context.go | 4 +- cmd/prepNode.go | 48 +++++++++- cmd/root.go | 13 ++- go.mod | 3 +- go.sum | 12 +++ main.go | 15 +-- pkg/constants/constants.go | 3 +- pkg/pmk/clients/clients.go | 4 +- pkg/pmk/clients/executor.go | 49 +++++++++- pkg/pmk/cluster.go | 2 +- pkg/pmk/common.go | 139 ++-------------------------- pkg/pmk/node.go | 60 ++++++------ pkg/ssh/sshclient.go | 176 ++++++++++++++++++++++++++++++++++++ 14 files changed, 338 insertions(+), 192 deletions(-) create mode 100644 pkg/ssh/sshclient.go diff --git a/cmd/bootstrap.go b/cmd/bootstrap.go index 472a7311..0c5e18d2 100644 --- a/cmd/bootstrap.go +++ b/cmd/bootstrap.go @@ -50,7 +50,7 @@ func bootstrapCmdRun(cmd *cobra.Command, args []string) { log.Fatalf("Unable to load context: %s", err.Error()) } - c, err := clients.New(ctx.Fqdn) + c, err := clients.New(ctx.Fqdn, clients.LocalExecutor{}) if err != nil { log.Fatalf("Unable to load clients: %s", err.Error()) } diff --git a/cmd/context.go b/cmd/context.go index 31da2003..dfe4667a 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -78,6 +78,6 @@ var contextCmdGet = &cobra.Command{ } func init() { - createCmd.AddCommand(contextCmdCreate) - getCmd.AddCommand(contextCmdGet) + rootCmd.AddCommand(contextCmdCreate) + rootCmd.AddCommand(contextCmdGet) } diff --git a/cmd/prepNode.go b/cmd/prepNode.go index f90f410e..dcf6d1cd 100644 --- a/cmd/prepNode.go +++ b/cmd/prepNode.go @@ -8,6 +8,7 @@ import ( "github.com/platform9/pf9ctl/pkg/pmk" "github.com/platform9/pf9ctl/pkg/pmk/clients" "github.com/spf13/cobra" + "io/ioutil" ) // prepNodeCmd represents the prepNode command @@ -30,21 +31,27 @@ var ( func init() { prepNodeCmd.Flags().StringVarP(&user, "user", "u", "", "ssh username for the nodes") prepNodeCmd.Flags().StringVarP(&password, "password", "p", "", "ssh password for the nodes") - prepNodeCmd.Flags().StringVarP(&sshKey, "ssh-key", "s", "", "ssh key for connecting to the nodes") + prepNodeCmd.Flags().StringVarP(&sshKey, "ssh-key", "s", "", "ssh key file for connecting to the nodes") prepNodeCmd.Flags().StringSliceVarP(&ips, "ips", "i", []string{}, "ips of host to be prepared") prepNodeCmd.Flags().BoolVarP(&floatingIP, "floating-ip", "f", false, "") rootCmd.AddCommand(prepNodeCmd) } + func prepNodeRun(cmd *cobra.Command, args []string) { ctx, err := pmk.LoadContext(constants.Pf9DBLoc) if err != nil { log.Fatalf("Unable to load the context: %s\n", err.Error()) } - - c, err := clients.New(ctx.Fqdn) + // TODO: there seems to be a bug, we will need multiple executors one per ip, so at this moment + // it will only work with one remote host + executor, err := getExecutor() + if err != nil { + log.Fatalf("Error connecting to host %s",err.Error()) + } + c, err := clients.New(ctx.Fqdn, executor) if err != nil { log.Fatalf("Unable to load clients needed for the Cmd. Error: %s", err.Error()) } @@ -54,3 +61,38 @@ func prepNodeRun(cmd *cobra.Command, args []string) { log.Fatalf("Unable to prep node: %s\n", err.Error()) } } + +// checkAndValidateRemote check if any of the command line +func checkAndValidateRemote() bool { + foundRemote := false + for _, ip := range ips { + if ip != "localhost" && ip != "127.0.0.1" && ip != "::1" { + // lets create a remote executor, but before that check if we got user and either of password or ssh-key + if user =="" || (sshKey == "" && password == "") { + log.Fatalf("please provider 'user' and one of 'password' or ''ssh-key'") + } + foundRemote = true + return foundRemote + } + } + log.Info("Using local exeuctor") + return foundRemote +} + + +// getExecutor creates the right Executor +func getExecutor() (clients.Executor, error) { + if checkAndValidateRemote() { + var pKey []byte + var err error + if sshKey != "" { + pKey, err = ioutil.ReadFile(sshKey) + if err != nil { + log.Fatalf("Unale to read the sshKey %s, %s", sshKey, err.Error()) + } + } + return clients.NewRemoteExecutor(ips[0], 22, user, pKey, password) + } + log.Info("Using local exeuctor") + return clients.LocalExecutor{}, nil +} diff --git a/cmd/root.go b/cmd/root.go index e3fdd6e4..d31bb398 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -43,10 +43,15 @@ func Execute() { } func initializeBaseDirs() (err error) { - err = os.MkdirAll(constants.Pf9Dir, os.ModeDir) - err = os.MkdirAll(constants.Pf9DBDir, os.ModeDir) - err = os.MkdirAll(constants.Pf9LogDir, os.ModeDir) - + err = os.MkdirAll(constants.Pf9Dir, 0700) + if err != nil { + return + } + err = os.MkdirAll(constants.Pf9DBDir, 0700) + if err != nil { + return + } + err = os.MkdirAll(constants.Pf9LogDir, 0700) return } diff --git a/go.mod b/go.mod index c63ec8b4..10fe468f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/google/uuid v1.1.2 github.com/hashicorp/go-retryablehttp v0.6.7 github.com/mitchellh/go-homedir v1.1.0 + github.com/pkg/sftp v1.12.0 github.com/prometheus/common v0.4.0 github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect github.com/sethgrid/pester v1.1.0 @@ -13,6 +14,6 @@ require ( github.com/spf13/viper v1.6.3 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect go.uber.org/zap v1.10.0 - golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 + golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a gopkg.in/segmentio/analytics-go.v3 v3.1.0 ) diff --git a/go.sum b/go.sum index 8ed2a06e..4c8180ce 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -88,6 +90,10 @@ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.12.0 h1:/f3b24xrDhkhddlaobPe2JgBqfdt+gC/NYl0QY9IOuI= +github.com/pkg/sftp v1.12.0/go.mod h1:fUqqXB5vEgVCZ131L+9say31RAri6aF6KDViawhxKK8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -133,6 +139,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -152,12 +159,14 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -169,6 +178,8 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -195,4 +206,5 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 3f08cd4b..fbcfccc7 100644 --- a/main.go +++ b/main.go @@ -3,18 +3,21 @@ package main import ( - "fmt" - "os" + //"fmt" + //"os" "github.com/platform9/pf9ctl/cmd" ) func main() { + // the program may run a machine different from the one where the installation is happening + // so removing this check. If this check needs to happen it should happen where the installation is + // happening // Check if program is run using root privileges - if os.Geteuid() != 0 { - fmt.Println("This program requires root privileges. Please run the binary as a root user.") - os.Exit(1) - } + //if os.Geteuid() != 0 { + // fmt.Println("This program requires root privileges. Please run the binary as a root user.") + // os.Exit(1) + //} // Read the context variables. cmd.Execute() } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 2ef2f26d..fc60fcf6 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -5,7 +5,8 @@ import ( "path/filepath" "time" ) - +// Pkg structure is meant for other code to import it into their own +// code base, this MUST not be here var ( homeDir, _ = os.UserHomeDir() //Pf9Dir is the base pf9dir diff --git a/pkg/pmk/clients/clients.go b/pkg/pmk/clients/clients.go index 57d05c16..8140838f 100644 --- a/pkg/pmk/clients/clients.go +++ b/pkg/pmk/clients/clients.go @@ -14,12 +14,12 @@ type Client struct { // New creates the clients needed by the CLI // to interact with the external services. -func New(fqdn string) (Client, error) { +func New(fqdn string, executor Executor) (Client, error) { return Client{ Resmgr: NewResmgr(fqdn), Keystone: NewKeystone(fqdn), Qbert: NewQbert(fqdn), - Executor: ExecutorImpl{}, + Executor: executor, Segment: NewSegment(fqdn), }, nil } diff --git a/pkg/pmk/clients/executor.go b/pkg/pmk/clients/executor.go index 4441b776..b22ac2a2 100644 --- a/pkg/pmk/clients/executor.go +++ b/pkg/pmk/clients/executor.go @@ -1,20 +1,61 @@ package clients -import "os/exec" +import ( + "os/exec" + "github.com/platform9/pf9ctl/pkg/ssh" + "github.com/platform9/pf9ctl/pkg/log" + "fmt" +) +// Executor interace abstracts us from local or remote execution type Executor interface { Run(name string, args ...string) error RunWithStdout(name string, args ...string) (string, error) } -type ExecutorImpl struct{} +// LocalExecutor as the name implies executes commands locally +type LocalExecutor struct{} -func (c ExecutorImpl) Run(name string, args ...string) error { +// Run runs a command locally returning just success or failure +func (c LocalExecutor) Run(name string, args ...string) error { cmd := exec.Command(name, args...) return cmd.Run() } -func (c ExecutorImpl) RunWithStdout(name string, args ...string) (string, error) { +// RunWithStdout runs a command locally returning stdout and err +func (c LocalExecutor) RunWithStdout(name string, args ...string) (string, error) { byt, err := exec.Command(name, args...).Output() return string(byt), err } + +// RemoteExecutor as the name implies runs commands usign SSH on remote host +type RemoteExecutor struct{ + Client ssh.Client +} + +// Run runs a command locally returning just success or failure +func (r *RemoteExecutor) Run(name string, args ...string) error { + _,err := r.RunWithStdout(name, args...) + return err +} + +// RunWithStdout runs a command locally returning stdout and err +func (r *RemoteExecutor) RunWithStdout(name string, args ...string) (string, error) { + cmd := name + for _, arg := range args { + cmd = fmt.Sprintf("%s \"%s\"", cmd, arg) + } + stdout, stderr, err := r.Client.RunCommand(cmd) + log.Debug("Running command ",cmd, "stdout:", string(stdout), "stderr:", string(stderr)) + return string(stdout), err +} + +// NewRemoteExecutor create an Executor interface to execute commands remotely +func NewRemoteExecutor(host string, port int, username string, privateKey []byte, password string) (Executor, error) { + client, err := ssh.NewClient(host, port, username, privateKey, password) + if err != nil { + return nil, err + } + re := &RemoteExecutor{Client: client} + return re, nil +} diff --git a/pkg/pmk/cluster.go b/pkg/pmk/cluster.go index 74b1d733..13831a90 100644 --- a/pkg/pmk/cluster.go +++ b/pkg/pmk/cluster.go @@ -44,7 +44,7 @@ func Bootstrap(ctx Context, c clients.Client, req clients.ClusterCreateRequest) return fmt.Errorf("Unable to create cluster: %w", err) } - cmd := `cat /etc/pf9/host_id.conf | grep ^host_id | cut -d = -f2 | cut -d ' ' -f2` + cmd := `\"cat /etc/pf9/host_id.conf | grep ^host_id | cut -d = -f2 | cut -d ' ' -f2\"` output, err := c.Executor.RunWithStdout("bash", "-c", cmd) if err != nil { return fmt.Errorf("Unable to execute command: %w", err) diff --git a/pkg/pmk/common.go b/pkg/pmk/common.go index 8cf5f3cf..45c7792d 100644 --- a/pkg/pmk/common.go +++ b/pkg/pmk/common.go @@ -1,150 +1,23 @@ package pmk import ( - "fmt" - "os" - "os/exec" - "os/user" - "github.com/platform9/pf9ctl/pkg/log" + "github.com/platform9/pf9ctl/pkg/pmk/clients" ) - -func setupNode(hostOS string) (err error) { +// This files needs to be organized little better +func setupNode(hostOS string, exec clients.Executor) (err error) { log.Debug("Received a call to setup the node") - if err := swapOff(); err != nil { - return err - } - - if err := handlePF9UserGroup(); err != nil { - return err - } - - switch hostOS { - case "redhat": - err = redhatCentosPackageInstall() - if err != nil { - return - } - err = ntpInstallActivateRedhatCentos() - - case "debian": - err = ubuntuPackageInstall() - if err != nil { - return - } - err = ntpInstallActivateUbuntu() - - default: - err = fmt.Errorf("Invalid Host: %s", hostOS) - } - - return -} - -func handlePF9UserGroup() error { - err := os.MkdirAll("/opt/pf9", 0755) - if err != nil { + if err := swapOff(exec); err != nil { return err } - - err = createPF9Group("pf9group") - if err != nil { - return err - } - - return createPF9User("pf9") -} - -func createPF9User(name string) error { - log.Info("Creating Pf9 User") - - _, err := user.Lookup(name) - if err != nil { - - if _, ok := err.(user.UnknownUserError); !ok { - return err - } - - log.Debug("User not present, creating it") - if _, err := exec.Command("bash", "-c", "useradd -g pf9group -d '/opt/pf9/home' -s '/bin/bash' pf9").Output(); err != nil { - return err - } - } - return nil } -func createPF9Group(name string) error { - log.Info("Creating Pf9 group") - - _, err := user.LookupGroup(name) - if err != nil { - - if _, ok := err.(user.UnknownGroupError); !ok { - return fmt.Errorf("Unable to crate a pf9group: %s", err.Error()) - } - - cmd := fmt.Sprintf("groupadd %s", name) - if _, err := exec.Command( - "bash", "-c", cmd).Output(); err != nil { - return err - } - - } - - return nil -} - -func ubuntuPackageInstall() error { - log.Info("Installing required ubuntu packages") - - _, err := exec.Command("bash", "-c", "apt-get update ").Output() - _, err = exec.Command("bash", "-c", "apt-get install curl uuid-runtime software-properties-common logrotate -y").Output() - return err -} - -func redhatCentosPackageInstall() error { - log.Debug("Installing required RedHat packages") - - _, err := exec.Command("bash", "-c", "yum install libselinux-python -y").Output() - return err -} - -func ntpInstallActivateUbuntu() error { - log.Info("Installing NTP") - - _, err := exec.Command("bash", "-c", "apt-get install ntp -y").Output() - if err != nil { - return fmt.Errorf("ntp package installation failed: %s", err.Error()) - } - - log.Debug("ntpd installation completed successfully") - _, err = exec.Command("bash", "-c", "systemctl enable --now ntp").Output() - if err != nil { - return fmt.Errorf("ntp startup failed: %s", err.Error()) - } - - return nil -} -func ntpInstallActivateRedhatCentos() error { - _, err := exec.Command("bash", "-c", "yum install ntp -y").Output() - if err != nil { - return fmt.Errorf("ntp package installation failed: %s", err.Error()) - } - - log.Debug("ntpd installation completed successfully") - _, err = exec.Command("bash", "-c", "systemctl enable --now ntpd").Output() - if err != nil { - return fmt.Errorf("ntp startup failed: %s", err.Error()) - } - - return nil -} -func swapOff() error { +func swapOff(exec clients.Executor) error { log.Info("Disabling swap") - _, err := exec.Command("bash", "-c", "swapoff -a").Output() + _, err := exec.RunWithStdout("bash", "-c", "swapoff -a") return err } diff --git a/pkg/pmk/node.go b/pkg/pmk/node.go index 31f85f59..7866996a 100644 --- a/pkg/pmk/node.go +++ b/pkg/pmk/node.go @@ -3,10 +3,7 @@ package pmk import ( "encoding/base64" "fmt" - "io/ioutil" "net/http" - "os/exec" - "runtime" "strings" "time" @@ -18,7 +15,7 @@ import ( // PrepNode sets up prerequisites for k8s stack func PrepNode( ctx Context, - c clients.Client, + allClients clients.Client, user string, password string, sshkey string, @@ -26,22 +23,22 @@ func PrepNode( log.Debug("Received a call to start preping node(s).") - hostOS, err := validatePlatform() + hostOS, err := validatePlatform(allClients.Executor) if err != nil { return fmt.Errorf("Invalid host os: %s", err.Error()) } - present := pf9PackagesPresent(hostOS, c.Executor) + present := pf9PackagesPresent(hostOS, allClients.Executor) if present { return fmt.Errorf("Platform9 packages already present on the host. Please uninstall these packages if you want to prep the node again") } - err = setupNode(hostOS) + err = setupNode(hostOS, allClients.Executor) if err != nil { return fmt.Errorf("Unable to setup node: %s", err.Error()) } - auth, err := c.Keystone.GetAuth( + auth, err := allClients.Keystone.GetAuth( ctx.Username, ctx.Password, ctx.Tenant, @@ -51,13 +48,13 @@ func PrepNode( return fmt.Errorf("Unable to locate keystone credentials: %s", err.Error()) } - if err := installHostAgent(ctx, auth, hostOS); err != nil { + if err := installHostAgent(ctx, auth, hostOS, allClients.Executor); err != nil { return fmt.Errorf("Unable to install hostagent: %w", err) } log.Debug("Identifying the hostID from conf") cmd := `cat /etc/pf9/host_id.conf | grep ^host_id | cut -d = -f2 | cut -d ' ' -f2` - output, err := c.Executor.RunWithStdout("bash", "-c", cmd) + output, err := allClients.Executor.RunWithStdout("bash", "-c", cmd) if err != nil { return fmt.Errorf("Unable to fetch host ID for host authorization: %s", err.Error()) @@ -66,18 +63,18 @@ func PrepNode( hostID := strings.TrimSuffix(output, "\n") time.Sleep(constants.WaitPeriod * time.Second) - if err := c.Resmgr.AuthorizeHost(hostID, auth.Token); err != nil { + if err := allClients.Resmgr.AuthorizeHost(hostID, auth.Token); err != nil { return err } - if err := c.Segment.SendEvent("Prep Node - Successful", auth); err != nil { + if err := allClients.Segment.SendEvent("Prep Node - Successful", auth); err != nil { log.Errorf("Unable to send Segment event for Node prep. Error: %s", err.Error()) } return nil } -func installHostAgent(ctx Context, auth clients.KeystoneAuth, hostOS string) error { +func installHostAgent(ctx Context, auth clients.KeystoneAuth, hostOS string, exec clients.Executor) error { log.Debug("Downloading Hostagent") url := fmt.Sprintf("%s/clarity/platform9-install-%s.sh", ctx.Fqdn, hostOS) @@ -94,15 +91,15 @@ func installHostAgent(ctx Context, auth clients.KeystoneAuth, hostOS string) err switch resp.StatusCode { case 404: - return installHostAgentLegacy(ctx, auth, hostOS) + return installHostAgentLegacy(ctx, auth, hostOS, exec) case 200: - return installHostAgentCertless(ctx, auth, hostOS) + return installHostAgentCertless(ctx, auth, hostOS, exec) default: return fmt.Errorf("Invalid status code when identifiying hostagent type: %d", resp.StatusCode) } } -func installHostAgentCertless(ctx Context, auth clients.KeystoneAuth, hostOS string) error { +func installHostAgentCertless(ctx Context, auth clients.KeystoneAuth, hostOS string, exec clients.Executor) error { log.Info("Downloading Hostagent Installer Certless") url := fmt.Sprintf( @@ -110,7 +107,7 @@ func installHostAgentCertless(ctx Context, auth clients.KeystoneAuth, hostOS str ctx.Fqdn, hostOS) cmd := fmt.Sprintf(`curl --silent --show-error %s -o /tmp/installer.sh`, url) - _, err := exec.Command("bash", "-c", cmd).Output() + _, err := exec.RunWithStdout("bash", "-c", cmd) if err != nil { return err } @@ -124,13 +121,13 @@ func installHostAgentCertless(ctx Context, auth clients.KeystoneAuth, hostOS str decodedPassword := string(decodedBytePassword) installOptions := fmt.Sprintf(`--no-project --controller=%s --username=%s --password=%s`, ctx.Fqdn, ctx.Username, decodedPassword) - _, err = exec.Command("bash", "-c", "chmod +x /tmp/installer.sh").Output() + _, err = exec.RunWithStdout("bash", "-c", "chmod +x /tmp/installer.sh") if err != nil { return err } cmd = fmt.Sprintf(`/tmp/installer.sh --no-proxy --skip-os-check --ntpd %s`, installOptions) - _, err = exec.Command("bash", "-c", cmd).Output() + _, err = exec.RunWithStdout("bash", "-c", cmd) if err != nil { return err } @@ -140,15 +137,10 @@ func installHostAgentCertless(ctx Context, auth clients.KeystoneAuth, hostOS str return nil } -func validatePlatform() (string, error) { +func validatePlatform(exec clients.Executor) (string, error) { log.Debug("Received a call to validate platform") - OS := runtime.GOOS - if OS != "linux" { - return "", fmt.Errorf("Unsupported OS: %s", OS) - } - - data, err := ioutil.ReadFile("/etc/os-release") + data, err := exec.RunWithStdout("cat /etc/os-release") if err != nil { return "", fmt.Errorf("failed reading data from file: %s", err) } @@ -156,10 +148,10 @@ func validatePlatform() (string, error) { strDataLower := strings.ToLower(string(data)) switch { case strings.Contains(strDataLower, "centos") || strings.Contains(strDataLower, "redhat"): - out, err := exec.Command( + out, err := exec.RunWithStdout( "bash", "-c", - "cat /etc/*release | grep '(Core)' | grep 'CentOS Linux release' -m 1 | cut -f4 -d ' '").Output() + "cat /etc/*release | grep '(Core)' | grep 'CentOS Linux release' -m 1 | cut -f4 -d ' '") if err != nil { return "", fmt.Errorf("Couldn't read the OS configuration file os-release: %s", err.Error()) } @@ -168,10 +160,10 @@ func validatePlatform() (string, error) { } case strings.Contains(strDataLower, "ubuntu"): - out, err := exec.Command( + out, err := exec.RunWithStdout( "bash", "-c", - "cat /etc/*os-release | grep -i pretty_name | cut -d ' ' -f 2").Output() + "cat /etc/*os-release | grep -i pretty_name | cut -d ' ' -f 2") if err != nil { return "", fmt.Errorf("Couldn't read the OS configuration file os-release: %s", err.Error()) } @@ -200,26 +192,26 @@ func pf9PackagesPresent(hostOS string, exec clients.Executor) bool { return err == nil } -func installHostAgentLegacy(ctx Context, auth clients.KeystoneAuth, hostOS string) error { +func installHostAgentLegacy(ctx Context, auth clients.KeystoneAuth, hostOS string, exec clients.Executor) error { log.Info("Downloading Hostagent Installer Legacy") url := fmt.Sprintf("%s/private/platform9-install-%s.sh", ctx.Fqdn, hostOS) installOptions := fmt.Sprintf("--insecure --project-name=%s 2>&1 | tee -a /tmp/agent_install", auth.ProjectID) cmd := fmt.Sprintf(`curl --silent --show-error -H "X-Auth-Token: %s" %s -o /tmp/installer.sh`, auth.Token, url) - _, err := exec.Command("bash", "-c", cmd).Output() + _, err := exec.RunWithStdout("bash", "-c", cmd) if err != nil { return err } log.Debug("Hostagent download completed successfully") - _, err = exec.Command("bash", "-c", "chmod +x /tmp/installer.sh").Output() + _, err = exec.RunWithStdout("bash", "-c", "chmod +x /tmp/installer.sh") if err != nil { return err } cmd = fmt.Sprintf(`/tmp/installer.sh --no-proxy --skip-os-check --ntpd %s`, installOptions) - _, err = exec.Command("bash", "-c", cmd).Output() + _, err = exec.RunWithStdout("bash", "-c", cmd) if err != nil { return err } diff --git a/pkg/ssh/sshclient.go b/pkg/ssh/sshclient.go new file mode 100644 index 00000000..e44d115e --- /dev/null +++ b/pkg/ssh/sshclient.go @@ -0,0 +1,176 @@ +// Copyright 2020 Platform9 Systems Inc. +package ssh +// The content of this files are shamelessly copied from the SSH Provider code base of cctl +// the CCTL ssh-provider can't handle large files and hence this step was taken, perhaps +// the original source should have been modified. + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "bufio" + "github.com/pkg/sftp" + "go.uber.org/zap" + "golang.org/x/crypto/ssh" +) +// Client interface provides ways to run command and upload files to remote hosts +type Client interface { + // RunCommand executes the remote command returning the stdout, stderr and any error associated with it + RunCommand(cmd string) ([]byte, []byte, error) + // Uploadfile uploads the srcFile to remoteDestFilePath and changes the mode to the filemode + UploadFile(srcFilePath, remoteDstFilePath string, mode os.FileMode, cb func(read int64, total int64)) error +} + +type client struct { + sshClient *ssh.Client + sftpClient *sftp.Client +} + +const ( + runAsSudo = true +) + +// NewClient creates a new Client that can be used to perform action on a +// machine +func NewClient(host string, port int, username string, privateKey []byte, password string) (Client, error) { + + authMethods := make([]ssh.AuthMethod, 1) + // give preferece to privateKey + if privateKey != nil { + signer, err := ssh.ParsePrivateKey([]byte(privateKey)) + if err != nil { + return nil, fmt.Errorf("error parsing private key: %s", err) + } + authMethods[0] = ssh.PublicKeys(signer) + } else { + authMethods[0] = ssh.Password(password) + } + + sshConfig := &ssh.ClientConfig{ + User: string(username), + Auth: authMethods, + // by default ignore host key checks + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + sshClient, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), sshConfig) + if err != nil { + return nil, fmt.Errorf("unable to dial %s:%d: %s", host, port, err) + } + sftpClient, err := sftp.NewClient(sshClient) + return &client{ + sshClient: sshClient, + sftpClient: sftpClient, + }, nil +} + + +// RunCommand runs a command on the machine and returns stdout and stderr +// separately +func (c *client) RunCommand(cmd string) ([]byte, []byte, error) { + session, err := c.sshClient.NewSession() + if err != nil { + return nil, nil, fmt.Errorf("unable to create session: %s", err) + } + stdOutPipe, err := session.StdoutPipe() + if err != nil { + return nil, nil, fmt.Errorf("unable to pipe stdout: %s", err) + } + stdErrPipe, err := session.StderrPipe() + if err != nil { + return nil, nil, fmt.Errorf("unable to pipe stderr: %s", err) + } + // Prepend sudo if runAsSudo set to true + if runAsSudo { + cmd = fmt.Sprintf("sudo %s", cmd) + } + err = session.Start(cmd) + if err != nil { + return nil, nil, fmt.Errorf("unable to run command: %s", err) + } + stdOut, err := ioutil.ReadAll(stdOutPipe) + stdErr, err := ioutil.ReadAll(stdErrPipe) + err = session.Wait() + if err != nil { + retError := err + switch err.(type) { + case *ssh.ExitError: + retError = fmt.Errorf("command %s failed: %s", cmd, err) + case *ssh.ExitMissingError: + retError = fmt.Errorf("command %s failed (no exit status): %s", cmd, err) + default: + retError = fmt.Errorf("command %s failed: %s", cmd, err) + } + zap.L().Error("Error ", zap.String("stdout", string(stdOut)), + zap.String("stderr", string(stdErr))) + + return stdOut, stdErr, retError + } + return stdOut, stdErr, nil +} + +// Upload writes a file to the machine +func (c *client) UploadFile(localFile string, remoteFilePath string, mode os.FileMode, cb func(read int64, total int64)) error { + // first check if the local file exists or not + localFp, err := os.Open(localFile) + if err != nil { + return fmt.Errorf("unable to read localFile: %s", err) + } + defer localFp.Close() + fInfo, err := localFp.Stat() + if err != nil { + return fmt.Errorf("Unable to find size of the file %s", localFile) + } + + localFileReader := bufio.NewReader(localFp) + // create a progrssReader that will call the callback function after each read + progressReader := newProgressCBReader(fInfo.Size(), localFileReader, cb) + + remoteFile, err := c.sftpClient.Create(remoteFilePath) + if err != nil { + return fmt.Errorf("unable to create file: %s", err) + } + defer remoteFile.Close() + // IMHO this function is misnomer, it actually writes to the remoteFile + _, err = remoteFile.ReadFrom(progressReader) + if err != nil { + // rmove the remote file since write failed and ignore the errors + // we can't do much about it anyways. + c.sftpClient.Remove(remoteFilePath) + return fmt.Errorf("write failed: %s, ", err) + } + err = remoteFile.Chmod(mode) + if err != nil { + return fmt.Errorf("chmod failed: %s", err) + } + return nil +} + +func newProgressCBReader(totalSize int64, orig io.Reader, cb func(read int64, total int64) ) io.Reader { + progReader := &ProgressCBReader{ + TotalSize:totalSize, + ReadCount:0, + ProgressCB: cb, + OrigReader: orig, + } + return progReader +} + +// ProgressCBReader implements a reader that can call back +// a function on regular interval to report progress +type ProgressCBReader struct { + TotalSize int64 + ReadCount int64 + ProgressCB func(read int64, total int64) + OrigReader io.Reader +} + +func (r *ProgressCBReader) Read(p []byte) (int, error) { + read, err:= r.OrigReader.Read(p) + r.ReadCount = r.ReadCount + int64(read) + if r.ProgressCB != nil { + r.ProgressCB(r.ReadCount, r.TotalSize) + } + return read, err +} \ No newline at end of file