Merge branch 'master' of ssh://git.planet-lab.org/git/sfa
[sfa.git] / sfa / client / sfi.py
index bc6cc60..293e5bd 100755 (executable)
@@ -10,13 +10,14 @@ import traceback
 import socket
 import random
 import datetime
+import zlib
+import codecs
 from lxml import etree
 from StringIO import StringIO
 from types import StringTypes, ListType
 from optparse import OptionParser
-import zlib
 
-from sfa.util.sfalogging import sfa_logger,sfa_logger_goes_to_console
+from sfa.util.sfalogging import sfi_logger
 from sfa.trust.certificate import Keypair, Certificate
 from sfa.trust.gid import GID
 from sfa.trust.credential import Credential
@@ -26,6 +27,9 @@ from sfa.util.xrn import Xrn, get_leaf, get_authority, hrn_to_urn
 import sfa.util.xmlrpcprotocol as xmlrpcprotocol
 from sfa.util.config import Config
 from sfa.util.version import version_core
+from sfa.util.cache import Cache
+from sfa.rspecs.rspec_version import RSpecVersion
+from sfa.rspecs.pg_rspec import pg_rspec_request_version
 
 AGGREGATE_PORT=12346
 CM_PORT=12346
@@ -106,13 +110,17 @@ def save_record_to_file(filename, record):
     else:
         record = SfaRecord(dict=record)
     str = record.save_to_string()
-    file(filename, "w").write(str)
+    f=codecs.open(filename, encoding='utf-8',mode="w")
+    f.write(str)
+    f.close()
     return
 
 
 # load methods
 def load_record_from_file(filename):
-    str = file(filename, "r").read()
+    f=codecs.open(filename, encoding="utf-8", mode="r")
+    str = f.read()
+    f.close()
     record = SfaRecord(string=str)
     return record
 
@@ -134,14 +142,17 @@ class Sfi:
         for opt in Sfi.required_options:
             if not hasattr(options,opt): setattr(options,opt,None)
         if not hasattr(options,'sfi_dir'): options.sfi_dir=os.path.expanduser("~/.sfi/")
+        # xxx oops, this is dangerous, sounds like ww sometimes have discrepency
+        # would be safer to remove self.sfi_dir altogether
+        self.sfi_dir = options.sfi_dir
         self.options = options
         self.slicemgr = None
         self.registry = None
         self.user = None
         self.authority = None
         self.hashrequest = False
-        sfa_logger_goes_to_console()
-        self.logger=sfa_logger()
+        self.logger = sfi_logger
+        self.logger.enable_console()
    
     def create_cmd_parser(self, command, additional_cmdargs=None):
         cmdargs = {"list": "authority",
@@ -151,6 +162,7 @@ class Sfi:
                   "update": "record",
                   "aggregates": "[name]",
                   "registries": "[name]",
+                  "create_gid": "[name]",
                   "get_gid": [],  
                   "get_trusted_certs": "cred",
                   "slices": "",
@@ -204,11 +216,17 @@ class Sfi:
                             default="all")
         # display formats
         if command in ("resources"):
+            parser.add_option("-r", "--rspec-version", dest="rspec_version", default="SFA 1",
+                              help="schema type and version of resulting RSpec")
             parser.add_option("-f", "--format", dest="format", type="choice",
                              help="display format ([xml]|dns|ip)", default="xml",
                              choices=("xml", "dns", "ip"))
+            #panos: a new option to define the type of information about resources a user is interested in
+           parser.add_option("-i", "--info", dest="info",
+                                help="optional component information", default=None)
 
-        if command in ("resources", "show", "list"):
+
+        if command in ("resources", "show", "list", "create_gid"):
            parser.add_option("-o", "--output", dest="file",
                             help="output XML to file", metavar="FILE", default=None)
         
@@ -225,6 +243,10 @@ class Sfi:
                             help="delegate slice credential", metavar="HRN", default=None)
         
         if command in ("version"):
+            parser.add_option("-a", "--aggregate", dest="aggregate",
+                             default=None, help="aggregate host")
+            parser.add_option("-p", "--port", dest="port",
+                             default=AGGREGATE_PORT, help="aggregate port")
             parser.add_option("-R","--registry-version",
                               action="store_true", dest="version_registry", default=False,
                               help="probe registry version instead of slicemgr")
