diff --git a/docs/drivers/openstack.md b/docs/drivers/openstack.md index 25d82d2..3eb89ee 100644 --- a/docs/drivers/openstack.md +++ b/docs/drivers/openstack.md @@ -28,10 +28,12 @@ Options: - `--openstack-floatingip-pool`: The IP pool that will be used to get a public IP can assign it to the machine. If there is an IP address already allocated but not assigned to any machine, this IP will be chosen and assigned to the machine. If there is no IP address already allocated a new IP will be allocated and assigned to the machine. +- `--openstack-keypair-name`: Specify the existing Nova keypair to use. - `--openstack-insecure`: Explicitly allow openstack driver to perform "insecure" SSL (https) requests. The server's certificate will not be verified against any certificate authorities. This option should be used with caution. - `--openstack-ip-version`: If the instance has both IPv4 and IPv6 address, you can select IP version. If not provided `4` will be used. - `--openstack-net-name` or `--openstack-net-id`: Identify the private network the machine will be connected on. If your OpenStack project project contains only one private network it will be use automatically. - `--openstack-password`: User password. It can be omitted if the standard environment variable `OS_PASSWORD` is set. +- `--openstack-private-key-file`: Used with `--openstack-keypair-name`, associates the private key to the keypair. - `--openstack-region`: The region to work on. Can be omitted if there is only one region on the OpenStack. - `--openstack-sec-groups`: If security groups are available on your OpenStack you can specify a comma separated list to use for the machine (e.g. `secgrp001,secgrp002`). @@ -57,9 +59,11 @@ Environment variables and default values: | `--openstack-image-name` | `OS_IMAGE_NAME` | - | | `--openstack-insecure` | `OS_INSECURE` | `false` | | `--openstack-ip-version` | `OS_IP_VERSION` | `4` | +| `--openstack-keypair-name` | `OS_KEYPAIR_NAME` | - | | `--openstack-net-id` | `OS_NETWORK_ID` | - | | `--openstack-net-name` | `OS_NETWORK_NAME` | - | | `--openstack-password` | `OS_PASSWORD` | - | +| `--openstack-private-key-file` | `OS_PRIVATE_KEY_FILE` | - | | `--openstack-region` | `OS_REGION_NAME` | - | | `--openstack-sec-groups` | `OS_SECURITY_GROUPS` | - | | `--openstack-ssh-port` | `OS_SSH_PORT` | `22` | diff --git a/drivers/openstack/client.go b/drivers/openstack/client.go index 1019fb0..791a66f 100644 --- a/drivers/openstack/client.go +++ b/drivers/openstack/client.go @@ -38,6 +38,7 @@ type Client interface { DeleteInstance(d *Driver) error WaitForInstanceStatus(d *Driver, status string) error GetInstanceIPAddresses(d *Driver) ([]IPAddress, error) + GetPublicKey(keyPairName string) ([]byte, error) CreateKeyPair(d *Driver, name string, publicKey string) error DeleteKeyPair(d *Driver, name string) error GetNetworkID(d *Driver) (string, error) @@ -300,6 +301,14 @@ func (c *GenericClient) GetTenantID(d *Driver) (string, error) { return tenantId, err } +func (c *GenericClient) GetPublicKey(keyPairName string) ([]byte, error) { + kp, err := keypairs.Get(c.Compute, keyPairName).Extract() + if err != nil { + return nil, err + } + return []byte(kp.PublicKey), nil +} + func (c *GenericClient) CreateKeyPair(d *Driver, name string, publicKey string) error { opts := keypairs.CreateOpts{ Name: name, diff --git a/drivers/openstack/openstack.go b/drivers/openstack/openstack.go index 100ba62..d2dd0d5 100644 --- a/drivers/openstack/openstack.go +++ b/drivers/openstack/openstack.go @@ -37,6 +37,7 @@ type Driver struct { KeyPairName string NetworkName string NetworkId string + PrivateKeyFile string SecurityGroups []string FloatingIpPool string ComputeNetwork bool @@ -142,12 +143,24 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag { Usage: "OpenStack image name to use for the instance", Value: "", }, + mcnflag.StringFlag{ + EnvVar: "OS_KEYPAIR_NAME", + Name: "openstack-keypair-name", + Usage: "OpenStack keypair to use to SSH to the instance", + Value: "", + }, mcnflag.StringFlag{ EnvVar: "OS_NETWORK_ID", Name: "openstack-net-id", Usage: "OpenStack network id the machine will be connected on", Value: "", }, + mcnflag.StringFlag{ + EnvVar: "OS_PRIVATE_KEY_FILE", + Name: "openstack-private-key-file", + Usage: "Private keyfile to use for SSH (absolute path)", + Value: "", + }, mcnflag.StringFlag{ EnvVar: "OS_NETWORK_NAME", Name: "openstack-net-name", @@ -255,6 +268,8 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { d.ComputeNetwork = flags.Bool("openstack-nova-network") d.SSHUser = flags.String("openstack-ssh-user") d.SSHPort = flags.Int("openstack-ssh-port") + d.KeyPairName = flags.String("openstack-keypair-name") + d.PrivateKeyFile = flags.String("openstack-private-key-file") d.SwarmMaster = flags.Bool("swarm-master") d.SwarmHost = flags.String("swarm-host") d.SwarmDiscovery = flags.String("swarm-discovery") @@ -339,13 +354,18 @@ func (d *Driver) GetState() (state.State, error) { } func (d *Driver) Create() error { - d.KeyPairName = fmt.Sprintf("%s-%s", d.MachineName, mcnutils.GenerateRandomID()) - if err := d.resolveIds(); err != nil { return err } - if err := d.createSSHKey(); err != nil { - return err + if d.KeyPairName != "" { + if err := d.loadSSHKey(); err != nil { + return err + } + } else { + d.KeyPairName = fmt.Sprintf("%s-%s", d.MachineName, mcnutils.GenerateRandomID()) + if err := d.createSSHKey(); err != nil { + return err + } } if err := d.createMachine(); err != nil { return err @@ -397,6 +417,7 @@ func (d *Driver) Remove() error { return err } log.Debug("deleting key pair...", map[string]string{"Name": d.KeyPairName}) + // TODO (fsoppelsa) maybe we want to check this, in case of shared keypairs, before removal if err := d.client.DeleteKeyPair(d, d.KeyPairName); err != nil { return err } @@ -422,6 +443,7 @@ const ( errorMandatoryEnvOrOption string = "%s must be specified either using the environment variable %s or the CLI option %s" errorMandatoryOption string = "%s must be specified using the CLI option %s" errorExclusiveOptions string = "Either %s or %s must be specified, not both" + errorBothOptions string = "Both %s and %s must be specified" errorMandatoryTenantNameOrID string = "Tenant id or name must be provided either using one of the environment variables OS_TENANT_ID and OS_TENANT_NAME or one of the CLI options --openstack-tenant-id and --openstack-tenant-name" errorWrongEndpointType string = "Endpoint type must be 'publicURL', 'adminURL' or 'internalURL'" errorUnknownFlavorName string = "Unable to find flavor named %s" @@ -464,6 +486,9 @@ func (d *Driver) checkConfig() error { if d.EndpointType != "" && (d.EndpointType != "publicURL" && d.EndpointType != "adminURL" && d.EndpointType != "internalURL") { return fmt.Errorf(errorWrongEndpointType) } + if (d.KeyPairName != "" && d.PrivateKeyFile == "") || (d.KeyPairName == "" && d.PrivateKeyFile != "") { + return fmt.Errorf(errorBothOptions, "KeyPairName", "PrivateKeyFile") + } return nil } @@ -607,6 +632,30 @@ func (d *Driver) initNetwork() error { return nil } +func (d *Driver) loadSSHKey() error { + log.Debug("Loading Key Pair", d.KeyPairName) + if err := d.initCompute(); err != nil { + return err + } + log.Debug("Loading Private Key from", d.PrivateKeyFile) + privateKey, err := ioutil.ReadFile(d.PrivateKeyFile) + if err != nil { + return err + } + publicKey, err := d.client.GetPublicKey(d.KeyPairName) + if err != nil { + return err + } + if err := ioutil.WriteFile(d.privateSSHKeyPath(), privateKey, 0600); err != nil { + return err + } + if err := ioutil.WriteFile(d.publicSSHKeyPath(), publicKey, 0600); err != nil { + return err + } + + return nil +} + func (d *Driver) createSSHKey() error { sanitizeKeyPairName(&d.KeyPairName) log.Debug("Creating Key Pair...", map[string]string{"Name": d.KeyPairName}) @@ -715,6 +764,10 @@ func (d *Driver) lookForIPAddress() error { return nil } +func (d *Driver) privateSSHKeyPath() string { + return d.GetSSHKeyPath() +} + func (d *Driver) publicSSHKeyPath() string { return d.GetSSHKeyPath() + ".pub" }