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 for karg in build_sh_spec['kargs']:
342 build_sh_options += ' -k "%s"'%karg
344 log_file="%s.log"%node_image
346 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
354 print "The build command line is %s" % command
358 def call(self, auth, node_id_or_hostname, action, filename, options = []):
362 ### compute file suffix and type
363 if action.find("-iso") >= 0 :
366 elif action.find("-usb") >= 0:
373 # check for node existence and get node_type
374 nodes = Nodes(self.api, [node_id_or_hostname])
376 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
379 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
380 % (action, node['node_id'], node['node_type'])
382 # check the required action against the node type
383 node_type = node['node_type']
384 if action not in allowed_actions[node_type]:
385 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
386 % (action, node_type, "|".join(allowed_actions[node_type]))
388 # handle / canonicalize options
391 raise PLCInvalidArgument, "Options are not supported for node configs"
393 # create a dict for build.sh
394 build_sh_spec={'kargs':[]}
395 # use node tags as defaults
396 # check for node tag equivalents
397 tags = NodeTags(self.api,
398 {'node_id': node['node_id'],
399 'tagname': ['serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck']},
400 ['tagname', 'value'])
403 if tag['tagname'] == 'serial':
404 build_sh_spec['serial'] = tag['value']
405 if tag['tagname'] == 'cramfs':
406 build_sh_spec['cramfs'] = True
407 if tag['tagname'] == 'kvariant':
408 build_sh_spec['variant'] = tag['value']
409 if tag['tagname'] == 'kargs':
410 build_sh_spec['kargs'] += tag['value'].split()
411 if tag['tagname'] == 'no-hangcheck':
412 build_sh_spec['kargs'].append('hcheck_reboot0')
413 # then options can override tags
414 for option in options:
415 if option == "cramfs":
416 build_sh_spec['cramfs']=True
417 elif option == 'partition':
419 raise PLCInvalidArgument, "option 'partition' is for USB images only"
422 elif option == "serial":
423 build_sh_spec['serial']='default'
424 elif option.find("serial:") == 0:
425 build_sh_spec['serial']=option.replace("serial:","")
426 elif option.find("variant:") == 0:
427 build_sh_spec['variant']=option.replace("variant:","")
428 elif option == "no-hangcheck":
429 build_sh_spec['kargs'].append('hcheck_reboot0')
431 raise PLCInvalidArgument, "unknown option %s"%option
433 # compute nodename according the action
434 if action.find("node-") == 0:
435 nodename = node['hostname']
438 # compute a 8 bytes random number
439 tempbytes = random.sample (xrange(0,256), 8);
440 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
441 nodename = "".join(map(hexa2,tempbytes))
444 (pldistro,fcdistro,arch) = self.get_nodefamily(node,auth)
445 self.nodefamily="%s-%s-%s"%(pldistro,fcdistro,arch)
448 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
449 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
451 filename = self.handle_filename(filename, nodename, suffix, arch)
455 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
456 self.event_objects={'Node': [ node ['node_id'] ]}
458 self.message='GetBootMedium - generic - action=%s'%action
461 if action == 'generic-iso' or action == 'generic-usb':
463 raise PLCInvalidArgument, "Options are not supported for generic images"
464 # this raises an exception if bootcd is missing
465 version = self.bootcd_version()
466 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
469 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
472 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
476 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
478 ### return the generic medium content as-is, just base64 encoded
479 return base64.b64encode(file(generic_path).read())
481 ### config file preview or regenerated
482 if action == 'node-preview' or action == 'node-floppy':
483 renew_key = (action == 'node-floppy')
484 floppy = self.floppy_contents (node,renew_key)
487 file(filename,'w').write(floppy)
489 raise PLCPermissionDenied, "Could not write into %s"%filename
494 ### we're left with node-iso and node-usb
495 # the steps involved in the image creation are:
496 # - create and test the working environment
497 # - generate the configuration file
498 # - build and invoke the build command
499 # - delivery the resulting image file
501 if action == 'node-iso' or action == 'node-usb':
503 ### check we've got required material
504 version = self.bootcd_version()
506 if not os.path.isfile(self.BOOTCDBUILD):
507 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
509 # create the workdir if needed
510 if not os.path.isdir(self.WORKDIR):
512 os.makedirs(self.WORKDIR,0777)
513 os.chmod(self.WORKDIR,0777)
515 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
518 # generate floppy config
519 floppy_text = self.floppy_contents(node,True)
521 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
523 file(floppy_file,"w").write(floppy_text)
525 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
527 self.trash.append(floppy_file)
529 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
530 log_file="%s.log"%node_image
532 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
534 # invoke the image build script
536 ret=os.system(command)
539 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
540 (self.BOOTCDBUILD, command, file(log_file).read())
542 self.trash.append(log_file)
544 if not os.path.isfile (node_image):
545 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
549 ret=os.system('mv "%s" "%s"'%(node_image,filename))
551 self.trash.append(node_image)
553 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
557 result = file(node_image).read()
558 self.trash.append(node_image)
560 return base64.b64encode(result)
565 # we're done here, or we missed something
566 raise PLCAPIError,'Unhandled action %s'%action