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',
33 # Generate 32 random bytes
34 bytes = random.sample(xrange(0, 256), 32)
35 # Base64 encode their string representation
36 key = base64.b64encode("".join(map(chr, bytes)))
37 # Boot Manager cannot handle = in the key
38 # XXX this sounds wrong, as it might prevent proper decoding
39 key = key.replace("=", "")
42 class GetBootMedium(Method):
44 This method is a redesign based on former, supposedly dedicated,
45 AdmGenerateNodeConfFile
47 As compared with its ancestor, this method provides a much more
48 detailed interface, that allows to
49 (*) either just preview the node config file -- in which case
50 the node key is NOT recomputed, and NOT provided in the output
51 (*) or regenerate the node config file for storage on a floppy
52 that is, exactly what the ancestor method used todo,
53 including renewing the node's key
54 (*) or regenerate the config file and bundle it inside an ISO or USB image
55 (*) or just provide the generic ISO or USB boot images
56 in which case of course the node_id_or_hostname parameter is not used
58 action is expected among the following string constants according the
69 Apart for the preview mode, this method generates a new node key for the
70 specified node, effectively invalidating any old boot medium.
72 In addition, two return mechanisms are supported.
73 (*) The default behaviour is that the file's content is returned as a
74 base64-encoded string. This is how the ancestor method used to work.
75 To use this method, pass an empty string as the file parameter.
77 (*) Or, for efficiency -- this makes sense only when the API is used
78 by the web pages that run on the same host -- the caller may provide
79 a filename, in which case the resulting file is stored in that location instead.
80 The filename argument can use the following markers, that are expanded
82 - %d : default root dir (some builtin dedicated area under /var/tmp/)
83 Using this is recommended, and enforced for non-admin users
84 - %n : the node's name when this makes sense, or a mktemp-like name when
85 generic media is requested
86 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
87 - %v : the bootcd version string (e.g. 4.0)
91 With the file-based return mechanism, the method returns the full pathname
94 It is the caller's responsability to remove this file after use.
96 Options: an optional array of keywords.
97 options are not supported for generic images
98 Currently supported are
99 - 'partition' - for USB actions only
101 - 'serial' or 'serial:<console_spec>'
102 console_spec (or 'default') is passed as-is to bootcd/build.sh
103 it is expected to be a colon separated string denoting
104 tty - baudrate - parity - bits
105 e.g. ttyS0:115200:n:8
106 - 'variant:<variantname>'
107 passed to build.sh as -V <variant>
108 variants are used to run a different kernel on the bootCD
109 see kvariant.sh for how to create a variant
110 - 'no-hangcheck' - disable hangcheck
112 Tags: the following tags are taken into account when attached to the node:
113 'serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck'
116 - Non-admins can only generate files for nodes at their sites.
117 - Non-admins, when they provide a filename, *must* specify it in the %d area
120 Whenever needed, the method stores intermediate files in a
121 private area, typically not located under the web server's
122 accessible area, and are cleaned up by the method.
126 roles = ['admin', 'pi', 'tech']
130 Mixed(Node.fields['node_id'],
131 Node.fields['hostname']),
132 Parameter (str, "Action mode, expected value depends of the type of node"),
133 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
134 Parameter ([str], "Options"),
137 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
139 # define globals for regular nodes, override later for other types
140 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
141 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
142 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
143 WORKDIR = "/var/tmp/bootmedium"
145 # uncomment this to preserve temporary area and bootcustom logs
148 ### returns (host, domain) :
149 # 'host' : host part of the hostname
150 # 'domain' : domain part of the hostname
151 def split_hostname (self, node):
152 # Split hostname into host and domain parts
153 parts = node['hostname'].split(".", 1)
155 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
158 # Generate the node (plnode.txt) configuration content.
160 # This function will create the configuration file a node
162 # - a common part, regardless of the 'node_type' tag
163 # - XXX a special part, depending on the 'node_type' tag value.
164 def floppy_contents (self, node, renew_key):
167 if node['peer_id'] is not None:
168 raise PLCInvalidArgument, "Not a local node"
170 # If we are not an admin, make sure that the caller is a
171 # member of the site at which the node is located.
172 if 'admin' not in self.caller['roles']:
173 if node['site_id'] not in self.caller['site_ids']:
174 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
176 # Get interface for this node
178 interfaces = Interfaces(self.api, node['interface_ids'])
179 for interface in interfaces:
180 if interface['is_primary']:
184 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
186 ( host, domain ) = self.split_hostname (node)
188 # renew the key and save it on the database
190 node['key'] = compute_key()
193 # Generate node configuration file suitable for BootCD
197 file += 'NODE_ID="%d"\n' % node['node_id']
198 file += 'NODE_KEY="%s"\n' % node['key']
199 # not used anywhere, just a note for operations people
200 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
203 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
205 file += 'IP_METHOD="%s"\n' % primary['method']
207 if primary['method'] == 'static':
208 file += 'IP_ADDRESS="%s"\n' % primary['ip']
209 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
210 file += 'IP_NETMASK="%s"\n' % primary['netmask']
211 file += 'IP_NETADDR="%s"\n' % primary['network']
212 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
213 file += 'IP_DNS1="%s"\n' % primary['dns1']
214 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
216 file += 'HOST_NAME="%s"\n' % host
217 file += 'DOMAIN_NAME="%s"\n' % domain
219 # define various interface settings attached to the primary interface
220 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
223 for setting in settings:
224 if setting['category'] is not None:
225 categories.add(setting['category'])
227 for category in categories:
228 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
229 'category':category})
230 if category_settings:
231 file += '### Category : %s\n'%category
232 for setting in category_settings:
233 file += '%s_%s="%s"\n'%(category.upper(),setting['tagname'].upper(),setting['value'])
235 for interface in interfaces:
236 if interface['method'] == 'ipmi':
237 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
239 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
244 # see also InstallBootstrapFS in bootmanager that does similar things
245 def get_nodefamily (self, node):
246 # get defaults from the myplc build
248 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
250 (pldistro,arch) = ("planetlab","i386")
252 # with no valid argument, return system-wide defaults
254 return (pldistro,arch)
256 node_id=node['node_id']
258 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
260 tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
263 return (pldistro,arch)
265 def bootcd_version (self):
267 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
269 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
271 def cleantrash (self):
272 for file in self.trash:
274 print 'DEBUG -- preserving',file
279 # build the filename string
280 # check for permissions and concurrency
281 # returns the filename
282 def handle_filename (self, filename, nodename, suffix, arch):
283 # allow to set filename to None or any other empty value
284 if not filename: filename=''
285 filename = filename.replace ("%d",self.WORKDIR)
286 filename = filename.replace ("%n",nodename)
287 filename = filename.replace ("%s",suffix)
288 filename = filename.replace ("%p",self.api.config.PLC_NAME)
290 try: filename = filename.replace ("%f", self.nodefamily)
292 try: filename = filename.replace ("%a", arch)
294 try: filename = filename.replace ("%v",self.bootcd_version())
297 ### Check filename location
299 if 'admin' not in self.caller['roles']:
300 if ( filename.index(self.WORKDIR) != 0):
301 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
303 ### output should not exist (concurrent runs ..)
304 if os.path.exists(filename):
305 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
307 ### we can now safely create the file,
308 ### either we are admin or under a controlled location
309 filedir=os.path.dirname(filename)
310 # dirname does not return "." for a local filename like its shell counterpart
312 if not os.path.exists(filedir):
314 os.makedirs (filedir,0777)
316 raise PLCPermissionDenied, "Could not create dir %s"%filedir
320 # Build the command line to be executed
321 # according the node type
322 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
326 # regular node, make build's arguments
327 # and build the full command line to be called
328 if node_type == 'regular':
331 if "cramfs" in build_sh_spec:
333 if "serial" in build_sh_spec:
334 build_sh_options += " -s %s"%build_sh_spec['serial']
335 if "variant" in build_sh_spec:
336 build_sh_options += " -V %s"%build_sh_spec['variant']
338 for karg in build_sh_spec['kargs']:
339 build_sh_options += ' -k "%s"'%karg
341 log_file="%s.log"%node_image
343 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
351 print "The build command line is %s" % command
355 def call(self, auth, node_id_or_hostname, action, filename, options = []):
359 ### compute file suffix and type
360 if action.find("-iso") >= 0 :
363 elif action.find("-usb") >= 0:
370 # check for node existence and get node_type
371 nodes = Nodes(self.api, [node_id_or_hostname])
373 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
376 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
377 % (action, node['node_id'], node['node_type'])
379 # check the required action against the node type
380 node_type = node['node_type']
381 if action not in allowed_actions[node_type]:
382 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
383 % (action, node_type, "|".join(allowed_actions[node_type]))
385 # handle / canonicalize options
388 raise PLCInvalidArgument, "Options are not supported for node configs"
390 # create a dict for build.sh
391 build_sh_spec={'kargs':[]}
392 # use node tags as defaults
393 # check for node tag equivalents
394 tags = NodeTags(self.api,
395 {'node_id': node['node_id'],
396 'tagname': ['serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck']},
397 ['tagname', 'value'])
400 if tag['tagname'] == 'serial':
401 build_sh_spec['serial'] = tag['value']
402 if tag['tagname'] == 'cramfs':
403 build_sh_spec['cramfs'] = True
404 if tag['tagname'] == 'kvariant':
405 build_sh_spec['variant'] = tag['value']
406 if tag['tagname'] == 'kargs':
407 build_sh_spec['kargs'].append(tag['value'].split())
408 if tag['tagname'] == 'no-hangcheck':
409 build_sh_spec['kargs'].append('hcheck_reboot0')
410 # then options can override tags
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:
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))
441 (pldistro,arch) = self.get_nodefamily(node)
442 self.nodefamily="%s-%s"%(pldistro,arch)
445 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
446 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
448 filename = self.handle_filename(filename, nodename, suffix, arch)
452 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
453 self.event_objects={'Node': [ node ['node_id'] ]}
455 self.message='GetBootMedium - generic - action=%s'%action
458 if action == 'generic-iso' or action == 'generic-usb':
460 raise PLCInvalidArgument, "Options are not supported for generic images"
461 # this raises an exception if bootcd is missing
462 version = self.bootcd_version()
463 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
466 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
469 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
473 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
475 ### return the generic medium content as-is, just base64 encoded
476 return base64.b64encode(file(generic_path).read())
478 ### config file preview or regenerated
479 if action == 'node-preview' or action == 'node-floppy':
480 renew_key = (action == 'node-floppy')
481 floppy = self.floppy_contents (node,renew_key)
484 file(filename,'w').write(floppy)
486 raise PLCPermissionDenied, "Could not write into %s"%filename
491 ### we're left with node-iso and node-usb
492 # the steps involved in the image creation are:
493 # - create and test the working environment
494 # - generate the configuration file
495 # - build and invoke the build command
496 # - delivery the resulting image file
498 if action == 'node-iso' or action == 'node-usb':
500 ### check we've got required material
501 version = self.bootcd_version()
503 if not os.path.isfile(self.BOOTCDBUILD):
504 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
506 # create the workdir if needed
507 if not os.path.isdir(self.WORKDIR):
509 os.makedirs(self.WORKDIR,0777)
510 os.chmod(self.WORKDIR,0777)
512 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
515 # generate floppy config
516 floppy_text = self.floppy_contents(node,True)
518 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
520 file(floppy_file,"w").write(floppy_text)
522 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
524 self.trash.append(floppy_file)
526 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
527 log_file="%s.log"%node_image
529 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
531 # invoke the image build script
533 ret=os.system(command)
536 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
537 (self.BOOTCDBUILD, command, file(log_file).read())
539 self.trash.append(log_file)
541 if not os.path.isfile (node_image):
542 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
546 ret=os.system('mv "%s" "%s"'%(node_image,filename))
548 self.trash.append(node_image)
550 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
554 result = file(node_image).read()
555 self.trash.append(node_image)
557 return base64.b64encode(result)
562 # we're done here, or we missed something
563 raise PLCAPIError,'Unhandled action %s'%action