@@ -262,13 +284,15 @@ class Sfi:
         parser.add_option("-k", "--hashrequest",
                          action="store_true", dest="hashrequest", default=False,
                          help="Create a hash of the request that will be authenticated on the server")
+        parser.add_option("-t", "--timeout", dest="timeout", default=None,
+                         help="Amout of time tom wait before timing out the request")
         parser.disable_interspersed_args()
 
         return parser
         
+
     def read_config(self):
-       config_file = self.options.sfi_dir + os.sep + "sfi_config"
+       config_file = os.path.join(self.options.sfi_dir,"sfi_config")
        try:
           config = Config (config_file)
        except:
@@ -334,15 +358,56 @@ class Sfi:
        self.key = Keypair(filename=key_file) 
        self.key_file = key_file
        self.cert_file = cert_file
-       self.cert = Certificate(filename=cert_file) 
-       # Establish connection to server(s)
+       self.cert = GID(filename=cert_file)
        self.logger.info("Contacting Registry at: %s"%self.reg_url)
-       self.registry = xmlrpcprotocol.get_server(self.reg_url, key_file, cert_file, self.options)  
+       self.registry = xmlrpcprotocol.get_server(self.reg_url, key_file, cert_file, timeout=self.options.timeout, verbose=self.options.debug)  
        self.logger.info("Contacting Slice Manager at: %s"%self.sm_url)
-       self.slicemgr = xmlrpcprotocol.get_server(self.sm_url, key_file, cert_file, self.options)
-
+       self.slicemgr = xmlrpcprotocol.get_server(self.sm_url, key_file, cert_file, timeout=self.options.timeout, verbose=self.options.debug)
        return
-    
+
+    def get_cached_server_version(self, server):
+        # check local cache first
+        cache = None
+        version = None 
+        cache_file = os.path.join(self.options.sfi_dir,'sfi_cache.dat')
+        cache_key = server.url + "-version"
+        try:
+            cache = Cache(cache_file)
+        except IOError:
+            cache = Cache()
+            self.logger.info("Local cache not found at: %s" % cache_file)
+
+        if cache:
+            version = cache.get(cache_key)
+            
+        if not version: 
+            version = server.GetVersion()
+            # cache version for 24 hours
+            cache.add(cache_key, version, ttl= 60*60*24)
+            self.logger.info("Updating cache file %s" % cache_file)
+            cache.save_to_file(cache_file)
+
+
+        return version   
+        
+
+    def server_supports_call_id_arg(self, server):
+        """
+        Returns true if server support the optional call_id arg, false otherwise. 
+        """
+        server_version = self.get_cached_server_version(server)
+        if 'sfa' 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) > 0 or int(rev) > 20:
+                    return True
+        return False                
+             
     #
     # Get various credential and spec files
     #
@@ -368,27 +433,37 @@ class Sfi:
     
     def get_cert_file(self, key_file):
     
