Merge branch 'master' of ssh://git.planet-lab.org/git/sfa
[sfa.git] / sfa / client / sfi.py
index d4b095c..4ff43ac 100644 (file)
@@ -1,5 +1,8 @@
-
-# xxx NOTE this will soon be reviewed to take advantage of sfaclientlib
+# 
+# sfi.py - basic SFA command-line client
+# the actual binary in sfa/clientbin essentially runs main()
+# this module is used in sfascan
+# 
 
 import sys
 sys.path.append('.')
@@ -12,6 +15,7 @@ import pickle
 from lxml import etree
 from StringIO import StringIO
 from optparse import OptionParser
+from pprint import PrettyPrinter
 
 from sfa.trust.certificate import Keypair, Certificate
 from sfa.trust.gid import GID
@@ -161,7 +165,8 @@ def unique_call_id(): return uuid.uuid4().urn
 
 class Sfi:
     
-    required_options=['verbose',  'debug',  'registry',  'sm',  'auth',  'user']
+    # dirty hack to make this class usable from the outside
+    required_options=['verbose',  'debug',  'registry',  'sm',  'auth',  'user', 'user_private_key']
 
     @staticmethod
     def default_sfi_dir ():
@@ -251,8 +256,6 @@ class Sfi:
         # user specifies remote aggregate/sm/component                          
         if command in ("resources", "slices", "create", "delete", "start", "stop", 
                        "restart", "shutdown",  "get_ticket", "renew", "status"):
-            parser.add_option("-c", "--component", dest="component", default=None,
-                             help="component hrn")
             parser.add_option("-d", "--delegate", dest="delegate", default=None, 
                              action="store_true",
                              help="Include a credential delegated to the user's root"+\
@@ -264,10 +267,15 @@ class Sfi:
                             help="type filter ([all]|user|slice|authority|node|aggregate)",
                             choices=("all", "user", "slice", "authority", "node", "aggregate"),
                             default="all")
-        # display formats
         if command in ("resources"):
+            # rspec version
             parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
                               help="schema type and version of resulting RSpec")
+            # disable/enable cached rspecs
+            parser.add_option("-c", "--current", dest="current", default=False,
+                              action="store_true",  
+                              help="Request the current rspec bypassing the cache. Cached rspecs are returned by default")
+            # display formats
             parser.add_option("-f", "--format", dest="format", type="choice",
                              help="display format ([xml]|dns|ip)", default="xml",
                              choices=("xml", "dns", "ip"))
@@ -336,8 +344,6 @@ class Sfi:
         parser.add_option("-D", "--debug",
                           action="store_true", dest="debug", default=False,
                           help="Debug (xml-rpc) protocol messages")
