Create a function used to build che command line according
[plcapi.git] / PLC / Methods / GetBootMedium.py
index b3017f9..3e260ed 100644 (file)
@@ -1,7 +1,9 @@
+# $Id$
 import random
 import base64
 import os
 import os.path
 import random
 import base64
 import os
 import os.path
+import time
 
 from PLC.Faults import *
 from PLC.Method import Method
 
 from PLC.Faults import *
 from PLC.Method import Method
@@ -9,39 +11,35 @@ from PLC.Parameter import Parameter, Mixed
 from PLC.Auth import Auth
 
 from PLC.Nodes import Node, Nodes
 from PLC.Auth import Auth
 
 from PLC.Nodes import Node, Nodes
-from PLC.NodeNetworks import NodeNetwork, NodeNetworks
-from PLC.NodeNetworkSettings import NodeNetworkSetting, NodeNetworkSettings
-
-#
-# xxx todo
-# Thierry on june 5 2007
-# 
-# it turns out that having either apache (when invoked through xmlrpc)
-# or root (when running plcsh directly) run this piece of code is
-# problematic. In fact although we try to create intermediate dirs
-# with mode 777, what happens is that root's umask in the plc chroot
-# jail is set to 0022.
-# 
-# the bottom line is, depending on who (apache or root) runs this for
-# the first time, we can access denied issued (when root comes first)
-# so probably we'd better implement a scheme where files are stored
-# directly under /var/tmp or something
-# 
-# in addition the sequels of a former run (e.g. with a non-empty
-# filename) can prevent subsequent runs if the file is not properly
-# cleaned up after use, which is generally the case if someone invokes
-# this through plcsh and does not clean up
-# so maybe a dedicated cleanup method could be useful just in case
-# 
+from PLC.Interfaces import Interface, Interfaces
+from PLC.InterfaceTags import InterfaceTag, InterfaceTags
 
 # could not define this in the class..
 
 # could not define this in the class..
-boot_medium_actions = [ 'node-preview',
-                        'node-floppy',
-                        'node-iso',
-                        'node-usb',
-                        'generic-iso',
-                        'generic-usb',
-                        ]
+# create a dict with the allowed actions for each type of node
+allowed_actions = {
+                'regular' : [ 'node-preview',
+                              'node-floppy',
+                              'node-iso',
+                              'node-usb',
+                              'generic-iso',
+                              'generic-usb',
+                               ],
+                'dummynet' : [ 'node-preview',
+                               'dummynet-iso',
+                               'dummynet-usb',
+                             ],
+                }
+
+# compute a new key
+def compute_key():
+    # Generate 32 random bytes
+    bytes = random.sample(xrange(0, 256), 32)
+    # Base64 encode their string representation
+    key = base64.b64encode("".join(map(chr, bytes)))
+    # Boot Manager cannot handle = in the key
+    # XXX this sounds wrong, as it might prevent proper decoding
+    key = key.replace("=", "")
+    return key
 
 class GetBootMedium(Method):
     """
 
 class GetBootMedium(Method):
     """
@@ -50,7 +48,7 @@ class GetBootMedium(Method):
 
     As compared with its ancestor, this method provides a much more detailed
     detailed interface, that allows to
 
     As compared with its ancestor, this method provides a much more detailed
     detailed interface, that allows to
