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
120 - 'no-hangcheck' - disable hangcheck
122 Tags: the following tags are taken into account when attached to the node:
123 'serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck'
126 - Non-admins can only generate files for nodes at their sites.
127 - Non-admins, when they provide a filename, *must* specify it in the %d area
130 Whenever needed, the method stores intermediate files in a
131 private area, typically not located under the web server's
132 accessible area, and are cleaned up by the method.
136 roles = ['admin', 'pi', 'tech']
140 Mixed(Node.fields['node_id'],
141 Node.fields['hostname']),
142 Parameter (str, "Action mode, expected value depends of the type of node"),
143 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
144 Parameter ([str], "Options"),
147 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
149 # define globals for regular nodes, override later for other types
150 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
151 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
152 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
153 WORKDIR = "/var/tmp/bootmedium"
155 # uncomment this to preserve temporary area and bootcustom logs
158 ### returns (host, domain) :
159 # 'host' : host part of the hostname
160 # 'domain' : domain part of the hostname
161 def split_hostname (self, node):
162 # Split hostname into host and domain parts
163 parts = node['hostname'].split(".", 1)
165 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
168 # Generate the node (plnode.txt) configuration content.
170 # This function will create the configuration file a node
172 # - a common part, regardless of the 'node_type' tag
173 # - XXX a special part, depending on the 'node_type' tag value.
174 def floppy_contents (self, node, renew_key):
177 if node['peer_id'] is not None:
178 raise PLCInvalidArgument, "Not a local node"
180 # If we are not an admin, make sure that the caller is a
181 # member of the site at which the node is located.
182 if 'admin' not in self.caller['roles']:
183 if node['site_id'] not in self.caller['site_ids']:
184 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
186 # Get interface for this node
188 interfaces = Interfaces(self.api, node['interface_ids'])
189 for interface in interfaces:
190 if interface['is_primary']:
194 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
196 ( host, domain ) = self.split_hostname (node)
198 # renew the key and save it on the database
200 node['key'] = compute_key()
203 # Generate node configuration file suitable for BootCD
207 file += 'NODE_ID="%d"\n' % node['node_id']
208 file += 'NODE_KEY="%s"\n' % node['key']
209 # not used anywhere, just a note for operations people
210 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
213 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
215 file += 'IP_METHOD="%s"\n' % primary['method']
217 if primary['method'] == 'static':
218 file += 'IP_ADDRESS="%s"\n' % primary['ip']
219 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
220 file += 'IP_NETMASK="%s"\n' % primary['netmask']
221 file += 'IP_NETADDR="%s"\n' % primary['network']
222 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
223 file += 'IP_DNS1="%s"\n' % primary['dns1']
224 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
226 file += 'HOST_NAME="%s"\n' % host
227 file += 'DOMAIN_NAME="%s"\n' % domain
229 # define various interface settings attached to the primary interface
230 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
233 for setting in settings:
234 if setting['category'] is not None:
235 categories.add(setting['category'])
237 for category in categories:
238 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
239 'category':category})
240 if category_settings:
241 file += '### Category : %s\n'%category
242 for setting in category_settings:
243 file += '%s_%s="%s"\n'%(category.upper(),setting['tagname'].upper(),setting['value'])
245 for interface in interfaces:
246 if interface['method'] == 'ipmi':
247 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
249 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
254 # see also InstallBootstrapFS in bootmanager that does similar things
255 def get_nodefamily (self, node):
256 # get defaults from the myplc build
258 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
260 (pldistro,arch) = ("planetlab","i386")
262 # with no valid argument, return system-wide defaults
264 return (pldistro,arch)
266 node_id=node['node_id']
268 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
270 tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
273 return (pldistro,arch)
275 def bootcd_version (self):
277 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
279 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
281 def cleantrash (self):
282 for file in self.trash:
284 print 'DEBUG -- preserving',file
289 # build the filename string
290 # check for permissions and concurrency
291 # returns the filename
292 def handle_filename (self, filename, nodename, suffix, arch):
293 # allow to set filename to None or any other empty value
294 if not filename: filename=''
295 filename = filename.replace ("%d",self.WORKDIR)
296 filename = filename.replace ("%n",nodename)
297 filename = filename.replace ("%s",suffix)
298 filename = filename.replace ("%p",self.api.config.PLC_NAME)
300 try: filename = filename.replace ("%f", self.nodefamily)
302 try: filename = filename.replace ("%a", arch)
304 try: filename = filename.replace ("%v",self.bootcd_version())
307 ### Check filename location
309 if 'admin' not in self.caller['roles']:
310 if ( filename.index(self.WORKDIR) != 0):
311 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
313 ### output should not exist (concurrent runs ..)
314 if os.path.exists(filename):
315 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
317 ### we can now safely create the file,
318 ### either we are admin or under a controlled location
319 filedir=os.path.dirname(filename)
320 # dirname does not return "." for a local filename like its shell counterpart
322 if not os.path.exists(filedir):
324 os.makedirs (filedir,0777)
326 raise PLCPermissionDenied, "Could not create dir %s"%filedir
330 # Build the command line to be executed
331 # according the node type
332 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
336 # regular node, make build's arguments
337 # and build the full command line to be called
338 if node_type == 'regular':
341 if "cramfs" in build_sh_spec:
343 if "serial" in build_sh_spec:
344 build_sh_options += " -s %s"%build_sh_spec['serial']
345 if "variant" in build_sh_spec:
346 build_sh_options += " -V %s"%build_sh_spec['variant']
348 for karg in build_sh_spec['kargs']:
349 build_sh_options += ' -k "%s"'%karg
351 log_file="%s.log"%node_image
353 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
360 elif node_type == 'dummynet':
361 # the build script expect the following parameters:
362 # the package base directory
363 # the working directory
364 # the full path of the configuration file
365 # the name of the resulting image file
366 # the type of the generated image
367 # the name of the log file
368 command = "%s -b %s -w %s -f %s -o %s -t %s -l %s" \
369 % (self.BOOTCDBUILD, self.BOOTCDDIR, self.WORKDIR,
370 floppy_file, node_image, type, log_file)
371 command = "touch %s %s; echo 'dummynet build script not yet supported'" \
372 % (log_file, node_image)
375 print "The build command line is %s" % command
379 def call(self, auth, node_id_or_hostname, action, filename, options = []):
383 ### compute file suffix and type
384 if action.find("-iso") >= 0 :
387 elif action.find("-usb") >= 0:
394 # check for node existence and get node_type
395 nodes = Nodes(self.api, [node_id_or_hostname])
397 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
400 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
401 % (action, node['node_id'], node['node_type'])
403 # check the required action against the node type
404 node_type = node['node_type']
405 if action not in allowed_actions[node_type]:
406 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
407 % (action, node_type, "|".join(allowed_actions[node_type]))
409 # handle / canonicalize options
412 raise PLCInvalidArgument, "Options are not supported for node configs"
414 # create a dict for build.sh
415 build_sh_spec={'kargs':[]}
416 # use node tags as defaults
417 # check for node tag equivalents
418 tags = NodeTags(self.api,
419 {'node_id': node['node_id'],
420 'tagname': ['serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck']},
421 ['tagname', 'value'])
424 if tag['tagname'] == 'serial':
425 build_sh_spec['serial'] = tag['value']
426 if tag['tagname'] == 'cramfs':
427 build_sh_spec['cramfs'] = True
428 if tag['tagname'] == 'kvariant':
429 build_sh_spec['variant'] = tag['value']
430 if tag['tagname'] == 'kargs':
431 build_sh_spec['kargs'].append(tag['value'].split())
432 if tag['tagname'] == 'no-hangcheck':
433 build_sh_spec['kargs'].append('hcheck_reboot0')
434 # then options can override tags
435 for option in options:
436 if option == "cramfs":
437 build_sh_spec['cramfs']=True
438 elif option == 'partition':
440 raise PLCInvalidArgument, "option 'partition' is for USB images only"
443 elif option == "serial":
444 build_sh_spec['serial']='default'
445 elif option.find("serial:") == 0:
446 build_sh_spec['serial']=option.replace("serial:","")
447 elif option.find("variant:") == 0:
448 build_sh_spec['variant']=option.replace("variant:","")
449 elif option == "no-hangcheck":
450 build_sh_spec['kargs'].append('hcheck_reboot0')
452 raise PLCInvalidArgument, "unknown option %s"%option
454 # compute nodename according the action
455 if action.find("node-") == 0 or action.find("dummynet-") == 0:
456 nodename = node['hostname']
459 # compute a 8 bytes random number
460 tempbytes = random.sample (xrange(0,256), 8);
461 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
462 nodename = "".join(map(hexa2,tempbytes))
464 # override some global definition, according node_type
465 if node_type == 'dummynet':
466 self.BOOTCDDIR = "/usr/share/dummynet" # the base installation dir
467 self.BOOTCDBUILD = "/usr/share/dummynet/build.sh" # dummynet build script
468 self.WORKDIR = "/var/tmp/DummynetBoxMedium" # temporary working dir
471 (pldistro,arch) = self.get_nodefamily(node)
472 self.nodefamily="%s-%s"%(pldistro,arch)
475 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
476 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
478 filename = self.handle_filename(filename, nodename, suffix, arch)
482 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
483 self.event_objects={'Node': [ node ['node_id'] ]}
485 self.message='GetBootMedium - generic - action=%s'%action
488 if action == 'generic-iso' or action == 'generic-usb':
490 raise PLCInvalidArgument, "Options are not supported for generic images"
491 # this raises an exception if bootcd is missing
492 version = self.bootcd_version()
493 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
496 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
499 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
503 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
505 ### return the generic medium content as-is, just base64 encoded
506 return base64.b64encode(file(generic_path).read())
508 ### config file preview or regenerated
509 if action == 'node-preview' or action == 'node-floppy':
510 renew_key = (action == 'node-floppy')
511 floppy = self.floppy_contents (node,renew_key)
514 file(filename,'w').write(floppy)
516 raise PLCPermissionDenied, "Could not write into %s"%filename
521 ### we're left with node-iso and node-usb
522 # the steps involved in the image creation are:
523 # - create and test the working environment
524 # - generate the configuration file
525 # - build and invoke the build command
526 # - delivery the resulting image file
528 if action == 'node-iso' or action == 'node-usb' \
529 or action == 'dummynet-iso' or action == 'dummynet-usb':
531 ### check we've got required material
532 version = self.bootcd_version()
534 if not os.path.isfile(self.BOOTCDBUILD):
535 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
537 # create the workdir if needed
538 if not os.path.isdir(self.WORKDIR):
540 os.makedirs(self.WORKDIR,0777)
541 os.chmod(self.WORKDIR,0777)
543 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
546 # generate floppy config
547 floppy_text = self.floppy_contents(node,True)
549 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
551 file(floppy_file,"w").write(floppy_text)
553 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
555 self.trash.append(floppy_file)
557 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
558 log_file="%s.log"%node_image
560 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
562 # invoke the image build script
564 ret=os.system(command)
567 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
568 (self.BOOTCDBUILD, command, file(log_file).read())
570 self.trash.append(log_file)
572 if not os.path.isfile (node_image):
573 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
577 ret=os.system('mv "%s" "%s"'%(node_image,filename))
579 self.trash.append(node_image)
581 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
585 result = file(node_image).read()
586 self.trash.append(node_image)
588 return base64.b64encode(result)
593 # we're done here, or we missed something
594 raise PLCAPIError,'Unhandled action %s'%action