Index: fedd/fedd_image.py
===================================================================
--- fedd/fedd_image.py	(revision c324ad3254730c1436e8bd4ac106e49dd7195996)
+++ fedd/fedd_image.py	(revision 1f356d38533d830e355d1c226f24575c749d2427)
@@ -39,6 +39,41 @@
         self.add_option("--file", dest="file", 
                 help="experiment description file")
-
-def gen_dot_topo(t, labels, dotfile):
+	self.add_option("--group", dest="group", default=None, 
+		help="Group nodes by this attribute")
+
+def make_subgraph(topelems, attr):
+    """
+    Take a list of topdl elements (Computers and Substrates ) and partition
+    them into groups based on the value of the given attribute.  All computers
+    with the same value for the attribute are in a subgraph. A substrate is in
+    the subgraph if all its interfaces are, and in the "default" subgraph if
+    not.
+    """
+
+    def add_to_map(m, a, v):
+	"""
+	Add this value to the list in m[a] if that list exists, or create it.
+	"""
+	if a in m: m[a].append(v)
+	else: m[a] = [ v]
+
+    sg = { }
+    for e in topelems:
+	if isinstance(e, topdl.Computer):
+	    add_to_map(sg, e.get_attribute(attr) or 'default', e)
+	elif isinstance(e, topdl.Substrate):
+	    # A little hairy.  Make a set of the values of the given attribute
+	    # for all the elements in the substrate's interfaces.  If that set
+	    # has one element, put the substrate in that subgraph.  Otherwise
+	    # put it in default.
+	    atts = set([i.element.get_attribute(attr) or 'default' \
+		    for i in e.interfaces])
+
+	    if len(atts) == 1: add_to_map(sg, atts.pop(), e)
+	    else: add_to_map(sg, 'default', e)
+
+    return sg
+
+def gen_dot_topo(t, labels, dotfile, sg_attr=None):
     """
     Write a dot description of the topology in t, a topdl.Topology to the open
@@ -48,4 +83,5 @@
     """
 
+
     # The routine draws a circle for substrates with more than 2 interfaces
     # connected to it, so this makes lists of each of those.
@@ -53,18 +89,41 @@
     links = [ s for s in t.substrates if len(s.interfaces) == 2]
 
-    # Output the nodes.  Deal with the possibility that there's no name, but
-    # that's unlikely.
-    for i, n in enumerate(t.elements):
-	if n.name:
-	    print >>dotfile, '\t"%s" [shape=box,style=filled,\\' % n.name
-	else:
-	    print >>dotfile, \
-		    '\t"unnamed_node%d" [shape=box,style=filled,\\' % i
-	print >>dotfile, '\t\tcolor=black,fillcolor="#60f8c0",regular=1]'
-
-    # Encode the lans and the links
+    # Break into subgraphs by the attribute of one's given
+    if sg_attr: sg = make_subgraph(t.elements + lans, sg_attr)
+    else: sg = { 'default': t.elements + lans }
+
+
+    # Construct subgraphs featurning nodes from those graphs
+    for sn, nodes in sg.items():
+	# Output the computers and lans in subgraphs, if necessary.  Subgraphs
+	# treated as clusters are designated by "cluster" starting the name.
+	# (The default subgraph is not treated as a cluster.
+	if sn != 'default': 
+	    print >>dotfile, 'subgraph "cluster-%s" {' % sn
+	    print >>dotfile, 'label="%s"' % sn
+	else: 
+	    print >>dotfile, 'subgraph "%s" {' % sn
+	print >>dotfile, 'color="#08c0f8"'
+	print >>dotfile, 'clusterrank="local"'
+
+	# For each node in the subgraph, output its representation
+	for i, n in enumerate(nodes):
+	    if isinstance(n, topdl.Computer):
+		if n.name:
+		    print >>dotfile, '\t"%s" [shape=box,style=filled,\\' % \
+			    n.name
+		else:
+		    print >>dotfile, \
+			    '\t"unnamed_node%d" [shape=box,style=filled,\\' % i
+		print >>dotfile, \
+			'\t\tcolor=black,fillcolor="#60f8c0",regular=1]'
+	    elif isinstance(n, topdl.Substrate):
+		print >>dotfile, '\t"%s" [shape=ellipse, style=filled,\\' % \
+			n.name
+		print >>dotfile,'\t\tcolor=black,fillcolor="#80c0f8",regular=1]'
+	print >>dotfile, '}'
+
+    # Pull the edges out ot the lans and links
     for l in lans:
-	print >>dotfile, '\t"%s" [shape=ellipse, style=filled,\\' % l.name
-	print >>dotfile,'\t\tcolor=black,fillcolor="#80c0f8",regular=1]'
 	for i in l.interfaces:
 	    ip = i.get_attribute('ip4_address')
@@ -90,5 +149,5 @@
 		    (s.element.name, d.element.name)
 
-def gen_image(top, file, fmt, neato, labels, pix=None):
+def gen_image(top, file, fmt, neato, labels, pix=None, sg_attr=None):
     """
     Create a dot formatted input file from the topdl.Topology in top and run
@@ -109,6 +168,14 @@
     # Look for neato if the executable is unspecified.
     if not neato:
-	for f in ['/usr/bin/neato', '/usr/local/bin/neato', 
-		'/usr/bin/dot', '/usr/local/bin/dot']:
+	# If a subgraph attribute has been specified, prefer dot to neato.  dot
+	# is more "natural" looking graphs, but neato segregates better.
+	if sg_attr: 
+	    search_list = ['/usr/bin/dot', '/usr/local/bin/dot',
+		    '/usr/bin/neato', '/usr/local/bin/neato']
+	else:
+	    search_list = ['/usr/bin/neato', '/usr/local/bin/neato', 
+		'/usr/bin/dot', '/usr/local/bin/dot']
+
+	for f in search_list:
 	    if os.access(f, os.X_OK):
 		neato = f
@@ -141,5 +208,5 @@
 		% (size, size, dpi)
     else:
-	print >>dotfile, '\tgraph [size="%i,%i", ratio=fill];' \
+	print >>dotfile, '\tgraph [size="%i,%i", ratio=fill, clusterrank="local"compound="true"];' \
 		% (size, size)
 
@@ -150,5 +217,5 @@
 	print >>dotfile, '\tnode [label=""];'
 
-    gen_dot_topo(top, labels, dotfile)
+    gen_dot_topo(top, labels, dotfile, sg_attr=sg_attr)
     print >>dotfile, "}"
     dotfile.close()
@@ -311,5 +378,6 @@
 if not opts.serialize_only: 
     try:
-	if gen_image(top, file, fmt, opts.neato, opts.labels, opts.pixels) !=0:
+	if gen_image(top, file, fmt, opts.neato, opts.labels, opts.pixels,
+		opts.group) !=0:
 	    sys.exit("Error rendering graph (subcommand returned non-0)")
     except EnvironmentError, e:
