home *** CD-ROM | disk | FTP | other *** search
/ Chip 2011 November / CHIP_2011_11.iso / Programy / Linux / Ubuntu_64-bit / ubuntu-11.04-desktop-amd64.iso / casper / filesystem.squashfs / bin / initctl2dot < prev    next >
Text File  |  2011-04-20  |  16KB  |  572 lines

  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()
  572.