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>'
97 console_spec (or 'default') is passed as-is to bootcd/build.sh
98 it is expected to be a colon separated string denoting
99 tty - baudrate - parity - bits
100 e.g. ttyS0:115200:n:8
103 - Non-admins can only generate files for nodes at their sites.
104 - Non-admins, when they provide a filename, *must* specify it in the %d area
107 Whenever needed, the method stores intermediate files in a
108 private area, typically not located under the web server's
109 accessible area, and are cleaned up by the method.
113 roles = ['admin', 'pi', 'tech']
117 Mixed(Node.fields['node_id'],
118 Node.fields['hostname']),
119 Parameter (str, "Action mode, expected in " + "|".join(boot_medium_actions)),
120 Parameter (str, "Empty string for verbatim result, resulting file full path otherwise"),
121 Parameter ([str], "Options"),
124 returns = Parameter(str, "Node boot medium, either inlined, or filename, depending on the filename parameter")
126 BOOTCDDIR = "/usr/share/bootcd-@NODEFAMILY@/"
127 BOOTCDBUILD = "/usr/share/bootcd-@NODEFAMILY@/build.sh"
128 GENERICDIR = "/var/www/html/download-@NODEFAMILY@/"
129 WORKDIR = "/var/tmp/bootmedium"
131 # uncomment this to preserve temporary area and bootcustom logs
134 ### returns (host, domain) :
135 # 'host' : host part of the hostname
136 # 'domain' : domain part of the hostname
137 def split_hostname (self, node):
138 # Split hostname into host and domain parts
139 parts = node['hostname'].split(".", 1)
141 raise PLCInvalidArgument, "Node hostname %s is invalid"%node['hostname']
145 def floppy_contents (self, node, renew_key):
147 if node['peer_id'] is not None:
148 raise PLCInvalidArgument, "Not a local node"
150 # If we are not an admin, make sure that the caller is a
151 # member of the site at which the node is located.
152 if 'admin' not in self.caller['roles']:
153 if node['site_id'] not in self.caller['site_ids']:
154 raise PLCPermissionDenied, "Not allowed to generate a configuration file for %s"%node['hostname']
156 # Get node networks for this node
158 interfaces = Interfaces(self.api, node['interface_ids'])
159 for interface in interfaces:
160 if interface['is_primary']:
164 raise PLCInvalidArgument, "No primary network configured on %s"%node['hostname']
166 ( host, domain ) = self.split_hostname (node)
169 node['key'] = compute_key()
173 # Generate node configuration file suitable for BootCD
177 file += 'NODE_ID="%d"\n' % node['node_id']
178 file += 'NODE_KEY="%s"\n' % node['key']
179 # not used anywhere, just a note for operations people
180 file += 'KEY_RENEWAL_DATE="%s"\n' % time.strftime('%Y/%m/%d at %H:%M +0000',time.gmtime())
183 file += 'NET_DEVICE="%s"\n' % primary['mac'].lower()
185 file += 'IP_METHOD="%s"\n' % primary['method']
187 if primary['method'] == 'static':
188 file += 'IP_ADDRESS="%s"\n' % primary['ip']
189 file += 'IP_GATEWAY="%s"\n' % primary['gateway']
190 file += 'IP_NETMASK="%s"\n' % primary['netmask']
191 file += 'IP_NETADDR="%s"\n' % primary['network']
192 file += 'IP_BROADCASTADDR="%s"\n' % primary['broadcast']
193 file += 'IP_DNS1="%s"\n' % primary['dns1']
194 file += 'IP_DNS2="%s"\n' % (primary['dns2'] or "")
196 file += 'HOST_NAME="%s"\n' % host
197 file += 'DOMAIN_NAME="%s"\n' % domain
199 # define various interface settings attached to the primary interface
200 settings = InterfaceSettings (self.api, {'interface_id':interface['interface_id']})
203 for setting in settings:
204 if setting['category'] is not None:
205 categories.add(setting['category'])
207 for category in categories:
208 category_settings = InterfaceSettings(self.api,{'interface_id':interface['interface_id'],
209 'category':category})
210 if category_settings:
211 file += '### Category : %s\n'%category
212 for setting in category_settings:
213 file += '%s_%s="%s"\n'%(category.upper(),setting['name'].upper(),setting['value'])
215 for interface in interfaces:
216 if interface['method'] == 'ipmi':
217 file += 'IPMI_ADDRESS="%s"\n' % interface['ip']
219 file += 'IPMI_MAC="%s"\n' % interface['mac'].lower()
224 # see also InstallBootstrapFS in bootmanager that does similar things
225 def get_nodefamily (self, node):
226 # get defaults from the myplc build
228 (pldistro,arch) = file("/etc/planetlab/nodefamily").read().strip().split("-")
230 (pldistro,arch) = ("planetlab","i386")
232 # with no valid argument, return system-wide defaults
234 return (pldistro,arch)
236 node_id=node['node_id']
237 # cannot use accessors in the API itself
238 # the 'arch' tag type is assumed to exist, see db-config
239 arch_tags = NodeTags (self.api, {'tagname':'arch','node_id':node_id},['tagvalue'])
241 arch=arch_tags[0]['tagvalue']
243 pldistro_tags = NodeTags (self.api, {'tagname':'pldistro','node_id':node_id},['tagvalue'])
245 pldistro=pldistro_tags[0]['tagvalue']
247 return (pldistro,arch)
249 def bootcd_version (self):
251 return file(self.BOOTCDDIR + "/build/version.txt").readline().strip()
253 raise Exception,"Unknown boot cd version - probably wrong bootcd dir : %s"%self.BOOTCDDIR
255 def cleantrash (self):
256 for file in self.trash:
258 print 'DEBUG -- preserving',file
262 def call(self, auth, node_id_or_hostname, action, filename, options = []):
266 if action not in boot_medium_actions:
267 raise PLCInvalidArgument, "Unknown action %s"%action
269 ### compute file suffix and type
270 if action.find("-iso") >= 0 :
273 elif action.find("-usb") >= 0:
280 # handle / caconicalize options
283 raise PLCInvalidArgument, "Options are not supported for node configs"
285 # create a dict for build.sh
286 build_sh_spec={'kargs':[]}
287 for option in options:
288 if option == "cramfs":
289 build_sh_spec['cramfs']=True
290 elif option == 'partition':
292 raise PLCInvalidArgument, "option 'partition' is for USB images only"
295 elif option == "serial":
296 build_sh_spec['serial']='default'
297 elif option.find("serial:") == 0:
298 build_sh_spec['serial']=option.replace("serial:","")
299 elif option == "no-hangcheck":
300 build_sh_spec['kargs'].append('hcheck_reboot=0')
301 build_sh_spec['kargs'].append('debug')
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 (filedir,0777)
361 raise PLCPermissionDenied, "Could not create dir %s"%filedir
366 self.message='GetBootMedium on node %s - action=%s'%(nodename,action)
367 self.event_objects={'Node': [ node ['node_id'] ]}
369 self.message='GetBootMedium - generic - action=%s'%action
372 if action == 'generic-iso' or action == 'generic-usb':
374 raise PLCInvalidArgument, "Options are not supported for generic images"
375 # this raises an exception if bootcd is missing
376 version = self.bootcd_version()
377 generic_name = "%s-BootCD-%s%s"%(self.api.config.PLC_NAME,
380 generic_path = "%s/%s" % (self.GENERICDIR,generic_name)
383 ret=os.system ("cp %s %s"%(generic_path,filename))
387 raise PLCPermissionDenied, "Could not copy %s into"%(generic_path,filename)
389 ### return the generic medium content as-is, just base64 encoded
390 return base64.b64encode(file(generic_path).read())
392 ### config file preview or regenerated
393 if action == 'node-preview' or action == 'node-floppy':
394 renew_key = (action == 'node-floppy')
395 floppy = self.floppy_contents (node,renew_key)
398 file(filename,'w').write(floppy)
400 raise PLCPermissionDenied, "Could not write into %s"%filename
405 ### we're left with node-iso and node-usb
406 if action == 'node-iso' or action == 'node-usb':
408 ### check we've got required material
409 version = self.bootcd_version()
411 if not os.path.isfile(self.BOOTCDBUILD):
412 raise PLCAPIError, "Cannot locate bootcd/build.sh script %s"%self.BOOTCDBUILD
414 # create the workdir if needed
415 if not os.path.isdir(self.WORKDIR):
417 os.makedirs(self.WORKDIR,0777)
418 os.chmod(self.WORKDIR,0777)
420 raise PLCPermissionDenied, "Could not create dir %s"%self.WORKDIR
423 # generate floppy config
424 floppy_text = self.floppy_contents(node,True)
426 floppy_file = "%s/%s.txt"%(self.WORKDIR,nodename)
428 file(floppy_file,"w").write(floppy_text)
430 raise PLCPermissionDenied, "Could not write into %s"%floppy_file
432 self.trash.append(floppy_file)
434 node_image = "%s/%s%s"%(self.WORKDIR,nodename,suffix)
436 # make build's arguments
438 if "cramfs" in build_sh_spec:
440 if "serial" in build_sh_spec:
441 build_sh_options += " -s %s"%build_sh_spec['serial']
443 for karg in build_sh_spec['kargs']:
444 build_sh_options += ' -k "%s"'%karg
446 log_file="%s.log"%node_image
448 build_command = '%s -f "%s" -o "%s" -t "%s" %s &> %s' % (self.BOOTCDBUILD,
455 print 'build command:',build_command
456 ret=os.system(build_command)
458 raise PLCAPIError,"bootcd/build.sh failed\n%s\n%s"%(
459 build_command,file(log_file).read())
461 self.trash.append(log_file)
462 if not os.path.isfile (node_image):
463 raise PLCAPIError,"Unexpected location of build.sh output - %s"%node_image
467 ret=os.system("mv %s %s"%(node_image,filename))
469 self.trash.append(node_image)
471 raise PLCAPIError, "Could not move node image %s into %s"%(node_image,filename)
475 result = file(node_image).read()
476 self.trash.append(node_image)
478 return base64.b64encode(result)
483 # we're done here, or we missed something
484 raise PLCAPIError,'Unhandled action %s'%action