1 # $Id: GetBootMedium.py 8284 2008-02-25 11:30:13Z thierry $
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.Nodes import Node, Nodes
13 from PLC.NodeNetworks import NodeNetwork, NodeNetworks
14 from PLC.NodeNetworkSettings import NodeNetworkSetting, NodeNetworkSettings
15 from PLC.NodeGroups import NodeGroup, NodeGroups
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>'
95 console_spec (or 'default') is passed as-is to bootcd/build.sh
96 it is expected to be a colon separated string denoting
97 tty - baudrate - parity - bits
101 - Non-admins can only generate files for nodes at their sites.
102 - Non-admins, when they provide a filename, *must* specify it in the %d area
105 Whenever needed, the method stores intermediate files in a
106 private area, typically not located under the web server's
107 accessible area, and are cleaned up by the method.
111 roles = ['admin', 'pi', 'tech']
115 Mixed(Node.fields['node_id'],
116 Node.fields['hostname']),
117 Parameter (str, "Action mode, expected in " + "|".join(boot_medium_actions)),
118 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
119 Parameter ([str], "Options"),
122 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
124 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
125 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
126 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
127 WORKDIR = "/var/tmp/bootmedium"
129 # uncomment this to preserve temporary area and bootcustom logs
132 ### returns (host, domain) :
133 # 'host' : host part of the hostname
134 # 'domain' : domain part of the hostname
135 def split_hostname (self, node):
136 # Split hostname into host and domain parts
137 parts = node['hostname'].split(".", 1)
139 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
143 def floppy_contents (self, node, renew_key):
145 if node['peer_id'] is not None:
146 raise PLCInvalidArgument, "Not a local node"
148 # If we are not an admin, make sure that the caller is a
149 # member of the site at which the node is located.
150 if 'admin' not in self.caller['roles']:
151 if node['site_id'] not in self.caller['site_ids']:
152 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
154 # Get node networks for this node
156 nodenetworks = NodeNetworks(self.api, node['nodenetwork_ids'])
157 for nodenetwork in nodenetworks:
158 if nodenetwork['is_primary']:
159 primary = nodenetwork
162 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
164 ( host, domain ) = self.split_hostname (node)
167 node['key'] = compute_key()
171 # Generate node configuration file suitable for BootCD
175 file += 'NODE_ID="%d"\n' % node['node_id']
176 file += 'NODE_KEY="%s"\n' % node['key']
179 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
181 file += 'IP_METHOD="%s"\n' % primary['method']
183 if primary['method'] == 'static':
184 file += 'IP_ADDRESS="%s"\n' % primary['ip']
185 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
186 file += 'IP_NETMASK="%s"\n' % primary['netmask']
187 file += 'IP_NETADDR="%s"\n' % primary['network']
188 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
189 file += 'IP_DNS1="%s"\n' % primary['dns1']
190 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
192 file += 'HOST_NAME="%s"\n' % host
193 file += 'DOMAIN_NAME="%s"\n' % domain
195 # define various nodenetwork settings attached to the primary nodenetwork
196 settings = NodeNetworkSettings (self.api, {'nodenetwork_id':nodenetwork['nodenetwork_id']})
199 for setting in settings:
200 if setting['category'] is not None:
201 categories.add(setting['category'])
203 for category in categories:
204 category_settings = NodeNetworkSettings(self.api,{'nodenetwork_id':nodenetwork['nodenetwork_id'],
205 'category':category})
206 if category_settings:
207 file += '### Category : %s\n'%category
208 for setting in category_settings:
209 file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
211 for nodenetwork in nodenetworks:
212 if nodenetwork['method'] == 'ipmi':
213 file += 'IPMI_ADDRESS="%s"\n' % nodenetwork['ip']
214 if nodenetwork['mac']:
215 file += 'IPMI_MAC="%s"\n' % nodenetwork['mac'].lower()
220 # see also InstallBootstrapFS in bootmanager that does similar things
221 def get_nodefamily (self, node):
223 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().split("-")
225 (pldistro,arch) = ("planetlab","i386")
228 return (pldistro,arch)
230 known_archs = [ 'i386', 'x86_64' ]
231 nodegroupnames = [ ng['name'] for ng in NodeGroups (self.api, node['nodegroup_ids'],['name'])]
232 # (1) if groupname == arch, nodefamily becomes pldistro-groupname
233 # (2) else if groupname looks like pldistro-arch, it is taken as a nodefamily
234 # (3) otherwise groupname is taken as an extension
235 for nodegroupname in nodegroupnames:
236 if nodegroupname in known_archs:
239 for known_arch in known_archs:
241 (api_pldistro,api_arch)=nodegroupname.split("-")
243 if api_arch != known_arch: raise Exception,"mismatch"
244 (pldistro,arch) = (api_pldistro, api_arch)
248 return (pldistro,arch)
250 def bootcd_version (self):
252 f = open (self.BOOTCDDIR + "/build/version.txt")
253 version=f.readline().strip()
258 def cleantrash (self):
259 for file in self.trash:
261 print 'DEBUG -- preserving',file
265 def call(self, auth, node_id_or_hostname, action, filename, options = []):
269 if action not in boot_medium_actions:
270 raise PLCInvalidArgument, "Unknown action %s"%action
272 ### compute file suffix and type
273 if action.find("-iso") >= 0 :
276 elif action.find("-usb") >= 0:
283 # handle / caconicalize options
286 raise PLCInvalidArgument, "Options are not supported for node configs"
288 # create a dict for build.sh
290 for option in options:
291 if option == "cramfs":
292 optdict['cramfs']=True
293 elif option == 'partition':
295 raise PLCInvalidArgument, "option 'partition' is for USB images only"
298 elif option == "serial":
299 optdict['serial']='default'
300 elif option.find("serial:") == 0:
301 optdict['serial']=option.replace("serial:","")
303 raise PLCInvalidArgument, "unknown option %s"%option
305 ### check node if needed
306 if action.find("node-") == 0:
307 nodes = Nodes(self.api, [node_id_or_hostname])
309 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
311 nodename = node['hostname']
315 # compute a 8 bytes random number
316 tempbytes = random.sample (xrange(0,256), 8);
317 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
318 nodename = "".join(map(hexa2,tempbytes))
321 (pldistro,arch) = self.get_nodefamily(node)
322 self.nodefamily="%s-%s"%(pldistro,arch)
324 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
325 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
328 # allow to set filename to None or any other empty value
329 if not filename: filename=''
330 filename = filename.replace ("%d",self.WORKDIR)
331 filename = filename.replace ("%n",nodename)
332 filename = filename.replace ("%s",suffix)
333 filename = filename.replace ("%p",self.api.config.PLC_NAME)
335 try: filename = filename.replace ("%f", self.nodefamily)
337 try: filename = filename.replace ("%a", arch)
339 try: filename = filename.replace ("%v",self.bootcd_version())
342 ### Check filename location
344 if 'admin' not in self.caller['roles']:
345 if ( filename.index(self.WORKDIR) != 0):
346 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
348 ### output should not exist (concurrent runs ..)
349 if os.path.exists(filename):
350 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
352 ### we can now safely create the file,
353 ### either we are admin or under a controlled location
354 filedir=os.path.dirname(filename)
355 # dirname does not return "." for a local filename like its shell counterpart
357 if not os.path.exists(filedir):
359 os.makedirs (dirname,0777)
361 raise PLCPermissionDenied, "Could not create dir %s"%dirname
365 if action == 'generic-iso' or action == 'generic-usb':
367 raise PLCInvalidArgument, "Options are not supported for generic images"
368 # this raises an exception if bootcd is missing
369 version = self.bootcd_version()
370 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
373 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
376 ret=os.system ("cp %s %s"%(generic_path,filename))
380 raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
382 ### return the generic medium content as-is, just base64 encoded
383 return base64.b64encode(file(generic_path).read())
385 ### config file preview or regenerated
386 if action == 'node-preview' or action == 'node-floppy':
387 renew_key = (action == 'node-floppy')
388 floppy = self.floppy_contents (node,renew_key)
391 file(filename,'w').write(floppy)
393 raise PLCPermissionDenied, "Could not write into %s"%filename
398 ### we're left with node-iso and node-usb
399 if action == 'node-iso' or action == 'node-usb':
401 ### check we've got required material
402 version = self.bootcd_version()
404 if not os.path.isfile(self.BOOTCDBUILD):
405 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
407 # create the workdir if needed
408 if not os.path.isdir(self.WORKDIR):
410 os.makedirs(self.WORKDIR,0777)
411 os.chmod(self.WORKDIR,0777)
413 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
416 # generate floppy config
417 floppy_text = self.floppy_contents(node,True)
419 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
421 file(floppy_file,"w").write(floppy_text)
423 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
425 self.trash.append(floppy_file)
427 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
429 # make build's arguments
431 if "cramfs" in optdict: type += "_cramfs"
432 if "serial" in optdict: serial_arg = "-s %s"%optdict['serial']
433 log_file="%s.log"%node_image
435 build_command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
442 print 'build command:',build_command
443 ret=os.system(build_command)
445 raise PLCAPIError,"bootcd/build.sh failed\n%s\n%s"%(
446 build_command,file(log_file).read())
448 self.trash.append(log_file)
449 if not os.path.isfile (node_image):
450 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
454 ret=os.system("mv %s %s"%(node_image,filename))
456 self.trash.append(node_image)
458 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
462 result = file(node_image).read()
463 self.trash.append(node_image)
465 return base64.b64encode(result)
470 # we're done here, or we missed something
471 raise PLCAPIError,'Unhandled action %s'%action