8 from PLC.Faults import *
9 from PLC.Method import Method
10 from PLC.Parameter import Parameter, Mixed
11 from PLC.Auth import Auth
13 from PLC.Nodes import Node, Nodes
14 from PLC.Interfaces import Interface, Interfaces
15 from PLC.InterfaceTags import InterfaceTag, InterfaceTags
16 from PLC.NodeTags import NodeTag, NodeTags
18 # could not define this in the class..
19 # create a dict with the allowed actions for each type of node
21 'regular' : [ 'node-preview',
28 'dummynet' : [ 'node-preview',
36 # Generate 32 random bytes
37 bytes = random.sample(xrange(0, 256), 32)
38 # Base64 encode their string representation
39 key = base64.b64encode("".join(map(chr, bytes)))
40 # Boot Manager cannot handle = in the key
41 # XXX this sounds wrong, as it might prevent proper decoding
42 key = key.replace("=", "")
45 class GetBootMedium(Method):
47 This method is a redesign based on former, supposedly dedicated,
48 AdmGenerateNodeConfFile
50 As compared with its ancestor, this method provides a much more detailed
51 detailed interface, that allows to
52 (*) either just preview the node config file -- in which case
53 the node key is NOT recomputed, and NOT provided in the output
54 (*) or regenerate the node config file for storage on a floppy
55 that is, exactly what the ancestor method used todo,
56 including renewing the node's key
57 (*) or regenerate the config file and bundle it inside an ISO or USB image
58 (*) or just provide the generic ISO or USB boot images
59 in which case of course the node_id_or_hostname parameter is not used
61 action is expected among the following string constants according the
72 for a 'dummynet' node:
77 Apart for the preview mode, this method generates a new node key for the
78 specified node, effectively invalidating any old boot medium.
80 In addition, two return mechanisms are supported.
81 (*) The default behaviour is that the file's content is returned as a
82 base64-encoded string. This is how the ancestor method used to work.
83 To use this method, pass an empty string as the file parameter.
85 (*) Or, for efficiency -- this makes sense only when the API is used
86 by the web pages that run on the same host -- the caller may provide
87 a filename, in which case the resulting file is stored in that location instead.
88 The filename argument can use the following markers, that are expanded
90 - %d : default root dir (some builtin dedicated area under /var/tmp/)
91 Using this is recommended, and enforced for non-admin users
92 - %n : the node's name when this makes sense, or a mktemp-like name when
93 generic media is requested
94 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
95 - %v : the bootcd version string (e.g. 4.0)
99 With the file-based return mechanism, the method returns the full pathname
102 It is the caller's responsability to remove this file after use.
104 Options: an optional array of keywords.
105 options are not supported for generic images
106 options are not supported for dummynet boxes
107 Currently supported are
108 - 'partition' - for USB actions only
110 - 'serial' or 'serial:<console_spec>'
111 console_spec (or 'default') is passed as-is to bootcd/build.sh
112 it is expected to be a colon separated string denoting
113 tty - baudrate - parity - bits
114 e.g. ttyS0:115200:n:8
115 - 'variant:<variantname>'
116 passed to build.sh as -V <variant>
117 variants are used to run a different kernel on the bootCD
118 see kvariant.sh for how to create a variant
122 - Non-admins can only generate files for nodes at their sites.
123 - Non-admins, when they provide a filename, *must* specify it in the %d area
126 Whenever needed, the method stores intermediate files in a
127 private area, typically not located under the web server's
128 accessible area, and are cleaned up by the method.
132 roles = ['admin', 'pi', 'tech']
136 Mixed(Node.fields['node_id'],
137 Node.fields['hostname']),
138 Parameter (str, "Action mode, expected value depends of the type of node"),
139 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
140 Parameter ([str], "Options"),
143 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
145 # define globals for regular nodes, override later for other types
146 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
147 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
148 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
149 WORKDIR = "/var/tmp/bootmedium"
151 # uncomment this to preserve temporary area and bootcustom logs
154 ### returns (host, domain) :
155 # 'host' : host part of the hostname
156 # 'domain' : domain part of the hostname
157 def split_hostname (self, node):
158 # Split hostname into host and domain parts
159 parts = node['hostname'].split(".", 1)
161 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
164 # Generate the node (plnode.txt) configuration content.
166 # This function will create the configuration file a node
168 # - a common part, regardless of the 'node_type' tag
169 # - XXX a special part, depending on the 'node_type' tag value.
170 def floppy_contents (self, node, renew_key):
173 if node['peer_id'] is not None:
174 raise PLCInvalidArgument, "Not a local node"
176 # If we are not an admin, make sure that the caller is a
177 # member of the site at which the node is located.
178 if 'admin' not in self.caller['roles']:
179 if node['site_id'] not in self.caller['site_ids']:
180 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
182 # Get interface for this node
184 interfaces = Interfaces(self.api, node['interface_ids'])
185 for interface in interfaces:
186 if interface['is_primary']:
190 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
192 ( host, domain ) = self.split_hostname (node)
194 # renew the key and save it on the database
196 node['key'] = compute_key()
199 # Generate node configuration file suitable for BootCD
203 file += 'NODE_ID="%d"\n' % node['node_id']
204 file += 'NODE_KEY="%s"\n' % node['key']
205 # not used anywhere, just a note for operations people
206 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
209 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
211 file += 'IP_METHOD="%s"\n' % primary['method']
213 if primary['method'] == 'static':
214 file += 'IP_ADDRESS="%s"\n' % primary['ip']
215 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
216 file += 'IP_NETMASK="%s"\n' % primary['netmask']
217 file += 'IP_NETADDR="%s"\n' % primary['network']
218 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
219 file += 'IP_DNS1="%s"\n' % primary['dns1']
220 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
222 file += 'HOST_NAME="%s"\n' % host
223 file += 'DOMAIN_NAME="%s"\n' % domain
225 # define various interface settings attached to the primary interface
226 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
229 for setting in settings:
230 if setting['category'] is not None:
231 categories.add(setting['category'])
233 for category in categories:
234 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
235 'category':category})
236 if category_settings:
237 file += '### Category : %s\n'%category
238 for setting in category_settings:
239 file += '%s_%s="%s"\n'%(category.upper(),setting['tagname'].upper(),setting['value'])
241 for interface in interfaces:
242 if interface['method'] == 'ipmi':
243 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
245 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
250 # see also InstallBootstrapFS in bootmanager that does similar things
251 def get_nodefamily (self, node):
252 # get defaults from the myplc build
254 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
256 (pldistro,arch) = ("planetlab","i386")
258 # with no valid argument, return system-wide defaults
260 return (pldistro,arch)
262 node_id=node['node_id']
264 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
266 tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
269 return (pldistro,arch)
271 def bootcd_version (self):
273 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
275 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
277 def cleantrash (self):
278 for file in self.trash:
280 print 'DEBUG -- preserving',file
285 # build the filename string
286 # check for permissions and concurrency
287 # returns the filename
288 def handle_filename (self, filename, nodename, suffix, arch):
289 # allow to set filename to None or any other empty value
290 if not filename: filename=''
291 filename = filename.replace ("%d",self.WORKDIR)
292 filename = filename.replace ("%n",nodename)
293 filename = filename.replace ("%s",suffix)
294 filename = filename.replace ("%p",self.api.config.PLC_NAME)
296 try: filename = filename.replace ("%f", self.nodefamily)
298 try: filename = filename.replace ("%a", arch)
300 try: filename = filename.replace ("%v",self.bootcd_version())
303 ### Check filename location
305 if 'admin' not in self.caller['roles']:
306 if ( filename.index(self.WORKDIR) != 0):
307 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
309 ### output should not exist (concurrent runs ..)
310 if os.path.exists(filename):
311 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
313 ### we can now safely create the file,
314 ### either we are admin or under a controlled location
315 filedir=os.path.dirname(filename)
316 # dirname does not return "." for a local filename like its shell counterpart
318 if not os.path.exists(filedir):
320 os.makedirs (filedir,0777)
322 raise PLCPermissionDenied, "Could not create dir %s"%filedir
326 # Build the command line to be executed
327 # according the node type
328 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
332 # regular node, make build's arguments
333 # and build the full command line to be called
334 if node_type == 'regular':
337 if "cramfs" in build_sh_spec:
339 if "serial" in build_sh_spec:
340 build_sh_options += " -s %s"%build_sh_spec['serial']
341 if "variant" in build_sh_spec:
342 build_sh_options += " -V %s"%build_sh_spec['variant']
344 for karg in build_sh_spec['kargs']:
345 build_sh_options += ' -k "%s"'%karg
347 log_file="%s.log"%node_image
349 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
356 elif node_type == 'dummynet':
357 # the build script expect the following parameters:
358 # the package base directory
359 # the working directory
360 # the full path of the configuration file
361 # the name of the resulting image file
362 # the type of the generated image
363 # the name of the log file
364 command = "%s -b %s -w %s -f %s -o %s -t %s -l %s" \
365 % (self.BOOTCDBUILD, self.BOOTCDDIR, self.WORKDIR,
366 floppy_file, node_image, type, log_file)
367 command = "touch %s %s; echo 'dummynet build script not yet supported'" \
368 % (log_file, node_image)
371 print "The build command line is %s" % command
375 def call(self, auth, node_id_or_hostname, action, filename, options = []):
379 ### compute file suffix and type
380 if action.find("-iso") >= 0 :
383 elif action.find("-usb") >= 0:
390 # check for node existence and get node_type
391 nodes = Nodes(self.api, [node_id_or_hostname])
393 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
396 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
397 % (action, node['node_id'], node['node_type'])
399 # check the required action against the node type
400 node_type = node['node_type']
401 if action not in allowed_actions[node_type]:
402 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
403 % (action, node_type, "|".join(allowed_actions[node_type]))
405 # handle / canonicalize options
408 raise PLCInvalidArgument, "Options are not supported for node configs"
410 # create a dict for build.sh
411 build_sh_spec={'kargs':[]}
412 for option in options:
413 if option == "cramfs":
414 build_sh_spec['cramfs']=True
415 elif option == 'partition':
417 raise PLCInvalidArgument, "option 'partition' is for USB images only"
420 elif option == "serial":
421 build_sh_spec['serial']='default'
422 elif option.find("serial:") == 0:
423 build_sh_spec['serial']=option.replace("serial:","")
424 elif option.find("variant:") == 0:
425 build_sh_spec['variant']=option.replace("variant:","")
426 elif option == "no-hangcheck":
427 build_sh_spec['kargs'].append('hcheck_reboot0')
429 raise PLCInvalidArgument, "unknown option %s"%option
430 # check for node tag equivalents
431 tags = NodeTags(self.api, {'node_id': node['node_id'], 'tagname': [
432 'serial', 'cramfs', 'kvariant',
433 'kargs', 'no-hangcheck']},
434 ['tagname', 'value'])
437 if tag['tagname'] == 'serial':
438 build_sh_spec['serial'] = tag['value']
439 if tag['tagname'] == 'cramfs':
440 build_sh_spec['cramfs'] = True
441 if tag['tagname'] == 'kvariant':
442 build_sh_spec['variant'] = tag['value']
443 if tag['tagname'] == 'kargs':
444 build_sh_spec['kargs'].append(tag['value'].split())
445 if tag['tagname'] == 'no-hangcheck':
446 build_sh_spec['kargs'].append('hcheck_reboot0')
448 # compute nodename according the action
449 if action.find("node-") == 0 or action.find("dummynet-") == 0:
450 nodename = node['hostname']
453 # compute a 8 bytes random number
454 tempbytes = random.sample (xrange(0,256), 8);
455 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
456 nodename = "".join(map(hexa2,tempbytes))
458 # override some global definition, according node_type
459 if node_type == 'dummynet':
460 self.BOOTCDDIR = "/usr/share/dummynet" # the base installation dir
461 self.BOOTCDBUILD = "/usr/share/dummynet/build.sh" # dummynet build script
462 self.WORKDIR = "/var/tmp/DummynetBoxMedium" # temporary working dir
465 (pldistro,arch) = self.get_nodefamily(node)
466 self.nodefamily="%s-%s"%(pldistro,arch)
469 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
470 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
472 filename = self.handle_filename(filename, nodename, suffix, arch)
476 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
477 self.event_objects={'Node': [ node ['node_id'] ]}
479 self.message='GetBootMedium - generic - action=%s'%action
482 if action == 'generic-iso' or action == 'generic-usb':
484 raise PLCInvalidArgument, "Options are not supported for generic images"
485 # this raises an exception if bootcd is missing
486 version = self.bootcd_version()
487 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
490 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
493 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
497 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
499 ### return the generic medium content as-is, just base64 encoded
500 return base64.b64encode(file(generic_path).read())
502 ### config file preview or regenerated
503 if action == 'node-preview' or action == 'node-floppy':
504 renew_key = (action == 'node-floppy')
505 floppy = self.floppy_contents (node,renew_key)
508 file(filename,'w').write(floppy)
510 raise PLCPermissionDenied, "Could not write into %s"%filename
515 ### we're left with node-iso and node-usb
516 # the steps involved in the image creation are:
517 # - create and test the working environment
518 # - generate the configuration file
519 # - build and invoke the build command
520 # - delivery the resulting image file
522 if action == 'node-iso' or action == 'node-usb' \
523 or action == 'dummynet-iso' or action == 'dummynet-usb':
525 ### check we've got required material
526 version = self.bootcd_version()
528 if not os.path.isfile(self.BOOTCDBUILD):
529 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
531 # create the workdir if needed
532 if not os.path.isdir(self.WORKDIR):
534 os.makedirs(self.WORKDIR,0777)
535 os.chmod(self.WORKDIR,0777)
537 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
540 # generate floppy config
541 floppy_text = self.floppy_contents(node,True)
543 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
545 file(floppy_file,"w").write(floppy_text)
547 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
549 self.trash.append(floppy_file)
551 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
552 log_file="%s.log"%node_image
554 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
556 # invoke the image build script
558 ret=os.system(command)
561 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
562 (self.BOOTCDBUILD, command, file(log_file).read())
564 self.trash.append(log_file)
566 if not os.path.isfile (node_image):
567 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
571 ret=os.system('mv "%s" "%s"'%(node_image,filename))
573 self.trash.append(node_image)
575 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
579 result = file(node_image).read()
580 self.trash.append(node_image)
582 return base64.b64encode(result)
587 # we're done here, or we missed something
588 raise PLCAPIError,'Unhandled action %s'%action