-        file = os.path.join(self.options.sfi_dir, self.user.replace(self.authority + '.', '') + ".cert")
-        if (os.path.isfile(file)):
-            # use existing cert if it exists                     
-            return file
-        else:
-            try:
-                # attempt to use gid as the cert.  
-                gid = self._get_gid()
-                self.logger.info("Writing certificate to %s"%file)
-                gid.save_to_file(file) 
-            except:
-                # generate self signed certificate
-                k = Keypair(filename=key_file)
-                cert = Certificate(subject=self.user)
-                cert.set_pubkey(k)
-                cert.set_issuer(k, self.user)
-                cert.sign()
-                self.logger.info("Writing self-signed certificate to %s"%file)
-                cert.save_to_file(file)
-            
-            return file
+        cert_file = os.path.join(self.options.sfi_dir, self.user.replace(self.authority + '.', '') + ".cert")
+        if (os.path.isfile(cert_file)):
+            # we'd perfer to use Registry issued certs instead of self signed certs. 
+            # if this is a Registry cert (GID) then we are done 
+            gid = GID(filename=cert_file)
+            if gid.get_urn():
+                return cert_file
+
+        # generate self signed certificate
+        k = Keypair(filename=key_file)
+        cert = Certificate(subject=self.user)
+        cert.set_pubkey(k)
+        cert.set_issuer(k, self.user)
+        cert.sign()
+        self.logger.info("Writing self-signed certificate to %s"%cert_file)
+        cert.save_to_file(cert_file)
+        self.cert = cert
+        # try to get registry issued cert
+        try:
+            self.logger.info("Getting Registry issued cert")
+            self.read_config()
+            # *hack.  need to set registyr before _get_gid() is called 
+            self.registry = xmlrpcprotocol.get_server(self.reg_url, key_file, cert_file, timeout=self.options.timeout, verbose=self.options.debug)
+            gid = self._get_gid(type='user')
+            self.registry = None 
+            self.logger.info("Writing certificate to %s"%cert_file)
+            gid.save_to_file(cert_file)
+        except:
+            self.logger.info("Failed to download Registry issued cert")
+
+        return cert_file
 
     def get_cached_gid(self, file):
         """
@@ -411,7 +486,7 @@ class Sfi:
         self.logger.debug("Sfi.get_gid-> %s",gid.save_to_string(save_parents=True))
         return gid
 
-    def _get_gid(self, hrn=None):
+    def _get_gid(self, hrn=None, type=None):
         """
         git_gid helper. Retrive the gid from the registry and save it to file.
         """
@@ -420,13 +495,23 @@ class Sfi:
             hrn = self.user
  
         gidfile = os.path.join(self.options.sfi_dir, hrn + ".gid")
+        print gidfile
         gid = self.get_cached_gid(gidfile)
         if not gid:
             user_cred = self.get_user_cred()
             records = self.registry.Resolve(hrn, user_cred.save_to_string(save_parents=True))
             if not records:
                 raise RecordNotFound(args[0])
-            gid = GID(string=records[0]['gid'])
+            record = records[0]
+            if type:
+                record=None
+                for rec in records:
+                   if type == rec['type']:
+                        record = rec 
+            if not record:
+                raise RecordNotFound(args[0])
+            
+            gid = GID(string=record['gid'])
             self.logger.info("Writing gid to %s"%gidfile)
             gid.save_to_file(filename=gidfile)
         return gid   
@@ -494,7 +579,7 @@ class Sfi:
        if (os.path.isfile(file)):
           return file
        else:
-          self.logger.critical("No such rspec file"%rspec)
+          self.logger.critical("No such rspec file %s"%rspec)
           sys.exit(1)
     
     def get_record_file(self, record):
@@ -544,7 +629,7 @@ class Sfi:
         host_parts = host.split('/')
         host_parts[0] = host_parts[0] + ":" + str(port)
         url =  "http://%s" %  "/".join(host_parts)    
-        return xmlrpcprotocol.get_server(url, keyfile, certfile, self.options)
+        return xmlrpcprotocol.get_server(url, keyfile, certfile, timeout=self.options.timeout, verbose=self.options.debug)
 
     # xxx opts could be retrieved in self.options
     def get_server_from_opts(self, opts):
@@ -569,7 +654,22 @@ class Sfi:
   
     def dispatch(self, command, cmd_opts, cmd_args):
         return getattr(self, command)(cmd_opts, cmd_args)
+
+    def create_gid(self, opts, args):
+        if len(args) < 1:
+            self.print_help()
+            sys.exit(1)
+        target_hrn = args[0]
+        user_cred = self.get_user_cred().save_to_string(save_parents=True)
+        gid = self.registry.CreateGid(user_cred, target_hrn, self.cert.save_to_string())
+        if opts.file:
+            filename = opts.file
+        else:
+            filename = os.sep.join([self.sfi_dir, '%s.gid' % target_hrn])
+        self.logger.info("writing %s gid to %s" % (target_hrn, filename))
+        GID(string=gid).save_to_file(filename)
+         
+     
     # list entires in named authority registry
     def list(self, opts, args):
         if len(args)!= 1:
@@ -602,6 +702,7 @@ class Sfi:
         hrn = args[0]
         user_cred = self.get_user_cred().save_to_string(save_parents=True)
         records = self.registry.Resolve(hrn, user_cred)
+        print records
         records = filter_records(opts.type, records)
         if not records:
             print "No record of type", opts.type
@@ -612,7 +713,7 @@ class Sfi:
                 record = SliceRecord(dict=record)
             elif record['type'] in ['node']:
                 record = NodeRecord(dict=record)
-            elif record['type'] in ['authority', 'ma', 'sa']:
+            elif record['type'].startswith('authority'):
                 record = AuthorityRecord(dict=record)
             else:
                 record = SfaRecord(dict=record)
@@ -620,7 +721,6 @@ class Sfi:
                 record.dump()  
             else:
                 print record.save_to_string() 
-       
         if opts.file:
             file = opts.file
             if not file.startswith(os.sep):
@@ -738,6 +838,8 @@ class Sfi:
         """ 
         trusted_certs = self.registry.get_trusted_certs()
         for trusted_cert in trusted_certs:
