Copyright notice for both Intel and Princeton
[bootmanager.git] / source / BootServerRequest.py
1 #!/usr/bin/python2
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         if hasattr(tempfile, "NamedTemporaryFile"):
182             buffer = tempfile.NamedTemporaryFile()
183             buffer_name = buffer.name
184         else:
185             buffer_name = tempfile.mktemp("MakeRequest")
186             buffer = open(buffer_name, "w+")
187
188         ok = self.DownloadFile(PartialPath, GetVars, PostVars,
189                                DoSSL, DoCertCheck, buffer_name,
190                                ConnectTimeout,
191                                MaxTransferTime,
192                                FormData)
193
194         # check the code, return the string only if it was successfull
195         if ok:
196             buffer.seek(0)
197             return buffer.read()
198         else:
199             return None
200
201     def DownloadFile(self, PartialPath, GetVars, PostVars,
202                      DoSSL, DoCertCheck, DestFilePath,
203                      ConnectTimeout= DEFAULT_CURL_CONNECT_TIMEOUT,
204                      MaxTransferTime= DEFAULT_CURL_MAX_TRANSFER_TIME,
205                      FormData= None):
206
207         self.Message( "Attempting to retrieve %s" % PartialPath )
208
209         # we can't do ssl and check the cert if we don't have a bootcd
210         if DoSSL and DoCertCheck and not self.HAS_BOOTCD:
211             self.Error( "No boot cd exists (needed to use -c and -s.\n" )
212             return 0
213
214         if DoSSL and not PYCURL_LOADED:
215             self.Warning( "Using SSL without pycurl will by default " \
216                           "check at least standard certs." )
217
218         # ConnectTimeout has to be greater than 0
219         if ConnectTimeout <= 0:
220             self.Error( "Connect timeout must be greater than zero.\n" )
221             return 0
222
223
224         self.CheckProxy()
225
226         dopostdata= 0
227
228         # setup the post and get vars for the request
229         if PostVars:
230             dopostdata= 1
231             postdata = urllib.urlencode(PostVars)
232             self.Message( "Posting data:\n%s\n" % postdata )
233             
234         getstr= ""
235         if GetVars:
236             getstr= "?" + urllib.urlencode(GetVars)
237             self.Message( "Get data:\n%s\n" % getstr )
238
239         # now, attempt to make the request, starting at the first
240         # server in the list
241         
242         for server in self.BOOTSERVER_CERTS:
243             self.Message( "Contacting server %s." % server )
244                         
245             certpath = self.BOOTSERVER_CERTS[server]
246
247             
248             # output what we are going to be doing
249             self.Message( "Connect timeout is %s seconds" % \
250                           ConnectTimeout )
251
252             self.Message( "Max transfer time is %s seconds" % \
253                           MaxTransferTime )
254
255             if DoSSL:
256                 url = "https://%s/%s%s" % (server,PartialPath,getstr)
257                 
258                 if DoCertCheck and PYCURL_LOADED:
259                     self.Message( "Using SSL version %d and verifying peer." %
260                              self.CURL_SSL_VERSION )
261                 else:
262                     self.Message( "Using SSL version %d." %
263                              self.CURL_SSL_VERSION )
264             else:
265                 url = "http://%s/%s%s" % (server,PartialPath,getstr)
266                 
267             self.Message( "URL: %s" % url )
268             
269             # setup a new pycurl instance, or a curl command line string
270             # if we don't have pycurl
271             
272             if PYCURL_LOADED:
273                 curl= pycurl.Curl()
274
275                 # don't want curl sending any signals
276                 curl.setopt(pycurl.NOSIGNAL, 1)
277             
278                 curl.setopt(pycurl.CONNECTTIMEOUT, ConnectTimeout)
279                 curl.setopt(pycurl.TIMEOUT, MaxTransferTime)
280
281                 # do not follow location when attempting to download a file
282                 curl.setopt(pycurl.FOLLOWLOCATION, 0)
283
284                 if self.USE_PROXY:
285                     curl.setopt(pycurl.PROXY, self.PROXY )
286
287                 if DoSSL:
288                     curl.setopt(pycurl.SSLVERSION, self.CURL_SSL_VERSION)
289                 
290                     if DoCertCheck:
291                         curl.setopt(pycurl.CAINFO, certpath)
292                         curl.setopt(pycurl.SSL_VERIFYPEER, 2)
293                         
294                     else:
295                         curl.setopt(pycurl.SSL_VERIFYPEER, 0)
296                 
297                 if dopostdata:
298                     curl.setopt(pycurl.POSTFIELDS, postdata)
299
300                 # setup multipart/form-data upload
301                 if FormData:
302                     curl.setopt(pycurl.HTTPPOST, FormData)
303
304                 curl.setopt(pycurl.URL, url)
305             else:
306
307                 cmdline = "%s " \
308                           "--connect-timeout %d " \
309                           "--max-time %d " \
310                           "--header Pragma: " \
311                           "--output %s " \
312                           "--fail " % \
313                           (self.CURL_CMD, ConnectTimeout,
314                            MaxTransferTime, DestFilePath)
315
316                 if dopostdata:
317                     cmdline = cmdline + "--data '" + postdata + "' "
318
319                 if FormData:
320                     cmdline = cmdline + "".join(["--form '" + field + "' " for field in FormData])
321
322                 if not self.VERBOSE:
323                     cmdline = cmdline + "--silent "
324                     
325                 if self.USE_PROXY:
326                     cmdline = cmdline + "--proxy %s " % self.PROXY
327
328                 if DoSSL:
329                     cmdline = cmdline + "--sslv%d " % self.CURL_SSL_VERSION
330
331                     if DoCertCheck:
332                         cmdline = cmdline + "--cacert %s " % certpath
333                  
334                 cmdline = cmdline + url
335
336                 self.Message( "curl command: %s" % cmdline )
337                 
338                 
339             if PYCURL_LOADED:
340                 try:
341                     # setup the output file
342                     outfile = open(DestFilePath,"wb")
343                     
344                     self.Message( "Opened output file %s" % DestFilePath )
345                 
346                     curl.setopt(pycurl.WRITEDATA, outfile)
347                 
348                     self.Message( "Fetching..." )
349                     curl.perform()
350                     self.Message( "Done." )
351                 
352                     http_result= curl.getinfo(pycurl.HTTP_CODE)
353                     curl.close()
354                 
355                     outfile.close()
356                     self.Message( "Results saved in %s" % DestFilePath )
357
358                     # check the code, return 1 if successfull
359                     if http_result == self.HTTP_SUCCESS:
360                         self.Message( "Successfull!" )
361                         return 1
362                     else:
363                         self.Message( "Failure, resultant http code: %d" % \
364                                       http_result )
365
366                 except pycurl.error, err:
367                     errno, errstr= err
368                     self.Error( "connect to %s failed; curl error %d: '%s'\n" %
369                        (server,errno,errstr) )
370         
371                 if not outfile.closed:
372                     try:
373                         os.unlink(DestFilePath)
374                         outfile.close()
375                     except OSError:
376                         pass
377
378             else:
379                 self.Message( "Fetching..." )
380                 rc = os.system(cmdline)
381                 self.Message( "Done." )
382                 
383                 if rc != 0:
384                     try:
385                         os.unlink( DestFilePath )
386                     except OSError:
387                         pass
388                     self.Message( "Failure, resultant curl code: %d" % rc )
389                     self.Message( "Removed %s" % DestFilePath )
390                 else:
391                     self.Message( "Successfull!" )
392                     return 1
393             
394         self.Error( "Unable to successfully contact any boot servers.\n" )
395         return 0
396
397
398
399
400 def usage():
401     print(
402     """
403 Usage: BootServerRequest.py [options] <partialpath>
404 Options:
405  -c/--checkcert        Check SSL certs. Ignored if -s/--ssl missing.
406  -h/--help             This help text
407  -o/--output <file>    Write result to file
408  -s/--ssl              Make the request over HTTPS
409  -v                    Makes the operation more talkative
410 """);  
411
412
413
414 if __name__ == "__main__":
415     import getopt
416     
417     # check out our command line options
418     try:
419         opt_list, arg_list = getopt.getopt(sys.argv[1:],
420                                            "o:vhsc",
421                                            [ "output=", "verbose", \
422                                              "help","ssl","checkcert"])
423
424         ssl= 0
425         checkcert= 0
426         output_file= None
427         verbose= 0
428         
429         for opt, arg in opt_list:
430             if opt in ("-h","--help"):
431                 usage(0)
432                 sys.exit()
433             
434             if opt in ("-c","--checkcert"):
435                 checkcert= 1
436             
437             if opt in ("-s","--ssl"):
438                 ssl= 1
439
440             if opt in ("-o","--output"):
441                 output_file= arg
442
443             if opt == "-v":
444                 verbose= 1
445     
446         if len(arg_list) != 1:
447             raise Exception
448
449         partialpath= arg_list[0]
450         if string.lower(partialpath[:4]) == "http":
451             raise Exception
452
453     except:
454         usage()
455         sys.exit(2)
456
457     # got the command line args straightened out
458     requestor= BootServerRequest(verbose)
459         
460     if output_file:
461         requestor.DownloadFile( partialpath, None, None, ssl,
462                                 checkcert, output_file)
463     else:
464         result= requestor.MakeRequest( partialpath, None, None, ssl, checkcert)
465         if result:
466             print result
467         else:
468             sys.exit(1)