github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/controller/addmodel.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package controller
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  	"gopkg.in/juju/names.v2"
    16  
    17  	"github.com/juju/juju/api"
    18  	"github.com/juju/juju/api/base"
    19  	cloudapi "github.com/juju/juju/api/cloud"
    20  	"github.com/juju/juju/api/modelmanager"
    21  	"github.com/juju/juju/apiserver/params"
    22  	jujucloud "github.com/juju/juju/cloud"
    23  	"github.com/juju/juju/cmd/juju/common"
    24  	"github.com/juju/juju/cmd/modelcmd"
    25  	"github.com/juju/juju/cmd/output"
    26  	"github.com/juju/juju/environs/config"
    27  	"github.com/juju/juju/jujuclient"
    28  )
    29  
    30  // NewAddModelCommand returns a command to add a model.
    31  func NewAddModelCommand() cmd.Command {
    32  	return modelcmd.WrapController(&addModelCommand{
    33  		newAddModelAPI: func(caller base.APICallCloser) AddModelAPI {
    34  			return modelmanager.NewClient(caller)
    35  		},
    36  		newCloudAPI: func(caller base.APICallCloser) CloudAPI {
    37  			return cloudapi.NewClient(caller)
    38  		},
    39  	})
    40  }
    41  
    42  // addModelCommand calls the API to add a new model.
    43  type addModelCommand struct {
    44  	modelcmd.ControllerCommandBase
    45  	apiRoot        api.Connection
    46  	newAddModelAPI func(base.APICallCloser) AddModelAPI
    47  	newCloudAPI    func(base.APICallCloser) CloudAPI
    48  
    49  	Name           string
    50  	Owner          string
    51  	CredentialName string
    52  	CloudRegion    string
    53  	Config         common.ConfigFlag
    54  }
    55  
    56  const addModelHelpDoc = `
    57  Adding a model is typically done in order to run a specific workload.
    58  To add a model, you must at a minimum specify a model name. You may
    59  also supply model-specific configuration, a credential, and which
    60  cloud/region to deploy the model to. The cloud/region and credentials
    61  are the ones used to create any future resources within the model.
    62  
    63  Model names can be duplicated across controllers but must be unique for
    64  any given controller. Model names may only contain lowercase letters,
    65  digits and hyphens, and may not start with a hyphen.
    66  
    67  Credential names are specified either in the form "credential-name", or
    68  "credential-owner/credential-name". There is currently no way to acquire
    69  access to another user's credentials, so the only valid value for
    70  credential-owner is your own user name. This may change in a future
    71  release.
    72  
    73  If no cloud/region is specified, then the model will be deployed to
    74  the same cloud/region as the controller model. If a region is specified
    75  without a cloud qualifier, then it is assumed to be in the same cloud
    76  as the controller model. It is not currently possible for a controller
    77  to manage multiple clouds, so the only valid cloud is the same cloud
    78  as the controller model is deployed to. This may change in a future
    79  release.
    80  
    81  Examples:
    82  
    83      juju add-model mymodel
    84      juju add-model mymodel us-east-1
    85      juju add-model mymodel aws/us-east-1
    86      juju add-model mymodel --config my-config.yaml --config image-stream=daily
    87      juju add-model mymodel --credential credential_name --config authorized-keys="ssh-rsa ..."
    88  `
    89  
    90  func (c *addModelCommand) Info() *cmd.Info {
    91  	return &cmd.Info{
    92  		Name:    "add-model",
    93  		Args:    "<model name> [cloud|region|(cloud/region)]",
    94  		Purpose: "Adds a hosted model.",
    95  		Doc:     strings.TrimSpace(addModelHelpDoc),
    96  	}
    97  }
    98  
    99  func (c *addModelCommand) SetFlags(f *gnuflag.FlagSet) {
   100  	c.ControllerCommandBase.SetFlags(f)
   101  	f.StringVar(&c.Owner, "owner", "", "The owner of the new model if not the current user")
   102  	f.StringVar(&c.CredentialName, "credential", "", "Credential used to add the model")
   103  	f.Var(&c.Config, "config", "Path to YAML model configuration file or individual options (--config config.yaml [--config key=value ...])")
   104  }
   105  
   106  func (c *addModelCommand) Init(args []string) error {
   107  	if len(args) == 0 {
   108  		return errors.New("model name is required")
   109  	}
   110  	c.Name, args = args[0], args[1:]
   111  
   112  	if len(args) > 0 {
   113  		c.CloudRegion, args = args[0], args[1:]
   114  	}
   115  
   116  	if !names.IsValidModelName(c.Name) {
   117  		return errors.Errorf("%q is not a valid name: model names may only contain lowercase letters, digits and hyphens", c.Name)
   118  	}
   119  
   120  	if c.Owner != "" && !names.IsValidUser(c.Owner) {
   121  		return errors.Errorf("%q is not a valid user", c.Owner)
   122  	}
   123  
   124  	return cmd.CheckEmpty(args)
   125  }
   126  
   127  type AddModelAPI interface {
   128  	CreateModel(
   129  		name, owner, cloudName, cloudRegion string,
   130  		cloudCredential names.CloudCredentialTag,
   131  		config map[string]interface{},
   132  	) (base.ModelInfo, error)
   133  }
   134  
   135  type CloudAPI interface {
   136  	DefaultCloud() (names.CloudTag, error)
   137  	Clouds() (map[names.CloudTag]jujucloud.Cloud, error)
   138  	Cloud(names.CloudTag) (jujucloud.Cloud, error)
   139  	UserCredentials(names.UserTag, names.CloudTag) ([]names.CloudCredentialTag, error)
   140  	UpdateCredential(names.CloudCredentialTag, jujucloud.Credential) error
   141  }
   142  
   143  func (c *addModelCommand) newAPIRoot() (api.Connection, error) {
   144  	if c.apiRoot != nil {
   145  		return c.apiRoot, nil
   146  	}
   147  	return c.NewAPIRoot()
   148  }
   149  
   150  func (c *addModelCommand) Run(ctx *cmd.Context) error {
   151  	api, err := c.newAPIRoot()
   152  	if err != nil {
   153  		return errors.Annotate(err, "opening API connection")
   154  	}
   155  	defer api.Close()
   156  
   157  	store := c.ClientStore()
   158  	controllerName := c.ControllerName()
   159  	accountDetails, err := store.AccountDetails(controllerName)
   160  	if err != nil {
   161  		return errors.Trace(err)
   162  	}
   163  
   164  	modelOwner := accountDetails.User
   165  	if c.Owner != "" {
   166  		if !names.IsValidUser(c.Owner) {
   167  			return errors.Errorf("%q is not a valid user name", c.Owner)
   168  		}
   169  		modelOwner = names.NewUserTag(c.Owner).Canonical()
   170  	}
   171  	forUserSuffix := fmt.Sprintf(" for user '%s'", names.NewUserTag(modelOwner).Name())
   172  
   173  	attrs, err := c.getConfigValues(ctx)
   174  	if err != nil {
   175  		return errors.Trace(err)
   176  	}
   177  
   178  	cloudClient := c.newCloudAPI(api)
   179  	var cloudTag names.CloudTag
   180  	var cloud jujucloud.Cloud
   181  	var cloudRegion string
   182  	if c.CloudRegion != "" {
   183  		cloudTag, cloud, cloudRegion, err = c.getCloudRegion(cloudClient)
   184  		if err != nil {
   185  			return errors.Trace(err)
   186  		}
   187  	}
   188  
   189  	// If the user has specified a credential, then we will upload it if
   190  	// it doesn't already exist in the controller, and it exists locally.
   191  	var credentialTag names.CloudCredentialTag
   192  	if c.CredentialName != "" {
   193  		var err error
   194  		if c.CloudRegion == "" {
   195  			if cloudTag, cloud, err = defaultCloud(cloudClient); err != nil {
   196  				return errors.Trace(err)
   197  			}
   198  		}
   199  		credentialTag, err = c.maybeUploadCredential(ctx, cloudClient, cloudTag, cloudRegion, cloud, modelOwner)
   200  		if err != nil {
   201  			return errors.Trace(err)
   202  		}
   203  	}
   204  
   205  	addModelClient := c.newAddModelAPI(api)
   206  	model, err := addModelClient.CreateModel(c.Name, modelOwner, cloudTag.Id(), cloudRegion, credentialTag, attrs)
   207  	if err != nil {
   208  		return errors.Trace(err)
   209  	}
   210  
   211  	messageFormat := "Added '%s' model"
   212  	messageArgs := []interface{}{c.Name}
   213  
   214  	if modelOwner == accountDetails.User {
   215  		controllerName := c.ControllerName()
   216  		if err := store.UpdateModel(controllerName, c.Name, jujuclient.ModelDetails{
   217  			model.UUID,
   218  		}); err != nil {
   219  			return errors.Trace(err)
   220  		}
   221  		if err := store.SetCurrentModel(controllerName, c.Name); err != nil {
   222  			return errors.Trace(err)
   223  		}
   224  	}
   225  
   226  	if c.CloudRegion != "" || model.CloudRegion != "" {
   227  		// The user explicitly requested a cloud/region,
   228  		// or the cloud supports multiple regions. Whichever
   229  		// the case, tell the user which cloud/region the
   230  		// model was deployed to.
   231  		cloudRegion := model.Cloud
   232  		if model.CloudRegion != "" {
   233  			cloudRegion += "/" + model.CloudRegion
   234  		}
   235  		messageFormat += " on %s"
   236  		messageArgs = append(messageArgs, cloudRegion)
   237  	}
   238  	if model.CloudCredential != "" {
   239  		tag := names.NewCloudCredentialTag(model.CloudCredential)
   240  		credentialName := tag.Name()
   241  		if tag.Owner().Canonical() != modelOwner {
   242  			credentialName = fmt.Sprintf("%s/%s", tag.Owner().Canonical(), credentialName)
   243  		}
   244  		messageFormat += " with credential '%s'"
   245  		messageArgs = append(messageArgs, credentialName)
   246  	}
   247  
   248  	messageFormat += forUserSuffix
   249  
   250  	// "Added '<model>' model [on <cloud>/<region>] [with credential '<credential>'] for user '<user namePart>'"
   251  	ctx.Infof(messageFormat, messageArgs...)
   252  
   253  	if _, ok := attrs[config.AuthorizedKeysKey]; !ok {
   254  		// It is not an error to have no authorized-keys when adding a
   255  		// model, though this should never happen since we generate
   256  		// juju-specific SSH keys.
   257  		ctx.Infof(`
   258  No SSH authorized-keys were found. You must use "juju add-ssh-key"
   259  before "juju ssh", "juju scp", or "juju debug-hooks" will work.`)
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  func (c *addModelCommand) getCloudRegion(cloudClient CloudAPI) (cloudTag names.CloudTag, cloud jujucloud.Cloud, cloudRegion string, err error) {
   266  	var cloudName string
   267  	sep := strings.IndexRune(c.CloudRegion, '/')
   268  	if sep >= 0 {
   269  		// User specified "cloud/region".
   270  		cloudName, cloudRegion = c.CloudRegion[:sep], c.CloudRegion[sep+1:]
   271  		if !names.IsValidCloud(cloudName) {
   272  			return names.CloudTag{}, jujucloud.Cloud{}, "", errors.NotValidf("cloud name %q", cloudName)
   273  		}
   274  		cloudTag = names.NewCloudTag(cloudName)
   275  		if cloud, err = cloudClient.Cloud(cloudTag); err != nil {
   276  			return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   277  		}
   278  	} else {
   279  		// User specified "cloud" or "region". We'll try first
   280  		// for cloud (check if it's a valid cloud name, and
   281  		// whether there is a cloud by that name), and then
   282  		// as a region within the default cloud.
   283  		if names.IsValidCloud(c.CloudRegion) {
   284  			cloudName = c.CloudRegion
   285  		} else {
   286  			cloudRegion = c.CloudRegion
   287  		}
   288  		if cloudName != "" {
   289  			cloudTag = names.NewCloudTag(cloudName)
   290  			cloud, err = cloudClient.Cloud(cloudTag)
   291  			if params.IsCodeNotFound(err) {
   292  				// No such cloud with the specified name,
   293  				// so we'll try the name as a region in
   294  				// the default cloud.
   295  				cloudRegion, cloudName = cloudName, ""
   296  			} else if err != nil {
   297  				return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   298  			}
   299  		}
   300  		if cloudName == "" {
   301  			cloudTag, cloud, err = defaultCloud(cloudClient)
   302  			if err != nil && !errors.IsNotFound(err) {
   303  				return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   304  			}
   305  		}
   306  	}
   307  	if cloudRegion != "" {
   308  		// A region has been specified, make sure it exists.
   309  		if _, err := jujucloud.RegionByName(cloud.Regions, cloudRegion); err != nil {
   310  			if cloudRegion == c.CloudRegion {
   311  				// The string is not in the format cloud/region,
   312  				// so we should tell that the user that it is
   313  				// neither a cloud nor a region in the default
   314  				// cloud (or that there is no default cloud).
   315  				err := c.unsupportedCloudOrRegionError(cloudClient, cloudTag)
   316  				return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   317  			}
   318  			return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   319  		}
   320  	} else if len(cloud.Regions) > 0 {
   321  		// The first region in the list is the default.
   322  		cloudRegion = cloud.Regions[0].Name
   323  	}
   324  	return cloudTag, cloud, cloudRegion, nil
   325  }
   326  
   327  func (c *addModelCommand) unsupportedCloudOrRegionError(cloudClient CloudAPI, defaultCloudTag names.CloudTag) (err error) {
   328  	clouds, err := cloudClient.Clouds()
   329  	if err != nil {
   330  		return errors.Annotate(err, "querying supported clouds")
   331  	}
   332  	cloudNames := make([]string, 0, len(clouds))
   333  	for tag := range clouds {
   334  		cloudNames = append(cloudNames, tag.Id())
   335  	}
   336  	sort.Strings(cloudNames)
   337  
   338  	var buf bytes.Buffer
   339  	tw := output.TabWriter(&buf)
   340  	fmt.Fprintln(tw, "CLOUD\tREGIONS")
   341  	for _, cloudName := range cloudNames {
   342  		cloud := clouds[names.NewCloudTag(cloudName)]
   343  		regionNames := make([]string, len(cloud.Regions))
   344  		for i, region := range cloud.Regions {
   345  			regionNames[i] = region.Name
   346  		}
   347  		fmt.Fprintf(tw, "%s\t%s\n", cloudName, strings.Join(regionNames, ", "))
   348  	}
   349  	tw.Flush()
   350  
   351  	var prefix string
   352  	if defaultCloudTag != (names.CloudTag{}) {
   353  		prefix = fmt.Sprintf(`
   354  %q is neither a cloud supported by this controller,
   355  nor a region in the controller's default cloud %q.
   356  The clouds/regions supported by this controller are:`[1:],
   357  			c.CloudRegion, defaultCloudTag.Id())
   358  	} else {
   359  		prefix = fmt.Sprintf(`
   360  %q is not a cloud supported by this controller,
   361  and there is no default cloud. The clouds/regions supported
   362  by this controller are:`[1:], c.CloudRegion)
   363  	}
   364  	return errors.Errorf("%s\n\n%s", prefix, buf.String())
   365  }
   366  
   367  func defaultCloud(cloudClient CloudAPI) (names.CloudTag, jujucloud.Cloud, error) {
   368  	cloudTag, err := cloudClient.DefaultCloud()
   369  	if err != nil {
   370  		if params.IsCodeNotFound(err) {
   371  			return names.CloudTag{}, jujucloud.Cloud{}, errors.NewNotFound(nil, `
   372  there is no default cloud defined, please specify one using:
   373  
   374      juju add-model [flags] <model-name> cloud[/region]`[1:])
   375  		}
   376  		return names.CloudTag{}, jujucloud.Cloud{}, errors.Trace(err)
   377  	}
   378  	cloud, err := cloudClient.Cloud(cloudTag)
   379  	if err != nil {
   380  		return names.CloudTag{}, jujucloud.Cloud{}, errors.Trace(err)
   381  	}
   382  	return cloudTag, cloud, nil
   383  }
   384  
   385  func (c *addModelCommand) maybeUploadCredential(
   386  	ctx *cmd.Context,
   387  	cloudClient CloudAPI,
   388  	cloudTag names.CloudTag,
   389  	cloudRegion string,
   390  	cloud jujucloud.Cloud,
   391  	modelOwner string,
   392  ) (names.CloudCredentialTag, error) {
   393  
   394  	modelOwnerTag := names.NewUserTag(modelOwner)
   395  	credentialTag, err := common.ResolveCloudCredentialTag(
   396  		modelOwnerTag, cloudTag, c.CredentialName,
   397  	)
   398  	if err != nil {
   399  		return names.CloudCredentialTag{}, errors.Trace(err)
   400  	}
   401  
   402  	// Check if the credential is already in the controller.
   403  	//
   404  	// TODO(axw) consider implementing a call that can check
   405  	// that the credential exists without fetching all of the
   406  	// names.
   407  	credentialTags, err := cloudClient.UserCredentials(modelOwnerTag, cloudTag)
   408  	if err != nil {
   409  		return names.CloudCredentialTag{}, errors.Trace(err)
   410  	}
   411  	credentialId := credentialTag.Canonical()
   412  	for _, tag := range credentialTags {
   413  		if tag.Canonical() != credentialId {
   414  			continue
   415  		}
   416  		ctx.Infof("Using credential '%s' cached in controller", c.CredentialName)
   417  		return credentialTag, nil
   418  	}
   419  
   420  	if credentialTag.Owner().Canonical() != modelOwner {
   421  		// Another user's credential was specified, so
   422  		// we cannot automatically upload.
   423  		return names.CloudCredentialTag{}, errors.NotFoundf(
   424  			"credential '%s'", c.CredentialName,
   425  		)
   426  	}
   427  
   428  	// Upload the credential from the client, if it exists locally.
   429  	credential, _, _, err := modelcmd.GetCredentials(
   430  		ctx, c.ClientStore(), modelcmd.GetCredentialsParams{
   431  			Cloud:          cloud,
   432  			CloudName:      cloudTag.Id(),
   433  			CloudRegion:    cloudRegion,
   434  			CredentialName: credentialTag.Name(),
   435  		},
   436  	)
   437  	if err != nil {
   438  		return names.CloudCredentialTag{}, errors.Trace(err)
   439  	}
   440  	ctx.Infof("Uploading credential '%s' to controller", credentialTag.Id())
   441  	if err := cloudClient.UpdateCredential(credentialTag, *credential); err != nil {
   442  		return names.CloudCredentialTag{}, errors.Trace(err)
   443  	}
   444  	return credentialTag, nil
   445  }
   446  
   447  func (c *addModelCommand) getConfigValues(ctx *cmd.Context) (map[string]interface{}, error) {
   448  	configValues, err := c.Config.ReadAttrs(ctx)
   449  	if err != nil {
   450  		return nil, errors.Annotate(err, "unable to parse config")
   451  	}
   452  	coercedValues, err := common.ConformYAML(configValues)
   453  	if err != nil {
   454  		return nil, errors.Annotatef(err, "unable to parse config")
   455  	}
   456  	attrs, ok := coercedValues.(map[string]interface{})
   457  	if !ok {
   458  		return nil, errors.New("params must contain a YAML map with string keys")
   459  	}
   460  	if err := common.FinalizeAuthorizedKeys(ctx, attrs); err != nil {
   461  		if errors.Cause(err) != common.ErrNoAuthorizedKeys {
   462  			return nil, errors.Trace(err)
   463  		}
   464  	}
   465  	return attrs, nil
   466  }
   467  
   468  func canonicalCredentialIds(tags []names.CloudCredentialTag) []string {
   469  	ids := make([]string, len(tags))
   470  	for i, tag := range tags {
   471  		ids[i] = tag.Canonical()
   472  	}
   473  	return ids
   474  }