8 from PLC.Faults import *
9 from PLC.Method import Method
10 from PLC.Parameter import Parameter, Mixed
11 from PLC.Auth import Auth
13 from PLC.Nodes import Node, Nodes
14 from PLC.Interfaces import Interface, Interfaces
15 from PLC.InterfaceTags import InterfaceTag, InterfaceTags
17 # could not define this in the class..
18 # create a dict with the allowed actions for each type of node
20 'regular' : [ 'node-preview',
27 'dummynet' : [ 'node-preview',
34 # xxx used by GetDummyBoxMedium
36 # Generate 32 random bytes
37 bytes = random.sample(xrange(0, 256), 32)
38 # Base64 encode their string representation
39 key = base64.b64encode("".join(map(chr, bytes)))
40 # Boot Manager cannot handle = in the key
41 # XXX this sounds wrong, as it might prevent proper decoding
42 key = key.replace("=", "")
45 class GetBootMedium(Method):
47 This method is a redesign based on former, supposedly dedicated,
48 AdmGenerateNodeConfFile
50 As compared with its ancestor, this method provides a much more detailed
51 detailed interface, that allows to
52 (*) either just preview the node config file -- in which case
53 the node key is NOT recomputed, and NOT provided in the output
54 (*) or regenerate the node config file for storage on a floppy
55 that is, exactly what the ancestor method used todo,
56 including renewing the node's key
57 (*) or regenerate the config file and bundle it inside an ISO or USB image
58 (*) or just provide the generic ISO or USB boot images
59 in which case of course the node_id_or_hostname parameter is not used
61 action is expected among the following string constants
69 Apart for the preview mode, this method generates a new node key for the
70 specified node, effectively invalidating any old boot medium.
72 In addition, two return mechanisms are supported.
73 (*) The default behaviour is that the file's content is returned as a
74 base64-encoded string. This is how the ancestor method used to work.
75 To use this method, pass an empty string as the file parameter.
77 (*) Or, for efficiency -- this makes sense only when the API is used
78 by the web pages that run on the same host -- the caller may provide
79 a filename, in which case the resulting file is stored in that location instead.
80 The filename argument can use the following markers, that are expanded
82 - %d : default root dir (some builtin dedicated area under /var/tmp/)
83 Using this is recommended, and enforced for non-admin users
84 - %n : the node's name when this makes sense, or a mktemp-like name when
85 generic media is requested
86 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
87 - %v : the bootcd version string (e.g. 4.0)
91 With the file-based return mechanism, the method returns the full pathname
94 It is the caller's responsability to remove this file after use.
96 Options: an optional array of keywords.
97 options are not supported for generic images
98 Currently supported are
99 - 'partition' - for USB actions only
101 - 'serial' or 'serial:<console_spec>'
103 console_spec (or 'default') is passed as-is to bootcd/build.sh
104 it is expected to be a colon separated string denoting
105 tty - baudrate - parity - bits
106 e.g. ttyS0:115200:n:8
109 - Non-admins can only generate files for nodes at their sites.
110 - Non-admins, when they provide a filename, *must* specify it in the %d area
113 Whenever needed, the method stores intermediate files in a
114 private area, typically not located under the web server's
115 accessible area, and are cleaned up by the method.
119 roles = ['admin', 'pi', 'tech']
123 Mixed(Node.fields['node_id'],
124 Node.fields['hostname']),
125 Parameter (str, "Action mode, expected value depends of the type of node"),
126 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
127 Parameter ([str], "Options"),
130 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
132 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
133 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
134 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
135 WORKDIR = "/var/tmp/bootmedium"
137 # uncomment this to preserve temporary area and bootcustom logs
140 ### returns (host, domain) :
141 # 'host' : host part of the hostname
142 # 'domain' : domain part of the hostname
143 def split_hostname (self, node):
144 # Split hostname into host and domain parts
145 parts = node['hostname'].split(".", 1)
147 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
150 # Generate the node (plnode.txt) configuration content.
152 # This function will create the configuration file a node
154 # - a common part, regardless of the 'node_type' tag
155 # - XXX a special part, depending on the 'snode_type' tag value.
156 def floppy_contents (self, node, renew_key):
159 if node['peer_id'] is not None:
160 raise PLCInvalidArgument, "Not a local node"
162 # If we are not an admin, make sure that the caller is a
163 # member of the site at which the node is located.
164 if 'admin' not in self.caller['roles']:
165 if node['site_id'] not in self.caller['site_ids']:
166 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
168 # Get interface for this node
170 interfaces = Interfaces(self.api, node['interface_ids'])
171 for interface in interfaces:
172 if interface['is_primary']:
176 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
178 ( host, domain ) = self.split_hostname (node)
180 # renew the key and save it on the database
182 node['key'] = compute_key()
185 # Generate node configuration file suitable for BootCD
189 file += 'NODE_ID="%d"\n' % node['node_id']
190 file += 'NODE_KEY="%s"\n' % node['key']
191 # not used anywhere, just a note for operations people
192 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
195 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
197 file += 'IP_METHOD="%s"\n' % primary['method']
199 if primary['method'] == 'static':
200 file += 'IP_ADDRESS="%s"\n' % primary['ip']
201 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
202 file += 'IP_NETMASK="%s"\n' % primary['netmask']
203 file += 'IP_NETADDR="%s"\n' % primary['network']
204 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
205 file += 'IP_DNS1="%s"\n' % primary['dns1']
206 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
208 file += 'HOST_NAME="%s"\n' % host
209 file += 'DOMAIN_NAME="%s"\n' % domain
211 # define various interface settings attached to the primary interface
212 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
215 for setting in settings:
216 if setting['category'] is not None:
217 categories.add(setting['category'])
219 for category in categories:
220 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
221 'category':category})
222 if category_settings:
223 file += '### Category : %s\n'%category
224 for setting in category_settings:
225 file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
227 for interface in interfaces:
228 if interface['method'] == 'ipmi':
229 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
231 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
236 # see also InstallBootstrapFS in bootmanager that does similar things
237 def get_nodefamily (self, node):
238 # get defaults from the myplc build
240 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
242 (pldistro,arch) = ("planetlab","i386")
244 # with no valid argument, return system-wide defaults
246 return (pldistro,arch)
248 node_id=node['node_id']
250 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
252 tag=Nodes(self.api,[node_id],['pldistro'])[0]['pldistro']
255 return (pldistro,arch)
257 def bootcd_version (self):
259 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
261 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
263 def cleantrash (self):
264 for file in self.trash:
266 print 'DEBUG -- preserving',file
271 # build the filename string
272 # check for permissions and concurrency
273 # returns the filename
274 def handle_filename (self, filename, nodename, suffix, arch):
275 # allow to set filename to None or any other empty value
276 if not filename: filename=''
277 filename = filename.replace ("%d",self.WORKDIR)
278 filename = filename.replace ("%n",nodename)
279 filename = filename.replace ("%s",suffix)
280 filename = filename.replace ("%p",self.api.config.PLC_NAME)
282 try: filename = filename.replace ("%f", self.nodefamily)
284 try: filename = filename.replace ("%a", arch)
286 try: filename = filename.replace ("%v",self.bootcd_version())
289 ### Check filename location
291 if 'admin' not in self.caller['roles']:
292 if ( filename.index(self.WORKDIR) != 0):
293 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
295 ### output should not exist (concurrent runs ..)
296 if os.path.exists(filename):
297 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
299 ### we can now safely create the file,
300 ### either we are admin or under a controlled location
301 filedir=os.path.dirname(filename)
302 # dirname does not return "." for a local filename like its shell counterpart
304 if not os.path.exists(filedir):
306 os.makedirs (filedir,0777)
308 raise PLCPermissionDenied, "Could not create dir %s"%filedir
312 def call(self, auth, node_id_or_hostname, action, filename, options = []):
316 ### compute file suffix and type
317 if action.find("-iso") >= 0 :
320 elif action.find("-usb") >= 0:
327 # handle / caconicalize options
330 raise PLCInvalidArgument, "Options are not supported for node configs"
332 # create a dict for build.sh
333 build_sh_spec={'kargs':[]}
334 for option in options:
335 if option == "cramfs":
336 build_sh_spec['cramfs']=True
337 elif option == 'partition':
339 raise PLCInvalidArgument, "option 'partition' is for USB images only"
342 elif option == "serial":
343 build_sh_spec['serial']='default'
344 elif option.find("serial:") == 0:
345 build_sh_spec['serial']=option.replace("serial:","")
346 elif option == "no-hangcheck":
347 build_sh_spec['kargs'].append('hcheck_reboot0')
349 raise PLCInvalidArgument, "unknown option %s"%option
351 ### check for node existence and get node_type
352 nodes = Nodes(self.api, [node_id_or_hostname])
354 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
357 if self.DEBUG: print "%s required on node %s. Node type is: %s" \
358 % (action, node['node_id'], node['node_type'])
360 # checks required action against the node type
361 node_type = node['node_type']
362 if action not in allowed_actions[node_type]:
363 raise PLCInvalidArgument, "Action %s not valid for %s nodes, valid actions are %s" \
364 % (action, node_type, "|".join(allowed_actions[node_type]))
367 if action.find("node-") == 0 or action.find("dummynet-") == 0:
368 nodename = node['hostname']
371 # compute a 8 bytes random number
372 tempbytes = random.sample (xrange(0,256), 8);
373 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
374 nodename = "".join(map(hexa2,tempbytes))
377 (pldistro,arch) = self.get_nodefamily(node)
378 self.nodefamily="%s-%s"%(pldistro,arch)
381 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
382 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
384 filename = self.handle_filename(filename, nodename, suffix, arch)
388 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
389 self.event_objects={'Node': [ node ['node_id'] ]}
391 self.message='GetBootMedium - generic - action=%s'%action
394 if action == 'generic-iso' or action == 'generic-usb':
396 raise PLCInvalidArgument, "Options are not supported for generic images"
397 # this raises an exception if bootcd is missing
398 version = self.bootcd_version()
399 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
402 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
405 ret=os.system ("cp %s %s"%(generic_path,filename))
409 raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
411 ### return the generic medium content as-is, just base64 encoded
412 return base64.b64encode(file(generic_path).read())
414 ### config file preview or regenerated
415 if action == 'node-preview' or action == 'node-floppy':
416 renew_key = (action == 'node-floppy')
417 floppy = self.floppy_contents (node,renew_key)
420 file(filename,'w').write(floppy)
422 raise PLCPermissionDenied, "Could not write into %s"%filename
427 ### we're left with node-iso and node-usb
428 if action == 'node-iso' or action == 'node-usb':
430 ### check we've got required material
431 version = self.bootcd_version()
433 if not os.path.isfile(self.BOOTCDBUILD):
434 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
436 # create the workdir if needed
437 if not os.path.isdir(self.WORKDIR):
439 os.makedirs(self.WORKDIR,0777)
440 os.chmod(self.WORKDIR,0777)
442 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
445 # generate floppy config
446 floppy_text = self.floppy_contents(node,True)
448 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
450 file(floppy_file,"w").write(floppy_text)
452 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
454 self.trash.append(floppy_file)
456 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
458 # make build's arguments
460 if "cramfs" in build_sh_spec:
462 if "serial" in build_sh_spec:
463 build_sh_options += " -s %s"%build_sh_spec['serial']
465 for karg in build_sh_spec['kargs']:
466 build_sh_options += ' -k "%s"'%karg
468 log_file="%s.log"%node_image
470 build_command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
477 print 'build command:',build_command
478 ret=os.system(build_command)
480 raise PLCAPIError,"bootcd/build.sh failed\n%s\n%s"%(
481 build_command,file(log_file).read())
483 self.trash.append(log_file)
484 if not os.path.isfile (node_image):
485 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
489 ret=os.system("mv %s %s"%(node_image,filename))
491 self.trash.append(node_image)
493 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
497 result = file(node_image).read()
498 self.trash.append(node_image)
500 return base64.b64encode(result)
505 # we're done here, or we missed something
506 raise PLCAPIError,'Unhandled action %s'%action