-    (*) either just preview the node config file (in which case 
+    (*) either just preview the node config file -- in which case 
         the node key is NOT recomputed, and NOT provided in the output
     (*) or regenerate the node config file for storage on a floppy 
         that is, exactly what the ancestor method used todo, 
         the node key is NOT recomputed, and NOT provided in the output
     (*) or regenerate the node config file for storage on a floppy 
         that is, exactly what the ancestor method used todo, 
@@ -59,7 +57,10 @@ class GetBootMedium(Method):
     (*) or just provide the generic ISO or USB boot images 
         in which case of course the node_id_or_hostname parameter is not used
 
     (*) or just provide the generic ISO or USB boot images 
         in which case of course the node_id_or_hostname parameter is not used
 
-    action is expected among the following string constants
+    action is expected among the following string constants according the
+    node type value:
+
+    for a 'regular' node:
     (*) node-preview
     (*) node-floppy
     (*) node-iso
     (*) node-preview
     (*) node-floppy
     (*) node-iso
@@ -67,11 +68,14 @@ class GetBootMedium(Method):
     (*) generic-iso
     (*) generic-usb
 
     (*) generic-iso
     (*) generic-usb
 
+    for a 'dummynet' node:
+    (*) node-preview
+    (*) dummynet-iso
+    (*) dummynet-usb
+
     Apart for the preview mode, this method generates a new node key for the
     specified node, effectively invalidating any old boot medium.
 
     Apart for the preview mode, this method generates a new node key for the
     specified node, effectively invalidating any old boot medium.
 
-    Non-admins can only generate files for nodes at their sites.
-
     In addition, two return mechanisms are supported.
     (*) The default behaviour is that the file's content is returned as a 
         base64-encoded string. This is how the ancestor method used to work.
     In addition, two return mechanisms are supported.
     (*) The default behaviour is that the file's content is returned as a 
         base64-encoded string. This is how the ancestor method used to work.
@@ -89,15 +93,31 @@ class GetBootMedium(Method):
         - %s : a file suffix appropriate in the context (.txt, .iso or the like)
         - %v : the bootcd version string (e.g. 4.0)
         - %p : the PLC name
         - %s : a file suffix appropriate in the context (.txt, .iso or the like)
         - %v : the bootcd version string (e.g. 4.0)
         - %p : the PLC name
+        - %f : the nodefamily
+        - %a : arch
         With the file-based return mechanism, the method returns the full pathname 
         With the file-based return mechanism, the method returns the full pathname 
-        of the result file; it is the caller's responsability to remove 
-        this file after use.
-
-        Security:
-        When the user's role is not admin, the provided directory *must* be under
-        the %d area
-
-        Housekeeping: 
+        of the result file; 
+        ** WARNING **
+        It is the caller's responsability to remove this file after use.
+
+    Options: an optional array of keywords. 
+        options are not supported for generic images
+        options are not supported for dummynet boxes
+    Currently supported are
+        - 'partition' - for USB actions only
+        - 'cramfs'
+        - 'serial' or 'serial:<console_spec>'
+        - 'no-hangcheck'
+        console_spec (or 'default') is passed as-is to bootcd/build.sh
+        it is expected to be a colon separated string denoting
+        tty - baudrate - parity - bits
+        e.g. ttyS0:115200:n:8
+
+    Security:
+        - Non-admins can only generate files for nodes at their sites.
+        - Non-admins, when they provide a filename, *must* specify it in the %d area
+
+   Housekeeping: 
         Whenever needed, the method stores intermediate files in a
         private area, typically not located under the web server's
         accessible area, and are cleaned up by the method.
         Whenever needed, the method stores intermediate files in a
         private area, typically not located under the web server's
         accessible area, and are cleaned up by the method.
@@ -110,17 +130,18 @@ class GetBootMedium(Method):
         Auth(),
         Mixed(Node.fields['node_id'],
               Node.fields['hostname']),
         Auth(),
         Mixed(Node.fields['node_id'],
               Node.fields['hostname']),
-        Parameter (str, "Action mode, expected in " + "|".join(boot_medium_actions)),
+        Parameter (str, "Action mode, expected value depends of the type of node"),
         Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
         Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
+        Parameter ([str], "Options"),
         ]
 
         ]
 
-    returns = Parameter(str, "Node boot medium, either inlined, or filename, depending to the filename parameter")
+    returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
 
 
-    BOOTCDDIR = "/usr/share/bootcd/"
-    BOOTCUSTOM = "/usr/share/bootcd/bootcustom.sh"
-    GENERICDIR = "/var/www/html/download/"
-    NODEDIR = "/var/tmp/bootmedium/results"
-    WORKDIR = "/var/tmp/bootmedium/work"
+    # define globals for regular nodes, override later for other types
+    BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
+    BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
+    GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
+    WORKDIR = "/var/tmp/bootmedium"
     DEBUG = False
     # uncomment this to preserve temporary area and bootcustom logs
     #DEBUG = True
     DEBUG = False
     # uncomment this to preserve temporary area and bootcustom logs
     #DEBUG = True