+            gid = GID(string=trusted_cert)
+            gid.dump()
             cert = Certificate(string=trusted_cert)
             self.logger.debug('Sfi.get_trusted_certs -> %r'%cert.get_subject())
         return 
@@ -771,7 +873,6 @@ class Sfi:
     # ==================================================================
     # Slice-related commands
     # ==================================================================
-    
 
     def version(self, opts, args):
         if opts.version_local:
@@ -796,7 +897,8 @@ class Sfi:
             delegated_cred = self.delegate_cred(user_cred, get_authority(self.authority))
             creds.append(delegated_cred)  
         server = self.get_server_from_opts(opts)
-        results = server.ListSlices(creds, unique_call_id())
+        #results = server.ListSlices(creds, unique_call_id())
+        results = server.ListSlices(creds)
         display_list(results)
         return
     
@@ -810,7 +912,7 @@ class Sfi:
         if args:
             cred = self.get_slice_cred(args[0]).save_to_string(save_parents=True)
             hrn = args[0]
-            call_options = {'geni_slice_urn': hrn_to_urn(hrn, 'slice')}
+           call_options = {'geni_slice_urn': hrn_to_urn(hrn, 'slice')}
         else:
             cred = user_cred
             hrn = None
@@ -818,8 +920,24 @@ class Sfi:
         creds = [cred]
         if opts.delegate:
             delegated_cred = self.delegate_cred(cred, get_authority(self.authority))
-            creds.append(delegated_cred) 
-        result = server.ListResources(creds, call_options,unique_call_id())
+            creds.append(delegated_cred)
+        if opts.rspec_version:
+            server_version = self.get_cached_server_version(server)
+            if 'sfa' in server_version:
+                # just request the version the client wants 
+                call_options['rspec_version'] = dict(RSpecVersion(opts.rspec_version)) 
+            else:
+                # this must be a protogeni aggregate. We should request a v2 ad rspec
+                # regardless of what the client user requested 
+                call_options['rspec_version'] = dict(pg_rspec_request_version)     
+        #panos add info options
+        if opts.info:
+            call_options['info'] = opts.info 
+
+        call_args = [creds, call_options]
+        if self.server_supports_call_id_arg(server):
+            call_args.append(unique_call_id())
+        result = server.ListResources(*call_args)
         format = opts.format
         if opts.file is None:
             display_rspec(result, format)
@@ -832,6 +950,8 @@ class Sfi:
     
     # created named slice with given rspec
     def create(self, opts, args):
+        server = self.get_server_from_opts(opts)
+        server_version = self.get_cached_server_version(server)
         slice_hrn = args[0]
         slice_urn = hrn_to_urn(slice_hrn, 'slice') 
         user_cred = self.get_user_cred()
@@ -842,8 +962,49 @@ class Sfi:
             creds.append(delegated_cred)
         rspec_file = self.get_rspec_file(args[1])
         rspec = open(rspec_file).read()
