launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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/log" 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 log.Noticef(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 env, err := environs.New(cfg) 138 if err != nil { 139 return err 140 } 141 v, err := c.initVersions(cfg, env) 142 if err != nil { 143 return err 144 } 145 if c.UploadTools { 146 series := getUploadSeries(cfg, c.Series) 147 if err := v.uploadTools(env.Storage(), series); err != nil { 148 return err 149 } 150 } 151 if err := v.validate(); err != nil { 152 return err 153 } 154 log.Infof("upgrade version chosen: %s", v.chosen) 155 // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. 156 log.Infof("available tools: %s", v.tools) 157 158 if err := client.SetEnvironAgentVersion(v.chosen); err != nil { 159 return err 160 } 161 log.Noticef("started upgrade to %s", v.chosen) 162 return nil 163 } 164 165 // run1dot16 implements the command without access to the API. This is 166 // needed for compatibility, so 1.16 can be upgraded to newer 167 // releases. It should be removed in 1.18. 168 func (c *UpgradeJujuCommand) run1dot16() error { 169 log.Warningf("running in 1.16 compatibility mode") 170 conn, err := juju.NewConnFromName(c.EnvName) 171 if err != nil { 172 return err 173 } 174 defer conn.Close() 175 defer func() { 176 if err == errUpToDate { 177 log.Noticef(err.Error()) 178 err = nil 179 } 180 }() 181 182 // Determine the version to upgrade to, uploading tools if necessary. 183 env := conn.Environ 184 cfg, err := conn.State.EnvironConfig() 185 if err != nil { 186 return err 187 } 188 v, err := c.initVersions(cfg, env) 189 if err != nil { 190 return err 191 } 192 if c.UploadTools { 193 series := getUploadSeries(cfg, c.Series) 194 if err := v.uploadTools(env.Storage(), series); err != nil { 195 return err 196 } 197 } 198 if err := v.validate(); err != nil { 199 return err 200 } 201 log.Infof("upgrade version chosen: %s", v.chosen) 202 // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. 203 log.Infof("available tools: %s", v.tools) 204 205 if err := conn.State.SetEnvironAgentVersion(v.chosen); err != nil { 206 return err 207 } 208 log.Noticef("started upgrade to %s", v.chosen) 209 return nil 210 } 211 212 // initVersions collects state relevant to an upgrade decision. The returned 213 // agent and client versions, and the list of currently available tools, will 214 // always be accurate; the chosen version, and the flag indicating development 215 // mode, may remain blank until uploadTools or validate is called. 216 func (c *UpgradeJujuCommand) initVersions(cfg *config.Config, env environs.Environ) (*upgradeVersions, error) { 217 agent, ok := cfg.AgentVersion() 218 if !ok { 219 // Can't happen. In theory. 220 return nil, fmt.Errorf("incomplete environment configuration") 221 } 222 if c.Version == agent { 223 return nil, errUpToDate 224 } 225 client := version.Current.Number 226 // TODO use an API call rather than requiring the environment, 227 // so that we can restrict access to the provider secrets 228 // while still allowing users to upgrade. 229 available, err := envtools.FindTools(env, client.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry) 230 if err != nil { 231 if !errors.IsNotFoundError(err) { 232 return nil, err 233 } 234 if !c.UploadTools { 235 // No tools found and we shouldn't upload any, so pretend 236 // there is no more recent version available. 237 if c.Version == version.Zero { 238 return nil, errUpToDate 239 } 240 return nil, err 241 } 242 } 243 return &upgradeVersions{ 244 agent: agent, 245 client: client, 246 chosen: c.Version, 247 tools: available, 248 }, nil 249 } 250 251 // upgradeVersions holds the version information for making upgrade decisions. 252 type upgradeVersions struct { 253 agent version.Number 254 client version.Number 255 chosen version.Number 256 tools coretools.List 257 } 258 259 // uploadTools compiles jujud from $GOPATH and uploads it into the supplied 260 // storage. If no version has been explicitly chosen, the version number 261 // reported by the built tools will be based on the client version number. 262 // In any case, the version number reported will have a build component higher 263 // than that of any otherwise-matching available envtools. 264 // uploadTools resets the chosen version and replaces the available tools 265 // with the ones just uploaded. 266 func (v *upgradeVersions) uploadTools(storage storage.Storage, series []string) error { 267 // TODO(fwereade): this is kinda crack: we should not assume that 268 // version.Current matches whatever source happens to be built. The 269 // ideal would be: 270 // 1) compile jujud from $GOPATH into some build dir 271 // 2) get actual version with `jujud version` 272 // 3) check actual version for compatibility with CLI tools 273 // 4) generate unique build version with reference to available tools 274 // 5) force-version that unique version into the dir directly 275 // 6) archive and upload the build dir 276 // ...but there's no way we have time for that now. In the meantime, 277 // considering the use cases, this should work well enough; but it 278 // won't detect an incompatible major-version change, which is a shame. 279 if v.chosen == version.Zero { 280 v.chosen = v.client 281 } 282 v.chosen = uploadVersion(v.chosen, v.tools) 283 284 // TODO(fwereade): envtools.Upload should return envtools.List, and should 285 // include all the extra series we build, so we can set *that* onto 286 // v.available and maybe one day be able to check that a given upgrade 287 // won't leave out-of-date machines lying around, starved of envtools. 288 uploaded, err := sync.Upload(storage, &v.chosen, series...) 289 if err != nil { 290 return err 291 } 292 v.tools = coretools.List{uploaded} 293 return nil 294 } 295 296 // validate chooses an upgrade version, if one has not already been chosen, 297 // and ensures the tools list contains no entries that do not have that version. 298 // If validate returns no error, the environment agent-version can be set to 299 // the value of the chosen field. 300 func (v *upgradeVersions) validate() (err error) { 301 if v.chosen == version.Zero { 302 // No explicitly specified version, so find the next available 303 // stable release to upgrade to, starting from the current agent 304 // version and doing major.minor+1 or +2 as needed. 305 nextStable := v.agent 306 if v.agent.IsDev() { 307 nextStable.Minor += 1 308 } else { 309 nextStable.Minor += 2 310 } 311 312 newestNextStable, found := v.tools.NewestCompatible(nextStable) 313 if found { 314 log.Debugf("found a more recent stable version %s", newestNextStable) 315 v.chosen = newestNextStable 316 } else { 317 newestCurrent, found := v.tools.NewestCompatible(v.agent) 318 if found { 319 log.Debugf("found more recent current version %s", newestCurrent) 320 v.chosen = newestCurrent 321 } else { 322 return fmt.Errorf("no more recent supported versions available") 323 } 324 } 325 } else { 326 // If not completely specified already, pick a single tools version. 327 filter := coretools.Filter{Number: v.chosen, Released: !v.chosen.IsDev()} 328 if v.tools, err = v.tools.Match(filter); err != nil { 329 return err 330 } 331 v.chosen, v.tools = v.tools.Newest() 332 } 333 if v.chosen == v.agent { 334 return errUpToDate 335 } 336 337 // Major version upgrade 338 if v.chosen.Major < v.agent.Major { 339 // TODO(fwereade): I'm a bit concerned about old agent/CLI tools even 340 // *connecting* to environments with higher agent-versions; but ofc they 341 // have to connect in order to discover they shouldn't. However, once 342 // any of our tools detect an incompatible version, they should act to 343 // minimize damage: the CLI should abort politely, and the agents should 344 // run an Upgrader but no other tasks. 345 return fmt.Errorf("cannot change major version from %d to %d", v.agent.Major, v.chosen.Major) 346 } else if v.chosen.Major > v.agent.Major { 347 return fmt.Errorf("major version upgrades are not supported yet") 348 } 349 350 return nil 351 } 352 353 // uploadVersion returns a copy of the supplied version with a build number 354 // higher than any of the supplied tools that share its major, minor and patch. 355 func uploadVersion(vers version.Number, existing coretools.List) version.Number { 356 vers.Build++ 357 for _, t := range existing { 358 if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { 359 continue 360 } 361 if t.Version.Build >= vers.Build { 362 vers.Build = t.Version.Build + 1 363 } 364 } 365 return vers 366 }