diff --git a/cmd/bootstrap.go b/cmd/bootstrap.go index 472a7311..a12e045b 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, ctx.Proxy) if err != nil { log.Fatalf("Unable to load clients: %s", err.Error()) } @@ -69,6 +69,7 @@ func bootstrapCmdRun(cmd *cobra.Command, args []string) { MetalLBAddressPool: metallbIPRange, AllowWorkloadOnMaster: allowWorkloadsOnMaster, Privileged: privileged, + HTTPProxy: ctx.Proxy, } err = pmk.Bootstrap(ctx, c, payload) diff --git a/cmd/context.go b/cmd/context.go index 31da2003..ae484795 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -47,6 +47,10 @@ func contextCmdCreateRun(cmd *cobra.Command, args []string) { service, _ := reader.ReadString('\n') service = strings.TrimSuffix(service, "\n") + fmt.Printf("Proxy: ") + proxy, _ := reader.ReadString('\n') + proxy = strings.TrimSuffix(proxy, "\n") + if region == "" { region = "RegionOne" } @@ -61,6 +65,7 @@ func contextCmdCreateRun(cmd *cobra.Command, args []string) { Password: encodedPasswd, Region: region, Tenant: service, + Proxy: proxy, } if err := pmk.StoreContext(ctx, constants.Pf9DBLoc); err != nil { diff --git a/cmd/prepNode.go b/cmd/prepNode.go index f90f410e..8dbd4982 100644 --- a/cmd/prepNode.go +++ b/cmd/prepNode.go @@ -44,7 +44,7 @@ func prepNodeRun(cmd *cobra.Command, args []string) { log.Fatalf("Unable to load the context: %s\n", err.Error()) } - c, err := clients.New(ctx.Fqdn) + c, err := clients.New(ctx.Fqdn, ctx.Proxy) if err != nil { log.Fatalf("Unable to load clients needed for the Cmd. Error: %s", err.Error()) } diff --git a/go.mod b/go.mod index c63ec8b4..db4fef39 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/platform9/pf9ctl go 1.14 require ( + github.com/PuerkitoBio/rehttp v1.0.0 + github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 // indirect + github.com/benbjohnson/clock v1.0.3 // indirect github.com/google/uuid v1.1.2 github.com/hashicorp/go-retryablehttp v0.6.7 github.com/mitchellh/go-homedir v1.1.0 @@ -11,6 +14,7 @@ require ( github.com/sethgrid/pester v1.1.0 github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.6.3 + github.com/stretchr/testify v1.3.0 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 diff --git a/go.sum b/go.sum index 8ed2a06e..7a6fb602 100644 --- a/go.sum +++ b/go.sum @@ -2,11 +2,17 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/rehttp v1.0.0 h1:aJ7A7YI2lIvOxcJVeUZY4P6R7kKZtLeONjgyKGwOIu8= +github.com/PuerkitoBio/rehttp v1.0.0/go.mod h1:ItsOiHl4XeMOV3rzbZqQRjLc3QQxbE6391/9iNG7rE8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= +github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0/go.mod h1:6L7zgvqo0idzI7IO8de6ZC051AfXb5ipkIJ7bIA2tGA= +github.com/benbjohnson/clock v1.0.3 h1:vkLuvpK4fmtSCuo60+yC63p7y0BmQ8gm5ZXGuBCJyXg= +github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -158,6 +164,7 @@ golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73r 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-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 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= diff --git a/pkg/pmk/clients/clients.go b/pkg/pmk/clients/clients.go index 57d05c16..f5d66136 100644 --- a/pkg/pmk/clients/clients.go +++ b/pkg/pmk/clients/clients.go @@ -1,8 +1,10 @@ package clients -const HTTPMaxRetry = 5 +// HTTPMaxRetry indicates the number of +// retries to be carried out before giving up. +const HTTPMaxRetry = 9 -// Clients struct encapsulate the collection of +// Client struct encapsulate the collection of // external services type Client struct { Resmgr Resmgr @@ -10,16 +12,27 @@ type Client struct { Qbert Qbert Executor Executor Segment Segment + HTTP HTTP } // New creates the clients needed by the CLI // to interact with the external services. -func New(fqdn string) (Client, error) { +func New(fqdn string, proxy string) (Client, error) { + + http, err := NewHTTP( + func(impl *HTTPImpl) { impl.Proxy = proxy }, + func(impl *HTTPImpl) { impl.Retry = HTTPMaxRetry }) + + if err != nil { + return Client{}, err + } + return Client{ - Resmgr: NewResmgr(fqdn), - Keystone: NewKeystone(fqdn), - Qbert: NewQbert(fqdn), + Resmgr: NewResmgr(fqdn, http), + Keystone: NewKeystone(fqdn, http), + Qbert: NewQbert(fqdn, http), Executor: ExecutorImpl{}, Segment: NewSegment(fqdn), + HTTP: http, }, nil } diff --git a/pkg/pmk/clients/http.go b/pkg/pmk/clients/http.go new file mode 100644 index 00000000..c552c806 --- /dev/null +++ b/pkg/pmk/clients/http.go @@ -0,0 +1,59 @@ +package clients + +import ( + "net/http" + "net/url" + "time" + + "github.com/PuerkitoBio/rehttp" +) + +type HTTP interface { + Do(req *http.Request) (*http.Response, error) +} + +type HTTPImpl struct { + Proxy string + Retry int + client *http.Client + ProxyURL *url.URL +} + +func NewHTTP(options ...func(*HTTPImpl)) (*HTTPImpl, error) { + resp := &HTTPImpl{} + + for _, option := range options { + option(resp) + } + + var transport http.RoundTripper + if resp.Proxy != "" { + proxyURL, err := url.Parse(resp.Proxy) + if err != nil { + return nil, err + } + + resp.ProxyURL = proxyURL + transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} + } else { + transport = http.DefaultTransport + } + + t := rehttp.NewTransport(transport, rehttp.RetryAll( + rehttp.RetryAny( + rehttp.RetryTemporaryErr(), + rehttp.RetryStatuses(400, 404), + ), + rehttp.RetryMaxRetries(resp.Retry), + ), + // rehttp.ExpJitterDelay(time.Second*time.Duration(2), time.Second*time.Duration(60)) + rehttp.ConstDelay(time.Second*time.Duration(10))) + + resp.client = &http.Client{Transport: t} + return resp, nil +} + +// Do function simply calls the underlying client to make the request. +func (c HTTPImpl) Do(req *http.Request) (*http.Response, error) { + return c.client.Do(req) +} diff --git a/pkg/pmk/clients/keystone.go b/pkg/pmk/clients/keystone.go index ed922fd5..c15170af 100644 --- a/pkg/pmk/clients/keystone.go +++ b/pkg/pmk/clients/keystone.go @@ -21,11 +21,12 @@ type Keystone interface { } type KeystoneImpl struct { - fqdn string + fqdn string + client HTTP } -func NewKeystone(fqdn string) Keystone { - return KeystoneImpl{fqdn} +func NewKeystone(fqdn string, client HTTP) Keystone { + return KeystoneImpl{fqdn: fqdn, client: client} } func (k KeystoneImpl) GetAuth( @@ -65,7 +66,13 @@ func (k KeystoneImpl) GetAuth( } }`, username, decodedPassword, tenant) - resp, err := http.Post(url, "application/json", strings.NewReader(body)) + req, err := http.NewRequest("POST", url, strings.NewReader(body)) + if err != nil { + return auth, nil + } + req.Header.Add("Content-Type", "application/json") + + resp, err := k.client.Do(req) if err != nil { return auth, err } diff --git a/pkg/pmk/clients/qbert.go b/pkg/pmk/clients/qbert.go index 00a9808d..664dfee8 100644 --- a/pkg/pmk/clients/qbert.go +++ b/pkg/pmk/clients/qbert.go @@ -7,9 +7,7 @@ import ( "net/http" "strings" - rhttp "github.com/hashicorp/go-retryablehttp" "github.com/platform9/pf9ctl/pkg/log" - "github.com/platform9/pf9ctl/pkg/util" ) // CloudProviderType specifies the infrastructure where the cluster runs @@ -24,12 +22,13 @@ type Qbert interface { GetNodePoolID(projectID, token string) (string, error) } -func NewQbert(fqdn string) Qbert { - return QbertImpl{fqdn} +func NewQbert(fqdn string, client HTTP) Qbert { + return QbertImpl{fqdn, client} } type QbertImpl struct { - fqdn string + fqdn string + client HTTP } type ClusterCreateRequest struct { @@ -46,6 +45,7 @@ type ClusterCreateRequest struct { NodePoolUUID string `json:"nodePoolUuid"` EnableMetalLb bool `json:"enableMetallb"` Masterless bool `json:"masterless"` + HTTPProxy string `json:"httpProxy,omitempty"` } func (c QbertImpl) CreateCluster( @@ -75,17 +75,14 @@ func (c QbertImpl) CreateCluster( url := fmt.Sprintf("%s/qbert/v3/%s/clusters", c.fqdn, projectID) - client := http.Client{} req, err := http.NewRequest("POST", url, strings.NewReader(string(byt))) - if err != nil { - fmt.Println(err.Error()) return "", err } req.Header.Set("X-Auth-Token", token) req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := c.client.Do(req) if err != nil { return "", err } @@ -122,18 +119,14 @@ func (c QbertImpl) AttachNode(clusterID, nodeID, projectID, token string) error "%s/qbert/v3/%s/clusters/%s/attach", c.fqdn, projectID, clusterID) - client := rhttp.Client{} - client.RetryMax = 5 - client.CheckRetry = rhttp.CheckRetry(util.RetryPolicyOn404) - - req, err := rhttp.NewRequest("POST", attachEndpoint, strings.NewReader(string(byt))) + req, err := http.NewRequest("POST", attachEndpoint, strings.NewReader(string(byt))) if err != nil { return fmt.Errorf("Unable to create a request: %w", err) } req.Header.Set("X-Auth-Token", token) req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := c.client.Do(req) if err != nil { return fmt.Errorf("Unable to POST request through client: %w", err) } @@ -148,24 +141,25 @@ func (c QbertImpl) AttachNode(clusterID, nodeID, projectID, token string) error func (c QbertImpl) GetNodePoolID(projectID, token string) (string, error) { - qbertAPIEndpoint := fmt.Sprintf("%s/qbert/v3/%s/cloudProviders", c.fqdn, projectID) // Context should return projectID,make changes to keystoneAuth. - client := http.Client{} - + qbertAPIEndpoint := fmt.Sprintf("%s/qbert/v3/%s/cloudProviders", c.fqdn, projectID) req, err := http.NewRequest("GET", qbertAPIEndpoint, nil) if err != nil { return "", err } - req.Header.Set("X-Auth-Token", token) // + req.Header.Set("X-Auth-Token", token) req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + + resp, err := c.client.Do(req) if err != nil { return "", err } + if resp.StatusCode != 200 { return "", fmt.Errorf("Couldn't query the qbert Endpoint: %s", err.Error()) } + var payload []map[string]string decoder := json.NewDecoder(resp.Body) diff --git a/pkg/pmk/clients/resmgr.go b/pkg/pmk/clients/resmgr.go index 57885b2b..6b08ffb1 100644 --- a/pkg/pmk/clients/resmgr.go +++ b/pkg/pmk/clients/resmgr.go @@ -2,10 +2,9 @@ package clients import ( "fmt" + "net/http" - rhttp "github.com/hashicorp/go-retryablehttp" "github.com/platform9/pf9ctl/pkg/log" - "github.com/platform9/pf9ctl/pkg/util" ) type Resmgr interface { @@ -13,24 +12,20 @@ type Resmgr interface { } type ResmgrImpl struct { - fqdn string + fqdn string + client HTTP } -func NewResmgr(fqdn string) Resmgr { - return &ResmgrImpl{fqdn} +func NewResmgr(fqdn string, client HTTP) Resmgr { + return &ResmgrImpl{fqdn: fqdn, client: client} } // AuthorizeHost registers the host with hostID to the resmgr. func (c *ResmgrImpl) AuthorizeHost(hostID string, token string) error { log.Debugf("Authorizing the host: %s with DU: %s", hostID, c.fqdn) - client := rhttp.NewClient() - client.RetryMax = HTTPMaxRetry - client.CheckRetry = rhttp.CheckRetry(util.RetryPolicyOn404) - client.Logger = nil - url := fmt.Sprintf("%s/resmgr/v1/hosts/%s/roles/pf9-kube", c.fqdn, hostID) - req, err := rhttp.NewRequest("PUT", url, nil) + req, err := http.NewRequest("PUT", url, nil) if err != nil { return fmt.Errorf("Unable to create a new request: %w", err) } @@ -38,7 +33,7 @@ func (c *ResmgrImpl) AuthorizeHost(hostID string, token string) error { req.Header.Set("X-Auth-Token", token) req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := c.client.Do(req) if err != nil { return fmt.Errorf("Unable to send request to the client: %w", err) } diff --git a/pkg/pmk/cluster.go b/pkg/pmk/cluster.go index 74b1d733..f1e24c44 100644 --- a/pkg/pmk/cluster.go +++ b/pkg/pmk/cluster.go @@ -3,9 +3,7 @@ package pmk import ( "fmt" "strings" - "time" - "github.com/platform9/pf9ctl/pkg/constants" "github.com/platform9/pf9ctl/pkg/log" "github.com/platform9/pf9ctl/pkg/pmk/clients" "github.com/platform9/pf9ctl/pkg/util" @@ -13,16 +11,22 @@ import ( // Bootstrap simply preps the local node and attach it as master to a newly // created cluster. -func Bootstrap(ctx Context, c clients.Client, req clients.ClusterCreateRequest) error { - log.Debug("Received a call to boostrap the local node") +func Bootstrap( + ctx Context, + c clients.Client, + req clients.ClusterCreateRequest) error { + log.Info("Received a call to boostrap the local node") - resp, err := util.AskBool("Prep local node for kubernetes cluster") - if err != nil || !resp { - log.Errorf("Couldn't fetch user content") + prep, err := util.AskBool("PrepLocal node for kubernetes cluster") + if err != nil { + return fmt.Errorf("Unable to capture user response: %w", err) } - if err := PrepNode(ctx, c, "", "", "", []string{}); err != nil { - return fmt.Errorf("Unable to prepnode: %w", err) + if prep { + err = PrepNode(ctx, c, "", "", "", []string{}) + if err != nil { + return fmt.Errorf("Unable to prepnode: %w", err) + } } keystoneAuth, err := c.Keystone.GetAuth( @@ -43,17 +47,15 @@ func Bootstrap(ctx Context, c clients.Client, req clients.ClusterCreateRequest) if err != nil { return fmt.Errorf("Unable to create cluster: %w", err) } + log.Info("Cluster created successfully") 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) } - nodeID := strings.TrimSuffix(string(output), "\n") - - time.Sleep(constants.WaitPeriod * time.Second) - log.Info("Attaching node to the cluster...") + nodeID := strings.TrimSuffix(output, "\n") err = c.Qbert.AttachNode( clusterID, nodeID, diff --git a/pkg/pmk/common.go b/pkg/pmk/common.go index 8cf5f3cf..e3d05728 100644 --- a/pkg/pmk/common.go +++ b/pkg/pmk/common.go @@ -9,37 +9,26 @@ import ( "github.com/platform9/pf9ctl/pkg/log" ) -func setupNode(hostOS string) (err error) { +func setupNode(host Host) (err error) { log.Debug("Received a call to setup the node") - if err := swapOff(); err != nil { - return err + if err := host.SwapOff(); err != nil { + return fmt.Errorf("Unable to disable swap: %w", 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() + if err := host.Setup(); err != nil { + return fmt.Errorf("Unable to setup the host: %w", err) + } - default: - err = fmt.Errorf("Invalid Host: %s", hostOS) + if err := host.EnableNTP(); err != nil { + return fmt.Errorf("Unable to enable NTP for host: %w", err) } - return + return nil } func handlePF9UserGroup() error { diff --git a/pkg/pmk/context.go b/pkg/pmk/context.go index c5cdbeda..dd5b09b9 100644 --- a/pkg/pmk/context.go +++ b/pkg/pmk/context.go @@ -15,6 +15,7 @@ type Context struct { Password string `json:"os_password"` Tenant string `json:"os_tenant"` Region string `json:"os_region"` + Proxy string `json:"proxy"` } // StoreContext simply updates the in-memory object diff --git a/pkg/pmk/host.go b/pkg/pmk/host.go new file mode 100644 index 00000000..4a90b093 --- /dev/null +++ b/pkg/pmk/host.go @@ -0,0 +1,114 @@ +package pmk + +import ( + "fmt" + + "github.com/platform9/pf9ctl/pkg/pmk/clients" +) + +// Host interface exposes the functions +// required to setup the host correctly. +type Host interface { + Setup() error + InstallPackage(...string) error + EnableNTP() error + SwapOff() error + String() string + PackagePresent(name string) bool +} + +//Redhat host encapsulates menthods +//required to bootup the redhat host. +type Redhat struct { + exec clients.Executor +} + +func (h Redhat) String() string { + return "redhat" +} + +// PackagePresent checks for the package in the Redhat host. +func (h Redhat) PackagePresent(name string) bool { + err := h.exec.Run("bash", "-c", fmt.Sprintf("yum list | grep -i '%s'", name)) + return err == nil +} + +// Setup installes the necessary packages for the +// host to start functioning correctly. +func (h Redhat) Setup() error { + return h.InstallPackage("libselinux-python") +} + +// InstallPackage installs packages provided in the arguments +// for the Redhat host. +func (h Redhat) InstallPackage(names ...string) error { + packages := "" + for _, name := range names { + packages += " " + name + } + return h.exec.Run( + "bash", + "-c", + fmt.Sprintf("yum install -y %s", packages)) +} + +//EnableNTP enables the NTP service for the Redhat host. +func (h Redhat) EnableNTP() error { + err := h.InstallPackage("ntpd") + err = h.exec.Run("bash", "-c", "systemctl enable --now ntpd") + return err +} + +//SwapOff disables the swap for the Redhat host. +func (h Redhat) SwapOff() error { + return h.exec.Run("bash", "-c", "swapoff -a") +} + +type Debian struct { + exec clients.Executor +} + +//Setup sets the host up with required packages for +//the preping the node up. +func (h Debian) Setup() error { + err := h.exec.Run("bash", "-c", "apt-get update") + err = h.InstallPackage("curl", "uuid-runtime", "software-properties-common", "logrotate") + return err +} + +// InstallPackage installs packages provided in the arguments +// for the DebianHost +func (h Debian) InstallPackage(names ...string) error { + + packages := "" + for _, p := range names { + packages += " " + p + } + + return h.exec.Run( + "bash", + "-c", + fmt.Sprintf("apt-get install -y %s", packages)) +} + +// EnableNTP service for the DebianHost +func (h Debian) EnableNTP() error { + err := h.InstallPackage("ntp") + err = h.exec.Run("bash", "-c", "systemctl enable --now ntp") + return err +} + +// SwapOff disables the swap. +func (h Debian) SwapOff() error { + return h.exec.Run("bash", "-c", "swapoff -a") +} + +func (h Debian) String() string { + return "debian" +} + +//PackagePresent checks if the package present on the host. +func (h Debian) PackagePresent(name string) bool { + err := h.exec.Run("bash", "-c", fmt.Sprintf("dpkg -l | grep '%s'", name)) + return err == nil +} diff --git a/pkg/pmk/node.go b/pkg/pmk/node.go index 31f85f59..7efab51b 100644 --- a/pkg/pmk/node.go +++ b/pkg/pmk/node.go @@ -3,16 +3,16 @@ package pmk import ( "encoding/base64" "fmt" + "io" "io/ioutil" "net/http" - "os/exec" + "os" "runtime" "strings" - "time" - "github.com/platform9/pf9ctl/pkg/constants" "github.com/platform9/pf9ctl/pkg/log" "github.com/platform9/pf9ctl/pkg/pmk/clients" + "github.com/platform9/pf9ctl/pkg/util" ) // PrepNode sets up prerequisites for k8s stack @@ -26,17 +26,17 @@ func PrepNode( log.Debug("Received a call to start preping node(s).") - hostOS, err := validatePlatform() + host, err := getHost("/etc/os-release", c.Executor) if err != nil { return fmt.Errorf("Invalid host os: %s", err.Error()) } - present := pf9PackagesPresent(hostOS, c.Executor) + present := host.PackagePresent("pf9-") 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(host) if err != nil { return fmt.Errorf("Unable to setup node: %s", err.Error()) } @@ -51,7 +51,7 @@ 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, c, auth, host.String()); err != nil { return fmt.Errorf("Unable to install hostagent: %w", err) } @@ -64,7 +64,6 @@ func PrepNode( } hostID := strings.TrimSuffix(output, "\n") - time.Sleep(constants.WaitPeriod * time.Second) if err := c.Resmgr.AuthorizeHost(hostID, auth.Token); err != nil { return err @@ -77,46 +76,51 @@ func PrepNode( return nil } -func installHostAgent(ctx Context, auth clients.KeystoneAuth, hostOS string) error { - log.Debug("Downloading Hostagent") +func installHostAgent( + ctx Context, c clients.Client, auth clients.KeystoneAuth, host string) error { + log.Info("Download Hostagent") - url := fmt.Sprintf("%s/clarity/platform9-install-%s.sh", ctx.Fqdn, hostOS) + url := fmt.Sprintf("%s/clarity/platform9-install-%s.sh", ctx.Fqdn, host) req, err := http.NewRequest("GET", url, nil) if err != nil { return fmt.Errorf("Unable to create a http request: %w", err) } - client := http.Client{} - resp, err := client.Do(req) + resp, err := c.HTTP.Do(req) if err != nil { - return fmt.Errorf("Unable to send a request to clientL %w", err) + return fmt.Errorf("Unable to send a request to client %w", err) } switch resp.StatusCode { case 404: - return installHostAgentLegacy(ctx, auth, hostOS) + return installHostAgentLegacy( + ctx, auth, c.HTTP, c.Executor, host) case 200: - return installHostAgentCertless(ctx, auth, hostOS) + return installHostAgentCertless( + ctx, auth, c.HTTP, c.Executor, host) 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, httpClient clients.HTTP, exec clients.Executor, hostOS string) error { log.Info("Downloading Hostagent Installer Certless") url := fmt.Sprintf( "%s/clarity/platform9-install-%s.sh", ctx.Fqdn, hostOS) - cmd := fmt.Sprintf(`curl --silent --show-error %s -o /tmp/installer.sh`, url) - _, err := exec.Command("bash", "-c", cmd).Output() + req, err := http.NewRequest("GET", url, nil) if err != nil { - return err + return fmt.Errorf("Unable to create request: %w", err) + } + + if err := downloadFile(httpClient, req, "/tmp/installer.sh"); err != nil { + return fmt.Errorf("Unable to download hostagent installer: %w", err) } - log.Debug("Hostagent download completed successfully") - // Decoding base64 encoded password + log.Info("Hostagent download completed successfully") decodedBytePassword, err := base64.StdEncoding.DecodeString(ctx.Password) if err != nil { return err @@ -124,13 +128,17 @@ 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.Run("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() + cmd := fmt.Sprintf(`/tmp/installer.sh --no-proxy --skip-os-check --ntpd %s`, installOptions) + if ctx.Proxy != "" { + cmd = fmt.Sprintf(`/tmp/installer.sh --proxy=%s --skip-os-check --ntpd %s`, ctx.Proxy, installOptions) + } + + err = exec.Run("bash", "-c", cmd) if err != nil { return err } @@ -140,91 +148,117 @@ func installHostAgentCertless(ctx Context, auth clients.KeystoneAuth, hostOS str return nil } -func validatePlatform() (string, error) { +func getHost(hostReleaseLoc string, exec clients.Executor) (Host, error) { log.Debug("Received a call to validate platform") OS := runtime.GOOS if OS != "linux" { - return "", fmt.Errorf("Unsupported OS: %s", OS) + return nil, fmt.Errorf("Unsupported OS: %s", OS) } - data, err := ioutil.ReadFile("/etc/os-release") + data, err := ioutil.ReadFile(hostReleaseLoc) if err != nil { - return "", fmt.Errorf("failed reading data from file: %s", err) + return nil, fmt.Errorf("failed reading data from file: %w", err) } - strDataLower := strings.ToLower(string(data)) + release := strings.ToLower(string(data)) switch { - case strings.Contains(strDataLower, "centos") || strings.Contains(strDataLower, "redhat"): - out, err := exec.Command( + case strings.Contains(release, "centos") || strings.Contains(release, "redhat"): + 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()) + return nil, fmt.Errorf("Couldn't read the OS configuration file os-release: %w", err) } - if strings.Contains(string(out), "7.5") || strings.Contains(string(out), "7.6") || strings.Contains(string(out), "7.7") || strings.Contains(string(out), "7.8") { - return "redhat", nil + + if util.StringContainsAny(out, []string{"7.5", "7.6", "7.7", "7.8"}) { + return Redhat{exec}, nil } + return nil, fmt.Errorf("Only %s versions of centos are supported", "7.5 7.6 7.7 7.8") - case strings.Contains(strDataLower, "ubuntu"): - out, err := exec.Command( + case strings.Contains(release, "ubuntu"): + 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()) + return nil, fmt.Errorf("Couldn't read the OS configuration file os-release: %s", err.Error()) } - if strings.Contains(string(out), "16") || strings.Contains(string(out), "18") { - return "debian", nil - } - } - return "", nil -} + if util.StringContainsAny(out, []string{"16", "18"}) { + return Debian{exec: exec}, nil + } + return nil, fmt.Errorf("Only %s versions of ubuntu are supported", "16 18") -func pf9PackagesPresent(hostOS string, exec clients.Executor) bool { - var err error - if hostOS == "debian" { - err = exec.Run("bash", - "-c", - "dpkg -l | grep -i 'pf9-'") - } else { - // not checking for redhat because if it has already passed validation - // it must be either debian or redhat based - err = exec.Run("bash", - "-c", - "yum list | grep -i 'pf9-'") + default: + return nil, fmt.Errorf("Invalid release: %s", release) } - - return err == nil } -func installHostAgentLegacy(ctx Context, auth clients.KeystoneAuth, hostOS string) error { +func installHostAgentLegacy( + ctx Context, + auth clients.KeystoneAuth, + httpClient clients.HTTP, + exec clients.Executor, + hostOS string) 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() + req, err := http.NewRequest("GET", url, nil) if err != nil { - return err + return fmt.Errorf("Unable to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Auth-Token", auth.Token) + + if err := downloadFile(httpClient, req, "/tmp/installer.sh"); err != nil { + return fmt.Errorf("Unable to download hostagent installer: %w", err) } log.Debug("Hostagent download completed successfully") - _, err = exec.Command("bash", "-c", "chmod +x /tmp/installer.sh").Output() + err = exec.Run("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() + installOptions := fmt.Sprintf("--insecure --project-name=%s 2>&1 | tee -a /tmp/agent_install.log", auth.ProjectID) + + cmd := fmt.Sprintf(`/tmp/installer.sh --no-proxy --skip-os-check --ntpd %s`, installOptions) + if ctx.Proxy != "" { + cmd = fmt.Sprintf(`/tmp/installer.sh --proxy=%s --skip-os-check --ntpd %s`, ctx.Proxy, installOptions) + } + + err = exec.Run("bash", "-c", cmd) if err != nil { return err } - // TODO: here we actually need additional validation by checking /tmp/agent_install. log log.Info("Hostagent installed successfully") return nil } + +func downloadFile(client clients.HTTP, req *http.Request, loc string) error { + + f, err := os.Create(loc) + if err != nil { + return fmt.Errorf("Unable to create a file for download: %w", err) + } + + defer f.Close() + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Unable to make a request to client: %w", err) + } + + defer resp.Body.Close() + + _, err = io.Copy(f, resp.Body) + if err != nil { + return fmt.Errorf("Unable to copy the body into file: %w", err) + } + + return nil +} diff --git a/pkg/util/encode_test.go b/pkg/util/encode_test.go new file mode 100644 index 00000000..3e10779d --- /dev/null +++ b/pkg/util/encode_test.go @@ -0,0 +1,25 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncode(t *testing.T) { + ip := "Hello World" + + expect := "SGVsbG8gV29ybGQ=" + actual := EncodeString(ip) + + assert.Equal(t, expect, actual) +} + +func TestDecode(t *testing.T) { + + encoded := "SGVsbG8gV29ybGQ=" + decoded := "Hello World" + + actual := DecodeString(encoded) + assert.Equal(t, decoded, actual) +} diff --git a/pkg/util/helper.go b/pkg/util/helper.go index 3ab9107d..15174047 100644 --- a/pkg/util/helper.go +++ b/pkg/util/helper.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "regexp" + "strings" ) var ( @@ -98,3 +99,14 @@ func AskBool(msg string, args ...interface{}) (bool, error) { return false, fmt.Errorf("Please provide input as y or n, provided: %s", resp) } + +func StringContainsAny(base string, contains []string) bool { + + for _, contain := range contains { + if strings.Contains(base, contain) { + return true + } + } + + return false +} diff --git a/pkg/util/helper_test.go b/pkg/util/helper_test.go new file mode 100644 index 00000000..4083b402 --- /dev/null +++ b/pkg/util/helper_test.go @@ -0,0 +1,19 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStringContainsAny(t *testing.T) { + + base := "Ubuntu16" + + actual := StringContainsAny(base, []string{"16", "18"}) + assert.Equal(t, true, actual) + + actual = StringContainsAny(base, []string{"7.5"}) + assert.Equal(t, false, actual) + +} diff --git a/pkg/util/keystone/keystone.go b/pkg/util/keystone/keystone.go deleted file mode 100644 index 2fb6fb5d..00000000 --- a/pkg/util/keystone/keystone.go +++ /dev/null @@ -1 +0,0 @@ -package keystone