From: Tony Mack Date: Tue, 27 Jan 2009 00:20:28 +0000 (+0000) Subject: copy util dir into geni dir X-Git-Tag: sfa-0.9-0@14641~702 X-Git-Url: http://git.onelab.eu/?a=commitdiff_plain;h=4974354ff0e3d431b1b6c981f64b8b4bdb9b4eec;p=sfa.git copy util dir into geni dir --- diff --git a/geni/util/__init__.py b/geni/util/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/geni/util/cert.py b/geni/util/cert.py new file mode 100644 index 00000000..5d4796e0 --- /dev/null +++ b/geni/util/cert.py @@ -0,0 +1,471 @@ +## +# Geniwrapper uses two crypto libraries: pyOpenSSL and M2Crypto to implement +# the necessary crypto functionality. Ideally just one of these libraries +# would be used, but unfortunately each of these libraries is independently +# lacking. The pyOpenSSL library is missing many necessary functions, and +# the M2Crypto library has crashed inside of some of the functions. The +# design decision is to use pyOpenSSL whenever possible as it seems more +# stable, and only use M2Crypto for those functions that are not possible +# in pyOpenSSL. +# +# This module exports two classes: Keypair and Certificate. +## + +import os +import tempfile +from OpenSSL import crypto +import M2Crypto +from M2Crypto import X509 +from M2Crypto import EVP + +from excep import * + +## +# Public-private key pairs are implemented by the Keypair class. +# A Keypair object may represent both a public and private key pair, or it +# may represent only a public key (this usage is consistent with OpenSSL). + +class Keypair: + key = None # public/private keypair + m2key = None # public key (m2crypto format) + + ## + # Creates a Keypair object + # @param create If create==True, creates a new public/private key and + # stores it in the object + # @param string If string!=None, load the keypair from the string (PEM) + # @param filename If filename!=None, load the keypair from the file + + def __init__(self, create=False, string=None, filename=None): + if create: + self.create() + if string: + self.load_from_string(string) + if filename: + self.load_from_file(filename) + + ## + # Create a RSA public/private key pair and store it inside the keypair object + + def create(self): + self.key = crypto.PKey() + self.key.generate_key(crypto.TYPE_RSA, 1024) + + ## + # Save the private key to a file + # @param filename name of file to store the keypair in + + def save_to_file(self, filename): + open(filename, 'w').write(self.as_pem()) + + ## + # Load the private key from a file. Implicity the private key includes the public key. + + def load_from_file(self, filename): + buffer = open(filename, 'r').read() + self.load_from_string(buffer) + + ## + # Load the private key from a string. Implicitly the private key includes the public key. + + def load_from_string(self, string): + self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string) + self.m2key = M2Crypto.EVP.load_key_string(string) + + ## + # Load the public key from a string. No private key is loaded. + + def load_pubkey_from_file(self, filename): + # load the m2 public key + m2rsakey = M2Crypto.RSA.load_pub_key(filename) + self.m2key = M2Crypto.EVP.PKey() + self.m2key.assign_rsa(m2rsakey) + + # create an m2 x509 cert + m2name = M2Crypto.X509.X509_Name() + m2name.add_entry_by_txt(field="CN", type=0x1001, entry="junk", len=-1, loc=-1, set=0) + m2x509 = M2Crypto.X509.X509() + m2x509.set_pubkey(self.m2key) + m2x509.set_serial_number(0) + m2x509.set_issuer_name(m2name) + m2x509.set_subject_name(m2name) + ASN1 = M2Crypto.ASN1.ASN1_UTCTIME() + ASN1.set_time(500) + m2x509.set_not_before(ASN1) + m2x509.set_not_after(ASN1) + junk_key = Keypair(create=True) + m2x509.sign(pkey=junk_key.get_m2_pkey(), md="sha1") + + # convert the m2 x509 cert to a pyopenssl x509 + m2pem = m2x509.as_pem() + pyx509 = crypto.load_certificate(crypto.FILETYPE_PEM, m2pem) + + # get the pyopenssl pkey from the pyopenssl x509 + self.key = pyx509.get_pubkey() + + ## + # Load the public key from a string. No private key is loaded. + + def load_pubkey_from_string(self, string): + (f, fn) = tempfile.mkstemp() + os.write(f, string) + os.close(f) + self.load_pubkey_from_file(fn) + os.remove(fn) + + ## + # Return the private key in PEM format. + + def as_pem(self): + return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key) + + ## + # Return an OpenSSL pkey object + + def get_m2_pkey(self): + if not self.m2key: + self.m2key = M2Crypto.EVP.load_key_string(self.as_pem()) + return self.m2key + + ## + # Given another Keypair object, return TRUE if the two keys are the same. + + def get_openssl_pkey(self): + return self.key + + def is_same(self, pkey): + return self.as_pem() == pkey.as_pem() + +## +# The certificate class implements a general purpose X509 certificate, making +# use of the appropriate pyOpenSSL or M2Crypto abstractions. It also adds +# several addition features, such as the ability to maintain a chain of +# parent certificates, and storage of application-specific data. +# +# Certificates include the ability to maintain a chain of parents. Each +# certificate includes a pointer to it's parent certificate. When loaded +# from a file or a string, the parent chain will be automatically loaded. +# When saving a certificate to a file or a string, the caller can choose +# whether to save the parent certificates as well. + +class Certificate: + digest = "md5" + + data = None + cert = None + issuerKey = None + issuerSubject = None + parent = None + + ## + # Create a certificate object. + # + # @param create If create==True, then also create a blank X509 certificate. + # @param subject If subject!=None, then create a blank certificate and set + # it's subject name. + # @param string If string!=None, load the certficate from the string. + # @param filename If filename!=None, load the certficiate from the file. + + def __init__(self, create=False, subject=None, string=None, filename=None): + if create or subject: + self.create() + if subject: + self.set_subject(subject) + if string: + self.load_from_string(string) + if filename: + self.load_from_file(filename) + + ## + # Create a blank X509 certificate and store it in this object. + + def create(self): + self.cert = crypto.X509() + self.cert.set_serial_number(1) + self.cert.gmtime_adj_notBefore(0) + self.cert.gmtime_adj_notAfter(60*60*24*365*5) # five years + + ## + # Given a pyOpenSSL X509 object, store that object inside of this + # certificate object. + + def load_from_pyopenssl_x509(self, x509): + self.cert = x509 + + ## + # Load the certificate from a string + + def load_from_string(self, string): + # if it is a chain of multiple certs, then split off the first one and + # load it + parts = string.split("-----parent-----", 1) + self.cert = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0]) + + # if there are more certs, then create a parent and let the parent load + # itself from the remainder of the string + if len(parts) > 1: + self.parent = self.__class__() + self.parent.load_from_string(parts[1]) + + ## + # Load the certificate from a file + + def load_from_file(self, filename): + file = open(filename) + string = file.read() + self.load_from_string(string) + + ## + # Save the certificate to a string. + # + # @param save_parents If save_parents==True, then also save the parent certificates. + + def save_to_string(self, save_parents=False): + string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert) + if save_parents and self.parent: + string = string + "-----parent-----" + self.parent.save_to_string(save_parents) + return string + + ## + # Save the certificate to a file. + # @param save_parents If save_parents==True, then also save the parent certificates. + + def save_to_file(self, filename, save_parents=False): + string = self.save_to_string(save_parents=save_parents) + open(filename, 'w').write(string) + + ## + # Sets the issuer private key and name + # @param key Keypair object containing the private key of the issuer + # @param subject String containing the name of the issuer + # @param cert (optional) Certificate object containing the name of the issuer + + def set_issuer(self, key, subject=None, cert=None): + self.issuerKey = key + if subject: + # it's a mistake to use subject and cert params at the same time + assert(not cert) + if isinstance(subject, dict) or isinstance(subject, str): + req = crypto.X509Req() + reqSubject = req.get_subject() + if (isinstance(subject, dict)): + for key in reqSubject.keys(): + setattr(reqSubject, key, name[key]) + else: + setattr(reqSubject, "CN", subject) + subject = reqSubject + # subject is not valid once req is out of scope, so save req + self.issuerReq = req + if cert: + # if a cert was supplied, then get the subject from the cert + subject = cert.cert.get_issuer() + assert(subject) + self.issuerSubject = subject + + ## + # Get the issuer name + + def get_issuer(self, which="CN"): + x = self.cert.get_issuer() + return getattr(x, which) + + ## + # Set the subject name of the certificate + + def set_subject(self, name): + req = crypto.X509Req() + subj = req.get_subject() + if (isinstance(name, dict)): + for key in name.keys(): + setattr(subj, key, name[key]) + else: + setattr(subj, "CN", name) + self.cert.set_subject(subj) + ## + # Get the subject name of the certificate + + def get_subject(self, which="CN"): + x = self.cert.get_subject() + return getattr(x, which) + + ## + # Get the public key of the certificate. + # + # @param key Keypair object containing the public key + + def set_pubkey(self, key): + assert(isinstance(key, Keypair)) + self.cert.set_pubkey(key.get_openssl_pkey()) + + ## + # Get the public key of the certificate. + # It is returned in the form of a Keypair object. + + def get_pubkey(self): + m2x509 = X509.load_cert_string(self.save_to_string()) + pkey = Keypair() + pkey.key = self.cert.get_pubkey() + pkey.m2key = m2x509.get_pubkey() + return pkey + + ## + # Add an X509 extension to the certificate. Add_extension can only be called + # once for a particular extension name, due to limitations in the underlying + # library. + # + # @param name string containing name of extension + # @param value string containing value of the extension + + def add_extension(self, name, critical, value): + ext = crypto.X509Extension (name, critical, value) + self.cert.add_extensions([ext]) + + ## + # Get an X509 extension from the certificate + + def get_extension(self, name): + # pyOpenSSL does not have a way to get extensions + m2x509 = X509.load_cert_string(self.save_to_string()) + value = m2x509.get_ext(name).get_value() + return value + + ## + # Set_data is a wrapper around add_extension. It stores the parameter str in + # the X509 subject_alt_name extension. Set_data can only be called once, due + # to limitations in the underlying library. + + def set_data(self, str): + # pyOpenSSL only allows us to add extensions, so if we try to set the + # same extension more than once, it will not work + if self.data != None: + raise "cannot set subjectAltName more than once" + self.data = str + self.add_extension("subjectAltName", 0, "URI:http://" + str) + + ## + # Return the data string that was previously set with set_data + + def get_data(self): + if self.data: + return self.data + + try: + uri = self.get_extension("subjectAltName") + except LookupError: + self.data = None + return self.data + + if not uri.startswith("URI:http://"): + raise "bad encoding in subjectAltName" + self.data = uri[11:] + return self.data + + ## + # Sign the certificate using the issuer private key and issuer subject previous set with set_issuer(). + + def sign(self): + assert self.cert != None + assert self.issuerSubject != None + assert self.issuerKey != None + self.cert.set_issuer(self.issuerSubject) + self.cert.sign(self.issuerKey.get_openssl_pkey(), self.digest) + + ## + # Verify the authenticity of a certificate. + # @param pkey is a Keypair object representing a public key. If Pkey + # did not sign the certificate, then an exception will be thrown. + + def verify(self, pkey): + # pyOpenSSL does not have a way to verify signatures + m2x509 = X509.load_cert_string(self.save_to_string()) + m2pkey = pkey.get_m2_pkey() + # verify it + return m2x509.verify(m2pkey) + + # XXX alternatively, if openssl has been patched, do the much simpler: + # try: + # self.cert.verify(pkey.get_openssl_key()) + # return 1 + # except: + # return 0 + + ## + # Return True if pkey is identical to the public key that is contained in the certificate. + # @param pkey Keypair object + + def is_pubkey(self, pkey): + return self.get_pubkey().is_same(pkey) + + ## + # Given a certificate cert, verify that this certificate was signed by the + # public key contained in cert. Throw an exception otherwise. + # + # @param cert certificate object + + def is_signed_by_cert(self, cert): + k = cert.get_pubkey() + result = self.verify(k) + return result + + ## + # Set the parent certficiate. + # + # @param p certificate object. + + def set_parent(self, p): + self.parent = p + + ## + # Return the certificate object of the parent of this certificate. + + def get_parent(self): + return self.parent + + ## + # Verification examines a chain of certificates to ensure that each parent + # signs the child, and that some certificate in the chain is signed by a + # trusted certificate. + # + # Verification is a basic recursion:
+   #     if this_certificate was signed by trusted_certs:
+   #         return
+   #     else
+   #         return verify_chain(parent, trusted_certs)
+   # 
+ # + # At each recursion, the parent is tested to ensure that it did sign the + # child. If a parent did not sign a child, then an exception is thrown. If + # the bottom of the recursion is reached and the certificate does not match + # a trusted root, then an exception is thrown. + # + # @param Trusted_certs is a list of certificates that are trusted. + # + + def verify_chain(self, trusted_certs = None): + # Verify a chain of certificates. Each certificate must be signed by + # the public key contained in it's parent. The chain is recursed + # until a certificate is found that is signed by a trusted root. + + # TODO: verify expiration time + + # if this cert is signed by a trusted_cert, then we are set + for trusted_cert in trusted_certs: + # TODO: verify expiration of trusted_cert ? + if self.is_signed_by_cert(trusted_cert): + #print self.get_subject(), "is signed by a root" + return + + # if there is no parent, then no way to verify the chain + if not self.parent: + #print self.get_subject(), "has no parent" + raise CertMissingParent(self.get_subject()) + + # if it wasn't signed by the parent... + if not self.is_signed_by_cert(self.parent): + #print self.get_subject(), "is not signed by parent" + return CertNotSignedByParent(self.get_subject()) + + # if the parent isn't verified... + self.parent.verify_chain(trusted_certs) + + return diff --git a/geni/util/config.py b/geni/util/config.py new file mode 100644 index 00000000..36eacb12 --- /dev/null +++ b/geni/util/config.py @@ -0,0 +1,89 @@ +## +# Geniwrapper Configuration Info +# +# This module holds configuration parameters for geniwrapper. There are two +# main pieces of information that are used: the database connection and +# the PLCAPI connection +## + +## +# Geniwrapper uses a MYSQL database to store records. This database may be +# co-located with the PLC database, or it may be a separate database. The +# following parameters define the connection to the database. +# +# Note that Geniwrapper does not access any of the PLC databases directly via +# a mysql connection; All PLC databases are accessed via PLCAPI. + +import os +import sys + +# If we have been checked out into a directory at the same +# level as myplc, where plc_config.py lives. If we are in a +# MyPLC environment, plc_config.py has already been installed +# in site-packages. +myplc = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + \ + os.sep + "myplc" + +class Config: + """ + Parse the bash/Python/PHP version of the configuration file. Very + fast but no type conversions. + """ + + def __init__(self, file = "/usr/share/geniwrapper/util/geni_config"): + # Load plc_config + + loaded = False + path = os.path.dirname(os.path.abspath(__file__)) + filename = file.split(os.sep)[-1] + alt_file = path + os.sep + filename + myplc_file = myplc + os.sep + filename + files = [file, alt_file, myplc_file] + + for file in files: + try: + execfile(file, self.__dict__) + loaded = True + except: + pass + + if not loaded: + raise Exception, "Could not find config in " + ", ".join(files) + + + def load(self, file): + try: + execfile(file, self.__dict__) + except: + raise Exception, "Could not find config in " + file + +plcConfig = Config("/etc/planetlab/plc_config") + +def get_default_dbinfo(): + dbinfo={ 'dbname' : plcConfig.PLC_DB_NAME, + 'address' : plcConfig.PLC_DB_HOST, + 'port' : plcConfig.PLC_DB_PORT, + 'user' : plcConfig.PLC_DB_USER, + 'password' : plcConfig.PLC_DB_PASSWORD + } + + return dbinfo + +## +# Geniwrapper uses a PLCAPI connection to perform operations on the registry, +# such as creating and deleting slices. This connection requires an account +# on the PLC server with full administrator access. +# +# The Url parameter controls whether the connection uses PLCAPI directly (i.e. +# Geniwrapper is located on the same machine as PLC), or uses a XMLRPC connection +# to the PLC machine. If you wish to use the API directly, then remove the Url +# field from the dictionary. + +def get_pl_auth(): + pl_auth = {'Username': plcConfig.PLC_API_MAINTENANCE_USER, + 'AuthMethod': 'capability', + 'AuthString': plcConfig.PLC_MAINTENANCE_PASSWORD, + "Url": 'https://%s:%s%s' %(plcConfig.PLC_API_HOST, plcConfig.PLC_API_PORT, plcConfig.PLC_API_PATH) + } + + return pl_auth diff --git a/geni/util/credential.py b/geni/util/credential.py new file mode 100644 index 00000000..697adf05 --- /dev/null +++ b/geni/util/credential.py @@ -0,0 +1,236 @@ +## +# Implements Geni Credentials +# +# Credentials are layered on top of certificates, and are essentially a +# certificate that stores a tuple of parameters. +## + +from cert import * +from rights import * +from gid import * +import xmlrpclib + +## +# Credential is a tuple: +# (GIDCaller, GIDObject, LifeTime, Privileges, Delegate) +# +# These fields are encoded using xmlrpc into the subjectAltName field of the +# x509 certificate. Note: Call encode() once the fields have been filled in +# to perform this encoding. + +class Credential(Certificate): + gidCaller = None + gidObject = None + lifeTime = None + privileges = None + delegate = False + + ## + # Create a Credential object + # + # @param create If true, create a blank x509 certificate + # @param subject If subject!=None, create an x509 cert with the subject name + # @param string If string!=None, load the credential from the string + # @param filename If filename!=None, load the credential from the file + + def __init__(self, create=False, subject=None, string=None, filename=None): + Certificate.__init__(self, create, subject, string, filename) + + ## + # set the GID of the caller + # + # @param gid GID object of the caller + + def set_gid_caller(self, gid): + self.gidCaller = gid + + ## + # get the GID of the object + + def get_gid_caller(self): + if not self.gidCaller: + self.decode() + return self.gidCaller + + ## + # set the GID of the object + # + # @param gid GID object of the object + + def set_gid_object(self, gid): + self.gidObject = gid + + ## + # get the GID of the object + + def get_gid_object(self): + if not self.gidObject: + self.decode() + return self.gidObject + + ## + # set the lifetime of this credential + # + # @param lifetime lifetime of credential + + def set_lifetime(self, lifeTime): + self.lifeTime = lifeTime + + ## + # get the lifetime of the credential + + def get_lifetime(self): + if not self.lifeTime: + self.decode() + return self.lifeTime + + ## + # set the delegate bit + # + # @param delegate boolean (True or False) + + def set_delegate(self, delegate): + self.delegate = delegate + + ## + # get the delegate bit + + def get_delegate(self): + if not self.delegate: + self.decode() + return self.delegate + + ## + # set the privileges + # + # @param privs either a comma-separated list of privileges of a RightList object + + def set_privileges(self, privs): + if isinstance(privs, str): + self.privileges = RightList(string = privs) + else: + self.privileges = privs + + ## + # return the privileges as a RightList object + + def get_privileges(self): + if not self.privileges: + self.decode() + return self.privileges + + ## + # determine whether the credential allows a particular operation to be + # performed + # + # @param op_name string specifying name of operation ("lookup", "update", etc) + + def can_perform(self, op_name): + rights = self.get_privileges() + if not rights: + return False + return rights.can_perform(op_name) + + ## + # Encode the attributes of the credential into a string and store that + # string in the alt-subject-name field of the X509 object. This should be + # done immediately before signing the credential. + + def encode(self): + dict = {"gidCaller": None, + "gidObject": None, + "lifeTime": self.lifeTime, + "privileges": None, + "delegate": self.delegate} + if self.gidCaller: + dict["gidCaller"] = self.gidCaller.save_to_string(save_parents=True) + if self.gidObject: + dict["gidObject"] = self.gidObject.save_to_string(save_parents=True) + if self.privileges: + dict["privileges"] = self.privileges.save_to_string() + str = xmlrpclib.dumps((dict,), allow_none=True) + self.set_data(str) + + ## + # Retrieve the attributes of the credential from the alt-subject-name field + # of the X509 certificate. This is automatically done by the various + # get_* methods of this class and should not need to be called explicitly. + + def decode(self): + data = self.get_data() + if data: + dict = xmlrpclib.loads(self.get_data())[0][0] + else: + dict = {} + + self.lifeTime = dict.get("lifeTime", None) + self.delegate = dict.get("delegate", None) + + privStr = dict.get("privileges", None) + if privStr: + self.privileges = RightList(string = privStr) + else: + self.privileges = None + + gidCallerStr = dict.get("gidCaller", None) + if gidCallerStr: + self.gidCaller = GID(string=gidCallerStr) + else: + self.gidCaller = None + + gidObjectStr = dict.get("gidObject", None) + if gidObjectStr: + self.gidObject = GID(string=gidObjectStr) + else: + self.gidObject = None + + ## + # Verify that a chain of credentials is valid (see cert.py:verify). In + # addition to the checks for ordinary certificates, verification also + # ensures that the delegate bit was set by each parent in the chain. If + # a delegate bit was not set, then an exception is thrown. + # + # Each credential must be a subset of the rights of the parent. + + def verify_chain(self, trusted_certs = None): + # do the normal certificate verification stuff + Certificate.verify_chain(self, trusted_certs) + + if self.parent: + # make sure the parent delegated rights to the child + if not self.parent.get_delegate(): + raise MissingDelegateBit(self.parent.get_subject()) + + # make sure the rights given to the child are a subset of the + # parents rights + if not self.parent.get_privileges().is_superset(self.get_privileges()): + raise ChildRightsNotSubsetOfParent(self.get_subject()) + + return + + ## + # Dump the contents of a credential to stdout in human-readable format + # + # @param dump_parents If true, also dump the parent certificates + + def dump(self, dump_parents=False): + print "CREDENTIAL", self.get_subject() + + print " privs:", self.get_privileges().save_to_string() + + print " gidCaller:" + gidCaller = self.get_gid_caller() + if gidCaller: + gidCaller.dump(8, dump_parents) + + print " gidObject:" + gidObject = self.get_gid_object() + if gidObject: + gidObject.dump(8, dump_parents) + + print " delegate:", self.get_delegate() + + if self.parent and dump_parents: + print "PARENT", + self.parent.dump(dump_parents) + diff --git a/geni/util/excep.py b/geni/util/excep.py new file mode 100644 index 00000000..5a6c99fd --- /dev/null +++ b/geni/util/excep.py @@ -0,0 +1,129 @@ + +class MalformedHrnException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class TreeException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class NonexistingRecord(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class ExistingRecord(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class NonexistingCredType(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class NonexistingFile(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class InvalidRPCParams(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +# SMBAKER exceptions follow + +class ConnectionKeyGIDMismatch(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class MissingCallerGID(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class RecordNotFound(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class UnknownGeniType(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class MissingAuthority(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class PlanetLabRecordDoesNotExist(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class PermissionError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class InsufficientRights(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class MissingDelegateBit(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class ChildRightsNotSubsetOfParent(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class CertMissingParent(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class CertNotSignedByParent(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class GidInvalidParentHrn(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class SliverDoesNotExist(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + diff --git a/geni/util/geni_config b/geni/util/geni_config new file mode 100644 index 00000000..e70fe4ab --- /dev/null +++ b/geni/util/geni_config @@ -0,0 +1,80 @@ +# Directory where Geni intefaces are installed +GENI_BASE_DIR="/usr/share/geniwrapper/" + +# HRN +# Human readable name for the interfaces +GENI_INTERFACE_HRN="planetlab.us" + + +## Aggregate Configuration +# +# Enable aggregate inteface +# Enable the aggregate inteface. +GENI_AGGREGATE_ENABLED=1 + +# Hostname +# The fully qualified hostname of the aggregate server +GENI_AGGREGATE_HOSTNAME="localhost" + +# IP +# Geni aggregate ip address +GENI_AGGREGATE_IP="127.0.0.1" + +# Port +# Geni aggregate server port +GENI_AGGREGATE_PORT=12346 + + +# Registry Configuration +# +# Enabled +# Enable the registry interface +GENI_REGISTRY_ENABLE=1 + +# Hostname +# The fully qualified hostname of the registry server +GENI_REGISTRY_HOSTNAME="localhost" + +# IP +# Geni registry ip address +GENI_REGISTRY_IP="127.0.0.1" + +# Port +# Geni registry port +GENI_REGISTRY_PORT=12345 + +# Slice Manager Configuration +# +# Enabled +# Enable the slice manager +GENI_SM_ENABLED=1 + + +# PLC Interace Access Configuration +# +# PLC user +# Valid plc account geni interfaces will use for plc requests + +GENI_PLC_USER='root@localhost.localdomain' + +# Passowrd +# Password for user account +GENI_PLC_PASSWORD='root' + +# Hostname +# Fully qualified hostname of PLC interface +GENI_PLC_HOST='localhost' + +# Port +# Port of PLC interface +GENI_PLC_PORT=443 + +# API Path +# Path of PLC api interface +GENI_PLC_API_PATH='PLCAPI' + +# PLC Shell PATH +# Path of PLC shell. This only applies if Geni interfaces and PLC interface are running on the same +# machine +GENI_PLC_SHELL_PATH='/usr/share/plc_api' + diff --git a/geni/util/geniclient.py b/geni/util/geniclient.py new file mode 100644 index 00000000..f3013df6 --- /dev/null +++ b/geni/util/geniclient.py @@ -0,0 +1,333 @@ +## +# This module implements the client-side of the Geni API. Stubs are provided +# that convert the supplied parameters to the necessary format and send them +# via XMLRPC to a Geni Server. +# +# TODO: Investigate ways to combine this with existing PLC API? +## + +import xmlrpclib + +from gid import * +from credential import * +from record import * +from geniticket import * + +## +# ServerException, ExceptionUnmarshaller +# +# Used to convert server exception strings back to an exception. +# from usenet, Raghuram Devarakonda + +class ServerException(Exception): + pass + +class ExceptionUnmarshaller(xmlrpclib.Unmarshaller): + def close(self): + try: + return xmlrpclib.Unmarshaller.close(self) + except xmlrpclib.Fault, e: + raise ServerException(e.faultString) + +## +# GeniTransport +# +# A transport for XMLRPC that works on top of HTTPS + +class GeniTransport(xmlrpclib.Transport): + key_file = None + cert_file = None + def make_connection(self, host): + # create a HTTPS connection object from a host descriptor + # host may be a string, or a (host, x509-dict) tuple + import httplib + host, extra_headers, x509 = self.get_host_info(host) + try: + HTTPS = httplib.HTTPS() + except AttributeError: + raise NotImplementedError( + "your version of httplib doesn't support HTTPS" + ) + else: + return httplib.HTTPS(host, None, key_file=self.key_file, cert_file=self.cert_file) #**(x509 or {})) + + def getparser(self): + unmarshaller = ExceptionUnmarshaller() + parser = xmlrpclib.ExpatParser(unmarshaller) + return parser, unmarshaller + +## +# The GeniClient class provides stubs for executing Geni operations. A given +# client object connects to one server. To connect to multiple servers, create +# multiple GeniClient objects. +# +# The Geni protocol uses an HTTPS connection, and the client's side of the +# connection uses his private key. Generally, this private key must match the +# public key that is containing in the GID that the client is providing for +# those functions that take a GID. + +class GeniClient(): + ## + # Create a new GeniClient object. + # + # @param url is the url of the server + # @param key_file = private key file of client + # @param cert_file = x.509 cert containing the client's public key. This + # could be a GID certificate, or any x.509 cert. + + def __init__(self, url, key_file, cert_file): + self.url = url + self.key_file = key_file + self.cert_file = cert_file + self.transport = GeniTransport() + self.transport.key_file = self.key_file + self.transport.cert_file = self.cert_file + self.server = xmlrpclib.ServerProxy(self.url, self.transport, allow_none=True) + + # ------------------------------------------------------------------------- + # Registry Interface + # ------------------------------------------------------------------------- + + ## + # Create a new GID. For MAs and SAs that are physically located on the + # registry, this allows a owner/operator/PI to create a new GID and have it + # signed by his respective authority. + # + # @param cred credential of caller + # @param name hrn for new GID + # @param uuid unique identifier for new GID + # @param pkey_string public-key string (TODO: why is this a string and not a keypair object?) + # + # @return a GID object + + def create_gid(self, cred, name, uuid, pkey_string): + gid_str = self.server.create_gid(cred.save_to_string(save_parents=True), name, uuid, pkey_string) + return GID(string=gid_str) + + ## + # Retrieve the GID for an object. This function looks up a record in the + # registry and returns the GID of the record if it exists. + # TODO: Is this function needed? It's a shortcut for Resolve() + # + # @param name hrn to look up + # + # @return a GID object + + def get_gid(self, name): + gid_str_list = self.server.get_gid(name) + gid_list = [] + for str in gid_str_list: + gid_list.append(GID(string=str)) + return gid_list + + ## + # Get_self_credential a degenerate version of get_credential used by a + # client to get his initial credential when he doesn't have one. This is + # the same as get_credential(..., cred=None,...). + # + # The registry ensures that the client is the principal that is named by + # (type, name) by comparing the public key in the record's GID to the + # private key used to encrypt the client-side of the HTTPS connection. Thus + # it is impossible for one principal to retrieve another principal's + # credential without having the appropriate private key. + # + # @param type type of object (user | slice | sa | ma | node + # @param name human readable name of object + # + # @return a credential object + + def get_self_credential(self, type, name): + cred_str = self.server.get_self_credential(type, name) + return Credential(string = cred_str) + + ## + # Retrieve a credential for an object. + # + # If cred==None, then the behavior reverts to get_self_credential() + # + # @param cred credential object specifying rights of the caller + # @param type type of object (user | slice | sa | ma | node) + # @param name human readable name of object + # + # @return a credental object + + def get_credential(self, cred, type, name): + if cred == None: + return self.get_self_credential(type, name) + cred_str = self.server.get_credential(cred.save_to_string(save_parents=True), type, name) + return Credential(string = cred_str) + + ## + # List the records in an authority. The objectGID in the supplied credential + # should name the authority that will be listed. + # + # @param cred credential object specifying rights of the caller + # + # @return list of record objects + + def list(self, cred, auth_hrn): + result_dict_list = self.server.list(cred.save_to_string(save_parents=True), auth_hrn) + result_rec_list = [] + for dict in result_dict_list: + result_rec_list.append(GeniRecord(dict=dict)) + return result_rec_list + + ## + # Register an object with the registry. In addition to being stored in the + # Geni database, the appropriate records will also be created in the + # PLC databases. + # + # The geni_info and/or pl_info fields must in the record must be filled + # out correctly depending on the type of record that is being registered. + # + # TODO: The geni_info member of the record should be parsed and the pl_info + # adjusted as necessary (add/remove users from a slice, etc) + # + # @param cred credential object specifying rights of the caller + # @return record to register + # + # @return GID object for the newly-registered record + + def register(self, cred, record): + gid_str = self.server.register(cred.save_to_string(save_parents=True), record.as_dict()) + return GID(string = gid_str) + + ## + # Remove an object from the registry. If the object represents a PLC object, + # then the PLC records will also be removed. + # + # @param cred credential object specifying rights of the caller + # @param type + # @param hrn + + def remove(self, cred, type, hrn): + result = self.server.remove(cred.save_to_string(save_parents=True), type, hrn) + return result + + ## + # Resolve an object in the registry. A given HRN may have multiple records + # associated with it, and therefore multiple records may be returned. The + # caller should check the type fields of the records to find the one that + # he is interested in. + # + # @param cred credential object specifying rights of the caller + # @param name human readable name of object + + def resolve(self, cred, name): + result_dict_list = self.server.resolve(cred.save_to_string(save_parents=True), name) + result_rec_list = [] + for dict in result_dict_list: + result_rec_list.append(GeniRecord(dict=dict)) + return result_rec_list + + ## + # Update an object in the registry. Currently, this only updates the + # PLC information associated with the record. The Geni fields (name, type, + # GID) are fixed. + # + # The record is expected to have the pl_info field filled in with the data + # that should be updated. + # + # TODO: The geni_info member of the record should be parsed and the pl_info + # adjusted as necessary (add/remove users from a slice, etc) + # + # @param cred credential object specifying rights of the caller + # @param record a record object to be updated + + def update(self, cred, record): + result = self.server.update(cred.save_to_string(save_parents=True), record.as_dict()) + return result + + # ------------------------------------------------------------------------ + # Slice Interface + # ------------------------------------------------------------------------ + + ## + # Start a slice. + # + # @param cred a credential identifying the caller (callerGID) and the slice + # (objectGID) + + def start_slice(self, cred): + result = self.server.start_slice(cred.save_to_string(save_parents=True)) + return result + + ## + # Stop a slice. + # + # @param cred a credential identifying the caller (callerGID) and the slice + # (objectGID) + + def stop_slice(self, cred): + result = self.server.stop_slice(cred.save_to_string(save_parents=True)) + return result + + ## + # Reset a slice. + # + # @param cred a credential identifying the caller (callerGID) and the slice + # (objectGID) + + def reset_slice(self, cred): + result = self.server.reset_slice(cred.save_to_string(save_parents=True)) + return result + + ## + # Delete a slice. + # + # @param cred a credential identifying the caller (callerGID) and the slice + # (objectGID) + + def delete_slice(self, cred): + result = self.server.delete_slice(cred.save_to_string(save_parents=True)) + return result + + ## + # List the slices on a component. + # + # @param cred credential object that authorizes the caller + # + # @return a list of slice names + + def list_slices(self, cred): + result = self.server.list_slices(cred.save_to_string(save_parents=True)) + return result + + ## + # Retrieve a ticket. This operation is currently implemented on the + # registry (see SFA, engineering decisions), and is not implemented on + # components. + # + # The ticket is filled in with information from the PLC database. This + # information includes resources, and attributes such as user keys and + # initscripts. + # + # @param cred credential object + # @param name name of the slice to retrieve a ticket for + # @param rspec resource specification dictionary + # + # @return a ticket object + + def get_ticket(self, cred, name, rspec): + ticket_str = self.server.get_ticket(cred.save_to_string(save_parents=True), name, rspec) + ticket = Ticket(string=ticket_str) + return ticket + + ## + # Redeem a ticket. This operation is currently implemented on the + # component. + # + # The ticket is submitted to the node manager, and the slice is instantiated + # or updated as appropriate. + # + # TODO: This operation should return a sliver credential and indicate + # whether or not the component will accept only sliver credentials, or + # will accept both sliver and slice credentials. + # + # @param ticket a ticket object containing the ticket + + def redeem_ticket(self, ticket): + result = self.server.redeem_ticket(ticket.save_to_string(save_parents=True)) + return result + + diff --git a/geni/util/geniserver.py b/geni/util/geniserver.py new file mode 100644 index 00000000..b9975719 --- /dev/null +++ b/geni/util/geniserver.py @@ -0,0 +1,246 @@ +## +# This module implements a general-purpose server layer for geni. +# The same basic server should be usable on the registry, component, or +# other interfaces. +# +# TODO: investigate ways to combine this with existing PLC server? +## + +import SimpleXMLRPCServer + +import sys +import traceback +import threading +import SocketServer +import BaseHTTPServer +import SimpleHTTPServer +import SimpleXMLRPCServer + +from excep import * +from cert import * +from credential import * + +import socket, os +from OpenSSL import SSL + +## +# Verification callback for pyOpenSSL. We do our own checking of keys because +# we have our own authentication spec. Thus we disable several of the normal +# prohibitions that OpenSSL places on certificates + +def verify_callback(conn, x509, err, depth, preverify): + # if the cert has been preverified, then it is ok + if preverify: + #print " preverified" + return 1 + + # we're only passing single certificates, not chains + if depth > 0: + #print " depth > 0 in verify_callback" + return 0 + + # create a Certificate object and load it from the client's x509 + ctx = conn.get_context() + server = ctx.get_app_data() + server.peer_cert = Certificate() + server.peer_cert.load_from_pyopenssl_x509(x509) + + # the certificate verification done by openssl checks a number of things + # that we aren't interested in, so we look out for those error messages + # and ignore them + + # XXX SMBAKER: I don't know what this error is, but it's being returned + # by newer pl nodes. + if err == 9: + #print " X509_V_ERR_CERT_NOT_YET_VALID" + return 1 + + # allow self-signed certificates + if err == 18: + #print " X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT" + return 1 + + # allow certs that don't have an issuer + if err == 20: + #print " X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY" + return 1 + + # allow certs that are untrusted + if err == 21: + #print " X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE" + return 1 + + # allow certs that are untrusted + if err == 27: + #print " X509_V_ERR_CERT_UNTRUSTED" + return 1 + + print " error", err, "in verify_callback" + + return 0 + +## +# Taken from the web (XXX find reference). Implements an HTTPS xmlrpc server + +class SecureXMLRPCServer(BaseHTTPServer.HTTPServer,SimpleXMLRPCServer.SimpleXMLRPCDispatcher): + def __init__(self, server_address, HandlerClass, key_file, cert_file, logRequests=True): + """Secure XML-RPC server. + + It it very similar to SimpleXMLRPCServer but it uses HTTPS for transporting XML data. + """ + self.logRequests = logRequests + + SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, True, None) + SocketServer.BaseServer.__init__(self, server_address, HandlerClass) + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_privatekey_file(key_file) + ctx.use_certificate_file(cert_file) + ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback) + ctx.set_app_data(self) + self.socket = SSL.Connection(ctx, socket.socket(self.address_family, + self.socket_type)) + self.server_bind() + self.server_activate() + + # _dispatch + # + # Convert an exception on the server to a full stack trace and send it to + # the client. + + def _dispatch(self, method, params): + try: + return SimpleXMLRPCServer.SimpleXMLRPCDispatcher._dispatch(self, method, params) + except: + # can't use format_exc() as it is not available in jython yet + # (evein in trunk). + type, value, tb = sys.exc_info() + raise xmlrpclib.Fault(1,''.join(traceback.format_exception(type, value, tb))) + +## +# taken from the web (XXX find reference). Implents HTTPS xmlrpc request handler + +class SecureXMLRpcRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + """Secure XML-RPC request handler class. + + It it very similar to SimpleXMLRPCRequestHandler but it uses HTTPS for transporting XML data. + """ + def setup(self): + self.connection = self.request + self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) + self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) + + def do_POST(self): + """Handles the HTTPS POST request. + + It was copied out from SimpleXMLRPCServer.py and modified to shutdown the socket cleanly. + """ + + try: + # get arguments + data = self.rfile.read(int(self.headers["content-length"])) + # In previous versions of SimpleXMLRPCServer, _dispatch + # could be overridden in this class, instead of in + # SimpleXMLRPCDispatcher. To maintain backwards compatibility, + # check to see if a subclass implements _dispatch and dispatch + # using that method if present. + response = self.server._marshaled_dispatch( + data, getattr(self, '_dispatch', None) + ) + except: # This should only happen if the module is buggy + # internal error, report as HTTP server error + self.send_response(500) + + self.end_headers() + else: + # got a valid XML RPC response + self.send_response(200) + self.send_header("Content-type", "text/xml") + self.send_header("Content-length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + # shut down the connection + self.wfile.flush() + self.connection.shutdown() # Modified here! + +## +# Implements an HTTPS XML-RPC server. Generally it is expected that GENI +# functions will take a credential string, which is passed to +# decode_authentication. Decode_authentication() will verify the validity of +# the credential, and verify that the user is using the key that matches the +# GID supplied in the credential. + +class GeniServer(threading.Thread): + + ## + # Create a new GeniServer object. + # + # @param ip the ip address to listen on + # @param port the port to listen on + # @param key_file private key filename of registry + # @param cert_file certificate filename containing public key + # (could be a GID file) + + def __init__(self, ip, port, key_file, cert_file): + threading.Thread.__init__(self) + self.key = Keypair(filename = key_file) + self.cert = Certificate(filename = cert_file) + self.server = SecureXMLRPCServer((ip, port), SecureXMLRpcRequestHandler, key_file, cert_file) + self.trusted_cert_list = None + self.register_functions() + + ## + # Decode the credential string that was submitted by the caller. Several + # checks are performed to ensure that the credential is valid, and that the + # callerGID included in the credential matches the caller that is + # connected to the HTTPS connection. + + def decode_authentication(self, cred_string, operation): + self.client_cred = Credential(string = cred_string) + 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.server.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) + + ## + # Register functions that will be served by the XMLRPC server. This + # function should be overrided by each descendant class. + + def register_functions(self): + self.server.register_function(self.noop) + + ## + # Sample no-op server function. The no-op function decodes the credential + # that was passed to it. + + def noop(self, cred, anything): + self.decode_authentication(cred) + + return anything + + ## + # Execute the server, serving requests forever. + + def run(self): + self.server.serve_forever() + + diff --git a/geni/util/genitable.py b/geni/util/genitable.py new file mode 100644 index 00000000..53b69c61 --- /dev/null +++ b/geni/util/genitable.py @@ -0,0 +1,124 @@ +# genitable.py +# +# implements support for geni records stored in db tables +# +# TODO: Use existing PLC database methods? or keep this separate? + +import report + +from pg import DB +from gid import * +from record import * + +GENI_TABLE_PREFIX = "geni$" + +class GeniTable(): + def __init__(self, create=False, hrn="unspecified.default.registry", cninfo=None): + global GENI_TABLE_PREFIX + + self.hrn = hrn + + # pgsql doesn't like table names with "." in them, to replace it with "$" + self.tablename = GENI_TABLE_PREFIX + self.hrn.replace(".", "$") + + # establish a connection to the pgsql server + self.cnx = DB(cninfo['dbname'], cninfo['address'], port=cninfo['port'], user=cninfo['user'], passwd=cninfo['password']) + + # if asked to create the table, then create it + if create: + self.create() + + def exists(self): + tableList = self.cnx.get_tables() + if 'public.' + self.tablename in tableList: + return True + if 'public."' + self.tablename + '"' in tableList: + return True + return False + + def create(self): + querystr = "CREATE TABLE " + self.tablename + " ( \ + key text, \ + name text, \ + gid text, \ + type text, \ + pointer integer);" + + self.cnx.query('DROP TABLE IF EXISTS ' + self.tablename) + self.cnx.query(querystr) + + def remove(self, record): + query_str = "DELETE FROM " + self.tablename + " WHERE key = '" + record.get_key() + "'" + self.cnx.query(query_str) + + def insert(self, record): + fieldnames = ["key"] + record.get_field_names() + fieldvals = record.get_field_value_strings(fieldnames) + query_str = "INSERT INTO " + self.tablename + \ + "(" + ",".join(fieldnames) + ") " + \ + "VALUES(" + ",".join(fieldvals) + ")" + #print query_str + self.cnx.query(query_str) + + def update(self, record): + names = record.get_field_names() + pairs = [] + for name in names: + val = record.get_field_value_string(name) + pairs.append(name + " = " + val) + update = ", ".join(pairs) + + query_str = "UPDATE " + self.tablename+ " SET " + update + " WHERE key = '" + record.get_key() + "'" + #print query_str + self.cnx.query(query_str) + + def find_dict(self, type, value, searchfield): + query_str = "SELECT * FROM " + self.tablename + " WHERE " + searchfield + " = '" + str(value) + "'" + dict_list = self.cnx.query(query_str).dictresult() + result_dict_list = [] + for dict in dict_list: + if (type=="*") or (dict['type'] == type): + result_dict_list.append(dict) + return result_dict_list + + def find(self, type, value, searchfield): + result_dict_list = self.find_dict(type, value, searchfield) + result_rec_list = [] + for dict in result_dict_list: + result_rec_list.append(GeniRecord(dict=dict)) + return result_rec_list + + def resolve_dict(self, type, hrn): + return self.find_dict(type, hrn, "name") + + def resolve(self, type, hrn): + return self.find(type, hrn, "name") + + def list_dict(self): + query_str = "SELECT * FROM " + self.tablename + result_dict_list = self.cnx.query(query_str).dictresult() + return result_dict_list + + def list(self): + result_dict_list = self.list_dict() + result_rec_list = [] + for dict in result_dict_list: + result_rec_list.append(GeniRecord(dict=dict)) + return result_rec_list + +def set_geni_table_prefix(x): + global GENI_TABLE_PREFIX + + GENI_TABLE_PREFIX = x + +def geni_records_purge(cninfo): + global GENI_TABLE_PREFIX + + cnx = DB(cninfo['dbname'], cninfo['address'], port=cninfo['port'], user=cninfo['user'], passwd=cninfo['password']) + tableList = cnx.get_tables() + for table in tableList: + if table.startswith(GENI_TABLE_PREFIX) or \ + table.startswith('public.' + GENI_TABLE_PREFIX) or \ + table.startswith('public."' + GENI_TABLE_PREFIX): + report.trace("dropping table " + table) + cnx.query("DROP TABLE " + table) diff --git a/geni/util/geniticket.py b/geni/util/geniticket.py new file mode 100644 index 00000000..1869e581 --- /dev/null +++ b/geni/util/geniticket.py @@ -0,0 +1,128 @@ +# tickets.py +# +# implements GENI tickets +# + +from cert import * +from rights import * +from gid import * +import xmlrpclib + +# Ticket is tuple: +# (gidCaller, gidObject, attributes, rspec, delegate) +# +# gidCaller = GID of the caller performing the operation +# gidObject = GID of the slice +# attributes = slice attributes (keys, vref, instantiation, etc) +# rspec = resources + +class Ticket(Certificate): + gidCaller = None + gidObject = None + attributes = {} + rspec = {} + delegate = False + + def __init__(self, create=False, subject=None, string=None, filename=None): + Certificate.__init__(self, create, subject, string, filename) + + def set_gid_caller(self, gid): + self.gidCaller = gid + + def get_gid_caller(self): + if not self.gidCaller: + self.decode() + return self.gidCaller + + def set_gid_object(self, gid): + self.gidObject = gid + + def get_gid_object(self): + if not self.gidObject: + self.decode() + return self.gidObject + + def set_attributes(self, gid): + self.attributes = gid + + def get_attributes(self): + if not self.attributes: + self.decode() + return self.attributes + + def set_rspec(self, gid): + self.rspec = gid + + def get_rspec(self): + if not self.rspec: + self.decode() + return self.rspec + + def set_delegate(self, delegate): + self.delegate = delegate + + def get_delegate(self): + if not self.delegate: + self.decode() + return self.delegate + + def encode(self): + dict = {"gidCaller": None, + "gidObject": None, + "attributes": self.attributes, + "rspec": self.rspec, + "delegate": self.delegate} + if self.gidCaller: + dict["gidCaller"] = self.gidCaller.save_to_string(save_parents=True) + if self.gidObject: + dict["gidObject"] = self.gidObject.save_to_string(save_parents=True) + str = xmlrpclib.dumps((dict,), allow_none=True) + self.set_data(str) + + def decode(self): + data = self.get_data() + if data: + dict = xmlrpclib.loads(self.get_data())[0][0] + else: + dict = {} + + self.attributes = dict.get("attributes", {}) + self.rspec = dict.get("rspec", {}) + self.delegate = dict.get("delegate", False) + + gidCallerStr = dict.get("gidCaller", None) + if gidCallerStr: + self.gidCaller = GID(string=gidCallerStr) + else: + self.gidCaller = None + + gidObjectStr = dict.get("gidObject", None) + if gidObjectStr: + self.gidObject = GID(string=gidObjectStr) + else: + self.gidObject = None + + def dump(self, dump_parents=False): + print "TICKET", self.get_subject() + + print " gidCaller:" + gidCaller = self.get_gid_caller() + if gidCaller: + gidCaller.dump(8, dump_parents) + + print " gidObject:" + gidObject = self.get_gid_object() + if gidObject: + gidObject.dump(8, dump_parents) + + print " attributes:" + for attrname in self.get_attributes().keys(): + print " ", attrname, self.get_attributes()[attrname] + + print " rspec:" + for attrname in self.get_rspec().keys(): + print " ", attrname, self.get_rspec()[attrname] + + if self.parent and dump_parents: + print "PARENT", + self.parent.dump(dump_parents) diff --git a/geni/util/gid.py b/geni/util/gid.py new file mode 100644 index 00000000..65aff6b3 --- /dev/null +++ b/geni/util/gid.py @@ -0,0 +1,136 @@ +## +# Implements GENI GID. GIDs are based on certificates, and the GID class is a +# descendant of the certificate class. +## + +from cert import * +import uuid +import xmlrpclib + +## +# Create a new uuid. Returns the UUID as a string. + +def create_uuid(): + return str(uuid.uuid4().int) + +## +# GID is a tuplie: +# (uuid, hrn, public_key) +# +# UUID is a unique identifier and is created by the python uuid module +# (or the utility function create_uuid() in gid.py). +# +# HRN is a human readable name. It is a dotted form similar to a backward domain +# name. For example, planetlab.us.arizona.bakers. +# +# PUBLIC_KEY is the public key of the principal identified by the UUID/HRN. +# It is a Keypair object as defined in the cert.py module. +# +# It is expected that there is a one-to-one pairing between UUIDs and HRN, +# but it is uncertain how this would be inforced or if it needs to be enforced. +# +# These fields are encoded using xmlrpc into the subjectAltName field of the +# x509 certificate. Note: Call encode() once the fields have been filled in +# to perform this encoding. + + +class GID(Certificate): + uuid = None + hrn = None + + ## + # Create a new GID object + # + # @param create If true, create the X509 certificate + # @param subject If subject!=None, create the X509 cert and set the subject name + # @param string If string!=None, load the GID from a string + # @param filename If filename!=None, load the GID from a file + + def __init__(self, create=False, subject=None, string=None, filename=None, uuid=None, hrn=None): + Certificate.__init__(self, create, subject, string, filename) + if uuid: + self.uuid = uuid + if hrn: + self.hrn = hrn + + def set_uuid(self, uuid): + self.uuid = uuid + + def get_uuid(self): + if not self.uuid: + self.decode() + return self.uuid + + def set_hrn(self, hrn): + self.hrn = hrn + + def get_hrn(self): + if not self.hrn: + self.decode() + return self.hrn + + ## + # Encode the GID fields and package them into the subject-alt-name field + # of the X509 certificate. This must be called prior to signing the + # certificate. It may only be called once per certificate. + + def encode(self): + dict = {"uuid": self.uuid, + "hrn": self.hrn} + str = xmlrpclib.dumps((dict,)) + self.set_data(str) + + ## + # Decode the subject-alt-name field of the X509 certificate into the + # fields of the GID. This is automatically called by the various get_*() + # functions in this class. + + def decode(self): + data = self.get_data() + if data: + dict = xmlrpclib.loads(self.get_data())[0][0] + else: + dict = {} + + self.uuid = dict.get("uuid", None) + self.hrn = dict.get("hrn", None) + + ## + # Dump the credential to stdout. + # + # @param indent specifies a number of spaces to indent the output + # @param dump_parents If true, also dump the parents of the GID + + def dump(self, indent=0, dump_parents=False): + print " "*indent, " hrn:", self.get_hrn() + print " "*indent, "uuid:", self.get_uuid() + + if self.parent and dump_parents: + print " "*indent, "parent:" + self.parent.dump(indent+4) + + ## + # Verify the chain of authenticity of the GID. First perform the checks + # of the certificate class (verifying that each parent signs the child, + # etc). In addition, GIDs also confirm that the parent's HRN is a prefix + # of the child's HRN. + # + # Verifying these prefixes prevents a rogue authority from signing a GID + # for a principal that is not a member of that authority. For example, + # planetlab.us.arizona cannot sign a GID for planetlab.us.princeton.foo. + + def verify_chain(self, trusted_certs = None): + # do the normal certificate verification stuff + Certificate.verify_chain(self, trusted_certs) + + if self.parent: + # make sure the parent's hrn is a prefix of the child's hrn + if not self.get_hrn().startswith(self.parent.get_hrn()): + raise GidParentHrn(self.parent.get_subject()) + + return + + + + + diff --git a/geni/util/hierarchy.py b/geni/util/hierarchy.py new file mode 100644 index 00000000..a96ec560 --- /dev/null +++ b/geni/util/hierarchy.py @@ -0,0 +1,330 @@ +## +# This module implements a hierarchy of authorities and performs a similar +# function as the "tree" module of the original geniwrapper prototype. An HRN +# is assumed to be a string of authorities separated by dots. For example, +# "planetlab.us.arizona.bakers". Each component of the HRN is a different +# authority, with the last component being a leaf in the tree. +# +# Each authority is stored in a subdirectory on the registry. Inside this +# subdirectory are several files: +# *.GID - GID file +# *.PKEY - private key file +# *.DBINFO - database info +## + +import os +import report +from cert import * +from credential import * +from gid import * +from misc import * +from config import * +from geniticket import * + +## +# The AuthInfo class contains the information for an authority. This information +# includes the GID, private key, and database connection information. + +class AuthInfo(): + hrn = None + gid_object = None + gid_filename = None + privkey_filename = None + dbinfo_filename = None + + ## + # Initialize and authority object. + # + # @param hrn the human readable name of the authority + # @param gid_filename the filename containing the GID + # @param privkey_filename the filename containing the private key + # @param dbinfo_filename the filename containing the database info + + def __init__(self, hrn, gid_filename, privkey_filename, dbinfo_filename): + self.hrn = hrn + self.set_gid_filename(gid_filename) + self.privkey_filename = privkey_filename + self.dbinfo_filename = dbinfo_filename + + ## + # Set the filename of the GID + # + # @param fn filename of file containing GID + + def set_gid_filename(self, fn): + self.gid_filename = fn + self.gid_object = None + + ## + # Get the GID in the form of a GID object + + def get_gid_object(self): + if not self.gid_object: + self.gid_object = GID(filename = self.gid_filename) + return self.gid_object + + ## + # Get the private key in the form of a Keypair object + + def get_pkey_object(self): + return Keypair(filename = self.privkey_filename) + + ## + # Get the dbinfo in the form of a dictionary + + def get_dbinfo(self): + f = file(self.dbinfo_filename) + dict = eval(f.read()) + f.close() + return dict + + ## + # Replace the GID with a new one. The file specified by gid_filename is + # overwritten with the new GID object + # + # @param gid object containing new GID + + def update_gid_object(self, gid): + gid.save_to_file(self.gid_filename) + self.gid_object = gid + +## +# The Hierarchy class is responsible for managing the tree of authorities. +# Each authority is a node in the tree and exists as an AuthInfo object. +# +# The tree is stored on disk in a hierarchical manner than reflects the +# structure of the tree. Each authority is a subdirectory, and each subdirectory +# contains the GID, pkey, and dbinfo files for that authority (as well as +# subdirectories for each sub-authority) + +class Hierarchy(): + ## + # Create the hierarchy object. + # + # @param basedir the base directory to store the hierarchy in + + def __init__(self, basedir="."): + self.basedir = os.path.join(basedir, "authorities") + + ## + # Given a hrn, return the filenames of the GID, private key, and dbinfo + # files. + # + # @param hrn the human readable name of the authority + + def get_auth_filenames(self, hrn): + leaf = get_leaf(hrn) + parent_hrn = get_authority(hrn) + directory = os.path.join(self.basedir, hrn.replace(".", "/")) + + gid_filename = os.path.join(directory, leaf+".gid") + privkey_filename = os.path.join(directory, leaf+".pkey") + dbinfo_filename = os.path.join(directory, leaf+".dbinfo") + + return (directory, gid_filename, privkey_filename, dbinfo_filename) + + ## + # Check to see if an authority exists. An authority exists if it's disk + # files exist. + # + # @param the human readable name of the authority to check + + def auth_exists(self, hrn): + (directory, gid_filename, privkey_filename, dbinfo_filename) = \ + self.get_auth_filenames(hrn) + + return os.path.exists(gid_filename) and \ + os.path.exists(privkey_filename) and \ + os.path.exists(dbinfo_filename) + + ## + # Create an authority. A private key for the authority and the associated + # GID are created and signed by the parent authority. + # + # @param hrn the human readable name of the authority to create + # @param create_parents if true, also create the parents if they do not exist + + def create_auth(self, hrn, create_parents=False): + report.trace("Hierarchy: creating authority: " + hrn) + + # create the parent authority if necessary + parent_hrn = get_authority(hrn) + if (parent_hrn) and (not self.auth_exists(parent_hrn)) and (create_parents): + self.create_auth(parent_hrn, create_parents) + + (directory, gid_filename, privkey_filename, dbinfo_filename) = \ + self.get_auth_filenames(hrn) + + # create the directory to hold the files + try: + os.makedirs(directory) + # if the path already exists then pass + except OSError, (errno, strerr): + if errno == 17: + pass + + pkey = Keypair(create = True) + pkey.save_to_file(privkey_filename) + + gid = self.create_gid(hrn, create_uuid(), pkey) + gid.save_to_file(gid_filename, save_parents=True) + + # XXX TODO: think up a better way for the dbinfo to work + + dbinfo = get_default_dbinfo() + dbinfo_file = file(dbinfo_filename, "w") + dbinfo_file.write(str(dbinfo)) + dbinfo_file.close() + + ## + # Return the AuthInfo object for the specified authority. If the authority + # does not exist, then an exception is thrown. As a side effect, disk files + # and a subdirectory may be created to store the authority. + # + # @param hrn the human readable name of the authority to create. + + def get_auth_info(self, hrn): + #report.trace("Hierarchy: getting authority: " + hrn) + + if not self.auth_exists(hrn): + raise MissingAuthority(hrn) + + (directory, gid_filename, privkey_filename, dbinfo_filename) = \ + self.get_auth_filenames(hrn) + + auth_info = AuthInfo(hrn, gid_filename, privkey_filename, dbinfo_filename) + + # check the GID and see if it needs to be refreshed + gid = auth_info.get_gid_object() + gid_refreshed = self.refresh_gid(gid) + if gid != gid_refreshed: + auth_info.update_gid_object(gid_refreshed) + + return auth_info + + ## + # Create a new GID. The GID will be signed by the authority that is it's + # immediate parent in the hierarchy (and recursively, the parents' GID + # will be signed by its parent) + # + # @param hrn the human readable name to store in the GID + # @param uuid the unique identifier to store in the GID + # @param pkey the public key to store in the GID + + def create_gid(self, hrn, uuid, pkey): + gid = GID(subject=hrn, uuid=uuid, hrn=hrn) + + parent_hrn = get_authority(hrn) + if not parent_hrn: + # if there is no parent hrn, then it must be self-signed. this + # is where we terminate the recursion + gid.set_issuer(pkey, hrn) + else: + # we need the parent's private key in order to sign this GID + parent_auth_info = self.get_auth_info(parent_hrn) + gid.set_issuer(parent_auth_info.get_pkey_object(), parent_auth_info.hrn) + gid.set_parent(parent_auth_info.get_gid_object()) + + gid.set_pubkey(pkey) + gid.encode() + gid.sign() + + return gid + + ## + # Refresh a GID. The primary use of this function is to refresh the + # the expiration time of the GID. It may also be used to change the HRN, + # UUID, or Public key of the GID. + # + # @param gid the GID to refresh + # @param hrn if !=None, change the hrn + # @param uuid if !=None, change the uuid + # @param pubkey if !=None, change the public key + + def refresh_gid(self, gid, hrn=None, uuid=None, pubkey=None): + # TODO: compute expiration time of GID, refresh it if necessary + gid_is_expired = False + + # update the gid if we need to + if gid_is_expired or hrn or uuid or pubkey: + if not hrn: + hrn = gid.get_hrn() + if not uuid: + uuid = gid.get_uuid() + if not pubkey: + pubkey = gid.get_pubkey() + + gid = self.create_gid(hrn, uuid, pubkey) + + return gid + + ## + # Retrieve an authority credential for an authority. The authority + # credential will contain the authority privilege and will be signed by + # the authority's parent. + # + # @param hrn the human readable name of the authority + + def get_auth_cred(self, hrn): + auth_info = self.get_auth_info(hrn) + gid = auth_info.get_gid_object() + + cred = Credential(subject=hrn) + cred.set_gid_caller(gid) + cred.set_gid_object(gid) + cred.set_privileges("authority") + cred.set_delegate(True) + cred.set_pubkey(auth_info.get_gid_object().get_pubkey()) + + parent_hrn = get_authority(hrn) + if not parent_hrn: + # if there is no parent hrn, then it must be self-signed. this + # is where we terminate the recursion + cred.set_issuer(auth_info.get_pkey_object(), hrn) + else: + # we need the parent's private key in order to sign this GID + parent_auth_info = self.get_auth_info(parent_hrn) + cred.set_issuer(parent_auth_info.get_pkey_object(), parent_auth_info.hrn) + cred.set_parent(self.get_auth_cred(parent_hrn)) + + cred.encode() + cred.sign() + + return cred + ## + # Retrieve an authority ticket. An authority ticket is not actually a + # redeemable ticket, but only serves the purpose of being included as the + # parent of another ticket, in order to provide a chain of authentication + # for a ticket. + # + # This looks almost the same as get_auth_cred, but works for tickets + # XXX does similarity imply there should be more code re-use? + # + # @param hrn the human readable name of the authority + + def get_auth_ticket(self, hrn): + auth_info = self.get_auth_info(hrn) + gid = auth_info.get_gid_object() + + ticket = Ticket(subject=hrn) + ticket.set_gid_caller(gid) + ticket.set_gid_object(gid) + ticket.set_delegate(True) + ticket.set_pubkey(auth_info.get_gid_object().get_pubkey()) + + parent_hrn = get_authority(hrn) + if not parent_hrn: + # if there is no parent hrn, then it must be self-signed. this + # is where we terminate the recursion + ticket.set_issuer(auth_info.get_pkey_object(), hrn) + else: + # we need the parent's private key in order to sign this GID + parent_auth_info = self.get_auth_info(parent_hrn) + ticket.set_issuer(parent_auth_info.get_pkey_object(), parent_auth_info.hrn) + ticket.set_parent(self.get_auth_cred(parent_hrn)) + + ticket.encode() + ticket.sign() + + return ticket + diff --git a/geni/util/misc.py b/geni/util/misc.py new file mode 100644 index 00000000..bd703b70 --- /dev/null +++ b/geni/util/misc.py @@ -0,0 +1,31 @@ +from excep import * + +def get_leaf(hrn): + parts = hrn.split(".") + return ".".join(parts[-1:]) + +def get_authority(hrn): + parts = hrn.split(".") + return ".".join(parts[:-1]) + +def get_auth_type(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(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(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(hrn): + return hrn_to_pl_authname(hrn) + diff --git a/geni/util/record.py b/geni/util/record.py new file mode 100644 index 00000000..086723d6 --- /dev/null +++ b/geni/util/record.py @@ -0,0 +1,285 @@ +## +# Implements support for geni records +# +# TODO: Use existing PLC database methods? or keep this separate? +## + +import report +from gid import * + +## +# The GeniRecord class implements a Geni Record. A GeniRecord is a tuple +# (Name, GID, Type, Info). +# +# Name specifies the HRN of the object +# GID is the GID of the object +# Type is user | sa | ma | slice | component +# +# Info is comprised of the following sub-fields +# pointer = a pointer to the record in the PL database +# pl_info = planetlab-specific info (when talking to client) +# geni_info = geni-specific info (when talking to client) +# +# The pointer is interpreted depending on the type of the record. For example, +# if the type=="user", then pointer is assumed to be a person_id that indexes +# into the persons table. +# +# A given HRN may have more than one record, provided that the records are +# of different types. For example, planetlab.us.arizona may have both an SA +# and a MA record, but cannot have two SA records. + +class GeniRecord(): + + ## + # Create a Geni Record + # + # @param name if !=None, assign the name of the record + # @param gid if !=None, assign the gid of the record + # @param type one of user | sa | ma | slice | component + # @param pointer is a pointer to a PLC record + # @param dict if !=None, then fill in this record from the dictionary + + def __init__(self, name=None, gid=None, type=None, pointer=None, dict=None, string=None): + self.dirty = True + self.pl_info = None + self.geni_info = None + self.name = None + self.gid = None + self.type = None + self.pointer = None + if name: + self.set_name(name) + if gid: + self.set_gid(gid) + if type: + self.set_type(type) + if pointer: + self.set_pointer(pointer) + if dict: + self.load_from_dict(dict) + if string: + self.load_from_string(string) + + ## + # Set the name of the record + # + # @param name is a string containing the HRN + + def set_name(self, name): + self.name = name + self.dirty = True + + ## + # Set the GID of the record + # + # @param gid is a GID object or the string representation of a GID object + + def set_gid(self, gid): + if isinstance(gid, str): + self.gid = gid + else: + self.gid = gid.save_to_string(save_parents=True) + self.dirty = True + + ## + # Set the type of the record + # + # @param type is a string: user | sa | ma | slice | component + + def set_type(self, type): + self.type = type + self.dirty = True + + ## + # Set the pointer of the record + # + # @param pointer is an integer containing the ID of a PLC record + + def set_pointer(self, pointer): + self.pointer = pointer + self.dirty = True + + ## + # Set the PLC info of the record + # + # @param pl_info is a dictionary containing planetlab info + + def set_pl_info(self, pl_info): + self.pl_info = pl_info + self.dirty = True + + ## + # Set the geni info the record + # + # @param geni_info is a dictionary containing geni info + + def set_geni_info(self, geni_info): + self.geni_info = geni_info + self.dirty = True + + ## + # Return the pl_info of the record, or an empty dictionary if none exists + + def get_pl_info(self): + if self.pl_info: + return self.pl_info + else: + return {} + + ## + # Return the geni_info of the record, or an empty dictionary if none exists + + def get_geni_info(self): + if self.geni_info: + return self.geni_info + else: + return {} + + ## + # Return the name (HRN) of the record + + def get_name(self): + return self.name + + ## + # Return the type of the record + + def get_type(self): + return self.type + + ## + # Return the pointer of the record. The pointer is an integer that may be + # used to look up the record in the PLC database. The evaluation of pointer + # depends on the type of the record + + def get_pointer(self): + return self.pointer + + ## + # Return the GID of the record, in the form of a GID object + # TODO: not the best name for the function, because we have things called + # gidObjects in the Cred + + def get_gid_object(self): + return GID(string=self.gid) + + ## + # Return a key that uniquely identifies this record among all records in + # Geni. This key is used to uniquely identify the record in the Geni + # database. + + def get_key(self): + return self.name + "#" + self.type + + ## + # Returns a list of field names in this record. pl_info, geni_info are not + # included because they are not part of the record that is stored in the + # database, but are rather computed values from other entities + + def get_field_names(self): + return ["name", "gid", "type", "pointer"] + + ## + # Given a field name ("name", "gid", ...) return the value of that field. + # + # @param name is the name of field to be returned + + def get_field_value_string(self, fieldname): + if fieldname == "key": + val = self.get_key() + else: + val = getattr(self, fieldname) + if isinstance(val, str): + return "'" + str(val) + "'" + else: + return str(val) + + ## + # Given a list of field names, return a list of values for those fields. + # + # @param fieldnames is a list of field names + + def get_field_value_strings(self, fieldnames): + strs = [] + for fieldname in fieldnames: + strs.append(self.get_field_value_string(fieldname)) + return strs + + ## + # Return the record in the form of a dictionary + + def as_dict(self): + dict = {} + names = self.get_field_names() + for name in names: + dict[name] = getattr(self, name) + + if self.pl_info: + dict['pl_info'] = self.pl_info + + if self.geni_info: + dict['geni_info'] = self.geni_info + + return dict + + ## + # Load the record from a dictionary + # + # @param dict dictionary to load record fields from + + def load_from_dict(self, dict): + self.set_name(dict['name']) + self.set_gid(dict['gid']) + self.set_type(dict['type']) + self.set_pointer(dict['pointer']) + if "pl_info" in dict: + self.set_pl_info(dict["pl_info"]) + if "geni_info" in dict: + self.set_geni_info(dict["geni_info"]) + + ## + # Save the record to a string. The string contains an XML representation of + # the record. + + def save_to_string(self): + dict = self.as_dict() + str = xmlrpclib.dumps((dict,), allow_none=True) + return str + + ## + # Load the record from a string. The string is assumed to contain an XML + # representation of the record. + + def load_from_string(self, str): + dict = xmlrpclib.loads(str)[0][0] + self.load_from_dict(dict) + + ## + # Dump the record to stdout + # + # @param dump_parents if true, then the parents of the GID will be dumped + + def dump(self, dump_parents=False): + print "RECORD", self.name + print " hrn:", self.name + print " type:", self.type + print " gid:" + if (not self.gid): + print " None" + else: + self.get_gid_object().dump(8, dump_parents) + print " pointer:", self.pointer + + print " geni_info:" + geni_info = getattr(self, "geni_info", {}) + if geni_info: + for key in geni_info.keys(): + print " ", key, ":", geni_info[key] + + print " pl_info:" + pl_info = getattr(self, "pl_info", {}) + if pl_info: + for key in pl_info.keys(): + print " ", key, ":", pl_info[key] + + diff --git a/geni/util/remoteshell.py b/geni/util/remoteshell.py new file mode 100644 index 00000000..9697ae23 --- /dev/null +++ b/geni/util/remoteshell.py @@ -0,0 +1,98 @@ +# remoteshell.py +# +# interface to the PLC api via xmlrpc +# +# RemoteShell() exports an API that looks identical to that exported by +# PLC.Shell.Shell(). It's meant to be a drop in replacement for running +# geniwrapper on a different machine than PLC. + +import xmlrpclib + +class RemoteShell: + def __init__(self): + self.servers = {} + + def call(self, name, pl_auth, *args): + + key = pl_auth["Url"] + "#" + pl_auth["Username"] + + if not (key in self.servers): + server = xmlrpclib.Server(pl_auth["Url"], verbose = 0, allow_none=True) + #server.AdmAuthCheck(pl_auth) + server.AuthCheck(pl_auth) + self.servers[key] = server + + server = self.servers[key] + + arglist = ["pl_auth"] + for arg in args: + arglist.append(repr(arg)) + + str = "server." + name + "(" + ",".join(arglist) + ")" + result = eval(str) + + return result + + # TODO: there's probably an automatic way to import all these stubs + + def AddInitScript(self, pl_auth, *args): + return self.call("AddInitScript", pl_auth, *args) + + def AddNode(self, pl_auth, *args): + return self.call("AddNode", pl_auth, *args) + + def AddPerson(self, pl_auth, *args): + return self.call("AddPerson", pl_auth, *args) + + def AddSite(self, pl_auth, *args): + return self.call("AddSite", pl_auth, *args) + + def AddSlice(self, pl_auth, *args): + return self.call("AddSlice", pl_auth, *args) + + def DeleteNode(self, pl_auth, *args): + return self.call("DeleteNode", pl_auth, *args) + + def DeletePerson(self, pl_auth, *args): + return self.call("DeletePerson", pl_auth, *args) + + def DeleteSite(self, pl_auth, *args): + return self.call("DeleteSite", pl_auth, *args) + + def DeleteSlice(self, pl_auth, *args): + return self.call("DeleteSlice", pl_auth, *args) + + def GetInitScripts(self, pl_auth, *args): + return self.call("GetInitScripts", pl_auth, *args) + + def GetKeys(self, pl_auth, *args): + return self.call("GetKeys", pl_auth, *args) + + def GetNodes(self, pl_auth, *args): + return self.call("GetNodes", pl_auth, *args) + + def GetPersons(self, pl_auth, *args): + return self.call("GetPersons", pl_auth, *args) + + def GetSites(self, pl_auth, *args): + return self.call("GetSites", pl_auth, *args) + + def GetSliceAttributes(self, pl_auth, *args): + return self.call("GetSliceAttributes", pl_auth, *args) + + def GetSlices(self, pl_auth, *args): + return self.call("GetSlices", pl_auth, *args) + + def UpdateNode(self, pl_auth, *args): + return self.call("UpdateNode", pl_auth, *args) + + def UpdatePerson(self, pl_auth, *args): + return self.call("UpdatePerson", pl_auth, *args) + + def UpdateSite(self, pl_auth, *args): + return self.call("UpdateSite", pl_auth, *args) + + def UpdateSlice(self, pl_auth, *args): + return self.call("UpdateSlice", pl_auth, *args) + + diff --git a/geni/util/report.py b/geni/util/report.py new file mode 100644 index 00000000..28dd79ae --- /dev/null +++ b/geni/util/report.py @@ -0,0 +1,5 @@ +def trace(x): + print x + +def error(x): + print x diff --git a/geni/util/rights.py b/geni/util/rights.py new file mode 100644 index 00000000..c2af9dc8 --- /dev/null +++ b/geni/util/rights.py @@ -0,0 +1,155 @@ +## +# This Module implements rights and lists of rights for the Geni wrapper. Rights +# are implemented by two classes: +# +# Right - represents a single right +# +# RightList - represents a list of rights +# +# A right may allow several different operations. For example, the "info" right +# allows "listslices", "listcomponentresources", etc. +## + +## +# privilege_table is a list of priviliges and what operations are allowed +# per privilege. + +privilege_table = {"authority": ["*"], + "refresh": ["remove", "update"], + "resolve": ["resolve", "list", "getcredential"], + "sa": ["*"], + "embed": ["getticket", "redeemslice", "createslice", "deleteslice", "updateslice", "getsliceresources"], + "bind": ["getticket", "loanresources"], + "control": ["updateslice", "createslice", "stopslice", "startslice", "deleteslice", "resetslice", "getsliceresources"], + "info": ["listslices", "listnodes"], + "ma": ["*"]} + +## +# The Right class represents a single privilege. + +class Right: + ## + # Create a new right. + # + # @param kind is a string naming the right. For example "control" + + def __init__(self, kind): + self.kind = kind + + ## + # Test to see if this right object is allowed to perform an operation. + # Returns True if the operation is allowed, False otherwise. + # + # @param op_name is a string naming the operation. For example "listslices". + + def can_perform(self, op_name): + allowed_ops = privilege_table.get(self.kind.lower(), None) + if not allowed_ops: + return False + + # if "*" is specified, then all ops are permitted + if "*" in allowed_ops: + return True + + return (op_name.lower() in allowed_ops) + + ## + # Test to see if this right is a superset of a child right. A right is a + # superset if every operating that is allowed by the child is also allowed + # by this object. + # + # @param child is a Right object describing the child right + + def is_superset(self, child): + my_allowed_ops = privilege_table.get(self.kind.lower(), None) + child_allowed_ops = privilege_table.get(child.kind.lower(), None) + + if "*" in my_allowed_ops: + return True + + for right in child_allowed_ops: + if not right in my_allowed_ops: + return False + + return True + +## +# A RightList object represents a list of privileges. + +class RightList: + ## + # Create a new rightlist object, containing no rights. + # + # @param string if string!=None, load the rightlist from the string + + def __init__(self, string=None): + self.rights = [] + if string: + self.load_from_string(string) + + ## + # Add a right to this list + # + # @param right is either a Right object or a string describing the right + + def add(self, right): + if isinstance(right, str): + right = Right(kind = right) + self.rights.append(right) + + ## + # Load the rightlist object from a string + + def load_from_string(self, string): + self.rights = [] + + # none == no rights, so leave the list empty + if not string: + return + + parts = string.split(",") + for part in parts: + self.rights.append(Right(part)) + + ## + # Save the rightlist object to a string. It is saved in the format of a + # comma-separated list. + + def save_to_string(self): + right_names = [] + for right in self.rights: + right_names.append(right.kind) + + return ",".join(right_names) + + ## + # Check to see if some right in this list allows an operation. This is + # done by evaluating the can_perform function of each operation in the + # list. + # + # @param op_name is an operation to check, for example "listslices" + + def can_perform(self, op_name): + for right in self.rights: + if right.can_perform(op_name): + return True + return False + + ## + # Check to see if all of the rights in this rightlist are a superset + # of all the rights in a child rightlist. A rightlist is a superset + # if there is no operation in the child rightlist that cannot be + # performed in the parent rightlist. + # + # @param child is a rightlist object describing the child + + def is_superset(self, child): + for child_right in child.rights: + allowed = False + for my_right in self.rights: + if my_right.is_superset(child_right): + allowed = True + if not allowed: + return False + return True + diff --git a/geni/util/rspec.py b/geni/util/rspec.py new file mode 100644 index 00000000..23e20f9f --- /dev/null +++ b/geni/util/rspec.py @@ -0,0 +1,163 @@ +import sys +import pprint +import os +from xml.dom import minidom +from types import StringTypes + +class Rspec(): + + def __init__(self, xml = None, xsd = None): + self.xsd = xsd # schema + self.rootNode = None # root of the dom + self.dict = {} # dict of the rspec. + if xml: + if type(xml) == file: + self.parseFile(xml) + if type(xml) == str: + self.parseString(xml) + self.dict = self.toDict() + + def _getText(self, nodelist): + rc = "" + for node in nodelist: + if node.nodeType == node.TEXT_NODE: + rc = rc + node.data + return rc + + # The rspec is comprised of 2 parts, and 1 reference: + # attributes/elements describe individual resources + # complexTypes are used to describe a set of attributes/elements + # complexTypes can include a reference to other complexTypes. + + + def _getName(self, node): + '''Gets name of node. If tag has no name, then return tag's localName''' + name = None + if not node.nodeName.startswith("#"): + if node.localName: + name = node.localName + elif node.attributes.has_key("name"): + name = node.attributes.get("name").value + return name + + + # Attribute. {name : nameofattribute, {items: values}) + def _attributeDict(self, attributeDom): + '''Traverse single attribute node. Create a dict {attributename : {name: value,}]}''' + node = {} # parsed dict + for attr in attributeDom.attributes.keys(): + node[attr] = attributeDom.attributes.get(attr).value + return node + + + def toDict(self, nodeDom = None): + """ + convert this rspec to a dict and return it. + """ + node = {} + if not nodeDom: + nodeDom = self.rootNode + + elementName = nodeDom.nodeName + if elementName and not elementName.startswith("#"): + # attributes have tags and values. get {tag: value}, else {type: value} + node[elementName] = self._attributeDict(nodeDom) + #node.update(self._attributeDict(nodeDom)) + # resolve the child nodes. + if nodeDom.hasChildNodes(): + for child in nodeDom.childNodes: + childName = self._getName(child) + if not childName: + continue + if not node[elementName].has_key(childName): + node[elementName][childName] = [] + #node[childName] = [] + childdict = self.toDict(child) + for value in childdict.values(): + node[elementName][childName].append(value) + #node[childName].append(self.toDict(child)) + return node + + + def toxml(self): + """ + convert this rspec to an xml string and return it. + """ + return self.rootNode.toxml() + + + def toprettyxml(self): + """ + print this rspec in xml in a pretty format. + """ + return self.rootNode.toprettyxml() + + + def parseFile(self, filename): + """ + read a local xml file and store it as a dom object. + """ + dom = minidom.parse(filename) + self.rootNode = dom.childNodes[0] + + + def parseString(self, xml): + """ + read an xml string and store it as a dom object. + """ + xml = xml.replace('\n', '').replace('\t', '').strip() + dom = minidom.parseString(xml) + self.rootNode = dom.childNodes[0] + + + def dict2dom(self, rdict, include_doc = False): + """ + convert a dict object into a dom object. + """ + + def elementNode(tagname, rd): + element = minidom.Element(tagname) + for key in rd.keys(): + if isinstance(rd[key], StringTypes): + element.setAttribute(key, rd[key]) + elif isinstance(rd[key], dict): + child = elementNode(key, rd[key]) + element.appendChild(child) + elif isinstance(rd[key], list): + for item in rd[key]: + child = elementNode(key, item) + element.appendChild(child) + return element + + node = elementNode(rdict.keys()[0], rdict.values()[0]) + if include_doc: + rootNode = minidom.Document() + rootNode.appendChild(node) + else: + rootNode = node + return rootNode + + + def parseDict(self, rdict, include_doc = True): + """ + Convert a dictionary into a dom object and store it. + """ + self.rootNode = self.dict2dom(rdict, include_doc) + + + def getDictsByTagName(self, tagname, dom = None): + """ + Search the dom for all elements with the specified tagname + and return them as a list of dicts + """ + if not dom: + dom = self.rootNode + dicts = [] + doms = dom.getElementsByTagName(tagname) + dictlist = [self.toDict(d) for d in doms] + for item in dictlist: + for value in item.values(): + dicts.append(value) + return dicts + +# vim:ts=4:expandtab diff --git a/geni/util/trustedroot.py b/geni/util/trustedroot.py new file mode 100644 index 00000000..e957655d --- /dev/null +++ b/geni/util/trustedroot.py @@ -0,0 +1,33 @@ +import os + +from gid import * + +class TrustedRootList(): + def __init__(self, dir="./trusted_roots"): + self.basedir = dir + + # create the directory to hold the files + try: + os.makedirs(self.basedir) + # if the path already exists then pass + except OSError, (errno, strerr): + if errno == 17: + pass + + def add_gid(self, gid): + fn = os.path.join(self.basedir, gid.get_hrn() + ".gid") + + gid.save_to_file(fn) + + def get_list(self): + gid_list = [] + + file_list = os.listdir(self.basedir) + for gid_file in file_list: + fn = os.path.join(self.basedir, gid_file) + if os.path.isfile(fn): + gid = GID(filename = fn) + gid_list.append(gid) + + return gid_list + diff --git a/geni/util/util.py b/geni/util/util.py new file mode 100644 index 00000000..f18fdac4 --- /dev/null +++ b/geni/util/util.py @@ -0,0 +1,69 @@ +from excep import * +SR_SUFFIX = '_srr' +CR_SUFFIX = '_crr' + +global_sr_tree = None +global_cr_tree = None + +def set_tree_globals(tree1, tree2): + global global_sr_tree + global global_cr_tree + global_sr_tree = tree1 + global_cr_tree = tree2 + +def get_tree_globals(): + return (global_sr_tree, global_cr_tree) + +#function converts a hierarchical name from geni format to array of strings +def geni_to_arr(name): + arrayName = [] + try: + parts = name.split(".") + for i in range(len(parts)): + arrayName.append(parts[i]) + return arrayName + except: + raise MalformedHrnException(name) + +#used to parse the function name and the parameters specified in "operation_request" +def msg_to_params(str): + try: + return eval(str) + except: + raise InvalidRPCParams(str) + +#returns the authority hrn of a given 'hrn' +def obtain_authority(hrn): + parts = hrn.split(".") + auth_str = '' + if len(parts) > 1: + auth_str = parts[0]+'' + for i in range(1, len(parts)-1): + auth_str = auth_str + '.' + parts[i] + return auth_str + +#returns the last element of an hrn +def get_leaf(hrn): + parts = hrn.split(".") + return parts[len(parts)-1] + +#checks whether the 'hrn_auth' is an authority of 'hrn' +def check_authority(hrn, hrn_auth): + arr = geni_to_arr(hrn) + arr_auth = geni_to_arr(hrn_auth) + try: + for i in range(len(arr_auth)): + if arr[i] != arr_auth[i]: + return False + except: + return False + return True + +def hrn_to_tablename(hrn,type): + hrn = hrn.replace(".","$") + if type == 'slc': + hrn = hrn + SR_SUFFIX + else: + hrn = hrn + CR_SUFFIX + return hrn +