@@ -135,9 +156,15 @@ class GetBootMedium(Method):
             raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
         return parts
         
             raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
         return parts
         
-    # plnode.txt content
+    # Generate the node (plnode.txt) configuration content.
+    #
+    # This function will create the configuration file a node
+    # composed by:
+    #  - a common part, regardless of the 'node_type' tag
+    #  - XXX a special part, depending on the 'node_type' tag value.
     def floppy_contents (self, node, renew_key):
 
     def floppy_contents (self, node, renew_key):
 
+        # Do basic checks
         if node['peer_id'] is not None:
             raise PLCInvalidArgument, "Not a local node"
 
         if node['peer_id'] is not None:
             raise PLCInvalidArgument, "Not a local node"
 
@@ -147,26 +174,21 @@ class GetBootMedium(Method):
             if node['site_id'] not in self.caller['site_ids']:
                 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
 
             if node['site_id'] not in self.caller['site_ids']:
                 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
 
-        # Get node networks for this node
+        # Get interface for this node
         primary = None
         primary = None
-        nodenetworks = NodeNetworks(self.api, node['nodenetwork_ids'])
-        for nodenetwork in nodenetworks:
-            if nodenetwork['is_primary']:
-                primary = nodenetwork
+        interfaces = Interfaces(self.api, node['interface_ids'])
+        for interface in interfaces:
+            if interface['is_primary']:
+                primary = interface
                 break
         if primary is None:
             raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
 
         ( host, domain ) = self.split_hostname (node)
 
                 break
         if primary is None:
             raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
 
         ( host, domain ) = self.split_hostname (node)
 
+        # renew the key and save it on the database
         if renew_key:
         if renew_key:
-            # Generate 32 random bytes
-            bytes = random.sample(xrange(0, 256), 32)
-            # Base64 encode their string representation
-            node['key'] = base64.b64encode("".join(map(chr, bytes)))
-            # XXX Boot Manager cannot handle = in the key
-            node['key'] = node['key'].replace("=", "")
-            # Save it
+            node['key'] = compute_key()
             node.sync()
 
         # Generate node configuration file suitable for BootCD
             node.sync()
 
         # Generate node configuration file suitable for BootCD
@@ -175,6 +197,8 @@ class GetBootMedium(Method):
         if renew_key:
             file += 'NODE_ID="%d"\n' % node['node_id']
             file += 'NODE_KEY="%s"\n' % node['key']
         if renew_key:
             file += 'NODE_ID="%d"\n' % node['node_id']
             file += 'NODE_KEY="%s"\n' % node['key']
+            # not used anywhere, just a note for operations people
+            file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
 
         if primary['mac']:
             file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
 
         if primary['mac']:
             file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
@@ -193,8 +217,8 @@ class GetBootMedium(Method):
         file += 'HOST_NAME="%s"\n' % host
         file += 'DOMAIN_NAME="%s"\n' % domain
 
         file += 'HOST_NAME="%s"\n' % host
         file += 'DOMAIN_NAME="%s"\n' % domain
 
-        # define various nodenetwork settings attached to the primary nodenetwork
-        settings = NodeNetworkSettings (self.api, {'nodenetwork_id':nodenetwork['nodenetwork_id']})
+        # define various interface settings attached to the primary interface
+        settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
 
         categories = set()
         for setting in settings:
 
         categories = set()
         for setting in settings:
@@ -202,80 +226,80 @@ class GetBootMedium(Method):
                 categories.add(setting['category'])
         
         for category in categories:
                 categories.add(setting['category'])
         
         for category in categories:
