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 }