-        parser.add_option("-p", "--protocol", dest="protocol", default="xmlrpc",
-                         help="RPC protocol (xmlrpc or soap)")
         # would it make sense to use ~/.ssh/id_rsa as a default here ?
         parser.add_option("-k", "--private-key",
                          action="store", dest="user_private_key", default=None,
@@ -549,6 +555,8 @@ class Sfi:
                 self.sliceapi_proxy=SfaServerProxy(cm_url, self.private_key, self.my_gid)
             else:
                 # otherwise use what was provided as --sliceapi, or SFI_SM in the config
+                if not self.sm_url.startswith('http://') or self.sm_url.startswith('https://'):
+                    self.sm_url = 'http://' + self.sm_url
                 self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
                 self.sliceapi_proxy = SfaServerProxy(self.sm_url, self.private_key, self.my_gid, 
                                                      timeout=self.options.timeout, verbose=self.options.debug)  
@@ -572,8 +580,8 @@ class Sfi:
         if not version: 
             result = server.GetVersion()
             version= ReturnValue.get_value(result)
-            # cache version for 24 hours
-            cache.add(cache_key, version, ttl= 60*60*24)
+            # cache version for 20 minutes
+            cache.add(cache_key, version, ttl= 60*20)
             self.logger.info("Updating cache file %s" % cache_file)
             cache.save_to_file(cache_file)
 
@@ -585,24 +593,41 @@ class Sfi:
         Returns true if server support the optional call_id arg, false otherwise. 
         """
         server_version = self.get_cached_server_version(server)
-        return True
+        result = False
         # xxx need to rewrite this 
+        if int(server_version.get('geni_api')) >= 2:
+            result = True
+        return result
+
+    def server_supports_call_id_arg(self, server):
+        server_version = self.get_cached_server_version(server)
+        result = False      
         if 'sfa' in server_version and 'code_tag' in server_version:
             code_tag = server_version['code_tag']
             code_tag_parts = code_tag.split("-")
-            
             version_parts = code_tag_parts[0].split(".")
             major, minor = version_parts[0], version_parts[1]
             rev = code_tag_parts[1]
-            if int(major) >= 1:
-                if int(minor) >= 2:
-                    return True
-        return False                
+            if int(major) == 1 and minor == 0 and build >= 22:
+                result = True
+        return result                 
 
     ### ois = options if supported
+    # to be used in something like serverproxy.Method (arg1, arg2, *self.ois(api_options))
     def ois (self, server, option_dict):
-        if self.server_supports_options_arg (server) : return [option_dict]
-        else: return []
+        if self.server_supports_options_arg (server): 
+            return [option_dict]
+        elif self.server_supports_call_id_arg (server):
+            return [ unique_call_id () ]
+        else: 
+            return []
+
+    ### cis = call_id if supported - like ois
+    def cis (self, server):
+        if self.server_supports_call_id_arg (server):
+            return [ unique_call_id ]
+        else:
+            return []
 
     ######################################## miscell utilities
     def get_rspec_file(self, rspec):
@@ -648,8 +673,8 @@ or version information about sfi itself
                 server = self.sliceapi()
             result = server.GetVersion()
             version = ReturnValue.get_value(result)
-        for (k,v) in version.iteritems():
-            print "%-20s: %s"%(k,v)
+        pprinter = PrettyPrinter(indent=4)
+        pprinter.pprint(version)
         if options.file:
             save_variable_to_file(version, options.file, options.fileformat)
 
@@ -766,13 +791,15 @@ or version information about sfi itself
 
     def slices(self, options, args):
         "list instantiated slices (ListSlices) - returns urn's"
+        server = self.sliceapi()
+        # creds
         creds = [self.my_credential_string]
         if options.delegate:
             delegated_cred = self.delegate_cred(self.my_credential_string, get_authority(self.authority))
             creds.append(delegated_cred)  
+        # options and call_id when supported
         api_options = {}
-        api_options ['call_id'] = unique_call_id()
-        server = self.sliceapi()
+       api_options['call_id']=unique_call_id()
         result = server.ListSlices(creds, *self.ois(server,api_options))
         value = ReturnValue.get_value(result)
         display_list(value)
@@ -781,41 +808,48 @@ or version information about sfi itself
     # show rspec for named slice
     def resources(self, options, args):
         """
-        with no arg, discover available resources,
-or currently provisioned resources  (ListResources)
+        with no arg, discover available resources, (ListResources)
+or with an slice hrn, shows currently provisioned resources
         """
+        server = self.sliceapi()
+
+        # set creds
+        creds = []
+        if args:
+            creds.append(self.slice_credential_string(args[0]))
+        else:
+            creds.append(self.my_credential_string)
+        if options.delegate:
+            creds.append(self.delegate_cred(cred, get_authority(self.authority)))
+       
+        # no need to check if server accepts the options argument since the options has
+        # been a required argument since v1 API   
         api_options = {}
+        # always send call_id to v2 servers
         api_options ['call_id'] = unique_call_id()
-        #panos add info api_options
-        if options.info:
-            api_options['info'] = options.info
-        
+        # ask for cached value if available
+        api_options ['cached'] = True
         if args:
-            cred = self.slice_credential_string(args[0])
             hrn = args[0]
             api_options['geni_slice_urn'] = hrn_to_urn(hrn, 'slice')
-        else:
-            cred = self.my_credential_string
-     
-        server = self.sliceapi()
-        creds = [cred]
-        if options.delegate:
-            delegated_cred = self.delegate_cred(cred, get_authority(self.authority))
-            creds.append(delegated_cred)
+        if options.info:
+            api_options['info'] = options.info
+        if options.current:
+            if options.current == True:
+                api_options['cached'] = False
+            else:
+                api_options['cached'] = True
         if options.rspec_version:
             version_manager = VersionManager()
             server_version = self.get_cached_server_version(server)
             if 'sfa' in server_version:
-                # just request the version the client wants 
+                # just request the version the client wants
                 api_options['geni_rspec_version'] = version_manager.get_version(options.rspec_version).to_dict()
             else:
-                # this must be a protogeni aggregate. We should request a v2 ad rspec
-                # regardless of what the client user requested 
-                api_options['geni_rspec_version'] = version_manager.get_version('ProtoGENI 2').to_dict()     
+                api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}    
         else:
-            api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}
-
-        result = server.ListResources(creds, *self.ois(server,api_options))
+            api_options['geni_rspec_version'] = {'type': 'geni', 'version': '3.0'}    
+        result = server.ListResources (creds, api_options)
         value = ReturnValue.get_value(result)
         if options.file is None:
             display_rspec(value, options.format)
@@ -828,11 +862,16 @@ or currently provisioned resources  (ListResources)
         create or update named slice with given rspec
         """
         server = self.sliceapi()
-        server_version = self.get_cached_server_version(server)
+
+        # xxx do we need to check usage (len(args)) ?
+        # slice urn
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice')
-        slice_cred = self.slice_credential_string(slice_hrn)
+
+        # credentials
+        creds = [self.slice_credential_string(slice_hrn)]
         delegated_cred = None
+        server_version = self.get_cached_server_version(server)
         if server_version.get('interface') == 'slicemgr':
             # delegate our cred to the slice manager
             # do not delegate cred to slicemgr...not working at the moment
@@ -841,10 +880,12 @@ or currently provisioned resources  (ListResources)
             #    delegated_cred = self.delegate_cred(slice_cred, server_version['hrn'])
             #elif server_version.get('urn'):
             #    delegated_cred = self.delegate_cred(slice_cred, urn_to_hrn(server_version['urn']))
-                 
+                
+        # rspec 
         rspec_file = self.get_rspec_file(args[1])
         rspec = open(rspec_file).read()
 
+        # users
         # need to pass along user keys to the aggregate.
         # users = [
         #  { urn: urn:publicid:IDN+emulab.net+user+alice
@@ -863,18 +904,19 @@ or currently provisioned resources  (ListResources)
                 rspec = RSpec(rspec)
                 rspec.filter({'component_manager_id': server_version['urn']})
                 rspec = RSpecConverter.to_pg_rspec(rspec.toxml(), content_type='request')
-                creds = [slice_cred]
             else:
                 users = sfa_users_arg(user_records, slice_record)
-                creds = [slice_cred]
-                if delegated_cred:
-                    creds.append(delegated_cred)
+        
         # do not append users, keys, or slice tags. Anything 
-        # not contained in this request will be removed from the slice 
+        # not contained in this request will be removed from the slice
+
+        # CreateSliver has supported the options argument for a while now so it should
+        # be safe to assume this server support it
         api_options = {}
         api_options ['append'] = False
         api_options ['call_id'] = unique_call_id()
-        result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server,api_options))
+
+        result = server.CreateSliver(slice_urn, creds, rspec, users, *self.ois(server, api_options))
         value = ReturnValue.get_value(result)
         if options.file is None:
             print value
