github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/machine/upgradeseries.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package machine 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/juju/cmd" 11 "github.com/juju/collections/set" 12 "github.com/juju/errors" 13 "github.com/juju/gnuflag" 14 "github.com/juju/os/series" 15 "gopkg.in/juju/names.v2" 16 "gopkg.in/juju/worker.v1/catacomb" 17 18 "github.com/juju/juju/api" 19 "github.com/juju/juju/api/machinemanager" 20 "github.com/juju/juju/apiserver/params" 21 jujucmd "github.com/juju/juju/cmd" 22 "github.com/juju/juju/cmd/modelcmd" 23 "github.com/juju/juju/core/watcher" 24 ) 25 26 // Actions 27 const ( 28 PrepareCommand = "prepare" 29 CompleteCommand = "complete" 30 ) 31 32 var upgradeSeriesConfirmationMsg = ` 33 WARNING: This command will mark machine %q as being upgraded to series %q. 34 This operation cannot be reverted or canceled once started. 35 %s 36 Continue [y/N]?`[1:] 37 38 var upgradeSeriesAffectedMsg = ` 39 Units running on the machine will also be upgraded. These units include: 40 %s 41 42 Leadership for the following applications will be pinned and not 43 subject to change until the "complete" command is run: 44 %s 45 `[1:] 46 47 const UpgradeSeriesPrepareFinishedMessage = ` 48 Juju is now ready for the series to be updated. 49 Perform any manual steps required along with "do-release-upgrade". 50 When ready, run the following to complete the upgrade series process: 51 52 juju upgrade-series %s complete` 53 54 const UpgradeSeriesCompleteFinishedMessage = ` 55 Upgrade series for machine %q has successfully completed` 56 57 // NewUpgradeSeriesCommand returns a command which upgrades the series of 58 // an application or machine. 59 func NewUpgradeSeriesCommand() cmd.Command { 60 return modelcmd.Wrap(&upgradeSeriesCommand{}) 61 } 62 63 //go:generate mockgen -package mocks -destination mocks/upgradeMachineSeriesAPI_mock.go github.com/juju/juju/cmd/juju/machine UpgradeMachineSeriesAPI 64 type UpgradeMachineSeriesAPI interface { 65 BestAPIVersion() int 66 Close() error 67 UpgradeSeriesValidate(string, string) ([]string, error) 68 UpgradeSeriesPrepare(string, string, bool) error 69 UpgradeSeriesComplete(string) error 70 WatchUpgradeSeriesNotifications(string) (watcher.NotifyWatcher, string, error) 71 GetUpgradeSeriesMessages(string, string) ([]string, error) 72 } 73 74 // upgradeSeriesCommand is responsible for updating the series of an application or machine. 75 type upgradeSeriesCommand struct { 76 modelcmd.ModelCommandBase 77 modelcmd.IAASOnlyCommand 78 79 upgradeMachineSeriesClient UpgradeMachineSeriesAPI 80 81 subCommand string 82 force bool 83 machineNumber string 84 series string 85 yes bool 86 87 catacomb catacomb.Catacomb 88 plan catacomb.Plan 89 } 90 91 var upgradeSeriesDoc = ` 92 Upgrade a machine's operating system series. 93 94 upgrade-series allows users to perform a managed upgrade of the operating system 95 series of a machine. This command is performed in two steps; prepare and complete. 96 97 The "prepare" step notifies Juju that a series upgrade is taking place for a given 98 machine and as such Juju guards that machine against operations that would 99 interfere with the upgrade process. 100 101 The "complete" step notifies juju that the managed upgrade has been successfully completed. 102 103 It should be noted that once the prepare command is issued there is no way to 104 cancel or abort the process. Once you commit to prepare you must complete the 105 process or you will end up with an unusable machine! 106 107 The requested series must be explicitly supported by all charms deployed to 108 the specified machine. To override this constraint the --force option may be used. 109 110 The --force option should be used with caution since using a charm on a machine 111 running an unsupported series may cause unexpected behavior. Alternately, if the 112 requested series is supported in later revisions of the charm, upgrade-charm can 113 run beforehand. 114 115 Examples: 116 117 Prepare machine 3 for upgrade to series "bionic"": 118 119 juju upgrade-series 3 prepare bionic 120 121 Prepare machine 4 for upgrade to series "cosmic" even if there are applications 122 running units that do not support the target series: 123 124 juju upgrade-series 4 prepare cosmic --force 125 126 Complete upgrade of machine 5, indicating that all automatic and any 127 necessary manual upgrade steps have completed successfully: 128 129 juju upgrade-series 5 complete 130 131 See also: 132 machines 133 status 134 upgrade-charm 135 set-series 136 ` 137 138 func (c *upgradeSeriesCommand) Info() *cmd.Info { 139 return jujucmd.Info(&cmd.Info{ 140 Name: "upgrade-series", 141 Args: "<machine> <command> [args]", 142 Purpose: "Upgrade the Ubuntu series of a machine.", 143 Doc: upgradeSeriesDoc, 144 }) 145 } 146 147 func (c *upgradeSeriesCommand) SetFlags(f *gnuflag.FlagSet) { 148 c.ModelCommandBase.SetFlags(f) 149 f.BoolVar(&c.force, "force", false, 150 "Upgrade even if the series is not supported by the charm and/or related subordinate charms.") 151 f.BoolVar(&c.yes, "y", false, 152 "Agree that the operation cannot be reverted or canceled once started without being prompted.") 153 f.BoolVar(&c.yes, "yes", false, "") 154 } 155 156 // Init implements cmd.Command. 157 func (c *upgradeSeriesCommand) Init(args []string) error { 158 if len(args) < 2 { 159 return errors.Errorf("wrong number of arguments") 160 } 161 162 subCommand, err := checkSubCommands([]string{PrepareCommand, CompleteCommand}, args[1]) 163 if err != nil { 164 return errors.Annotate(err, "invalid argument") 165 } 166 c.subCommand = subCommand 167 168 numArguments := 3 169 if c.subCommand == CompleteCommand { 170 numArguments = 2 171 } 172 if len(args) != numArguments { 173 return errors.Errorf("wrong number of arguments") 174 } 175 176 if names.IsValidMachine(args[0]) { 177 c.machineNumber = args[0] 178 } else { 179 return errors.Errorf("%q is an invalid machine name", args[0]) 180 } 181 182 if c.subCommand == PrepareCommand { 183 s, err := checkSeries(series.SupportedSeries(), args[2]) 184 if err != nil { 185 return err 186 } 187 c.series = s 188 } 189 190 return nil 191 } 192 193 // Run implements cmd.Run. 194 func (c *upgradeSeriesCommand) Run(ctx *cmd.Context) error { 195 if c.subCommand == PrepareCommand { 196 err := c.UpgradeSeriesPrepare(ctx) 197 if err != nil { 198 return errors.Trace(err) 199 } 200 } 201 if c.subCommand == CompleteCommand { 202 err := c.UpgradeSeriesComplete(ctx) 203 if err != nil { 204 return errors.Trace(err) 205 } 206 } 207 return nil 208 } 209 210 // UpgradeSeriesPrepare is the interface to the API server endpoint of the same 211 // name. Since this function's interface will be mocked as an external test 212 // dependency this function should contain minimal logic other than gathering an 213 // API handle and making the API call. 214 func (c *upgradeSeriesCommand) UpgradeSeriesPrepare(ctx *cmd.Context) (err error) { 215 apiRoot, err := c.ensureAPIClient() 216 if err != nil { 217 return errors.Trace(err) 218 } 219 if apiRoot != nil { 220 defer apiRoot.Close() 221 } 222 223 units, err := c.upgradeMachineSeriesClient.UpgradeSeriesValidate(c.machineNumber, c.series) 224 if err != nil { 225 return errors.Trace(err) 226 } 227 if err := c.promptConfirmation(ctx, units); err != nil { 228 return errors.Trace(err) 229 } 230 231 if err = c.upgradeMachineSeriesClient.UpgradeSeriesPrepare(c.machineNumber, c.series, c.force); err != nil { 232 return errors.Trace(err) 233 } 234 235 if err = c.handleNotifications(ctx); err != nil { 236 return errors.Trace(err) 237 } 238 239 m := UpgradeSeriesPrepareFinishedMessage + "\n" 240 ctx.Infof(m, c.machineNumber) 241 242 return nil 243 } 244 245 func (c *upgradeSeriesCommand) promptConfirmation(ctx *cmd.Context, affectedUnits []string) error { 246 if c.yes { 247 return nil 248 } 249 250 affectedMsg := "" 251 if len(affectedUnits) > 0 { 252 apps := set.NewStrings() 253 for _, unit := range affectedUnits { 254 app, err := names.UnitApplication(unit) 255 if err != nil { 256 return errors.Annotatef(err, "deriving application for unit %q", unit) 257 } 258 apps.Add(app) 259 } 260 261 affectedMsg = fmt.Sprintf( 262 upgradeSeriesAffectedMsg, strings.Join(affectedUnits, "\n "), strings.Join(apps.SortedValues(), "\n ")) 263 } 264 265 fmt.Fprintf(ctx.Stdout, upgradeSeriesConfirmationMsg, c.machineNumber, c.series, affectedMsg) 266 if err := jujucmd.UserConfirmYes(ctx); err != nil { 267 return errors.Annotate(err, "upgrade series") 268 } 269 return nil 270 } 271 272 func (c *upgradeSeriesCommand) handleNotifications(ctx *cmd.Context) error { 273 if c.plan.Work == nil { 274 c.plan = catacomb.Plan{ 275 Site: &c.catacomb, 276 Work: c.displayNotifications(ctx), 277 } 278 } 279 err := catacomb.Invoke(c.plan) 280 if err != nil { 281 return errors.Trace(err) 282 } 283 err = c.catacomb.Wait() 284 if err != nil { 285 if params.IsCodeStopped(err) { 286 logger.Debugf("the upgrade series watcher has been stopped") 287 } else { 288 return errors.Trace(err) 289 } 290 } 291 return nil 292 } 293 294 // displayNotifications handles the writing of upgrade series notifications to 295 // standard out. 296 func (c *upgradeSeriesCommand) displayNotifications(ctx *cmd.Context) func() error { 297 // We return and anonymous function here to satisfy the catacomb plan's 298 // need for a work function and to close over the commands context. 299 return func() error { 300 uw, wid, err := c.upgradeMachineSeriesClient.WatchUpgradeSeriesNotifications(c.machineNumber) 301 if err != nil { 302 return errors.Trace(err) 303 } 304 err = c.catacomb.Add(uw) 305 if err != nil { 306 return errors.Trace(err) 307 } 308 for { 309 select { 310 case <-c.catacomb.Dying(): 311 return c.catacomb.ErrDying() 312 case <-uw.Changes(): 313 err = c.handleUpgradeSeriesChange(ctx, wid) 314 if err != nil { 315 return errors.Trace(err) 316 } 317 } 318 } 319 } 320 } 321 322 func (c *upgradeSeriesCommand) handleUpgradeSeriesChange(ctx *cmd.Context, wid string) error { 323 messages, err := c.upgradeMachineSeriesClient.GetUpgradeSeriesMessages(c.machineNumber, wid) 324 if err != nil { 325 return errors.Trace(err) 326 } 327 if len(messages) == 0 { 328 return nil 329 } 330 ctx.Infof(strings.Join(messages, "\n")) 331 return nil 332 } 333 334 // UpgradeSeriesComplete completes a series upgrade for a given machine, 335 // that is if a machine is marked as upgrading using UpgradeSeriesPrepare, 336 // then this command will complete that process and the machine will no longer 337 // be marked as upgrading. 338 func (c *upgradeSeriesCommand) UpgradeSeriesComplete(ctx *cmd.Context) error { 339 apiRoot, err := c.ensureAPIClient() 340 if err != nil { 341 return errors.Trace(err) 342 } 343 if apiRoot != nil { 344 defer apiRoot.Close() 345 } 346 347 if err := c.upgradeMachineSeriesClient.UpgradeSeriesComplete(c.machineNumber); err != nil { 348 return errors.Trace(err) 349 } 350 351 if err := c.handleNotifications(ctx); err != nil { 352 return errors.Trace(err) 353 } 354 355 m := UpgradeSeriesCompleteFinishedMessage + "\n" 356 ctx.Infof(m, c.machineNumber) 357 358 return nil 359 } 360 361 // ensureAPIClient checks to see if the API client is already instantiated. 362 // If not, a new api Connection is created and used to instantiate it. 363 // If it has been set elsewhere (such as by a test) we leave it as is. 364 func (c *upgradeSeriesCommand) ensureAPIClient() (api.Connection, error) { 365 var apiRoot api.Connection 366 if c.upgradeMachineSeriesClient == nil { 367 var err error 368 apiRoot, err = c.NewAPIRoot() 369 if err != nil { 370 return nil, errors.Trace(err) 371 } 372 c.upgradeMachineSeriesClient = machinemanager.NewClient(apiRoot) 373 } 374 return apiRoot, nil 375 } 376 377 func checkSubCommands(validCommands []string, argCommand string) (string, error) { 378 for _, subCommand := range validCommands { 379 if subCommand == argCommand { 380 return subCommand, nil 381 } 382 } 383 384 return "", errors.Errorf("%q is an invalid upgrade-series command; valid commands are: %s.", 385 argCommand, strings.Join(validCommands, ", ")) 386 } 387 388 func checkSeries(supportedSeries []string, seriesArgument string) (string, error) { 389 for _, s := range supportedSeries { 390 if s == strings.ToLower(seriesArgument) { 391 return s, nil 392 } 393 } 394 395 return "", errors.Errorf("%q is an unsupported series", seriesArgument) 396 }