-        server = self.get_server_from_opts(opts)
-        result =  server.CreateSliver(slice_urn, creds, rspec, [], unique_call_id())
+
+        # need to pass along user keys to the aggregate.  
+        # users = [
+        #  { urn: urn:publicid:IDN+emulab.net+user+alice
+        #    keys: [<ssh key A>, <ssh key B>] 
+        #  }]
+        users = []
+        all_keys = []
+        all_key_ids = []
+        slice_records = self.registry.Resolve(slice_urn, [user_cred.save_to_string(save_parents=True)])
+        if slice_records and 'researcher' in slice_records[0]:
+            slice_record = slice_records[0]
+            user_hrns = slice_record['researcher']
+            user_urns = [hrn_to_urn(hrn, 'user') for hrn in user_hrns]
+            user_records = self.registry.Resolve(user_urns, [user_cred.save_to_string(save_parents=True)])
+            for user_record in user_records:
+                if user_record['type'] != 'user':
+                    continue
+                #user = {'urn': user_cred.get_gid_caller().get_urn(),'keys': []}
+                user = {'urn': user_cred.get_gid_caller().get_urn(), #
+                        'keys': user_record['keys'],
+                        'email': user_record['email'], #  needed for MyPLC
+                        'person_id': user_record['person_id'], # needed for MyPLC
+                        'first_name': user_record['first_name'], # needed for MyPLC
+                        'last_name': user_record['last_name'], # needed for MyPLC
+                        'slice_record': slice_record, # needed for legacy refresh peer
+                        'key_ids': user_record['key_ids'] # needed for legacy refresh peer
+                } 
+                users.append(user)
+                all_keys.extend(user_record['keys'])
+                all_key_ids.extend(user_record['key_ids'])
+            # ProtoGeni Aggregates will only install the keys of the user that is issuing the
+            # request. So we will add all to the current caller's list of keys
+            if 'sfa' not in server_version:
+                for user in users:
+                    if user['urn'] == user_cred.get_gid_caller().get_urn():
+                        user['keys'] = all_keys  
+
+        call_args = [slice_urn, creds, rspec, users]
+        if self.server_supports_call_id_arg(server):
+            call_args.append(unique_call_id())
+             
+        result = server.CreateSliver(*call_args)
         print result
         return result
 
@@ -910,8 +1071,12 @@ class Sfi:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
         server = self.get_server_from_opts(opts)
-        return server.DeleteSliver(slice_urn, creds, unique_call_id())
-    
+
+        call_args = [slice_urn, creds]
+        if self.server_supports_call_id_arg(server):
+            call_args.append(unique_call_id())
+        return server.DeleteSliver(*call_args) 
+  
     # start named slice
     def start(self, opts, args):
         slice_hrn = args[0]
@@ -958,7 +1123,11 @@ class Sfi:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
         time = args[1]
-        return server.RenewSliver(slice_urn, creds, time, unique_call_id())
+        
+        call_args = [slice_urn, creds, time]
+        if self.server_supports_call_id_arg(server):
+            call_args.append(unique_call_id())
+        return server.RenewSliver(*call_args)
 
 
     def status(self, opts, args):
@@ -970,7 +1139,10 @@ class Sfi:
             delegated_cred = self.delegate_cred(slice_cred, get_authority(self.authority))
             creds.append(delegated_cred)
         server = self.get_server_from_opts(opts)
-        print server.SliverStatus(slice_urn, creds, unique_call_id())
+        call_args = [slice_urn, creds]
+        if self.server_supports_call_id_arg(server):
+            call_args.append(unique_call_id())
+        print server.SliverStatus(*call_args)
 
 
     def shutdown(self, opts, args):
@@ -1009,18 +1181,18 @@ class Sfi:
         (cmd_opts, cmd_args) = self.cmd_parser.parse_args(args[1:])
 
         self.set_servers()
-    
         self.logger.info("Command=%s" % command)
         if command in ("resources"):
             self.logger.debug("resources cmd_opts %s" % cmd_opts.format)
         elif command in ("list", "show", "remove"):
             self.logger.debug("cmd_opts.type %s" % cmd_opts.type)
-        self.logger.debug('cmd_args %s',cmd_args)
+        self.logger.debug('cmd_args %s' % cmd_args)
 
         try:
             self.dispatch(command, cmd_opts, cmd_args)
         except KeyError:
             self.logger.critical ("Unknown command %s"%command)
+            raise
             sys.exit(1)
     
         return