-            category_settings = NodeNetworkSettings(self.api,{'nodenetwork_id':nodenetwork['nodenetwork_id'],
+            category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
                                                               'category':category})
             if category_settings:
                 file += '### Category : %s\n'%category
                 for setting in category_settings:
                     file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
 
                                                               'category':category})
             if category_settings:
                 file += '### Category : %s\n'%category
                 for setting in category_settings:
                     file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
 
-        for nodenetwork in nodenetworks:
-            if nodenetwork['method'] == 'ipmi':
-                file += 'IPMI_ADDRESS="%s"\n' % nodenetwork['ip']
-                if nodenetwork['mac']:
-                    file += 'IPMI_MAC="%s"\n' % nodenetwork['mac'].lower()
+        for interface in interfaces:
+            if interface['method'] == 'ipmi':
+                file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
+                if interface['mac']:
+                    file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
                 break
 
         return file
 
                 break
 
         return file
 
-    def bootcd_version (self):
+    # see also InstallBootstrapFS in bootmanager that does similar things
+    def get_nodefamily (self, node):
+        # get defaults from the myplc build
         try:
         try:
-            f = open (self.BOOTCDDIR + "/build/version.txt")
-            version=f.readline().strip()
-        finally:
-            f.close()
-        return version
-
-    def cleandir (self,tempdir):
-        if not self.DEBUG:
-            os.system("rm -rf %s"%tempdir)
+            (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
+        except:
+            (pldistro,arch) = ("planetlab","i386")
+            
+        # with no valid argument, return system-wide defaults
+        if not node:
+            return (pldistro,arch)
 
 
-    def call(self, auth, node_id_or_hostname, action, filename):
+        node_id=node['node_id']
 
 
-        ### check action
-        if action not in boot_medium_actions:
-            raise PLCInvalidArgument, "Unknown action %s"%action
+        tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
+        if tag: arch=tag
+        tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
+        if tag: pldistro=tag
 
 
-        ### compute file suffix 
-        if action.find("-iso") >= 0 :
-            suffix=".iso"
-        elif action.find("-usb") >= 0:
-            suffix=".usb"
-        else:
-            suffix=".txt"
+        return (pldistro,arch)
 
 
-        ### compute a 8 bytes random number
-        tempbytes = random.sample (xrange(0,256), 8);
-        def hexa2 (c):
-            return chr((c>>4)+65) + chr ((c&16)+65)
-        temp = "".join(map(hexa2,tempbytes))
-
-        ### check node if needed
-        if action.find("node-") == 0:
-            nodes = Nodes(self.api, [node_id_or_hostname])
-            if not nodes:
-                raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
-            node = nodes[0]
-            nodename = node['hostname']
-            
-        else:
-            node = None
-            nodename = temp
-            
-        ### handle filename
-        filename = filename.replace ("%d",self.NODEDIR)
+    def bootcd_version (self):
+        try:
+            return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
+        except:
+            raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
+    
+    def cleantrash (self):
+        for file in self.trash:
+            if self.DEBUG:
+                print 'DEBUG -- preserving',file
+            else:
+                os.unlink(file)
+
+    ### handle filename
+    # build the filename string 
+    # check for permissions and concurrency
+    # returns the filename
+    def handle_filename (self, filename, nodename, suffix, arch):
+        # allow to set filename to None or any other empty value
+        if not filename: filename=''
+        filename = filename.replace ("%d",self.WORKDIR)
         filename = filename.replace ("%n",nodename)
         filename = filename.replace ("%s",suffix)
         filename = filename.replace ("%p",self.api.config.PLC_NAME)
         filename = filename.replace ("%n",nodename)
         filename = filename.replace ("%s",suffix)
         filename = filename.replace ("%p",self.api.config.PLC_NAME)
-        # only if filename contains "%v", bootcd is maybe not avail ?
-        if filename.find("%v") >=0:
-            filename = filename.replace ("%v",self.bootcd_version())
+        # let's be cautious
+        try: filename = filename.replace ("%f", self.nodefamily)
+        except: pass
+        try: filename = filename.replace ("%a", arch)
+        except: pass
+        try: filename = filename.replace ("%v",self.bootcd_version())
+        except: pass
 
         ### Check filename location
         if filename != '':
             if 'admin' not in self.caller['roles']:
 
         ### Check filename location
         if filename != '':
             if 'admin' not in self.caller['roles']:
-                if ( filename.index(self.NODEDIR) != 0):
-                    raise PLCInvalidArgument, "File %s not under %s"%(filename,self.NODEDIR)
+                if ( filename.index(self.WORKDIR) != 0):
+                    raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
 
             ### output should not exist (concurrent runs ..)
             if os.path.exists(filename):
 
             ### output should not exist (concurrent runs ..)
             if os.path.exists(filename):
@@ -283,15 +307,155 @@ class GetBootMedium(Method):
 
             ### we can now safely create the file, 
             ### either we are admin or under a controlled location
 
             ### we can now safely create the file, 
             ### either we are admin or under a controlled location
-            if not os.path.exists(os.path.dirname(filename)):
-                try:
-                    os.makedirs (os.path.dirname(filename),0777)
-                except:
-                    raise PLCPermissionDenied, "Could not create dir %s"%os.path.dirname(filename)
+            filedir=os.path.dirname(filename)
+            # dirname does not return "." for a local filename like its shell counterpart
+            if filedir:
+                if not os.path.exists(filedir):
+                    try:
+                        os.makedirs (filedir,0777)
+                    except:
+                        raise PLCPermissionDenied, "Could not create dir %s"%filedir
+
+        return filename
+
+    # Build the command line to be executed
+    # according the node type
+    def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
+
+        command = ""
+
+        # regular node, make build's arguments
+        # and build the full command line to be called
+        if node_type == 'regular':
+
+            build_sh_options=""
+            if "cramfs" in build_sh_spec: 
+                type += "_cramfs"
+            if "serial" in build_sh_spec: 
+                build_sh_options += " -s %s"%build_sh_spec['serial']
+            
+            for karg in build_sh_spec['kargs']:
+                build_sh_options += ' -k "%s"'%karg
+
+            log_file="%s.log"%node_image
+
+            command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
+                                                                 floppy_file,
+                                                                 node_image,
+                                                                 type,
+                                                                 build_sh_options,
+                                                                 log_file)
+        # dummynet node
+        elif node_type == 'dummynet':
+            # the build script expect the following parameters:
+            # the package base directory
+            # the working directory
+            # the full path of the configuration file
+            # the name of the resulting image file
+            # the type of the generated image
+            # the name of the log file
+            command = "%s -b %s -w %s -f %s -o %s -t %s -l %s" \
+                        % (self.BOOTCDBUILD, self.BOOTCDDIR, self.WORKDIR,
+                           floppy_file, node_image, type, log_file)
+            command = "touch %s %s; echo 'dummynet build script not yet supported'" \
+                        % (log_file, node_image)
+
+        if self.DEBUG:
+            print "The build command line is %s" % command
+
+        return command 
+
+    def call(self, auth, node_id_or_hostname, action, filename, options = []):
+
+        self.trash=[]
+
+        ### compute file suffix and type
+        if action.find("-iso") >= 0 :
+            suffix=".iso"
+            type = "iso"
+        elif action.find("-usb") >= 0:
+            suffix=".usb"
+            type = "usb"
+        else:
+            suffix=".txt"
+            type = "txt"
+
+        # check for node existence and get node_type
+        nodes = Nodes(self.api, [node_id_or_hostname])
+        if not nodes:
+            raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
+        node = nodes[0]
+
+        if self.DEBUG: print "%s required on node %s. Node type is: %s" \
+                % (action, node['node_id'], node['node_type'])
+
+        # check the required action against the node type
+        node_type = node['node_type']
+        if action not in allowed_actions[node_type]:
+            raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
+                                   % (action, node_type, "|".join(allowed_actions[node_type]))
+
+        # handle / canonicalize options
+        if type == "txt":
+            if options:
+                raise PLCInvalidArgument, "Options are not supported for node configs"
+        else:
+            # create a dict for build.sh 
+            build_sh_spec={'kargs':[]}
+            for option in options:
+                if option == "cramfs":
+                    build_sh_spec['cramfs']=True
+                elif option == 'partition':
+                    if type != "usb":
+                        raise PLCInvalidArgument, "option 'partition' is for USB images only"
+                    else:
+                        type="usb_partition"
+                elif option == "serial":
+                    build_sh_spec['serial']='default'
+                elif option.find("serial:") == 0:
+                    build_sh_spec['serial']=option.replace("serial:","")
+                elif option == "no-hangcheck":
+                    build_sh_spec['kargs'].append('hcheck_reboot0')
+                else:
+                    raise PLCInvalidArgument, "unknown option %s"%option
 
 
+        # compute nodename according the action
+        if action.find("node-") == 0 or action.find("dummynet-") == 0:
+            nodename = node['hostname']
+        else:
+            node = None
+            # compute a 8 bytes random number
+            tempbytes = random.sample (xrange(0,256), 8);
+            def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
+            nodename = "".join(map(hexa2,tempbytes))
+
+        # override some global definition, according node_type
+        if node_type == 'dummynet':
+            self.BOOTCDDIR = "/usr/share/dummynet"             # the base installation dir
+            self.BOOTCDBUILD = "/usr/share/dummynet/build.sh"  # dummynet build script
+            self.WORKDIR = "/var/tmp/DummynetBoxMedium"                # temporary working dir
+
+        # get nodefamily
+        (pldistro,arch) = self.get_nodefamily(node)
+        self.nodefamily="%s-%s"%(pldistro,arch)
+
+        # apply on globals
+        for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
+            setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
+            
+        filename = self.handle_filename(filename, nodename, suffix, arch)
         
         
+        # log call
+        if node:
+            self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
+            self.event_objects={'Node': [ node ['node_id'] ]}
+        else:
+            self.message='GetBootMedium - generic - action=%s'%action
+
         ### generic media
         if action == 'generic-iso' or action == 'generic-usb':
         ### generic media
         if action == 'generic-iso' or action == 'generic-usb':
