Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2016 Canonical Ltd. | |
| // Licensed under the AGPLv3, see LICENCE file for details. | |
| package cloud | |
| import ( | |
| "fmt" | |
| "io/ioutil" | |
| "sort" | |
| "strings" | |
| "github.com/juju/cmd" | |
| "github.com/juju/errors" | |
| "github.com/juju/gnuflag" | |
| "gopkg.in/juju/names.v2" | |
| "gopkg.in/yaml.v2" | |
| "github.com/juju/juju/cloud" | |
| "github.com/juju/juju/cmd/juju/common" | |
| "github.com/juju/juju/cmd/juju/interact" | |
| "github.com/juju/juju/environs" | |
| ) | |
| type CloudMetadataStore interface { | |
| ParseCloudMetadataFile(path string) (map[string]cloud.Cloud, error) | |
| ParseOneCloud(data []byte) (cloud.Cloud, error) | |
| PublicCloudMetadata(searchPaths ...string) (result map[string]cloud.Cloud, fallbackUsed bool, _ error) | |
| PersonalCloudMetadata() (map[string]cloud.Cloud, error) | |
| WritePersonalCloudMetadata(cloudsMap map[string]cloud.Cloud) error | |
| } | |
| var usageAddCloudSummary = ` | |
| Adds a user-defined cloud to Juju from among known cloud types.`[1:] | |
| var usageAddCloudDetails = ` | |
| A cloud definition file has the following YAML format: | |
| clouds: | |
| mycloud: | |
| type: openstack | |
| auth-types: [ userpass ] | |
| regions: | |
| london: | |
| endpoint: https://london.mycloud.com:35574/v3.0/ | |
| If the named cloud already exists, the `[1:] + "`--replace`" + ` option is required to | |
| overwrite its configuration. | |
| Known cloud types: azure, cloudsigma, ec2, gce, joyent, lxd, maas, manual, | |
| openstack, rackspace | |
| Examples: | |
| juju add-cloud mycloud ~/mycloud.yaml | |
| See also: | |
| clouds` | |
| // AddCloudCommand is the command that allows you to add a cloud configuration | |
| // for use with juju bootstrap. | |
| type AddCloudCommand struct { | |
| cmd.CommandBase | |
| // Replace, if true, existing cloud information is overwritten. | |
| Replace bool | |
| // Cloud is the name fo the cloud to add. | |
| Cloud string | |
| // CloudFile is the name of the cloud YAML file. | |
| CloudFile string | |
| // Ping contains the logic for pinging a cloud endpoint to know whether or | |
| // not it really has a valid cloud of the same type as the provider. By | |
| // default it just calls the correct provider's Ping method. | |
| Ping func(p environs.EnvironProvider, endpoint string) error | |
| cloudMetadataStore CloudMetadataStore | |
| } | |
| // NewAddCloudCommand returns a command to add cloud information. | |
| func NewAddCloudCommand(cloudMetadataStore CloudMetadataStore) *AddCloudCommand { | |
| // Ping is provider.Ping except in tests where we don't actually want to | |
| // require a valid cloud. | |
| return &AddCloudCommand{ | |
| cloudMetadataStore: cloudMetadataStore, | |
| Ping: func(p environs.EnvironProvider, endpoint string) error { | |
| return p.Ping(endpoint) | |
| }, | |
| } | |
| } | |
| // Info returns help information about the command. | |
| func (c *AddCloudCommand) Info() *cmd.Info { | |
| return &cmd.Info{ | |
| Name: "add-cloud", | |
| Args: "<cloud name> <cloud definition file>", | |
| Purpose: usageAddCloudSummary, | |
| Doc: usageAddCloudDetails, | |
| } | |
| } | |
| // SetFlags initializes the flags supported by the command. | |
| func (c *AddCloudCommand) SetFlags(f *gnuflag.FlagSet) { | |
| c.CommandBase.SetFlags(f) | |
| f.BoolVar(&c.Replace, "replace", false, "Overwrite any existing cloud information") | |
| f.StringVar(&c.CloudFile, "f", "", "The path to a cloud definition file") | |
| } | |
| // Init populates the command with the args from the command line. | |
| func (c *AddCloudCommand) Init(args []string) (err error) { | |
| if len(args) > 0 { | |
| c.Cloud = args[0] | |
| if ok := names.IsValidCloud(c.Cloud); !ok { | |
| return errors.NotValidf("cloud name %q", c.Cloud) | |
| } | |
| } | |
| if len(args) > 1 { | |
| if c.CloudFile != args[1] && c.CloudFile != "" { | |
| return errors.BadRequestf("cannot specify cloud file with flag and argument") | |
| } | |
| c.CloudFile = args[1] | |
| } | |
| if len(args) > 2 { | |
| return cmd.CheckEmpty(args[2:]) | |
| } | |
| return nil | |
| } | |
| // Run executes the add cloud command, adding a cloud based on a passed-in yaml | |
| // file or interactive queries. | |
| func (c *AddCloudCommand) Run(ctxt *cmd.Context) error { | |
| if c.CloudFile == "" { | |
| return c.runInteractive(ctxt) | |
| } | |
| specifiedClouds, err := c.cloudMetadataStore.ParseCloudMetadataFile(c.CloudFile) | |
| if err != nil { | |
| return err | |
| } | |
| if specifiedClouds == nil { | |
| return errors.New("no personal clouds are defined") | |
| } | |
| newCloud, ok := specifiedClouds[c.Cloud] | |
| if !ok { | |
| return errors.Errorf("cloud %q not found in file %q", c.Cloud, c.CloudFile) | |
| } | |
| // first validate cloud input | |
| data, err := ioutil.ReadFile(c.CloudFile) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if err = cloud.ValidateCloudSet([]byte(data)); err != nil { | |
| ctxt.Warningf(err.Error()) | |
| } | |
| // validate cloud data | |
| provider, err := environs.Provider(newCloud.Type) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| schemas := provider.CredentialSchemas() | |
| for _, authType := range newCloud.AuthTypes { | |
| if _, defined := schemas[authType]; !defined { | |
| return errors.NotSupportedf("auth type %q", authType) | |
| } | |
| } | |
| if err := c.verifyName(c.Cloud); err != nil { | |
| return errors.Trace(err) | |
| } | |
| return addCloud(c.cloudMetadataStore, newCloud) | |
| } | |
| func (c *AddCloudCommand) runInteractive(ctxt *cmd.Context) error { | |
| errout := interact.NewErrWriter(ctxt.Stdout) | |
| pollster := interact.New(ctxt.Stdin, ctxt.Stdout, errout) | |
| cloudType, err := queryCloudType(pollster) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| name, err := queryName(c.cloudMetadataStore, c.Cloud, cloudType, pollster) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| provider, err := environs.Provider(cloudType) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| pollster.VerifyURLs = func(s string) (ok bool, msg string, err error) { | |
| err = c.Ping(provider, s) | |
| if err != nil { | |
| return false, "Can't validate endpoint: " + err.Error(), nil | |
| } | |
| return true, "", nil | |
| } | |
| v, err := pollster.QuerySchema(provider.CloudSchema()) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| b, err := yaml.Marshal(v) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| newCloud, err := c.cloudMetadataStore.ParseOneCloud(b) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| newCloud.Name = name | |
| newCloud.Type = cloudType | |
| if err := addCloud(c.cloudMetadataStore, newCloud); err != nil { | |
| return errors.Trace(err) | |
| } | |
| ctxt.Infof("Cloud %q successfully added", name) | |
| ctxt.Infof("You may bootstrap with 'juju bootstrap %s'", name) | |
| return nil | |
| } | |
| func queryName( | |
| cloudMetadataStore CloudMetadataStore, | |
| cloudName string, | |
| cloudType string, | |
| pollster *interact.Pollster, | |
| ) (string, error) { | |
| public, _, err := cloudMetadataStore.PublicCloudMetadata() | |
| if err != nil { | |
| return "", err | |
| } | |
| personal, err := cloudMetadataStore.PersonalCloudMetadata() | |
| if err != nil { | |
| return "", err | |
| } | |
| for { | |
| if cloudName == "" { | |
| name, err := pollster.Enter(fmt.Sprintf("a name for your %s cloud", cloudType)) | |
| if err != nil { | |
| return "", errors.Trace(err) | |
| } | |
| cloudName = name | |
| } | |
| if _, ok := personal[cloudName]; ok { | |
| override, err := pollster.YN(fmt.Sprintf("A cloud named %q already exists. Do you want to replace that definition", cloudName), false) | |
| if err != nil { | |
| return "", errors.Trace(err) | |
| } | |
| if override { | |
| return cloudName, nil | |
| } | |
| // else, ask again | |
| cloudName = "" | |
| continue | |
| } | |
| msg, err := nameExists(cloudName, public) | |
| if err != nil { | |
| return "", errors.Trace(err) | |
| } | |
| if msg == "" { | |
| return cloudName, nil | |
| } | |
| override, err := pollster.YN(msg+", do you want to override that definition", false) | |
| if err != nil { | |
| return "", errors.Trace(err) | |
| } | |
| if override { | |
| return cloudName, nil | |
| } | |
| // else, ask again | |
| } | |
| } | |
| // addableCloudProviders returns the names of providers supported by add-cloud, | |
| // and also the names of those which are not supported. | |
| func addableCloudProviders() (providers []string, unsupported []string, _ error) { | |
| allproviders := environs.RegisteredProviders() | |
| for _, name := range allproviders { | |
| provider, err := environs.Provider(name) | |
| if err != nil { | |
| // should be impossible | |
| return nil, nil, errors.Trace(err) | |
| } | |
| if provider.CloudSchema() != nil { | |
| providers = append(providers, name) | |
| } else { | |
| unsupported = append(unsupported, name) | |
| } | |
| } | |
| sort.Strings(providers) | |
| return providers, unsupported, nil | |
| } | |
| func queryCloudType(pollster *interact.Pollster) (string, error) { | |
| providers, unsupported, err := addableCloudProviders() | |
| if err != nil { | |
| // should be impossible | |
| return "", errors.Trace(err) | |
| } | |
| supportedCloud := interact.VerifyOptions("cloud type", providers, false) | |
| cloudVerify := func(s string) (ok bool, errmsg string, err error) { | |
| ok, errmsg, err = supportedCloud(s) | |
| if err != nil { | |
| return false, "", errors.Trace(err) | |
| } | |
| if ok { | |
| return true, "", nil | |
| } | |
| // Print out a different message if they entered a valid provider that | |
| // just isn't something we want people to add (like ec2). | |
| for _, name := range unsupported { | |
| if strings.ToLower(name) == strings.ToLower(s) { | |
| return false, fmt.Sprintf("Cloud type %q not supported for interactive add-cloud.", s), nil | |
| } | |
| } | |
| return false, errmsg, nil | |
| } | |
| return pollster.SelectVerify(interact.List{ | |
| Singular: "cloud type", | |
| Plural: "cloud types", | |
| Options: providers, | |
| }, cloudVerify) | |
| } | |
| func (c *AddCloudCommand) verifyName(name string) error { | |
| if c.Replace { | |
| return nil | |
| } | |
| public, _, err := c.cloudMetadataStore.PublicCloudMetadata() | |
| if err != nil { | |
| return err | |
| } | |
| personal, err := c.cloudMetadataStore.PersonalCloudMetadata() | |
| if err != nil { | |
| return err | |
| } | |
| if _, ok := personal[name]; ok { | |
| return errors.Errorf("%q already exists; use --replace to replace this existing cloud", name) | |
| } | |
| msg, err := nameExists(name, public) | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if msg != "" { | |
| return errors.Errorf(msg + "; use --replace to override this definition") | |
| } | |
| return nil | |
| } | |
| // nameExists returns either an empty string if the name does not exist, or a | |
| // non-empty string with an error message if it does exist. | |
| func nameExists(name string, public map[string]cloud.Cloud) (string, error) { | |
| if _, ok := public[name]; ok { | |
| return fmt.Sprintf("%q is the name of a public cloud", name), nil | |
| } | |
| builtin, err := common.BuiltInClouds() | |
| if err != nil { | |
| return "", errors.Trace(err) | |
| } | |
| if _, ok := builtin[name]; ok { | |
| return fmt.Sprintf("%q is the name of a built-in cloud", name), nil | |
| } | |
| return "", nil | |
| } | |
| func addCloud(cloudMetadataStore CloudMetadataStore, newCloud cloud.Cloud) error { | |
| personalClouds, err := cloudMetadataStore.PersonalCloudMetadata() | |
| if err != nil { | |
| return err | |
| } | |
| if personalClouds == nil { | |
| personalClouds = make(map[string]cloud.Cloud) | |
| } | |
| personalClouds[newCloud.Name] = newCloud | |
| return cloudMetadataStore.WritePersonalCloudMetadata(personalClouds) | |
| } |