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