9 from PLC.Faults import *
10 from PLC.Method import Method
11 from PLC.Parameter import Parameter, Mixed
12 from PLC.Auth import Auth
14 from PLC.Nodes import Node, Nodes
15 from PLC.Interfaces import Interface, Interfaces
16 from PLC.InterfaceTags import InterfaceTag, InterfaceTags
17 from PLC.NodeTags import NodeTag, NodeTags
19 # could not define this in the class..
20 # create a dict with the allowed actions for each type of node
22 'regular' : [ 'node-preview',
29 'dummynet' : [ 'node-preview',
37 # Generate 32 random bytes
38 bytes = random.sample(xrange(0, 256), 32)
39 # Base64 encode their string representation
40 key = base64.b64encode("".join(map(chr, bytes)))
41 # Boot Manager cannot handle = in the key
42 # XXX this sounds wrong, as it might prevent proper decoding
43 key = key.replace("=", "")
46 class GetBootMedium(Method):
48 This method is a redesign based on former, supposedly dedicated,
49 AdmGenerateNodeConfFile
51 As compared with its ancestor, this method provides a much more
52 detailed interface, that allows to
53 (*) either just preview the node config file -- in which case
54 the node key is NOT recomputed, and NOT provided in the output
55 (*) or regenerate the node config file for storage on a floppy
56 that is, exactly what the ancestor method used todo,
57 including renewing the node's key
58 (*) or regenerate the config file and bundle it inside an ISO or USB image
59 (*) or just provide the generic ISO or USB boot images
60 in which case of course the node_id_or_hostname parameter is not used
62 action is expected among the following string constants according the
73 for a 'dummynet' node:
78 Apart for the preview mode, this method generates a new node key for the
79 specified node, effectively invalidating any old boot medium.
81 In addition, two return mechanisms are supported.
82 (*) The default behaviour is that the file's content is returned as a
83 base64-encoded string. This is how the ancestor method used to work.
84 To use this method, pass an empty string as the file parameter.
86 (*) Or, for efficiency -- this makes sense only when the API is used
87 by the web pages that run on the same host -- the caller may provide
88 a filename, in which case the resulting file is stored in that location instead.
89 The filename argument can use the following markers, that are expanded
91 - %d : default root dir (some builtin dedicated area under /var/tmp/)
92 Using this is recommended, and enforced for non-admin users
93 - %n : the node's name when this makes sense, or a mktemp-like name when
94 generic media is requested
95 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
96 - %v : the bootcd version string (e.g. 4.0)
100 With the file-based return mechanism, the method returns the full pathname
103 It is the caller's responsability to remove this file after use.
105 Options: an optional array of keywords.
106 options are not supported for generic images
107 options are not supported for dummynet boxes
108 Currently supported are
109 - 'partition' - for USB actions only
111 - 'serial' or 'serial:<console_spec>'
112 console_spec (or 'default') is passed as-is to bootcd/build.sh
113 it is expected to be a colon separated string denoting
114 tty - baudrate - parity - bits
115 e.g. ttyS0:115200:n:8
116 - 'variant:<variantname>'
117 passed to build.sh as -V <variant>
118 variants are used to run a different kernel on the bootCD
119 see kvariant.sh for how to create a variant
123 - Non-admins can only generate files for nodes at their sites.
124 - Non-admins, when they provide a filename, *must* specify it in the %d area
127 Whenever needed, the method stores intermediate files in a
128 private area, typically not located under the web server's
129 accessible area, and are cleaned up by the method.
133 roles = ['admin', 'pi', 'tech']
137 Mixed(Node.fields['node_id'],
138 Node.fields['hostname']),
139 Parameter (str, "Action mode, expected value depends of the type of node"),
140 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
141 Parameter ([str], "Options"),
144 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
146 # define globals for regular nodes, override later for other types
147 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
148 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
149 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
150 WORKDIR = "/var/tmp/bootmedium"
152 # uncomment this to preserve temporary area and bootcustom logs
155 ### returns (host, domain) :
156 # 'host' : host part of the hostname
157 # 'domain' : domain part of the hostname
158 def split_hostname (self, node):
159 # Split hostname into host and domain parts
160 parts = node['hostname'].split(".", 1)
162 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
165 # Generate the node (plnode.txt) configuration content.
167 # This function will create the configuration file a node
169 # - a common part, regardless of the 'node_type' tag
170 # - XXX a special part, depending on the 'node_type' tag value.
171 def floppy_contents (self, node, renew_key):
174 if node['peer_id'] is not None:
175 raise PLCInvalidArgument, "Not a local node"
177 # If we are not an admin, make sure that the caller is a
178 # member of the site at which the node is located.
179 if 'admin' not in self.caller['roles']:
180 if node['site_id'] not in self.caller['site_ids']:
181 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
183 # Get interface for this node
185 interfaces = Interfaces(self.api, node['interface_ids'])
186 for interface in interfaces:
187 if interface['is_primary']:
191 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
193 ( host, domain ) = self.split_hostname (node)
195 # renew the key and save it on the database
197 node['key'] = compute_key()
200 # Generate node configuration file suitable for BootCD
204 file += 'NODE_ID="%d"\n' % node['node_id']
205 file += 'NODE_KEY="%s"\n' % node['key']
206 # not used anywhere, just a note for operations people
207 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
210 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
212 file += 'IP_METHOD="%s"\n' % primary['method']
214 if primary['method'] == 'static':
215 file += 'IP_ADDRESS="%s"\n' % primary['ip']
216 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
217 file += 'IP_NETMASK="%s"\n' % primary['netmask']
218 file += 'IP_NETADDR="%s"\n' % primary['network']
219 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
220 file += 'IP_DNS1="%s"\n' % primary['dns1']
221 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
223 file += 'HOST_NAME="%s"\n' % host
224 file += 'DOMAIN_NAME="%s"\n' % domain
226 # define various interface settings attached to the primary interface
227 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
230 for setting in settings:
231 if setting['category'] is not None:
232 categories.add(setting['category'])
234 for category in categories:
235 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
236 'category':category})
237 if category_settings:
238 file += '### Category : %s\n'%category
239 for setting in category_settings:
240 file += '%s_%s="%s"\n'%(category.upper(),setting['tagname'].upper(),setting['value'])
242 for interface in interfaces:
243 if interface['method'] == 'ipmi':
244 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
246 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
251 # see also InstallBootstrapFS in bootmanager that does similar things
252 def get_nodefamily (self, node):
253 # get defaults from the myplc build
255 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
257 (pldistro,arch) = ("planetlab","i386")
259 # with no valid argument, return system-wide defaults
261 return (pldistro,arch)
263 node_id=node['node_id']
265 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
267 tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
270 return (pldistro,arch)
272 def bootcd_version (self):
274 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
276 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
278 def cleantrash (self):
279 for file in self.trash:
281 print 'DEBUG -- preserving',file
286 # build the filename string
287 # check for permissions and concurrency
288 # returns the filename
289 def handle_filename (self, filename, nodename, suffix, arch):
290 # allow to set filename to None or any other empty value
291 if not filename: filename=''
292 filename = filename.replace ("%d",self.WORKDIR)
293 filename = filename.replace ("%n",nodename)
294 filename = filename.replace ("%s",suffix)
295 filename = filename.replace ("%p",self.api.config.PLC_NAME)
297 try: filename = filename.replace ("%f", self.nodefamily)
299 try: filename = filename.replace ("%a", arch)
301 try: filename = filename.replace ("%v",self.bootcd_version())
304 ### Check filename location
306 if 'admin' not in self.caller['roles']:
307 if ( filename.index(self.WORKDIR) != 0):
308 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
310 ### output should not exist (concurrent runs ..)
311 if os.path.exists(filename):
312 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
314 ### we can now safely create the file,
315 ### either we are admin or under a controlled location
316 filedir=os.path.dirname(filename)
317 # dirname does not return "." for a local filename like its shell counterpart
319 if not os.path.exists(filedir):
321 os.makedirs (filedir,0777)
323 raise PLCPermissionDenied, "Could not create dir %s"%filedir
327 # Build the command line to be executed
328 # according the node type
329 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
333 # regular node, make build's arguments
334 # and build the full command line to be called
335 if node_type == 'regular':
338 if "cramfs" in build_sh_spec:
340 if "serial" in build_sh_spec:
341 build_sh_options += " -s %s"%build_sh_spec['serial']
342 if "variant" in build_sh_spec:
343 build_sh_options += " -V %s"%build_sh_spec['variant']
345 for karg in build_sh_spec['kargs']:
346 build_sh_options += ' -k "%s"'%karg
348 log_file="%s.log"%node_image
350 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
357 elif node_type == 'dummynet':
358 # the build script expect the following parameters:
359 # the package base directory
360 # the working directory
361 # the full path of the configuration file
362 # the name of the resulting image file
363 # the type of the generated image
364 # the name of the log file
365 command = "%s -b %s -w %s -f %s -o %s -t %s -l %s" \
366 % (self.BOOTCDBUILD, self.BOOTCDDIR, self.WORKDIR,
367 floppy_file, node_image, type, log_file)
368 command = "touch %s %s; echo 'dummynet build script not yet supported'" \
369 % (log_file, node_image)
372 print "The build command line is %s" % command
376 def call(self, auth, node_id_or_hostname, action, filename, options = []):
380 ### compute file suffix and type
381 if action.find("-iso") >= 0 :
384 elif action.find("-usb") >= 0:
391 # check for node existence and get node_type
392 nodes = Nodes(self.api, [node_id_or_hostname])
394 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
397 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
398 % (action, node['node_id'], node['node_type'])
400 # check the required action against the node type
401 node_type = node['node_type']
402 if action not in allowed_actions[node_type]:
403 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
404 % (action, node_type, "|".join(allowed_actions[node_type]))
406 # handle / canonicalize options
409 raise PLCInvalidArgument, "Options are not supported for node configs"
411 # create a dict for build.sh
412 build_sh_spec={'kargs':[]}
413 for option in options:
414 if option == "cramfs":
415 build_sh_spec['cramfs']=True
416 elif option == 'partition':
418 raise PLCInvalidArgument, "option 'partition' is for USB images only"
421 elif option == "serial":
422 build_sh_spec['serial']='default'
423 elif option.find("serial:") == 0:
424 build_sh_spec['serial']=option.replace("serial:","")
425 elif option.find("variant:") == 0:
426 build_sh_spec['variant']=option.replace("variant:","")
427 elif option == "no-hangcheck":
428 build_sh_spec['kargs'].append('hcheck_reboot0')
430 raise PLCInvalidArgument, "unknown option %s"%option
431 # check for node tag equivalents
432 tags = NodeTags(self.api, {'node_id': node['node_id'], 'tagname': [
433 'serial', 'cramfs', 'kvariant',
434 'kargs', 'no-hangcheck']},
435 ['tagname', 'value'])
438 if tag['tagname'] == 'serial':
439 build_sh_spec['serial'] = tag['value']
440 if tag['tagname'] == 'cramfs':
441 build_sh_spec['cramfs'] = True
442 if tag['tagname'] == 'kvariant':
443 build_sh_spec['variant'] = tag['value']
444 if tag['tagname'] == 'kargs':
445 build_sh_spec['kargs'].append(tag['value'].split())
446 if tag['tagname'] == 'no-hangcheck':
447 build_sh_spec['kargs'].append('hcheck_reboot0')
449 # compute nodename according the action
450 if action.find("node-") == 0 or action.find("dummynet-") == 0:
451 nodename = node['hostname']
454 # compute a 8 bytes random number
455 tempbytes = random.sample (xrange(0,256), 8);
456 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
457 nodename = "".join(map(hexa2,tempbytes))
459 # override some global definition, according node_type
460 if node_type == 'dummynet':
461 self.BOOTCDDIR = "/usr/share/dummynet" # the base installation dir
462 self.BOOTCDBUILD = "/usr/share/dummynet/build.sh" # dummynet build script
463 self.WORKDIR = "/var/tmp/DummynetBoxMedium" # temporary working dir
466 (pldistro,arch) = self.get_nodefamily(node)
467 self.nodefamily="%s-%s"%(pldistro,arch)
470 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
471 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
473 filename = self.handle_filename(filename, nodename, suffix, arch)
477 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
478 self.event_objects={'Node': [ node ['node_id'] ]}
480 self.message='GetBootMedium - generic - action=%s'%action
483 if action == 'generic-iso' or action == 'generic-usb':
485 raise PLCInvalidArgument, "Options are not supported for generic images"
486 # this raises an exception if bootcd is missing
487 version = self.bootcd_version()
488 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
491 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
494 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
498 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
500 ### return the generic medium content as-is, just base64 encoded
501 return base64.b64encode(file(generic_path).read())
503 ### config file preview or regenerated
504 if action == 'node-preview' or action == 'node-floppy':
505 renew_key = (action == 'node-floppy')
506 floppy = self.floppy_contents (node,renew_key)
509 file(filename,'w').write(floppy)
511 raise PLCPermissionDenied, "Could not write into %s"%filename
516 ### we're left with node-iso and node-usb
517 # the steps involved in the image creation are:
518 # - create and test the working environment
519 # - generate the configuration file
520 # - build and invoke the build command
521 # - delivery the resulting image file
523 if action == 'node-iso' or action == 'node-usb' \
524 or action == 'dummynet-iso' or action == 'dummynet-usb':
526 ### check we've got required material
527 version = self.bootcd_version()
529 if not os.path.isfile(self.BOOTCDBUILD):
530 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
532 # create the workdir if needed
533 if not os.path.isdir(self.WORKDIR):
535 os.makedirs(self.WORKDIR,0777)
536 os.chmod(self.WORKDIR,0777)
538 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
541 # generate floppy config
542 floppy_text = self.floppy_contents(node,True)
544 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
546 file(floppy_file,"w").write(floppy_text)
548 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
550 self.trash.append(floppy_file)
552 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
553 log_file="%s.log"%node_image
555 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
557 # invoke the image build script
559 ret=os.system(command)
562 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
563 (self.BOOTCDBUILD, command, file(log_file).read())
565 self.trash.append(log_file)
567 if not os.path.isfile (node_image):
568 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
572 ret=os.system('mv "%s" "%s"'%(node_image,filename))
574 self.trash.append(node_image)
576 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
580 result = file(node_image).read()
581 self.trash.append(node_image)
583 return base64.b64encode(result)
588 # we're done here, or we missed something
589 raise PLCAPIError,'Unhandled action %s'%action