diff --git a/README.md b/README.md index bdc05dea12..83623c14e0 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,53 @@ Options: - `--vmwarevsphere-pool`: Resource pool for Docker VM. - `--vmwarevsphere-vcenter`: IP/hostname for vCenter (or ESXi if connecting directly to a single host). +### OpenStack + +Create machines on [Openstack](http://www.openstack.org/software/) + +#### Options + +Mandatory + + - `--openstack-flavor-id`: The flavor ID to use when creating the machine + - `--openstack-image-id`: The image ID to use when creating the machine. At the moment, the driver does not install + docker on the machine. That means the image you use should already contains a ready to use docker installation + +Optional + + - `--openstack-auth-url`: Keystone service base URL + - `--openstack-username`: User identifer to authenticate with. + - `--openstack-password`: User password. It can be omitted if the standard environment variable `OS_PASSWORD` is set + - `--openstack-tenant-name` or `--openstack-tenant-id`: Identify the tenant in which the machine will be created. + - `--openstack-region`: The region to work on. Can be omitted if there is ony one region on the OpenStack + - `--openstack-endpoint-type`: Endpoint type can be `internalURL`, `adminURL` on `publicURL`. If is a helper for the driver + to choose the right URL in the OpenStack service catalog. If not provided the default id `publicURL` + - `--openstack-net-id`: The private network id the machine will be connected on. If your OpenStack project project + contains only one private network it will be use automatically + - `--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`) + - `--openstack-floatingip-pool`: The IP pool that will be used to get an IP an 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 our machine. If + there is no IP address already allocated, an new IP will be allocated and assigned to the machine + - `--openstack-ssh-user`: The username to use for SSH into the machine. If not provided `root` will be used. + - `--openstack-ssh-port`: Customize the SSH port if the SSH server on the machine does not listen on the default port + +#### Environment variables + +Some options can be omitted if the corresponding standard OpenStack environment variable is set. Here +comes the list of the supported variables with the corresponding options. If both environment variable +and CLI option are provided the CLI option takes the precedence. + +| Environment variable | CLI option | +|----------------------|-----------------------------| +| `OS_AUTH_URL` | `--openstack-auth-url` | +| `OS_USERNAME` | `--openstack-username` | +| `OS_PASSWORD` | `--openstack-password` | +| `OS_TENANT_NAME` | `--openstack-tenant-name` | +| `OS_TENANT_ID` | `--openstack-tenant-id` | +| `OS_REGION_NAME` | `--openstack-region` | +| `OS_ENDPOINT_TYPE` | `--openstack-endpoint-type` | + ## Contributing [![GoDoc](https://godoc.org/github.com/docker/machine?status.png)](https://godoc.org/github.com/docker/machine) diff --git a/commands.go b/commands.go index 33a7efd78d..451f99b4fa 100644 --- a/commands.go +++ b/commands.go @@ -18,6 +18,7 @@ import ( _ "github.com/docker/machine/drivers/digitalocean" _ "github.com/docker/machine/drivers/google" _ "github.com/docker/machine/drivers/none" + _ "github.com/docker/machine/drivers/openstack" _ "github.com/docker/machine/drivers/virtualbox" _ "github.com/docker/machine/drivers/vmwarefusion" _ "github.com/docker/machine/drivers/vmwarevcloudair" diff --git a/drivers/openstack/client.go b/drivers/openstack/client.go new file mode 100644 index 0000000000..f9960af3fd --- /dev/null +++ b/drivers/openstack/client.go @@ -0,0 +1,297 @@ +package openstack + +import ( + log "github.com/Sirupsen/logrus" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/startstop" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" +) + +type Client struct { + Provider *gophercloud.ProviderClient + Compute *gophercloud.ServiceClient + Network *gophercloud.ServiceClient +} + +func (c *Client) CreateInstance(d *Driver) (string, error) { + if err := c.initComputeClient(d); err != nil { + return "", err + } + serverOpts := servers.CreateOpts{ + Name: d.MachineName, + FlavorRef: d.FlavorId, + ImageRef: d.ImageId, + SecurityGroups: d.SecurityGroups, + } + if d.NetworkId != "" { + serverOpts.Networks = []servers.Network{ + { + UUID: d.NetworkId, + }, + } + } + server, err := servers.Create(c.Compute, keypairs.CreateOptsExt{ + serverOpts, + d.KeyPairName, + }).Extract() + if err != nil { + return "", err + } + return server.ID, nil +} + +const ( + Floating string = "floating" + Fixed string = "fixed" +) + +type IpAddress struct { + Network string + AddressType string + Address string + Mac string +} + +func (c *Client) GetInstanceState(d *Driver) (string, error) { + server, err := c.getServerDetail(d) + if err != nil { + return "", err + } + + c.getFloatingIPs(d) + + c.getPorts(d) + + return server.Status, nil +} + +func (c *Client) StartInstance(d *Driver) error { + if err := c.initComputeClient(d); err != nil { + return err + } + if result := startstop.Start(c.Compute, d.MachineId); result.Err != nil { + return result.Err + } + return nil +} + +func (c *Client) StopInstance(d *Driver) error { + if err := c.initComputeClient(d); err != nil { + return err + } + if result := startstop.Stop(c.Compute, d.MachineId); result.Err != nil { + return result.Err + } + return nil +} + +func (c *Client) RestartInstance(d *Driver) error { + if err := c.initComputeClient(d); err != nil { + return err + } + if result := servers.Reboot(c.Compute, d.MachineId, servers.SoftReboot); result.Err != nil { + return result.Err + } + return nil +} + +func (c *Client) DeleteInstance(d *Driver) error { + if err := c.initComputeClient(d); err != nil { + return err + } + if result := servers.Delete(c.Compute, d.MachineId); result.Err != nil { + return result.Err + } + return nil +} + +func (c *Client) WaitForInstanceStatus(d *Driver, status string, timeout int) error { + if err := servers.WaitForStatus(c.Compute, d.MachineId, status, timeout); err != nil { + return err + } + return nil +} + +func (c *Client) GetInstanceIpAddresses(d *Driver) ([]IpAddress, error) { + server, err := c.getServerDetail(d) + if err != nil { + return nil, err + } + addresses := []IpAddress{} + for network, networkAddresses := range server.Addresses { + for _, element := range networkAddresses.([]interface{}) { + address := element.(map[string]interface{}) + addresses = append(addresses, IpAddress{ + Network: network, + AddressType: address["OS-EXT-IPS:type"].(string), + Address: address["addr"].(string), + Mac: address["OS-EXT-IPS-MAC:mac_addr"].(string), + }) + } + } + return addresses, nil +} + +func (c *Client) CreateKeyPair(d *Driver, name string, publicKey string) error { + if err := c.initComputeClient(d); err != nil { + return err + } + opts := keypairs.CreateOpts{ + Name: name, + PublicKey: publicKey, + } + if result := keypairs.Create(c.Compute, opts); result.Err != nil { + return result.Err + } + return nil +} + +func (c *Client) DeleteKeyPair(d *Driver, name string) error { + if err := c.initComputeClient(d); err != nil { + return err + } + if result := keypairs.Delete(c.Compute, name); result.Err != nil { + return result.Err + } + return nil +} + +func (c *Client) getServerDetail(d *Driver) (*servers.Server, error) { + if err := c.initComputeClient(d); err != nil { + return nil, err + } + server, err := servers.Get(c.Compute, d.MachineId).Extract() + if err != nil { + return nil, err + } + return server, nil +} + +func (c *Client) getFloatingIPs(d *Driver) ([]string, error) { + + if err := c.initNetworkClient(d); err != nil { + return nil, err + } + + pager := floatingips.List(c.Network, floatingips.ListOpts{}) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + floatingipList, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + return false, err + } + for _, f := range floatingipList { + log.Info("### FloatingIP => %s", f) + } + return true, nil + }) + + if err != nil { + return nil, err + } + return nil, nil +} + +func (c *Client) getPorts(d *Driver) ([]string, error) { + + if err := c.initNetworkClient(d); err != nil { + return nil, err + } + + pager := ports.List(c.Network, ports.ListOpts{ + DeviceID: d.MachineId, + }) + + err := pager.EachPage(func(page pagination.Page) (bool, error) { + portList, err := ports.ExtractPorts(page) + if err != nil { + return false, err + } + for _, port := range portList { + log.Info("### Port => %s", port) + } + return true, nil + }) + + if err != nil { + return nil, err + } + return nil, nil +} + +func (c *Client) initComputeClient(d *Driver) error { + if c.Provider == nil { + err := c.Authenticate(d) + if err != nil { + return err + } + } + compute, err := openstack.NewComputeV2(c.Provider, gophercloud.EndpointOpts{ + Region: d.Region, + Availability: c.getEndpointType(d), + }) + if err != nil { + return err + } + c.Compute = compute + return nil +} + +func (c *Client) initNetworkClient(d *Driver) error { + if c.Provider == nil { + err := c.Authenticate(d) + if err != nil { + return err + } + } + network, err := openstack.NewNetworkV2(c.Provider, gophercloud.EndpointOpts{ + Region: d.Region, + Availability: c.getEndpointType(d), + }) + if err != nil { + return err + } + c.Network = network + return nil +} + +func (c *Client) getEndpointType(d *Driver) gophercloud.Availability { + switch d.EndpointType { + case "internalURL": + return gophercloud.AvailabilityInternal + case "adminURL": + return gophercloud.AvailabilityAdmin + } + return gophercloud.AvailabilityPublic +} + +func (c *Client) Authenticate(d *Driver) error { + log.WithFields(log.Fields{ + "AuthUrl": d.AuthUrl, + "Username": d.Username, + "TenantName": d.TenantName, + "TenantID": d.TenantId, + }).Info("Authenticating...") + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: d.AuthUrl, + Username: d.Username, + Password: d.Password, + TenantName: d.TenantName, + TenantID: d.TenantId, + AllowReauth: true, + } + + provider, err := openstack.AuthenticatedClient(opts) + if err != nil { + return err + } + c.Provider = provider + + return nil +} diff --git a/drivers/openstack/openstack.go b/drivers/openstack/openstack.go new file mode 100644 index 0000000000..5ea7cc99b0 --- /dev/null +++ b/drivers/openstack/openstack.go @@ -0,0 +1,450 @@ +package openstack + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "strings" + + log "github.com/Sirupsen/logrus" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/utils" + "github.com/docker/machine/drivers" + "github.com/docker/machine/ssh" + "github.com/docker/machine/state" +) + +type Driver struct { + AuthUrl string + Username string + Password string + TenantName string + TenantId string + Region string + EndpointType string + MachineName string + MachineId string + FlavorId string + ImageId string + KeyPairName string + NetworkId string + SecurityGroups []string + FloatingIpPool string + SSHUser string + SSHPort int + storePath string + client *Client +} + +type CreateFlags struct { + AuthUrl *string + Username *string + Password *string + TenantName *string + TenantId *string + Region *string + EndpointType *string + FlavorId *string + ImageId *string + NetworkId *string + SecurityGroups *string + FloatingIpPool *string + SSHUser *string + SSHPort *int +} + +func init() { + drivers.Register("openstack", &drivers.RegisteredDriver{ + New: NewDriver, + RegisterCreateFlags: RegisterCreateFlags, + }) +} + +func RegisterCreateFlags(cmd *flag.FlagSet) interface{} { + createFlags := new(CreateFlags) + createFlags.AuthUrl = cmd.String( + []string{"-openstack-auth-url"}, + os.Getenv("OS_AUTH_URL"), + "OpenStack authentication URL", + ) + createFlags.Username = cmd.String( + []string{"-openstack-username"}, + os.Getenv("OS_USERNAME"), + "OpenStack username", + ) + createFlags.Password = cmd.String( + []string{"-openstack-password"}, + os.Getenv("OS_PASSWORD"), + "OpenStack password", + ) + createFlags.TenantName = cmd.String( + []string{"-openstack-tenant-name"}, + os.Getenv("OS_TENANT_NAME"), + "OpenStack tenant name", + ) + createFlags.TenantId = cmd.String( + []string{"-openstack-tenant-id"}, + os.Getenv("OS_TENANT_ID"), + "OpenStack tenant id", + ) + createFlags.Region = cmd.String( + []string{"-openstack-region"}, + os.Getenv("OS_REGION_NAME"), + "OpenStack region name", + ) + createFlags.EndpointType = cmd.String( + []string{"-openstack-endpoint-type"}, + os.Getenv("OS_ENDPOINT_TYPE"), + "OpenStack endpoint type (adminURL, internalURL or the default publicURL)", + ) + createFlags.FlavorId = cmd.String( + []string{"-openstack-flavor-id"}, + "", + "OpenStack flavor id to use for the instance", + ) + createFlags.ImageId = cmd.String( + []string{"-openstack-image-id"}, + "", + "OpenStack image id to use for the instance", + ) + createFlags.NetworkId = cmd.String( + []string{"-openstack-net-id"}, + "", + "OpenStack network id the machine will be connected on", + ) + createFlags.SecurityGroups = cmd.String( + []string{"-openstack-sec-groups"}, + "", + "OpenStack comma separated security groups for the machine", + ) + createFlags.FloatingIpPool = cmd.String( + []string{"-openstack-floatingip-pool"}, + "", + "OpenStack floating IP pool to get an IP from to assign to the instance", + ) + createFlags.SSHUser = cmd.String( + []string{"-openstack-ssh-user"}, + "root", + "OpenStack SSH user. Set to root by default", + ) + createFlags.SSHPort = cmd.Int( + []string{"-openstack-ssh-port"}, + 22, + "OpenStack SSH port. Set to 22 by default", + ) + return createFlags +} + +func NewDriver(storePath string) (drivers.Driver, error) { + log.WithFields(log.Fields{ + "storePath": storePath, + }).Info("Instanciate OpenStack driver...") + return &Driver{ + storePath: storePath, + client: &Client{}, + }, nil +} + +func (d *Driver) DriverName() string { + return "openstack" +} + +func (d *Driver) SetConfigFromFlags(flagsInterface interface{}) error { + flags := flagsInterface.(*CreateFlags) + d.AuthUrl = *flags.AuthUrl + d.Username = *flags.Username + d.Password = *flags.Password + d.TenantName = *flags.TenantName + d.TenantId = *flags.TenantId + d.Region = *flags.Region + d.EndpointType = *flags.EndpointType + d.FlavorId = *flags.FlavorId + d.ImageId = *flags.ImageId + d.NetworkId = *flags.NetworkId + if *flags.SecurityGroups != "" { + d.SecurityGroups = strings.Split(*flags.SecurityGroups, ",") + } + d.FloatingIpPool = *flags.FloatingIpPool + d.SSHUser = *flags.SSHUser + d.SSHPort = *flags.SSHPort + return d.checkConfig() +} + +func (d *Driver) GetURL() (string, error) { + ip, err := d.GetIP() + if err != nil { + return "", err + } + if ip == "" { + return "", nil + } + return fmt.Sprintf("tcp://%s:2376", ip), nil +} + +func (d *Driver) GetIP() (string, error) { + addresses, err := d.client.GetInstanceIpAddresses(d) + if err != nil { + return "", err + } + + floating := []string{} + fixed := []string{} + + for _, address := range addresses { + if address.AddressType == Floating { + floating = append(floating, address.Address) + continue + } + if address.AddressType == Fixed { + fixed = append(fixed, address.Address) + continue + } + log.Warnf("Unknown IP address type : %s", address) + } + + if len(floating) == 1 { + return d.foundIP(floating[0]), nil + } else if len(floating) > 1 { + log.Warnf("Multiple floating IP found. Take the first one of %s", floating) + return d.foundIP(floating[0]), nil + } + + if len(fixed) == 1 { + return d.foundIP(fixed[0]), nil + } else if len(fixed) > 1 { + log.Warnf("Multiple fixed IP found. Take the first one of %s", floating) + return d.foundIP(fixed[0]), nil + } + + return "", fmt.Errorf("No IP found for the machine") +} + +func (d *Driver) GetState() (state.State, error) { + + log.WithField("MachineId", d.MachineId).Info("Get status for OpenStack instance...") + + s, err := d.client.GetInstanceState(d) + if err != nil { + return state.None, err + } + + log.WithFields(log.Fields{ + "MachineId": d.MachineId, + "State": s, + }).Info("State for OpenStack instance") + + switch s { + case "ACTIVE": + return state.Running, nil + case "PAUSED": + return state.Paused, nil + case "SUSPENDED": + return state.Saved, nil + case "SHUTOFF": + return state.Stopped, nil + case "BUILDING": + return state.Starting, nil + case "ERROR": + return state.Error, nil + } + return state.None, nil +} + +func (d *Driver) Create() error { + + d.setMachineNameIfNotSet() + d.KeyPairName = d.MachineName + + if err := d.createSSHKey(); err != nil { + return err + } + if err := d.createMachine(); err != nil { + return err + } + if err := d.waitForInstanceToStart(); err != nil { + return err + } + if err := d.installDocker(); err != nil { + return err + } + return nil +} + +func (d *Driver) Start() error { + log.WithField("MachineId", d.MachineId).Info("Starting OpenStack instance...") + if err := d.client.StartInstance(d); err != nil { + return err + } + return d.waitForInstanceToStart() +} + +func (d *Driver) Stop() error { + + log.WithField("MachineId", d.MachineId).Info("Stopping OpenStack instance...") + if err := d.client.StopInstance(d); err != nil { + return err + } + + log.WithField("MachineId", d.MachineId).Info("Waiting for the OpenStack instance to stop...") + if err := d.client.WaitForInstanceStatus(d, "SHUTOFF", 200); err != nil { + return err + } + return nil +} + +func (d *Driver) Remove() error { + log.WithField("MachineId", d.MachineId).Info("Deleting OpenStack instance...") + if err := d.client.DeleteInstance(d); err != nil { + return err + } + log.WithField("Name", d.KeyPairName).Info("Deleting Key Pair...") + if err := d.client.DeleteKeyPair(d, d.KeyPairName); err != nil { + return err + } + return nil +} + +func (d *Driver) Restart() error { + log.WithField("MachineId", d.MachineId).Info("Restarting OpenStack instance...") + if err := d.client.RestartInstance(d); err != nil { + return err + } + return d.waitForInstanceToStart() +} + +func (d *Driver) Kill() error { + return d.Stop() +} + +func (d *Driver) Upgrade() error { + return fmt.Errorf("Upgrate is currently not available for the OpenStack driver") +} + +func (d *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) { + ip, err := d.GetIP() + if err != nil { + return nil, err + } + return ssh.GetSSHCommand(ip, d.SSHPort, d.SSHUser, d.sshKeyPath(), args...), nil +} + +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" + 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'" +) + +func (d *Driver) checkConfig() error { + if d.AuthUrl == "" { + return fmt.Errorf(errorMandatoryEnvOrOption, "Autentication URL", "OS_AUTH_URL", "--openstack-auth-url") + } + if d.Username == "" { + return fmt.Errorf(errorMandatoryEnvOrOption, "Username", "OS_USERNAME", "--openstack-username") + } + if d.Password == "" { + return fmt.Errorf(errorMandatoryEnvOrOption, "Password", "OS_PASSWORD", "--openstack-password") + } + if d.TenantName == "" && d.TenantId == "" { + return fmt.Errorf(errorMandatoryTenantNameOrId) + } + if d.TenantName != "" && d.TenantId != "" { + return fmt.Errorf(errorExclusiveOptions, "tenant id", "tenant name") + } + if d.FlavorId == "" { + return fmt.Errorf(errorMandatoryOption, "Flavor id", "--openstack-flavor-id") + } + if d.ImageId == "" { + return fmt.Errorf(errorMandatoryOption, "Image id", "--openstack-image-id") + } + if d.EndpointType != "" && (d.EndpointType != "publicURL" || d.EndpointType != "adminURL" || d.EndpointType != "internalURL") { + return fmt.Errorf(errorWrongEndpointType) + } + return nil +} + +func (d *Driver) foundIP(ip string) string { + log.WithFields(log.Fields{ + "IP": ip, + "MachineId": d.MachineId, + }).Info("IP address found") + return ip +} + +func (d *Driver) createSSHKey() error { + log.WithField("Name", d.KeyPairName).Info("Creating Key Pair...") + if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil { + return err + } + publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath()) + if err != nil { + return err + } + if err := d.client.CreateKeyPair(d, d.KeyPairName, string(publicKey)); err != nil { + return err + } + return nil +} + +func (d *Driver) createMachine() error { + log.WithFields(log.Fields{ + "FlavorId": d.FlavorId, + "ImageId": d.ImageId, + }).Info("Creating OpenStack instance...") + instanceId, err := d.client.CreateInstance(d) + if err != nil { + return err + } + d.MachineId = instanceId + return nil +} + +func (d *Driver) waitForInstanceToStart() error { + log.WithField("MachineId", d.MachineId).Info("Waiting for the OpenStack instance to start...") + if err := d.client.WaitForInstanceStatus(d, "ACTIVE", 200); err != nil { + return err + } + ip, err := d.GetIP() + if err != nil { + return err + } + return ssh.WaitForTCP(fmt.Sprintf("%s:%d", ip, d.SSHPort)) +} + +func (d *Driver) installDocker() error { + log.WithField("MachineId", d.MachineId).Info("Installing dock on the machine") + cmdTemplate := "%scurl -sSL https://gist.githubusercontent.com/smashwilson/1a286139720a28ac6ead/raw/41d93c57ea2e86815cdfbfec42aaa696034afcc8/setup-docker.sh | /bin/bash" + var cmd string + if d.SSHUser == "root" { + cmd = fmt.Sprintf(cmdTemplate, "") + } else { + cmd = fmt.Sprintf(cmdTemplate, "sudo ") + } + log.Infof(cmd) + sshCmd, err := d.GetSSHCommand(cmd) + if err != nil { + return err + } + if err := sshCmd.Run(); err != nil { + log.Warnf("Docker installation failed: %v", err) + log.Warnf("The machine is not ready to run docker containers") + } + return nil +} + +func (d *Driver) sshKeyPath() string { + return path.Join(d.storePath, "id_rsa") +} + +func (d *Driver) publicSSHKeyPath() string { + return d.sshKeyPath() + ".pub" +} + +func (d *Driver) setMachineNameIfNotSet() { + if d.MachineName == "" { + d.MachineName = fmt.Sprintf("docker-host-%s", utils.GenerateRandomID()) + } +}