github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/model/defaultscommand.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 package model 4 5 import ( 6 "bytes" 7 "fmt" 8 "io" 9 "sort" 10 "strings" 11 12 "gopkg.in/juju/names.v2" 13 14 "github.com/juju/cmd" 15 "github.com/juju/errors" 16 "github.com/juju/gnuflag" 17 "github.com/juju/utils/keyvalues" 18 19 "github.com/juju/juju/api" 20 "github.com/juju/juju/api/base" 21 cloudapi "github.com/juju/juju/api/cloud" 22 "github.com/juju/juju/api/modelmanager" 23 jujucloud "github.com/juju/juju/cloud" 24 "github.com/juju/juju/cmd/juju/block" 25 "github.com/juju/juju/cmd/modelcmd" 26 "github.com/juju/juju/cmd/output" 27 "github.com/juju/juju/environs/config" 28 ) 29 30 const ( 31 modelDefaultsSummary = `Displays or sets default configuration settings for a model.` 32 modelDefaultsHelpDoc = ` 33 By default, all default configuration (keys and values) are 34 displayed if a key is not specified. Supplying key=value will set the 35 supplied key to the supplied value. This can be repeated for multiple keys. 36 By default, the model is the current model. 37 38 39 Examples: 40 juju model-defaults 41 juju model-defaults http-proxy 42 juju model-defaults aws/us-east-1 http-proxy 43 juju model-defaults us-east-1 http-proxy 44 juju model-defaults -m mymodel type 45 juju model-defaults ftp-proxy=10.0.0.1:8000 46 juju model-defaults aws/us-east-1 ftp-proxy=10.0.0.1:8000 47 juju model-defaults us-east-1 ftp-proxy=10.0.0.1:8000 48 juju model-defaults -m othercontroller:mymodel default-series=yakkety test-mode=false 49 juju model-defaults --reset default-series test-mode 50 juju model-defaults aws/us-east-1 --reset http-proxy 51 juju model-defaults us-east-1 --reset http-proxy 52 53 See also: 54 models 55 model-config 56 ` 57 ) 58 59 // NewDefaultsCommand wraps defaultsCommand with sane model settings. 60 func NewDefaultsCommand() cmd.Command { 61 defaultsCmd := &defaultsCommand{ 62 newCloudAPI: func(caller base.APICallCloser) cloudAPI { 63 return cloudapi.NewClient(caller) 64 }, 65 newDefaultsAPI: func(caller base.APICallCloser) defaultsCommandAPI { 66 return modelmanager.NewClient(caller) 67 }, 68 } 69 defaultsCmd.newAPIRoot = defaultsCmd.NewAPIRoot 70 return modelcmd.WrapController(defaultsCmd) 71 } 72 73 // defaultsCommand is compound command for accessing and setting attributes 74 // related to default model configuration. 75 type defaultsCommand struct { 76 out cmd.Output 77 modelcmd.ControllerCommandBase 78 79 newAPIRoot func() (api.Connection, error) 80 newDefaultsAPI func(base.APICallCloser) defaultsCommandAPI 81 newCloudAPI func(base.APICallCloser) cloudAPI 82 83 action func(defaultsCommandAPI, *cmd.Context) error // The function handling the input, set in Init. 84 key string 85 resetKeys []string // Holds the keys to be reset once parsed. 86 cloudName, regionName string 87 reset []string // Holds the keys to be reset until parsed. 88 values attributes 89 } 90 91 // cloudAPI defines an API to be passed in for testing. 92 type cloudAPI interface { 93 Close() error 94 DefaultCloud() (names.CloudTag, error) 95 Cloud(names.CloudTag) (jujucloud.Cloud, error) 96 } 97 98 // defaultsCommandAPI defines an API to be used during testing. 99 type defaultsCommandAPI interface { 100 // Close closes the api connection. 101 Close() error 102 103 // ModelDefaults returns the default config values used when creating a new model. 104 ModelDefaults() (config.ModelDefaultAttributes, error) 105 106 // SetModelDefaults sets the default config values to use 107 // when creating new models. 108 SetModelDefaults(cloud, region string, config map[string]interface{}) error 109 110 // UnsetModelDefaults clears the default model 111 // configuration values. 112 UnsetModelDefaults(cloud, region string, keys ...string) error 113 } 114 115 // Info implements part of the cmd.Command interface. 116 func (c *defaultsCommand) Info() *cmd.Info { 117 return &cmd.Info{ 118 Args: "[[<cloud/>]<region> ]<model-key>[<=value>] ...]", 119 Doc: modelDefaultsHelpDoc, 120 Name: "model-defaults", 121 Purpose: modelDefaultsSummary, 122 } 123 } 124 125 // SetFlags implements part of the cmd.Command interface. 126 func (c *defaultsCommand) SetFlags(f *gnuflag.FlagSet) { 127 c.ControllerCommandBase.SetFlags(f) 128 129 c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{ 130 "yaml": cmd.FormatYaml, 131 "json": cmd.FormatJson, 132 "tabular": formatDefaultConfigTabular, 133 }) 134 f.Var(cmd.NewAppendStringsValue(&c.reset), "reset", "Reset the provided comma delimited keys") 135 } 136 137 // Init implements part of the cmd.Command interface. 138 // This needs to parse a command line invocation to reset and set, or get 139 // model-default values. The arguments may be interspersed as demosntrated in 140 // the examples. 141 // 142 // This sets foo=baz and unsets bar in aws/us-east-1 143 // juju model-defaults aws/us-east-1 foo=baz --reset bar 144 // 145 // If aws is the cloud of the current or specified controller -- specified by 146 // -c somecontroller -- then the following would also be equivalent. 147 // juju model-defaults --reset bar us-east-1 foo=baz 148 // 149 // If one doesn't specify a cloud or region the command is still valid but for 150 // setting the default on the controller: 151 // juju model-defaults foo=baz --reset bar 152 // 153 // Of course one can specify multiple keys to reset --reset a,b,c and one can 154 // also specify multiple values to set a=b c=d e=f. I.e. comma separated for 155 // resetting and space separated for setting. One may also only set or reset as 156 // a singular action. 157 // juju model-defaults --reset foo 158 // juju model-defaults a=b c=d e=f 159 // juju model-defaults a=b c=d --reset e,f 160 // 161 // cloud/region may also be specified so above examples with that option might 162 // be like the following invokation. 163 // juju model-defaults us-east-1 a=b c=d --reset e,f 164 // 165 // Finally one can also ask for the all the defaults or the defaults for one 166 // specific setting. In this case specifying a region is not valid as 167 // model-defaults shows the settings for a value at all locations that it has a 168 // default set -- or at a minimum the default and "-" for a controller with no 169 // value set. 170 // juju model-defaults 171 // juju model-defaults no-proxy 172 // 173 // It is not valid to reset and get or to set and get values. It is also 174 // neither valid to reset and set the same key, nor to set the same key to 175 // different values in the same command. 176 // 177 // For those playing along that all means the first positional arg can be a 178 // cloud/region, a region, a key=value to set, a key to get the settings for, 179 // or empty. Other caveats are that one cannot set and reset a value for the 180 // same key, that is to say keys to be mutated must be unique. 181 // 182 // Here we go... 183 func (c *defaultsCommand) Init(args []string) error { 184 var err error 185 // If there's nothing to reset and no args we're returning everything. So 186 // we short circuit immediately. 187 if len(args) == 0 && len(c.reset) == 0 { 188 c.action = c.getDefaults 189 return nil 190 } 191 192 // If there is an argument provided to reset, we turn it into a slice of 193 // strings and verify them. If there is one or more valid keys to reset and 194 // no other errors initalizing the command, c.resetDefaults will be called 195 // in c.Run. 196 if err = c.parseResetKeys(); err != nil { 197 return errors.Trace(err) 198 } 199 200 // Look at the first positional arg and test to see if it is a valid 201 // optional specification of cloud/region or region. If it is then 202 // cloudName and regionName are set on the object and the positional args 203 // are returned without the first element. If it cannot be validated; 204 // cloudName and regionName are left empty and we get back the same args we 205 // passed in. 206 args, err = c.parseArgsForRegion(args) 207 if err != nil { 208 return errors.Trace(err) 209 } 210 211 // Remember we *might* have one less arg at this point if we chopped the 212 // first off because it was a valid cloud/region option. 213 switch { 214 case len(args) > 0 && strings.Contains(args[len(args)-1], "="): 215 // In the event that we are setting values, the final positional arg 216 // will always have an "=" in it. So if we see that we know we want to 217 // set args. 218 return c.handleSetArgs(args) 219 case len(args) == 0: 220 c.action = c.getDefaults 221 return nil 222 case len(args) == 1: 223 // We want to get settings for the provided key. 224 return c.handleOneArg(args[0]) 225 default: // case args > 1 226 // Specifying any non key=value positional args after a key=value pair 227 // is invalid input. So if we have more than one the input is almost 228 // certainly invalid, but in different possible ways. 229 return c.handleExtraArgs(args) 230 } 231 232 } 233 234 // parseResetKeys splits the keys provided to --reset after trimming any 235 // leading or trailing comma. It then verifies that we haven't incorrectly 236 // received any key=value pairs and finally sets the value(s) on c.resetKeys. 237 func (c *defaultsCommand) parseResetKeys() error { 238 if len(c.reset) == 0 { 239 return nil 240 } 241 var resetKeys []string 242 for _, value := range c.reset { 243 keys := strings.Split(strings.Trim(value, ","), ",") 244 resetKeys = append(resetKeys, keys...) 245 } 246 247 for _, k := range resetKeys { 248 if k == config.AgentVersionKey { 249 return errors.Errorf("%q cannot be reset", config.AgentVersionKey) 250 } 251 if strings.Contains(k, "=") { 252 return errors.Errorf( 253 `--reset accepts a comma delimited set of keys "a,b,c", received: %q`, k) 254 } 255 } 256 c.resetKeys = resetKeys 257 return nil 258 } 259 260 // parseArgsForRegion parses args to check if the first arg is a region and 261 // returns the appropriate remaining args. 262 func (c *defaultsCommand) parseArgsForRegion(args []string) ([]string, error) { 263 var err error 264 if len(args) > 0 { 265 // determine if the first arg is cloud/region or region and return 266 // appropriate positional args. 267 args, err = c.parseCloudRegion(args) 268 if err != nil { 269 return nil, errors.Trace(err) 270 } 271 } 272 return args, nil 273 } 274 275 // parseCloudRegion examines args to see if the first arg is a cloud/region or 276 // region. If not it returns the full args slice. If it is then it sets cloud 277 // and/or region on the object and sends the remaining args back to the caller. 278 func (c *defaultsCommand) parseCloudRegion(args []string) ([]string, error) { 279 var cloud, region string 280 cr := args[0] 281 // Must have no more than one slash and it must not be at the beginning or end. 282 if strings.Count(cr, "/") == 1 && !strings.HasPrefix(cr, "/") && !strings.HasSuffix(cr, "/") { 283 elems := strings.Split(cr, "/") 284 cloud, region = elems[0], elems[1] 285 } else { 286 region = cr 287 } 288 289 // TODO(redir) 2016-10-05 #1627162 290 // We don't disallow "=" in region names, but probably should. 291 if strings.Contains(region, "=") { 292 return args, nil 293 } 294 295 valid, err := c.validCloudRegion(cloud, region) 296 if err != nil { 297 return nil, errors.Trace(err) 298 } 299 if !valid { 300 return args, nil 301 } 302 return args[1:], nil 303 } 304 305 // validCloudRegion checks that region is a valid region in cloud, or default cloud 306 // if cloud is not specified. 307 func (c *defaultsCommand) validCloudRegion(cloudName, region string) (bool, error) { 308 var ( 309 isCloudRegion bool 310 cloud jujucloud.Cloud 311 cTag names.CloudTag 312 err error 313 ) 314 315 root, err := c.newAPIRoot() 316 if err != nil { 317 return false, errors.Trace(err) 318 } 319 cc := c.newCloudAPI(root) 320 defer cc.Close() 321 322 if cloudName == "" { 323 cTag, err = cc.DefaultCloud() 324 if err != nil { 325 return false, errors.Trace(err) 326 } 327 } else { 328 if !names.IsValidCloud(cloudName) { 329 return false, errors.Errorf("invalid cloud %q", cloudName) 330 } 331 cTag = names.NewCloudTag(cloudName) 332 } 333 cloud, err = cc.Cloud(cTag) 334 if err != nil { 335 return false, errors.Trace(err) 336 } 337 338 for _, r := range cloud.Regions { 339 if r.Name == region { 340 c.cloudName = cTag.Id() 341 c.regionName = region 342 isCloudRegion = true 343 break 344 } 345 } 346 return isCloudRegion, nil 347 } 348 349 // handleSetArgs parses args for setting defaults. 350 func (c *defaultsCommand) handleSetArgs(args []string) error { 351 argZeroKeyOnly := !strings.Contains(args[0], "=") 352 // If an invalid region was specified, the first positional arg won't have 353 // an "=". If we see one here we know it is invalid. 354 switch { 355 case argZeroKeyOnly && c.regionName == "": 356 return errors.Errorf("invalid region specified: %q", args[0]) 357 case argZeroKeyOnly && c.regionName != "": 358 return errors.New("cannot set and retrieve default values simultaneously") 359 default: 360 if err := c.parseSetKeys(args); err != nil { 361 return errors.Trace(err) 362 } 363 c.action = c.setDefaults 364 return nil 365 } 366 } 367 368 // parseSetKeys iterates over the args and make sure that the key=value pairs 369 // are valid. It also checks that the same key isn't also being reset. 370 func (c *defaultsCommand) parseSetKeys(args []string) error { 371 options, err := keyvalues.Parse(args, true) 372 if err != nil { 373 return errors.Trace(err) 374 } 375 376 c.values = make(attributes) 377 for k, v := range options { 378 if k == config.AgentVersionKey { 379 return errors.Errorf(`%q must be set via "upgrade-juju"`, config.AgentVersionKey) 380 } 381 c.values[k] = v 382 } 383 for _, k := range c.resetKeys { 384 if _, ok := c.values[k]; ok { 385 return errors.Errorf( 386 "key %q cannot be both set and unset in the same command", k) 387 } 388 } 389 return nil 390 } 391 392 // handleOneArg handles the case where we have one positional arg after 393 // processing for a region and the reset flag. 394 func (c *defaultsCommand) handleOneArg(arg string) error { 395 resetSpecified := c.resetKeys != nil 396 regionSpecified := c.regionName != "" 397 398 if regionSpecified { 399 if resetSpecified { 400 // If a region was specified and reset was specified, we shouldn't have 401 // an extra arg. If it had an "=" in it, we should have handled it 402 // already. 403 return errors.New("cannot retrieve defaults for a region and reset attributes at the same time") 404 } 405 } 406 if resetSpecified { 407 // It makes no sense to supply a positional arg that isn't a region if 408 // we are resetting keys in a region, so we must have gotten an invalid 409 // region. 410 return errors.Errorf("invalid region specified: %q", arg) 411 } 412 // We can retrieve a value. 413 c.key = arg 414 c.action = c.getDefaults 415 return nil 416 } 417 418 // handleExtraArgs handles the case where too many args were supplied. 419 func (c *defaultsCommand) handleExtraArgs(args []string) error { 420 resetSpecified := c.resetKeys != nil 421 regionSpecified := c.regionName != "" 422 numArgs := len(args) 423 424 // if we have a key=value pair here then something is wrong because the 425 // last positional arg is not one. We assume the user intended to get a 426 // value after setting them. 427 for _, arg := range args { 428 if strings.Contains(arg, "=") { 429 return errors.New("cannot set and retrieve default values simultaneously") 430 } 431 } 432 433 if !regionSpecified { 434 if resetSpecified { 435 if numArgs == 2 { 436 // It makes no sense to supply a positional arg that isn't a 437 // region if we are resetting a region, so we must have gotten 438 // an invalid region. 439 return errors.Errorf("invalid region specified: %q", args[0]) 440 } 441 } 442 if !resetSpecified { 443 // If we land here it is because there are extraneous positional 444 // args. 445 return errors.New("can only retrieve defaults for one key or all") 446 } 447 } 448 return errors.New("invalid input") 449 } 450 451 // Run implements part of the cmd.Command interface. 452 func (c *defaultsCommand) Run(ctx *cmd.Context) error { 453 root, err := c.newAPIRoot() 454 if err != nil { 455 return errors.Trace(err) 456 } 457 client := c.newDefaultsAPI(root) 458 if err != nil { 459 return errors.Trace(err) 460 } 461 defer client.Close() 462 463 if len(c.resetKeys) > 0 { 464 err := c.resetDefaults(client, ctx) 465 if err != nil { 466 // We return this error naked as it is almost certainly going to be 467 // cmd.ErrSilent and the cmd.Command framework expects that back 468 // from cmd.Run if the process is blocked. 469 return err 470 } 471 } 472 if c.action == nil { 473 // If we are reset only we end up here, only we've already done that. 474 return nil 475 } 476 return c.action(client, ctx) 477 } 478 479 // getDefaults writes out the value for a single key or the full tree of 480 // defaults. 481 func (c *defaultsCommand) getDefaults(client defaultsCommandAPI, ctx *cmd.Context) error { 482 attrs, err := client.ModelDefaults() 483 if err != nil { 484 return err 485 } 486 487 valueForRegion := func(region string, regions []config.RegionDefaultValue) (config.RegionDefaultValue, bool) { 488 for _, r := range regions { 489 if r.Name == region { 490 return r, true 491 } 492 } 493 return config.RegionDefaultValue{}, false 494 } 495 496 // Filter by region if necessary. 497 if c.regionName != "" { 498 for attrName, attr := range attrs { 499 if regionDefault, ok := valueForRegion(c.regionName, attr.Regions); !ok { 500 delete(attrs, attrName) 501 } else { 502 attrForRegion := attr 503 attrForRegion.Regions = []config.RegionDefaultValue{regionDefault} 504 attrs[attrName] = attrForRegion 505 } 506 } 507 } 508 509 if c.key != "" { 510 if value, ok := attrs[c.key]; ok { 511 attrs = config.ModelDefaultAttributes{ 512 c.key: value, 513 } 514 } else { 515 msg := fmt.Sprintf("there are no default model values for %q", c.key) 516 if c.regionName != "" { 517 msg += fmt.Sprintf(" in region %q", c.regionName) 518 } 519 return errors.New(msg) 520 } 521 } 522 // If c.keys is empty, write out the whole lot. 523 return c.out.Write(ctx, attrs) 524 } 525 526 // setDefaults sets defaults as provided in c.values. 527 func (c *defaultsCommand) setDefaults(client defaultsCommandAPI, ctx *cmd.Context) error { 528 // ctx unused in this method. 529 if err := c.verifyKnownKeys(client); err != nil { 530 return errors.Trace(err) 531 } 532 return block.ProcessBlockedError( 533 client.SetModelDefaults( 534 c.cloudName, c.regionName, c.values), block.BlockChange) 535 } 536 537 // resetDefaults resets the keys in resetKeys. 538 func (c *defaultsCommand) resetDefaults(client defaultsCommandAPI, ctx *cmd.Context) error { 539 // ctx unused in this method. 540 if err := c.verifyKnownKeys(client); err != nil { 541 return errors.Trace(err) 542 } 543 return block.ProcessBlockedError( 544 client.UnsetModelDefaults( 545 c.cloudName, c.regionName, c.resetKeys...), block.BlockChange) 546 547 } 548 549 // verifyKnownKeys is a helper to validate the keys we are operating with 550 // against the set of known attributes from the model. 551 func (c *defaultsCommand) verifyKnownKeys(client defaultsCommandAPI) error { 552 known, err := client.ModelDefaults() 553 if err != nil { 554 return errors.Trace(err) 555 } 556 557 allKeys := c.resetKeys[:] 558 for k := range c.values { 559 allKeys = append(allKeys, k) 560 } 561 562 for _, key := range allKeys { 563 // check if the key exists in the known config 564 // and warn the user if the key is not defined 565 if _, exists := known[key]; !exists { 566 logger.Warningf( 567 "key %q is not defined in the known model configuration: possible misspelling", key) 568 } 569 } 570 return nil 571 } 572 573 // formatConfigTabular writes a tabular summary of default config information. 574 func formatDefaultConfigTabular(writer io.Writer, value interface{}) error { 575 defaultValues, ok := value.(config.ModelDefaultAttributes) 576 if !ok { 577 return errors.Errorf("expected value of type %T, got %T", defaultValues, value) 578 } 579 580 tw := output.TabWriter(writer) 581 w := output.Wrapper{tw} 582 583 p := func(name string, value config.AttributeDefaultValues) { 584 var c, d interface{} 585 switch value.Default { 586 case nil: 587 d = "-" 588 case "": 589 d = `""` 590 default: 591 d = value.Default 592 } 593 switch value.Controller { 594 case nil: 595 c = "-" 596 case "": 597 c = `""` 598 default: 599 c = value.Controller 600 } 601 w.Println(name, d, c) 602 for _, region := range value.Regions { 603 w.Println(" "+region.Name, region.Value, "-") 604 } 605 } 606 var valueNames []string 607 for name := range defaultValues { 608 valueNames = append(valueNames, name) 609 } 610 sort.Strings(valueNames) 611 612 w.Println("Attribute", "Default", "Controller") 613 614 for _, name := range valueNames { 615 info := defaultValues[name] 616 out := &bytes.Buffer{} 617 err := cmd.FormatYaml(out, info) 618 if err != nil { 619 return errors.Annotatef(err, "formatting value for %q", name) 620 } 621 p(name, info) 622 } 623 624 tw.Flush() 625 return nil 626 }