~clint-fewbar/ubuntu/oneiric/upstart/add-plymouth-messages

« back to all changes in this revision

Viewing changes to scripts/initctl2dot.py

  • Committer: Colin Watson
  • Date: 2011-06-23 14:24:49 UTC
  • mfrom: (1316.1.3 ubuntu-upstart)
  • Revision ID: cjwatson@canonical.com-20110623142449-etfog8caqqc5yhpo
Tags: 1.3-0ubuntu3
merge lp:~jamesodhunt/ubuntu/oneiric/upstart/1.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
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()