OLD | NEW |
| (Empty) |
1 // Copyright 2012, 2013 Canonical Ltd. | |
2 // Licensed under the AGPLv3, see LICENCE file for details. | |
3 | |
4 package main | |
5 | |
6 import ( | |
7 "fmt" | |
8 "os" | |
9 "time" | |
10 | |
11 "launchpad.net/tomb" | |
12 | |
13 "launchpad.net/juju-core/agent/tools" | |
14 "launchpad.net/juju-core/downloader" | |
15 "launchpad.net/juju-core/environs" | |
16 "launchpad.net/juju-core/errors" | |
17 "launchpad.net/juju-core/log" | |
18 "launchpad.net/juju-core/state" | |
19 "launchpad.net/juju-core/state/watcher" | |
20 "launchpad.net/juju-core/version" | |
21 ) | |
22 | |
23 var upgraderKillDelay = 5 * time.Minute | |
24 | |
25 // An Upgrader observes the version information for an agent in the | |
26 // environment state, and handles the downloading and unpacking of | |
27 // new versions of the juju tools when necessary. | |
28 // | |
29 // When a new version is available Wait and Stop return UpgradeReadyError. | |
30 type Upgrader struct { | |
31 tomb tomb.Tomb | |
32 st *state.State | |
33 agentState AgentState | |
34 dataDir string | |
35 } | |
36 | |
37 // UpgradeReadyError is returned by an Upgrader to report that | |
38 // an upgrade is ready to be performed and a restart is due. | |
39 type UpgradeReadyError struct { | |
40 AgentName string | |
41 OldTools *tools.Tools | |
42 NewTools *tools.Tools | |
43 DataDir string | |
44 } | |
45 | |
46 func (e *UpgradeReadyError) Error() string { | |
47 return "must restart: an agent upgrade is available" | |
48 } | |
49 | |
50 // ChangeAgentTools does the actual agent upgrade. | |
51 func (e *UpgradeReadyError) ChangeAgentTools() error { | |
52 tools, err := tools.ChangeAgentTools(e.DataDir, e.AgentName, e.NewTools.
Version) | |
53 if err != nil { | |
54 return err | |
55 } | |
56 log.Infof("upgrader upgraded from %v to %v (%q)", e.OldTools.Version, to
ols.Version, tools.URL) | |
57 return nil | |
58 } | |
59 | |
60 // NewUpgrader returns a new Upgrader watching the given agent. | |
61 func NewUpgrader(st *state.State, agentState AgentState, dataDir string) *Upgrad
er { | |
62 u := &Upgrader{ | |
63 st: st, | |
64 agentState: agentState, | |
65 dataDir: dataDir, | |
66 } | |
67 go func() { | |
68 defer u.tomb.Done() | |
69 u.tomb.Kill(u.run()) | |
70 }() | |
71 return u | |
72 } | |
73 | |
74 func (u *Upgrader) String() string { | |
75 return "upgrader" | |
76 } | |
77 | |
78 func (u *Upgrader) Kill() { | |
79 u.tomb.Kill(nil) | |
80 } | |
81 | |
82 func (u *Upgrader) Stop() error { | |
83 u.tomb.Kill(nil) | |
84 return u.tomb.Wait() | |
85 } | |
86 | |
87 func (u *Upgrader) Wait() error { | |
88 return u.tomb.Wait() | |
89 } | |
90 | |
91 func (u *Upgrader) run() error { | |
92 // Let the state know the version that is currently running. | |
93 currentTools, err := tools.ReadTools(u.dataDir, version.Current) | |
94 if err != nil { | |
95 // Don't abort everything because we can't find the tools direct
ory. | |
96 // The problem should sort itself out as we will immediately | |
97 // download some more tools and upgrade. | |
98 log.Warningf("upgrader cannot read current tools: %v", err) | |
99 currentTools = &tools.Tools{ | |
100 Version: version.Current, | |
101 } | |
102 } | |
103 err = u.agentState.SetAgentTools(currentTools) | |
104 if err != nil { | |
105 return err | |
106 } | |
107 | |
108 // TODO(fwereade): this whole package should be ignorant of environs, | |
109 // so it shouldn't be watching environ config (and it shouldn't be | |
110 // looking in storage): we should be able to find out what to download | |
111 // from state, exactly as we do for charms. | |
112 w := u.st.WatchEnvironConfig() | |
113 defer watcher.Stop(w, &u.tomb) | |
114 | |
115 // Rather than using worker.WaitForEnviron, invalid environments are | |
116 // managed explicitly so that all configuration changes are observed | |
117 // by the loop below. | |
118 var environ environs.Environ | |
119 | |
120 // TODO(rog) retry downloads when they fail. | |
121 var ( | |
122 download *downloader.Download | |
123 downloadTools *tools.Tools | |
124 downloadDone <-chan downloader.Status | |
125 ) | |
126 // If we're killed early on (probably as a result of some other | |
127 // task dying) we allow ourselves some time to try to connect to | |
128 // the state and download a new version. We return to normal | |
129 // undelayed behaviour when: | |
130 // 1) We find there's no upgrade to do. | |
131 // 2) A download fails. | |
132 tomb := delayedTomb(&u.tomb, upgraderKillDelay) | |
133 noDelay := func() { | |
134 if tomb != &u.tomb { | |
135 tomb.Kill(nil) | |
136 tomb = &u.tomb | |
137 } | |
138 } | |
139 for { | |
140 // We wait for the tools to change while we're downloading | |
141 // so that if something goes wrong (for instance a bad URL | |
142 // hangs up) another change to the proposed tools can | |
143 // potentially fix things. | |
144 select { | |
145 case cfg, ok := <-w.Changes(): | |
146 if !ok { | |
147 return watcher.MustErr(w) | |
148 } | |
149 var err error | |
150 if environ == nil { | |
151 environ, err = environs.New(cfg) | |
152 if err != nil { | |
153 log.Errorf("upgrader loaded invalid init
ial environment configuration: %v", err) | |
154 break | |
155 } | |
156 } else { | |
157 err = environ.SetConfig(cfg) | |
158 if err != nil { | |
159 log.Warningf("upgrader loaded invalid en
vironment configuration: %v", err) | |
160 // continue on, because the version numb
er is still significant. | |
161 } | |
162 } | |
163 proposed, ok := cfg.AgentVersion() | |
164 if !ok { | |
165 // This shouldn't be possible; but if it happens
it's no reason | |
166 // to kill this task. Just wait for the config t
o change again. | |
167 continue | |
168 } | |
169 if download != nil { | |
170 // There's a download in progress, stop it if we
need to. | |
171 if downloadTools.Version.Number == proposed { | |
172 // We are already downloading the reques
ted tools. | |
173 break | |
174 } | |
175 // Tools changed. We need to stop and restart. | |
176 download.Stop() | |
177 download, downloadTools, downloadDone = nil, nil
, nil | |
178 } | |
179 // TODO: major version upgrades. | |
180 if proposed.Major != version.Current.Major { | |
181 log.Errorf("major version upgrades are not suppo
rted yet") | |
182 noDelay() | |
183 break | |
184 } | |
185 if proposed == version.Current.Number { | |
186 noDelay() | |
187 break | |
188 } | |
189 required := version.Binary{ | |
190 Number: proposed, | |
191 Series: version.Current.Series, | |
192 Arch: version.Current.Arch, | |
193 } | |
194 if tools, err := tools.ReadTools(u.dataDir, required); e
rr == nil { | |
195 // The exact tools have already been downloaded,
so use them. | |
196 return u.upgradeReady(currentTools, tools) | |
197 } | |
198 tools, err := environs.FindExactTools(environ, required) | |
199 if err != nil { | |
200 log.Errorf("upgrader error finding tools for %v:
%v", required, err) | |
201 if !errors.IsNotFoundError(err) { | |
202 return err | |
203 } | |
204 noDelay() | |
205 // TODO(rog): poll until tools become available. | |
206 break | |
207 } | |
208 log.Infof("upgrader downloading %q", tools.URL) | |
209 download = downloader.New(tools.URL, "") | |
210 downloadTools = tools | |
211 downloadDone = download.Done() | |
212 case status := <-downloadDone: | |
213 newTools := downloadTools | |
214 download, downloadTools, downloadDone = nil, nil, nil | |
215 if status.Err != nil { | |
216 log.Errorf("upgrader download of %v failed: %v",
newTools.Version, status.Err) | |
217 noDelay() | |
218 break | |
219 } | |
220 err := tools.UnpackTools(u.dataDir, newTools, status.Fil
e) | |
221 status.File.Close() | |
222 if err := os.Remove(status.File.Name()); err != nil { | |
223 log.Warningf("upgrader cannot remove temporary d
ownload file: %v", err) | |
224 } | |
225 if err != nil { | |
226 log.Errorf("upgrader cannot unpack %v tools: %v"
, newTools.Version, err) | |
227 noDelay() | |
228 break | |
229 } | |
230 return u.upgradeReady(currentTools, newTools) | |
231 case <-tomb.Dying(): | |
232 if download != nil { | |
233 return fmt.Errorf("upgrader aborted download of
%q", downloadTools.URL) | |
234 } | |
235 return nil | |
236 } | |
237 } | |
238 panic("not reached") | |
239 } | |
240 | |
241 func (u *Upgrader) upgradeReady(old, new *tools.Tools) *UpgradeReadyError { | |
242 return &UpgradeReadyError{ | |
243 AgentName: u.agentState.Tag(), | |
244 OldTools: old, | |
245 DataDir: u.dataDir, | |
246 NewTools: new, | |
247 } | |
248 } | |
249 | |
250 // delayedTomb returns a tomb that starts dying a given duration | |
251 // after t starts dying. | |
252 func delayedTomb(t *tomb.Tomb, d time.Duration) *tomb.Tomb { | |
253 var delayed tomb.Tomb | |
254 go func() { | |
255 select { | |
256 case <-t.Dying(): | |
257 time.Sleep(d) | |
258 delayed.Kill(nil) | |
259 case <-delayed.Dying(): | |
260 return | |
261 } | |
262 }() | |
263 return &delayed | |
264 } | |
OLD | NEW |