github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/provider/openstack/provider.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // Stub provider for OpenStack, using goose will be implemented here
     5  
     6  package openstack
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"regexp"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/juju/loggo"
    19  	"launchpad.net/goose/client"
    20  	gooseerrors "launchpad.net/goose/errors"
    21  	"launchpad.net/goose/identity"
    22  	"launchpad.net/goose/nova"
    23  	"launchpad.net/goose/swift"
    24  
    25  	"launchpad.net/juju-core/constraints"
    26  	"launchpad.net/juju-core/environs"
    27  	"launchpad.net/juju-core/environs/cloudinit"
    28  	"launchpad.net/juju-core/environs/config"
    29  	"launchpad.net/juju-core/environs/imagemetadata"
    30  	"launchpad.net/juju-core/environs/instances"
    31  	"launchpad.net/juju-core/environs/simplestreams"
    32  	"launchpad.net/juju-core/environs/storage"
    33  	envtools "launchpad.net/juju-core/environs/tools"
    34  	"launchpad.net/juju-core/instance"
    35  	"launchpad.net/juju-core/names"
    36  	"launchpad.net/juju-core/provider/common"
    37  	"launchpad.net/juju-core/state"
    38  	"launchpad.net/juju-core/state/api"
    39  	"launchpad.net/juju-core/tools"
    40  	"launchpad.net/juju-core/utils"
    41  )
    42  
    43  var logger = loggo.GetLogger("juju.provider.openstack")
    44  
    45  type environProvider struct{}
    46  
    47  var _ environs.EnvironProvider = (*environProvider)(nil)
    48  
    49  var providerInstance environProvider
    50  
    51  // Use shortAttempt to poll for short-term events.
    52  // TODO: This was kept to a long timeout because Nova needs more time than EC2.
    53  // For example, HP Cloud takes around 9.1 seconds (10 samples) to return a
    54  // BUILD(spawning) status. But storage delays are handled separately now, and
    55  // perhaps other polling attempts can time out faster.
    56  var shortAttempt = utils.AttemptStrategy{
    57  	Total: 15 * time.Second,
    58  	Delay: 200 * time.Millisecond,
    59  }
    60  
    61  func init() {
    62  	environs.RegisterProvider("openstack", environProvider{})
    63  }
    64  
    65  func (p environProvider) BoilerplateConfig() string {
    66  	return `
    67  # https://juju.ubuntu.com/docs/config-openstack.html
    68  openstack:
    69      type: openstack
    70      # use-floating-ip specifies whether a floating IP address is required
    71      # to give the nodes a public IP address. Some installations assign public IP
    72      # addresses by default without requiring a floating IP address.
    73      # use-floating-ip: false
    74  
    75      # use-default-secgroup specifies whether new machine instances should have the "default"
    76      # Openstack security group assigned.
    77      # use-default-secgroup: false
    78  
    79      # network specifies the network label or uuid to bring machines up on, in
    80      # the case where multiple networks exist. It may be omitted otherwise.
    81      # network: <your network label or uuid>
    82  
    83      # tools-metadata-url specifies the location of the Juju tools and metadata. It defaults to the
    84      # global public tools metadata location https://streams.canonical.com/tools.
    85      # tools-metadata-url:  https://you-tools-metadata-url
    86  
    87      # image-metadata-url specifies the location of Ubuntu cloud image metadata. It defaults to the
    88      # global public image metadata location https://cloud-images.ubuntu.com/releases.
    89      # image-metadata-url:  https://you-tools-metadata-url
    90  
    91      # image-stream chooses a simplestreams stream to select OS images from,
    92      # for example daily or released images (or any other stream available on simplestreams).
    93      # image-stream: "released"
    94  
    95      # auth-url defaults to the value of the environment variable OS_AUTH_URL,
    96      # but can be specified here.
    97      # auth-url: https://yourkeystoneurl:443/v2.0/
    98  
    99      # tenant-name holds the openstack tenant name. It defaults to
   100      # the environment variable OS_TENANT_NAME.
   101      # tenant-name: <your tenant name>
   102  
   103      # region holds the openstack region.  It defaults to
   104      # the environment variable OS_REGION_NAME.
   105      # region: <your region>
   106  
   107      # The auth-mode, username and password attributes
   108      # are used for userpass authentication (the default).
   109  
   110      # auth-mode holds the authentication mode. For user-password
   111      # authentication, auth-mode should be "userpass" and username
   112      # and password should be set appropriately; they default to
   113      # the environment variables OS_USERNAME and OS_PASSWORD
   114       # respectively.
   115      # auth-mode: userpass
   116      # username: <your username>
   117      # password: <secret>
   118       
   119      # For key-pair authentication, auth-mode should  be "keypair"
   120      # and access-key and secret-key should be  set appropriately; they default to
   121      # the environment variables OS_ACCESS_KEY and OS_SECRET_KEY
   122      # respectively.
   123      # auth-mode: keypair
   124      # access-key: <secret>
   125      # secret-key: <secret>
   126  
   127  # https://juju.ubuntu.com/docs/config-hpcloud.html
   128  hpcloud:
   129      type: openstack
   130      
   131      # use-floating-ip specifies whether a floating IP address is required
   132      # to give the nodes a public IP address. Some installations assign public IP
   133      # addresses by default without requiring a floating IP address.
   134      # use-floating-ip: false
   135  
   136      # use-default-secgroup specifies whether new machine instances should have the "default"
   137      # Openstack security group assigned.
   138      # use-default-secgroup: false
   139  
   140      # tenant-name holds the openstack tenant name. In HPCloud, this is 
   141      # synonymous with the project-name  It defaults to
   142      # the environment variable OS_TENANT_NAME.
   143      # tenant-name: <your tenant name>
   144      
   145      # auth-url holds the keystone url for authentication. 
   146      # It defaults to the value of the environment variable OS_AUTH_URL.
   147      # auth-url: https://region-a.geo-1.identity.hpcloudsvc.com:35357/v2.0/
   148  
   149      # region holds the HP Cloud region (e.g. az-1.region-a.geo-1).  
   150      # It defaults to the environment variable OS_REGION_NAME.
   151      # region: <your region>
   152      
   153      # auth-mode holds the authentication mode. For user-password
   154      # authentication, auth-mode should be "userpass" and username
   155      # and password should be set appropriately; they default to
   156      # the environment variables OS_USERNAME and OS_PASSWORD
   157      # respectively.
   158      # auth-mode: userpass
   159      # username: <your_username>
   160      # password: <your_password>
   161      
   162      # For key-pair authentication, auth-mode should  be "keypair"
   163      # and access-key and secret-key should be  set appropriately; they default to
   164      # the environment variables OS_ACCESS_KEY and OS_SECRET_KEY
   165      # respectively.
   166      # auth-mode: keypair
   167      # access-key: <secret>
   168      # secret-key: <secret>
   169  `[1:]
   170  }
   171  
   172  func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) {
   173  	logger.Infof("opening environment %q", cfg.Name())
   174  	e := new(environ)
   175  	err := e.SetConfig(cfg)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	e.name = cfg.Name()
   180  	return e, nil
   181  }
   182  
   183  func (p environProvider) Prepare(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) {
   184  	attrs := cfg.UnknownAttrs()
   185  	if _, ok := attrs["control-bucket"]; !ok {
   186  		uuid, err := utils.NewUUID()
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  		attrs["control-bucket"] = fmt.Sprintf("%x", uuid.Raw())
   191  	}
   192  	cfg, err := cfg.Apply(attrs)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	return p.Open(cfg)
   197  }
   198  
   199  // MetadataLookupParams returns parameters which are used to query image metadata to
   200  // find matching image information.
   201  func (p environProvider) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) {
   202  	if region == "" {
   203  		return nil, fmt.Errorf("region must be specified")
   204  	}
   205  	return &simplestreams.MetadataLookupParams{
   206  		Region:        region,
   207  		Architectures: []string{"amd64", "arm", "arm64", "ppc64"},
   208  	}, nil
   209  }
   210  
   211  func (p environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
   212  	m := make(map[string]string)
   213  	ecfg, err := providerInstance.newConfig(cfg)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	m["username"] = ecfg.username()
   218  	m["password"] = ecfg.password()
   219  	m["tenant-name"] = ecfg.tenantName()
   220  	return m, nil
   221  }
   222  
   223  func (p environProvider) PublicAddress() (string, error) {
   224  	if addr, err := fetchMetadata("public-ipv4"); err != nil {
   225  		return "", err
   226  	} else if addr != "" {
   227  		return addr, nil
   228  	}
   229  	return p.PrivateAddress()
   230  }
   231  
   232  func (p environProvider) PrivateAddress() (string, error) {
   233  	return fetchMetadata("local-ipv4")
   234  }
   235  
   236  // metadataHost holds the address of the instance metadata service.
   237  // It is a variable so that tests can change it to refer to a local
   238  // server when needed.
   239  var metadataHost = "http://169.254.169.254"
   240  
   241  // fetchMetadata fetches a single atom of data from the openstack instance metadata service.
   242  // http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html
   243  // (the same specs is implemented in ec2, hence the reference)
   244  func fetchMetadata(name string) (value string, err error) {
   245  	uri := fmt.Sprintf("%s/latest/meta-data/%s", metadataHost, name)
   246  	data, err := retryGet(uri)
   247  	if err != nil {
   248  		return "", err
   249  	}
   250  	return strings.TrimSpace(string(data)), nil
   251  }
   252  
   253  func retryGet(uri string) (data []byte, err error) {
   254  	for a := shortAttempt.Start(); a.Next(); {
   255  		var resp *http.Response
   256  		resp, err = http.Get(uri)
   257  		if err != nil {
   258  			continue
   259  		}
   260  		defer resp.Body.Close()
   261  		if resp.StatusCode != http.StatusOK {
   262  			err = fmt.Errorf("bad http response %v", resp.Status)
   263  			continue
   264  		}
   265  		var data []byte
   266  		data, err = ioutil.ReadAll(resp.Body)
   267  		if err != nil {
   268  			continue
   269  		}
   270  		return data, nil
   271  	}
   272  	if err != nil {
   273  		return nil, fmt.Errorf("cannot get %q: %v", uri, err)
   274  	}
   275  	return
   276  }
   277  
   278  type environ struct {
   279  	name string
   280  
   281  	ecfgMutex       sync.Mutex
   282  	imageBaseMutex  sync.Mutex
   283  	toolsBaseMutex  sync.Mutex
   284  	ecfgUnlocked    *environConfig
   285  	client          client.AuthenticatingClient
   286  	novaUnlocked    *nova.Client
   287  	storageUnlocked storage.Storage
   288  	// An ordered list of sources in which to find the simplestreams index files used to
   289  	// look up image ids.
   290  	imageSources []simplestreams.DataSource
   291  	// An ordered list of paths in which to find the simplestreams index files used to
   292  	// look up tools ids.
   293  	toolsSources []simplestreams.DataSource
   294  }
   295  
   296  var _ environs.Environ = (*environ)(nil)
   297  var _ imagemetadata.SupportsCustomSources = (*environ)(nil)
   298  var _ envtools.SupportsCustomSources = (*environ)(nil)
   299  var _ simplestreams.HasRegion = (*environ)(nil)
   300  
   301  type openstackInstance struct {
   302  	e        *environ
   303  	instType *instances.InstanceType
   304  	arch     *string
   305  
   306  	mu           sync.Mutex
   307  	serverDetail *nova.ServerDetail
   308  }
   309  
   310  func (inst *openstackInstance) String() string {
   311  	return string(inst.Id())
   312  }
   313  
   314  var _ instance.Instance = (*openstackInstance)(nil)
   315  
   316  func (inst *openstackInstance) Refresh() error {
   317  	inst.mu.Lock()
   318  	defer inst.mu.Unlock()
   319  	server, err := inst.e.nova().GetServer(inst.serverDetail.Id)
   320  	if err != nil {
   321  		return err
   322  	}
   323  	inst.serverDetail = server
   324  	return nil
   325  }
   326  
   327  func (inst *openstackInstance) getServerDetail() *nova.ServerDetail {
   328  	inst.mu.Lock()
   329  	defer inst.mu.Unlock()
   330  	return inst.serverDetail
   331  }
   332  
   333  func (inst *openstackInstance) Id() instance.Id {
   334  	return instance.Id(inst.getServerDetail().Id)
   335  }
   336  
   337  func (inst *openstackInstance) Status() string {
   338  	return inst.getServerDetail().Status
   339  }
   340  
   341  func (inst *openstackInstance) hardwareCharacteristics() *instance.HardwareCharacteristics {
   342  	hc := &instance.HardwareCharacteristics{Arch: inst.arch}
   343  	if inst.instType != nil {
   344  		hc.Mem = &inst.instType.Mem
   345  		// openstack is special in that a 0-size root disk means that
   346  		// the root disk will result in an instance with a root disk
   347  		// the same size as the image that created it, so we just set
   348  		// the HardwareCharacteristics to nil to signal that we don't
   349  		// know what the correct size is.
   350  		if inst.instType.RootDisk == 0 {
   351  			hc.RootDisk = nil
   352  		} else {
   353  			hc.RootDisk = &inst.instType.RootDisk
   354  		}
   355  		hc.CpuCores = &inst.instType.CpuCores
   356  		hc.CpuPower = inst.instType.CpuPower
   357  		// tags not currently supported on openstack
   358  	}
   359  	return hc
   360  }
   361  
   362  // getAddress returns the existing server information on addresses,
   363  // but fetches the details over the api again if no addresses exist.
   364  func (inst *openstackInstance) getAddresses() (map[string][]nova.IPAddress, error) {
   365  	addrs := inst.getServerDetail().Addresses
   366  	if len(addrs) == 0 {
   367  		server, err := inst.e.nova().GetServer(string(inst.Id()))
   368  		if err != nil {
   369  			return nil, err
   370  		}
   371  		addrs = server.Addresses
   372  	}
   373  	return addrs, nil
   374  }
   375  
   376  // Addresses implements instance.Addresses() returning generic address
   377  // details for the instances, and calling the openstack api if needed.
   378  func (inst *openstackInstance) Addresses() ([]instance.Address, error) {
   379  	addresses, err := inst.getAddresses()
   380  	if err != nil {
   381  		return nil, err
   382  	}
   383  	return convertNovaAddresses(addresses), nil
   384  }
   385  
   386  // convertNovaAddresses returns nova addresses in generic format
   387  func convertNovaAddresses(addresses map[string][]nova.IPAddress) []instance.Address {
   388  	// TODO(gz) Network ordering may be significant but is not preserved by
   389  	// the map, see lp:1188126 for example. That could potentially be fixed
   390  	// in goose, or left to be derived by other means.
   391  	var machineAddresses []instance.Address
   392  	for network, ips := range addresses {
   393  		networkscope := instance.NetworkUnknown
   394  		// For canonistack and hpcloud, public floating addresses may
   395  		// be put in networks named something other than public. Rely
   396  		// on address sanity logic to catch and mark them corectly.
   397  		if network == "public" {
   398  			networkscope = instance.NetworkPublic
   399  		}
   400  		for _, address := range ips {
   401  			// Assume ipv4 unless specified otherwise
   402  			addrtype := instance.Ipv4Address
   403  			if address.Version == 6 {
   404  				addrtype = instance.Ipv6Address
   405  			}
   406  			// TODO(gz): Use NewAddress... with sanity checking
   407  			machineAddr := instance.Address{
   408  				Value:        address.Address,
   409  				Type:         addrtype,
   410  				NetworkName:  network,
   411  				NetworkScope: networkscope,
   412  			}
   413  			machineAddresses = append(machineAddresses, machineAddr)
   414  		}
   415  	}
   416  	return machineAddresses
   417  }
   418  
   419  func (inst *openstackInstance) DNSName() (string, error) {
   420  	addresses, err := inst.Addresses()
   421  	if err != nil {
   422  		return "", err
   423  	}
   424  	addr := instance.SelectPublicAddress(addresses)
   425  	if addr == "" {
   426  		return "", instance.ErrNoDNSName
   427  	}
   428  	return addr, nil
   429  }
   430  
   431  func (inst *openstackInstance) WaitDNSName() (string, error) {
   432  	return common.WaitDNSName(inst)
   433  }
   434  
   435  // TODO: following 30 lines nearly verbatim from environs/ec2
   436  
   437  func (inst *openstackInstance) OpenPorts(machineId string, ports []instance.Port) error {
   438  	if inst.e.Config().FirewallMode() != config.FwInstance {
   439  		return fmt.Errorf("invalid firewall mode %q for opening ports on instance",
   440  			inst.e.Config().FirewallMode())
   441  	}
   442  	name := inst.e.machineGroupName(machineId)
   443  	if err := inst.e.openPortsInGroup(name, ports); err != nil {
   444  		return err
   445  	}
   446  	logger.Infof("opened ports in security group %s: %v", name, ports)
   447  	return nil
   448  }
   449  
   450  func (inst *openstackInstance) ClosePorts(machineId string, ports []instance.Port) error {
   451  	if inst.e.Config().FirewallMode() != config.FwInstance {
   452  		return fmt.Errorf("invalid firewall mode %q for closing ports on instance",
   453  			inst.e.Config().FirewallMode())
   454  	}
   455  	name := inst.e.machineGroupName(machineId)
   456  	if err := inst.e.closePortsInGroup(name, ports); err != nil {
   457  		return err
   458  	}
   459  	logger.Infof("closed ports in security group %s: %v", name, ports)
   460  	return nil
   461  }
   462  
   463  func (inst *openstackInstance) Ports(machineId string) ([]instance.Port, error) {
   464  	if inst.e.Config().FirewallMode() != config.FwInstance {
   465  		return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance",
   466  			inst.e.Config().FirewallMode())
   467  	}
   468  	name := inst.e.machineGroupName(machineId)
   469  	return inst.e.portsInGroup(name)
   470  }
   471  
   472  func (e *environ) ecfg() *environConfig {
   473  	e.ecfgMutex.Lock()
   474  	ecfg := e.ecfgUnlocked
   475  	e.ecfgMutex.Unlock()
   476  	return ecfg
   477  }
   478  
   479  func (e *environ) nova() *nova.Client {
   480  	e.ecfgMutex.Lock()
   481  	nova := e.novaUnlocked
   482  	e.ecfgMutex.Unlock()
   483  	return nova
   484  }
   485  
   486  func (e *environ) Name() string {
   487  	return e.name
   488  }
   489  
   490  func (e *environ) Storage() storage.Storage {
   491  	e.ecfgMutex.Lock()
   492  	stor := e.storageUnlocked
   493  	e.ecfgMutex.Unlock()
   494  	return stor
   495  }
   496  
   497  func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error {
   498  	// The client's authentication may have been reset when finding tools if the agent-version
   499  	// attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated.
   500  	// An authenticated client is needed for the URL() call below.
   501  	err := e.client.Authenticate()
   502  	if err != nil {
   503  		return err
   504  	}
   505  	return common.Bootstrap(ctx, e, cons)
   506  }
   507  
   508  func (e *environ) StateInfo() (*state.Info, *api.Info, error) {
   509  	return common.StateInfo(e)
   510  }
   511  
   512  func (e *environ) Config() *config.Config {
   513  	return e.ecfg().Config
   514  }
   515  
   516  func (e *environ) authClient(ecfg *environConfig, authModeCfg AuthMode) client.AuthenticatingClient {
   517  	cred := &identity.Credentials{
   518  		User:       ecfg.username(),
   519  		Secrets:    ecfg.password(),
   520  		Region:     ecfg.region(),
   521  		TenantName: ecfg.tenantName(),
   522  		URL:        ecfg.authURL(),
   523  	}
   524  	// authModeCfg has already been validated so we know it's one of the values below.
   525  	var authMode identity.AuthMode
   526  	switch authModeCfg {
   527  	case AuthLegacy:
   528  		authMode = identity.AuthLegacy
   529  	case AuthUserPass:
   530  		authMode = identity.AuthUserPass
   531  	case AuthKeyPair:
   532  		authMode = identity.AuthKeyPair
   533  		cred.User = ecfg.accessKey()
   534  		cred.Secrets = ecfg.secretKey()
   535  	}
   536  	newClient := client.NewClient
   537  	if !ecfg.SSLHostnameVerification() {
   538  		newClient = client.NewNonValidatingClient
   539  	}
   540  	return newClient(cred, authMode, nil)
   541  }
   542  
   543  func (e *environ) SetConfig(cfg *config.Config) error {
   544  	ecfg, err := providerInstance.newConfig(cfg)
   545  	if err != nil {
   546  		return err
   547  	}
   548  	// At this point, the authentication method config value has been validated so we extract it's value here
   549  	// to avoid having to validate again each time when creating the OpenStack client.
   550  	var authModeCfg AuthMode
   551  	e.ecfgMutex.Lock()
   552  	defer e.ecfgMutex.Unlock()
   553  	authModeCfg = AuthMode(ecfg.authMode())
   554  	e.ecfgUnlocked = ecfg
   555  
   556  	e.client = e.authClient(ecfg, authModeCfg)
   557  	e.novaUnlocked = nova.New(e.client)
   558  
   559  	// create new control storage instance, existing instances continue
   560  	// to reference their existing configuration.
   561  	// public storage instance creation is deferred until needed since authenticated
   562  	// access to the identity service is required so that any juju-tools endpoint can be used.
   563  	e.storageUnlocked = &openstackstorage{
   564  		containerName: ecfg.controlBucket(),
   565  		// this is possibly just a hack - if the ACL is swift.Private,
   566  		// the machine won't be able to get the tools (401 error)
   567  		containerACL: swift.PublicRead,
   568  		swift:        swift.New(e.client)}
   569  	return nil
   570  }
   571  
   572  // GetImageSources returns a list of sources which are used to search for simplestreams image metadata.
   573  func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) {
   574  	e.imageBaseMutex.Lock()
   575  	defer e.imageBaseMutex.Unlock()
   576  
   577  	if e.imageSources != nil {
   578  		return e.imageSources, nil
   579  	}
   580  	if !e.client.IsAuthenticated() {
   581  		err := e.client.Authenticate()
   582  		if err != nil {
   583  			return nil, err
   584  		}
   585  	}
   586  	// Add the simplestreams source off the control bucket.
   587  	e.imageSources = append(e.imageSources, storage.NewStorageSimpleStreamsDataSource(
   588  		"cloud storage", e.Storage(), storage.BaseImagesPath))
   589  	// Add the simplestreams base URL from keystone if it is defined.
   590  	productStreamsURL, err := e.client.MakeServiceURL("product-streams", nil)
   591  	if err == nil {
   592  		verify := simplestreams.VerifySSLHostnames
   593  		if !e.Config().SSLHostnameVerification() {
   594  			verify = simplestreams.NoVerifySSLHostnames
   595  		}
   596  		source := simplestreams.NewURLDataSource("keystone catalog", productStreamsURL, verify)
   597  		e.imageSources = append(e.imageSources, source)
   598  	}
   599  	return e.imageSources, nil
   600  }
   601  
   602  // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata.
   603  func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) {
   604  	e.toolsBaseMutex.Lock()
   605  	defer e.toolsBaseMutex.Unlock()
   606  
   607  	if e.toolsSources != nil {
   608  		return e.toolsSources, nil
   609  	}
   610  	if !e.client.IsAuthenticated() {
   611  		err := e.client.Authenticate()
   612  		if err != nil {
   613  			return nil, err
   614  		}
   615  	}
   616  	verify := simplestreams.VerifySSLHostnames
   617  	if !e.Config().SSLHostnameVerification() {
   618  		verify = simplestreams.NoVerifySSLHostnames
   619  	}
   620  	// Add the simplestreams source off the control bucket.
   621  	e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource(
   622  		"cloud storage", e.Storage(), storage.BaseToolsPath))
   623  	// Add the simplestreams base URL from keystone if it is defined.
   624  	toolsURL, err := e.client.MakeServiceURL("juju-tools", nil)
   625  	if err == nil {
   626  		source := simplestreams.NewURLDataSource("keystone catalog", toolsURL, verify)
   627  		e.toolsSources = append(e.toolsSources, source)
   628  	}
   629  	return e.toolsSources, nil
   630  }
   631  
   632  // TODO(gz): Move this somewhere more reusable
   633  const uuidPattern = "^([a-fA-F0-9]{8})-([a-fA-f0-9]{4})-([1-5][a-fA-f0-9]{3})-([a-fA-f0-9]{4})-([a-fA-f0-9]{12})$"
   634  
   635  var uuidRegexp = regexp.MustCompile(uuidPattern)
   636  
   637  // resolveNetwork takes either a network id or label and returns a network id
   638  func (e *environ) resolveNetwork(networkName string) (string, error) {
   639  	if uuidRegexp.MatchString(networkName) {
   640  		// Network id supplied, assume valid as boot will fail if not
   641  		return networkName, nil
   642  	}
   643  	// Network label supplied, resolve to a network id
   644  	networks, err := e.nova().ListNetworks()
   645  	if err != nil {
   646  		return "", err
   647  	}
   648  	var networkIds = []string{}
   649  	for _, network := range networks {
   650  		if network.Label == networkName {
   651  			networkIds = append(networkIds, network.Id)
   652  		}
   653  	}
   654  	switch len(networkIds) {
   655  	case 1:
   656  		return networkIds[0], nil
   657  	case 0:
   658  		return "", fmt.Errorf("No networks exist with label %q", networkName)
   659  	}
   660  	return "", fmt.Errorf("Multiple networks with label %q: %v", networkName, networkIds)
   661  }
   662  
   663  // allocatePublicIP tries to find an available floating IP address, or
   664  // allocates a new one, returning it, or an error
   665  func (e *environ) allocatePublicIP() (*nova.FloatingIP, error) {
   666  	fips, err := e.nova().ListFloatingIPs()
   667  	if err != nil {
   668  		return nil, err
   669  	}
   670  	var newfip *nova.FloatingIP
   671  	for _, fip := range fips {
   672  		newfip = &fip
   673  		if fip.InstanceId != nil && *fip.InstanceId != "" {
   674  			// unavailable, skip
   675  			newfip = nil
   676  			continue
   677  		} else {
   678  			// unassigned, we can use it
   679  			return newfip, nil
   680  		}
   681  	}
   682  	if newfip == nil {
   683  		// allocate a new IP and use it
   684  		newfip, err = e.nova().AllocateFloatingIP()
   685  		if err != nil {
   686  			return nil, err
   687  		}
   688  	}
   689  	return newfip, nil
   690  }
   691  
   692  // assignPublicIP tries to assign the given floating IP address to the
   693  // specified server, or returns an error.
   694  func (e *environ) assignPublicIP(fip *nova.FloatingIP, serverId string) (err error) {
   695  	if fip == nil {
   696  		return fmt.Errorf("cannot assign a nil public IP to %q", serverId)
   697  	}
   698  	if fip.InstanceId != nil && *fip.InstanceId == serverId {
   699  		// IP already assigned, nothing to do
   700  		return nil
   701  	}
   702  	// At startup nw_info is not yet cached so this may fail
   703  	// temporarily while the server is being built
   704  	for a := common.LongAttempt.Start(); a.Next(); {
   705  		err = e.nova().AddServerFloatingIP(serverId, fip.IP)
   706  		if err == nil {
   707  			return nil
   708  		}
   709  	}
   710  	return err
   711  }
   712  
   713  // StartInstance is specified in the InstanceBroker interface.
   714  func (e *environ) StartInstance(cons constraints.Value, possibleTools tools.List,
   715  	machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) {
   716  
   717  	series := possibleTools.OneSeries()
   718  	arches := possibleTools.Arches()
   719  	spec, err := findInstanceSpec(e, &instances.InstanceConstraint{
   720  		Region:      e.ecfg().region(),
   721  		Series:      series,
   722  		Arches:      arches,
   723  		Constraints: cons,
   724  	})
   725  	if err != nil {
   726  		return nil, nil, err
   727  	}
   728  	tools, err := possibleTools.Match(tools.Filter{Arch: spec.Image.Arch})
   729  	if err != nil {
   730  		return nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches)
   731  	}
   732  
   733  	machineConfig.Tools = tools[0]
   734  
   735  	if err := environs.FinishMachineConfig(machineConfig, e.Config(), cons); err != nil {
   736  		return nil, nil, err
   737  	}
   738  	userData, err := environs.ComposeUserData(machineConfig)
   739  	if err != nil {
   740  		return nil, nil, fmt.Errorf("cannot make user data: %v", err)
   741  	}
   742  	logger.Debugf("openstack user data; %d bytes", len(userData))
   743  	var networks = []nova.ServerNetworks{}
   744  	usingNetwork := e.ecfg().network()
   745  	if usingNetwork != "" {
   746  		networkId, err := e.resolveNetwork(usingNetwork)
   747  		if err != nil {
   748  			return nil, nil, err
   749  		}
   750  		logger.Debugf("using network id %q", networkId)
   751  		networks = append(networks, nova.ServerNetworks{NetworkId: networkId})
   752  	}
   753  	withPublicIP := e.ecfg().useFloatingIP()
   754  	var publicIP *nova.FloatingIP
   755  	if withPublicIP {
   756  		if fip, err := e.allocatePublicIP(); err != nil {
   757  			return nil, nil, fmt.Errorf("cannot allocate a public IP as needed: %v", err)
   758  		} else {
   759  			publicIP = fip
   760  			logger.Infof("allocated public IP %s", publicIP.IP)
   761  		}
   762  	}
   763  	cfg := e.Config()
   764  	groups, err := e.setUpGroups(machineConfig.MachineId, cfg.StatePort(), cfg.APIPort())
   765  	if err != nil {
   766  		return nil, nil, fmt.Errorf("cannot set up groups: %v", err)
   767  	}
   768  	var groupNames = make([]nova.SecurityGroupName, len(groups))
   769  	for i, g := range groups {
   770  		groupNames[i] = nova.SecurityGroupName{g.Name}
   771  	}
   772  	var opts = nova.RunServerOpts{
   773  		Name:               e.machineFullName(machineConfig.MachineId),
   774  		FlavorId:           spec.InstanceType.Id,
   775  		ImageId:            spec.Image.Id,
   776  		UserData:           userData,
   777  		SecurityGroupNames: groupNames,
   778  		Networks:           networks,
   779  	}
   780  	var server *nova.Entity
   781  	for a := shortAttempt.Start(); a.Next(); {
   782  		server, err = e.nova().RunServer(opts)
   783  		if err == nil || !gooseerrors.IsNotFound(err) {
   784  			break
   785  		}
   786  	}
   787  	if err != nil {
   788  		return nil, nil, fmt.Errorf("cannot run instance: %v", err)
   789  	}
   790  	detail, err := e.nova().GetServer(server.Id)
   791  	if err != nil {
   792  		return nil, nil, fmt.Errorf("cannot get started instance: %v", err)
   793  	}
   794  	inst := &openstackInstance{
   795  		e:            e,
   796  		serverDetail: detail,
   797  		arch:         &spec.Image.Arch,
   798  		instType:     &spec.InstanceType,
   799  	}
   800  	logger.Infof("started instance %q", inst.Id())
   801  	if withPublicIP {
   802  		if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil {
   803  			if err := e.terminateInstances([]instance.Id{inst.Id()}); err != nil {
   804  				// ignore the failure at this stage, just log it
   805  				logger.Debugf("failed to terminate instance %q: %v", inst.Id(), err)
   806  			}
   807  			return nil, nil, fmt.Errorf("cannot assign public address %s to instance %q: %v", publicIP.IP, inst.Id(), err)
   808  		}
   809  		logger.Infof("assigned public IP %s to %q", publicIP.IP, inst.Id())
   810  	}
   811  	return inst, inst.hardwareCharacteristics(), nil
   812  }
   813  
   814  func (e *environ) StopInstances(insts []instance.Instance) error {
   815  	ids := make([]instance.Id, len(insts))
   816  	for i, inst := range insts {
   817  		instanceValue, ok := inst.(*openstackInstance)
   818  		if !ok {
   819  			return errors.New("Incompatible instance.Instance supplied")
   820  		}
   821  		ids[i] = instanceValue.Id()
   822  	}
   823  	logger.Debugf("terminating instances %v", ids)
   824  	return e.terminateInstances(ids)
   825  }
   826  
   827  // collectInstances tries to get information on each instance id in ids.
   828  // It fills the slots in the given map for known servers with status
   829  // either ACTIVE or BUILD. Returns a list of missing ids.
   830  func (e *environ) collectInstances(ids []instance.Id, out map[instance.Id]instance.Instance) []instance.Id {
   831  	var err error
   832  	serversById := make(map[string]nova.ServerDetail)
   833  	if len(ids) == 1 {
   834  		// most common case - single instance
   835  		var server *nova.ServerDetail
   836  		server, err = e.nova().GetServer(string(ids[0]))
   837  		if server != nil {
   838  			serversById[server.Id] = *server
   839  		}
   840  	} else {
   841  		var servers []nova.ServerDetail
   842  		servers, err = e.nova().ListServersDetail(e.machinesFilter())
   843  		for _, server := range servers {
   844  			serversById[server.Id] = server
   845  		}
   846  	}
   847  	if err != nil {
   848  		return ids
   849  	}
   850  	var missing []instance.Id
   851  	for _, id := range ids {
   852  		if server, found := serversById[string(id)]; found {
   853  			// HPCloud uses "BUILD(spawning)" as an intermediate BUILD states once networking is available.
   854  			switch server.Status {
   855  			case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning:
   856  				// TODO(wallyworld): lookup the flavor details to fill in the instance type data
   857  				out[id] = &openstackInstance{e: e, serverDetail: &server}
   858  				continue
   859  			}
   860  		}
   861  		missing = append(missing, id)
   862  	}
   863  	return missing
   864  }
   865  
   866  func (e *environ) Instances(ids []instance.Id) ([]instance.Instance, error) {
   867  	if len(ids) == 0 {
   868  		return nil, nil
   869  	}
   870  	missing := ids
   871  	found := make(map[instance.Id]instance.Instance)
   872  	// Make a series of requests to cope with eventual consistency.
   873  	// Each request will attempt to add more instances to the requested
   874  	// set.
   875  	for a := shortAttempt.Start(); a.Next(); {
   876  		if missing = e.collectInstances(missing, found); len(missing) == 0 {
   877  			break
   878  		}
   879  	}
   880  	if len(found) == 0 {
   881  		return nil, environs.ErrNoInstances
   882  	}
   883  	insts := make([]instance.Instance, len(ids))
   884  	var err error
   885  	for i, id := range ids {
   886  		if inst := found[id]; inst != nil {
   887  			insts[i] = inst
   888  		} else {
   889  			err = environs.ErrPartialInstances
   890  		}
   891  	}
   892  	return insts, err
   893  }
   894  
   895  func (e *environ) AllInstances() (insts []instance.Instance, err error) {
   896  	servers, err := e.nova().ListServersDetail(e.machinesFilter())
   897  	if err != nil {
   898  		return nil, err
   899  	}
   900  	for _, server := range servers {
   901  		if server.Status == nova.StatusActive || server.Status == nova.StatusBuild {
   902  			var s = server
   903  			// TODO(wallyworld): lookup the flavor details to fill in the instance type data
   904  			insts = append(insts, &openstackInstance{
   905  				e:            e,
   906  				serverDetail: &s,
   907  			})
   908  		}
   909  	}
   910  	return insts, err
   911  }
   912  
   913  func (e *environ) Destroy() error {
   914  	return common.Destroy(e)
   915  }
   916  
   917  func (e *environ) globalGroupName() string {
   918  	return fmt.Sprintf("%s-global", e.jujuGroupName())
   919  }
   920  
   921  func (e *environ) machineGroupName(machineId string) string {
   922  	return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId)
   923  }
   924  
   925  func (e *environ) jujuGroupName() string {
   926  	return fmt.Sprintf("juju-%s", e.name)
   927  }
   928  
   929  func (e *environ) machineFullName(machineId string) string {
   930  	return fmt.Sprintf("juju-%s-%s", e.Name(), names.MachineTag(machineId))
   931  }
   932  
   933  // machinesFilter returns a nova.Filter matching all machines in the environment.
   934  func (e *environ) machinesFilter() *nova.Filter {
   935  	filter := nova.NewFilter()
   936  	filter.Set(nova.FilterServer, fmt.Sprintf("juju-%s-machine-\\d*", e.Name()))
   937  	return filter
   938  }
   939  
   940  func (e *environ) openPortsInGroup(name string, ports []instance.Port) error {
   941  	novaclient := e.nova()
   942  	group, err := novaclient.SecurityGroupByName(name)
   943  	if err != nil {
   944  		return err
   945  	}
   946  	for _, port := range ports {
   947  		_, err := novaclient.CreateSecurityGroupRule(nova.RuleInfo{
   948  			ParentGroupId: group.Id,
   949  			FromPort:      port.Number,
   950  			ToPort:        port.Number,
   951  			IPProtocol:    port.Protocol,
   952  			Cidr:          "0.0.0.0/0",
   953  		})
   954  		if err != nil {
   955  			// TODO: if err is not rule already exists, raise?
   956  			logger.Debugf("error creating security group rule: %v", err.Error())
   957  		}
   958  	}
   959  	return nil
   960  }
   961  
   962  func (e *environ) closePortsInGroup(name string, ports []instance.Port) error {
   963  	if len(ports) == 0 {
   964  		return nil
   965  	}
   966  	novaclient := e.nova()
   967  	group, err := novaclient.SecurityGroupByName(name)
   968  	if err != nil {
   969  		return err
   970  	}
   971  	// TODO: Hey look ma, it's quadratic
   972  	for _, port := range ports {
   973  		for _, p := range (*group).Rules {
   974  			if p.IPProtocol == nil || *p.IPProtocol != port.Protocol ||
   975  				p.FromPort == nil || *p.FromPort != port.Number ||
   976  				p.ToPort == nil || *p.ToPort != port.Number {
   977  				continue
   978  			}
   979  			err := novaclient.DeleteSecurityGroupRule(p.Id)
   980  			if err != nil {
   981  				return err
   982  			}
   983  			break
   984  		}
   985  	}
   986  	return nil
   987  }
   988  
   989  func (e *environ) portsInGroup(name string) (ports []instance.Port, err error) {
   990  	group, err := e.nova().SecurityGroupByName(name)
   991  	if err != nil {
   992  		return nil, err
   993  	}
   994  	for _, p := range (*group).Rules {
   995  		for i := *p.FromPort; i <= *p.ToPort; i++ {
   996  			ports = append(ports, instance.Port{
   997  				Protocol: *p.IPProtocol,
   998  				Number:   i,
   999  			})
  1000  		}
  1001  	}
  1002  	instance.SortPorts(ports)
  1003  	return ports, nil
  1004  }
  1005  
  1006  // TODO: following 30 lines nearly verbatim from environs/ec2
  1007  
  1008  func (e *environ) OpenPorts(ports []instance.Port) error {
  1009  	if e.Config().FirewallMode() != config.FwGlobal {
  1010  		return fmt.Errorf("invalid firewall mode %q for opening ports on environment",
  1011  			e.Config().FirewallMode())
  1012  	}
  1013  	if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil {
  1014  		return err
  1015  	}
  1016  	logger.Infof("opened ports in global group: %v", ports)
  1017  	return nil
  1018  }
  1019  
  1020  func (e *environ) ClosePorts(ports []instance.Port) error {
  1021  	if e.Config().FirewallMode() != config.FwGlobal {
  1022  		return fmt.Errorf("invalid firewall mode %q for closing ports on environment",
  1023  			e.Config().FirewallMode())
  1024  	}
  1025  	if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil {
  1026  		return err
  1027  	}
  1028  	logger.Infof("closed ports in global group: %v", ports)
  1029  	return nil
  1030  }
  1031  
  1032  func (e *environ) Ports() ([]instance.Port, error) {
  1033  	if e.Config().FirewallMode() != config.FwGlobal {
  1034  		return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment",
  1035  			e.Config().FirewallMode())
  1036  	}
  1037  	return e.portsInGroup(e.globalGroupName())
  1038  }
  1039  
  1040  func (e *environ) Provider() environs.EnvironProvider {
  1041  	return &providerInstance
  1042  }
  1043  
  1044  func (e *environ) setUpGlobalGroup(groupName string, statePort, apiPort int) (nova.SecurityGroup, error) {
  1045  	return e.ensureGroup(groupName,
  1046  		[]nova.RuleInfo{
  1047  			{
  1048  				IPProtocol: "tcp",
  1049  				FromPort:   22,
  1050  				ToPort:     22,
  1051  				Cidr:       "0.0.0.0/0",
  1052  			},
  1053  			{
  1054  				IPProtocol: "tcp",
  1055  				FromPort:   statePort,
  1056  				ToPort:     statePort,
  1057  				Cidr:       "0.0.0.0/0",
  1058  			},
  1059  			{
  1060  				IPProtocol: "tcp",
  1061  				FromPort:   apiPort,
  1062  				ToPort:     apiPort,
  1063  				Cidr:       "0.0.0.0/0",
  1064  			},
  1065  			{
  1066  				IPProtocol: "tcp",
  1067  				FromPort:   1,
  1068  				ToPort:     65535,
  1069  			},
  1070  			{
  1071  				IPProtocol: "udp",
  1072  				FromPort:   1,
  1073  				ToPort:     65535,
  1074  			},
  1075  			{
  1076  				IPProtocol: "icmp",
  1077  				FromPort:   -1,
  1078  				ToPort:     -1,
  1079  			},
  1080  		})
  1081  }
  1082  
  1083  // setUpGroups creates the security groups for the new machine, and
  1084  // returns them.
  1085  //
  1086  // Instances are tagged with a group so they can be distinguished from
  1087  // other instances that might be running on the same OpenStack account.
  1088  // In addition, a specific machine security group is created for each
  1089  // machine, so that its firewall rules can be configured per machine.
  1090  //
  1091  // Note: ideally we'd have a better way to determine group membership so that 2
  1092  // people that happen to share an openstack account and name their environment
  1093  // "openstack" don't end up destroying each other's machines.
  1094  func (e *environ) setUpGroups(machineId string, statePort, apiPort int) ([]nova.SecurityGroup, error) {
  1095  	jujuGroup, err := e.setUpGlobalGroup(e.jujuGroupName(), statePort, apiPort)
  1096  	if err != nil {
  1097  		return nil, err
  1098  	}
  1099  	var machineGroup nova.SecurityGroup
  1100  	switch e.Config().FirewallMode() {
  1101  	case config.FwInstance:
  1102  		machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil)
  1103  	case config.FwGlobal:
  1104  		machineGroup, err = e.ensureGroup(e.globalGroupName(), nil)
  1105  	}
  1106  	if err != nil {
  1107  		return nil, err
  1108  	}
  1109  	groups := []nova.SecurityGroup{jujuGroup, machineGroup}
  1110  	if e.ecfg().useDefaultSecurityGroup() {
  1111  		defaultGroup, err := e.nova().SecurityGroupByName("default")
  1112  		if err != nil {
  1113  			return nil, fmt.Errorf("loading default security group: %v", err)
  1114  		}
  1115  		groups = append(groups, *defaultGroup)
  1116  	}
  1117  	return groups, nil
  1118  }
  1119  
  1120  // zeroGroup holds the zero security group.
  1121  var zeroGroup nova.SecurityGroup
  1122  
  1123  // ensureGroup returns the security group with name and perms.
  1124  // If a group with name does not exist, one will be created.
  1125  // If it exists, its permissions are set to perms.
  1126  func (e *environ) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) {
  1127  	novaClient := e.nova()
  1128  	// First attempt to look up an existing group by name.
  1129  	group, err := novaClient.SecurityGroupByName(name)
  1130  	if err == nil {
  1131  		// Group exists, so assume it is correctly set up and return it.
  1132  		// TODO(jam): 2013-09-18 http://pad.lv/121795
  1133  		// We really should verify the group is set up correctly,
  1134  		// because deleting and re-creating environments can get us bad
  1135  		// groups (especially if they were set up under Python)
  1136  		return *group, nil
  1137  	}
  1138  	// Doesn't exist, so try and create it.
  1139  	group, err = novaClient.CreateSecurityGroup(name, "juju group")
  1140  	if err != nil {
  1141  		if !gooseerrors.IsDuplicateValue(err) {
  1142  			return zeroGroup, err
  1143  		} else {
  1144  			// We just tried to create a duplicate group, so load the existing group.
  1145  			group, err = novaClient.SecurityGroupByName(name)
  1146  			if err != nil {
  1147  				return zeroGroup, err
  1148  			}
  1149  			return *group, nil
  1150  		}
  1151  	}
  1152  	// The new group is created so now add the rules.
  1153  	group.Rules = make([]nova.SecurityGroupRule, len(rules))
  1154  	for i, rule := range rules {
  1155  		rule.ParentGroupId = group.Id
  1156  		if rule.Cidr == "" {
  1157  			// http://pad.lv/1226996 Rules that don't have a CIDR
  1158  			// are meant to apply only to this group. If you don't
  1159  			// supply CIDR or GroupId then openstack assumes you
  1160  			// mean CIDR=0.0.0.0/0
  1161  			rule.GroupId = &group.Id
  1162  		}
  1163  		groupRule, err := novaClient.CreateSecurityGroupRule(rule)
  1164  		if err != nil && !gooseerrors.IsDuplicateValue(err) {
  1165  			return zeroGroup, err
  1166  		}
  1167  		group.Rules[i] = *groupRule
  1168  	}
  1169  	return *group, nil
  1170  }
  1171  
  1172  func (e *environ) terminateInstances(ids []instance.Id) error {
  1173  	if len(ids) == 0 {
  1174  		return nil
  1175  	}
  1176  	var firstErr error
  1177  	novaClient := e.nova()
  1178  	for _, id := range ids {
  1179  		err := novaClient.DeleteServer(string(id))
  1180  		if gooseerrors.IsNotFound(err) {
  1181  			err = nil
  1182  		}
  1183  		if err != nil && firstErr == nil {
  1184  			logger.Debugf("error terminating instance %q: %v", id, err)
  1185  			firstErr = err
  1186  		}
  1187  	}
  1188  	return firstErr
  1189  }
  1190  
  1191  // MetadataLookupParams returns parameters which are used to query simplestreams metadata.
  1192  func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) {
  1193  	if region == "" {
  1194  		region = e.ecfg().region()
  1195  	}
  1196  	return &simplestreams.MetadataLookupParams{
  1197  		Series:        e.ecfg().DefaultSeries(),
  1198  		Region:        region,
  1199  		Endpoint:      e.ecfg().authURL(),
  1200  		Architectures: []string{"amd64", "arm", "arm64", "ppc64"},
  1201  	}, nil
  1202  }
  1203  
  1204  // Region is specified in the HasRegion interface.
  1205  func (e *environ) Region() (simplestreams.CloudSpec, error) {
  1206  	return simplestreams.CloudSpec{
  1207  		Region:   e.ecfg().region(),
  1208  		Endpoint: e.ecfg().authURL(),
  1209  	}, nil
  1210  }