github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/cloud/add.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package cloud
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  	"github.com/juju/utils"
    16  	"github.com/juju/utils/cert"
    17  	"gopkg.in/juju/names.v2"
    18  	"gopkg.in/yaml.v2"
    19  
    20  	cloudapi "github.com/juju/juju/api/cloud"
    21  	"github.com/juju/juju/apiserver/params"
    22  	jujucloud "github.com/juju/juju/cloud"
    23  	jujucmd "github.com/juju/juju/cmd"
    24  	"github.com/juju/juju/cmd/juju/common"
    25  	"github.com/juju/juju/cmd/juju/interact"
    26  	"github.com/juju/juju/cmd/modelcmd"
    27  	"github.com/juju/juju/environs"
    28  	"github.com/juju/juju/environs/context"
    29  	"github.com/juju/juju/jujuclient"
    30  )
    31  
    32  type CloudMetadataStore interface {
    33  	ParseCloudMetadataFile(path string) (map[string]jujucloud.Cloud, error)
    34  	ParseOneCloud(data []byte) (jujucloud.Cloud, error)
    35  	PublicCloudMetadata(searchPaths ...string) (result map[string]jujucloud.Cloud, fallbackUsed bool, _ error)
    36  	PersonalCloudMetadata() (map[string]jujucloud.Cloud, error)
    37  	WritePersonalCloudMetadata(cloudsMap map[string]jujucloud.Cloud) error
    38  }
    39  
    40  var usageAddCloudSummary = `
    41  Adds a cloud definition to Juju.`[1:]
    42  
    43  var usageAddCloudDetails = `
    44  Juju needs to know how to connect to clouds. A cloud definition 
    45  describes a cloud's endpoints and authentication requirements. Each
    46  definition is stored and accessed later as <cloud name>.
    47  
    48  If you are accessing a public cloud, running add-cloud unlikely to be 
    49  necessary.  Juju already contains definitions for the public cloud 
    50  providers it supports.
    51  
    52  add-cloud operates in two modes:
    53  
    54      juju add-cloud
    55      juju add-cloud <cloud name> <cloud definition file>
    56  
    57  When invoked without arguments, add-cloud begins an interactive session
    58  designed for working with private clouds.  The session will enable you 
    59  to instruct Juju how to connect to your private cloud.
    60  
    61  When <cloud definition file> is provided with <cloud name>, 
    62  Juju stores that definition its internal cache directly after 
    63  validating the contents.
    64  
    65  If <cloud name> already exists in Juju's cache, then the `[1:] + "`--replace`" + ` 
    66  option is required.
    67  
    68  A cloud definition file has the following YAML format:
    69  
    70  clouds:                           # mandatory
    71    mycloud:                        # <cloud name> argument
    72      type: openstack               # <cloud type>, see below
    73      auth-types: [ userpass ]
    74      regions:
    75        london:
    76          endpoint: https://london.mycloud.com:35574/v3.0/
    77  
    78  <cloud types> for private clouds: 
    79   - lxd
    80   - maas
    81   - manual
    82   - openstack
    83   - vsphere
    84  
    85  <cloud types> for public clouds:
    86   - azure
    87   - cloudsigma
    88   - ec2
    89   - gce
    90   - joyent
    91   - oci
    92  
    93  If you do not supply a controller name, only the local Juju cache
    94  is updated. If you want to update a running controller to upload a
    95  new, additional cloud, you can use the --controller or -c option.
    96  The cloud details will be uploaded to the controller, along with
    97  a credential for the cloud. As with the cloud, the credential needs
    98  to have been added to the local Juju cache; add-credential is used to
    99  do that. If there's only one credential for the cloud it will be
   100  uploaded to the controller. If the cloud has multiple local credentials
   101  you can specify which to upload with the --credential option.
   102  
   103  Examples:
   104      juju add-cloud
   105      juju add-cloud mycloud ~/mycloud.yaml
   106      juju add-cloud --replace mycloud ~/mycloud2.yaml
   107      juju add-cloud --controller mycontroller mycloud
   108      juju add-cloud --controller mycontroller mycloud --credential mycred
   109  
   110  See also: 
   111      clouds`
   112  
   113  // AddCloudAPI - Implemented by cloudapi.Client.
   114  type AddCloudAPI interface {
   115  	AddCloud(jujucloud.Cloud) error
   116  	AddCredential(tag string, credential jujucloud.Credential) error
   117  	Close() error
   118  }
   119  
   120  // AddCloudCommand is the command that allows you to add a cloud configuration
   121  // for use with juju bootstrap.
   122  type AddCloudCommand struct {
   123  	modelcmd.CommandBase
   124  
   125  	// Replace, if true, existing cloud information is overwritten.
   126  	Replace bool
   127  
   128  	// Cloud is the name fo the cloud to add.
   129  	Cloud string
   130  
   131  	// CloudFile is the name of the cloud YAML file.
   132  	CloudFile string
   133  
   134  	// Ping contains the logic for pinging a cloud endpoint to know whether or
   135  	// not it really has a valid cloud of the same type as the provider.  By
   136  	// default it just calls the correct provider's Ping method.
   137  	Ping func(p environs.EnvironProvider, endpoint string) error
   138  
   139  	// CloudCallCtx contains context to be used for any cloud calls.
   140  	CloudCallCtx       *context.CloudCallContext
   141  	cloudMetadataStore CloudMetadataStore
   142  
   143  	// These attributes are used when adding a cloud to a controller.
   144  	controllerName  string
   145  	credentialName  string
   146  	store           jujuclient.ClientStore
   147  	addCloudAPIFunc func() (AddCloudAPI, error)
   148  }
   149  
   150  // NewAddCloudCommand returns a command to add cloud information.
   151  func NewAddCloudCommand(cloudMetadataStore CloudMetadataStore) cmd.Command {
   152  	cloudCallCtx := context.NewCloudCallContext()
   153  	c := &AddCloudCommand{
   154  		cloudMetadataStore: cloudMetadataStore,
   155  		CloudCallCtx:       cloudCallCtx,
   156  		// Ping is provider.Ping except in tests where we don't actually want to
   157  		// require a valid cloud.
   158  		Ping: func(p environs.EnvironProvider, endpoint string) error {
   159  			return p.Ping(cloudCallCtx, endpoint)
   160  		},
   161  		store: jujuclient.NewFileClientStore(),
   162  	}
   163  	c.addCloudAPIFunc = c.cloudAPI
   164  	return modelcmd.WrapBase(c)
   165  }
   166  
   167  func (c *AddCloudCommand) cloudAPI() (AddCloudAPI, error) {
   168  	root, err := c.NewAPIRoot(c.store, c.controllerName, "")
   169  	if err != nil {
   170  		return nil, errors.Trace(err)
   171  	}
   172  	return cloudapi.NewClient(root), nil
   173  }
   174  
   175  // Info returns help information about the command.
   176  func (c *AddCloudCommand) Info() *cmd.Info {
   177  	return jujucmd.Info(&cmd.Info{
   178  		Name:    "add-cloud",
   179  		Args:    "<cloud name> [<cloud definition file>]",
   180  		Purpose: usageAddCloudSummary,
   181  		Doc:     usageAddCloudDetails,
   182  	})
   183  }
   184  
   185  // SetFlags initializes the flags supported by the command.
   186  func (c *AddCloudCommand) SetFlags(f *gnuflag.FlagSet) {
   187  	c.CommandBase.SetFlags(f)
   188  	f.BoolVar(&c.Replace, "replace", false, "Overwrite any existing cloud information for <cloud name>")
   189  	f.StringVar(&c.CloudFile, "f", "", "The path to a cloud definition file")
   190  	f.StringVar(&c.credentialName, "credential", "", "Credential to use for new cloud")
   191  	f.StringVar(&c.controllerName, "c", "", "Controller to operate in")
   192  	f.StringVar(&c.controllerName, "controller", "", "")
   193  }
   194  
   195  // Init populates the command with the args from the command line.
   196  func (c *AddCloudCommand) Init(args []string) (err error) {
   197  	if len(args) > 0 {
   198  		c.Cloud = args[0]
   199  		if ok := names.IsValidCloud(c.Cloud); !ok {
   200  			return errors.NotValidf("cloud name %q", c.Cloud)
   201  		}
   202  	}
   203  	if len(args) > 1 {
   204  		if c.CloudFile != args[1] && c.CloudFile != "" {
   205  			return errors.BadRequestf("cannot specify cloud file with option and argument")
   206  		}
   207  		c.CloudFile = args[1]
   208  	}
   209  	if len(args) > 2 {
   210  		return cmd.CheckEmpty(args[2:])
   211  	}
   212  	return nil
   213  }
   214  
   215  var ambiguousCredentialError = errors.New(`
   216  more than one credential is available
   217  specify a credential using the --credential argument`[1:],
   218  )
   219  
   220  func (c *AddCloudCommand) findLocalCredential(ctx *cmd.Context, cloud jujucloud.Cloud, credentialName string) (*jujucloud.Credential, string, error) {
   221  	credential, chosenCredentialName, _, err := modelcmd.GetCredentials(ctx, c.store, modelcmd.GetCredentialsParams{
   222  		Cloud:          cloud,
   223  		CredentialName: credentialName,
   224  	})
   225  	if err == nil {
   226  		return credential, chosenCredentialName, nil
   227  	}
   228  
   229  	switch errors.Cause(err) {
   230  	case modelcmd.ErrMultipleCredentials:
   231  		return nil, "", ambiguousCredentialError
   232  	}
   233  	return nil, "", errors.Trace(err)
   234  }
   235  
   236  func (c *AddCloudCommand) addCredentialToController(ctx *cmd.Context, cloud jujucloud.Cloud, apiClient AddCloudAPI) error {
   237  	_, err := c.store.ControllerByName(c.controllerName)
   238  	if err != nil {
   239  		return errors.Trace(err)
   240  	}
   241  
   242  	currentAccountDetails, err := c.store.AccountDetails(c.controllerName)
   243  	if err != nil {
   244  		return errors.Trace(err)
   245  	}
   246  
   247  	cred, credentialName, err := c.findLocalCredential(ctx, cloud, c.credentialName)
   248  	if err != nil {
   249  		return errors.Trace(err)
   250  	}
   251  
   252  	cloudCredTag := names.NewCloudCredentialTag(fmt.Sprintf("%s/%s/%s",
   253  		c.Cloud, currentAccountDetails.User, credentialName))
   254  
   255  	if err := apiClient.AddCredential(cloudCredTag.String(), *cred); err != nil {
   256  		return errors.Trace(err)
   257  	}
   258  	return nil
   259  }
   260  
   261  // Run executes the add cloud command, adding a cloud based on a passed-in yaml
   262  // file or interactive queries.
   263  func (c *AddCloudCommand) Run(ctxt *cmd.Context) error {
   264  	if c.CloudFile == "" && c.controllerName == "" {
   265  		return c.runInteractive(ctxt)
   266  	}
   267  
   268  	var newCloud *jujucloud.Cloud
   269  	if c.CloudFile != "" {
   270  		var err error
   271  		newCloud, err = c.readCloudFromFile(ctxt)
   272  		if err != nil {
   273  			return errors.Trace(err)
   274  		}
   275  	} else {
   276  		// No cloud file specified so we try and use a named
   277  		// cloud that already has been added to the local cache.
   278  		details, err := listCloudDetails()
   279  		if err != nil {
   280  			return err
   281  		}
   282  		cloudDetails, ok := details.all()[c.Cloud]
   283  		if !ok {
   284  			return errors.NotFoundf("cloud %q", c.Cloud)
   285  		}
   286  		newCloud = &jujucloud.Cloud{
   287  			Name:             c.Cloud,
   288  			Type:             cloudDetails.CloudType,
   289  			Description:      cloudDetails.CloudDescription,
   290  			Endpoint:         cloudDetails.Endpoint,
   291  			IdentityEndpoint: cloudDetails.IdentityEndpoint,
   292  			StorageEndpoint:  cloudDetails.StorageEndpoint,
   293  			CACertificates:   cloudDetails.CACredentials,
   294  			Config:           cloudDetails.Config,
   295  			RegionConfig:     cloudDetails.RegionConfig,
   296  		}
   297  		for _, at := range cloudDetails.AuthTypes {
   298  			newCloud.AuthTypes = append(newCloud.AuthTypes, jujucloud.AuthType(at))
   299  		}
   300  		for name, r := range cloudDetails.RegionsMap {
   301  			newCloud.Regions = append(newCloud.Regions, jujucloud.Region{
   302  				Name:             name,
   303  				Endpoint:         r.Endpoint,
   304  				StorageEndpoint:  r.StorageEndpoint,
   305  				IdentityEndpoint: r.IdentityEndpoint,
   306  			})
   307  		}
   308  	}
   309  	if c.controllerName == "" {
   310  		return addLocalCloud(c.cloudMetadataStore, *newCloud)
   311  	}
   312  
   313  	// A controller has been specified so upload the cloud details
   314  	// plus a corresponding credential to the controller.
   315  	api, err := c.addCloudAPIFunc()
   316  	if err != nil {
   317  		return err
   318  	}
   319  	err = api.AddCloud(*newCloud)
   320  	if err != nil && params.ErrCode(err) != params.CodeAlreadyExists {
   321  		return err
   322  	}
   323  	// Add a credential for the newly added cloud.
   324  	return c.addCredentialToController(ctxt, *newCloud, api)
   325  }
   326  
   327  func (c *AddCloudCommand) readCloudFromFile(ctxt *cmd.Context) (*jujucloud.Cloud, error) {
   328  	specifiedClouds, err := c.cloudMetadataStore.ParseCloudMetadataFile(c.CloudFile)
   329  	if err != nil {
   330  		return nil, errors.Trace(err)
   331  	}
   332  	if specifiedClouds == nil {
   333  		return nil, errors.New("no personal clouds are defined")
   334  	}
   335  	newCloud, ok := specifiedClouds[c.Cloud]
   336  	if !ok {
   337  		return nil, errors.Errorf("cloud %q not found in file %q", c.Cloud, c.CloudFile)
   338  	}
   339  
   340  	// first validate cloud input
   341  	data, err := ioutil.ReadFile(c.CloudFile)
   342  	if err != nil {
   343  		return nil, errors.Trace(err)
   344  	}
   345  	if err = jujucloud.ValidateCloudSet(data); err != nil {
   346  		ctxt.Warningf(err.Error())
   347  	}
   348  
   349  	// validate cloud data
   350  	provider, err := environs.Provider(newCloud.Type)
   351  	if err != nil {
   352  		return nil, errors.Trace(err)
   353  	}
   354  	schemas := provider.CredentialSchemas()
   355  	for _, authType := range newCloud.AuthTypes {
   356  		if _, defined := schemas[authType]; !defined {
   357  			return nil, errors.NotSupportedf("auth type %q", authType)
   358  		}
   359  	}
   360  	if err := c.verifyName(c.Cloud); err != nil {
   361  		return nil, errors.Trace(err)
   362  	}
   363  	return &newCloud, nil
   364  }
   365  
   366  func (c *AddCloudCommand) runInteractive(ctxt *cmd.Context) error {
   367  	errout := interact.NewErrWriter(ctxt.Stdout)
   368  	pollster := interact.New(ctxt.Stdin, ctxt.Stdout, errout)
   369  
   370  	cloudType, err := queryCloudType(pollster)
   371  	if err != nil {
   372  		return errors.Trace(err)
   373  	}
   374  
   375  	name, err := queryName(c.cloudMetadataStore, c.Cloud, cloudType, pollster)
   376  	if err != nil {
   377  		return errors.Trace(err)
   378  	}
   379  
   380  	provider, err := environs.Provider(cloudType)
   381  	if err != nil {
   382  		return errors.Trace(err)
   383  	}
   384  
   385  	// At this stage, since we do not have a reference to any model, nor can we get it,
   386  	// nor do we need to have a model for anything that this command does,
   387  	// no cloud credential stored server-side can be invalidated.
   388  	// So, just log an informative message.
   389  	c.CloudCallCtx.InvalidateCredentialFunc = func(reason string) error {
   390  		ctxt.Infof("Cloud credential is not accepted by cloud provider: %v", reason)
   391  		return nil
   392  	}
   393  
   394  	// VerifyURLs will return true if a schema format type jsonschema.FormatURI is used
   395  	// and the value will Ping().
   396  	pollster.VerifyURLs = func(s string) (bool, string, error) {
   397  		err := c.Ping(provider, s)
   398  		if err != nil {
   399  			return false, "Can't validate endpoint: " + err.Error(), nil
   400  		}
   401  		return true, "", nil
   402  	}
   403  
   404  	// VerifyCertFile will return true if the schema format type "cert-filename" is used
   405  	// and the value is readable and a valid cert file.
   406  	pollster.VerifyCertFile = func(s string) (bool, string, error) {
   407  		out, err := ioutil.ReadFile(s)
   408  		if err != nil {
   409  			return false, "Can't validate CA Certificate file: " + err.Error(), nil
   410  		}
   411  		if _, err := cert.ParseCert(string(out)); err != nil {
   412  			return false, fmt.Sprintf("Can't validate CA Certificate %s: %s", s, err.Error()), nil
   413  		}
   414  		return true, "", nil
   415  	}
   416  
   417  	v, err := pollster.QuerySchema(provider.CloudSchema())
   418  	if err != nil {
   419  		return errors.Trace(err)
   420  	}
   421  	b, err := yaml.Marshal(v)
   422  	if err != nil {
   423  		return errors.Trace(err)
   424  	}
   425  
   426  	filename, alt, err := addCertificate(b)
   427  	switch {
   428  	case errors.IsNotFound(err):
   429  	case err != nil:
   430  		return errors.Annotate(err, "CA Certificate")
   431  	default:
   432  		ctxt.Infof("Successfully read CA Certificate from %s", filename)
   433  		b = alt
   434  	}
   435  
   436  	newCloud, err := c.cloudMetadataStore.ParseOneCloud(b)
   437  	if err != nil {
   438  		return errors.Trace(err)
   439  	}
   440  	newCloud.Name = name
   441  	newCloud.Type = cloudType
   442  	if err := addLocalCloud(c.cloudMetadataStore, newCloud); err != nil {
   443  		return errors.Trace(err)
   444  	}
   445  	ctxt.Infof("Cloud %q successfully added", name)
   446  	ctxt.Infof("")
   447  	ctxt.Infof("You will need to add credentials for this cloud (`juju add-credential %s`)", name)
   448  	ctxt.Infof("before creating a controller (`juju bootstrap %s`).", name)
   449  
   450  	return nil
   451  }
   452  
   453  // addCertificate reads the cloud certificate file if available and adds the contents
   454  // to the byte slice with the appropriate key.  A NotFound error is returned if
   455  // a cloud.CertFilenameKey is not contained in the data, or the value is empty, this is
   456  // not a fatal error.
   457  func addCertificate(data []byte) (string, []byte, error) {
   458  	vals, err := ensureStringMaps(string(data))
   459  	if err != nil {
   460  		return "", nil, err
   461  	}
   462  	name, ok := vals[jujucloud.CertFilenameKey]
   463  	if !ok {
   464  		return "", nil, errors.NotFoundf("yaml has no certificate file")
   465  	}
   466  	filename := name.(string)
   467  	if ok && filename != "" {
   468  		out, err := ioutil.ReadFile(filename)
   469  		if err != nil {
   470  			return filename, nil, err
   471  		}
   472  		certificate := string(out)
   473  		if _, err := cert.ParseCert(certificate); err != nil {
   474  			return filename, nil, errors.Annotate(err, "bad cloud CA certificate")
   475  		}
   476  		vals["ca-certificates"] = []string{certificate}
   477  
   478  	} else {
   479  		return filename, nil, errors.NotFoundf("yaml has no certificate file")
   480  	}
   481  	alt, err := yaml.Marshal(vals)
   482  	return filename, alt, err
   483  }
   484  
   485  func ensureStringMaps(in string) (map[string]interface{}, error) {
   486  	userDataMap := make(map[string]interface{})
   487  	if err := yaml.Unmarshal([]byte(in), &userDataMap); err != nil {
   488  		return nil, errors.Annotate(err, "must be valid YAML")
   489  	}
   490  	out, err := utils.ConformYAML(userDataMap)
   491  	if err != nil {
   492  		return nil, err
   493  	}
   494  	return out.(map[string]interface{}), nil
   495  }
   496  
   497  func queryName(
   498  	cloudMetadataStore CloudMetadataStore,
   499  	cloudName string,
   500  	cloudType string,
   501  	pollster *interact.Pollster,
   502  ) (string, error) {
   503  	public, _, err := cloudMetadataStore.PublicCloudMetadata()
   504  	if err != nil {
   505  		return "", err
   506  	}
   507  	personal, err := cloudMetadataStore.PersonalCloudMetadata()
   508  	if err != nil {
   509  		return "", err
   510  	}
   511  
   512  	for {
   513  		if cloudName == "" {
   514  			name, err := pollster.Enter(fmt.Sprintf("a name for your %s cloud", cloudType))
   515  			if err != nil {
   516  				return "", errors.Trace(err)
   517  			}
   518  			cloudName = name
   519  		}
   520  		if _, ok := personal[cloudName]; ok {
   521  			override, err := pollster.YN(fmt.Sprintf("A cloud named %q already exists. Do you want to replace that definition", cloudName), false)
   522  			if err != nil {
   523  				return "", errors.Trace(err)
   524  			}
   525  			if override {
   526  				return cloudName, nil
   527  			}
   528  			// else, ask again
   529  			cloudName = ""
   530  			continue
   531  		}
   532  		msg, err := nameExists(cloudName, public)
   533  		if err != nil {
   534  			return "", errors.Trace(err)
   535  		}
   536  		if msg == "" {
   537  			return cloudName, nil
   538  		}
   539  		override, err := pollster.YN(msg+", do you want to override that definition", false)
   540  		if err != nil {
   541  			return "", errors.Trace(err)
   542  		}
   543  		if override {
   544  			return cloudName, nil
   545  		}
   546  		// else, ask again
   547  	}
   548  }
   549  
   550  // addableCloudProviders returns the names of providers supported by add-cloud,
   551  // and also the names of those which are not supported.
   552  func addableCloudProviders() (providers []string, unsupported []string, _ error) {
   553  	allproviders := environs.RegisteredProviders()
   554  	for _, name := range allproviders {
   555  		provider, err := environs.Provider(name)
   556  		if err != nil {
   557  			// should be impossible
   558  			return nil, nil, errors.Trace(err)
   559  		}
   560  
   561  		if provider.CloudSchema() != nil {
   562  			providers = append(providers, name)
   563  		} else {
   564  			unsupported = append(unsupported, name)
   565  		}
   566  	}
   567  	sort.Strings(providers)
   568  	return providers, unsupported, nil
   569  }
   570  
   571  func queryCloudType(pollster *interact.Pollster) (string, error) {
   572  	providers, unsupported, err := addableCloudProviders()
   573  	if err != nil {
   574  		// should be impossible
   575  		return "", errors.Trace(err)
   576  	}
   577  	supportedCloud := interact.VerifyOptions("cloud type", providers, false)
   578  
   579  	cloudVerify := func(s string) (ok bool, errmsg string, err error) {
   580  		ok, errmsg, err = supportedCloud(s)
   581  		if err != nil {
   582  			return false, "", errors.Trace(err)
   583  		}
   584  		if ok {
   585  			return true, "", nil
   586  		}
   587  		// Print out a different message if they entered a valid provider that
   588  		// just isn't something we want people to add (like ec2).
   589  		for _, name := range unsupported {
   590  			if strings.ToLower(name) == strings.ToLower(s) {
   591  				return false, fmt.Sprintf("Cloud type %q not supported for interactive add-cloud.", s), nil
   592  			}
   593  		}
   594  		return false, errmsg, nil
   595  	}
   596  
   597  	return pollster.SelectVerify(interact.List{
   598  		Singular: "cloud type",
   599  		Plural:   "cloud types",
   600  		Options:  providers,
   601  	}, cloudVerify)
   602  }
   603  
   604  func (c *AddCloudCommand) verifyName(name string) error {
   605  	if c.Replace {
   606  		return nil
   607  	}
   608  	public, _, err := c.cloudMetadataStore.PublicCloudMetadata()
   609  	if err != nil {
   610  		return err
   611  	}
   612  	personal, err := c.cloudMetadataStore.PersonalCloudMetadata()
   613  	if err != nil {
   614  		return err
   615  	}
   616  	if _, ok := personal[name]; ok {
   617  		return errors.Errorf("%q already exists; use --replace to replace this existing cloud", name)
   618  	}
   619  	msg, err := nameExists(name, public)
   620  	if err != nil {
   621  		return errors.Trace(err)
   622  	}
   623  	if msg != "" {
   624  		return errors.Errorf(msg + "; use --replace to override this definition")
   625  	}
   626  	return nil
   627  }
   628  
   629  // nameExists returns either an empty string if the name does not exist, or a
   630  // non-empty string with an error message if it does exist.
   631  func nameExists(name string, public map[string]jujucloud.Cloud) (string, error) {
   632  	if _, ok := public[name]; ok {
   633  		return fmt.Sprintf("%q is the name of a public cloud", name), nil
   634  	}
   635  	builtin, err := common.BuiltInClouds()
   636  	if err != nil {
   637  		return "", errors.Trace(err)
   638  	}
   639  	if _, ok := builtin[name]; ok {
   640  		return fmt.Sprintf("%q is the name of a built-in cloud", name), nil
   641  	}
   642  	return "", nil
   643  }
   644  
   645  func addLocalCloud(cloudMetadataStore CloudMetadataStore, newCloud jujucloud.Cloud) error {
   646  	personalClouds, err := cloudMetadataStore.PersonalCloudMetadata()
   647  	if err != nil {
   648  		return err
   649  	}
   650  	if personalClouds == nil {
   651  		personalClouds = make(map[string]jujucloud.Cloud)
   652  	}
   653  	personalClouds[newCloud.Name] = newCloud
   654  	return cloudMetadataStore.WritePersonalCloudMetadata(personalClouds)
   655  }