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 from PLC.Accessors.Accessors_standard import * # import node accessors
21 # could not define this in the class..
22 # create a dict with the allowed actions for each type of node
24 'regular' : [ '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
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 Apart for the preview mode, this method generates a new node key for the
72 specified node, effectively invalidating any old boot medium.
74 In addition, two return mechanisms are supported.
75 (*) The default behaviour is that the file's content is returned as a
76 base64-encoded string. This is how the ancestor method used to work.
77 To use this method, pass an empty string as the file parameter.
79 (*) Or, for efficiency -- this makes sense only when the API is used
80 by the web pages that run on the same host -- the caller may provide
81 a filename, in which case the resulting file is stored in that location instead.
82 The filename argument can use the following markers, that are expanded
84 - %d : default root dir (some builtin dedicated area under /var/tmp/)
85 Using this is recommended, and enforced for non-admin users
86 - %n : the node's name when this makes sense, or a mktemp-like name when
87 generic media is requested
88 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
89 - %v : the bootcd version string (e.g. 4.0)
93 With the file-based return mechanism, the method returns the full pathname
96 It is the caller's responsability to remove this file after use.
98 Options: an optional array of keywords.
99 options are not supported for generic images
100 Currently supported are
101 - 'partition' - for USB actions only
103 - 'serial' or 'serial:<console_spec>'
104 console_spec (or 'default') is passed as-is to bootcd/build.sh
105 it is expected to be a colon separated string denoting
106 tty - baudrate - parity - bits
107 e.g. ttyS0:115200:n:8
108 - 'variant:<variantname>'
109 passed to build.sh as -V <variant>
110 variants are used to run a different kernel on the bootCD
111 see kvariant.sh for how to create a variant
112 - 'no-hangcheck' - disable hangcheck
114 Tags: the following tags are taken into account when attached to the node:
115 'serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck'
118 - Non-admins can only generate files for nodes at their sites.
119 - Non-admins, when they provide a filename, *must* specify it in the %d area
122 Whenever needed, the method stores intermediate files in a
123 private area, typically not located under the web server's
124 accessible area, and are cleaned up by the method.
128 roles = ['admin', 'pi', 'tech']
132 Mixed(Node.fields['node_id'],
133 Node.fields['hostname']),
134 Parameter (str, "Action mode, expected value depends of the type of node"),
135 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
136 Parameter ([str], "Options"),
139 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
141 # define globals for regular nodes, override later for other types
142 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
143 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
144 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
145 WORKDIR = "/var/tmp/bootmedium"
147 # uncomment this to preserve temporary area and bootcustom logs
150 ### returns (host, domain) :
151 # 'host' : host part of the hostname
152 # 'domain' : domain part of the hostname
153 def split_hostname (self, node):
154 # Split hostname into host and domain parts
155 parts = node['hostname'].split(".", 1)
157 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
160 # Generate the node (plnode.txt) configuration content.
162 # This function will create the configuration file a node
164 # - a common part, regardless of the 'node_type' tag
165 # - XXX a special part, depending on the 'node_type' tag value.
166 def floppy_contents (self, node, renew_key):
169 if node['peer_id'] is not None:
170 raise PLCInvalidArgument, "Not a local node"
172 # If we are not an admin, make sure that the caller is a
173 # member of the site at which the node is located.
174 if 'admin' not in self.caller['roles']:
175 if node['site_id'] not in self.caller['site_ids']:
176 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
178 # Get interface for this node
180 interfaces = Interfaces(self.api, node['interface_ids'])
181 for interface in interfaces:
182 if interface['is_primary']:
186 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
188 ( host, domain ) = self.split_hostname (node)
190 # renew the key and save it on the database
192 node['key'] = compute_key()
195 # Generate node configuration file suitable for BootCD
199 file += 'NODE_ID="%d"\n' % node['node_id']
200 file += 'NODE_KEY="%s"\n' % node['key']
201 # not used anywhere, just a note for operations people
202 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
205 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
207 file += 'IP_METHOD="%s"\n' % primary['method']
209 if primary['method'] == 'static':
210 file += 'IP_ADDRESS="%s"\n' % primary['ip']
211 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
212 file += 'IP_NETMASK="%s"\n' % primary['netmask']
213 file += 'IP_NETADDR="%s"\n' % primary['network']
214 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
215 file += 'IP_DNS1="%s"\n' % primary['dns1']
216 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
218 file += 'HOST_NAME="%s"\n' % host
219 file += 'DOMAIN_NAME="%s"\n' % domain
221 # define various interface settings attached to the primary interface
222 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
225 for setting in settings:
226 if setting['category'] is not None:
227 categories.add(setting['category'])
229 for category in categories:
230 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
231 'category':category})
232 if category_settings:
233 file += '### Category : %s\n'%category
234 for setting in category_settings:
235 file += '%s_%s="%s"\n'%(category.upper(),setting['tagname'].upper(),setting['value'])
237 for interface in interfaces:
238 if interface['method'] == 'ipmi':
239 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
241 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
246 # see also GetNodeFlavour that does similar things
247 def get_nodefamily (self, node, auth):
248 pldistro = self.api.config.PLC_FLAVOUR_NODE_PLDISTRO
249 fcdistro = self.api.config.PLC_FLAVOUR_NODE_FCDISTRO
250 arch = self.api.config.PLC_FLAVOUR_NODE_ARCH
252 return (pldistro,fcdistro,arch)
254 node_id=node['node_id']
256 # no support for deployment-based BootCD's, use kvariants instead
257 node_pldistro = GetNodePldistro (self.api).call(auth, node_id)
258 if node_pldistro: pldistro = node_pldistro
260 node_fcdistro = GetNodeFcdistro (self.api).call(auth, node_id)
261 if node_fcdistro: fcdistro = node_fcdistro
263 node_arch = GetNodeArch (self.api).call(auth,node_id)
264 if node_arch: arch = node_arch
266 return (pldistro,fcdistro,arch)
268 def bootcd_version (self):
270 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
272 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
274 def cleantrash (self):
275 for file in self.trash:
277 print 'DEBUG -- preserving',file
282 # build the filename string
283 # check for permissions and concurrency
284 # returns the filename
285 def handle_filename (self, filename, nodename, suffix, arch):
286 # allow to set filename to None or any other empty value
287 if not filename: filename=''
288 filename = filename.replace ("%d",self.WORKDIR)
289 filename = filename.replace ("%n",nodename)
290 filename = filename.replace ("%s",suffix)
291 filename = filename.replace ("%p",self.api.config.PLC_NAME)
293 try: filename = filename.replace ("%f", self.nodefamily)
295 try: filename = filename.replace ("%a", arch)
297 try: filename = filename.replace ("%v",self.bootcd_version())
300 ### Check filename location
302 if 'admin' not in self.caller['roles']:
303 if ( filename.index(self.WORKDIR) != 0):
304 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
306 ### output should not exist (concurrent runs ..)
307 if os.path.exists(filename):
308 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
310 ### we can now safely create the file,
311 ### either we are admin or under a controlled location
312 filedir=os.path.dirname(filename)
313 # dirname does not return "." for a local filename like its shell counterpart
315 if not os.path.exists(filedir):
317 os.makedirs (filedir,0777)
319 raise PLCPermissionDenied, "Could not create dir %s"%filedir
323 # Build the command line to be executed
324 # according the node type
325 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
329 # regular node, make build's arguments
330 # and build the full command line to be called
331 if node_type == 'regular':
334 if "cramfs" in build_sh_spec:
336 if "serial" in build_sh_spec:
337 build_sh_options += " -s %s"%build_sh_spec['serial']
338 if "variant" in build_sh_spec:
339 build_sh_options += " -V %s"%build_sh_spec['variant']
341 if len(build_sh_spec['kargs']) > 0:
342 for karg in build_sh_spec['kargs'][0]:
343 build_sh_options += ' -k "%s"'%karg
345 log_file="%s.log"%node_image
347 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
355 print "The build command line is %s" % command
359 def call(self, auth, node_id_or_hostname, action, filename, options = []):
363 ### compute file suffix and type
364 if action.find("-iso") >= 0 :
367 elif action.find("-usb") >= 0:
374 # check for node existence and get node_type
375 nodes = Nodes(self.api, [node_id_or_hostname])
377 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
380 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
381 % (action, node['node_id'], node['node_type'])
383 # check the required action against the node type
384 node_type = node['node_type']
385 if action not in allowed_actions[node_type]:
386 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
387 % (action, node_type, "|".join(allowed_actions[node_type]))
389 # handle / canonicalize options
392 raise PLCInvalidArgument, "Options are not supported for node configs"
394 # create a dict for build.sh
395 build_sh_spec={'kargs':[]}
396 # use node tags as defaults
397 # check for node tag equivalents
398 tags = NodeTags(self.api,
399 {'node_id': node['node_id'],
400 'tagname': ['serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck']},
401 ['tagname', 'value'])
404 if tag['tagname'] == 'serial':
405 build_sh_spec['serial'] = tag['value']
406 if tag['tagname'] == 'cramfs':
407 build_sh_spec['cramfs'] = True
408 if tag['tagname'] == 'kvariant':
409 build_sh_spec['variant'] = tag['value']
410 if tag['tagname'] == 'kargs':
411 build_sh_spec['kargs'].append(tag['value'].split())
412 if tag['tagname'] == 'no-hangcheck':
413 build_sh_spec['kargs'].append('hcheck_reboot0')
414 # then options can override tags
415 for option in options:
416 if option == "cramfs":
417 build_sh_spec['cramfs']=True
418 elif option == 'partition':
420 raise PLCInvalidArgument, "option 'partition' is for USB images only"
423 elif option == "serial":
424 build_sh_spec['serial']='default'
425 elif option.find("serial:") == 0:
426 build_sh_spec['serial']=option.replace("serial:","")
427 elif option.find("variant:") == 0:
428 build_sh_spec['variant']=option.replace("variant:","")
429 elif option == "no-hangcheck":
430 build_sh_spec['kargs'].append('hcheck_reboot0')
432 raise PLCInvalidArgument, "unknown option %s"%option
434 # compute nodename according the action
435 if action.find("node-") == 0:
436 nodename = node['hostname']
439 # compute a 8 bytes random number
440 tempbytes = random.sample (xrange(0,256), 8);
441 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
442 nodename = "".join(map(hexa2,tempbytes))
445 (pldistro,fcdistro,arch) = self.get_nodefamily(node,auth)
446 self.nodefamily="%s-%s-%s"%(pldistro,fcdistro,arch)
449 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
450 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
452 filename = self.handle_filename(filename, nodename, suffix, arch)
456 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
457 self.event_objects={'Node': [ node ['node_id'] ]}
459 self.message='GetBootMedium - generic - action=%s'%action
462 if action == 'generic-iso' or action == 'generic-usb':
464 raise PLCInvalidArgument, "Options are not supported for generic images"
465 # this raises an exception if bootcd is missing
466 version = self.bootcd_version()
467 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
470 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
473 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
477 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
479 ### return the generic medium content as-is, just base64 encoded
480 return base64.b64encode(file(generic_path).read())
482 ### config file preview or regenerated
483 if action == 'node-preview' or action == 'node-floppy':
484 renew_key = (action == 'node-floppy')
485 floppy = self.floppy_contents (node,renew_key)
488 file(filename,'w').write(floppy)
490 raise PLCPermissionDenied, "Could not write into %s"%filename
495 ### we're left with node-iso and node-usb
496 # the steps involved in the image creation are:
497 # - create and test the working environment
498 # - generate the configuration file
499 # - build and invoke the build command
500 # - delivery the resulting image file
502 if action == 'node-iso' or action == 'node-usb':
504 ### check we've got required material
505 version = self.bootcd_version()
507 if not os.path.isfile(self.BOOTCDBUILD):
508 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
510 # create the workdir if needed
511 if not os.path.isdir(self.WORKDIR):
513 os.makedirs(self.WORKDIR,0777)
514 os.chmod(self.WORKDIR,0777)
516 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
519 # generate floppy config
520 floppy_text = self.floppy_contents(node,True)
522 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
524 file(floppy_file,"w").write(floppy_text)
526 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
528 self.trash.append(floppy_file)
530 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
531 log_file="%s.log"%node_image
533 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
535 # invoke the image build script
537 ret=os.system(command)
540 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
541 (self.BOOTCDBUILD, command, file(log_file).read())
543 self.trash.append(log_file)
545 if not os.path.isfile (node_image):
546 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
550 ret=os.system('mv "%s" "%s"'%(node_image,filename))
552 self.trash.append(node_image)
554 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
558 result = file(node_image).read()
559 self.trash.append(node_image)
561 return base64.b64encode(result)
566 # we're done here, or we missed something
567 raise PLCAPIError,'Unhandled action %s'%action