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.Debug import log
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
23 # reservable nodes being more recent, we do not support the floppy stuff anymore
42 # Generate 32 random bytes
43 bytes = random.sample(xrange(0, 256), 32)
44 # Base64 encode their string representation
45 key = base64.b64encode("".join(map(chr, bytes)))
46 # Boot Manager cannot handle = in the key
47 # XXX this sounds wrong, as it might prevent proper decoding
48 key = key.replace("=", "")
51 class GetBootMedium(Method):
53 This method is a redesign based on former, supposedly dedicated,
54 AdmGenerateNodeConfFile
56 As compared with its ancestor, this method provides a much more
57 detailed interface, that allows to
58 (*) either just preview the node config file -- in which case
59 the node key is NOT recomputed, and NOT provided in the output
60 (*) or regenerate the node config file for storage on a floppy
61 that is, exactly what the ancestor method used todo,
62 including renewing the node's key
63 (*) or regenerate the config file and bundle it inside an ISO or USB image
64 (*) or just provide the generic ISO or USB boot images
65 in which case of course the node_id_or_hostname parameter is not used
67 action is expected among the following string constants according the
78 Apart for the preview mode, this method generates a new node key for the
79 specified node, effectively invalidating any old boot medium.
80 Note that 'reservable' nodes do not support 'node-floppy',
81 'generic-iso' nor 'generic-usb'.
83 In addition, two return mechanisms are supported.
84 (*) The default behaviour is that the file's content is returned as a
85 base64-encoded string. This is how the ancestor method used to work.
86 To use this method, pass an empty string as the file parameter.
88 (*) Or, for efficiency -- this makes sense only when the API is used
89 by the web pages that run on the same host -- the caller may provide
90 a filename, in which case the resulting file is stored in that location instead.
91 The filename argument can use the following markers, that are expanded
93 - %d : default root dir (some builtin dedicated area under /var/tmp/)
94 Using this is recommended, and enforced for non-admin users
95 - %n : the node's name when this makes sense, or a mktemp-like name when
96 generic media is requested
97 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
98 - %v : the bootcd version string (e.g. 4.0)
100 - %f : the nodefamily
102 With the file-based return mechanism, the method returns the full pathname
105 It is the caller's responsability to remove this file after use.
107 Options: an optional array of keywords.
108 options are not supported for generic images
109 Currently supported are
110 - 'partition' - for USB actions only
112 - 'serial' or 'serial:<console_spec>'
113 console_spec (or 'default') is passed as-is to bootcd/build.sh
114 it is expected to be a colon separated string denoting
115 tty - baudrate - parity - bits
116 e.g. ttyS0:115200:n:8
117 - 'variant:<variantname>'
118 passed to build.sh as -V <variant>
119 variants are used to run a different kernel on the bootCD
120 see kvariant.sh for how to create a variant
121 - 'no-hangcheck' - disable hangcheck
122 - 'systemd-debug' - turn on systemd debug in bootcd
124 Tags: the following tags are taken into account when attached to the node:
125 'serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck', 'systemd-debug'
128 - Non-admins can only generate files for nodes at their sites.
129 - Non-admins, when they provide a filename, *must* specify it in the %d area
132 Whenever needed, the method stores intermediate files in a
133 private area, typically not located under the web server's
134 accessible area, and are cleaned up by the method.
138 roles = ['admin', 'pi', 'tech']
142 Mixed(Node.fields['node_id'],
143 Node.fields['hostname']),
144 Parameter (str, "Action mode, expected value depends of the type of node"),
145 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
146 Parameter ([str], "Options"),
149 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
151 # define globals for regular nodes, override later for other types
152 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
153 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
154 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
155 WORKDIR = "/var/tmp/bootmedium"
157 # uncomment this to preserve temporary area and bootcustom logs
160 ### returns (host, domain) :
161 # 'host' : host part of the hostname
162 # 'domain' : domain part of the hostname
163 def split_hostname (self, node):
164 # Split hostname into host and domain parts
165 parts = node['hostname'].split(".", 1)
167 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
170 # Generate the node (plnode.txt) configuration content.
172 # This function will create the configuration file a node
174 # - a common part, regardless of the 'node_type' tag
175 # - XXX a special part, depending on the 'node_type' tag value.
176 def floppy_contents (self, node, renew_key):
179 if node['peer_id'] is not None:
180 raise PLCInvalidArgument, "Not a local node"
182 # If we are not an admin, make sure that the caller is a
183 # member of the site at which the node is located.
184 if 'admin' not in self.caller['roles']:
185 if node['site_id'] not in self.caller['site_ids']:
186 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
188 # Get interface for this node
190 interfaces = Interfaces(self.api, node['interface_ids'])
191 for interface in interfaces:
192 if interface['is_primary']:
196 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
198 ( host, domain ) = self.split_hostname (node)
200 # renew the key and save it on the database
202 node['key'] = compute_key()
203 node.update_last_download(commit=False)
206 # Generate node configuration file suitable for BootCD
210 file += 'NODE_ID="%d"\n' % node['node_id']
211 file += 'NODE_KEY="%s"\n' % node['key']
212 # not used anywhere, just a note for operations people
213 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
216 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
218 file += 'IP_METHOD="%s"\n' % primary['method']
220 if primary['method'] == 'static':
221 file += 'IP_ADDRESS="%s"\n' % primary['ip']
222 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
223 file += 'IP_NETMASK="%s"\n' % primary['netmask']
224 file += 'IP_NETADDR="%s"\n' % primary['network']
225 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
226 file += 'IP_DNS1="%s"\n' % primary['dns1']
227 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
229 file += 'HOST_NAME="%s"\n' % host
230 file += 'DOMAIN_NAME="%s"\n' % domain
232 # define various interface settings attached to the primary interface
233 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
236 for setting in settings:
237 if setting['category'] is not None:
238 categories.add(setting['category'])
240 for category in categories:
241 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
242 'category':category})
243 if category_settings:
244 file += '### Category : %s\n'%category
245 for setting in category_settings:
246 file += '%s_%s="%s"\n'%(category.upper(),setting['tagname'].upper(),setting['value'])
248 for interface in interfaces:
249 if interface['method'] == 'ipmi':
250 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
252 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
257 # see also GetNodeFlavour that does similar things
258 def get_nodefamily (self, node, auth):
259 pldistro = self.api.config.PLC_FLAVOUR_NODE_PLDISTRO
260 fcdistro = self.api.config.PLC_FLAVOUR_NODE_FCDISTRO
261 arch = self.api.config.PLC_FLAVOUR_NODE_ARCH
263 return (pldistro,fcdistro,arch)
265 node_id=node['node_id']
267 # no support for deployment-based BootCD's, use kvariants instead
268 node_pldistro = GetNodePldistro (self.api,self.caller).call(auth, node_id)
269 if node_pldistro: pldistro = node_pldistro
271 node_fcdistro = GetNodeFcdistro (self.api,self.caller).call(auth, node_id)
272 if node_fcdistro: fcdistro = node_fcdistro
274 node_arch = GetNodeArch (self.api,self.caller).call(auth,node_id)
275 if node_arch: arch = node_arch
277 return (pldistro,fcdistro,arch)
279 def bootcd_version (self):
281 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
283 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
285 def cleantrash (self):
286 for file in self.trash:
288 print >> log, 'DEBUG -- preserving',file
293 # build the filename string
294 # check for permissions and concurrency
295 # returns the filename
296 def handle_filename (self, filename, nodename, suffix, arch):
297 # allow to set filename to None or any other empty value
298 if not filename: filename=''
299 filename = filename.replace ("%d",self.WORKDIR)
300 filename = filename.replace ("%n",nodename)
301 filename = filename.replace ("%s",suffix)
302 filename = filename.replace ("%p",self.api.config.PLC_NAME)
304 try: filename = filename.replace ("%f", self.nodefamily)
306 try: filename = filename.replace ("%a", arch)
308 try: filename = filename.replace ("%v",self.bootcd_version())
311 ### Check filename location
313 if 'admin' not in self.caller['roles']:
314 if ( filename.index(self.WORKDIR) != 0):
315 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
317 ### output should not exist (concurrent runs ..)
318 # numerous reports of issues with this policy
319 # looks like people sometime suspend/cancel their download
320 # and this leads to the old file sitting in there forever
321 # so, if the file is older than 5 minutes, we just trash
323 if os.path.exists(filename) and (time.time()-os.path.getmtime(filename)) >= (grace*60):
325 if os.path.exists(filename):
326 raise PLCInvalidArgument, "Resulting file %s already exists - please try again in %d minutes"%\
329 ### we can now safely create the file,
330 ### either we are admin or under a controlled location
331 filedir=os.path.dirname(filename)
332 # dirname does not return "." for a local filename like its shell counterpart
334 if not os.path.exists(filedir):
336 os.makedirs (filedir,0777)
338 raise PLCPermissionDenied, "Could not create dir %s"%filedir
342 # Build the command line to be executed
343 # according the node type
344 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
348 # regular node, make build's arguments
349 # and build the full command line to be called
350 if node_type in [ 'regular', 'reservable' ]:
353 if "cramfs" in build_sh_spec:
355 if "serial" in build_sh_spec:
356 build_sh_options += " -s %s"%build_sh_spec['serial']
357 if "variant" in build_sh_spec:
358 build_sh_options += " -V %s"%build_sh_spec['variant']
360 for karg in build_sh_spec['kargs']:
361 build_sh_options += ' -k "%s"'%karg
363 log_file="%s.log"%node_image
365 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
373 print >> log, "The build command line is %s" % command
377 def call(self, auth, node_id_or_hostname, action, filename, options = []):
381 ### compute file suffix and type
382 if action.find("-iso") >= 0 :
385 elif action.find("-usb") >= 0:
392 # check for node existence and get node_type
393 nodes = Nodes(self.api, [node_id_or_hostname])
395 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
399 print >> log, "%s requested on node %s. Node type is: %s" \
400 % (action, node['node_id'], node['node_type'])
402 # check the required action against the node type
403 node_type = node['node_type']
404 if action not in allowed_actions[node_type]:
405 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
406 % (action, node_type, "|".join(allowed_actions[node_type]))
408 # handle / canonicalize options
411 raise PLCInvalidArgument, "Options are not supported for node configs"
413 # create a dict for build.sh
414 build_sh_spec={'kargs':[]}
415 # use node tags as defaults
416 # check for node tag equivalents
417 tags = NodeTags(self.api,
418 {'node_id': node['node_id'],
419 'tagname': ['serial', 'cramfs', 'kvariant', 'kargs',
420 'no-hangcheck', 'systemd-debug' ]},
421 ['tagname', 'value'])
424 if tag['tagname'] == 'serial':
425 build_sh_spec['serial'] = tag['value']
426 elif tag['tagname'] == 'cramfs':
427 build_sh_spec['cramfs'] = True
428 elif tag['tagname'] == 'kvariant':
429 build_sh_spec['variant'] = tag['value']
430 elif tag['tagname'] == 'kargs':
431 build_sh_spec['kargs'] += tag['value'].split()
432 elif tag['tagname'] == 'no-hangcheck':
433 build_sh_spec['kargs'].append('hcheck_reboot0')
434 elif tag['tagname'] == 'systemd-debug':
435 build_sh_spec['kargs'].append('systemd.log_level=debug')
436 # then options can override tags
437 for option in options:
438 if option == "cramfs":
439 build_sh_spec['cramfs']=True
440 elif option == 'partition':
442 raise PLCInvalidArgument, "option 'partition' is for USB images only"
445 elif option == "serial":
446 build_sh_spec['serial']='default'
447 elif option.find("serial:") == 0:
448 build_sh_spec['serial']=option.replace("serial:","")
449 elif option.find("variant:") == 0:
450 build_sh_spec['variant']=option.replace("variant:","")
451 elif option == "no-hangcheck":
452 build_sh_spec['kargs'].append('hcheck_reboot0')
453 elif option == "systemd-debug":
454 build_sh_spec['kargs'].append('systemd.log_level=debug')
456 raise PLCInvalidArgument, "unknown option %s"%option
458 # compute nodename according the action
459 if action.find("node-") == 0:
460 nodename = node['hostname']
463 # compute a 8 bytes random number
464 tempbytes = random.sample (xrange(0,256), 8);
465 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
466 nodename = "".join(map(hexa2,tempbytes))
469 (pldistro,fcdistro,arch) = self.get_nodefamily(node,auth)
470 self.nodefamily="%s-%s-%s"%(pldistro,fcdistro,arch)
473 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
474 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
476 filename = self.handle_filename(filename, nodename, suffix, arch)
480 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
481 self.event_objects={'Node': [ node ['node_id'] ]}
483 self.message='GetBootMedium - generic - action=%s'%action
486 if action == 'generic-iso' or action == 'generic-usb':
488 raise PLCInvalidArgument, "Options are not supported for generic images"
489 # this raises an exception if bootcd is missing
490 version = self.bootcd_version()
491 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
494 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
497 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
501 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
503 ### return the generic medium content as-is, just base64 encoded
504 return base64.b64encode(file(generic_path).read())
506 ### config file preview or regenerated
507 if action == 'node-preview' or action == 'node-floppy':
508 renew_key = (action == 'node-floppy')
509 floppy = self.floppy_contents (node,renew_key)
512 file(filename,'w').write(floppy)
514 raise PLCPermissionDenied, "Could not write into %s"%filename
519 ### we're left with node-iso and node-usb
520 # the steps involved in the image creation are:
521 # - create and test the working environment
522 # - generate the configuration file
523 # - build and invoke the build command
524 # - delivery the resulting image file
526 if action == 'node-iso' or action == 'node-usb':
528 ### check we've got required material
529 version = self.bootcd_version()
531 if not os.path.isfile(self.BOOTCDBUILD):
532 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
534 # create the workdir if needed
535 if not os.path.isdir(self.WORKDIR):
537 os.makedirs(self.WORKDIR,0777)
538 os.chmod(self.WORKDIR,0777)
540 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
543 # generate floppy config
544 floppy_text = self.floppy_contents(node,True)
546 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
548 file(floppy_file,"w").write(floppy_text)
550 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
552 self.trash.append(floppy_file)
554 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
555 log_file="%s.log"%node_image
557 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
559 # invoke the image build script
561 ret=os.system(command)
564 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
565 (self.BOOTCDBUILD, command, file(log_file).read())
567 self.trash.append(log_file)
569 if not os.path.isfile (node_image):
570 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
574 ret=os.system('mv "%s" "%s"'%(node_image,filename))
576 self.trash.append(node_image)
578 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
582 result = file(node_image).read()
583 self.trash.append(node_image)
585 print >> log, "GetBootMedium - done with build.sh"
586 encoded_result = base64.b64encode(result)
587 print >> log, "GetBootMedium - done with base64 encoding - lengths=%s - %s"\
588 %(len(result),len(encoded_result))
589 return encoded_result
594 # we're done here, or we missed something
595 raise PLCAPIError,'Unhandled action %s'%action