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 boot_medium_actions = [ 'node-preview',
27 # xxx used by GetDummyBoxMedium
29 # Generate 32 random bytes
30 bytes = random.sample(xrange(0, 256), 32)
31 # Base64 encode their string representation
32 key = base64.b64encode("".join(map(chr, bytes)))
33 # Boot Manager cannot handle = in the key
34 # XXX this sounds wrong, as it might prevent proper decoding
35 key = key.replace("=", "")
38 class GetBootMedium(Method):
40 This method is a redesign based on former, supposedly dedicated,
41 AdmGenerateNodeConfFile
43 As compared with its ancestor, this method provides a much more detailed
44 detailed interface, that allows to
45 (*) either just preview the node config file -- in which case
46 the node key is NOT recomputed, and NOT provided in the output
47 (*) or regenerate the node config file for storage on a floppy
48 that is, exactly what the ancestor method used todo,
49 including renewing the node's key
50 (*) or regenerate the config file and bundle it inside an ISO or USB image
51 (*) or just provide the generic ISO or USB boot images
52 in which case of course the node_id_or_hostname parameter is not used
54 action is expected among the following string constants
62 Apart for the preview mode, this method generates a new node key for the
63 specified node, effectively invalidating any old boot medium.
65 In addition, two return mechanisms are supported.
66 (*) The default behaviour is that the file's content is returned as a
67 base64-encoded string. This is how the ancestor method used to work.
68 To use this method, pass an empty string as the file parameter.
70 (*) Or, for efficiency -- this makes sense only when the API is used
71 by the web pages that run on the same host -- the caller may provide
72 a filename, in which case the resulting file is stored in that location instead.
73 The filename argument can use the following markers, that are expanded
75 - %d : default root dir (some builtin dedicated area under /var/tmp/)
76 Using this is recommended, and enforced for non-admin users
77 - %n : the node's name when this makes sense, or a mktemp-like name when
78 generic media is requested
79 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
80 - %v : the bootcd version string (e.g. 4.0)
84 With the file-based return mechanism, the method returns the full pathname
87 It is the caller's responsability to remove this file after use.
89 Options: an optional array of keywords.
90 options are not supported for generic images
91 Currently supported are
92 - 'partition' - for USB actions only
94 - 'serial' or 'serial:<console_spec>'
96 console_spec (or 'default') is passed as-is to bootcd/build.sh
97 it is expected to be a colon separated string denoting
98 tty - baudrate - parity - bits
102 - Non-admins can only generate files for nodes at their sites.
103 - Non-admins, when they provide a filename, *must* specify it in the %d area
106 Whenever needed, the method stores intermediate files in a
107 private area, typically not located under the web server's
108 accessible area, and are cleaned up by the method.
112 roles = ['admin', 'pi', 'tech']
116 Mixed(Node.fields['node_id'],
117 Node.fields['hostname']),
118 Parameter (str, "Action mode, expected in " + "|".join(boot_medium_actions)),
119 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
120 Parameter ([str], "Options"),
123 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
125 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
126 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
127 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
128 WORKDIR = "/var/tmp/bootmedium"
130 # uncomment this to preserve temporary area and bootcustom logs
133 ### returns (host, domain) :
134 # 'host' : host part of the hostname
135 # 'domain' : domain part of the hostname
136 def split_hostname (self, node):
137 # Split hostname into host and domain parts
138 parts = node['hostname'].split(".", 1)
140 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
144 def floppy_contents (self, node, renew_key):
146 if node['peer_id'] is not None:
147 raise PLCInvalidArgument, "Not a local node"
149 # If we are not an admin, make sure that the caller is a
150 # member of the site at which the node is located.
151 if 'admin' not in self.caller['roles']:
152 if node['site_id'] not in self.caller['site_ids']:
153 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
155 # Get node networks for this node
157 interfaces = Interfaces(self.api, node['interface_ids'])
158 for interface in interfaces:
159 if interface['is_primary']:
163 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
165 ( host, domain ) = self.split_hostname (node)
168 node['key'] = compute_key()
172 # Generate node configuration file suitable for BootCD
176 file += 'NODE_ID="%d"\n' % node['node_id']
177 file += 'NODE_KEY="%s"\n' % node['key']
178 # not used anywhere, just a note for operations people
179 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
182 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
184 file += 'IP_METHOD="%s"\n' % primary['method']
186 if primary['method'] == 'static':
187 file += 'IP_ADDRESS="%s"\n' % primary['ip']
188 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
189 file += 'IP_NETMASK="%s"\n' % primary['netmask']
190 file += 'IP_NETADDR="%s"\n' % primary['network']
191 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
192 file += 'IP_DNS1="%s"\n' % primary['dns1']
193 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
195 file += 'HOST_NAME="%s"\n' % host
196 file += 'DOMAIN_NAME="%s"\n' % domain
198 # define various interface settings attached to the primary interface
199 settings = InterfaceTags (self.api, {'interface_id':interface['interface_id']})
202 for setting in settings:
203 if setting['category'] is not None:
204 categories.add(setting['category'])
206 for category in categories:
207 category_settings = InterfaceTags(self.api,{'interface_id':interface['interface_id'],
208 'category':category})
209 if category_settings:
210 file += '### Category : %s\n'%category
211 for setting in category_settings:
212 file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
214 for interface in interfaces:
215 if interface['method'] == 'ipmi':
216 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
218 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
223 # see also InstallBootstrapFS in bootmanager that does similar things
224 def get_nodefamily (self, node):
225 # get defaults from the myplc build
227 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
229 (pldistro,arch) = ("planetlab","i386")
231 # with no valid argument, return system-wide defaults
233 return (pldistro,arch)
235 node_id=node['node_id']
237 tag=Nodes(self.api,[node_id],['arch'])[0]['arch']
239 tag=Nodes(self.api,[node_id],['arch'])[0]['pldistro']
242 return (pldistro,arch)
244 def bootcd_version (self):
246 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
248 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
250 def cleantrash (self):
251 for file in self.trash:
253 print 'DEBUG -- preserving',file
257 def call(self, auth, node_id_or_hostname, action, filename, options = []):
261 if action not in boot_medium_actions:
262 raise PLCInvalidArgument, "Unknown action %s"%action
264 ### compute file suffix and type
265 if action.find("-iso") >= 0 :
268 elif action.find("-usb") >= 0:
275 # handle / caconicalize options
278 raise PLCInvalidArgument, "Options are not supported for node configs"
280 # create a dict for build.sh
281 build_sh_spec={'kargs':[]}
282 for option in options:
283 if option == "cramfs":
284 build_sh_spec['cramfs']=True
285 elif option == 'partition':
287 raise PLCInvalidArgument, "option 'partition' is for USB images only"
290 elif option == "serial":
291 build_sh_spec['serial']='default'
292 elif option.find("serial:") == 0:
293 build_sh_spec['serial']=option.replace("serial:","")
294 elif option == "no-hangcheck":
295 build_sh_spec['kargs'].append('hcheck_reboot0')
297 raise PLCInvalidArgument, "unknown option %s"%option
299 ### check node if needed
300 if action.find("node-") == 0:
301 nodes = Nodes(self.api, [node_id_or_hostname])
303 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
305 nodename = node['hostname']
309 # compute a 8 bytes random number
310 tempbytes = random.sample (xrange(0,256), 8);
311 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
312 nodename = "".join(map(hexa2,tempbytes))
315 (pldistro,arch) = self.get_nodefamily(node)
316 self.nodefamily="%s-%s"%(pldistro,arch)
318 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
319 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
322 # allow to set filename to None or any other empty value
323 if not filename: filename=''
324 filename = filename.replace ("%d",self.WORKDIR)
325 filename = filename.replace ("%n",nodename)
326 filename = filename.replace ("%s",suffix)
327 filename = filename.replace ("%p",self.api.config.PLC_NAME)
329 try: filename = filename.replace ("%f", self.nodefamily)
331 try: filename = filename.replace ("%a", arch)
333 try: filename = filename.replace ("%v",self.bootcd_version())
336 ### Check filename location
338 if 'admin' not in self.caller['roles']:
339 if ( filename.index(self.WORKDIR) != 0):
340 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
342 ### output should not exist (concurrent runs ..)
343 if os.path.exists(filename):
344 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
346 ### we can now safely create the file,
347 ### either we are admin or under a controlled location
348 filedir=os.path.dirname(filename)
349 # dirname does not return "." for a local filename like its shell counterpart
351 if not os.path.exists(filedir):
353 os.makedirs (filedir,0777)
355 raise PLCPermissionDenied, "Could not create dir %s"%filedir
360 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
361 self.event_objects={'Node': [ node ['node_id'] ]}
363 self.message='GetBootMedium - generic - action=%s'%action
366 if action == 'generic-iso' or action == 'generic-usb':
368 raise PLCInvalidArgument, "Options are not supported for generic images"
369 # this raises an exception if bootcd is missing
370 version = self.bootcd_version()
371 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
374 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
377 ret=os.system ("cp %s %s"%(generic_path,filename))
381 raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
383 ### return the generic medium content as-is, just base64 encoded
384 return base64.b64encode(file(generic_path).read())
386 ### config file preview or regenerated
387 if action == 'node-preview' or action == 'node-floppy':
388 renew_key = (action == 'node-floppy')
389 floppy = self.floppy_contents (node,renew_key)
392 file(filename,'w').write(floppy)
394 raise PLCPermissionDenied, "Could not write into %s"%filename
399 ### we're left with node-iso and node-usb
400 if action == 'node-iso' or action == 'node-usb':
402 ### check we've got required material
403 version = self.bootcd_version()
405 if not os.path.isfile(self.BOOTCDBUILD):
406 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
408 # create the workdir if needed
409 if not os.path.isdir(self.WORKDIR):
411 os.makedirs(self.WORKDIR,0777)
412 os.chmod(self.WORKDIR,0777)
414 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
417 # generate floppy config
418 floppy_text = self.floppy_contents(node,True)
420 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
422 file(floppy_file,"w").write(floppy_text)
424 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
426 self.trash.append(floppy_file)
428 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
430 # make build's arguments
432 if "cramfs" in build_sh_spec:
434 if "serial" in build_sh_spec:
435 build_sh_options += " -s %s"%build_sh_spec['serial']
437 for karg in build_sh_spec['kargs']:
438 build_sh_options += ' -k "%s"'%karg
440 log_file="%s.log"%node_image
442 build_command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
449 print 'build command:',build_command
450 ret=os.system(build_command)
452 raise PLCAPIError,"bootcd/build.sh failed\n%s\n%s"%(
453 build_command,file(log_file).read())
455 self.trash.append(log_file)
456 if not os.path.isfile (node_image):
457 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
461 ret=os.system("mv %s %s"%(node_image,filename))
463 self.trash.append(node_image)
465 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
469 result = file(node_image).read()
470 self.trash.append(node_image)
472 return base64.b64encode(result)
477 # we're done here, or we missed something
478 raise PLCAPIError,'Unhandled action %s'%action