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__":