+            if options:
+                raise PLCInvalidArgument, "Options are not supported for generic images"
             # this raises an exception if bootcd is missing
             version = self.bootcd_version()
             generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
             # this raises an exception if bootcd is missing
             version = self.bootcd_version()
             generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
@@ -309,20 +473,10 @@ class GetBootMedium(Method):
                 ### return the generic medium content as-is, just base64 encoded
                 return base64.b64encode(file(generic_path).read())
 
                 ### return the generic medium content as-is, just base64 encoded
                 return base64.b64encode(file(generic_path).read())
 
-        ### floppy preview
-        if action == 'node-preview':
-            floppy = self.floppy_contents (node,False)
-            if filename:
-                try:
-                    file(filename,'w').write(floppy)
-                except:
-                    raise PLCPermissionDenied, "Could not write into %s"%filename
-                return filename
-            else:
-                return floppy
-
-        if action == 'node-floppy':
-            floppy = self.floppy_contents (node,True)
+        ### config file preview or regenerated
+        if action == 'node-preview' or action == 'node-floppy':
+            renew_key = (action == 'node-floppy')
+            floppy = self.floppy_contents (node,renew_key)
             if filename:
                 try:
                     file(filename,'w').write(floppy)
             if filename:
                 try:
                     file(filename,'w').write(floppy)
