Add OpenStack driver

Signed-off-by: Guillaume Giamarchi <guillaume.giamarchi@gmail.com>
This commit is contained in:
Guillaume Giamarchi 2014-12-10 15:56:28 +01:00
parent 1785869490
commit f6b0c81996
4 changed files with 795 additions and 0 deletions

View File

@ -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)

View File

@ -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"

297
drivers/openstack/client.go Normal file
View File

@ -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
}

View File

@ -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())
}
}