- support version 2 boot CDs that do not have tempfile.NamedTemporaryFile
[bootmanager.git] / source / BootServerRequest.py
1 #!/usr/bin/python2
2
3 # Copyright (c) 2003 Intel Corporation
4 # All rights reserved.
5
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions are
8 # met:
9
10 #     * Redistributions of source code must retain the above copyright
11 #       notice, this list of conditions and the following disclaimer.
12
13 #     * Redistributions in binary form must reproduce the above
14 #       copyright notice, this list of conditions and the following
15 #       disclaimer in the documentation and/or other materials provided
16 #       with the distribution.
17
18 #     * Neither the name of the Intel Corporation nor the names of its
19 #       contributors may be used to endorse or promote products derived
20 #       from this software without specific prior written permission.
21
22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE INTEL OR
26 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
27 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
28 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
29 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
30 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
31 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
34 # EXPORT LAWS: THIS LICENSE ADDS NO RESTRICTIONS TO THE EXPORT LAWS OF
35 # YOUR JURISDICTION. It is licensee's responsibility to comply with any
36 # export regulations applicable in licensee's jurisdiction. Under
37 # CURRENT (May 2000) U.S. export regulations this software is eligible
38 # for export from the U.S. and can be downloaded by or otherwise
39 # exported or reexported worldwide EXCEPT to U.S. embargoed destinations
40 # which include Cuba, Iraq, Libya, North Korea, Iran, Syria, Sudan,
41 # Afghanistan and any other country to which the U.S. has embargoed
42 # goods and services.
43
44
45 import os, sys
46 import re
47 import string
48 import urllib
49 import tempfile
50
51 # try to load pycurl
52 try:
53     import pycurl
54     PYCURL_LOADED= 1
55 except:
56     PYCURL_LOADED= 0
57
58
59 # if there is no cStringIO, fall back to the original
60 try:
61     from cStringIO import StringIO
62 except:
63     from StringIO import StringIO
64
65
66
67 class BootServerRequest:
68
69     VERBOSE = 0
70
71     # all possible places to check the cdrom mount point.
72     # /mnt/cdrom is typically after the machine has come up,
73     # and /usr is when the boot cd is running
74     CDROM_MOUNT_PATH = ("/mnt/cdrom/","/usr/")
75
76     # this is the server to contact if we don't have a bootcd
77     DEFAULT_BOOT_SERVER = "boot.planet-lab.org"
78
79     BOOTCD_VERSION_FILE = "bootme/ID"
80     BOOTCD_SERVER_FILE = "bootme/BOOTSERVER"
81     BOOTCD_SERVER_CERT_DIR = "bootme/cacert"
82     CACERT_NAME = "cacert.pem"
83     
84     # location of file containing http/https proxy info, if needed
85     PROXY_FILE = '/etc/planetlab/http_proxy'
86
87     # location of curl executable, if pycurl isn't available
88     # and the DownloadFile method is called (backup, only
89     # really need for the boot cd environment where pycurl
90     # doesn't exist
91     CURL_CMD = 'curl'
92
93     # in seconds, how maximum time allowed for connect
94     DEFAULT_CURL_CONNECT_TIMEOUT = 30
95
96     # in seconds, maximum time allowed for any transfer
97     DEFAULT_CURL_MAX_TRANSFER_TIME = 3600
98
99     CURL_SSL_VERSION = 3
100
101     HTTP_SUCCESS = 200
102
103     # proxy variables
104     USE_PROXY = 0
105     PROXY = 0
106
107     # bootcd variables
108     HAS_BOOTCD = 0
109     BOOTCD_VERSION = ""
110     BOOTSERVER_CERTS= {}
111
112     def __init__(self, verbose=0):
113
114         self.VERBOSE= verbose
115             
116         # see if we have a boot cd mounted by checking for the version file
117         # if HAS_BOOTCD == 0 then either the machine doesn't have
118         # a boot cd, or something else is mounted
119         self.HAS_BOOTCD = 0
120
121         for path in self.CDROM_MOUNT_PATH:
122             self.Message( "Checking existance of boot cd on %s" % path )
123
124             os.system("/bin/mount %s > /dev/null 2>&1" % path )
125                 
126             version_file= path + self.BOOTCD_VERSION_FILE
127             self.Message( "Looking for version file %s" % version_file )
128
129             if os.access(version_file, os.R_OK) == 0:
130                 self.Message( "No boot cd found." );
131             else:
132                 self.Message( "Found boot cd." )
133                 self.HAS_BOOTCD=1
134                 break
135
136
137         if self.HAS_BOOTCD:
138
139             # check the version of the boot cd, and locate the certs
140             self.Message( "Getting boot cd version." )
141         
142             versionRegExp= re.compile(r"PlanetLab BootCD v(\S+)")
143                 
144             bootcd_version_f= file(version_file,"r")
145             line= string.strip(bootcd_version_f.readline())
146             bootcd_version_f.close()
147             
148             match= versionRegExp.findall(line)
149             if match:
150                 (self.BOOTCD_VERSION)= match[0]
151             
152             # right now, all the versions of the bootcd are supported,
153             # so no need to check it
154             
155             # create a list of the servers we should
156             # attempt to contact, and the certs for each
157             server_list= path + self.BOOTCD_SERVER_FILE
158             self.Message( "Getting list of servers off of cd from %s." %
159                           server_list )
160             
161             bootservers_f= file(server_list,"r")
162             bootservers= bootservers_f.readlines()
163             bootservers_f.close()
164             
165             for bootserver in bootservers:
166                 bootserver = string.strip(bootserver)
167                 cacert_path= "%s/%s/%s/%s" % \
168                              (path,self.BOOTCD_SERVER_CERT_DIR,
169                               bootserver,self.CACERT_NAME)
170                 if os.access(cacert_path, os.R_OK):
171                     self.BOOTSERVER_CERTS[bootserver]= cacert_path
172
173             self.Message( "Set of servers to contact: %s" %
174                           str(self.BOOTSERVER_CERTS) )
175         else:
176             self.Message( "Using default boot server address." )
177             self.BOOTSERVER_CERTS[self.DEFAULT_BOOT_SERVER]= ""
178
179
180     def CheckProxy( self ):
181         # see if we have any proxy info from the machine
182         self.USE_PROXY= 0
183         self.Message( "Checking existance of proxy config file..." )
184         
185         if os.access(self.PROXY_FILE, os.R_OK) and \
186                os.path.isfile(self.PROXY_FILE):
187             self.PROXY= string.strip(file(self.PROXY_FILE,'r').readline())
188             self.USE_PROXY= 1
189             self.Message( "Using proxy %s." % self.PROXY )
190         else:
191             self.Message( "Not using any proxy." )
192
193
194
195     def Message( self, Msg ):
196         if( self.VERBOSE ):
197             print( Msg )
198
199
200
201     def Error( self, Msg ):
202         sys.stderr.write( Msg + "\n" )
203
204
205
206     def Warning( self, Msg ):
207         self.Error(Msg)
208
209
210
211     def MakeRequest( self, PartialPath, GetVars,
212                      PostVars, DoSSL, DoCertCheck,
213                      ConnectTimeout= DEFAULT_CURL_CONNECT_TIMEOUT,
214                      MaxTransferTime= DEFAULT_CURL_MAX_TRANSFER_TIME,
215                      FormData= None):
216
217         if hasattr(tempfile, "NamedTemporaryFile"):
218             buffer = tempfile.NamedTemporaryFile()
219             buffer_name = buffer.name
220         else:
221             buffer_name = tempfile.mktemp("MakeRequest")
222             buffer = open(buffer_name, "w+")
223
224         ok = self.DownloadFile(PartialPath, GetVars, PostVars,
225                                DoSSL, DoCertCheck, buffer_name,
226                                ConnectTimeout,
227                                MaxTransferTime,
228                                FormData)
229
230         # check the code, return the string only if it was successfull
231         if ok:
232             buffer.seek(0)
233             return buffer.read()
234         else:
235             return None
236
237     def DownloadFile(self, PartialPath, GetVars, PostVars,
238                      DoSSL, DoCertCheck, DestFilePath,
239                      ConnectTimeout= DEFAULT_CURL_CONNECT_TIMEOUT,
240                      MaxTransferTime= DEFAULT_CURL_MAX_TRANSFER_TIME,
241                      FormData= None):
242
243         self.Message( "Attempting to retrieve %s" % PartialPath )
244
245         # we can't do ssl and check the cert if we don't have a bootcd
246         if DoSSL and DoCertCheck and not self.HAS_BOOTCD:
247             self.Error( "No boot cd exists (needed to use -c and -s.\n" )
248             return 0
249
250         if DoSSL and not PYCURL_LOADED:
251             self.Warning( "Using SSL without pycurl will by default " \
252                           "check at least standard certs." )
253
254         # ConnectTimeout has to be greater than 0
255         if ConnectTimeout <= 0:
256             self.Error( "Connect timeout must be greater than zero.\n" )
257             return 0
258
259
260         self.CheckProxy()
261
262         dopostdata= 0
263
264         # setup the post and get vars for the request
265         if PostVars:
266             dopostdata= 1
267             postdata = urllib.urlencode(PostVars)
268             self.Message( "Posting data:\n%s\n" % postdata )
269             
270         getstr= ""
271         if GetVars:
272             getstr= "?" + urllib.urlencode(GetVars)
273             self.Message( "Get data:\n%s\n" % getstr )
274
275         # now, attempt to make the request, starting at the first
276         # server in the list
277         
278         for server in self.BOOTSERVER_CERTS:
279             self.Message( "Contacting server %s." % server )
280                         
281             certpath = self.BOOTSERVER_CERTS[server]
282
283             
284             # output what we are going to be doing
285             self.Message( "Connect timeout is %s seconds" % \
286                           ConnectTimeout )
287
288             self.Message( "Max transfer time is %s seconds" % \
289                           MaxTransferTime )
290
291             if DoSSL:
292                 url = "https://%s/%s%s" % (server,PartialPath,getstr)
293                 
294                 if DoCertCheck and PYCURL_LOADED:
295                     self.Message( "Using SSL version %d and verifying peer." %
296                              self.CURL_SSL_VERSION )
297                 else:
298                     self.Message( "Using SSL version %d." %
299                              self.CURL_SSL_VERSION )
300             else:
301                 url = "http://%s/%s%s" % (server,PartialPath,getstr)
302                 
303             self.Message( "URL: %s" % url )
304             
305             # setup a new pycurl instance, or a curl command line string
306             # if we don't have pycurl
307             
308             if PYCURL_LOADED:
309                 curl= pycurl.Curl()
310
311                 # don't want curl sending any signals
312                 curl.setopt(pycurl.NOSIGNAL, 1)
313             
314                 curl.setopt(pycurl.CONNECTTIMEOUT, ConnectTimeout)
315                 curl.setopt(pycurl.TIMEOUT, MaxTransferTime)
316
317                 # do not follow location when attempting to download a file
318                 curl.setopt(pycurl.FOLLOWLOCATION, 0)
319
320                 if self.USE_PROXY:
321                     curl.setopt(pycurl.PROXY, self.PROXY )
322
323                 if DoSSL:
324                     curl.setopt(pycurl.SSLVERSION, self.CURL_SSL_VERSION)
325                 
326                     if DoCertCheck:
327                         curl.setopt(pycurl.CAINFO, certpath)
328                         curl.setopt(pycurl.SSL_VERIFYPEER, 2)
329                         
330                     else:
331                         curl.setopt(pycurl.SSL_VERIFYPEER, 0)
332                 
333                 if dopostdata:
334                     curl.setopt(pycurl.POSTFIELDS, postdata)
335
336                 # setup multipart/form-data upload
337                 if FormData:
338                     curl.setopt(pycurl.HTTPPOST, FormData)
339
340                 curl.setopt(pycurl.URL, url)
341             else:
342
343                 cmdline = "%s " \
344                           "--connect-timeout %d " \
345                           "--max-time %d " \
346                           "--header Pragma: " \
347                           "--output %s " \
348                           "--fail " % \
349                           (self.CURL_CMD, ConnectTimeout,
350                            MaxTransferTime, DestFilePath)
351
352                 if dopostdata:
353                     cmdline = cmdline + "--data '" + postdata + "' "
354
355                 if FormData:
356                     cmdline = cmdline + "".join(["--form '" + field + "' " for field in FormData])
357
358                 if not self.VERBOSE:
359                     cmdline = cmdline + "--silent "
360                     
361                 if self.USE_PROXY:
362                     cmdline = cmdline + "--proxy %s " % self.PROXY
363
364                 if DoSSL:
365                     cmdline = cmdline + "--sslv%d " % self.CURL_SSL_VERSION
366
367                     if DoCertCheck:
368                         cmdline = cmdline + "--cacert %s " % certpath
369                  
370                 cmdline = cmdline + url
371
372                 self.Message( "curl command: %s" % cmdline )
373                 
374                 
375             if PYCURL_LOADED:
376                 try:
377                     # setup the output file
378                     outfile = open(DestFilePath,"wb")
379                     
380                     self.Message( "Opened output file %s" % DestFilePath )
381                 
382                     curl.setopt(pycurl.WRITEDATA, outfile)
383                 
384                     self.Message( "Fetching..." )
385                     curl.perform()
386                     self.Message( "Done." )
387                 
388                     http_result= curl.getinfo(pycurl.HTTP_CODE)
389                     curl.close()
390                 
391                     outfile.close()
392                     self.Message( "Results saved in %s" % DestFilePath )
393
394                     # check the code, return 1 if successfull
395                     if http_result == self.HTTP_SUCCESS:
396                         self.Message( "Successfull!" )
397                         return 1
398                     else:
399                         self.Message( "Failure, resultant http code: %d" % \
400                                       http_result )
401
402                 except pycurl.error, err:
403                     errno, errstr= err
404                     self.Error( "connect to %s failed; curl error %d: '%s'\n" %
405                        (server,errno,errstr) )
406         
407                 if not outfile.closed:
408                     try:
409                         os.unlink(DestFilePath)
410                         outfile.close()
411                     except OSError:
412                         pass
413
414             else:
415                 self.Message( "Fetching..." )
416                 rc = os.system(cmdline)
417                 self.Message( "Done." )
418                 
419                 if rc != 0:
420                     try:
421                         os.unlink( DestFilePath )
422                     except OSError:
423                         pass
424                     self.Message( "Failure, resultant curl code: %d" % rc )
425                     self.Message( "Removed %s" % DestFilePath )
426                 else:
427                     self.Message( "Successfull!" )
428                     return 1
429             
430         self.Error( "Unable to successfully contact any boot servers.\n" )
431         return 0
432
433
434
435
436 def usage():
437     print(
438     """
439 Usage: BootServerRequest.py [options] <partialpath>
440 Options:
441  -c/--checkcert        Check SSL certs. Ignored if -s/--ssl missing.
442  -h/--help             This help text
443  -o/--output <file>    Write result to file
444  -s/--ssl              Make the request over HTTPS
445  -v                    Makes the operation more talkative
446 """);  
447
448
449
450 if __name__ == "__main__":
451     import getopt
452     
453     # check out our command line options
454     try:
455         opt_list, arg_list = getopt.getopt(sys.argv[1:],
456                                            "o:vhsc",
457                                            [ "output=", "verbose", \
458                                              "help","ssl","checkcert"])
459
460         ssl= 0
461         checkcert= 0
462         output_file= None
463         verbose= 0
464         
465         for opt, arg in opt_list:
466             if opt in ("-h","--help"):
467                 usage(0)
468                 sys.exit()
469             
470             if opt in ("-c","--checkcert"):
471                 checkcert= 1
472             
473             if opt in ("-s","--ssl"):
474                 ssl= 1
475
476             if opt in ("-o","--output"):
477                 output_file= arg
478
479             if opt == "-v":
480                 verbose= 1
481     
482         if len(arg_list) != 1:
483             raise Exception
484
485         partialpath= arg_list[0]
486         if string.lower(partialpath[:4]) == "http":
487             raise Exception
488
489     except:
490         usage()
491         sys.exit(2)
492
493     # got the command line args straightened out
494     requestor= BootServerRequest(verbose)
495         
496     if output_file:
497         requestor.DownloadFile( partialpath, None, None, ssl,
498                                 checkcert, output_file)
499     else:
500         result= requestor.MakeRequest( partialpath, None, None, ssl, checkcert)
501         if result:
502             print result
503         else:
504             sys.exit(1)