github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/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 "os" 10 "path" 11 12 "github.com/juju/cmd" 13 "launchpad.net/gnuflag" 14 15 "github.com/juju/juju/cmd/envcmd" 16 "github.com/juju/juju/environs" 17 "github.com/juju/juju/environs/bootstrap" 18 "github.com/juju/juju/environs/config" 19 "github.com/juju/juju/environs/sync" 20 envtools "github.com/juju/juju/environs/tools" 21 "github.com/juju/juju/juju" 22 "github.com/juju/juju/state/api" 23 "github.com/juju/juju/state/api/params" 24 coretools "github.com/juju/juju/tools" 25 "github.com/juju/juju/version" 26 ) 27 28 // UpgradeJujuCommand upgrades the agents in a juju installation. 29 type UpgradeJujuCommand struct { 30 envcmd.EnvCommandBase 31 vers string 32 Version version.Number 33 UploadTools bool 34 Series []string 35 } 36 37 var upgradeJujuDoc = ` 38 The upgrade-juju command upgrades a running environment by setting a version 39 number for all juju agents to run. By default, it chooses the most recent 40 supported version compatible with the command-line tools version. 41 42 A development version is defined to be any version with an odd minor 43 version or a nonzero build component (for example version 2.1.1, 3.3.0 44 and 2.0.0.1 are development versions; 2.0.3 and 3.4.1 are not). A 45 development version may be chosen in two cases: 46 47 - when the current agent version is a development one and there is 48 a more recent version available with the same major.minor numbers; 49 - when an explicit --version major.minor is given (e.g. --version 1.17, 50 or 1.17.2, but not just 1) 51 52 For development use, the --upload-tools flag specifies that the juju tools will 53 packaged (or compiled locally, if no jujud binaries exists, for which you will 54 need the golang packages installed) and uploaded before the version is set. 55 Currently the tools will be uploaded as if they had the version of the current 56 juju tool, unless specified otherwise by the --version flag. 57 58 When run without arguments. upgrade-juju will try to upgrade to the 59 following versions, in order of preference, depending on the current 60 value of the environment's agent-version setting: 61 62 - The highest patch.build version of the *next* stable major.minor version. 63 - The highest patch.build version of the *current* major.minor version. 64 65 Both of these depend on tools availability, which some situations (no 66 outgoing internet access) and provider types (such as maas) require that 67 you manage yourself; see the documentation for "sync-tools". 68 ` 69 70 func (c *UpgradeJujuCommand) Info() *cmd.Info { 71 return &cmd.Info{ 72 Name: "upgrade-juju", 73 Purpose: "upgrade the tools in a juju environment", 74 Doc: upgradeJujuDoc, 75 } 76 } 77 78 func (c *UpgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) { 79 f.StringVar(&c.vers, "version", "", "upgrade to specific version") 80 f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools") 81 f.Var(newSeriesValue(nil, &c.Series), "series", "upload tools for supplied comma-separated series list") 82 } 83 84 func (c *UpgradeJujuCommand) Init(args []string) error { 85 if c.vers != "" { 86 vers, err := version.Parse(c.vers) 87 if err != nil { 88 return err 89 } 90 if vers.Major != version.Current.Major { 91 return fmt.Errorf("cannot upgrade to version incompatible with CLI") 92 } 93 if c.UploadTools && vers.Build != 0 { 94 // TODO(fwereade): when we start taking versions from actual built 95 // code, we should disable --version when used with --upload-tools. 96 // For now, it's the only way to experiment with version upgrade 97 // behaviour live, so the only restriction is that Build cannot 98 // be used (because its value needs to be chosen internally so as 99 // not to collide with existing tools). 100 return fmt.Errorf("cannot specify build number when uploading tools") 101 } 102 c.Version = vers 103 } 104 if len(c.Series) > 0 && !c.UploadTools { 105 return fmt.Errorf("--series requires --upload-tools") 106 } 107 return cmd.CheckEmpty(args) 108 } 109 110 var errUpToDate = stderrors.New("no upgrades available") 111 112 // Run changes the version proposed for the juju envtools. 113 func (c *UpgradeJujuCommand) Run(_ *cmd.Context) (err error) { 114 client, err := juju.NewAPIClientFromName(c.EnvName) 115 if err != nil { 116 return err 117 } 118 defer client.Close() 119 defer func() { 120 if err == errUpToDate { 121 logger.Infof(err.Error()) 122 err = nil 123 } 124 }() 125 126 // Determine the version to upgrade to, uploading tools if necessary. 127 attrs, err := client.EnvironmentGet() 128 if err != nil { 129 return err 130 } 131 cfg, err := config.New(config.NoDefaults, attrs) 132 if err != nil { 133 return err 134 } 135 context, err := c.initVersions(client, cfg) 136 if err != nil { 137 return err 138 } 139 if c.UploadTools { 140 series := bootstrap.SeriesToUpload(cfg, c.Series) 141 if err := context.uploadTools(series); err != nil { 142 return err 143 } 144 } 145 if err := context.validate(); err != nil { 146 return err 147 } 148 logger.Infof("upgrade version chosen: %s", context.chosen) 149 // TODO(fwereade): this list may be incomplete, pending envtools.Upload change. 150 logger.Infof("available tools: %s", context.tools) 151 152 if err := client.SetEnvironAgentVersion(context.chosen); err != nil { 153 return err 154 } 155 logger.Infof("started upgrade to %s", context.chosen) 156 return nil 157 } 158 159 // initVersions collects state relevant to an upgrade decision. The returned 160 // agent and client versions, and the list of currently available tools, will 161 // always be accurate; the chosen version, and the flag indicating development 162 // mode, may remain blank until uploadTools or validate is called. 163 func (c *UpgradeJujuCommand) initVersions(client *api.Client, cfg *config.Config) (*upgradeContext, error) { 164 agent, ok := cfg.AgentVersion() 165 if !ok { 166 // Can't happen. In theory. 167 return nil, fmt.Errorf("incomplete environment configuration") 168 } 169 if c.Version == agent { 170 return nil, errUpToDate 171 } 172 clientVersion := version.Current.Number 173 findResult, err := client.FindTools(clientVersion.Major, -1, "", "") 174 var availableTools coretools.List 175 if params.IsCodeNotImplemented(err) { 176 availableTools, err = findTools1dot17(cfg) 177 } else { 178 availableTools = findResult.List 179 } 180 if err != nil { 181 return nil, err 182 } 183 err = findResult.Error 184 if findResult.Error != nil { 185 if !params.IsCodeNotFound(err) { 186 return nil, err 187 } 188 if !c.UploadTools { 189 // No tools found and we shouldn't upload any, so if we are not asking for a 190 // major upgrade, pretend there is no more recent version available. 191 if c.Version == version.Zero && agent.Major == clientVersion.Major { 192 return nil, errUpToDate 193 } 194 return nil, err 195 } 196 } 197 return &upgradeContext{ 198 agent: agent, 199 client: clientVersion, 200 chosen: c.Version, 201 tools: availableTools, 202 apiClient: client, 203 config: cfg, 204 }, nil 205 } 206 207 // findTools1dot17 allows 1.17.x versions to be upgraded. 208 func findTools1dot17(cfg *config.Config) (coretools.List, error) { 209 logger.Warningf("running find tools in 1.17 compatibility mode") 210 env, err := environs.New(cfg) 211 if err != nil { 212 return nil, err 213 } 214 clientVersion := version.Current.Number 215 return envtools.FindTools(env, clientVersion.Major, -1, coretools.Filter{}, envtools.DoNotAllowRetry) 216 } 217 218 // upgradeContext holds the version information for making upgrade decisions. 219 type upgradeContext struct { 220 agent version.Number 221 client version.Number 222 chosen version.Number 223 tools coretools.List 224 config *config.Config 225 apiClient *api.Client 226 } 227 228 // uploadTools compiles jujud from $GOPATH and uploads it into the supplied 229 // storage. If no version has been explicitly chosen, the version number 230 // reported by the built tools will be based on the client version number. 231 // In any case, the version number reported will have a build component higher 232 // than that of any otherwise-matching available envtools. 233 // uploadTools resets the chosen version and replaces the available tools 234 // with the ones just uploaded. 235 func (context *upgradeContext) uploadTools(series []string) (err error) { 236 // TODO(fwereade): this is kinda crack: we should not assume that 237 // version.Current matches whatever source happens to be built. The 238 // ideal would be: 239 // 1) compile jujud from $GOPATH into some build dir 240 // 2) get actual version with `jujud version` 241 // 3) check actual version for compatibility with CLI tools 242 // 4) generate unique build version with reference to available tools 243 // 5) force-version that unique version into the dir directly 244 // 6) archive and upload the build dir 245 // ...but there's no way we have time for that now. In the meantime, 246 // considering the use cases, this should work well enough; but it 247 // won't detect an incompatible major-version change, which is a shame. 248 if context.chosen == version.Zero { 249 context.chosen = context.client 250 } 251 context.chosen = uploadVersion(context.chosen, context.tools) 252 253 builtTools, err := sync.BuildToolsTarball(&context.chosen) 254 if err != nil { 255 return err 256 } 257 defer os.RemoveAll(builtTools.Dir) 258 259 var uploaded *coretools.Tools 260 toolsPath := path.Join(builtTools.Dir, builtTools.StorageName) 261 logger.Infof("uploading tools %v (%dkB) to Juju state server", builtTools.Version, (builtTools.Size+512)/1024) 262 uploaded, err = context.apiClient.UploadTools(toolsPath, builtTools.Version, series...) 263 if params.IsCodeNotImplemented(err) { 264 uploaded, err = context.uploadTools1dot17(builtTools, series...) 265 } 266 if err != nil { 267 return err 268 } 269 context.tools = coretools.List{uploaded} 270 return nil 271 } 272 273 func (context *upgradeContext) uploadTools1dot17(builtTools *sync.BuiltTools, 274 series ...string) (*coretools.Tools, error) { 275 276 logger.Warningf("running upload tools in 1.17 compatibility mode") 277 env, err := environs.New(context.config) 278 if err != nil { 279 return nil, err 280 } 281 return sync.SyncBuiltTools(env.Storage(), builtTools, series...) 282 } 283 284 // validate chooses an upgrade version, if one has not already been chosen, 285 // and ensures the tools list contains no entries that do not have that version. 286 // If validate returns no error, the environment agent-version can be set to 287 // the value of the chosen field. 288 func (context *upgradeContext) validate() (err error) { 289 if context.chosen == version.Zero { 290 // No explicitly specified version, so find the version to which we 291 // need to upgrade. If the CLI and agent major versions match, we find 292 // next available stable release to upgrade to by incrementing the 293 // minor version, starting from the current agent version and doing 294 // major.minor+1 or +2 as needed. If the CLI has a greater major version, 295 // we just use the CLI version as is. 296 nextVersion := context.agent 297 if nextVersion.Major == context.client.Major { 298 if context.agent.IsDev() { 299 nextVersion.Minor += 1 300 } else { 301 nextVersion.Minor += 2 302 } 303 } else { 304 nextVersion = context.client 305 } 306 307 newestNextStable, found := context.tools.NewestCompatible(nextVersion) 308 if found { 309 logger.Debugf("found a more recent stable version %s", newestNextStable) 310 context.chosen = newestNextStable 311 } else { 312 newestCurrent, found := context.tools.NewestCompatible(context.agent) 313 if found { 314 logger.Debugf("found more recent current version %s", newestCurrent) 315 context.chosen = newestCurrent 316 } else { 317 if context.agent.Major != context.client.Major { 318 return fmt.Errorf("no compatible tools available") 319 } else { 320 return fmt.Errorf("no more recent supported versions available") 321 } 322 } 323 } 324 } else { 325 // If not completely specified already, pick a single tools version. 326 filter := coretools.Filter{Number: context.chosen, Released: !context.chosen.IsDev()} 327 if context.tools, err = context.tools.Match(filter); err != nil { 328 return err 329 } 330 context.chosen, context.tools = context.tools.Newest() 331 } 332 if context.chosen == context.agent { 333 return errUpToDate 334 } 335 336 // Disallow major.minor version downgrades. 337 if context.chosen.Major < context.agent.Major || 338 context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor { 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 version from %s to %s", context.agent, context.chosen) 346 } 347 348 return nil 349 } 350 351 // uploadVersion returns a copy of the supplied version with a build number 352 // higher than any of the supplied tools that share its major, minor and patch. 353 func uploadVersion(vers version.Number, existing coretools.List) version.Number { 354 vers.Build++ 355 for _, t := range existing { 356 if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch { 357 continue 358 } 359 if t.Version.Build >= vers.Build { 360 vers.Build = t.Version.Build + 1 361 } 362 } 363 return vers 364 }