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.InterfaceSettings import InterfaceSetting, InterfaceSettings
16 from PLC.NodeTags import NodeTags
18 # could not define this in the class..
19 boot_medium_actions = [ 'node-preview',
28 # xxx used by GetDummyBoxMedium
30 # Generate 32 random bytes
31 bytes = random.sample(xrange(0, 256), 32)
32 # Base64 encode their string representation
33 key = base64.b64encode("".join(map(chr, bytes)))
34 # Boot Manager cannot handle = in the key
35 # XXX this sounds wrong, as it might prevent proper decoding
36 key = key.replace("=", "")
39 class GetBootMedium(Method):
41 This method is a redesign based on former, supposedly dedicated,
42 AdmGenerateNodeConfFile
44 As compared with its ancestor, this method provides a much more detailed
45 detailed interface, that allows to
46 (*) either just preview the node config file -- in which case
47 the node key is NOT recomputed, and NOT provided in the output
48 (*) or regenerate the node config file for storage on a floppy
49 that is, exactly what the ancestor method used todo,
50 including renewing the node's key
51 (*) or regenerate the config file and bundle it inside an ISO or USB image
52 (*) or just provide the generic ISO or USB boot images
53 in which case of course the node_id_or_hostname parameter is not used
55 action is expected among the following string constants
63 Apart for the preview mode, this method generates a new node key for the
64 specified node, effectively invalidating any old boot medium.
66 In addition, two return mechanisms are supported.
67 (*) The default behaviour is that the file's content is returned as a
68 base64-encoded string. This is how the ancestor method used to work.
69 To use this method, pass an empty string as the file parameter.
71 (*) Or, for efficiency -- this makes sense only when the API is used
72 by the web pages that run on the same host -- the caller may provide
73 a filename, in which case the resulting file is stored in that location instead.
74 The filename argument can use the following markers, that are expanded
76 - %d : default root dir (some builtin dedicated area under /var/tmp/)
77 Using this is recommended, and enforced for non-admin users
78 - %n : the node's name when this makes sense, or a mktemp-like name when
79 generic media is requested
80 - %s : a file suffix appropriate in the context (.txt, .iso or the like)
81 - %v : the bootcd version string (e.g. 4.0)
85 With the file-based return mechanism, the method returns the full pathname
88 It is the caller's responsability to remove this file after use.
90 Options: an optional array of keywords.
91 options are not supported for generic images
92 Currently supported are
93 - 'partition' - for USB actions only
95 - '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 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y-%m-%d at %H:%M:%S +0000',time.gmtime())
181 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
183 file += 'IP_METHOD="%s"\n' % primary['method']
185 if primary['method'] == 'static':
186 file += 'IP_ADDRESS="%s"\n' % primary['ip']
187 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
188 file += 'IP_NETMASK="%s"\n' % primary['netmask']
189 file += 'IP_NETADDR="%s"\n' % primary['network']
190 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
191 file += 'IP_DNS1="%s"\n' % primary['dns1']
192 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
194 file += 'HOST_NAME="%s"\n' % host
195 file += 'DOMAIN_NAME="%s"\n' % domain
197 # define various interface settings attached to the primary interface
198 settings = InterfaceSettings (self.api, {'interface_id':interface['interface_id']})
201 for setting in settings:
202 if setting['category'] is not None:
203 categories.add(setting['category'])
205 for category in categories:
206 category_settings = InterfaceSettings(self.api,{'interface_id':interface['interface_id'],
207 'category':category})
208 if category_settings:
209 file += '### Category : %s\n'%category
210 for setting in category_settings:
211 file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
213 for interface in interfaces:
214 if interface['method'] == 'ipmi':
215 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
217 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
222 # see also InstallBootstrapFS in bootmanager that does similar things
223 def get_nodefamily (self, node):
224 # get defaults from the myplc build
226 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
228 (pldistro,arch) = ("planetlab","i386")
230 # with no valid argument, return system-wide defaults
232 return (pldistro,arch)
234 node_id=node['node_id']
235 # cannot use accessors in the API itself
236 # the 'arch' tag type is assumed to exist, see db-config
237 arch_tags = NodeTags (self.api, {'tagname':'arch','node_id':node_id},['tagvalue'])
239 arch=arch_tags[0]['tagvalue']
241 pldistro_tags = NodeTags (self.api, {'tagname':'pldistro','node_id':node_id},['tagvalue'])
243 pldistro=pldistro_tags[0]['tagvalue']
245 return (pldistro,arch)
247 def bootcd_version (self):
249 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
251 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
253 def cleantrash (self):
254 for file in self.trash:
256 print 'DEBUG -- preserving',file
260 def call(self, auth, node_id_or_hostname, action, filename, options = []):
264 if action not in boot_medium_actions:
265 raise PLCInvalidArgument, "Unknown action %s"%action
267 ### compute file suffix and type
268 if action.find("-iso") >= 0 :
271 elif action.find("-usb") >= 0:
278 # handle / caconicalize options
281 raise PLCInvalidArgument, "Options are not supported for node configs"
283 # create a dict for build.sh
285 for option in options:
286 if option == "cramfs":
287 optdict['cramfs']=True
288 elif option == 'partition':
290 raise PLCInvalidArgument, "option 'partition' is for USB images only"
293 elif option == "serial":
294 optdict['serial']='default'
295 elif option.find("serial:") == 0:
296 optdict['serial']=option.replace("serial:","")
298 raise PLCInvalidArgument, "unknown option %s"%option
300 ### check node if needed
301 if action.find("node-") == 0:
302 nodes = Nodes(self.api, [node_id_or_hostname])
304 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
306 nodename = node['hostname']
310 # compute a 8 bytes random number
311 tempbytes = random.sample (xrange(0,256), 8);
312 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
313 nodename = "".join(map(hexa2,tempbytes))
316 (pldistro,arch) = self.get_nodefamily(node)
317 self.nodefamily="%s-%s"%(pldistro,arch)
319 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
320 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
323 # allow to set filename to None or any other empty value
324 if not filename: filename=''
325 filename = filename.replace ("%d",self.WORKDIR)
326 filename = filename.replace ("%n",nodename)
327 filename = filename.replace ("%s",suffix)
328 filename = filename.replace ("%p",self.api.config.PLC_NAME)
330 try: filename = filename.replace ("%f", self.nodefamily)
332 try: filename = filename.replace ("%a", arch)
334 try: filename = filename.replace ("%v",self.bootcd_version())
337 ### Check filename location
339 if 'admin' not in self.caller['roles']:
340 if ( filename.index(self.WORKDIR) != 0):
341 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
343 ### output should not exist (concurrent runs ..)
344 if os.path.exists(filename):
345 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
347 ### we can now safely create the file,
348 ### either we are admin or under a controlled location
349 filedir=os.path.dirname(filename)
350 # dirname does not return "." for a local filename like its shell counterpart
352 if not os.path.exists(filedir):
354 os.makedirs (filedir,0777)
356 raise PLCPermissionDenied, "Could not create dir %s"%filedir
360 if action == 'generic-iso' or action == 'generic-usb':
362 raise PLCInvalidArgument, "Options are not supported for generic images"
363 # this raises an exception if bootcd is missing
364 version = self.bootcd_version()
365 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
368 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
371 ret=os.system ("cp %s %s"%(generic_path,filename))
375 raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
377 ### return the generic medium content as-is, just base64 encoded
378 return base64.b64encode(file(generic_path).read())
380 ### config file preview or regenerated
381 if action == 'node-preview' or action == 'node-floppy':
382 renew_key = (action == 'node-floppy')
383 floppy = self.floppy_contents (node,renew_key)
386 file(filename,'w').write(floppy)
388 raise PLCPermissionDenied, "Could not write into %s"%filename
393 ### we're left with node-iso and node-usb
394 if action == 'node-iso' or action == 'node-usb':
396 ### check we've got required material
397 version = self.bootcd_version()
399 if not os.path.isfile(self.BOOTCDBUILD):
400 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
402 # create the workdir if needed
403 if not os.path.isdir(self.WORKDIR):
405 os.makedirs(self.WORKDIR,0777)
406 os.chmod(self.WORKDIR,0777)
408 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
411 # generate floppy config
412 floppy_text = self.floppy_contents(node,True)
414 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
416 file(floppy_file,"w").write(floppy_text)
418 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
420 self.trash.append(floppy_file)
422 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
424 # make build's arguments
426 if "cramfs" in optdict: type += "_cramfs"
427 if "serial" in optdict: serial_arg = "-s %s"%optdict['serial']
428 log_file="%s.log"%node_image
430 build_command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
437 print 'build command:',build_command
438 ret=os.system(build_command)
440 raise PLCAPIError,"bootcd/build.sh failed\n%s\n%s"%(
441 build_command,file(log_file).read())
443 self.trash.append(log_file)
444 if not os.path.isfile (node_image):
445 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
449 ret=os.system("mv %s %s"%(node_image,filename))
451 self.trash.append(node_image)
453 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
457 result = file(node_image).read()
458 self.trash.append(node_image)
460 return base64.b64encode(result)
465 # we're done here, or we missed something
466 raise PLCAPIError,'Unhandled action %s'%action