Tizen 2.0 Release
[pkgs/o/oma-ds-service.git] / bin / initctl2dot
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3 #---------------------------------------------------------------------
4 #
5 # Copyright © 2011 Canonical Ltd.
6 #
7 # Author: James Hunt <james.hunt@canonical.com>
8 #
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.
12 #
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.
17 #
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 #---------------------------------------------------------------------
22
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.
26 #
27 # Notes:
28 #
29 # - Slightly laborious logic used to satisfy graphviz requirement that
30 #   all nodes be defined before being referenced.
31 #
32 # Usage:
33 #
34 #   initctl show-config -e > initctl.out
35 #   initctl2dot -f initctl.out -o upstart.dot
36 #   dot -Tpng -o upstart.png upstart.dot
37 #
38 # Or more simply:
39 #
40 #  initctl2dot -o - | dot -Tpng -o upstart.png
41 #
42 # See also:
43 #
44 # - dot(1).
45 # - initctl(8).
46 # - http://www.graphviz.org.
47 #---------------------------------------------------------------------
48
49 import sys
50 import re
51 import fnmatch
52 import os
53 from string import split
54 import datetime
55 from subprocess import (Popen, PIPE)
56 from optparse import OptionParser
57
58 jobs   = {}
59 events = {}
60 cmd = "initctl --system show-config -e"
61 script_name =  os.path.basename(sys.argv[0])
62
63 job_events = [ 'starting', 'started', 'stopping', 'stopped' ]
64
65 # list of jobs to restict output to
66 restrictions_list = []
67
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'
75
76 default_outfile        = 'upstart.dot'
77
78
79 def header(ofh):
80   global options
81
82   str  = "digraph upstart {\n"
83
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
91
92   ofh.write(str)
93
94
95 def footer(ofh):
96   global options
97
98   epilog = "overlap=false;\n"
99   epilog += "label=\"Generated on %s by %s\\n" % \
100     (str(datetime.datetime.now()), script_name)
101
102   if options.restrictions:
103     epilog += "(subset, "
104   else:
105     epilog += "("
106
107   if options.infile:
108     epilog += "from file data).\\n"
109   else:
110     epilog += "from '%s' on host %s).\\n" % \
111       (cmd, os.uname()[1])
112
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
119   epilog += "\";\n"
120   epilog += "}\n"
121   ofh.write(epilog)
122
123
124 # Map dash to underscore since graphviz node names cannot
125 # contain dashes. Also remove dollars and colons
126 def sanitise(s):
127   return s.replace('-', '_').replace('$', 'dollar_').replace('[', \
128   'lbracket').replace(']', 'rbracket').replace('!', \
129   'bang').replace(':', '_').replace('*', 'star').replace('?', 'question')
130
131
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):
135   if name[0] == '$':
136     name = job + ':' + name
137   return name
138
139
140 def mk_node_name(name):
141   return sanitise(name)
142
143
144 # Jobs and events can have identical names, so prefix them to namespace
145 # them off.
146 def mk_job_node_name(name):
147   return mk_node_name('job_' + name)
148
149
150 def mk_event_node_name(name):
151   return mk_node_name('event_' + name)
152
153
154 def show_event(ofh, name):
155     global options
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)
158
159     if '*' in name:
160       str += " style=\"dotted\""
161     else:
162       str += " style=\"filled\""
163
164     str += "];\n"
165
166     ofh.write(str)
167
168 def show_events(ofh):
169   global events
170   global options
171   global restrictions_list
172
173   events_to_show = []
174
175   if restrictions_list:
176     for job in restrictions_list:
177
178       # We want all events emitted by the jobs in the restrictions_list.
179       events_to_show += jobs[job]['emits']
180
181       # We also want all events that jobs in restrictions_list start/stop
182       # on.
183       events_to_show += jobs[job]['start on']['event']
184       events_to_show += jobs[job]['stop on']['event']
185
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
189       # start/stop on.
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']
193
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']
197   else:
198     events_to_show = events
199
200   for e in events_to_show:
201     show_event(ofh, e)
202
203
204 def show_job(ofh, name):
205   global options
206
207   ofh.write("""
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))
210
211
212 def show_jobs(ofh):
213   global jobs
214   global options
215   global restrictions_list
216
217   if restrictions_list:
218     jobs_to_show = restrictions_list
219   else:
220     jobs_to_show = jobs
221
222   for j in jobs_to_show:
223     show_job(ofh, j)
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
227     # installed.
228     for s in jobs[j]['start on']['job']:
229       if s not in jobs_to_show:
230         show_job(ofh, s)
231
232     for s in jobs[j]['stop on']['job']:
233       if s not in jobs_to_show:
234         show_job(ofh, s)
235
236   if not restrictions_list:
237     return
238
239   # Having displayed the jobs in restrictions_list,
240   # we now need to display all jobs that *those* jobs
241   # start on/stop on.
242   for j in restrictions_list:
243     for job in jobs[j]['start on']['job']:
244       show_job(ofh, job)
245     for job in jobs[j]['stop on']['job']:
246       show_job(ofh, job)
247
248   # Finally, show all jobs which emit events that jobs in the
249   # restrictions_list care about.
250   for j in restrictions_list:
251
252     for e in jobs[j]['start on']['event']:
253       for k in jobs:
254         if e in jobs[k]['emits']:
255           show_job(ofh, k)
256
257     for e in jobs[j]['stop on']['event']:
258       for k in jobs:
259         if e in jobs[k]['emits']:
260           show_job(ofh, k)
261
262
263 def show_edge(ofh, from_node, to_node, color):
264   ofh.write("%s -> %s [color=\"%s\"];\n" % (from_node, to_node, color))
265
266
267 def show_start_on_job_edge(ofh, from_job, to_job):
268   global options
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)
271
272
273 def show_start_on_event_edge(ofh, from_job, to_event):
274   global options
275   show_edge(ofh, "%s:start" % mk_job_node_name(from_job),
276     mk_event_node_name(to_event), options.color_start_on)
277
278
279 def show_stop_on_job_edge(ofh, from_job, to_job):
280   global options
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)
283
284
285 def show_stop_on_event_edge(ofh, from_job, to_event):
286   global options
287   show_edge(ofh, "%s:stop" % mk_job_node_name(from_job),
288     mk_event_node_name(to_event), options.color_stop_on)
289
290
291 def show_job_emits_edge(ofh, from_job, to_event):
292   global options
293   show_edge(ofh, "%s:job" % mk_job_node_name(from_job),
294     mk_event_node_name(to_event), options.color_emits)
295
296
297 def show_edges(ofh):
298   global events
299   global jobs
300   global options
301   global restrictions_list
302
303   glob_jobs = {}
304
305   if restrictions_list:
306     jobs_list = restrictions_list
307   else:
308     jobs_list = jobs
309
310   for job in jobs_list:
311
312     for s in jobs[job]['start on']['job']:
313       show_start_on_job_edge(ofh, job, s)
314
315     for s in jobs[job]['start on']['event']:
316       show_start_on_event_edge(ofh, job, s)
317
318     for s in jobs[job]['stop on']['job']:
319       show_stop_on_job_edge(ofh, job, s)
320
321     for s in jobs[job]['stop on']['event']:
322       show_stop_on_event_edge(ofh, job, s)
323
324     for e in jobs[job]['emits']:
325       if '*' in e:
326         # handle glob patterns in 'emits'
327         glob_events = []
328         for _e in events:
329           if e != _e and fnmatch.fnmatch(_e, e):
330             glob_events.append(_e)
331         glob_jobs[job] = glob_events
332
333       show_job_emits_edge(ofh, job, e)
334
335     if not restrictions_list:
336       continue
337
338     # Add links to events emitted by all jobs which current job
339     # start/stops on
340     for j in jobs[job]['start on']['job']:
341       if not jobs.has_key(j):
342         continue
343       for e in jobs[j]['emits']:
344         show_job_emits_edge(ofh, j, e)
345
346     for j in jobs[job]['stop on']['job']:
347       for e in jobs[j]['emits']:
348         show_job_emits_edge(ofh, j, e)
349
350   # Create links from jobs (which advertise they emits a class of
351   # events, via the glob syntax) to all the events they create.
352   for g in glob_jobs:
353     for ge in glob_jobs[g]:
354       show_job_emits_edge(ofh, g, ge)
355
356   if not restrictions_list:
357     return
358
359   # Add jobs->event links to jobs which emit events that current job
360   # start/stops on.
361   for j in restrictions_list:
362
363     for e in jobs[j]['start on']['event']:
364       for k in jobs:
365         if e in jobs[k]['emits'] and e not in restrictions_list:
366           show_job_emits_edge(ofh, k, e)
367
368     for e in jobs[j]['stop on']['event']:
369       for k in jobs:
370         if e in jobs[k]['emits'] and e not in restrictions_list:
371           show_job_emits_edge(ofh, k, e)
372
373
374 def read_data():
375   global jobs
376   global events
377   global options
378   global cmd
379   global job_events
380
381   if options.infile:
382     try:
383       ifh = open(options.infile, 'r')
384     except:
385       sys.exit("ERROR: cannot read file '%s'" % options.infile)
386   else:
387     try:
388       ifh = Popen(split(cmd), stdout=PIPE).stdout
389     except:
390       sys.exit("ERROR: cannot run '%s'" % cmd)
391
392   for line in ifh.readlines():
393       record = {}
394       line = line.rstrip()
395
396       result = re.match('^\s+start on ([^,]+) \(job:\s*([^,]*), env:', line)
397       if result:
398         _event = encode_dollar(job, result.group(1))
399         _job   = result.group(2)
400         if _job:
401           jobs[job]['start on']['job'][_job] = 1
402         else:
403           jobs[job]['start on']['event'][_event] = 1
404           events[_event] = 1
405         continue
406
407       result = re.match('^\s+stop on ([^,]+) \(job:\s*([^,]*), env:', line)
408       if result:
409         _event = encode_dollar(job, result.group(1))
410         _job   = result.group(2)
411         if _job:
412           jobs[job]['stop on']['job'][_job] = 1
413         else:
414           jobs[job]['stop on']['event'][_event] = 1
415           events[_event] = 1
416         continue
417
418       if re.match('^\s+emits', line):
419         event = (line.lstrip().split())[1]
420         event = encode_dollar(job, event)
421         events[event] = 1
422         jobs[job]['emits'][event] = 1
423       else:
424         tokens = (line.lstrip().split())
425
426         if len(tokens) != 1:
427           sys.exit("ERROR: invalid line: %s" % line.lstrip())
428
429         job_record      = {}
430
431         start_on        = {}
432         start_on_jobs   = {}
433         start_on_events = {}
434
435         stop_on         = {}
436         stop_on_jobs    = {}
437         stop_on_events  = {}
438
439         emits           = {}
440
441         start_on['job']    = start_on_jobs
442         start_on['event']  = start_on_events
443
444         stop_on['job']     = stop_on_jobs
445         stop_on['event']   = stop_on_events
446
447         job_record['start on'] = start_on
448         job_record['stop on']  = stop_on
449         job_record['emits']    = emits
450
451         job = (tokens)[0]
452         jobs[job] = job_record
453
454
455 def main():
456   global jobs
457   global options
458   global cmd
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
467
468   description = "Convert initctl(8) output to GraphViz dot(1) format."
469   epilog = \
470     "See http://www.graphviz.org/doc/info/colors.html for available colours."
471
472   parser = OptionParser(description=description, epilog=epilog)
473
474   parser.add_option("-r", "--restrict-to-jobs",
475       dest="restrictions",
476       help="Limit display of 'start on' and 'stop on' conditions to " +
477       "specified jobs (comma-separated list).")
478
479   parser.add_option("-f", "--infile",
480       dest="infile",
481       help="File to read '%s' output from. If not specified, " \
482       "initctl will be run automatically." % cmd)
483
484   parser.add_option("-o", "--outfile",
485       dest="outfile",
486       help="File to write output to (default=%s)" % default_outfile)
487
488   parser.add_option("--color-emits",
489       dest="color_emits",
490       help="Specify color for 'emits' lines (default=%s)." %
491       default_color_emits)
492
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)
497
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)
502
503   parser.add_option("--color-event",
504       dest="color_event",
505       help="Specify color for event boxes (default=%s)." %
506       default_color_event)
507
508   parser.add_option("--color-text",
509       dest="color_text",
510       help="Specify color for summary text (default=%s)." %
511       default_color_text)
512
513   parser.add_option("--color-bg",
514       dest="color_bg",
515       help="Specify background color for diagram (default=%s)." %
516       default_color_bg)
517
518   parser.add_option("--color-event-text",
519       dest="color_event_text",
520       help="Specify color for text in event boxes (default=%s)." %
521       default_color_text)
522
523   parser.add_option("--color-job-text",
524       dest="color_job_text",
525       help="Specify color for text in job boxes (default=%s)." %
526       default_color_text)
527
528   parser.add_option("--color-job",
529       dest="color_job",
530       help="Specify color for job boxes (default=%s)." %
531       default_color_job)
532
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)
543
544   (options, args) = parser.parse_args()
545
546   if options.outfile == '-':
547     ofh = sys.stdout
548   else:
549     try:
550       ofh = open(options.outfile, "w")
551     except:
552       sys.exit("ERROR: cannot open file %s for writing" % options.outfile)
553
554   if options.restrictions:
555     restrictions_list = options.restrictions.split(",")
556
557   read_data()
558
559   for job in restrictions_list:
560     if not job in jobs:
561       sys.exit("ERROR: unknown job %s" % job)
562
563   header(ofh)
564   show_events(ofh)
565   show_jobs(ofh)
566   show_edges(ofh)
567   footer(ofh)
568
569
570 if __name__ == "__main__":
571   main()