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