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 }