@@ -886,32 +928,46 @@ or currently provisioned resources  (ListResources)
         """
         delete named slice (DeleteSliver)
         """
+        server = self.sliceapi()
+
+        # slice urn
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
+
+        # creds
         slice_cred = self.slice_credential_string(slice_hrn)
         creds = [slice_cred]
         if options.delegate:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
-        server = self.sliceapi()
+        
+        # options and call_id when supported
         api_options = {}
         api_options ['call_id'] = unique_call_id()
-        return server.DeleteSliver(slice_urn, creds, *self.ois(server,api_options))
+        result = server.DeleteSliver(slice_urn, creds, *self.ois(server, api_options ) )
+        # xxx no ReturnValue ??
+        return result
   
     def status(self, options, args):
         """
         retrieve slice status (SliverStatus)
         """
+        server = self.sliceapi()
+
+        # slice urn
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
+
+        # creds 
         slice_cred = self.slice_credential_string(slice_hrn)
         creds = [slice_cred]
         if options.delegate:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
-        server = self.sliceapi()
+
+        # options and call_id when supported
         api_options = {}
-        api_options ['call_id'] = unique_call_id()
+       api_options['call_id']=unique_call_id()
         result = server.SliverStatus(slice_urn, creds, *self.ois(server,api_options))
         value = ReturnValue.get_value(result)
         print value
@@ -922,14 +978,18 @@ or currently provisioned resources  (ListResources)
         """
         start named slice (Start)
         """
+        server = self.sliceapi()
+
+        # the slice urn
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
+        
+        # cred
         slice_cred = self.slice_credential_string(args[0])
         creds = [slice_cred]
         if options.delegate:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
-        server = self.sliceapi()
         # xxx Thierry - does this not need an api_options as well ?
         return server.Start(slice_urn, creds)
     
