Merge lp:~smoser/cloud-init/run-status into lp:~cloud-init-dev/cloud-init/trunk

Proposed by Scott Moser
Status: Merged
Merged at revision: 964
Proposed branch: lp:~smoser/cloud-init/run-status
Merge into: lp:~cloud-init-dev/cloud-init/trunk
Diff against target: 280 lines (+175/-15)
3 files modified
bin/cloud-init (+121/-14)
cloudinit/util.py (+3/-1)
doc/status.txt (+51/-0)
To merge this branch: bzr merge lp:~smoser/cloud-init/run-status
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+208056@code.launchpad.net

Description of the change

Add 'status' output

This adds a 'status' output in /var/lib/cloud/data/status.json and /var/lib/cloud/data/result.json.

status.json provides status of each of the cloud-init modes (init, init-local, config-modules, config-final).
result.json provides a more simplistic "did it finish successfully", and "did it finish".

The presense of result.json will tell you if it ran already, and parsing and looking at len(errors) tells you if there were errors.

There are symlinks provided to /run to make sure these files aren't confused with prior boots.

One less than ideal thing is that none of this is configurable at the moment.

To post a comment you must log in.
Revision history for this message
Robie Basak (racb) wrote :

This looks great, and I believe the behaviour you describe will fulfil my use case. Thanks!

One minor nitpick. When you overwrite status.json, is there a race when a reader might see an incompletely written file, or have I missed something? Can you rename the file in atomically? Same for result.json. Then readers are guaranteed to always be able to parse the files if they exist. I don't see the symlinks needing any special behaviour here, as they'll atomically switch to pointing to the new (complete) files as soon as they are renamed in.

lp:~smoser/cloud-init/run-status updated
961. By Scott Moser

be atomic when writing status files

962. By Scott Moser