@@ -333,66 +487,75 @@ class GetBootMedium(Method):
                 return floppy
 
         ### we're left with node-iso and node-usb
                 return floppy
 
         ### we're left with node-iso and node-usb
-        if action == 'node-iso' or action == 'node-usb':
+        # the steps involved in the image creation are:
+        # - create and test the working environment
+        # - generate the configuration file
+        # - build and invoke the build command
+        # - delivery the resulting image file
+
+        if action == 'node-iso' or action == 'node-usb' \
+                 or action == 'dummynet-iso' or action == 'dummynet-usb':
 
             ### check we've got required material
             version = self.bootcd_version()
 
             ### check we've got required material
             version = self.bootcd_version()
-            generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
-                                             version,
-                                             suffix)
-            generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
-            if not os.path.isfile(generic_path):
-                raise PLCAPIError, "Cannot locate generic medium %s"%generic_path
             
             
-            if not os.path.isfile(self.BOOTCUSTOM):
-                raise PLCAPIError, "Cannot locate bootcustom script %s"%self.BOOTCUSTOM
+            if not os.path.isfile(self.BOOTCDBUILD):
+                raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
 
 
-            # need a temporary area
-            tempdir = "%s/%s"%(self.WORKDIR,nodename)
-            if not os.path.isdir(tempdir):
+            # create the workdir if needed
+            if not os.path.isdir(self.WORKDIR):
                 try:
                 try:
