github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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).Id()
   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  		if params.IsCodeUnauthorized(err) {
   209  			common.PermissionsMessage(ctx.Stderr, "add a model")
   210  		}
   211  		return errors.Trace(err)
   212  	}
   213  
   214  	messageFormat := "Added '%s' model"
   215  	messageArgs := []interface{}{c.Name}
   216  
   217  	if modelOwner == accountDetails.User {
   218  		controllerName := c.ControllerName()
   219  		if err := store.UpdateModel(controllerName, c.Name, jujuclient.ModelDetails{
   220  			model.UUID,
   221  		}); err != nil {
   222  			return errors.Trace(err)
   223  		}
   224  		if err := store.SetCurrentModel(controllerName, c.Name); err != nil {
   225  			return errors.Trace(err)
   226  		}
   227  	}
   228  
   229  	if c.CloudRegion != "" || model.CloudRegion != "" {
   230  		// The user explicitly requested a cloud/region,
   231  		// or the cloud supports multiple regions. Whichever
   232  		// the case, tell the user which cloud/region the
   233  		// model was deployed to.
   234  		cloudRegion := model.Cloud
   235  		if model.CloudRegion != "" {
   236  			cloudRegion += "/" + model.CloudRegion
   237  		}
   238  		messageFormat += " on %s"
   239  		messageArgs = append(messageArgs, cloudRegion)
   240  	}
   241  	if model.CloudCredential != "" {
   242  		tag := names.NewCloudCredentialTag(model.CloudCredential)
   243  		credentialName := tag.Name()
   244  		if tag.Owner().Id() != modelOwner {
   245  			credentialName = fmt.Sprintf("%s/%s", tag.Owner().Id(), credentialName)
   246  		}
   247  		messageFormat += " with credential '%s'"
   248  		messageArgs = append(messageArgs, credentialName)
   249  	}
   250  
   251  	messageFormat += forUserSuffix
   252  
   253  	// "Added '<model>' model [on <cloud>/<region>] [with credential '<credential>'] for user '<user namePart>'"
   254  	ctx.Infof(messageFormat, messageArgs...)
   255  
   256  	if _, ok := attrs[config.AuthorizedKeysKey]; !ok {
   257  		// It is not an error to have no authorized-keys when adding a
   258  		// model, though this should never happen since we generate
   259  		// juju-specific SSH keys.
   260  		ctx.Infof(`
   261  No SSH authorized-keys were found. You must use "juju add-ssh-key"
   262  before "juju ssh", "juju scp", or "juju debug-hooks" will work.`)
   263  	}
   264  
   265  	return nil
   266  }
   267  
   268  func (c *addModelCommand) getCloudRegion(cloudClient CloudAPI) (cloudTag names.CloudTag, cloud jujucloud.Cloud, cloudRegion string, err error) {
   269  	var cloudName string
   270  	sep := strings.IndexRune(c.CloudRegion, '/')
   271  	if sep >= 0 {
   272  		// User specified "cloud/region".
   273  		cloudName, cloudRegion = c.CloudRegion[:sep], c.CloudRegion[sep+1:]
   274  		if !names.IsValidCloud(cloudName) {
   275  			return names.CloudTag{}, jujucloud.Cloud{}, "", errors.NotValidf("cloud name %q", cloudName)
   276  		}
   277  		cloudTag = names.NewCloudTag(cloudName)
   278  		if cloud, err = cloudClient.Cloud(cloudTag); err != nil {
   279  			return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   280  		}
   281  	} else {
   282  		// User specified "cloud" or "region". We'll try first
   283  		// for cloud (check if it's a valid cloud name, and
   284  		// whether there is a cloud by that name), and then
   285  		// as a region within the default cloud.
   286  		if names.IsValidCloud(c.CloudRegion) {
   287  			cloudName = c.CloudRegion
   288  		} else {
   289  			cloudRegion = c.CloudRegion
   290  		}
   291  		if cloudName != "" {
   292  			cloudTag = names.NewCloudTag(cloudName)
   293  			cloud, err = cloudClient.Cloud(cloudTag)
   294  			if params.IsCodeNotFound(err) {
   295  				// No such cloud with the specified name,
   296  				// so we'll try the name as a region in
   297  				// the default cloud.
   298  				cloudRegion, cloudName = cloudName, ""
   299  			} else if err != nil {
   300  				return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   301  			}
   302  		}
   303  		if cloudName == "" {
   304  			cloudTag, cloud, err = defaultCloud(cloudClient)
   305  			if err != nil && !errors.IsNotFound(err) {
   306  				return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   307  			}
   308  		}
   309  	}
   310  	if cloudRegion != "" {
   311  		// A region has been specified, make sure it exists.
   312  		if _, err := jujucloud.RegionByName(cloud.Regions, cloudRegion); err != nil {
   313  			if cloudRegion == c.CloudRegion {
   314  				// The string is not in the format cloud/region,
   315  				// so we should tell that the user that it is
   316  				// neither a cloud nor a region in the default
   317  				// cloud (or that there is no default cloud).
   318  				err := c.unsupportedCloudOrRegionError(cloudClient, cloudTag)
   319  				return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   320  			}
   321  			return names.CloudTag{}, jujucloud.Cloud{}, "", errors.Trace(err)
   322  		}
   323  	} else if len(cloud.Regions) > 0 {
   324  		// The first region in the list is the default.
   325  		cloudRegion = cloud.Regions[0].Name
   326  	}
   327  	return cloudTag, cloud, cloudRegion, nil
   328  }
   329  
   330  func (c *addModelCommand) unsupportedCloudOrRegionError(cloudClient CloudAPI, defaultCloudTag names.CloudTag) (err error) {
   331  	clouds, err := cloudClient.Clouds()
   332  	if err != nil {
   333  		return errors.Annotate(err, "querying supported clouds")
   334  	}
   335  	cloudNames := make([]string, 0, len(clouds))
   336  	for tag := range clouds {
   337  		cloudNames = append(cloudNames, tag.Id())
   338  	}
   339  	sort.Strings(cloudNames)
   340  
   341  	var buf bytes.Buffer
   342  	tw := output.TabWriter(&buf)
   343  	fmt.Fprintln(tw, "Cloud\tRegions")
   344  	for _, cloudName := range cloudNames {
   345  		cloud := clouds[names.NewCloudTag(cloudName)]
   346  		regionNames := make([]string, len(cloud.Regions))
   347  		for i, region := range cloud.Regions {
   348  			regionNames[i] = region.Name
   349  		}
   350  		fmt.Fprintf(tw, "%s\t%s\n", cloudName, strings.Join(regionNames, ", "))
   351  	}
   352  	tw.Flush()
   353  
   354  	var prefix string
   355  	if defaultCloudTag != (names.CloudTag{}) {
   356  		prefix = fmt.Sprintf(`
   357  %q is neither a cloud supported by this controller,
   358  nor a region in the controller's default cloud %q.
   359  The clouds/regions supported by this controller are:`[1:],
   360  			c.CloudRegion, defaultCloudTag.Id())
   361  	} else {
   362  		prefix = fmt.Sprintf(`
   363  %q is not a cloud supported by this controller,
   364  and there is no default cloud. The clouds/regions supported
   365  by this controller are:`[1:], c.CloudRegion)
   366  	}
   367  	return errors.Errorf("%s\n\n%s", prefix, buf.String())
   368  }
   369  
   370  func defaultCloud(cloudClient CloudAPI) (names.CloudTag, jujucloud.Cloud, error) {
   371  	cloudTag, err := cloudClient.DefaultCloud()
   372  	if err != nil {
   373  		if params.IsCodeNotFound(err) {
   374  			return names.CloudTag{}, jujucloud.Cloud{}, errors.NewNotFound(nil, `
   375  there is no default cloud defined, please specify one using:
   376  
   377      juju add-model [flags] <model-name> cloud[/region]`[1:])
   378  		}
   379  		return names.CloudTag{}, jujucloud.Cloud{}, errors.Trace(err)
   380  	}
   381  	cloud, err := cloudClient.Cloud(cloudTag)
   382  	if err != nil {
   383  		return names.CloudTag{}, jujucloud.Cloud{}, errors.Trace(err)
   384  	}
   385  	return cloudTag, cloud, nil
   386  }
   387  
   388  func (c *addModelCommand) maybeUploadCredential(
   389  	ctx *cmd.Context,
   390  	cloudClient CloudAPI,
   391  	cloudTag names.CloudTag,
   392  	cloudRegion string,
   393  	cloud jujucloud.Cloud,
   394  	modelOwner string,
   395  ) (names.CloudCredentialTag, error) {
   396  
   397  	modelOwnerTag := names.NewUserTag(modelOwner)
   398  	credentialTag, err := common.ResolveCloudCredentialTag(
   399  		modelOwnerTag, cloudTag, c.CredentialName,
   400  	)
   401  	if err != nil {
   402  		return names.CloudCredentialTag{}, errors.Trace(err)
   403  	}
   404  
   405  	// Check if the credential is already in the controller.
   406  	//
   407  	// TODO(axw) consider implementing a call that can check
   408  	// that the credential exists without fetching all of the
   409  	// names.
   410  	credentialTags, err := cloudClient.UserCredentials(modelOwnerTag, cloudTag)
   411  	if err != nil {
   412  		return names.CloudCredentialTag{}, errors.Trace(err)
   413  	}
   414  	credentialId := credentialTag.Id()
   415  	for _, tag := range credentialTags {
   416  		if tag.Id() != credentialId {
   417  			continue
   418  		}
   419  		ctx.Infof("Using credential '%s' cached in controller", c.CredentialName)
   420  		return credentialTag, nil
   421  	}
   422  
   423  	if credentialTag.Owner().Id() != modelOwner {
   424  		// Another user's credential was specified, so
   425  		// we cannot automatically upload.
   426  		return names.CloudCredentialTag{}, errors.NotFoundf(
   427  			"credential '%s'", c.CredentialName,
   428  		)
   429  	}
   430  
   431  	// Upload the credential from the client, if it exists locally.
   432  	credential, _, _, err := modelcmd.GetCredentials(
   433  		ctx, c.ClientStore(), modelcmd.GetCredentialsParams{
   434  			Cloud:          cloud,
   435  			CloudName:      cloudTag.Id(),
   436  			CloudRegion:    cloudRegion,
   437  			CredentialName: credentialTag.Name(),
   438  		},
   439  	)
   440  	if err != nil {
   441  		return names.CloudCredentialTag{}, errors.Trace(err)
   442  	}
   443  	ctx.Infof("Uploading credential '%s' to controller", credentialTag.Id())
   444  	if err := cloudClient.UpdateCredential(credentialTag, *credential); err != nil {
   445  		return names.CloudCredentialTag{}, errors.Trace(err)
   446  	}
   447  	return credentialTag, nil
   448  }
   449  
   450  func (c *addModelCommand) getConfigValues(ctx *cmd.Context) (map[string]interface{}, error) {
   451  	configValues, err := c.Config.ReadAttrs(ctx)
   452  	if err != nil {
   453  		return nil, errors.Annotate(err, "unable to parse config")
   454  	}
   455  	coercedValues, err := common.ConformYAML(configValues)
   456  	if err != nil {
   457  		return nil, errors.Annotatef(err, "unable to parse config")
   458  	}
   459  	attrs, ok := coercedValues.(map[string]interface{})
   460  	if !ok {
   461  		return nil, errors.New("params must contain a YAML map with string keys")
   462  	}
   463  	if err := common.FinalizeAuthorizedKeys(ctx, attrs); err != nil {
   464  		if errors.Cause(err) != common.ErrNoAuthorizedKeys {
   465  			return nil, errors.Trace(err)
   466  		}
   467  	}
   468  	return attrs, nil
   469  }