version space (v1:) result_path json also

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/cloud-init'
2--- bin/cloud-init 2014-01-09 00:16:24 +0000
3+++ bin/cloud-init 2014-03-03 21:49:48 +0000
4@@ -22,8 +22,11 @@
5 # along with this program. If not, see <http://www.gnu.org/licenses/>.
6
7 import argparse
8+import json
9 import os
10 import sys
11+import time
12+import tempfile
13 import traceback
14
15 # This is more just for running from the bin folder so that
16@@ -126,11 +129,11 @@
17 " under section '%s'") % (action_name, full_section_name)
18 sys.stderr.write("%s\n" % (msg))
19 LOG.debug(msg)
20- return 0
21+ return []
22 else:
23 LOG.debug("Ran %s modules with %s failures",
24 len(which_ran), len(failures))
25- return len(failures)
26+ return failures
27
28
29 def main_init(name, args):
30@@ -220,7 +223,7 @@
31 if existing_files:
32 LOG.debug("Exiting early due to the existence of %s files",
33 existing_files)
34- return 0
35+ return (None, [])
36 else:
37 # The cache is not instance specific, so it has to be purged
38 # but we want 'start' to benefit from a cache if
39@@ -249,9 +252,9 @@
40 " Likely bad things to come!"))
41 if not args.force:
42 if args.local:
43- return 0
44+ return (None, [])
45 else:
46- return 1
47+ return (None, ["No instance datasource found."])
48 # Stage 6
49 iid = init.instancify()
50 LOG.debug("%s will now be targeting instance id: %s", name, iid)
51@@ -274,7 +277,7 @@
52 init.consume_data(PER_ALWAYS)
53 except Exception:
54 util.logexc(LOG, "Consuming user data failed!")
55- return 1
56+ return (init.datasource, ["Consuming user data failed!"])
57
58 # Stage 8 - re-read and apply relevant cloud-config to include user-data
59 mods = stages.Modules(init, extract_fns(args))
60@@ -291,7 +294,7 @@
61 logging.setupLogging(mods.cfg)
62
63 # Stage 10
64- return run_module_section(mods, name, name)
65+ return (init.datasource, run_module_section(mods, name, name))
66
67
68 def main_modules(action_name, args):
69@@ -315,14 +318,12 @@
70 init.fetch()
71 except sources.DataSourceNotFoundException:
72 # There was no datasource found, theres nothing to do
73- util.logexc(LOG, ('Can not apply stage %s, '
74- 'no datasource found!'
75- " Likely bad things to come!"), name)
76- print_exc(('Can not apply stage %s, '
77- 'no datasource found!'
78- " Likely bad things to come!") % (name))
79+ msg = ('Can not apply stage %s, no datasource found! Likely bad '
80+ 'things to come!' % name)
81+ util.logexc(LOG, msg)
82+ print_exc(msg)
83 if not args.force:
84- return 1
85+ return [(msg)]
86 # Stage 3
87 mods = stages.Modules(init, extract_fns(args))
88 # Stage 4
89@@ -419,6 +420,110 @@
90 return 0
91
92
93+def atomic_write_json(path, data):
94+ tf = None
95+ try:
96+ tf = tempfile.NamedTemporaryFile(dir=os.path.dirname(path),
97+ delete=False)
98+ tf.write(json.dumps(data, indent=1) + "\n")
99+ tf.close()
100+ os.rename(tf.name, path)
101+ except Exception as e:
102+ if tf is not None:
103+ util.del_file(tf.name)
104+ raise e
105+
106+
107+def status_wrapper(name, args, data_d=None, link_d=None):
108+ if data_d is None:
109+ data_d = os.path.normpath("/var/lib/cloud/data")
110+ if link_d is None:
111+ link_d = os.path.normpath("/run/cloud-init")
112+
113+ status_path = os.path.join(data_d, "status.json")
114+ status_link = os.path.join(link_d, "status.json")
115+ result_path = os.path.join(data_d, "result.json")
116+ result_link = os.path.join(link_d, "result.json")
117+
118+ util.ensure_dirs((data_d, link_d,))
119+
120+ (_name, functor) = args.action
121+
122+ if name == "init":
123+ if args.local:
124+ mode = "init-local"
125+ else:
126+ mode = "init"
127+ elif name == "modules":
128+ mode = "modules-%s" % args.mode
129+ else:
130+ raise ValueError("unknown name: %s" % name)
131+
132+ modes = ('init', 'init-local', 'modules-config', 'modules-final')
133+
134+ status = None
135+ if mode == 'init-local':
136+ for f in (status_link, result_link, status_path, result_path):
137+ util.del_file(f)
138+ else:
139+ try:
140+ status = json.loads(util.load_file(status_path))
141+ except:
142+ pass
143+
144+ if status is None:
145+ nullstatus = {
146+ 'errors': [],
147+ 'start': None,
148+ 'end': None,
149+ }
150+ status = {'v1': {}}
151+ for m in modes:
152+ status['v1'][m] = nullstatus.copy()
153+ status['v1']['datasource'] = None
154+
155+ v1 = status['v1']
156+ v1['stage'] = mode
157+ v1[mode]['start'] = time.time()
158+
159+ atomic_write_json(status_path, status)
160+ util.sym_link(os.path.relpath(status_path, link_d), status_link,
161+ force=True)
162+
163+ try:
164+ ret = functor(name, args)
165+ if mode in ('init', 'init-local'):
166+ (datasource, errors) = ret
167+ if datasource is not None:
168+ v1['datasource'] = str(datasource)
169+ else:
170+ errors = ret
171+
172+ v1[mode]['errors'] = [str(e) for e in errors]
173+
174+ except Exception as e:
175+ v1[mode]['errors'] = [str(e)]
176+
177+ v1[mode]['finished'] = time.time()
178+ v1['stage'] = None
179+
180+ atomic_write_json(status_path, status)
181+
182+ if mode == "modules-final":
183+ # write the 'finished' file
184+ errors = []
185+ for m in modes:
186+ if v1[m]['errors']:
187+ errors.extend(v1[m].get('errors', []))
188+
189+ atomic_write_json(result_path,
190+ {'v1': {'datasource': v1['datasource'], 'errors': errors}})
191+ util.sym_link(os.path.relpath(result_path, link_d), result_link,
192+ force=True)
193+
194+ return len(v1[mode]['errors'])
195+
196+
197 def main():
198 parser = argparse.ArgumentParser()
199
200@@ -502,6 +607,8 @@
201 signal_handler.attach_handlers()
202
203 (name, functor) = args.action
204+ if name in ("modules", "init"):
205+ functor = status_wrapper
206
207 return util.log_time(logfunc=LOG.debug, msg="cloud-init mode '%s'" % name,
208 get_uptime=True, func=functor, args=(name, args))
209
210=== modified file 'cloudinit/util.py'
211--- cloudinit/util.py 2014-02-13 11:27:22 +0000
212+++ cloudinit/util.py 2014-03-03 21:49:48 +0000
213@@ -1395,8 +1395,10 @@
214 return obj_copy.deepcopy(CFG_BUILTIN)
215
216
217-def sym_link(source, link):
218+def sym_link(source, link, force=False):
219 LOG.debug("Creating symbolic link from %r => %r", link, source)
220+ if force and os.path.exists(link):
221+ del_file(link)
222 os.symlink(source, link)
223
224
225
226=== added file 'doc/status.txt'
227--- doc/status.txt 1970-01-01 00:00:00 +0000
228+++ doc/status.txt 2014-03-03 21:49:48 +0000
229@@ -0,0 +1,51 @@
230+cloud-init will keep a 'status' file up to date for other applications
231+wishing to use it to determine cloud-init status.
232+
233+It will manage 2 files:
234+ status.json
235+ finished.json
236+
237+The files will be written to /var/lib/cloud/data/ .
238+A symlink will be created in /run/cloud-init. The link from /run is to ensure
239+that if the file exists, it is not stale for this boot.
240+
241+status.json's format is:
242+ {
243+ 'v1': {
244+ 'init': {
245+ errors: [] # list of strings for each error that occurred
246+ start: float # time.time() that this stage started or None
247+ end: float # time.time() that this stage finished or None
248+ },
249+ 'init-local': {
250+ 'errors': [], 'start': <float>, 'end' <float> # (same as 'init' above)
251+ },
252+ 'modules-config': {
253+ 'errors': [], 'start': <float>, 'end' <float> # (same as 'init' above)
254+ },
255+ 'modules-final': {
256+ 'errors': [], 'start': <float>, 'end' <float> # (same as 'init' above)
257+ },
258+ 'datasource': string describing datasource found or None
259+ 'stage': string representing stage that is currently running
260+ ('init', 'init-local', 'modules-final', 'modules-config', None)
261+ if None, then no stage is running. Reader must read the start/end
262+ of each of the above stages to determine the state.
263+ }
264+
265+finished.json's format is:
266+ {
267+ 'datasource': string describing the datasource found
268+ 'errors': [] # list of errors reported
269+ }
270+
271+Thus, to determine if cloud-init is finished:
272+ fin = "/run/cloud-init/finished.json"
273+ if os.path.exists(fin):
274+ ret = json.load(open(fin, "r"))
275+ if len(ret):
276+ print "Finished with errors:" + "\n".join(ret['errors'])
277+ else:
278+ print "Finished no errors"
279+ else:
280+ print "Not Finished"