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 }