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.IpAddresses import IpAddress, IpAddresses
13 from PLC.Routes import Route, Routes
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
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
123 Tags: the following tags are taken into account when attached to the node:
124 'serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck'
127 - Non-admins can only generate files for nodes at their sites.
128 - Non-admins, when they provide a filename, *must* specify it in the %d area
131 Whenever needed, the method stores intermediate files in a
132 private area, typically not located under the web server's
133 accessible area, and are cleaned up by the method.
137 roles = ['admin', 'pi', 'tech']
141 Mixed(Node.fields['node_id'],
142 Node.fields['hostname']),
143 Parameter (str, "Action mode, expected value depends of the type of node"),
144 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
145 Parameter ([str], "Options"),
148 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
150 # define globals for regular nodes, override later for other types
151 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
152 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
153 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
154 WORKDIR = "/var/tmp/bootmedium"
156 # uncomment this to preserve temporary area and bootcustom logs
159 ### returns (host, domain) :
160 # 'host' : host part of the hostname
161 # 'domain' : domain part of the hostname
162 def split_hostname (self, node):
163 # Split hostname into host and domain parts
164 parts = node['hostname'].split(".", 1)
166 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
169 # Generate the node (plnode.txt) configuration content.
171 # This function will create the configuration file a node
173 # - a common part, regardless of the 'node_type' tag
174 # - XXX a special part, depending on the 'node_type' tag value.
175 def floppy_contents (self, node, renew_key):
178 if node['peer_id'] is not None:
179 raise PLCInvalidArgument, "Not a local node"
181 # If we are not an admin, make sure that the caller is a
182 # member of the site at which the node is located.
183 if 'admin' not in self.caller['roles']:
184 if node['site_id'] not in self.caller['site_ids']:
185 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
187 # Get interface for this node
189 interfaces = Interfaces(self.api, node['interface_ids'])
190 for interface in interfaces:
191 if interface['is_primary']:
195 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
197 ( host, domain ) = self.split_hostname (node)
199 # renew the key and save it on the database
201 node['key'] = compute_key()
202 node.update_last_download(commit=False)
205 # Generate node configuration file suitable for BootCD
209 file += 'NODE_ID="%d"\n' % node['node_id']
210 file += 'NODE_KEY="%s"\n' % node['key']
211 # not used anywhere, just a note for operations people
212 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
215 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
217 file += 'IP_METHOD="%s"\n' % primary['method']
219 if primary['method'] == 'static':
220 # FIXME: We currently get the first ip address
221 # only. plnode.txt depends on interface having a single ip
222 # address. We assume that the first ip address in the
223 # primary interface will be the primary ip address. This
224 # assumumption is probably not the right way to go... - baris
225 routes = Routes(self.api, {'node_id': primary['node_id']})
226 default_route = [r for r in routes if r['subnet'] == u'0.0.0.0/0']
229 gateway = default_route[0]['next_hop']
231 node = Nodes(self.api, primary['node_id'])[0]
232 dns = node['dns'].split(',')
240 ip_addresses = IpAddresses(self.api, primary['ip_address_ids'])
242 primary_ip_address = ip_addresses[0]
243 file += 'IP_ADDRESS="%s"\n' % primary_ip_address['ip_addr']
244 file += 'IP_GATEWAY="%s"\n' % gateway
245 file += 'IP_NETMASK="%s"\n' % primary_ip_address['netmask']
246 file += 'IP_NETADDR=""\n'
247 file += 'IP_BROADCASTADDR=""\n'
248 file += 'IP_DNS1="%s"\n' % (dns1 or "")
249 file += 'IP_DNS2="%s"\n' % (dns2 or "")
251 file += 'HOST_NAME="%s"\n' % host
252 file += 'DOMAIN_NAME="%s"\n' % domain
254 # define various interface settings attached to the primary interface
255 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
258 for setting in settings:
259 if setting['category'] is not None:
260 categories.add(setting['category'])
262 for category in categories:
263 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
264 'category':category})
265 if category_settings:
266 file += '### Category : %s\n'%category
267 for setting in category_settings:
268 file += '%s_%s="%s"\n'%(category.upper(),setting['tagname'].upper(),setting['value'])
270 for interface in interfaces:
271 if interface['method'] == 'ipmi':
272 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
274 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
279 # see also GetNodeFlavour that does similar things
280 def get_nodefamily (self, node, auth):
281 pldistro = self.api.config.PLC_FLAVOUR_NODE_PLDISTRO
282 fcdistro = self.api.config.PLC_FLAVOUR_NODE_FCDISTRO
283 arch = self.api.config.PLC_FLAVOUR_NODE_ARCH
285 return (pldistro,fcdistro,arch)
287 node_id=node['node_id']
289 # no support for deployment-based BootCD's, use kvariants instead
290 node_pldistro = GetNodePldistro (self.api,self.caller).call(auth, node_id)
291 if node_pldistro: pldistro = node_pldistro
293 node_fcdistro = GetNodeFcdistro (self.api,self.caller).call(auth, node_id)
294 if node_fcdistro: fcdistro = node_fcdistro
296 node_arch = GetNodeArch (self.api,self.caller).call(auth,node_id)
297 if node_arch: arch = node_arch
299 return (pldistro,fcdistro,arch)
301 def bootcd_version (self):
303 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
305 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
307 def cleantrash (self):
308 for file in self.trash:
310 print 'DEBUG -- preserving',file
315 # build the filename string
316 # check for permissions and concurrency
317 # returns the filename
318 def handle_filename (self, filename, nodename, suffix, arch):
319 # allow to set filename to None or any other empty value
320 if not filename: filename=''
321 filename = filename.replace ("%d",self.WORKDIR)
322 filename = filename.replace ("%n",nodename)
323 filename = filename.replace ("%s",suffix)
324 filename = filename.replace ("%p",self.api.config.PLC_NAME)
326 try: filename = filename.replace ("%f", self.nodefamily)
328 try: filename = filename.replace ("%a", arch)
330 try: filename = filename.replace ("%v",self.bootcd_version())
333 ### Check filename location
335 if 'admin' not in self.caller['roles']:
336 if ( filename.index(self.WORKDIR) != 0):
337 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
339 ### output should not exist (concurrent runs ..)
340 # numerous reports of issues with this policy
341 # looks like people sometime suspend/cancel their download
342 # and this leads to the old file sitting in there forever
343 # so, if the file is older than 5 minutes, we just trash
345 if os.path.exists(filename) and (time.time()-os.path.getmtime(filename)) >= (grace*60):
347 if os.path.exists(filename):
348 raise PLCInvalidArgument, "Resulting file %s already exists - please try again in %d minutes"%\
351 ### we can now safely create the file,
352 ### either we are admin or under a controlled location
353 filedir=os.path.dirname(filename)
354 # dirname does not return "." for a local filename like its shell counterpart
356 if not os.path.exists(filedir):
358 os.makedirs (filedir,0777)
360 raise PLCPermissionDenied, "Could not create dir %s"%filedir
364 # Build the command line to be executed
365 # according the node type
366 def build_command(self, node_type, build_sh_spec, node_image, type, floppy_file, log_file):
370 # regular node, make build's arguments
371 # and build the full command line to be called
372 if node_type in [ 'regular', 'reservable' ]:
375 if "cramfs" in build_sh_spec:
377 if "serial" in build_sh_spec:
378 build_sh_options += " -s %s"%build_sh_spec['serial']
379 if "variant" in build_sh_spec:
380 build_sh_options += " -V %s"%build_sh_spec['variant']
382 for karg in build_sh_spec['kargs']:
383 build_sh_options += ' -k "%s"'%karg
385 log_file="%s.log"%node_image
387 command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
395 print "The build command line is %s" % command
399 def call(self, auth, node_id_or_hostname, action, filename, options = []):
403 ### compute file suffix and type
404 if action.find("-iso") >= 0 :
407 elif action.find("-usb") >= 0:
414 # check for node existence and get node_type
415 nodes = Nodes(self.api, [node_id_or_hostname])
417 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
420 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
421 % (action, node['node_id'], node['node_type'])
423 # check the required action against the node type
424 node_type = node['node_type']
425 if action not in allowed_actions[node_type]:
426 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
427 % (action, node_type, "|".join(allowed_actions[node_type]))
429 # handle / canonicalize options
432 raise PLCInvalidArgument, "Options are not supported for node configs"
434 # create a dict for build.sh
435 build_sh_spec={'kargs':[]}
436 # use node tags as defaults
437 # check for node tag equivalents
438 tags = NodeTags(self.api,
439 {'node_id': node['node_id'],
440 'tagname': ['serial', 'cramfs', 'kvariant', 'kargs', 'no-hangcheck']},
441 ['tagname', 'value'])
444 if tag['tagname'] == 'serial':
445 build_sh_spec['serial'] = tag['value']
446 if tag['tagname'] == 'cramfs':
447 build_sh_spec['cramfs'] = True
448 if tag['tagname'] == 'kvariant':
449 build_sh_spec['variant'] = tag['value']
450 if tag['tagname'] == 'kargs':
451 build_sh_spec['kargs'] += tag['value'].split()
452 if tag['tagname'] == 'no-hangcheck':
453 build_sh_spec['kargs'].append('hcheck_reboot0')
454 # then options can override tags
455 for option in options:
456 if option == "cramfs":
457 build_sh_spec['cramfs']=True
458 elif option == 'partition':
460 raise PLCInvalidArgument, "option 'partition' is for USB images only"
463 elif option == "serial":
464 build_sh_spec['serial']='default'
465 elif option.find("serial:") == 0:
466 build_sh_spec['serial']=option.replace("serial:","")
467 elif option.find("variant:") == 0:
468 build_sh_spec['variant']=option.replace("variant:","")
469 elif option == "no-hangcheck":
470 build_sh_spec['kargs'].append('hcheck_reboot0')
472 raise PLCInvalidArgument, "unknown option %s"%option
474 # compute nodename according the action
475 if action.find("node-") == 0:
476 nodename = node['hostname']
479 # compute a 8 bytes random number
480 tempbytes = random.sample (xrange(0,256), 8);
481 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
482 nodename = "".join(map(hexa2,tempbytes))
485 (pldistro,fcdistro,arch) = self.get_nodefamily(node,auth)
486 self.nodefamily="%s-%s-%s"%(pldistro,fcdistro,arch)
489 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
490 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
492 filename = self.handle_filename(filename, nodename, suffix, arch)
496 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
497 self.event_objects={'Node': [ node ['node_id'] ]}
499 self.message='GetBootMedium - generic - action=%s'%action
502 if action == 'generic-iso' or action == 'generic-usb':
504 raise PLCInvalidArgument, "Options are not supported for generic images"
505 # this raises an exception if bootcd is missing
506 version = self.bootcd_version()
507 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
510 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
513 ret=os.system ('cp "%s" "%s"'%(generic_path,filename))
517 raise PLCPermissionDenied, "Could not copy %s into %s"%(generic_path,filename)
519 ### return the generic medium content as-is, just base64 encoded
520 return base64.b64encode(file(generic_path).read())
522 ### config file preview or regenerated
523 if action == 'node-preview' or action == 'node-floppy':
524 renew_key = (action == 'node-floppy')
525 floppy = self.floppy_contents (node,renew_key)
528 file(filename,'w').write(floppy)
530 raise PLCPermissionDenied, "Could not write into %s"%filename
535 ### we're left with node-iso and node-usb
536 # the steps involved in the image creation are:
537 # - create and test the working environment
538 # - generate the configuration file
539 # - build and invoke the build command
540 # - delivery the resulting image file
542 if action == 'node-iso' or action == 'node-usb':
544 ### check we've got required material
545 version = self.bootcd_version()
547 if not os.path.isfile(self.BOOTCDBUILD):
548 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
550 # create the workdir if needed
551 if not os.path.isdir(self.WORKDIR):
553 os.makedirs(self.WORKDIR,0777)
554 os.chmod(self.WORKDIR,0777)
556 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
559 # generate floppy config
560 floppy_text = self.floppy_contents(node,True)
562 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
564 file(floppy_file,"w").write(floppy_text)
566 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
568 self.trash.append(floppy_file)
570 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
571 log_file="%s.log"%node_image
573 command = self.build_command(node_type, build_sh_spec, node_image, type, floppy_file, log_file)
575 # invoke the image build script
577 ret=os.system(command)
580 raise PLCAPIError, "%s failed Command line was: %s Error logs: %s" % \
581 (self.BOOTCDBUILD, command, file(log_file).read())
583 self.trash.append(log_file)
585 if not os.path.isfile (node_image):
586 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
590 ret=os.system('mv "%s" "%s"'%(node_image,filename))
592 self.trash.append(node_image)
594 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
598 result = file(node_image).read()
599 self.trash.append(node_image)
601 return base64.b64encode(result)
606 # we're done here, or we missed something
607 raise PLCAPIError,'Unhandled action %s'%action