7 from PLC.Faults import *
8 from PLC.Method import Method
9 from PLC.Parameter import Parameter, Mixed
10 from PLC.Auth import Auth
12 from PLC.Nodes import Node, Nodes
13 from PLC.Interfaces import Interface, Interfaces
14 from PLC.InterfaceTags import InterfaceTag, InterfaceTags
15 from PLC.NodeTags import NodeTag, NodeTags
17 from PLC.Accessors.Accessors_standard import * # import node accessors
19 # could not define this in the class..
20 # create a dict with the allowed actions for each type of node
21 # reservable nodes being more recent, we do not support the floppy stuff anymore
40 # Generate 32 random bytes
41 bytes = random.sample(xrange(0, 256), 32)
42 # Base64 encode their string representation
43 key = base64.b64encode("".join(map(chr, bytes)))
44 # Boot Manager cannot handle = in the key
45 # XXX this sounds wrong, as it might prevent proper decoding
46 key = key.replace("=", "")
49 class GetBootMedium(Method):
51 This method is a redesign based on former, supposedly dedicated,
52 AdmGenerateNodeConfFile
54 As compared with its ancestor, this method provides a much more
55 detailed interface, that allows to
56 (*) either just preview the node config file -- in which case
57 the node key is NOT recomputed, and NOT provided in the output
58 (*) or regenerate the node config file for storage on a floppy
59 that is, exactly what the ancestor method used todo,
60 including renewing the node's key
61 (*) or regenerate the config file and bundle it inside an ISO or USB image
62 (*) or just provide the generic ISO or USB boot images
63 in which case of course the node_id_or_hostname parameter is not used
65 action is expected among the following string constants according the
76 Apart for the preview mode, this method generates a new node key for the
77 specified node, effectively invalidating any old boot medium.
78 Note that 'reservable' nodes do not support 'node-floppy',
79 'generic-iso' nor 'generic-usb'.
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 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
119 - 'no-hangcheck' - disable hangcheck
121 Tags: the following tags are taken into account when attached to the node:
122 'serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck'
125 - Non-admins can only generate files for nodes at their sites.
126 - Non-admins, when they provide a filename, *must* specify it in the %d area
129 Whenever needed, the method stores intermediate files in a
130 private area, typically not located under the web server's
131 accessible area, and are cleaned up by the method.
135 roles = ['admin', 'pi', 'tech']
139 Mixed(Node.fields['node_id'],
140 Node.fields['hostname']),
141 Parameter (str, "Action mode, expected value depends of the type of node"),
142 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
143 Parameter ([str], "Options"),
146 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
148 # define globals for regular nodes, override later for other types
149 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
150 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
151 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
152 WORKDIR = "/var/tmp/bootmedium"
154 # uncomment this to preserve temporary area and bootcustom logs
157 ### returns (host, domain) :
158 # 'host' : host part of the hostname
159 # 'domain' : domain part of the hostname
160 def split_hostname (self, node):
161 # Split hostname into host and domain parts
162 parts = node['hostname'].split(".", 1)
164 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
167 # Generate the node (plnode.txt) configuration content.
169 # This function will create the configuration file a node
171 # - a common part, regardless of the 'node_type' tag
172 # - XXX a special part, depending on the 'node_type' tag value.
173 def floppy_contents (self, node, renew_key):
176 if node['peer_id'] is not None:
177 raise PLCInvalidArgument, "Not a local node"
179 # If we are not an admin, make sure that the caller is a
180 # member of the site at which the node is located.
181 if 'admin' not in self.caller['roles']:
182 if node['site_id'] not in self.caller['site_ids']:
183 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
185 # Get interface for this node
187 interfaces = Interfaces(self.api, node['interface_ids'])
188 for interface in interfaces:
189 if interface['is_primary']:
193 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
195 ( host, domain ) = self.split_hostname (node)
197 # renew the key and save it on the database
199 node['key'] = compute_key()
200 node.update_last_download(commit=False)
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 GetNodeFlavour that does similar things
255 def get_nodefamily (self, node, auth):
256 pldistro = self.api.config.PLC_FLAVOUR_NODE_PLDISTRO
257 fcdistro = self.api.config.PLC_FLAVOUR_NODE_FCDISTRO
258 arch = self.api.config.PLC_FLAVOUR_NODE_ARCH
260 return (pldistro,fcdistro,arch)
262 node_id=node['node_id']
264 # no support for deployment-based BootCD's, use kvariants instead
265 node_pldistro = GetNodePldistro (self.api,self.caller).call(auth, node_id)
266 if node_pldistro: pldistro = node_pldistro
268 node_fcdistro = GetNodeFcdistro (self.api,self.caller).call(auth, node_id)
269 if node_fcdistro: fcdistro = node_fcdistro
271 node_arch = GetNodeArch (self.api,self.caller).call(auth,node_id)
272 if node_arch: arch = node_arch
274 return (pldistro,fcdistro,arch)
276 # xxx Thierry : 5.2.1 build/version.txt for some reason is empty, that's why
277 # the weird name with downloaded image filenames
278 def bootcd_version (self):
280 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
282 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
284 def cleantrash (self):
285 for file in self.trash:
287 print 'DEBUG -- preserving',file
292 # build the filename string
293 # check for permissions and concurrency
294 # returns the filename
295 def handle_filename (self, filename, nodename, suffix, arch):
296 # allow to set filename to None or any other empty value
297 if not filename: filename=''
298 filename = filename.replace ("%d",self.WORKDIR)
299 filename = filename.replace ("%n",nodename)
300 filename = filename.replace ("%s",suffix)
301 filename = filename.replace ("%p",self.api.config.PLC_NAME)
303 try: filename = filename.replace ("%f", self.nodefamily)
305 try: filename = filename.replace ("%a", arch)
307 try: filename = filename.replace ("%v",self.bootcd_version())
310 ### Check filename location
312 if 'admin' not in self.caller['roles']:
313 if ( filename.index(self.WORKDIR) != 0):
314 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
316 ### output should not exist (concurrent runs ..)
317 # numerous reports of issues with this policy
318 # looks like people sometime suspend/cancel their download
319 # and this leads to the old file sitting in there forever
320 # so, if the file is older than 5 minutes, we just trash
322 if os.path.exists(filename) and (time.time()-os.path.getmtime(filename)) >= (grace*60):
324 if os.path.exists(filename):
325 raise PLCInvalidArgument, "Resulting file %s already exists - please try again in %d minutes"%\
328 ### we can now safely create the file,
329 ### either we are admin or under a controlled location
330 filedir=os.path.dirname(filename)
331 # dirname does not return "." for a local filename like its shell counterpart
333 if not os.path.exists(filedir):
335 os.makedirs (filedir,0777)
337 raise PLCPermissionDenied, "Could not create dir %s"%filedir
341 # Build the command line to be executed
342 # according the node type
343 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
347 # regular node, make build's arguments
348 # and build the full command line to be called
349 if node_type in [ 'regular', 'reservable' ]:
352 if "cramfs" in build_sh_spec:
354 if "serial" in build_sh_spec:
355 build_sh_options += " -s %s"%build_sh_spec['serial']
356 if "variant" in build_sh_spec:
357 build_sh_options += " -V %s"%build_sh_spec['variant']
359 for karg in build_sh_spec['kargs']:
360 build_sh_options += ' -k "%s"'%karg
362 log_file="%s.log"%node_image
364 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
372 print "The build command line is %s" % command
376 def call(self, auth, node_id_or_hostname, action, filename, options = []):
380 ### compute file suffix and type
381 if action.find("-iso") >= 0 :
384 elif action.find("-usb") >= 0:
391 # check for node existence and get node_type
392 nodes = Nodes(self.api, [node_id_or_hostname])
394 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
397 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
398 % (action, node['node_id'], node['node_type'])
400 # check the required action against the node type
401 node_type = node['node_type']
402 if action not in allowed_actions[node_type]:
403 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
404 % (action, node_type, "|".join(allowed_actions[node_type]))
406 # handle / canonicalize options
409 raise PLCInvalidArgument, "Options are not supported for node configs"
411 # create a dict for build.sh
412 build_sh_spec={'kargs':[]}
413 # use node tags as defaults
414 # check for node tag equivalents
415 tags = NodeTags(self.api,
416 {'node_id': node['node_id'],
417 'tagname': ['serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck']},
418 ['tagname', 'value'])
421 if tag['tagname'] == 'serial':
422 build_sh_spec['serial'] = tag['value']
423 if tag['tagname'] == 'cramfs':
424 build_sh_spec['cramfs'] = True
425 if tag['tagname'] == 'kvariant':
426 build_sh_spec['variant'] = tag['value']
427 if tag['tagname'] == 'kargs':
428 build_sh_spec['kargs'] += tag['value'].split()
429 if tag['tagname'] == 'no-hangcheck':
430 build_sh_spec['kargs'].append('hcheck_reboot0')
431 # then options can override tags
432 for option in options:
433 if option == "cramfs":
434 build_sh_spec['cramfs']=True
435 elif option == 'partition':
437 raise PLCInvalidArgument, "option 'partition' is for USB images only"
440 elif option == "serial":
441 build_sh_spec['serial']='default'
442 elif option.find("serial:") == 0:
443 build_sh_spec['serial']=option.replace("serial:","")
444 elif option.find("variant:") == 0:
445 build_sh_spec['variant']=option.replace("variant:","")
446 elif option == "no-hangcheck":
447 build_sh_spec['kargs'].append('hcheck_reboot0')
449 raise PLCInvalidArgument, "unknown option %s"%option
451 # compute nodename according the action
452 if action.find("node-") == 0:
453 nodename = node['hostname']
456 # compute a 8 bytes random number
457 tempbytes = random.sample (xrange(0,256), 8);
458 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
459 nodename = "".join(map(hexa2,tempbytes))
462 (pldistro,fcdistro,arch) = self.get_nodefamily(node,auth)
463 self.nodefamily="%s-%s-%s"%(pldistro,fcdistro,arch)
466 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
467 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
469 filename = self.handle_filename(filename, nodename, suffix, arch)
473 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
474 self.event_objects={'Node': [ node ['node_id'] ]}
476 self.message='GetBootMedium - generic - action=%s'%action
479 if action == 'generic-iso' or action == 'generic-usb':
481 raise PLCInvalidArgument, "Options are not supported for generic images"
482 # this raises an exception if bootcd is missing
483 version = self.bootcd_version()
484 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
487 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
490 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
494 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
496 ### return the generic medium content as-is, just base64 encoded
497 return base64.b64encode(file(generic_path).read())
499 ### config file preview or regenerated
500 if action == 'node-preview' or action == 'node-floppy':
501 renew_key = (action == 'node-floppy')
502 floppy = self.floppy_contents (node,renew_key)
505 file(filename,'w').write(floppy)
507 raise PLCPermissionDenied, "Could not write into %s"%filename
512 ### we're left with node-iso and node-usb
513 # the steps involved in the image creation are:
514 # - create and test the working environment
515 # - generate the configuration file
516 # - build and invoke the build command
517 # - delivery the resulting image file
519 if action == 'node-iso' or action == 'node-usb':
521 ### check we've got required material
522 version = self.bootcd_version()
524 if not os.path.isfile(self.BOOTCDBUILD):
525 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
527 # create the workdir if needed
528 if not os.path.isdir(self.WORKDIR):
530 os.makedirs(self.WORKDIR,0777)
531 os.chmod(self.WORKDIR,0777)
533 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
536 # generate floppy config
537 floppy_text = self.floppy_contents(node,True)
539 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
541 file(floppy_file,"w").write(floppy_text)
543 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
545 self.trash.append(floppy_file)
547 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
548 log_file="%s.log"%node_image
550 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
552 # invoke the image build script
554 ret=os.system(command)
557 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
558 (self.BOOTCDBUILD, command, file(log_file).read())
560 self.trash.append(log_file)
562 if not os.path.isfile (node_image):
563 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
567 ret=os.system('mv "%s" "%s"'%(node_image,filename))
569 self.trash.append(node_image)
571 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
575 result = file(node_image).read()
576 self.trash.append(node_image)
578 return base64.b64encode(result)
583 # we're done here, or we missed something
584 raise PLCAPIError,'Unhandled action %s'%action