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  }