-                    os.makedirs(tempdir,0777)
+                    os.makedirs(self.WORKDIR,0777)
+                    os.chmod(self.WORKDIR,0777)
                 except:
                 except:
-                    raise PLCPermissionDenied, "Could not create dir %s"%tempdir
+                    raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
             
             try:
                 # generate floppy config
             
             try:
                 # generate floppy config
-                floppy = self.floppy_contents(node,True)
+                floppy_text = self.floppy_contents(node,True)
                 # store it
                 # store it
-                node_floppy = "%s/%s"%(tempdir,nodename)
+                floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
                 try:
                 try:
-                    file(node_floppy,"w").write(floppy)
+                    file(floppy_file,"w").write(floppy_text)
                 except:
                 except:
-                    raise PLCPermissionDenied, "Could not write into %s"%node_floppy
-
-                # invoke bootcustom
-                bootcustom_command = 'sudo %s -C "%s" "%s" "%s"'%(self.BOOTCUSTOM,
-                                                                  tempdir,
-                                                                  generic_path,
-                                                                  node_floppy)
-                if self.DEBUG:
-                    print 'bootcustom command:',bootcustom_command
-                ret=os.system(bootcustom_command)
+                    raise PLCPermissionDenied, "Could not write into %s"%floppy_file
+
+                self.trash.append(floppy_file)
+
+                node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
+                log_file="%s.log"%node_image
+
+                command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
+
+                # invoke the image build script
+                if command != "":
+                    ret=os.system(command)
+
                 if ret != 0:
                 if ret != 0:
-                    raise PLCPermissionDenied,"bootcustom.sh failed to create node-specific medium"
+                    raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
+                              (self.BOOTCDBUILD,  command, file(log_file).read())
+
+                self.trash.append(log_file)
 
 
-                node_image = "%s/%s%s"%(tempdir,nodename,suffix)
                 if not os.path.isfile (node_image):
                 if not os.path.isfile (node_image):
-                    raise PLCAPIError,"Unexpected location of bootcustom output - %s"%node_image
+                    raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
             
             
-                # cache result
+                # handle result
                 if filename:
                     ret=os.system("mv %s %s"%(node_image,filename))
                     if ret != 0:
                 if filename:
                     ret=os.system("mv %s %s"%(node_image,filename))
                     if ret != 0:
+                        self.trash.append(node_image)
+                        self.cleantrash()
                         raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
                         raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
-                    self.cleandir(tempdir)
+                    self.cleantrash()
                     return filename
                 else:
                     result = file(node_image).read()
                     return filename
                 else:
                     result = file(node_image).read()
-                    self.cleandir(tempdir)
+                    self.trash.append(node_image)
+                    self.cleantrash()
                     return base64.b64encode(result)
             except:
                     return base64.b64encode(result)
             except:
-                self.cleandir(tempdir)
+                self.cleantrash()
                 raise
                 
         # we're done here, or we missed something
                 raise
                 
         # we're done here, or we missed something