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>'
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
117 - Non-admins can only generate files for nodes at their sites.
118 - Non-admins, when they provide a filename, *must* specify it in the %d area
121 Whenever needed, the method stores intermediate files in a
122 private area, typically not located under the web server's
123 accessible area, and are cleaned up by the method.
127 roles = ['admin', 'pi', 'tech']
131 Mixed(Node.fields['node_id'],
132 Node.fields['hostname']),
133 Parameter (str, "Action mode, expected value depends of the type of node"),
134 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
135 Parameter ([str], "Options"),
138 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
140 # define globals for regular nodes, override later for other types
141 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
142 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
143 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
144 WORKDIR = "/var/tmp/bootmedium"
146 # uncomment this to preserve temporary area and bootcustom logs
149 ### returns (host, domain) :
150 # 'host' : host part of the hostname
151 # 'domain' : domain part of the hostname
152 def split_hostname (self, node):
153 # Split hostname into host and domain parts
154 parts = node['hostname'].split(".", 1)
156 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
159 # Generate the node (plnode.txt) configuration content.
161 # This function will create the configuration file a node
163 # - a common part, regardless of the 'node_type' tag
164 # - XXX a special part, depending on the 'node_type' tag value.
165 def floppy_contents (self, node, renew_key):
168 if node['peer_id'] is not None:
169 raise PLCInvalidArgument, "Not a local node"
171 # If we are not an admin, make sure that the caller is a
172 # member of the site at which the node is located.
173 if 'admin' not in self.caller['roles']:
174 if node['site_id'] not in self.caller['site_ids']:
175 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
177 # Get interface for this node
179 interfaces = Interfaces(self.api, node['interface_ids'])
180 for interface in interfaces:
181 if interface['is_primary']:
185 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
187 ( host, domain ) = self.split_hostname (node)
189 # renew the key and save it on the database
191 node['key'] = compute_key()
194 # Generate node configuration file suitable for BootCD
198 file += 'NODE_ID="%d"\n' % node['node_id']
199 file += 'NODE_KEY="%s"\n' % node['key']
200 # not used anywhere, just a note for operations people
201 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
204 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
206 file += 'IP_METHOD="%s"\n' % primary['method']
208 if primary['method'] == 'static':
209 file += 'IP_ADDRESS="%s"\n' % primary['ip']
210 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
211 file += 'IP_NETMASK="%s"\n' % primary['netmask']
212 file += 'IP_NETADDR="%s"\n' % primary['network']
213 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
214 file += 'IP_DNS1="%s"\n' % primary['dns1']
215 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
217 file += 'HOST_NAME="%s"\n' % host
218 file += 'DOMAIN_NAME="%s"\n' % domain
220 # define various interface settings attached to the primary interface
221 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
224 for setting in settings:
225 if setting['category'] is not None:
226 categories.add(setting['category'])
228 for category in categories:
229 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
230 'category':category})
231 if category_settings:
232 file += '### Category : %s\n'%category
233 for setting in category_settings:
234 file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
236 for interface in interfaces:
237 if interface['method'] == 'ipmi':
238 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
240 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
245 # see also InstallBootstrapFS in bootmanager that does similar things
246 def get_nodefamily (self, node):
247 # get defaults from the myplc build
249 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
251 (pldistro,arch) = ("planetlab","i386")
253 # with no valid argument, return system-wide defaults
255 return (pldistro,arch)
257 node_id=node['node_id']
259 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
261 tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
264 return (pldistro,arch)
266 def bootcd_version (self):
268 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
270 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
272 def cleantrash (self):
273 for file in self.trash:
275 print 'DEBUG -- preserving',file
280 # build the filename string
281 # check for permissions and concurrency
282 # returns the filename
283 def handle_filename (self, filename, nodename, suffix, arch):
284 # allow to set filename to None or any other empty value
285 if not filename: filename=''
286 filename = filename.replace ("%d",self.WORKDIR)
287 filename = filename.replace ("%n",nodename)
288 filename = filename.replace ("%s",suffix)
289 filename = filename.replace ("%p",self.api.config.PLC_NAME)
291 try: filename = filename.replace ("%f", self.nodefamily)
293 try: filename = filename.replace ("%a", arch)
295 try: filename = filename.replace ("%v",self.bootcd_version())
298 ### Check filename location
300 if 'admin' not in self.caller['roles']:
301 if ( filename.index(self.WORKDIR) != 0):
302 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
304 ### output should not exist (concurrent runs ..)
305 if os.path.exists(filename):
306 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
308 ### we can now safely create the file,
309 ### either we are admin or under a controlled location
310 filedir=os.path.dirname(filename)
311 # dirname does not return "." for a local filename like its shell counterpart
313 if not os.path.exists(filedir):
315 os.makedirs (filedir,0777)
317 raise PLCPermissionDenied, "Could not create dir %s"%filedir
321 # Build the command line to be executed
322 # according the node type
323 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
327 # regular node, make build's arguments
328 # and build the full command line to be called
329 if node_type == 'regular':
332 if "cramfs" in build_sh_spec:
334 if "serial" in build_sh_spec:
335 build_sh_options += " -s %s"%build_sh_spec['serial']
337 for karg in build_sh_spec['kargs']:
338 build_sh_options += ' -k "%s"'%karg
340 log_file="%s.log"%node_image
342 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
349 elif node_type == 'dummynet':
350 # the build script expect the following parameters:
351 # the package base directory
352 # the working directory
353 # the full path of the configuration file
354 # the name of the resulting image file
355 # the type of the generated image
356 # the name of the log file
357 command = "%s -b %s -w %s -f %s -o %s -t %s -l %s" \
358 % (self.BOOTCDBUILD, self.BOOTCDDIR, self.WORKDIR,
359 floppy_file, node_image, type, log_file)
360 command = "touch %s %s; echo 'dummynet build script not yet supported'" \
361 % (log_file, node_image)
364 print "The build command line is %s" % command
368 def call(self, auth, node_id_or_hostname, action, filename, options = []):
372 ### compute file suffix and type
373 if action.find("-iso") >= 0 :
376 elif action.find("-usb") >= 0:
383 # check for node existence and get node_type
384 nodes = Nodes(self.api, [node_id_or_hostname])
386 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
389 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
390 % (action, node['node_id'], node['node_type'])
392 # check the required action against the node type
393 node_type = node['node_type']
394 if action not in allowed_actions[node_type]:
395 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
396 % (action, node_type, "|".join(allowed_actions[node_type]))
398 # handle / canonicalize options
401 raise PLCInvalidArgument, "Options are not supported for node configs"
403 # create a dict for build.sh
404 build_sh_spec={'kargs':[]}
405 for option in options:
406 if option == "cramfs":
407 build_sh_spec['cramfs']=True
408 elif option == 'partition':
410 raise PLCInvalidArgument, "option 'partition' is for USB images only"
413 elif option == "serial":
414 build_sh_spec['serial']='default'
415 elif option.find("serial:") == 0:
416 build_sh_spec['serial']=option.replace("serial:","")
417 elif option == "no-hangcheck":
418 build_sh_spec['kargs'].append('hcheck_reboot0')
420 raise PLCInvalidArgument, "unknown option %s"%option
422 # compute nodename according the action
423 if action.find("node-") == 0 or action.find("dummynet-") == 0:
424 nodename = node['hostname']
427 # compute a 8 bytes random number
428 tempbytes = random.sample (xrange(0,256), 8);
429 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
430 nodename = "".join(map(hexa2,tempbytes))
432 # override some global definition, according node_type
433 if node_type == 'dummynet':
434 self.BOOTCDDIR = "/usr/share/dummynet" # the base installation dir
435 self.BOOTCDBUILD = "/usr/share/dummynet/build.sh" # dummynet build script
436 self.WORKDIR = "/var/tmp/DummynetBoxMedium" # temporary working dir
439 (pldistro,arch) = self.get_nodefamily(node)
440 self.nodefamily="%s-%s"%(pldistro,arch)
443 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
444 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
446 filename = self.handle_filename(filename, nodename, suffix, arch)
450 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
451 self.event_objects={'Node': [ node ['node_id'] ]}
453 self.message='GetBootMedium - generic - action=%s'%action
456 if action == 'generic-iso' or action == 'generic-usb':
458 raise PLCInvalidArgument, "Options are not supported for generic images"
459 # this raises an exception if bootcd is missing
460 version = self.bootcd_version()
461 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
464 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
467 ret=os.system ("cp %s %s"%(generic_path,filename))
471 raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
473 ### return the generic medium content as-is, just base64 encoded
474 return base64.b64encode(file(generic_path).read())
476 ### config file preview or regenerated
477 if action == 'node-preview' or action == 'node-floppy':
478 renew_key = (action == 'node-floppy')
479 floppy = self.floppy_contents (node,renew_key)
482 file(filename,'w').write(floppy)
484 raise PLCPermissionDenied, "Could not write into %s"%filename
489 ### we're left with node-iso and node-usb
490 # the steps involved in the image creation are:
491 # - create and test the working environment
492 # - generate the configuration file
493 # - build and invoke the build command
494 # - delivery the resulting image file
496 if action == 'node-iso' or action == 'node-usb' \
497 or action == 'dummynet-iso' or action == 'dummynet-usb':
499 ### check we've got required material
500 version = self.bootcd_version()
502 if not os.path.isfile(self.BOOTCDBUILD):
503 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
505 # create the workdir if needed
506 if not os.path.isdir(self.WORKDIR):
508 os.makedirs(self.WORKDIR,0777)
509 os.chmod(self.WORKDIR,0777)
511 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
514 # generate floppy config
515 floppy_text = self.floppy_contents(node,True)
517 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
519 file(floppy_file,"w").write(floppy_text)
521 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
523 self.trash.append(floppy_file)
525 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
526 log_file="%s.log"%node_image
528 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
530 # invoke the image build script
532 ret=os.system(command)
535 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
536 (self.BOOTCDBUILD, command, file(log_file).read())
538 self.trash.append(log_file)
540 if not os.path.isfile (node_image):
541 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
545 ret=os.system("mv %s %s"%(node_image,filename))
547 self.trash.append(node_image)
549 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
553 result = file(node_image).read()
554 self.trash.append(node_image)
556 return base64.b64encode(result)
561 # we're done here, or we missed something
562 raise PLCAPIError,'Unhandled action %s'%action