2 # -*- coding: utf-8 -*-
3 #---------------------------------------------------------------------
5 # Copyright © 2011 Canonical Ltd.
7 # Author: James Hunt <james.hunt@canonical.com>
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License version 2, as
11 # published by the Free Software Foundation.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License along
19 # with this program; if not, write to the Free Software Foundation, Inc.,
20 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21 #---------------------------------------------------------------------
23 #---------------------------------------------------------------------
24 # Script to take output of "initctl show-config -e" and convert it into
25 # a Graphviz DOT language (".dot") file for procesing with dot(1), etc.
29 # - Slightly laborious logic used to satisfy graphviz requirement that
30 # all nodes be defined before being referenced.
34 # initctl show-config -e > initctl.out
35 # initctl2dot -f initctl.out -o upstart.dot
36 # dot -Tpng -o upstart.png upstart.dot
40 # initctl2dot -o - | dot -Tpng -o upstart.png
46 # - http://www.graphviz.org.
47 #---------------------------------------------------------------------
53 from string import split
55 from subprocess import (Popen, PIPE)
56 from optparse import OptionParser
60 cmd = "initctl --system show-config -e"
61 script_name = os.path.basename(sys.argv[0])
63 job_events = [ 'starting', 'started', 'stopping', 'stopped' ]
65 # list of jobs to restict output to
66 restrictions_list = []
68 default_color_emits = 'green'
69 default_color_start_on = 'blue'
70 default_color_stop_on = 'red'
71 default_color_event = 'thistle'
72 default_color_job = '#DCDCDC' # "Gainsboro"
73 default_color_text = 'black'
74 default_color_bg = 'white'
76 default_outfile = 'upstart.dot'
82 str = "digraph upstart {\n"
84 # make the default node an event to simplify glob code
85 str += " node [shape=\"diamond\", fontcolor=\"%s\", fillcolor=\"%s\", style=\"filled\"];\n" \
86 % (options.color_event_text, options.color_event)
87 str += " rankdir=LR;\n"
88 str += " overlap=false;\n"
89 str += " bgcolor=\"%s\";\n" % options.color_bg
90 str += " fontcolor=\"%s\";\n" % options.color_text
98 epilog = "overlap=false;\n"
99 epilog += "label=\"Generated on %s by %s\\n" % \
100 (str(datetime.datetime.now()), script_name)
102 if options.restrictions:
103 epilog += "(subset, "
108 epilog += "from file data).\\n"
110 epilog += "from '%s' on host %s).\\n" % \
113 epilog += "Boxes of color %s denote jobs.\\n" % options.color_job
114 epilog += "Solid diamonds of color %s denote events.\\n" % options.color_event
115 epilog += "Dotted diamonds denote 'glob' events.\\n"
116 epilog += "Emits denoted by %s lines.\\n" % options.color_emits
117 epilog += "Start on denoted by %s lines.\\n" % options.color_start_on
118 epilog += "Stop on denoted by %s lines.\\n" % options.color_stop_on
124 # Map dash to underscore since graphviz node names cannot
125 # contain dashes. Also remove dollars and colons
127 return s.replace('-', '_').replace('$', 'dollar_').replace('[', \
128 'lbracket').replace(']', 'rbracket').replace('!', \
129 'bang').replace(':', '_').replace('*', 'star').replace('?', 'question')
132 # Convert a dollar in @name to a unique-ish new name, based on @job and
133 # return it. Used for very rudimentary instance handling.
134 def encode_dollar(job, name):
136 name = job + ':' + name
140 def mk_node_name(name):
141 return sanitise(name)
144 # Jobs and events can have identical names, so prefix them to namespace
146 def mk_job_node_name(name):
147 return mk_node_name('job_' + name)
150 def mk_event_node_name(name):
151 return mk_node_name('event_' + name)
154 def show_event(ofh, name):
156 str = "%s [label=\"%s\", shape=diamond, fontcolor=\"%s\", fillcolor=\"%s\"," % \
157 (mk_event_node_name(name), name, options.color_event_text, options.color_event)
160 str += " style=\"dotted\""
162 str += " style=\"filled\""
168 def show_events(ofh):
171 global restrictions_list
175 if restrictions_list:
176 for job in restrictions_list:
178 # We want all events emitted by the jobs in the restrictions_list.
179 events_to_show += jobs[job]['emits']
181 # We also want all events that jobs in restrictions_list start/stop
183 events_to_show += jobs[job]['start on']['event']
184 events_to_show += jobs[job]['stop on']['event']
186 # We also want all events emitted by all jobs that jobs in the
187 # restrictions_list start/stop on. Finally, we want all events
188 # emmitted by those jobs in the restrictions_list that we
190 for j in jobs[job]['start on']['job']:
191 if jobs.has_key(j) and jobs[j].has_key('emits'):
192 events_to_show += jobs[j]['emits']
194 for j in jobs[job]['stop on']['job']:
195 if jobs.has_key(j) and jobs[j].has_key('emits'):
196 events_to_show += jobs[j]['emits']
198 events_to_show = events
200 for e in events_to_show:
204 def show_job(ofh, name):
208 %s [shape=\"record\", label=\"<job> %s | { <start> start on | <stop> stop on }\", fontcolor=\"%s\", style=\"filled\", fillcolor=\"%s\"];
209 """ % (mk_job_node_name(name), name, options.color_job_text, options.color_job))
215 global restrictions_list
217 if restrictions_list:
218 jobs_to_show = restrictions_list
222 for j in jobs_to_show:
224 # add those jobs which are referenced by existing jobs, but which
225 # might not be available as .conf files. For example, plymouth.conf
226 # references gdm *or* kdm, but you are unlikely to have both
228 for s in jobs[j]['start on']['job']:
229 if s not in jobs_to_show:
232 for s in jobs[j]['stop on']['job']:
233 if s not in jobs_to_show:
236 if not restrictions_list:
239 # Having displayed the jobs in restrictions_list,
240 # we now need to display all jobs that *those* jobs
242 for j in restrictions_list:
243 for job in jobs[j]['start on']['job']:
245 for job in jobs[j]['stop on']['job']:
248 # Finally, show all jobs which emit events that jobs in the
249 # restrictions_list care about.
250 for j in restrictions_list:
252 for e in jobs[j]['start on']['event']:
254 if e in jobs[k]['emits']:
257 for e in jobs[j]['stop on']['event']:
259 if e in jobs[k]['emits']:
263 def show_edge(ofh, from_node, to_node, color):
264 ofh.write("%s -> %s [color=\"%s\"];\n" % (from_node, to_node, color))
267 def show_start_on_job_edge(ofh, from_job, to_job):
269 show_edge(ofh, "%s:start" % mk_job_node_name(from_job),
270 "%s:job" % mk_job_node_name(to_job), options.color_start_on)
273 def show_start_on_event_edge(ofh, from_job, to_event):
275 show_edge(ofh, "%s:start" % mk_job_node_name(from_job),
276 mk_event_node_name(to_event), options.color_start_on)
279 def show_stop_on_job_edge(ofh, from_job, to_job):
281 show_edge(ofh, "%s:stop" % mk_job_node_name(from_job),
282 "%s:job" % mk_job_node_name(to_job), options.color_stop_on)
285 def show_stop_on_event_edge(ofh, from_job, to_event):
287 show_edge(ofh, "%s:stop" % mk_job_node_name(from_job),
288 mk_event_node_name(to_event), options.color_stop_on)
291 def show_job_emits_edge(ofh, from_job, to_event):
293 show_edge(ofh, "%s:job" % mk_job_node_name(from_job),
294 mk_event_node_name(to_event), options.color_emits)
301 global restrictions_list
305 if restrictions_list:
306 jobs_list = restrictions_list
310 for job in jobs_list:
312 for s in jobs[job]['start on']['job']:
313 show_start_on_job_edge(ofh, job, s)
315 for s in jobs[job]['start on']['event']:
316 show_start_on_event_edge(ofh, job, s)
318 for s in jobs[job]['stop on']['job']:
319 show_stop_on_job_edge(ofh, job, s)
321 for s in jobs[job]['stop on']['event']:
322 show_stop_on_event_edge(ofh, job, s)
324 for e in jobs[job]['emits']:
326 # handle glob patterns in 'emits'
329 if e != _e and fnmatch.fnmatch(_e, e):
330 glob_events.append(_e)
331 glob_jobs[job] = glob_events
333 show_job_emits_edge(ofh, job, e)
335 if not restrictions_list:
338 # Add links to events emitted by all jobs which current job
340 for j in jobs[job]['start on']['job']:
341 if not jobs.has_key(j):
343 for e in jobs[j]['emits']:
344 show_job_emits_edge(ofh, j, e)
346 for j in jobs[job]['stop on']['job']:
347 for e in jobs[j]['emits']:
348 show_job_emits_edge(ofh, j, e)
350 # Create links from jobs (which advertise they emits a class of
351 # events, via the glob syntax) to all the events they create.
353 for ge in glob_jobs[g]:
354 show_job_emits_edge(ofh, g, ge)
356 if not restrictions_list:
359 # Add jobs->event links to jobs which emit events that current job
361 for j in restrictions_list:
363 for e in jobs[j]['start on']['event']:
365 if e in jobs[k]['emits'] and e not in restrictions_list:
366 show_job_emits_edge(ofh, k, e)
368 for e in jobs[j]['stop on']['event']:
370 if e in jobs[k]['emits'] and e not in restrictions_list:
371 show_job_emits_edge(ofh, k, e)
383 ifh = open(options.infile, 'r')
385 sys.exit("ERROR: cannot read file '%s'" % options.infile)
388 ifh = Popen(split(cmd), stdout=PIPE).stdout
390 sys.exit("ERROR: cannot run '%s'" % cmd)
392 for line in ifh.readlines():
396 result = re.match('^\s+start on ([^,]+) \(job:\s*([^,]*), env:', line)
398 _event = encode_dollar(job, result.group(1))
399 _job = result.group(2)
401 jobs[job]['start on']['job'][_job] = 1
403 jobs[job]['start on']['event'][_event] = 1
407 result = re.match('^\s+stop on ([^,]+) \(job:\s*([^,]*), env:', line)
409 _event = encode_dollar(job, result.group(1))
410 _job = result.group(2)
412 jobs[job]['stop on']['job'][_job] = 1
414 jobs[job]['stop on']['event'][_event] = 1
418 if re.match('^\s+emits', line):
419 event = (line.lstrip().split())[1]
420 event = encode_dollar(job, event)
422 jobs[job]['emits'][event] = 1
424 tokens = (line.lstrip().split())
427 sys.exit("ERROR: invalid line: %s" % line.lstrip())
441 start_on['job'] = start_on_jobs
442 start_on['event'] = start_on_events
444 stop_on['job'] = stop_on_jobs
445 stop_on['event'] = stop_on_events
447 job_record['start on'] = start_on
448 job_record['stop on'] = stop_on
449 job_record['emits'] = emits
452 jobs[job] = job_record
459 global default_color_emits
460 global default_color_start_on
461 global default_color_stop_on
462 global default_color_event
463 global default_color_job
464 global default_color_text
465 global default_color_bg
466 global restrictions_list
468 description = "Convert initctl(8) output to GraphViz dot(1) format."
470 "See http://www.graphviz.org/doc/info/colors.html for available colours."
472 parser = OptionParser(description=description, epilog=epilog)
474 parser.add_option("-r", "--restrict-to-jobs",
476 help="Limit display of 'start on' and 'stop on' conditions to " +
477 "specified jobs (comma-separated list).")
479 parser.add_option("-f", "--infile",
481 help="File to read '%s' output from. If not specified, " \
482 "initctl will be run automatically." % cmd)
484 parser.add_option("-o", "--outfile",
486 help="File to write output to (default=%s)" % default_outfile)
488 parser.add_option("--color-emits",
490 help="Specify color for 'emits' lines (default=%s)." %
493 parser.add_option("--color-start-on",
494 dest="color_start_on",
495 help="Specify color for 'start on' lines (default=%s)." %
496 default_color_start_on)
498 parser.add_option("--color-stop-on",
499 dest="color_stop_on",
500 help="Specify color for 'stop on' lines (default=%s)." %
501 default_color_stop_on)
503 parser.add_option("--color-event",
505 help="Specify color for event boxes (default=%s)." %
508 parser.add_option("--color-text",
510 help="Specify color for summary text (default=%s)." %
513 parser.add_option("--color-bg",
515 help="Specify background color for diagram (default=%s)." %
518 parser.add_option("--color-event-text",
519 dest="color_event_text",
520 help="Specify color for text in event boxes (default=%s)." %
523 parser.add_option("--color-job-text",
524 dest="color_job_text",
525 help="Specify color for text in job boxes (default=%s)." %
528 parser.add_option("--color-job",
530 help="Specify color for job boxes (default=%s)." %
533 parser.set_defaults(color_emits=default_color_emits,
534 color_start_on=default_color_start_on,
535 color_stop_on=default_color_stop_on,
536 color_event=default_color_event,
537 color_job=default_color_job,
538 color_job_text=default_color_text,
539 color_event_text=default_color_text,
540 color_text=default_color_text,
541 color_bg=default_color_bg,
542 outfile=default_outfile)
544 (options, args) = parser.parse_args()
546 if options.outfile == '-':
550 ofh = open(options.outfile, "w")
552 sys.exit("ERROR: cannot open file %s for writing" % options.outfile)
554 if options.restrictions:
555 restrictions_list = options.restrictions.split(",")
559 for job in restrictions_list:
561 sys.exit("ERROR: unknown job %s" % job)
570 if __name__ == "__main__":