@@ -937,14 +997,16 @@ or currently provisioned resources  (ListResources)
         """
         stop named slice (Stop)
         """
+        server = self.sliceapi()
+        # slice urn
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
+        # cred
         slice_cred = self.slice_credential_string(args[0])
         creds = [slice_cred]
         if options.delegate:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
-        server = self.sliceapi()
         return server.Stop(slice_urn, creds)
     
     # reset named slice
@@ -952,9 +1014,11 @@ or currently provisioned resources  (ListResources)
         """
         reset named slice (reset_slice)
         """
+        server = self.sliceapi()
+        # slice urn
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
-        server = self.sliceapi()
+        # cred
         slice_cred = self.slice_credential_string(args[0])
         creds = [slice_cred]
         if options.delegate:
@@ -966,17 +1030,21 @@ or currently provisioned resources  (ListResources)
         """
         renew slice (RenewSliver)
         """
+        server = self.sliceapi()
+        # slice urn    
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
-        server = self.sliceapi()
+        # creds
         slice_cred = self.slice_credential_string(args[0])
         creds = [slice_cred]
         if options.delegate:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
+        # time
         time = args[1]
+        # options and call_id when supported
         api_options = {}
-        api_options ['call_id'] = unique_call_id()
+       api_options['call_id']=unique_call_id()
         result =  server.RenewSliver(slice_urn, creds, time, *self.ois(server,api_options))
         value = ReturnValue.get_value(result)
         return value
@@ -986,14 +1054,16 @@ or currently provisioned resources  (ListResources)
         """
         shutdown named slice (Shutdown)
         """
+        server = self.sliceapi()
+        # slice urn
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
+        # creds
         slice_cred = self.slice_credential_string(slice_hrn)
         creds = [slice_cred]
         if options.delegate:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
-        server = self.sliceapi()
         return server.Shutdown(slice_urn, creds)         
     
 
@@ -1001,17 +1071,25 @@ or currently provisioned resources  (ListResources)
         """
         get a ticket for the specified slice
         """
+        server = self.sliceapi()
+        # slice urn
         slice_hrn, rspec_path = args[0], args[1]
         slice_urn = hrn_to_urn(slice_hrn, 'slice')
+        # creds
         slice_cred = self.slice_credential_string(slice_hrn)
         creds = [slice_cred]
         if options.delegate:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
+        # rspec
         rspec_file = self.get_rspec_file(rspec_path) 
         rspec = open(rspec_file).read()
-        server = self.sliceapi()
-        ticket_string = server.GetTicket(slice_urn, creds, rspec, [])
+        # options and call_id when supported
+        api_options = {}
+       api_options['call_id']=unique_call_id()
+        # get ticket at the server
+        ticket_string = server.GetTicket(slice_urn, creds, rspec, *self.ois(server,api_options))
+        # save
         file = os.path.join(self.options.sfi_dir, get_leaf(slice_hrn) + ".ticket")
         self.logger.info("writing ticket to %s"%file)
         ticket = SfaTicket(string=ticket_string)
@@ -1028,6 +1106,8 @@ or currently provisioned resources  (ListResources)
         # use this to get the right slice credential 
         ticket = SfaTicket(filename=ticket_file)
         ticket.decode()
+        ticket_string = ticket.save_to_string(save_parents=True)
+
         slice_hrn = ticket.gidObject.get_hrn()
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
         #slice_hrn = ticket.attributes['slivers'][0]['hrn']
@@ -1044,12 +1124,14 @@ or currently provisioned resources  (ListResources)
         for hostname in hostnames:
             try:
                 self.logger.info("Calling redeem_ticket at %(hostname)s " % locals())
-                server = self.server_proxy(hostname, CM_PORT, self.private_key, \
-                                               self.my_gid, verbose=self.options.debug)
-                server.RedeemTicket(ticket.save_to_string(save_parents=True), slice_cred)
+                cm_url="http://%s:%s/"%(hostname,CM_PORT)
+                server = SfaServerProxy(cm_url, self.private_key, self.my_gid)
+                server = self.server_proxy(hostname, CM_PORT, self.private_key, 
+                                           timeout=self.options.timeout, verbose=self.options.debug)
+                server.RedeemTicket(ticket_string, slice_cred)
                 self.logger.info("Success")
             except socket.gaierror:
-                self.logger.error("redeem_ticket failed: Component Manager not accepting requests")
+                self.logger.error("redeem_ticket failed on %s: Component Manager not accepting requests"%hostname)
             except Exception, e:
                 self.logger.log_exc(e.message)
         return