reorganizing code. moving some things out of the server interface classes and into...
authorTony Mack <tmack@cs.princeton.edu>
Wed, 8 Apr 2009 01:28:53 +0000 (01:28 +0000)
committerTony Mack <tmack@cs.princeton.edu>
Wed, 8 Apr 2009 01:28:53 +0000 (01:28 +0000)
geni/util/api.py [new file with mode: 0644]
geni/util/auth.py [new file with mode: 0644]
geni/util/faults.py [new file with mode: 0644]
geni/util/parameter.py [new file with mode: 0644]

diff --git a/geni/util/api.py b/geni/util/api.py
new file mode 100644 (file)
index 0000000..14008da
--- /dev/null
@@ -0,0 +1,363 @@
+#
+# Geniwrapper XML-RPC and SOAP interfaces
+#
+#
+
+import sys
+import os
+import traceback
+import string
+import xmlrpclib
+from geni.util.auth import Auth
+from geni.util.config import *
+from geni.util.faults import *
+from geni.util.debug import *
+
+# See "2.2 Characters" in the XML specification:
+#
+# #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
+# avoiding
+# [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
+
+invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
+xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
+
+def xmlrpclib_escape(s, replace = string.replace):
+    """
+    xmlrpclib does not handle invalid 7-bit control characters. This
+    function augments xmlrpclib.escape, which by default only replaces
+    '&', '<', and '>' with entities.
+    """
+
+    # This is the standard xmlrpclib.escape function
+    s = replace(s, "&", "&amp;")
+    s = replace(s, "<", "&lt;")
+    s = replace(s, ">", "&gt;",)
+
+    # Replace invalid 7-bit control characters with '?'
+    return s.translate(xml_escape_table)
+
+def xmlrpclib_dump(self, value, write):
+    """
+    xmlrpclib cannot marshal instances of subclasses of built-in
+    types. This function overrides xmlrpclib.Marshaller.__dump so that
+    any value that is an instance of one of its acceptable types is
+    marshalled as that type.
+
+    xmlrpclib also cannot handle invalid 7-bit control characters. See
+    above.
+    """
+
+    # Use our escape function
+    args = [self, value, write]
+    if isinstance(value, (str, unicode)):
+        args.append(xmlrpclib_escape)
+
+    try:
+        # Try for an exact match first
+        f = self.dispatch[type(value)]
+    except KeyError:
+        raise
+        # Try for an isinstance() match
+        for Type, f in self.dispatch.iteritems():
+            if isinstance(value, Type):
+                f(*args)
+                return
+        raise TypeError, "cannot marshal %s objects" % type(value)
+    else:
+        f(*args)
+
+# You can't hide from me!
+xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
+
+# SOAP support is optional
+try:
+    import SOAPpy
+    from SOAPpy.Parser import parseSOAPRPC
+    from SOAPpy.Types import faultType
+    from SOAPpy.NS import NS
+    from SOAPpy.SOAPBuilder import buildSOAP
+except ImportError:
+    SOAPpy = None
+
+import geni.methods
+
+def import_deep(name):
+    mod = __import__(name)
+    components = name.split('.')
+    for comp in components[1:]:
+        mod = getattr(mod, comp)
+    return mod
+
+class GeniAPI:
+
+    # flat list of method names
+    methods = geni.methods.methods
+    
+    def __init__(self, config = "/usr/share/geniwrapper/geni/util/geni_config", encoding = "utf-8", peer_cert = None, interface = None):
+        self.encoding = encoding
+
+        # Better just be documenting the API
+        if config is None:
+            return
+
+        # Load configuration
+        self.config = Config(config)
+        self.auth = Auth(peer_cert)
+        self.interface = interface
+        self.plshell = self.getPLCShell()
+        self.basedir = self.config.GENI_BASE_DIR + os.sep
+        self.server_basedir = self.basedir + os.sep + "geni" + os.sep
+        self.hrn = self.config.GENI_INTERFACE_HRN
+
+
+    def getPLCShell(self):
+        self.plauth = {'Username': self.config.GENI_PLC_USER,
+                         'AuthMethod': 'password',
+                         'AuthString': self.config.GENI_PLC_PASSWORD} 
+        try:
+            import PLC.Shell
+            shell = PLC.Shell.Shell(globals = globals())
+            shell.AuthCheck(self.plauth)
+            return shell
+        except ImportError:
+            # connect via xmlrpc
+            plc_host = self.config.GENI_PLC_HOST
+            plc_port = self.config.GENI_PLC_PORT
+            plc_api_path = self.config.GENI_PLC_API_PATH
+            url = "https://%(plc_host)s:%(plc_port)s/%(plc_api_path)s/" % \
+                   locals()
+             
+            shell = xmlrpclib.Server(url, verbose = 0, allow_none = True)
+            shell.AuthCheck(self.plauth)
+            return shell         
+   
+    def fill_record_pl_info(self, record):
+        """
+        Fill in the planetlab specific fields of a Geni record. This
+        involves calling the appropraite PLC method to retrie the 
+        dtabase record for the object.
+        
+        PLC data is filled into the pl_fino field of the record.
+    
+        @param record record to fill in field (in/out param)     
+        """
+        type = record.get_type()
+        pointer = record.get_pointer()
+
+        # records with pointer==-1 do not have plc info associated with them.
+        # for example, the top level authority records which are
+        # authorities, but not PL "sites"
+        if pointer == -1:
+            record.set_pl_info({})
+            return
+
+        if (type == "sa") or (type == "ma"):
+            pl_res = self.plshell.GetSites(self.plauth, [pointer])
+        elif (type == "slice"):
+            pl_res = self.plshell.GetSlices(self.plauth, [pointer])
+        elif (type == "user"):
+            pl_res = self.plshell.GetPersons(self.plauth, [pointer])
+            key_ids = pl_res[0]['key_ids']
+            keys = self.plshell.GetKeys(self.plauth, key_ids)
+            pubkeys = []
+            if keys:
+                pubkeys = [key['key'] for key in keys]
+            pl_res[0]['keys'] = pubkeys
+        elif (type == "node"):
+            pl_res = self.plshell.GetNodes(self.plauth, [pointer])
+        else:
+            raise UnknownGeniType(type)
+
+        if not pl_res:
+            # the planetlab record no longer exists
+            # TODO: delete the geni record ?
+            raise PlanetLabRecordDoesNotExist(record.get_name())
+
+        record.set_pl_info(pl_res[0])
+
+
+    def lookup_users(self, auth_table, user_id_list, role="*"):
+        record_list = []
+        for person_id in user_id_list:
+            user_records = auth_table.find("user", person_id, "pointer")
+            for user_record in user_records:
+                self.fill_record_info(user_record)
+
+                user_roles = user_record.get_pl_info().get("roles")
+                if (role=="*") or (role in user_roles):
+                    record_list.append(user_record.get_name())
+        return record_list
+
+    def fill_record_geni_info(self, record):
+        geni_info = {}
+        type = record.get_type()
+
+        if (type == "slice"):
+            auth_table = self.auth.get_auth_table(self.auth.get_authority(record.get_name()))
+            person_ids = record.pl_info.get("person_ids", [])
+            researchers = self.lookup_users(auth_table, person_ids)
+            geni_info['researcher'] = researchers
+
+        elif (type == "sa"):
+            auth_table = self.auth.get_auth_table(record.get_name())
+            person_ids = record.pl_info.get("person_ids", [])
+            pis = self.lookup_users(auth_table, person_ids, "pi")
+            geni_info['pi'] = pis
+            # TODO: OrganizationName
+
+        elif (type == "ma"):
+            auth_table = self.auth.get_auth_table(record.get_name())
+            person_ids = record.pl_info.get("person_ids", [])
+            operators = self.lookup_users(auth_table, person_ids, "tech")
+            geni_info['operator'] = operators
+            # TODO: OrganizationName
+
+            auth_table = self.auth.get_auth_table(record.get_name())
+            person_ids = record.pl_info.get("person_ids", [])
+            owners = self.lookup_users(auth_table, person_ids, "admin")
+            geni_info['owner'] = owners
+
+        elif (type == "node"):
+            geni_info['dns'] = record.pl_info.get("hostname", "")
+            # TODO: URI, LatLong, IP, DNS
+    
+        elif (type == "user"):
+            geni_info['email'] = record.pl_info.get("email", "")
+            # TODO: PostalAddress, Phone
+
+        record.set_geni_info(geni_info)
+
+    def fill_record_info(self, record):
+        """
+        Given a geni record, fill in the PLC specific and Geni specific
+        fields in the record. 
+        """
+        self.fill_record_pl_info(record)
+        self.fill_record_geni_info(record)
+
+    def update_membership_list(self, oldRecord, record, listName, addFunc, delFunc):
+        # get a list of the HRNs tht are members of the old and new records^M
+        if oldRecord:
+            if oldRecord.pl_info == None:
+                oldRecord.pl_info = {}
+            oldList = oldRecord.get_geni_info().get(listName, [])
+        else:
+            oldList = []
+        newList = record.get_geni_info().get(listName, [])
+
+        # if the lists are the same, then we don't have to update anything
+        if (oldList == newList):
+            return
+
+        # build a list of the new person ids, by looking up each person to get
+        # their pointer
+        newIdList = []
+        for hrn in newList:
+            userRecord = self.resolve_raw("user", hrn)[0]
+            newIdList.append(userRecord.get_pointer())
+
+        # build a list of the old person ids from the person_ids field of the
+        # pl_info
+        if oldRecord:
+            oldIdList = oldRecord.plinfo.get("person_ids", [])
+            containerId = oldRecord.get_pointer()
+        else:
+            # if oldRecord==None, then we are doing a Register, instead of an
+            # update.
+            oldIdList = []
+            containerId = record.get_pointer()
+
+    # add people who are in the new list, but not the oldList
+        for personId in newIdList:
+            if not (personId in oldIdList):
+                print "adding id", personId, "to", record.get_name()
+                addFunc(self.plauth, personId, containerId)
+
+        # remove people who are in the old list, but not the new list
+        for personId in oldIdList:
+            if not (personId in newIdList):
+                print "removing id", personId, "from", record.get_name()
+                delFunc(self.plauth, personId, containerId)
+
+    def update_membership(self, oldRecord, record):
+        if record.type == "slice":
+            self.update_membership_list(oldRecord, record, 'researcher',
+                                        self.plshell.AddPersonToSlice,
+                                        self.plshell.DeletePersonFromSlice)
+        elif record.type == "sa":
+            # TODO
+            pass
+        elif record.type == "ma":
+            # TODO
+            pass
+
+    def callable(self, method):
+        """
+        Return a new instance of the specified method.
+        """
+        # Look up method
+        if method not in self.methods:
+            raise GeniInvalidAPIMethod, method
+        
+        # Get new instance of method
+        try:
+            classname = method.split(".")[-1]
+            module = __import__("geni.methods." + method, globals(), locals(), [classname])
+            callablemethod = getattr(module, classname)(self)
+            return getattr(module, classname)(self)
+        except ImportError, AttributeError:
+            raise GeniInvalidAPIMethod, method
+
+    def call(self, source, method, *args):
+        """
+        Call the named method from the specified source with the
+        specified arguments.
+        """
+        function = self.callable(method)
+        function.source = source
+        return function(*args)
+
+    def handle(self, source, data):
+        """
+        Handle an XML-RPC or SOAP request from the specified source.
+        """
+
+        # Parse request into method name and arguments
+        try:
+            interface = xmlrpclib
+            (args, method) = xmlrpclib.loads(data)
+            methodresponse = True
+        except Exception, e:
+            if SOAPpy is not None:
+                interface = SOAPpy
+                (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
+                method = r._name
+                args = r._aslist()
+                # XXX Support named arguments
+            else:
+                raise e
+
+        try:
+            result = self.call(source, method, *args)
+        except Exception, fault:
+            traceback.print_exc(file = log)
+            # Handle expected faults
+            if interface == xmlrpclib:
+                result = fault
+                methodresponse = None
+            elif interface == SOAPpy:
+                result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
+                result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
+
+        # Return result
+        if interface == xmlrpclib:
+            if not isinstance(result, GeniFault):
+                result = (result,)
+
+            data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
+        elif interface == SOAPpy:
+            data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
+
+        return data
diff --git a/geni/util/auth.py b/geni/util/auth.py
new file mode 100644 (file)
index 0000000..746f0c0
--- /dev/null
@@ -0,0 +1,197 @@
+#
+# GeniAPI authentication 
+#
+#
+
+import time
+from geni.util.faults import *
+from geni.util.excep import *
+from geni.util.credential import Credential
+from geni.util.trustedroot import TrustedRootList
+from geni.util.hierarchy import Hierarchy
+from geni.util.rights import RightList
+from geni.util.genitable import *
+
+
+class Auth:
+    """
+    Credential based authentication
+    """
+
+    def __init__(self, peer_cert):
+        self.peer_cert = peer_cert
+        self.hierarchy = Hierarchy()
+        self.trusted_cert_list = TrustedRootList().get_list() 
+
+    
+
+    def check(self, cred, operation):
+        """
+        Check the credential against the peer cert (callerGID included 
+        in the credential matches the caller that is connected to the 
+        HTTPS connection, check if the credential was signed by a 
+        trusted cert and check if the credential is allowd to perform 
+        the specified operation.    
+        """
+        self.client_cred = Credential(string = cred)
+        self.client_gid = self.client_cred.get_gid_caller()
+        self.object_gid = self.client_cred.get_gid_object()
+        
+        # make sure the client_gid is not blank
+        if not self.client_gid:
+            raise MissingCallerGID(self.client_cred.get_subject())
+
+        # make sure the client_gid matches client's certificate
+        peer_cert = self.peer_cert
+        if not peer_cert.is_pubkey(self.client_gid.get_pubkey()):
+            raise ConnectionKeyGIDMismatch(self.client_gid.get_subject())
+
+        # make sure the client is allowed to perform the operation
+        if operation:
+            if not self.client_cred.can_perform(operation):
+                raise InsufficientRights(operation)
+
+        if self.trusted_cert_list:
+            self.client_cred.verify_chain(self.trusted_cert_list)
+            if self.client_gid:
+                self.client_gid.verify_chain(self.trusted_cert_list)
+            if self.object_gid:
+                self.object_gid.verify_chain(self.trusted_cert_list)
+
+        return True
+
+        
+    def get_auth_info(self, auth_hrn):
+        """
+        Given an authority name, return the information for that authority.
+        This is basically a stub that calls the hierarchy module.
+        
+        @param auth_hrn human readable name of authority  
+        """
+
+        return self.hierarchy.get_auth_info(auth_hrn)
+
+
+    def get_auth_table(self, auth_name):
+        """
+        Given an authority name, return the database table for that authority.
+        If the databse table does not exist, then one will be automatically
+        created.
+
+        @param auth_name human readable name of authority
+        """
+        auth_info = self.get_auth_info(auth_name)
+        table = GeniTable(hrn=auth_name,
+                          cninfo=auth_info.get_dbinfo())
+        # if the table doesn't exist, then it means we haven't put any records
+        # into this authority yet.
+
+        if not table.exists():
+            print >> log, "Registry: creating table for authority", auth_name
+            table.create()
+    
+        return table
+
+    def veriry_auth_belongs_to_me(self, name):
+        """
+        Verify that an authority belongs to our hierarchy. 
+        This is basically left up to the implementation of the hierarchy
+        module. If the specified name does not belong, ane exception is 
+        thrown indicating the caller should contact someone else.
+
+        @param auth_name human readable name of authority
+        """
+
+        self.get_auth_info(name)
+
+
+    def verify_object_belongs_to_me(self, name):
+        """
+        Verify that an object belongs to our hierarchy. By extension,
+        this implies that the authority that owns the object belongs
+        to our hierarchy. If it does not an exception is thrown.
+    
+        @param name human readable name of object        
+        """
+        auth_name = self.get_authority(name)
+        if not auth_name:
+            # the root authority belongs to the registry by default?
+            # TODO: is this true?
+            return
+        self.verify_auth_belongs_to_me(auth_name) 
+             
+    def verify_auth_belongs_to_me(self, name):
+        # get auth info will throw an exception if the authority doesnt exist
+        self.get_auth_info(name) 
+
+
+    def verify_object_permission(self, name):
+        """
+        Verify that the object gid that was specified in the credential
+        allows permission to the object 'name'. This is done by a simple
+        prefix test. For example, an object_gid for plc.arizona would 
+        match the objects plc.arizona.slice1 and plc.arizona.
+    
+        @param name human readable name to test  
+        """
+        object_hrn = self.object_gid.get_hrn()
+        if object_hrn == name:
+            return
+        if name.startswith(object_hrn + "."):
+            return
+        raise PermissionError(name)
+  
+    def verify_cancreate_credential(self, src_cred, record):
+        """
+        Verify that a user can retrive a particular type of credential. 
+        For slices, the user must be on the researcher list. For SA and
+        MA the user must be on the pi and operator lists respectively 
+        """
+
+        type = record.get_type()
+        cred_object_hrn = src_cred.get_gid_object().get_hrn()
+        if cred_object_hrn in [self.config.GENI_REGISTRY_ROOT_AUTH]:
+            return
+        if type=="slice":
+            researchers = record.get_geni_info().get("researcher", [])
+            if not (cred_object_hrn in researchers):
+                raise PermissionError(cred_object_hrn + " is not in researcher list for " + record.get_name())
+        elif type == "sa":
+            pis = record.get_geni_info().get("pi", [])
+            if not (cred_object_hrn in pis):
+                raise PermissionError(cred_object_hrn + " is not in pi list for " + record.get_name())
+        elif type == "ma":
+            operators = record.get_geni_info().get("operator", [])
+            if not (cred_object_hrn in operators):
+                raise PermissionError(cred_object_hrn + " is not in operator list for " + record.get_name())
+
+    def get_leaf(self, hrn):
+        parts = hrn.split(".")
+        return ".".join(parts[-1:])
+
+    def get_authority(self, hrn):
+
+        parts = hrn.split(".")
+        return ".".join(parts[:-1])
+
+    def get_auth_type(self, type):
+        if (type=="slice") or (type=="user") or (type=="sa"):
+            return "sa"
+        elif (type=="component") or (type=="ma"):
+            return "ma"
+        else:
+            raise UnknownGeniType(type)
+
+    def hrn_to_pl_slicename(self, hrn):
+        parts = hrn.split(".")
+        return parts[-2] + "_" + parts[-1]
+
+    # assuming hrn is the hrn of an authority, return the plc authority name
+    def hrn_to_pl_authname(self, hrn):
+        parts = hrn.split(".")
+        return parts[-1]
+
+    # assuming hrn is the hrn of an authority, return the plc login_base
+    def hrn_to_pl_login_base(self, hrn):
+        return self.hrn_to_pl_authname(hrn)
+      
diff --git a/geni/util/faults.py b/geni/util/faults.py
new file mode 100644 (file)
index 0000000..6332792
--- /dev/null
@@ -0,0 +1,62 @@
+#
+# GeniAPI XML-RPC faults
+#
+#
+
+import xmlrpclib
+
+class GeniFault(xmlrpclib.Fault):
+    def __init__(self, faultCode, faultString, extra = None):
+        if extra:
+            faultString += ": " + extra
+        xmlrpclib.Fault.__init__(self, faultCode, faultString)
+
+class GeniInvalidAPIMethod(GeniFault):
+    def __init__(self, method, role = None, extra = None):
+        faultString = "Invalid method " + method
+        if role:
+            faultString += " for role " + role
+        GeniFault.__init__(self, 100, faultString, extra)
+
+class GeniInvalidArgumentCount(GeniFault):
+    def __init__(self, got, min, max = min, extra = None):
+        if min != max:
+            expected = "%d-%d" % (min, max)
+        else:
+            expected = "%d" % min
+        faultString = "Expected %s arguments, got %d" % \
+                      (expected, got)
+        GeniFault.__init__(self, 101, faultString, extra)
+
+class GeniInvalidArgument(GeniFault):
+    def __init__(self, extra = None, name = None):
+        if name is not None:
+            faultString = "Invalid %s value" % name
+        else:
+            faultString = "Invalid argument"
+        GeniFault.__init__(self, 102, faultString, extra)
+
+class GeniAuthenticationFailure(GeniFault):
+    def __init__(self, extra = None):
+        faultString = "Failed to authenticate call"
+        GeniFault.__init__(self, 103, faultString, extra)
+
+class GeniDBError(GeniFault):
+    def __init__(self, extra = None):
+        faultString = "Database error"
+        GeniFault.__init__(self, 106, faultString, extra)
+
+class GeniPermissionDenied(GeniFault):
+    def __init__(self, extra = None):
+        faultString = "Permission denied"
+        GeniFault.__init__(self, 108, faultString, extra)
+
+class GeniNotImplemented(GeniFault):
+    def __init__(self, extra = None):
+        faultString = "Not fully implemented"
+        GeniFault.__init__(self, 109, faultString, extra)
+
+class GeniAPIError(GeniFault):
+    def __init__(self, extra = None):
+        faultString = "Internal API error"
+        GeniFault.__init__(self, 111, faultString, extra)
diff --git a/geni/util/parameter.py b/geni/util/parameter.py
new file mode 100644 (file)
index 0000000..2a52ce4
--- /dev/null
@@ -0,0 +1,105 @@
+#
+# Shared type definitions
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id: Parameter.py 5574 2007-10-25 20:33:17Z thierry $
+#
+
+from types import *
+from geni.util.faults import *
+
+class Parameter:
+    """
+    Typed value wrapper. Use in accepts and returns to document method
+    parameters. Set the optional and default attributes for
+    sub-parameters (i.e., dict fields).
+    """
+
+    def __init__(self, type, doc = "",
+                 min = None, max = None,
+                 optional = None,
+                 ro = False,
+                 nullok = False):
+        # Basic type of the parameter. Must be a builtin type
+        # that can be marshalled by XML-RPC.
+        self.type = type
+
+        # Documentation string for the parameter
+        self.doc = doc
+
+        # Basic value checking. For numeric types, the minimum and
+        # maximum possible values, inclusive. For string types, the
+        # minimum and maximum possible UTF-8 encoded byte lengths.
+        self.min = min
+        self.max = max
+
+        # Whether the sub-parameter is optional or not. If None,
+        # unknown whether it is optional.
+        self.optional = optional
+
+        # Whether the DB field is read-only.
+        self.ro = ro
+
+        # Whether the DB field can be NULL.
+        self.nullok = nullok
+
+    def type(self):
+        return self.type
+
+    def __repr__(self):
+        return repr(self.type)
+
+class Mixed(tuple):
+    """
+    A list (technically, a tuple) of types. Use in accepts and returns
+    to document method parameters that may return mixed types.
+    """
+
+    def __new__(cls, *types):
+        return tuple.__new__(cls, types)
+
+
+def python_type(arg):
+    """
+    Returns the Python type of the specified argument, which may be a
+    Python type, a typed value, or a Parameter.
+    """
+
+    if isinstance(arg, Parameter):
+        arg = arg.type
+
+    if isinstance(arg, type):
+        return arg
+    else:
+        return type(arg)
+
+def xmlrpc_type(arg):
+    """
+    Returns the XML-RPC type of the specified argument, which may be a
+    Python type, a typed value, or a Parameter.
+    """
+
+    arg_type = python_type(arg)
+
+    if arg_type == NoneType:
+        return "nil"
+    elif arg_type == IntType or arg_type == LongType:
+        return "int"
+    elif arg_type == bool:
+        return "boolean"
+    elif arg_type == FloatType:
+        return "double"
+    elif arg_type in StringTypes:
+        return "string"
+    elif arg_type == ListType or arg_type == TupleType:
+        return "array"
+    elif arg_type == DictType:
+        return "struct"
+    elif arg_type == Mixed:
+        # Not really an XML-RPC type but return "mixed" for
+        # documentation purposes.
+        return "mixed"
+    else:
+        raise GeniAPIError, "XML-RPC cannot marshal %s objects" % arg_type