github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/cmd/juju/upgradejuju.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package main 5 6 import ( 7 stderrors "errors" 8 "fmt" 9 10 "launchpad.net/gnuflag" 11 12 "launchpad.net/juju-core/cmd" 13 "launchpad.net/juju-core/environs" 14 "launchpad.net/juju-core/environs/config" 15 "launchpad.net/juju-core/environs/storage" 16 "launchpad.net/juju-core/environs/sync" 17 envtools "launchpad.net/juju-core/environs/tools" 18 "launchpad.net/juju-core/errors" 19 "launchpad.net/juju-core/juju" 20 "launchpad.net/juju-core/state/api" 21 "launchpad.net/juju-core/state/api/params" 22 coretools "launchpad.net/juju-core/tools" 23 "launchpad.net/juju-core/version" 24 ) 25 26 // UpgradeJujuCommand upgrades the agents in a juju installation. 27 type UpgradeJujuCommand struct { 28 cmd.EnvCommandBase 29 vers string 30 Version version.Number 31 UploadTools bool 32 Series []string 33 } 34 35 var upgradeJujuDoc = ` 36 The upgrade-juju command upgrades a running environment by setting a version 37 number for all juju agents to run. By default, it chooses the most recent 38 supported version compatible with the command-line tools version. 39 40 A development version is defined to be any version with an odd minor 41 version or a nonzero build component (for example version 2.1.1, 3.3.0 42 and 2.0.0.1 are development versions; 2.0.3 and 3.4.1 are not). A 43 development version may be chosen in two cases: 44 45 - when the current agent version is a development one and there is 46 a more recent version available with the same major.minor numbers; 47 - when an explicit --version major.minor is given (e.g. --version 1.17, 48 or 1.17.2, but not just 1) 49 50 For development use, the --upload-tools flag specifies that the juju tools will 51 packaged (or compiled locally, if no jujud binaries exists, for which you will 52 need the golang packages installed) and uploaded before the version is set. 53 Currently the tools will be uploaded as if they had the version of the current 54 juju tool, unless specified otherwise by the --version flag. 55 56 When run without arguments. upgrade-juju will try to upgrade to the 57 following versions, in order of preference, depending on the current 58 value of the environment's agent-version setting: 59 60 - The highest patch.build version of the *next* stable major.minor version. 61 - The highest patch.build version of the *current* major.minor version. 62 63 Both of these depend on tools availability, which some situations (no 64 outgoing internet access) and provider types (such as maas) require that 65 you manage yourself; see the documentation for "sync-tools". 66 ` 67 68 func (c *UpgradeJujuCommand) Info() *cmd.Info { 69 return &cmd.Info{ 70 Name: "upgrade-juju", 71 Purpose: "upgrade the tools in a juju environment", 72 Doc: upgradeJujuDoc, 73 } 74 } 75 76 func (c *UpgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) { 77 c.EnvCommandBase.SetFlags(f) 78 f.StringVar(&c.vers, "version", "", "upgrade to specific version") 79 f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools") 80 f.Var(seriesVar{&c.Series}, "series", "upload tools for supplied comma-separated series list") 81 } 82 83 func (c *UpgradeJujuCommand) Init(args []string) error { 84 if c.vers != "" { 85 vers, err := version.Parse(c.vers) 86 if err != nil { 87 return err 88 } 89 if vers.Major != version.Current.Major { 90 return fmt.Errorf("cannot upgrade to version incompatible with CLI") 91 } 92 if c.UploadTools && vers.Build != 0 { 93 // TODO(fwereade): when we start taking versions from actual built 94 // code, we should disable --version when used with --upload-tools. 95 // For now, it's the only way to experiment with version upgrade 96 // behaviour live, so the only restriction is that Build cannot 97 // be used (because its value needs to be chosen internally so as 98 // not to collide with existing tools). 99 return fmt.Errorf("cannot specify build number when uploading tools") 100 } 101 c.Version = vers 102 } 103 if len(c.Series) > 0 && !c.UploadTools { 104 return fmt.Errorf("--series requires --upload-tools") 105 } 106 return cmd.CheckEmpty(args) 107 } 108 109 var errUpToDate = stderrors.New("no upgrades available") 110 111 // Run changes the version proposed for the juju envtools. 112 func (c *UpgradeJujuCommand) Run(_ *cmd.Context) (err error) { 113 client, err := juju.NewAPIClientFromName(c.EnvName) 114 if err != nil { 115 return err 116 } 117 defer client.Close() 118 defer func() { 119 if err == errUpToDate { 120 logger.Infof(err.Error()) 121 err = nil 122 } 123 }() 124 125 // Determine the version to upgrade to, uploading tools if necessary. 126 attrs, err := client.EnvironmentGet() 127 if params.IsCodeNotImplemented(err) { 128 return c.run1dot16() 129 } 130 if err != nil { 131 return err 132 } 133 cfg, err := config.New(config.NoDefaults, attrs) 134 if err != nil { 135 return err 136 } 137 v, err := c.initVersions(client, cfg) 138 if err != nil { 139 return err 140 } 141 if c.UploadTools { 142 series := getUploadSeries(cfg, c.Series) 143 if err := v.uploadTools(cfg, series); err != nil { 144 return err 145 } 146 } 147 if err := v.validate(); err != nil { 148 return err 149 } 150 logger.Infof("upgrade version chosen: %s", v.chosen) 151 // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. 152 logger.Infof("available tools: %s", v.tools) 153 154 if err := client.SetEnvironAgentVersion(v.chosen); err != nil { 155 return err 156 } 157 logger.Infof("started upgrade to %s", v.chosen) 158 return nil 159 } 160 161 // initVersions collects state relevant to an upgrade decision. The returned 162 // agent and client versions, and the list of currently available tools, will 163 // always be accurate; the chosen version, and the flag indicating development 164 // mode, may remain blank until uploadTools or validate is called. 165 func (c *UpgradeJujuCommand) initVersions(client *api.Client, cfg *config.Config) (*upgradeVersions, error) { 166 agent, ok := cfg.AgentVersion() 167 if !ok { 168 // Can't happen. In theory. 169 return nil, fmt.Errorf("incomplete environment configuration") 170 } 171 if c.Version == agent { 172 return nil, errUpToDate 173 } 174 clientVersion := version.Current.Number 175 findResult, err := client.FindTools(clientVersion.Major, -1, "", "") 176 var availableTools coretools.List 177 if params.IsCodeNotImplemented(err) { 178 availableTools, err = findTools1dot17(cfg) 179 } else { 180 availableTools = findResult.List 181 } 182 if err != nil { 183 return nil, err 184 } 185 err = findResult.Error 186 if findResult.Error != nil { 187 if !params.IsCodeNotFound(err) { 188 return nil, err 189 } 190 if !c.UploadTools { 191 // No tools found and we shouldn't upload any, so if we are not asking for a 192 // major upgrade, pretend there is no more recent version available. 193 if c.Version == version.Zero && agent.Major == clientVersion.Major { 194 return nil, errUpToDate 195 } 196 return nil, err 197 } 198 } 199 return &upgradeVersions{ 200 agent: agent, 201 client: clientVersion, 202 chosen: c.Version, 203 tools: availableTools, 204 }, nil 205 } 206 207 // findTools1dot17 allows 1.17.x versions to be upgraded. 208 func findTools1dot17(cfg *config.Config) (coretools.List, error) { 209 env, err := environs.New(cfg) 210 if err != nil { 211 return nil, err 212 } 213 clientVersion := version.Current.Number 214 return envtools.FindTools(env, clientVersion.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry) 215 } 216 217 // upgradeVersions holds the version information for making upgrade decisions. 218 type upgradeVersions struct { 219 agent version.Number 220 client version.Number 221 chosen version.Number 222 tools coretools.List 223 } 224 225 // uploadTools compiles jujud from $GOPATH and uploads it into the supplied 226 // storage. If no version has been explicitly chosen, the version number 227 // reported by the built tools will be based on the client version number. 228 // In any case, the version number reported will have a build component higher 229 // than that of any otherwise-matching available envtools. 230 // uploadTools resets the chosen version and replaces the available tools 231 // with the ones just uploaded. 232 func (v *upgradeVersions) uploadTools(cfg *config.Config, series []string) (err error) { 233 // TODO(fwereade): this is kinda crack: we should not assume that 234 // version.Current matches whatever source happens to be built. The 235 // ideal would be: 236 // 1) compile jujud from $GOPATH into some build dir 237 // 2) get actual version with `jujud version` 238 // 3) check actual version for compatibility with CLI tools 239 // 4) generate unique build version with reference to available tools 240 // 5) force-version that unique version into the dir directly 241 // 6) archive and upload the build dir 242 // ...but there's no way we have time for that now. In the meantime, 243 // considering the use cases, this should work well enough; but it 244 // won't detect an incompatible major-version change, which is a shame. 245 if v.chosen == version.Zero { 246 v.chosen = v.client 247 } 248 v.chosen = uploadVersion(v.chosen, v.tools) 249 250 // TODO(wallyworld): we don't want to create an environment here but there's 251 // currently no choice. We need to add an UploadTools API. 252 env, err := environs.New(cfg) 253 if err != nil { 254 return err 255 } 256 // TODO(fwereade): sync.Upload should return coretools.List, and should 257 // include all the extra series we build, so we can set *that* onto 258 // v.available and maybe one day be able to check that a given upgrade 259 // won't leave out-of-date machines lying around, starved of tools. 260 uploaded, err := sync.Upload(env.Storage(), &v.chosen, series...) 261 if err != nil { 262 return err 263 } 264 v.tools = coretools.List{uploaded} 265 return nil 266 } 267 268 // validate chooses an upgrade version, if one has not already been chosen, 269 // and ensures the tools list contains no entries that do not have that version. 270 // If validate returns no error, the environment agent-version can be set to 271 // the value of the chosen field. 272 func (v *upgradeVersions) validate() (err error) { 273 if v.chosen == version.Zero { 274 // No explicitly specified version, so find the version to which we 275 // need to upgrade. If the CLI and agent major versions match, we find 276 // next available stable release to upgrade to by incrementing the 277 // minor version, starting from the current agent version and doing 278 // major.minor+1 or +2 as needed. If the CLI has a greater major version, 279 // we just use the CLI version as is. 280 nextVersion := v.agent 281 if nextVersion.Major == v.client.Major { 282 if v.agent.IsDev() { 283 nextVersion.Minor += 1 284 } else { 285 nextVersion.Minor += 2 286 } 287 } else { 288 nextVersion = v.client 289 } 290 291 newestNextStable, found := v.tools.NewestCompatible(nextVersion) 292 if found { 293 logger.Debugf("found a more recent stable version %s", newestNextStable) 294 v.chosen = newestNextStable 295 } else { 296 newestCurrent, found := v.tools.NewestCompatible(v.agent) 297 if found { 298 logger.Debugf("found more recent current version %s", newestCurrent) 299 v.chosen = newestCurrent 300 } else { 301 if v.agent.Major != v.client.Major { 302 return fmt.Errorf("no compatible tools available") 303 } else { 304 return fmt.Errorf("no more recent supported versions available") 305 } 306 } 307 } 308 } else { 309 // If not completely specified already, pick a single tools version. 310 filter := coretools.Filter{Number: v.chosen, Released: !v.chosen.IsDev()} 311 if v.tools, err = v.tools.Match(filter); err != nil { 312 return err 313 } 314 v.chosen, v.tools = v.tools.Newest() 315 } 316 if v.chosen == v.agent { 317 return errUpToDate 318 } 319 320 // Disallow major.minor version downgrades. 321 if v.chosen.Major < v.agent.Major || v.chosen.Major == v.agent.Major && v.chosen.Minor < v.agent.Minor { 322 // TODO(fwereade): I'm a bit concerned about old agent/CLI tools even 323 // *connecting* to environments with higher agent-versions; but ofc they 324 // have to connect in order to discover they shouldn't. However, once 325 // any of our tools detect an incompatible version, they should act to 326 // minimize damage: the CLI should abort politely, and the agents should 327 // run an Upgrader but no other tasks. 328 return fmt.Errorf("cannot change version from %s to %s", v.agent, v.chosen) 329 } 330 331 return nil 332 } 333 334 // uploadVersion returns a copy of the supplied version with a build number 335 // higher than any of the supplied tools that share its major, minor and patch. 336 func uploadVersion(vers version.Number, existing coretools.List) version.Number { 337 vers.Build++ 338 for _, t := range existing { 339 if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { 340 continue 341 } 342 if t.Version.Build >= vers.Build { 343 vers.Build = t.Version.Build + 1 344 } 345 } 346 return vers 347 } 348 349 // run1dot16 implements the command without access to the API. This is 350 // needed for compatibility, so 1.16 can be upgraded to newer 351 // releases. It should be removed in 1.18. 352 func (c *UpgradeJujuCommand) run1dot16() error { 353 logger.Warningf("running in 1.16 compatibility mode") 354 conn, err := juju.NewConnFromName(c.EnvName) 355 if err != nil { 356 return err 357 } 358 defer conn.Close() 359 defer func() { 360 if err == errUpToDate { 361 logger.Infof(err.Error()) 362 err = nil 363 } 364 }() 365 366 // Determine the version to upgrade to, uploading tools if necessary. 367 env := conn.Environ 368 cfg, err := conn.State.EnvironConfig() 369 if err != nil { 370 return err 371 } 372 v, err := c.initVersions1dot16(cfg, env) 373 if err != nil { 374 return err 375 } 376 if c.UploadTools { 377 series := getUploadSeries(cfg, c.Series) 378 if err := v.uploadTools1dot16(env.Storage(), series); err != nil { 379 return err 380 } 381 } 382 if err := v.validate(); err != nil { 383 return err 384 } 385 logger.Infof("upgrade version chosen: %s", v.chosen) 386 logger.Infof("available tools: %s", v.tools) 387 388 if err := conn.State.SetEnvironAgentVersion(v.chosen); err != nil { 389 return err 390 } 391 logger.Infof("started upgrade to %s", v.chosen) 392 return nil 393 } 394 395 func (c *UpgradeJujuCommand) initVersions1dot16(cfg *config.Config, env environs.Environ) (*upgradeVersions, error) { 396 agent, ok := cfg.AgentVersion() 397 if !ok { 398 // Can't happen. In theory. 399 return nil, fmt.Errorf("incomplete environment configuration") 400 } 401 if c.Version == agent { 402 return nil, errUpToDate 403 } 404 client := version.Current.Number 405 available, err := envtools.FindTools(env, client.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry) 406 if err != nil { 407 if !errors.IsNotFoundError(err) { 408 return nil, err 409 } 410 if !c.UploadTools { 411 // No tools found and we shouldn't upload any, so if we are not asking for a 412 // major upgrade, pretend there is no more recent version available. 413 if c.Version == version.Zero && agent.Major == client.Major { 414 return nil, errUpToDate 415 } 416 return nil, err 417 } 418 } 419 return &upgradeVersions{ 420 agent: agent, 421 client: client, 422 chosen: c.Version, 423 tools: available, 424 }, nil 425 } 426 427 func (v *upgradeVersions) uploadTools1dot16(storage storage.Storage, series []string) error { 428 if v.chosen == version.Zero { 429 v.chosen = v.client 430 } 431 v.chosen = uploadVersion(v.chosen, v.tools) 432 uploaded, err := sync.Upload(storage, &v.chosen, series...) 433 if err != nil { 434 return err 435 } 436 v.tools = coretools.List{uploaded} 437 return nil 438 }