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 # not used anywhere, just a note for operations people
179 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y-%m-%d at %H:%M:%S +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 = InterfaceSettings (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 = InterfaceSettings(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']
236 # cannot use accessors in the API itself
237 # the 'arch' tag type is assumed to exist, see db-config
238 arch_tags = NodeTags (self.api, {'tagname':'arch','node_id':node_id},['tagvalue'])
240 arch=arch_tags[0]['tagvalue']
242 pldistro_tags = NodeTags (self.api, {'tagname':'pldistro','node_id':node_id},['tagvalue'])
244 pldistro=pldistro_tags[0]['tagvalue']
246 return (pldistro,arch)
248 def bootcd_version (self):
250 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
252 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
254 def cleantrash (self):
255 for file in self.trash:
257 print 'DEBUG -- preserving',file
261 def call(self, auth, node_id_or_hostname, action, filename, options = []):
265 if action not in boot_medium_actions:
266 raise PLCInvalidArgument, "Unknown action %s"%action
268 ### compute file suffix and type
269 if action.find("-iso") >= 0 :
272 elif action.find("-usb") >= 0:
279 # handle / caconicalize options
282 raise PLCInvalidArgument, "Options are not supported for node configs"
284 # create a dict for build.sh
286 for option in options:
287 if option == "cramfs":
288 optdict['cramfs']=True
289 elif option == 'partition':
291 raise PLCInvalidArgument, "option 'partition' is for USB images only"
294 elif option == "serial":
295 optdict['serial']='default'
296 elif option.find("serial:") == 0:
297 optdict['serial']=option.replace("serial:","")
299 raise PLCInvalidArgument, "unknown option %s"%option
301 ### check node if needed
302 if action.find("node-") == 0:
303 nodes = Nodes(self.api, [node_id_or_hostname])
305 raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname
307 nodename = node['hostname']
311 # compute a 8 bytes random number
312 tempbytes = random.sample (xrange(0,256), 8);
313 def hexa2 (c): return chr((c>>4)+65) + chr ((c&16)+65)
314 nodename = "".join(map(hexa2,tempbytes))
317 (pldistro,arch) = self.get_nodefamily(node)
318 self.nodefamily="%s-%s"%(pldistro,arch)
320 for attr in [ "BOOTCDDIR", "BOOTCDBUILD", "GENERICDIR" ]:
321 setattr(self,attr,getattr(self,attr).replace("@NODEFAMILY@",self.nodefamily))
324 # allow to set filename to None or any other empty value
325 if not filename: filename=''
326 filename = filename.replace ("%d",self.WORKDIR)
327 filename = filename.replace ("%n",nodename)
328 filename = filename.replace ("%s",suffix)
329 filename = filename.replace ("%p",self.api.config.PLC_NAME)
331 try: filename = filename.replace ("%f", self.nodefamily)
333 try: filename = filename.replace ("%a", arch)
335 try: filename = filename.replace ("%v",self.bootcd_version())
338 ### Check filename location
340 if 'admin' not in self.caller['roles']:
341 if ( filename.index(self.WORKDIR) != 0):
342 raise PLCInvalidArgument, "File %s not under %s"%(filename,self.WORKDIR)
344 ### output should not exist (concurrent runs ..)
345 if os.path.exists(filename):
346 raise PLCInvalidArgument, "Resulting file %s already exists"%filename
348 ### we can now safely create the file,
349 ### either we are admin or under a controlled location
350 filedir=os.path.dirname(filename)
351 # dirname does not return "." for a local filename like its shell counterpart
353 if not os.path.exists(filedir):
355 os.makedirs (filedir,0777)
357 raise PLCPermissionDenied, "Could not create dir %s"%filedir
361 if action == 'generic-iso' or action == 'generic-usb':
363 raise PLCInvalidArgument, "Options are not supported for generic images"
364 # this raises an exception if bootcd is missing
365 version = self.bootcd_version()
366 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
369 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
372 ret=os.system ("cp %s %s"%(generic_path,filename))
376 raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
378 ### return the generic medium content as-is, just base64 encoded
379 return base64.b64encode(file(generic_path).read())
381 ### config file preview or regenerated
382 if action == 'node-preview' or action == 'node-floppy':
383 renew_key = (action == 'node-floppy')
384 floppy = self.floppy_contents (node,renew_key)
387 file(filename,'w').write(floppy)
389 raise PLCPermissionDenied, "Could not write into %s"%filename
394 ### we're left with node-iso and node-usb
395 if action == 'node-iso' or action == 'node-usb':
397 ### check we've got required material
398 version = self.bootcd_version()
400 if not os.path.isfile(self.BOOTCDBUILD):
401 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
403 # create the workdir if needed
404 if not os.path.isdir(self.WORKDIR):
406 os.makedirs(self.WORKDIR,0777)
407 os.chmod(self.WORKDIR,0777)
409 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
412 # generate floppy config
413 floppy_text = self.floppy_contents(node,True)
415 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
417 file(floppy_file,"w").write(floppy_text)
419 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
421 self.trash.append(floppy_file)
423 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
425 # make build's arguments
427 if "cramfs" in optdict: type += "_cramfs"
428 if "serial" in optdict: serial_arg = "-s %s"%optdict['serial']
429 log_file="%s.log"%node_image
431 build_command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
438 print 'build command:',build_command
439 ret=os.system(build_command)
441 raise PLCAPIError,"bootcd/build.sh failed\n%s\n%s"%(
442 build_command,file(log_file).read())
444 self.trash.append(log_file)
445 if not os.path.isfile (node_image):
446 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
450 ret=os.system("mv %s %s"%(node_image,filename))
452 self.trash.append(node_image)
454 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
458 result = file(node_image).read()
459 self.trash.append(node_image)
461 return base64.b64encode(result)
466 # we're done here, or we missed something
467 raise PLCAPIError,'Unhandled action %s'%action