github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/cmd/juju/commands/bootstrap.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils"
    14  	"github.com/juju/utils/featureflag"
    15  	"github.com/juju/version"
    16  	"gopkg.in/juju/charm.v6-unstable"
    17  	"launchpad.net/gnuflag"
    18  
    19  	jujucloud "github.com/juju/juju/cloud"
    20  	"github.com/juju/juju/cmd/juju/common"
    21  	"github.com/juju/juju/cmd/modelcmd"
    22  	"github.com/juju/juju/constraints"
    23  	"github.com/juju/juju/environs"
    24  	"github.com/juju/juju/environs/bootstrap"
    25  	"github.com/juju/juju/environs/config"
    26  	"github.com/juju/juju/environs/sync"
    27  	"github.com/juju/juju/feature"
    28  	"github.com/juju/juju/instance"
    29  	"github.com/juju/juju/juju/osenv"
    30  	"github.com/juju/juju/jujuclient"
    31  	jujuversion "github.com/juju/juju/version"
    32  )
    33  
    34  // provisionalProviders is the names of providers that are hidden behind
    35  // feature flags.
    36  var provisionalProviders = map[string]string{
    37  	"vsphere": feature.VSphereProvider,
    38  }
    39  
    40  var usageBootstrapSummary = `
    41  Initializes a cloud environment.`[1:]
    42  
    43  var usageBootstrapDetails = `
    44  Initialization consists of creating an 'admin' model and provisioning a 
    45  machine to act as controller.
    46  Credentials are set beforehand and are distinct from any other 
    47  configuration (see `[1:] + "`juju add-credential`" + `).
    48  The 'admin' model typically does not run workloads. It should remain
    49  pristine to run and manage Juju's own infrastructure for the corresponding
    50  cloud. Additional (hosted) models should be created with ` + "`juju create-\nmodel`" + ` for workload purposes.
    51  Note that a 'default' model is also created and becomes the current model
    52  of the environment once the command completes. It can be discarded if
    53  other models are created.
    54  If '--bootstrap-constraints' is used, its values will also apply to any
    55  future controllers provisioned for high availability (HA).
    56  If '--constraints' is used, its values will be set as the default 
    57  constraints for all future workload machines in the model, exactly as if 
    58  the constraints were set with ` + "`juju set-model-constraints`" + `.
    59  It is possible to override constraints and the automatic machine selection
    60  algorithm by assigning a "placement directive" via the '--to' option. This
    61  dictates what machine to use for the controller. This would typically be 
    62  used with the MAAS provider ('--to <host>.maas').
    63  You can change the default timeout and retry delays used during the 
    64  bootstrap by changing the following settings in your configuration file
    65  (all values represent number of seconds):
    66      # How long to wait for a connection to the controller
    67      bootstrap-timeout: 600 # default: 10 minutes
    68      # How long to wait between connection attempts to a controller 
    69  address.
    70      bootstrap-retry-delay: 5 # default: 5 seconds
    71      # How often to refresh controller addresses from the API server.
    72      bootstrap-addresses-delay: 10 # default: 10 seconds
    73  Private clouds may need to specify their own custom image metadata and
    74  tools/agent. Use '--metadata-source' whose value is a local directory.
    75  The value of '--agent-version' will become the default tools version to
    76  use in all models for this controller. The full binary version is accepted
    77  (e.g.: 2.0.1-xenial-amd64) but only the numeric version (e.g.: 2.0.1) is
    78  used. Otherwise, by default, the version used is that of the client.
    79  
    80  Examples:
    81      juju bootstrap mycontroller google
    82      juju bootstrap --config=~/config-rs.yaml mycontroller rackspace
    83      juju bootstrap --config agent-version=1.25.3 mycontroller aws
    84      juju bootstrap --config bootstrap-timeout=1200 mycontroller azure
    85  
    86  See also: 
    87      add-credentials
    88      add-model
    89      set-constraints`
    90  
    91  // defaultHostedModelName is the name of the hosted model created in each
    92  // controller for deploying workloads to, in addition to the "admin" model.
    93  const defaultHostedModelName = "default"
    94  
    95  func newBootstrapCommand() cmd.Command {
    96  	return modelcmd.Wrap(
    97  		&bootstrapCommand{},
    98  		modelcmd.ModelSkipFlags, modelcmd.ModelSkipDefault,
    99  	)
   100  }
   101  
   102  // bootstrapCommand is responsible for launching the first machine in a juju
   103  // environment, and setting up everything necessary to continue working.
   104  type bootstrapCommand struct {
   105  	modelcmd.ModelCommandBase
   106  
   107  	Constraints           constraints.Value
   108  	BootstrapConstraints  constraints.Value
   109  	BootstrapSeries       string
   110  	BootstrapImage        string
   111  	UploadTools           bool
   112  	MetadataSource        string
   113  	Placement             string
   114  	KeepBrokenEnvironment bool
   115  	AutoUpgrade           bool
   116  	AgentVersionParam     string
   117  	AgentVersion          *version.Number
   118  	config                common.ConfigFlag
   119  
   120  	controllerName  string
   121  	hostedModelName string
   122  	CredentialName  string
   123  	Cloud           string
   124  	Region          string
   125  	noGUI           bool
   126  }
   127  
   128  func (c *bootstrapCommand) Info() *cmd.Info {
   129  	return &cmd.Info{
   130  		Name:    "bootstrap",
   131  		Args:    "<controller name> <cloud name>[/region]",
   132  		Purpose: usageBootstrapSummary,
   133  		Doc:     usageBootstrapDetails,
   134  	}
   135  }
   136  
   137  func (c *bootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
   138  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "Set model constraints")
   139  	f.Var(constraints.ConstraintsValue{Target: &c.BootstrapConstraints}, "bootstrap-constraints", "Specify bootstrap machine constraints")
   140  	f.StringVar(&c.BootstrapSeries, "bootstrap-series", "", "Specify the series of the bootstrap machine")
   141  	if featureflag.Enabled(feature.ImageMetadata) {
   142  		f.StringVar(&c.BootstrapImage, "bootstrap-image", "", "Specify the image of the bootstrap machine")
   143  	}
   144  	f.BoolVar(&c.UploadTools, "upload-tools", false, "Upload local version of tools before bootstrapping")
   145  	f.StringVar(&c.MetadataSource, "metadata-source", "", "Local path to use as tools and/or metadata source")
   146  	f.StringVar(&c.Placement, "to", "", "Placement directive indicating an instance to bootstrap")
   147  	f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "Do not destroy the model if bootstrap fails")
   148  	f.BoolVar(&c.AutoUpgrade, "auto-upgrade", false, "Upgrade to the latest patch release tools on first bootstrap")
   149  	f.StringVar(&c.AgentVersionParam, "agent-version", "", "Version of tools to use for Juju agents")
   150  	f.StringVar(&c.CredentialName, "credential", "", "Credentials to use when bootstrapping")
   151  	f.Var(&c.config, "config", "Specify a controller configuration file, or one or more configuration\n    options\n    (--config config.yaml [--config key=value ...])")
   152  	f.StringVar(&c.hostedModelName, "d", defaultHostedModelName, "Name of the default hosted model for the controller")
   153  	f.StringVar(&c.hostedModelName, "default-model", defaultHostedModelName, "Name of the default hosted model for the controller")
   154  	f.BoolVar(&c.noGUI, "no-gui", false, "Do not install the Juju GUI in the controller when bootstrapping")
   155  }
   156  
   157  func (c *bootstrapCommand) Init(args []string) (err error) {
   158  	if c.AgentVersionParam != "" && c.UploadTools {
   159  		return fmt.Errorf("--agent-version and --upload-tools can't be used together")
   160  	}
   161  	if c.BootstrapSeries != "" && !charm.IsValidSeries(c.BootstrapSeries) {
   162  		return errors.NotValidf("series %q", c.BootstrapSeries)
   163  	}
   164  	if c.BootstrapImage != "" {
   165  		if c.BootstrapSeries == "" {
   166  			return errors.Errorf("--bootstrap-image must be used with --bootstrap-series")
   167  		}
   168  		cons, err := constraints.Merge(c.Constraints, c.BootstrapConstraints)
   169  		if err != nil {
   170  			return errors.Trace(err)
   171  		}
   172  		if !cons.HasArch() {
   173  			return errors.Errorf("--bootstrap-image must be used with --bootstrap-constraints, specifying architecture")
   174  		}
   175  	}
   176  
   177  	// Parse the placement directive. Bootstrap currently only
   178  	// supports provider-specific placement directives.
   179  	if c.Placement != "" {
   180  		_, err = instance.ParsePlacement(c.Placement)
   181  		if err != instance.ErrPlacementScopeMissing {
   182  			// We only support unscoped placement directives for bootstrap.
   183  			return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement)
   184  		}
   185  	}
   186  	if !c.AutoUpgrade {
   187  		// With no auto upgrade chosen, we default to the version matching the bootstrap client.
   188  		vers := jujuversion.Current
   189  		c.AgentVersion = &vers
   190  	}
   191  	if c.AgentVersionParam != "" {
   192  		if vers, err := version.ParseBinary(c.AgentVersionParam); err == nil {
   193  			c.AgentVersion = &vers.Number
   194  		} else if vers, err := version.Parse(c.AgentVersionParam); err == nil {
   195  			c.AgentVersion = &vers
   196  		} else {
   197  			return err
   198  		}
   199  	}
   200  	if c.AgentVersion != nil && (c.AgentVersion.Major != jujuversion.Current.Major || c.AgentVersion.Minor != jujuversion.Current.Minor) {
   201  		return fmt.Errorf("requested agent version major.minor mismatch")
   202  	}
   203  
   204  	// The user must specify two positional arguments: the controller name,
   205  	// and the cloud name (optionally with region specified).
   206  	if len(args) < 2 {
   207  		return errors.New("controller name and cloud name are required")
   208  	}
   209  	c.controllerName = bootstrappedControllerName(args[0])
   210  	c.Cloud = args[1]
   211  	if i := strings.IndexRune(c.Cloud, '/'); i > 0 {
   212  		c.Cloud, c.Region = c.Cloud[:i], c.Cloud[i+1:]
   213  	}
   214  	return cmd.CheckEmpty(args[2:])
   215  }
   216  
   217  var bootstrappedControllerName = func(controllerName string) string {
   218  	return fmt.Sprintf("local.%s", controllerName)
   219  }
   220  
   221  // BootstrapInterface provides bootstrap functionality that Run calls to support cleaner testing.
   222  type BootstrapInterface interface {
   223  	Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error
   224  }
   225  
   226  type bootstrapFuncs struct{}
   227  
   228  func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error {
   229  	return bootstrap.Bootstrap(ctx, env, args)
   230  }
   231  
   232  var getBootstrapFuncs = func() BootstrapInterface {
   233  	return &bootstrapFuncs{}
   234  }
   235  
   236  var (
   237  	environsPrepare            = environs.Prepare
   238  	environsDestroy            = environs.Destroy
   239  	waitForAgentInitialisation = common.WaitForAgentInitialisation
   240  )
   241  
   242  var ambiguousCredentialError = errors.New(`
   243  more than one credential detected
   244  run juju autoload-credentials and specify a credential using the --credential argument`[1:],
   245  )
   246  
   247  // Run connects to the environment specified on the command line and bootstraps
   248  // a juju in that environment if none already exists. If there is as yet no environments.yaml file,
   249  // the user is informed how to create one.
   250  func (c *bootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
   251  	bootstrapFuncs := getBootstrapFuncs()
   252  
   253  	// Get the cloud definition identified by c.Cloud. If c.Cloud does not
   254  	// identify a cloud in clouds.yaml, but is the name of a provider, and
   255  	// that provider implements environs.CloudRegionDetector, we'll
   256  	// synthesise a Cloud structure with the detected regions and no auth-
   257  	// types.
   258  	cloud, err := jujucloud.CloudByName(c.Cloud)
   259  	if errors.IsNotFound(err) {
   260  		ctx.Verbosef("cloud %q not found, trying as a provider name", c.Cloud)
   261  		provider, err := environs.Provider(c.Cloud)
   262  		if errors.IsNotFound(err) {
   263  			return errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", c.Cloud, "juju update-clouds"))
   264  		} else if err != nil {
   265  			return errors.Trace(err)
   266  		}
   267  		detector, ok := provider.(environs.CloudRegionDetector)
   268  		if !ok {
   269  			ctx.Verbosef(
   270  				"provider %q does not support detecting regions",
   271  				c.Cloud,
   272  			)
   273  			return errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", c.Cloud, "juju update-clouds"))
   274  		}
   275  		regions, err := detector.DetectRegions()
   276  		if err != nil && !errors.IsNotFound(err) {
   277  			// It's not an error to have no regions.
   278  			return errors.Annotatef(err,
   279  				"detecting regions for %q cloud provider",
   280  				c.Cloud,
   281  			)
   282  		}
   283  		cloud = &jujucloud.Cloud{
   284  			Type:    c.Cloud,
   285  			Regions: regions,
   286  		}
   287  	} else if err != nil {
   288  		return errors.Trace(err)
   289  	}
   290  	if err := checkProviderType(cloud.Type); errors.IsNotFound(err) {
   291  		// This error will get handled later.
   292  	} else if err != nil {
   293  		return errors.Trace(err)
   294  	}
   295  
   296  	// Get the credentials and region name.
   297  	store := c.ClientStore()
   298  	credential, credentialName, regionName, err := modelcmd.GetCredentials(
   299  		store, c.Region, c.CredentialName, c.Cloud, cloud.Type,
   300  	)
   301  	if errors.IsNotFound(err) && c.CredentialName == "" {
   302  		// No credential was explicitly specified, and no credential
   303  		// was found in credentials.yaml; have the provider detect
   304  		// credentials from the environment.
   305  		ctx.Verbosef("no credentials found, checking environment")
   306  		detected, err := modelcmd.DetectCredential(c.Cloud, cloud.Type)
   307  		if errors.Cause(err) == modelcmd.ErrMultipleCredentials {
   308  			return ambiguousCredentialError
   309  		} else if err != nil {
   310  			return errors.Trace(err)
   311  		}
   312  		// We have one credential so extract it from the map.
   313  		var oneCredential jujucloud.Credential
   314  		for _, oneCredential = range detected.AuthCredentials {
   315  		}
   316  		credential = &oneCredential
   317  		regionName = c.Region
   318  		if regionName == "" {
   319  			regionName = detected.DefaultRegion
   320  		}
   321  		logger.Tracef("authenticating with region %q and %v", regionName, credential)
   322  	} else if err != nil {
   323  		return errors.Trace(err)
   324  	}
   325  
   326  	region, err := getRegion(cloud, c.Cloud, regionName)
   327  	if err != nil {
   328  		return errors.Trace(err)
   329  	}
   330  
   331  	hostedModelUUID, err := utils.NewUUID()
   332  	if err != nil {
   333  		return errors.Trace(err)
   334  	}
   335  	controllerUUID, err := utils.NewUUID()
   336  	if err != nil {
   337  		return errors.Trace(err)
   338  	}
   339  
   340  	// Create an environment config from the cloud and credentials.
   341  	configAttrs := map[string]interface{}{
   342  		"type":                   cloud.Type,
   343  		"name":                   environs.ControllerModelName,
   344  		config.UUIDKey:           controllerUUID.String(),
   345  		config.ControllerUUIDKey: controllerUUID.String(),
   346  	}
   347  	userConfigAttrs, err := c.config.ReadAttrs(ctx)
   348  	if err != nil {
   349  		return errors.Trace(err)
   350  	}
   351  	for k, v := range userConfigAttrs {
   352  		configAttrs[k] = v
   353  	}
   354  	logger.Debugf("preparing controller with config: %v", configAttrs)
   355  
   356  	// Read existing current controller, account, model so we can clean up on error.
   357  	var oldCurrentController string
   358  	oldCurrentController, err = modelcmd.ReadCurrentController()
   359  	if err != nil {
   360  		return errors.Annotate(err, "error reading current controller")
   361  	}
   362  
   363  	defer func() {
   364  		if resultErr == nil || errors.IsAlreadyExists(resultErr) {
   365  			return
   366  		}
   367  		if oldCurrentController != "" {
   368  			if err := modelcmd.WriteCurrentController(oldCurrentController); err != nil {
   369  				logger.Warningf(
   370  					"cannot reset current controller to %q: %v",
   371  					oldCurrentController, err,
   372  				)
   373  			}
   374  		}
   375  		if err := store.RemoveController(c.controllerName); err != nil {
   376  			logger.Warningf(
   377  				"cannot destroy newly created controller %q details: %v",
   378  				c.controllerName, err,
   379  			)
   380  		}
   381  	}()
   382  
   383  	environ, err := environsPrepare(
   384  		modelcmd.BootstrapContext(ctx), store,
   385  		environs.PrepareParams{
   386  			BaseConfig:           configAttrs,
   387  			ControllerName:       c.controllerName,
   388  			CloudName:            c.Cloud,
   389  			CloudRegion:          region.Name,
   390  			CloudEndpoint:        region.Endpoint,
   391  			CloudStorageEndpoint: region.StorageEndpoint,
   392  			Credential:           *credential,
   393  			CredentialName:       credentialName,
   394  		},
   395  	)
   396  	if err != nil {
   397  		return errors.Trace(err)
   398  	}
   399  
   400  	// Set the current model to the initial hosted model.
   401  	accountName, err := store.CurrentAccount(c.controllerName)
   402  	if err != nil {
   403  		return errors.Trace(err)
   404  	}
   405  	if err := store.UpdateModel(c.controllerName, accountName, c.hostedModelName, jujuclient.ModelDetails{
   406  		hostedModelUUID.String(),
   407  	}); err != nil {
   408  		return errors.Trace(err)
   409  	}
   410  	if err := store.SetCurrentModel(c.controllerName, accountName, c.hostedModelName); err != nil {
   411  		return errors.Trace(err)
   412  	}
   413  
   414  	// Set the current controller so "juju status" can be run while
   415  	// bootstrapping is underway.
   416  	if err := modelcmd.WriteCurrentController(c.controllerName); err != nil {
   417  		return errors.Trace(err)
   418  	}
   419  
   420  	cloudRegion := c.Cloud
   421  	if region.Name != "" {
   422  		cloudRegion = fmt.Sprintf("%s/%s", cloudRegion, region.Name)
   423  	}
   424  	ctx.Infof(
   425  		"Creating Juju controller %q on %s",
   426  		c.controllerName, cloudRegion,
   427  	)
   428  
   429  	// If we error out for any reason, clean up the environment.
   430  	defer func() {
   431  		if resultErr != nil {
   432  			if c.KeepBrokenEnvironment {
   433  				logger.Warningf(`
   434  bootstrap failed but --keep-broken was specified so model is not being destroyed.
   435  When you are finished diagnosing the problem, remember to run juju destroy-model --force
   436  to clean up the model.`[1:])
   437  			} else {
   438  				handleBootstrapError(ctx, resultErr, func() error {
   439  					return environsDestroy(
   440  						c.controllerName, environ, store,
   441  					)
   442  				})
   443  			}
   444  		}
   445  	}()
   446  
   447  	// Block interruption during bootstrap. Providers may also
   448  	// register for interrupt notification so they can exit early.
   449  	interrupted := make(chan os.Signal, 1)
   450  	defer close(interrupted)
   451  	ctx.InterruptNotify(interrupted)
   452  	defer ctx.StopInterruptNotify(interrupted)
   453  	go func() {
   454  		for _ = range interrupted {
   455  			ctx.Infof("Interrupt signalled: waiting for bootstrap to exit")
   456  		}
   457  	}()
   458  
   459  	// If --metadata-source is specified, override the default tools metadata source so
   460  	// SyncTools can use it, and also upload any image metadata.
   461  	var metadataDir string
   462  	if c.MetadataSource != "" {
   463  		metadataDir = ctx.AbsPath(c.MetadataSource)
   464  	}
   465  
   466  	// Merge environ and bootstrap-specific constraints.
   467  	constraintsValidator, err := environ.ConstraintsValidator()
   468  	if err != nil {
   469  		return errors.Trace(err)
   470  	}
   471  	bootstrapConstraints, err := constraintsValidator.Merge(
   472  		c.Constraints, c.BootstrapConstraints,
   473  	)
   474  	if err != nil {
   475  		return errors.Trace(err)
   476  	}
   477  	logger.Infof("combined bootstrap constraints: %v", bootstrapConstraints)
   478  
   479  	hostedModelConfig := map[string]interface{}{
   480  		"name":         c.hostedModelName,
   481  		config.UUIDKey: hostedModelUUID.String(),
   482  	}
   483  
   484  	// We copy across any user supplied attributes to the hosted model config.
   485  	// But only if the attributes have not been removed from the controller
   486  	// model config as part of preparing the controller model.
   487  	controllerConfigAttrs := environ.Config().AllAttrs()
   488  	for k, v := range userConfigAttrs {
   489  		if _, ok := controllerConfigAttrs[k]; ok {
   490  			hostedModelConfig[k] = v
   491  		}
   492  	}
   493  	// Ensure that certain config attributes are not included in the hosted
   494  	// model config. These attributes may be modified during bootstrap; by
   495  	// removing them from this map, we ensure the modified values are
   496  	// inherited.
   497  	delete(hostedModelConfig, config.AuthKeysConfig)
   498  	delete(hostedModelConfig, config.AgentVersionKey)
   499  
   500  	// Check whether the Juju GUI must be installed in the controller.
   501  	// Leaving this value empty means no GUI will be installed.
   502  	var guiDataSourceBaseURL string
   503  	if !c.noGUI {
   504  		guiDataSourceBaseURL = common.GUIDataSourceBaseURL()
   505  	}
   506  
   507  	err = bootstrapFuncs.Bootstrap(modelcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{
   508  		ModelConstraints:     c.Constraints,
   509  		BootstrapConstraints: bootstrapConstraints,
   510  		BootstrapSeries:      c.BootstrapSeries,
   511  		BootstrapImage:       c.BootstrapImage,
   512  		Placement:            c.Placement,
   513  		UploadTools:          c.UploadTools,
   514  		BuildToolsTarball:    sync.BuildToolsTarball,
   515  		AgentVersion:         c.AgentVersion,
   516  		MetadataDir:          metadataDir,
   517  		HostedModelConfig:    hostedModelConfig,
   518  		GUIDataSourceBaseURL: guiDataSourceBaseURL,
   519  	})
   520  	if err != nil {
   521  		return errors.Annotate(err, "failed to bootstrap model")
   522  	}
   523  
   524  	if err := c.SetModelName(c.hostedModelName); err != nil {
   525  		return errors.Trace(err)
   526  	}
   527  
   528  	err = common.SetBootstrapEndpointAddress(c.ClientStore(), c.controllerName, environ)
   529  	if err != nil {
   530  		return errors.Annotate(err, "saving bootstrap endpoint address")
   531  	}
   532  
   533  	// To avoid race conditions when running scripted bootstraps, wait
   534  	// for the controller's machine agent to be ready to accept commands
   535  	// before exiting this bootstrap command.
   536  	return waitForAgentInitialisation(ctx, &c.ModelCommandBase, c.controllerName)
   537  }
   538  
   539  // getRegion returns the cloud.Region to use, based on the specified
   540  // region name, and the region name selected if none was specified.
   541  //
   542  // If no region name is specified, and there is at least one region,
   543  // we use the first region in the list.
   544  //
   545  // If no region name is specified, and there are no regions at all,
   546  // then we synthesise a region from the cloud's endpoint information
   547  // and just pass this on to the provider.
   548  func getRegion(cloud *jujucloud.Cloud, cloudName, regionName string) (jujucloud.Region, error) {
   549  	if len(cloud.Regions) == 0 {
   550  		// The cloud does not specify regions, so assume
   551  		// that the cloud provider does not have a concept
   552  		// of regions, or has no pre-defined regions, and
   553  		// defer validation to the provider.
   554  		region := jujucloud.Region{
   555  			regionName,
   556  			cloud.Endpoint,
   557  			cloud.StorageEndpoint,
   558  		}
   559  		return region, nil
   560  	}
   561  	if regionName == "" {
   562  		// No region was specified, use the first region in the list.
   563  		return cloud.Regions[0], nil
   564  	}
   565  	for _, region := range cloud.Regions {
   566  		// Do a case-insensitive comparison
   567  		if strings.EqualFold(region.Name, regionName) {
   568  			return region, nil
   569  		}
   570  	}
   571  	return jujucloud.Region{}, errors.NewNotFound(nil, fmt.Sprintf(
   572  		"region %q in cloud %q not found (expected one of %q)\nalternatively, try %q",
   573  		regionName, cloudName, cloudRegionNames(cloud), "juju update-clouds",
   574  	))
   575  }
   576  
   577  func cloudRegionNames(cloud *jujucloud.Cloud) []string {
   578  	var regionNames []string
   579  	for _, region := range cloud.Regions {
   580  		regionNames = append(regionNames, region.Name)
   581  	}
   582  	return regionNames
   583  }
   584  
   585  // checkProviderType ensures the provider type is okay.
   586  func checkProviderType(envType string) error {
   587  	featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey)
   588  	flag, ok := provisionalProviders[envType]
   589  	if ok && !featureflag.Enabled(flag) {
   590  		msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell model`
   591  		return errors.Errorf(msg, envType, flag)
   592  	}
   593  	return nil
   594  }
   595  
   596  // handleBootstrapError is called to clean up if bootstrap fails.
   597  func handleBootstrapError(ctx *cmd.Context, err error, cleanup func() error) {
   598  	ch := make(chan os.Signal, 1)
   599  	ctx.InterruptNotify(ch)
   600  	defer ctx.StopInterruptNotify(ch)
   601  	defer close(ch)
   602  	go func() {
   603  		for _ = range ch {
   604  			fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap")
   605  		}
   606  	}()
   607  	if err := cleanup(); err != nil {
   608  		logger.Errorf("error cleaning up: %v", err)
   609  	}
   610  }