github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/worker/upgrader/upgrader.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package upgrader
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"time"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"github.com/juju/utils"
    14  	"github.com/juju/utils/arch"
    15  	"github.com/juju/utils/series"
    16  	"github.com/juju/version"
    17  	"gopkg.in/juju/names.v2"
    18  
    19  	"github.com/juju/juju/agent"
    20  	agenttools "github.com/juju/juju/agent/tools"
    21  	"github.com/juju/juju/api/upgrader"
    22  	coretools "github.com/juju/juju/tools"
    23  	jujuversion "github.com/juju/juju/version"
    24  	"github.com/juju/juju/worker/catacomb"
    25  	"github.com/juju/juju/worker/gate"
    26  )
    27  
    28  // retryAfter returns a channel that receives a value
    29  // when a failed download should be retried.
    30  var retryAfter = func() <-chan time.Time {
    31  	// TODO(fwereade): 2016-03-17 lp:1558657
    32  	return time.After(5 * time.Second)
    33  }
    34  
    35  var logger = loggo.GetLogger("juju.worker.upgrader")
    36  
    37  // Upgrader represents a worker that watches the state for upgrade
    38  // requests.
    39  type Upgrader struct {
    40  	catacomb                    catacomb.Catacomb
    41  	st                          *upgrader.State
    42  	dataDir                     string
    43  	tag                         names.Tag
    44  	origAgentVersion            version.Number
    45  	upgradeStepsWaiter          gate.Waiter
    46  	initialUpgradeCheckComplete gate.Unlocker
    47  }
    48  
    49  // NewAgentUpgrader returns a new upgrader worker. It watches changes to the
    50  // current version of the current agent (with the given tag) and tries to
    51  // download the tools for any new version into the given data directory.  If
    52  // an upgrade is needed, the worker will exit with an UpgradeReadyError
    53  // holding details of the requested upgrade. The tools will have been
    54  // downloaded and unpacked.
    55  func NewAgentUpgrader(
    56  	st *upgrader.State,
    57  	agentConfig agent.Config,
    58  	origAgentVersion version.Number,
    59  	upgradeStepsWaiter gate.Waiter,
    60  	initialUpgradeCheckComplete gate.Unlocker,
    61  ) (*Upgrader, error) {
    62  	u := &Upgrader{
    63  		st:                          st,
    64  		dataDir:                     agentConfig.DataDir(),
    65  		tag:                         agentConfig.Tag(),
    66  		origAgentVersion:            origAgentVersion,
    67  		upgradeStepsWaiter:          upgradeStepsWaiter,
    68  		initialUpgradeCheckComplete: initialUpgradeCheckComplete,
    69  	}
    70  	err := catacomb.Invoke(catacomb.Plan{
    71  		Site: &u.catacomb,
    72  		Work: u.loop,
    73  	})
    74  	if err != nil {
    75  		return nil, errors.Trace(err)
    76  	}
    77  	return u, nil
    78  }
    79  
    80  // Kill implements worker.Worker.Kill.
    81  func (u *Upgrader) Kill() {
    82  	u.catacomb.Kill(nil)
    83  }
    84  
    85  // Wait implements worker.Worker.Wait.
    86  func (u *Upgrader) Wait() error {
    87  	return u.catacomb.Wait()
    88  }
    89  
    90  // Stop stops the upgrader and returns any
    91  // error it encountered when running.
    92  func (u *Upgrader) Stop() error {
    93  	u.Kill()
    94  	return u.Wait()
    95  }
    96  
    97  // allowedTargetVersion checks if targetVersion is too different from
    98  // curVersion to allow a downgrade.
    99  func allowedTargetVersion(
   100  	origAgentVersion version.Number,
   101  	curVersion version.Number,
   102  	upgradeStepsRunning bool,
   103  	targetVersion version.Number,
   104  ) bool {
   105  	if upgradeStepsRunning && targetVersion == origAgentVersion {
   106  		return true
   107  	}
   108  	if targetVersion.Major < curVersion.Major {
   109  		return false
   110  	}
   111  	if targetVersion.Major == curVersion.Major && targetVersion.Minor < curVersion.Minor {
   112  		return false
   113  	}
   114  	return true
   115  }
   116  
   117  func (u *Upgrader) loop() error {
   118  	// Start by reporting current tools (which includes arch/series, and is
   119  	// used by the controller in communicating the desired version below).
   120  	if err := u.st.SetVersion(u.tag.String(), toBinaryVersion(jujuversion.Current)); err != nil {
   121  		return errors.Annotate(err, "cannot set agent version")
   122  	}
   123  
   124  	// We don't read on the dying channel until we have received the
   125  	// initial event from the API version watcher, thus ensuring
   126  	// that we attempt an upgrade even if other workers are dying
   127  	// all around us. Similarly, we don't want to bind the watcher
   128  	// to the catacomb's lifetime (yet!) lest we wait forever for a
   129  	// stopped watcher.
   130  	//
   131  	// However, that absolutely depends on versionWatcher's guaranteed
   132  	// initial event, and we should assume that it'll break its contract
   133  	// sometime. So we allow the watcher to wait patiently for the event
   134  	// for a full minute; but after that we proceed regardless.
   135  	versionWatcher, err := u.st.WatchAPIVersion(u.tag.String())
   136  	if err != nil {
   137  		return errors.Trace(err)
   138  	}
   139  	logger.Infof("abort check blocked until version event received")
   140  	// TODO(fwereade): 2016-03-17 lp:1558657
   141  	mustProceed := time.After(time.Minute)
   142  	var dying <-chan struct{}
   143  	allowDying := func() {
   144  		if dying == nil {
   145  			logger.Infof("unblocking abort check")
   146  			mustProceed = nil
   147  			dying = u.catacomb.Dying()
   148  			if err := u.catacomb.Add(versionWatcher); err != nil {
   149  				u.catacomb.Kill(err)
   150  			}
   151  		}
   152  	}
   153  
   154  	var retry <-chan time.Time
   155  	for {
   156  		select {
   157  		// NOTE: retry and dying both start out nil, so they can't be chosen
   158  		// first time round the loop. However...
   159  		case <-retry:
   160  		case <-dying:
   161  			return u.catacomb.ErrDying()
   162  		// ...*every* other case *must* allowDying(), before doing anything
   163  		// else, lest an error cause us to leak versionWatcher.
   164  		case <-mustProceed:
   165  			logger.Infof("version event not received after one minute")
   166  			allowDying()
   167  		case _, ok := <-versionWatcher.Changes():
   168  			allowDying()
   169  			if !ok {
   170  				return errors.New("version watcher closed")
   171  			}
   172  		}
   173  
   174  		wantVersion, err := u.st.DesiredVersion(u.tag.String())
   175  		if err != nil {
   176  			return err
   177  		}
   178  		logger.Infof("desired tool version: %v", wantVersion)
   179  
   180  		if wantVersion == jujuversion.Current {
   181  			u.initialUpgradeCheckComplete.Unlock()
   182  			continue
   183  		} else if !allowedTargetVersion(
   184  			u.origAgentVersion,
   185  			jujuversion.Current,
   186  			!u.upgradeStepsWaiter.IsUnlocked(),
   187  			wantVersion,
   188  		) {
   189  			// See also bug #1299802 where when upgrading from
   190  			// 1.16 to 1.18 there is a race condition that can
   191  			// cause the unit agent to upgrade, and then want to
   192  			// downgrade when its associate machine agent has not
   193  			// finished upgrading.
   194  			logger.Infof("desired tool version: %s is older than current %s, refusing to downgrade",
   195  				wantVersion, jujuversion.Current)
   196  			u.initialUpgradeCheckComplete.Unlock()
   197  			continue
   198  		}
   199  		logger.Infof("upgrade requested from %v to %v", jujuversion.Current, wantVersion)
   200  
   201  		// Check if tools have already been downloaded.
   202  		wantVersionBinary := toBinaryVersion(wantVersion)
   203  		if u.toolsAlreadyDownloaded(wantVersionBinary) {
   204  			return u.newUpgradeReadyError(wantVersionBinary)
   205  		}
   206  
   207  		// Check if tools are available for download.
   208  		wantToolsList, err := u.st.Tools(u.tag.String())
   209  		if err != nil {
   210  			// Not being able to lookup Tools is considered fatal
   211  			return err
   212  		}
   213  		// The worker cannot be stopped while we're downloading
   214  		// the tools - this means that even if the API is going down
   215  		// repeatedly (causing the agent to be stopped), as long
   216  		// as we have got as far as this, we will still be able to
   217  		// upgrade the agent.
   218  		for _, wantTools := range wantToolsList {
   219  			err = u.ensureTools(wantTools)
   220  			if err == nil {
   221  				return u.newUpgradeReadyError(wantTools.Version)
   222  			}
   223  			logger.Errorf("failed to fetch tools from %q: %v", wantTools.URL, err)
   224  		}
   225  		retry = retryAfter()
   226  	}
   227  }
   228  
   229  func toBinaryVersion(vers version.Number) version.Binary {
   230  	outVers := version.Binary{
   231  		Number: vers,
   232  		Arch:   arch.HostArch(),
   233  		Series: series.HostSeries(),
   234  	}
   235  	return outVers
   236  }
   237  
   238  func (u *Upgrader) toolsAlreadyDownloaded(wantVersion version.Binary) bool {
   239  	_, err := agenttools.ReadTools(u.dataDir, wantVersion)
   240  	return err == nil
   241  }
   242  
   243  func (u *Upgrader) newUpgradeReadyError(newVersion version.Binary) *UpgradeReadyError {
   244  	return &UpgradeReadyError{
   245  		OldTools:  toBinaryVersion(jujuversion.Current),
   246  		NewTools:  newVersion,
   247  		AgentName: u.tag.String(),
   248  		DataDir:   u.dataDir,
   249  	}
   250  }
   251  
   252  func (u *Upgrader) ensureTools(agentTools *coretools.Tools) error {
   253  	logger.Infof("fetching tools from %q", agentTools.URL)
   254  	// The reader MUST verify the tools' hash, so there is no
   255  	// need to validate the peer. We cannot anyway: see http://pad.lv/1261780.
   256  	resp, err := utils.GetNonValidatingHTTPClient().Get(agentTools.URL)
   257  	if err != nil {
   258  		return err
   259  	}
   260  	defer resp.Body.Close()
   261  	if resp.StatusCode != http.StatusOK {
   262  		return fmt.Errorf("bad HTTP response: %v", resp.Status)
   263  	}
   264  	err = agenttools.UnpackTools(u.dataDir, agentTools, resp.Body)
   265  	if err != nil {
   266  		return fmt.Errorf("cannot unpack tools: %v", err)
   267  	}
   268  	logger.Infof("unpacked tools %s to %s", agentTools.Version, u.dataDir)
   269  	return nil
   270  }