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
17 # could not define this in the class..
18 # create a dict with the allowed actions for each type of node
20 'regular' : [ 'node-preview',
27 'dummynet' : [ 'node-preview',
35 # Generate 32 random bytes
36 bytes = random.sample(xrange(0, 256), 32)
37 # Base64 encode their string representation
38 key = base64.b64encode("".join(map(chr, bytes)))
39 # Boot Manager cannot handle = in the key
40 # XXX this sounds wrong, as it might prevent proper decoding
41 key = key.replace("=", "")
44 class GetBootMedium(Method):
46 This method is a redesign based on former, supposedly dedicated,
47 AdmGenerateNodeConfFile
49 As compared with its ancestor, this method provides a much more detailed
50 detailed interface, that allows to
51 (*) either just preview the node config file -- in which case
52 the node key is NOT recomputed, and NOT provided in the output
53 (*) or regenerate the node config file for storage on a floppy
54 that is, exactly what the ancestor method used todo,
55 including renewing the node's key
56 (*) or regenerate the config file and bundle it inside an ISO or USB image
57 (*) or just provide the generic ISO or USB boot images
58 in which case of course the node_id_or_hostname parameter is not used
60 action is expected among the following string constants according the
71 for a 'dummynet' node:
76 Apart for the preview mode, this method generates a new node key for the
77 specified node, effectively invalidating any old boot medium.
79 In addition, two return mechanisms are supported.
80 (*) The default behaviour is that the file's content is returned as a
81 base64-encoded string. This is how the ancestor method used to work.
82 To use this method, pass an empty string as the file parameter.
84 (*) Or, for efficiency -- this makes sense only when the API is used
85 by the web pages that run on the same host -- the caller may provide
86 a filename, in which case the resulting file is stored in that location instead.
87 The filename argument can use the following markers, that are expanded
89 - %d : default root dir (some builtin dedicated area under /var/tmp/)
90 Using this is recommended, and enforced for non-admin users
91 - %n : the node's name when this makes sense, or a mktemp-like name when
92 generic media is requested
93 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
94 - %v : the bootcd version string (e.g. 4.0)
98 With the file-based return mechanism, the method returns the full pathname
101 It is the caller's responsability to remove this file after use.
103 Options: an optional array of keywords.
104 options are not supported for generic images
105 options are not supported for dummynet boxes
106 Currently supported are
107 - 'partition' - for USB actions only
109 - 'serial' or 'serial:<console_spec>'
110 console_spec (or 'default') is passed as-is to bootcd/build.sh
111 it is expected to be a colon separated string denoting
112 tty - baudrate - parity - bits
113 e.g. ttyS0:115200:n:8
114 - 'variant:<variantname>'
115 passed to build.sh as -V <variant>
116 variants are used to run a different kernel on the bootCD
117 see kvariant.sh for how to create a variant
121 - Non-admins can only generate files for nodes at their sites.
122 - Non-admins, when they provide a filename, *must* specify it in the %d area
125 Whenever needed, the method stores intermediate files in a
126 private area, typically not located under the web server's
127 accessible area, and are cleaned up by the method.
131 roles = ['admin', 'pi', 'tech']
135 Mixed(Node.fields['node_id'],
136 Node.fields['hostname']),
137 Parameter (str, "Action mode, expected value depends of the type of node"),
138 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
139 Parameter ([str], "Options"),
142 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
144 # define globals for regular nodes, override later for other types
145 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
146 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
147 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
148 WORKDIR = "/var/tmp/bootmedium"
150 # uncomment this to preserve temporary area and bootcustom logs
153 ### returns (host, domain) :
154 # 'host' : host part of the hostname
155 # 'domain' : domain part of the hostname
156 def split_hostname (self, node):
157 # Split hostname into host and domain parts
158 parts = node['hostname'].split(".", 1)
160 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
163 # Generate the node (plnode.txt) configuration content.
165 # This function will create the configuration file a node
167 # - a common part, regardless of the 'node_type' tag
168 # - XXX a special part, depending on the 'node_type' tag value.
169 def floppy_contents (self, node, renew_key):
172 if node['peer_id'] is not None:
173 raise PLCInvalidArgument, "Not a local node"
175 # If we are not an admin, make sure that the caller is a
176 # member of the site at which the node is located.
177 if 'admin' not in self.caller['roles']:
178 if node['site_id'] not in self.caller['site_ids']:
179 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
181 # Get interface for this node
183 interfaces = Interfaces(self.api, node['interface_ids'])
184 for interface in interfaces:
185 if interface['is_primary']:
189 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
191 ( host, domain ) = self.split_hostname (node)
193 # renew the key and save it on the database
195 node['key'] = compute_key()
198 # Generate node configuration file suitable for BootCD
202 file += 'NODE_ID="%d"\n' % node['node_id']
203 file += 'NODE_KEY="%s"\n' % node['key']
204 # not used anywhere, just a note for operations people
205 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
208 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
210 file += 'IP_METHOD="%s"\n' % primary['method']
212 if primary['method'] == 'static':
213 file += 'IP_ADDRESS="%s"\n' % primary['ip']
214 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
215 file += 'IP_NETMASK="%s"\n' % primary['netmask']
216 file += 'IP_NETADDR="%s"\n' % primary['network']
217 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
218 file += 'IP_DNS1="%s"\n' % primary['dns1']
219 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
221 file += 'HOST_NAME="%s"\n' % host
222 file += 'DOMAIN_NAME="%s"\n' % domain
224 # define various interface settings attached to the primary interface
225 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
228 for setting in settings:
229 if setting['category'] is not None:
230 categories.add(setting['category'])
232 for category in categories:
233 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
234 'category':category})
235 if category_settings:
236 file += '### Category : %s\n'%category
237 for setting in category_settings:
238 file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
240 for interface in interfaces:
241 if interface['method'] == 'ipmi':
242 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
244 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
249 # see also InstallBootstrapFS in bootmanager that does similar things
250 def get_nodefamily (self, node):
251 # get defaults from the myplc build
253 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
255 (pldistro,arch) = ("planetlab","i386")
257 # with no valid argument, return system-wide defaults
259 return (pldistro,arch)
261 node_id=node['node_id']
263 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
265 tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
268 return (pldistro,arch)
270 def bootcd_version (self):
272 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
274 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
276 def cleantrash (self):
277 for file in self.trash:
279 print 'DEBUG -- preserving',file
284 # build the filename string
285 # check for permissions and concurrency
286 # returns the filename
287 def handle_filename (self, filename, nodename, suffix, arch):
288 # allow to set filename to None or any other empty value
289 if not filename: filename=''
290 filename = filename.replace ("%d",self.WORKDIR)
291 filename = filename.replace ("%n",nodename)
292 filename = filename.replace ("%s",suffix)
293 filename = filename.replace ("%p",self.api.config.PLC_NAME)
295 try: filename = filename.replace ("%f", self.nodefamily)
297 try: filename = filename.replace ("%a", arch)
299 try: filename = filename.replace ("%v",self.bootcd_version())
302 ### Check filename location
304 if 'admin' not in self.caller['roles']:
305 if ( filename.index(self.WORKDIR) != 0):
306 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
308 ### output should not exist (concurrent runs ..)
309 if os.path.exists(filename):
310 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
312 ### we can now safely create the file,
313 ### either we are admin or under a controlled location
314 filedir=os.path.dirname(filename)
315 # dirname does not return "." for a local filename like its shell counterpart
317 if not os.path.exists(filedir):
319 os.makedirs (filedir,0777)
321 raise PLCPermissionDenied, "Could not create dir %s"%filedir
325 # Build the command line to be executed
326 # according the node type
327 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
331 # regular node, make build's arguments
332 # and build the full command line to be called
333 if node_type == 'regular':
336 if "cramfs" in build_sh_spec:
338 if "serial" in build_sh_spec:
339 build_sh_options += " -s %s"%build_sh_spec['serial']
340 if "variant" in build_sh_spec:
341 build_sh_options += " -V %s"%build_sh_spec['variant']
343 for karg in build_sh_spec['kargs']:
344 build_sh_options += ' -k "%s"'%karg
346 log_file="%s.log"%node_image
348 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
355 elif node_type == 'dummynet':
356 # the build script expect the following parameters:
357 # the package base directory
358 # the working directory
359 # the full path of the configuration file
360 # the name of the resulting image file
361 # the type of the generated image
362 # the name of the log file
363 command = "%s -b %s -w %s -f %s -o %s -t %s -l %s" \
364 % (self.BOOTCDBUILD, self.BOOTCDDIR, self.WORKDIR,
365 floppy_file, node_image, type, log_file)
366 command = "touch %s %s; echo 'dummynet build script not yet supported'" \
367 % (log_file, node_image)
370 print "The build command line is %s" % command
374 def call(self, auth, node_id_or_hostname, action, filename, options = []):
378 ### compute file suffix and type
379 if action.find("-iso") >= 0 :
382 elif action.find("-usb") >= 0:
389 # check for node existence and get node_type
390 nodes = Nodes(self.api, [node_id_or_hostname])
392 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
395 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
396 % (action, node['node_id'], node['node_type'])
398 # check the required action against the node type
399 node_type = node['node_type']
400 if action not in allowed_actions[node_type]:
401 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
402 % (action, node_type, "|".join(allowed_actions[node_type]))
404 # handle / canonicalize options
407 raise PLCInvalidArgument, "Options are not supported for node configs"
409 # create a dict for build.sh
410 build_sh_spec={'kargs':[]}
411 for option in options:
412 if option == "cramfs":
413 build_sh_spec['cramfs']=True
414 elif option == 'partition':
416 raise PLCInvalidArgument, "option 'partition' is for USB images only"
419 elif option == "serial":
420 build_sh_spec['serial']='default'
421 elif option.find("serial:") == 0:
422 build_sh_spec['serial']=option.replace("serial:","")
423 elif option.find("variant:") == 0:
424 build_sh_spec['variant']=option.replace("variant:","")
425 elif option == "no-hangcheck":
426 build_sh_spec['kargs'].append('hcheck_reboot0')
428 raise PLCInvalidArgument, "unknown option %s"%option
430 # compute nodename according the action
431 if action.find("node-") == 0 or action.find("dummynet-") == 0:
432 nodename = node['hostname']
435 # compute a 8 bytes random number
436 tempbytes = random.sample (xrange(0,256), 8);
437 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
438 nodename = "".join(map(hexa2,tempbytes))
440 # override some global definition, according node_type
441 if node_type == 'dummynet':
442 self.BOOTCDDIR = "/usr/share/dummynet" # the base installation dir
443 self.BOOTCDBUILD = "/usr/share/dummynet/build.sh" # dummynet build script
444 self.WORKDIR = "/var/tmp/DummynetBoxMedium" # temporary working dir
447 (pldistro,arch) = self.get_nodefamily(node)
448 self.nodefamily="%s-%s"%(pldistro,arch)
451 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
452 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
454 filename = self.handle_filename(filename, nodename, suffix, arch)
458 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
459 self.event_objects={'Node': [ node ['node_id'] ]}
461 self.message='GetBootMedium - generic - action=%s'%action
464 if action == 'generic-iso' or action == 'generic-usb':
466 raise PLCInvalidArgument, "Options are not supported for generic images"
467 # this raises an exception if bootcd is missing
468 version = self.bootcd_version()
469 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
472 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
475 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
479 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
481 ### return the generic medium content as-is, just base64 encoded
482 return base64.b64encode(file(generic_path).read())
484 ### config file preview or regenerated
485 if action == 'node-preview' or action == 'node-floppy':
486 renew_key = (action == 'node-floppy')
487 floppy = self.floppy_contents (node,renew_key)
490 file(filename,'w').write(floppy)
492 raise PLCPermissionDenied, "Could not write into %s"%filename
497 ### we're left with node-iso and node-usb
498 # the steps involved in the image creation are:
499 # - create and test the working environment
500 # - generate the configuration file
501 # - build and invoke the build command
502 # - delivery the resulting image file
504 if action == 'node-iso' or action == 'node-usb' \
505 or action == 'dummynet-iso' or action == 'dummynet-usb':
507 ### check we've got required material
508 version = self.bootcd_version()
510 if not os.path.isfile(self.BOOTCDBUILD):
511 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
513 # create the workdir if needed
514 if not os.path.isdir(self.WORKDIR):
516 os.makedirs(self.WORKDIR,0777)
517 os.chmod(self.WORKDIR,0777)
519 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
522 # generate floppy config
523 floppy_text = self.floppy_contents(node,True)
525 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
527 file(floppy_file,"w").write(floppy_text)
529 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
531 self.trash.append(floppy_file)
533 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
534 log_file="%s.log"%node_image
536 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
538 # invoke the image build script
540 ret=os.system(command)
543 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
544 (self.BOOTCDBUILD, command, file(log_file).read())
546 self.trash.append(log_file)
548 if not os.path.isfile (node_image):
549 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
553 ret=os.system('mv "%s" "%s"'%(node_image,filename))
555 self.trash.append(node_image)
557 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
561 result = file(node_image).read()
562 self.trash.append(node_image)
564 return base64.b64encode(result)
569 # we're done here, or we missed something
570 raise PLCAPIError,'Unhandled action %s'%action