diff options
30 files changed, 1018 insertions, 864 deletions
diff --git a/nova/auth/access.py b/nova/auth/access.py deleted file mode 100644 index 2c780626d..000000000 --- a/nova/auth/access.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright [2010] [Anso Labs, LLC] -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple base set of RBAC rules which map API endpoints to LDAP groups. -For testing accounts, users will always have PM privileges. -""" - - -# This is logically a RuleSet or some such. - -def allow_describe_images(user, project, target_object): - return True - -def allow_describe_instances(user, project, target_object): - return True - -def allow_describe_addresses(user, project, target_object): - return True - -def allow_run_instances(user, project, target_object): - # target_object is a reservation, not an instance - # it needs to include count, type, image, etc. - - # First, is the project allowed to use this image - - # Second, is this user allowed to launch within this project - - # Third, is the count or type within project quota - - return True - -def allow_terminate_instances(user, project, target_object): - # In a project, the PMs and Sysadmins can terminate - return True - -def allow_get_console_output(user, project, target_object): - # If the user launched the instance, - # Or is a sysadmin in the project, - return True - -def allow_allocate_address(user, project, target_object): - # There's no security concern in allocation, - # but it can get expensive. Limit to PM and NE. - return True - -def allow_associate_address(user, project, target_object): - # project NE only - # In future, will perform a CloudAudit scan first - # (Pass / Fail gate) - return True - -def allow_register(user, project, target_object): - return False - -def is_allowed(action, user, project, target_object): - return globals()['allow_%s' % action](user, project, target_object) - diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py index c223b250c..8a5bbdf44 100644 --- a/nova/auth/fakeldap.py +++ b/nova/auth/fakeldap.py @@ -22,6 +22,12 @@ import logging from nova import datastore SCOPE_SUBTREE = 1 +MOD_ADD = 0 +MOD_DELETE = 1 + +SUBS = { + 'groupOfNames': ['novaProject'] +} class NO_SUCH_OBJECT(Exception): @@ -44,6 +50,46 @@ class FakeLDAP(object): def unbind_s(self): pass + def _paren_groups(self, source): + count = 0 + start = 0 + result = [] + for pos in xrange(len(source)): + if source[pos] == '(': + if count == 0: + start = pos + count += 1 + if source[pos] == ')': + count -= 1 + if count == 0: + result.append(source[start:pos+1]) + + def _match_query(self, query, attrs): + inner = query[1:-1] + if inner.startswith('&'): + l, r = self._paren_groups(inner[1:]) + return self._match_query(l, attrs) and self._match_query(r, attrs) + if inner.startswith('|'): + l, r = self._paren_groups(inner[1:]) + return self._match_query(l, attrs) or self._match_query(r, attrs) + if inner.startswith('!'): + return not self._match_query(query[2:-1], attrs) + + (k, sep, v) = inner.partition('=') + return self._match(k, v, attrs) + + def _subs(self, v): + if v in SUBS: + return [v] + SUBS[v] + return [v] + + def _match(self, k, v, attrs): + if attrs.has_key(k): + for v in self._subs(v): + if (v in attrs[k]): + return True + return False + def search_s(self, dn, scope, query=None, fields=None): logging.debug("searching for %s" % dn) filtered = {} @@ -51,12 +97,11 @@ class FakeLDAP(object): for cn, attrs in d.iteritems(): if cn[-len(dn):] == dn: filtered[cn] = attrs + objects = filtered if query: - k,v = query[1:-1].split('=') objects = {} for cn, attrs in filtered.iteritems(): - if attrs.has_key(k) and (v in attrs[k] or - v == attrs[k]): + if self._match_query(query, attrs): objects[cn] = attrs if objects == {}: raise NO_SUCH_OBJECT() @@ -75,7 +120,22 @@ class FakeLDAP(object): self.keeper['objects'] = d def delete_s(self, cn): - logging.debug("creating for %s" % cn) - d = self.keeper['objects'] or {} + logging.debug("deleting %s" % cn) + d = self.keeper['objects'] del d[cn] self.keeper['objects'] = d + + def modify_s(self, cn, attr): + logging.debug("modifying %s" % cn) + d = self.keeper['objects'] + for cmd, k, v in attr: + logging.debug("command %s" % cmd) + if cmd == MOD_ADD: + d[cn][k].append(v) + else: + d[cn][k].remove(v) + self.keeper['objects'] = d + + + + diff --git a/nova/auth/rbac.ldif b/nova/auth/rbac.ldif deleted file mode 100644 index 3878d2c1b..000000000 --- a/nova/auth/rbac.ldif +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright [2010] [Anso Labs, LLC] -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# LDIF fragment to create group branch under root - -#dn: ou=Groups,dc=example,dc=com -#objectclass:organizationalunit -#ou: groups -#description: generic groups branch - -# create the itpeople entry - -dn: cn=sysadmins,ou=Groups,dc=example,dc=com -objectclass: groupofnames -cn: itpeople -description: IT admin group -# add the group members all of which are -# assumed to exist under Users -#member: cn=micky mouse,ou=people,dc=example,dc=com -member: cn=admin,ou=Users,dc=example,dc=com - -dn: cn=netadmins,ou=Groups,dc=example,dc=com -objectclass: groupofnames -cn: netadmins -description: Network admin group -member: cn=admin,ou=Users,dc=example,dc=com - -dn: cn=cloudadmins,ou=Groups,dc=example,dc=com -objectclass: groupofnames -cn: cloudadmins -description: Cloud admin group -member: cn=admin,ou=Users,dc=example,dc=com - -dn: cn=itsec,ou=Groups,dc=example,dc=com -objectclass: groupofnames -cn: itsec -description: IT security users group -member: cn=admin,ou=Users,dc=example,dc=com - -# Example Project Group to demonstrate members -# and project members - -dn: cn=myproject,ou=Groups,dc=example,dc=com -objectclass: groupofnames -objectclass: novaProject -cn: myproject -description: My Project Group -member: cn=admin,ou=Users,dc=example,dc=com -projectManager: cn=admin,ou=Users,dc=example,dc=com diff --git a/nova/auth/signer.py b/nova/auth/signer.py index 00aa066fb..4b0169652 100644 --- a/nova/auth/signer.py +++ b/nova/auth/signer.py @@ -46,12 +46,9 @@ import urllib import base64 from nova.exception import Error -_log = logging.getLogger('signer') -logging.getLogger('signer').setLevel(logging.WARN) - class Signer(object): """ hacked up code from boto/connection.py """ - + def __init__(self, secret_key): self.hmac = hmac.new(secret_key, digestmod=hashlib.sha1) if hashlib.sha256: @@ -59,15 +56,14 @@ class Signer(object): def generate(self, params, verb, server_string, path): if params['SignatureVersion'] == '0': - t = self._calc_signature_0(params) - elif params['SignatureVersion'] == '1': - t = self._calc_signature_1(params) - elif params['SignatureVersion'] == '2': - t = self._calc_signature_2(params, verb, server_string, path) - else: - raise Error('Unknown Signature Version: %s' % self.SignatureVersion) - return t - + return self._calc_signature_0(params) + if params['SignatureVersion'] == '1': + return self._calc_signature_1(params) + if params['SignatureVersion'] == '2': + return self._calc_signature_2(params, verb, server_string, path) + raise Error('Unknown Signature Version: %s' % self.SignatureVersion) + + def _get_utf8_value(self, value): if not isinstance(value, str) and not isinstance(value, unicode): value = str(value) @@ -99,7 +95,7 @@ class Signer(object): return base64.b64encode(self.hmac.digest()) def _calc_signature_2(self, params, verb, server_string, path): - _log.debug('using _calc_signature_2') + logging.debug('using _calc_signature_2') string_to_sign = '%s\n%s\n%s\n' % (verb, server_string, path) if self.hmac_256: hmac = self.hmac_256 @@ -114,13 +110,13 @@ class Signer(object): val = self._get_utf8_value(params[key]) pairs.append(urllib.quote(key, safe='') + '=' + urllib.quote(val, safe='-_~')) qs = '&'.join(pairs) - _log.debug('query string: %s' % qs) + logging.debug('query string: %s' % qs) string_to_sign += qs - _log.debug('string_to_sign: %s' % string_to_sign) + logging.debug('string_to_sign: %s' % string_to_sign) hmac.update(string_to_sign) b64 = base64.b64encode(hmac.digest()) - _log.debug('len(b64)=%d' % len(b64)) - _log.debug('base64 encoded digest: %s' % b64) + logging.debug('len(b64)=%d' % len(b64)) + logging.debug('base64 encoded digest: %s' % b64) return b64 if __name__ == '__main__': diff --git a/nova/auth/slap.sh b/nova/auth/slap.sh index a0df4e0ae..c3369e396 100755 --- a/nova/auth/slap.sh +++ b/nova/auth/slap.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,21 +21,21 @@ cat >/etc/ldap/schema/openssh-lpk_openldap.schema <<LPK_SCHEMA_EOF # # LDAP Public Key Patch schema for use with openssh-ldappubkey # Author: Eric AUGE <eau@phear.org> -# +# # Based on the proposal of : Mark Ruijter # # octetString SYNTAX -attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' - DESC 'MANDATORY: OpenSSH Public key' +attributetype ( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' + DESC 'MANDATORY: OpenSSH Public key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 ) # printableString SYNTAX yes|no objectclass ( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' SUP top AUXILIARY DESC 'MANDATORY: OpenSSH LPK objectclass' - MAY ( sshPublicKey $ uid ) + MAY ( sshPublicKey $ uid ) ) LPK_SCHEMA_EOF @@ -44,7 +44,7 @@ cat >/etc/ldap/schema/nova.schema <<NOVA_SCHEMA_EOF # Person object for Nova # inetorgperson with extra attributes # Author: Vishvananda Ishaya <vishvananda@yahoo.com> -# +# # # using internet experimental oid arc as per BP64 3.1 @@ -54,32 +54,32 @@ objectidentifier novaOCs novaSchema:4 attributetype ( novaAttrs:1 - NAME 'accessKey' - DESC 'Key for accessing data' - EQUALITY caseIgnoreMatch - SUBSTR caseIgnoreSubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - SINGLE-VALUE + NAME 'accessKey' + DESC 'Key for accessing data' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE ) attributetype ( novaAttrs:2 - NAME 'secretKey' - DESC 'Secret key' - EQUALITY caseIgnoreMatch - SUBSTR caseIgnoreSubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - SINGLE-VALUE + NAME 'secretKey' + DESC 'Secret key' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE ) attributetype ( novaAttrs:3 - NAME 'keyFingerprint' - DESC 'Fingerprint of private key' - EQUALITY caseIgnoreMatch - SUBSTR caseIgnoreSubstringsMatch - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - SINGLE-VALUE + NAME 'keyFingerprint' + DESC 'Fingerprint of private key' + EQUALITY caseIgnoreMatch + SUBSTR caseIgnoreSubstringsMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + SINGLE-VALUE ) attributetype ( @@ -96,7 +96,7 @@ attributetype ( NAME 'projectManager' DESC 'Project Managers of a project' SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 - ) + ) objectClass ( novaOCs:1 @@ -120,7 +120,7 @@ objectClass ( novaOCs:3 NAME 'novaProject' DESC 'Container for project' - SUP groupofnames + SUP groupOfNames STRUCTURAL MUST ( cn $ projectManager ) ) diff --git a/nova/auth/users.py b/nova/auth/users.py index d8ea8ac68..b09bcfcf2 100755..100644 --- a/nova/auth/users.py +++ b/nova/auth/users.py @@ -22,6 +22,7 @@ import datetime import logging import os import shutil +import string import tempfile import uuid import zipfile @@ -40,7 +41,6 @@ from nova import exception from nova import flags from nova import crypto from nova import utils -import access as simplerbac from nova import objectstore # for flags @@ -50,16 +50,8 @@ flags.DEFINE_string('ldap_url', 'ldap://localhost', 'Point this at your ldap ser flags.DEFINE_string('ldap_password', 'changeme', 'LDAP password') flags.DEFINE_string('user_dn', 'cn=Manager,dc=example,dc=com', 'DN of admin user') flags.DEFINE_string('user_unit', 'Users', 'OID for Users') -flags.DEFINE_string('ldap_subtree', 'ou=Users,dc=example,dc=com', 'OU for Users') - -flags.DEFINE_string('ldap_sysadmin', - 'cn=sysadmins,ou=Groups,dc=example,dc=com', 'OU for Sysadmins') -flags.DEFINE_string('ldap_netadmin', - 'cn=netadmins,ou=Groups,dc=example,dc=com', 'OU for NetAdmins') -flags.DEFINE_string('ldap_cloudadmin', - 'cn=cloudadmins,ou=Groups,dc=example,dc=com', 'OU for Cloud Admins') -flags.DEFINE_string('ldap_itsec', - 'cn=itsec,ou=Groups,dc=example,dc=com', 'OU for ItSec') +flags.DEFINE_string('user_ldap_subtree', 'ou=Users,dc=example,dc=com', 'OU for Users') +flags.DEFINE_string('project_ldap_subtree', 'ou=Groups,dc=example,dc=com', 'OU for Projects') flags.DEFINE_string('credentials_template', utils.abspath('auth/novarc.template'), @@ -71,61 +63,35 @@ flags.DEFINE_string('credential_cert_file', 'cert.pem', flags.DEFINE_string('credential_rc_file', 'novarc', 'Filename of rc in credentials zip') -_log = logging.getLogger('auth') -_log.setLevel(logging.WARN) - - - -class UserError(exception.ApiError): - pass - -class InvalidKeyPair(exception.ApiError): - pass +class AuthBase(object): + @classmethod + def safe_id(cls, obj): + """this method will return the id of the object if the object is of this class, otherwise + it will return the original object. This allows methods to accept objects or + ids as paramaters""" + if isinstance(obj, cls): + return obj.id + else: + return obj -class User(object): +class User(AuthBase): + """id and name are currently the same""" def __init__(self, id, name, access, secret, admin): - self.manager = UserManager.instance() self.id = id self.name = name self.access = access self.secret = secret self.admin = admin - self.keeper = datastore.Keeper(prefix="user") - def is_admin(self): + """allows user to see objects from all projects""" return self.admin - def has_role(self, role_type): - return self.manager.has_role(self.id, role_type) - - def is_authorized(self, owner_id, action=None): - if self.is_admin() or owner_id == self.id: - return True - if action == None: - return False - project = None #(Fixme) - target_object = None # (Fixme, should be passed in) - return simplerbac.is_allowed(action, self, project, target_object) - - def get_credentials(self): - rc = self.generate_rc() - private_key, signed_cert = self.generate_x509_cert() - - tmpdir = tempfile.mkdtemp() - zf = os.path.join(tmpdir, "temp.zip") - zippy = zipfile.ZipFile(zf, 'w') - zippy.writestr(FLAGS.credential_rc_file, rc) - zippy.writestr(FLAGS.credential_key_file, private_key) - zippy.writestr(FLAGS.credential_cert_file, signed_cert) - zippy.writestr(FLAGS.ca_file, crypto.fetch_ca(self.id)) - zippy.close() - with open(zf, 'rb') as f: - buffer = f.read() - - shutil.rmtree(tmpdir) - return buffer + def is_project_member(self, project): + return UserManager.instance().is_project_member(self, project) + def is_project_manager(self, project): + return UserManager.instance().is_project_manager(self, project) def generate_rc(self): rc = open(FLAGS.credentials_template).read() @@ -140,36 +106,90 @@ class User(object): return rc def generate_key_pair(self, name): - return self.manager.generate_key_pair(self.id, name) - - def generate_x509_cert(self): - return self.manager.generate_x509_cert(self.id) + return UserManager.instance().generate_key_pair(self.id, name) def create_key_pair(self, name, public_key, fingerprint): - return self.manager.create_key_pair(self.id, + return UserManager.instance().create_key_pair(self.id, name, public_key, fingerprint) def get_key_pair(self, name): - return self.manager.get_key_pair(self.id, name) + return UserManager.instance().get_key_pair(self.id, name) def delete_key_pair(self, name): - return self.manager.delete_key_pair(self.id, name) + return UserManager.instance().delete_key_pair(self.id, name) def get_key_pairs(self): - return self.manager.get_key_pairs(self.id) + return UserManager.instance().get_key_pairs(self.id) -class KeyPair(object): - def __init__(self, name, owner, public_key, fingerprint): - self.manager = UserManager.instance() - self.owner = owner - self.name = name + def __repr__(self): + return "User('%s', '%s', '%s', '%s', %s)" % (self.id, self.name, self.access, self.secret, self.admin) + +class KeyPair(AuthBase): + def __init__(self, id, owner_id, public_key, fingerprint): + self.id = id + self.name = id + self.owner_id = owner_id self.public_key = public_key self.fingerprint = fingerprint def delete(self): - return self.manager.delete_key_pair(self.owner, self.name) + return UserManager.instance().delete_key_pair(self.owner, self.name) + + def __repr__(self): + return "KeyPair('%s', '%s', '%s', '%s')" % (self.id, self.owner_id, self.public_key, self.fingerprint) + +class Group(AuthBase): + """id and name are currently the same""" + def __init__(self, id, description = None, member_ids = None): + self.id = id + self.name = id + self.description = description + self.member_ids = member_ids + + def has_member(self, user): + return User.safe_id(user) in self.member_ids + + def __repr__(self): + return "Group('%s', '%s', %s)" % (self.id, self.description, self.member_ids) + +class Project(Group): + def __init__(self, id, project_manager_id, description, member_ids): + self.project_manager_id = project_manager_id + super(Project, self).__init__(id, description, member_ids) + self.keeper = datastore.Keeper(prefix="project-") + + @property + def project_manager(self): + return UserManager.instance().get_user(self.project_manager_id) + + def has_manager(self, user): + return User.safe_id(user) == self.project_manager_id + + def get_credentials(self, user): + rc = user.generate_rc() + private_key, signed_cert = self.generate_x509_cert(user) + + tmpdir = tempfile.mkdtemp() + zf = os.path.join(tmpdir, "temp.zip") + zippy = zipfile.ZipFile(zf, 'w') + zippy.writestr(FLAGS.credential_rc_file, rc) + zippy.writestr(FLAGS.credential_key_file, private_key) + zippy.writestr(FLAGS.credential_cert_file, signed_cert) + zippy.writestr(FLAGS.ca_file, crypto.fetch_ca(self.id)) + zippy.close() + with open(zf, 'rb') as f: + buffer = f.read() + + shutil.rmtree(tmpdir) + return buffer + + def generate_x509_cert(self, user): + return UserManager.instance().generate_x509_cert(user, self) + + def __repr__(self): + return "Project('%s', '%s', '%s', %s)" % (self.id, self.project_manager_id, self.description, self.member_ids) class UserManager(object): def __init__(self): @@ -193,31 +213,69 @@ class UserManager(object): except: pass return cls._instance - def authenticate(self, params, signature, verb='GET', server_string='127.0.0.1:8773', path='/'): + def authenticate(self, access, signature, params, verb='GET', server_string='127.0.0.1:8773', path='/', verify_signature=True): # TODO: Check for valid timestamp - access_key = params['AWSAccessKeyId'] + (access_key, sep, project_name) = access.partition(':') + user = self.get_user_from_access_key(access_key) if user == None: - return None - # hmac can't handle unicode, so encode ensures that secret isn't unicode - expected_signature = signer.Signer(user.secret.encode()).generate(params, verb, server_string, path) - _log.debug('user.secret: %s', user.secret) - _log.debug('expected_signature: %s', expected_signature) - _log.debug('signature: %s', signature) - if signature == expected_signature: - return user - - def has_role(self, user, role, project=None): - # Map role to ldap group - group = FLAGS.__getitem__("ldap_%s" % role) + raise exception.NotFound('No user found for access key') + if project_name is '': + project_name = user.name + + project = self.get_project(project_name) + if project == None: + raise exception.NotFound('No project called %s could be found' % project_name) + if not user.is_admin() and not project.has_member(user): + raise exception.NotFound('User %s is not a member of project %s' % (user.id, project.id)) + if verify_signature: + # hmac can't handle unicode, so encode ensures that secret isn't unicode + expected_signature = signer.Signer(user.secret.encode()).generate(params, verb, server_string, path) + logging.debug('user.secret: %s', user.secret) + logging.debug('expected_signature: %s', expected_signature) + logging.debug('signature: %s', signature) + if signature != expected_signature: + raise exception.NotAuthorized('Signature does not match') + return (user, project) + + def create_project(self, name, manager_user, description, member_users=None): + if member_users: + member_users = [User.safe_id(u) for u in member_users] + with LDAPWrapper() as conn: + return conn.create_project(name, User.safe_id(manager_user), description, member_users) + + def get_projects(self): + with LDAPWrapper() as conn: + return conn.find_projects() + + + def get_project(self, project): with LDAPWrapper() as conn: - return conn.is_member_of(user, group) + return conn.find_project(Project.safe_id(project)) - def add_role(self, user, role, project=None): - # TODO: Project-specific roles - group = FLAGS.__getitem__("ldap_%s" % role) + def add_to_project(self, user, project): with LDAPWrapper() as conn: - return conn.add_to_group(user, group) + return conn.add_to_project(User.safe_id(user), Project.safe_id(project)) + + def is_project_manager(self, user, project): + if not isinstance(project, Project): + project = self.get_project(project) + return project.has_manager(user) + + def is_project_member(self, user, project): + if isinstance(project, Project): + return project.has_member(user) + else: + with LDAPWrapper() as conn: + return conn.is_in_project(User.safe_id(user), project) + + def remove_from_project(self, user, project): + with LDAPWrapper() as conn: + return conn.remove_from_project(User.safe_id(user), Project.safe_id(project)) + + def delete_project(self, project): + with LDAPWrapper() as conn: + return conn.delete_project(Project.safe_id(project)) def get_user(self, uid): with LDAPWrapper() as conn: @@ -231,56 +289,59 @@ class UserManager(object): with LDAPWrapper() as conn: return conn.find_users() - def create_user(self, uid, access=None, secret=None, admin=False): + def create_user(self, user, access=None, secret=None, admin=False, create_project=True): if access == None: access = str(uuid.uuid4()) if secret == None: secret = str(uuid.uuid4()) with LDAPWrapper() as conn: - u = conn.create_user(uid, access, secret, admin) - return u + user = User.safe_id(user) + result = conn.create_user(user, access, secret, admin) + if create_project: + conn.create_project(user, user, user) + return result - def delete_user(self, uid): + def delete_user(self, user, delete_project=True): with LDAPWrapper() as conn: - conn.delete_user(uid) + user = User.safe_id(user) + if delete_project: + try: + conn.delete_project(user) + except exception.NotFound: + pass + conn.delete_user(user) - def generate_key_pair(self, uid, key_name): + def generate_key_pair(self, user, key_name): # generating key pair is slow so delay generation # until after check + user = User.safe_id(user) with LDAPWrapper() as conn: - if not conn.user_exists(uid): - raise UserError("User " + uid + " doesn't exist") - if conn.key_pair_exists(uid, key_name): - raise InvalidKeyPair("The keypair '" + - key_name + - "' already exists.", - "Duplicate") + if not conn.user_exists(user): + raise exception.NotFound("User %s doesn't exist" % user) + if conn.key_pair_exists(user, key_name): + raise exception.Duplicate("The keypair %s already exists" % key_name) private_key, public_key, fingerprint = crypto.generate_key_pair() - self.create_key_pair(uid, key_name, public_key, fingerprint) + self.create_key_pair(User.safe_id(user), key_name, public_key, fingerprint) return private_key, fingerprint - def create_key_pair(self, uid, key_name, public_key, fingerprint): + def create_key_pair(self, user, key_name, public_key, fingerprint): with LDAPWrapper() as conn: - return conn.create_key_pair(uid, key_name, public_key, fingerprint) + return conn.create_key_pair(User.safe_id(user), key_name, public_key, fingerprint) - def get_key_pair(self, uid, key_name): + def get_key_pair(self, user, key_name): with LDAPWrapper() as conn: - return conn.find_key_pair(uid, key_name) + return conn.find_key_pair(User.safe_id(user), key_name) - def get_key_pairs(self, uid): + def get_key_pairs(self, user): with LDAPWrapper() as conn: - return conn.find_key_pairs(uid) + return conn.find_key_pairs(User.safe_id(user)) - def delete_key_pair(self, uid, key_name): + def delete_key_pair(self, user, key_name): with LDAPWrapper() as conn: - conn.delete_key_pair(uid, key_name) - - def get_signed_zip(self, uid): - user = self.get_user(uid) - return user.get_credentials() + conn.delete_key_pair(User.safe_id(user), key_name) - def generate_x509_cert(self, uid): - (private_key, csr) = crypto.generate_x509_cert(self.__cert_subject(uid)) + def generate_x509_cert(self, user, project): + (private_key, csr) = crypto.generate_x509_cert(self.__cert_subject(User.safe_id(user))) # TODO - This should be async call back to the cloud controller - signed_cert = crypto.sign_csr(csr, uid) + signed_cert = crypto.sign_csr(csr, Project.safe_id(project)) return (private_key, signed_cert) def sign_cert(self, csr, uid): @@ -328,41 +389,63 @@ class LDAPWrapper(object): return [x[1] for x in res] def find_users(self): - attrs = self.find_objects(FLAGS.ldap_subtree, '(objectclass=novaUser)') + attrs = self.find_objects(FLAGS.user_ldap_subtree, '(objectclass=novaUser)') return [self.__to_user(attr) for attr in attrs] def find_key_pairs(self, uid): - dn = 'uid=%s,%s' % (uid, FLAGS.ldap_subtree) - attrs = self.find_objects(dn, '(objectclass=novaKeyPair)') + attrs = self.find_objects(self.__uid_to_dn(uid), '(objectclass=novaKeyPair)') return [self.__to_key_pair(uid, attr) for attr in attrs] - def find_user(self, name): - dn = 'uid=%s,%s' % (name, FLAGS.ldap_subtree) - attr = self.find_object(dn, '(objectclass=novaUser)') - return self.__to_user(attr) + def find_projects(self): + attrs = self.find_objects(FLAGS.project_ldap_subtree, '(objectclass=novaProject)') + return [self.__to_project(attr) for attr in attrs] - def user_exists(self, name): - return self.find_user(name) != None + def find_groups_with_member(self, tree, dn): + attrs = self.find_objects(tree, '(&(objectclass=groupOfNames)(member=%s))' % dn ) + return [self.__to_group(attr) for attr in attrs] + + def find_user(self, uid): + attr = self.find_object(self.__uid_to_dn(uid), '(objectclass=novaUser)') + return self.__to_user(attr) def find_key_pair(self, uid, key_name): - dn = 'cn=%s,uid=%s,%s' % (key_name, - uid, - FLAGS.ldap_subtree) + dn = 'cn=%s,%s' % (key_name, + self.__uid_to_dn(uid)) attr = self.find_object(dn, '(objectclass=novaKeyPair)') return self.__to_key_pair(uid, attr) + def find_group(self, dn): + """uses dn directly instead of custructing it from name""" + attr = self.find_object(dn, '(objectclass=groupOfNames)') + return self.__to_group(attr) + + def find_project(self, name): + dn = 'cn=%s,%s' % (name, + FLAGS.project_ldap_subtree) + attr = self.find_object(dn, '(objectclass=novaProject)') + return self.__to_project(attr) + + def user_exists(self, name): + return self.find_user(name) != None + + def key_pair_exists(self, uid, key_name): + return self.find_key_pair(uid, key_name) != None + + def project_exists(self, name): + return self.find_project(name) != None + + def group_exists(self, dn): + return self.find_group(dn) != None + def delete_key_pairs(self, uid): keys = self.find_key_pairs(uid) if keys != None: for key in keys: self.delete_key_pair(uid, key.name) - def key_pair_exists(self, uid, key_name): - return self.find_key_pair(uid, key_name) != None - def create_user(self, name, access_key, secret_key, is_admin): if self.user_exists(name): - raise UserError("LDAP user " + name + " already exists") + raise exception.Duplicate("LDAP user %s already exists" % name) attr = [ ('objectclass', ['person', 'organizationalPerson', @@ -376,22 +459,115 @@ class LDAPWrapper(object): ('accessKey', [access_key]), ('isAdmin', [str(is_admin).upper()]), ] - self.conn.add_s('uid=%s,%s' % (name, FLAGS.ldap_subtree), - attr) + self.conn.add_s(self.__uid_to_dn(name), attr) return self.__to_user(dict(attr)) - def create_project(self, name, project_manager): - # PM can be user object or string containing DN - pass - - def is_member_of(self, name, group): - return True - - def add_to_group(self, name, group): - pass + def create_project(self, name, manager_uid, description, member_uids = None): + if self.project_exists(name): + raise exception.Duplicate("Project can't be created because project %s already exists" % name) + if not self.user_exists(manager_uid): + raise exception.NotFound("Project can't be created because manager %s doesn't exist" % manager_uid) + manager_dn = self.__uid_to_dn(manager_uid) + members = [] + if member_uids != None: + for member_uid in member_uids: + if not self.user_exists(member_uid): + raise exception.NotFound("Project can't be created because user %s doesn't exist" % member_uid) + members.append(self.__uid_to_dn(member_uid)) + # always add the manager as a member because members is required + if not manager_dn in members: + members.append(manager_dn) + attr = [ + ('objectclass', ['novaProject']), + ('cn', [name]), + ('description', [description]), + ('projectManager', [manager_dn]), + ('member', members) + ] + self.conn.add_s('cn=%s,%s' % (name, FLAGS.project_ldap_subtree), attr) + return self.__to_project(dict(attr)) + + def add_to_project(self, uid, project_id): + dn = 'cn=%s,%s' % (project_id, FLAGS.project_ldap_subtree) + return self.add_to_group(uid, dn) + + def remove_from_project(self, uid, project_id): + dn = 'cn=%s,%s' % (project_id, FLAGS.project_ldap_subtree) + return self.remove_from_group(uid, dn) + + def is_in_project(self, uid, project_id): + dn = 'cn=%s,%s' % (project_id, FLAGS.project_ldap_subtree) + return self.is_in_group(uid, dn) + + def __create_group(self, group_dn, name, uid, description, member_uids = None): + if self.group_exists(name): + raise exception.Duplicate("Group can't be created because group %s already exists" % name) + members = [] + if member_uids != None: + for member_uid in member_uids: + if not self.user_exists(member_uid): + raise exception.NotFound("Group can't be created because user %s doesn't exist" % member_uid) + members.append(self.__uid_to_dn(member_uid)) + dn = self.__uid_to_dn(uid) + if not dn in members: + members.append(dn) + attr = [ + ('objectclass', ['groupOfNames']), + ('cn', [name]), + ('description', [description]), + ('member', members) + ] + self.conn.add_s(group_dn, attr) + return self.__to_group(dict(attr)) - def remove_from_group(self, name, group): - pass + def is_in_group(self, uid, group_dn): + if not self.user_exists(uid): + raise exception.NotFound("User %s can't be searched in group becuase the user doesn't exist" % (uid,)) + if not self.group_exists(group_dn): + return False + res = self.find_object(group_dn, + '(member=%s)' % self.__uid_to_dn(uid)) + return res != None + + def add_to_group(self, uid, group_dn): + if not self.user_exists(uid): + raise exception.NotFound("User %s can't be added to the group becuase the user doesn't exist" % (uid,)) + if not self.group_exists(group_dn): + raise exception.NotFound("The group at dn %s doesn't exist" % (group_dn,)) + if self.is_in_group(uid, group_dn): + raise exception.Duplicate("User %s is already a member of the group %s" % (uid, group_dn)) + attr = [ + (ldap.MOD_ADD, 'member', self.__uid_to_dn(uid)) + ] + self.conn.modify_s(group_dn, attr) + + def remove_from_group(self, uid, group_dn): + if not self.group_exists(group_dn): + raise exception.NotFound("The group at dn %s doesn't exist" % (group_dn,)) + if not self.user_exists(uid): + raise exception.NotFound("User %s can't be removed from the group because the user doesn't exist" % (uid,)) + if not self.is_in_group(uid, group_dn): + raise exception.NotFound("User %s is not a member of the group" % (uid,)) + attr = [ + (ldap.MOD_DELETE, 'member', self.__uid_to_dn(uid)) + ] + try: + self.conn.modify_s(group_dn, attr) + except ldap.OBJECT_CLASS_VIOLATION: + logging.debug("Attempted to remove the last member of a group. Deleting the group instead.") + self.delete_group(group_dn) + + def remove_from_all(self, uid): + # FIXME(vish): what if deleted user is a project manager? + if not self.user_exists(uid): + raise exception.NotFound("User %s can't be removed from all because the user doesn't exist" % (uid,)) + dn = self.__uid_to_dn(uid) + attr = [ + (ldap.MOD_DELETE, 'member', dn) + ] + projects = self.find_groups_with_member(FLAGS.project_ldap_subtree, dn) + for project in projects: + self.conn.modify_s('cn=%s,%s' % (project.id, FLAGS.project_ldap_subtree), attr) def create_key_pair(self, uid, key_name, public_key, fingerprint): """create's a public key in the directory underneath the user""" @@ -403,41 +579,46 @@ class LDAPWrapper(object): ('sshPublicKey', [public_key]), ('keyFingerprint', [fingerprint]), ] - self.conn.add_s('cn=%s,uid=%s,%s' % (key_name, - uid, - FLAGS.ldap_subtree), - attr) + self.conn.add_s('cn=%s,%s' % (key_name, + self.__uid_to_dn(uid)), + attr) return self.__to_key_pair(uid, dict(attr)) def find_user_by_access_key(self, access): - query = '(' + 'accessKey' + '=' + access + ')' - dn = FLAGS.ldap_subtree + query = '(accessKey=%s)' % access + dn = FLAGS.user_ldap_subtree return self.__to_user(self.find_object(dn, query)) + def delete_user(self, uid): + if not self.user_exists(uid): + raise exception.NotFound("User %s doesn't exist" % uid) + self.delete_key_pairs(uid) + self.remove_from_all(uid) + self.conn.delete_s('uid=%s,%s' % (uid, + FLAGS.user_ldap_subtree)) + def delete_key_pair(self, uid, key_name): if not self.key_pair_exists(uid, key_name): - raise UserError("Key Pair " + - key_name + - " doesn't exist for user " + - uid) + raise exception.NotFound("Key Pair %s doesn't exist for user %s" % + (key_name, uid)) self.conn.delete_s('cn=%s,uid=%s,%s' % (key_name, uid, - FLAGS.ldap_subtree)) + FLAGS.user_ldap_subtree)) - def delete_user(self, name): - if not self.user_exists(name): - raise UserError("User " + - name + - " doesn't exist") - self.delete_key_pairs(name) - self.conn.delete_s('uid=%s,%s' % (name, - FLAGS.ldap_subtree)) + def delete_group(self, group_dn): + if not self.group_exists(group_dn): + raise exception.NotFound("Group at dn %s doesn't exist" % group_dn) + self.conn.delete_s(group_dn) + + def delete_project(self, name): + project_dn = 'cn=%s,%s' % (name, FLAGS.project_ldap_subtree) + self.delete_group(project_dn) def __to_user(self, attr): if attr == None: return None return User( id = attr['uid'][0], - name = attr['uid'][0], + name = attr['cn'][0], access = attr['accessKey'][0], secret = attr['secretKey'][0], admin = (attr['isAdmin'][0] == 'TRUE') @@ -447,8 +628,35 @@ class LDAPWrapper(object): if attr == None: return None return KeyPair( - owner = owner, - name = attr['cn'][0], + id = attr['cn'][0], + owner_id = owner, public_key = attr['sshPublicKey'][0], fingerprint = attr['keyFingerprint'][0], ) + + def __to_group(self, attr): + if attr == None: + return None + member_dns = attr.get('member', []) + return Group( + id = attr['cn'][0], + description = attr.get('description', [None])[0], + member_ids = [self.__dn_to_uid(x) for x in member_dns] + ) + + def __to_project(self, attr): + if attr == None: + return None + member_dns = attr.get('member', []) + return Project( + id = attr['cn'][0], + project_manager_id = self.__dn_to_uid(attr['projectManager'][0]), + description = attr.get('description', [None])[0], + member_ids = [self.__dn_to_uid(x) for x in member_dns] + ) + + def __dn_to_uid(self, dn): + return dn.split(',')[0].split('=')[1] + + def __uid_to_dn(self, dn): + return 'uid=%s,%s' % (dn, FLAGS.user_ldap_subtree) diff --git a/nova/compute/linux_net.py b/nova/compute/linux_net.py index 0983241f9..319ccbdd6 100644 --- a/nova/compute/linux_net.py +++ b/nova/compute/linux_net.py @@ -1,10 +1,25 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 - -import signal +# Copyright [2010] [Anso Labs, LLC] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging import os -import nova.utils +import signal import subprocess +from nova import utils + # todo(ja): does the definition of network_path belong here? from nova import flags @@ -12,16 +27,16 @@ FLAGS=flags.FLAGS def execute(cmd): if FLAGS.fake_network: - print "FAKE NET: %s" % cmd + logging.debug("FAKE NET: %s" % cmd) return "fake", 0 else: - nova.utils.execute(cmd) + utils.execute(cmd) def runthis(desc, cmd): if FLAGS.fake_network: execute(cmd) else: - nova.utils.runthis(desc,cmd) + utils.runthis(desc,cmd) def Popen(cmd): if FLAGS.fake_network: @@ -110,7 +125,7 @@ def start_dnsmasq(network): os.kill(pid, signal.SIGHUP) return except Exception, e: - logging.debug("Killing dnsmasq threw %s", e) + logging.debug("Hupping dnsmasq threw %s", e) # otherwise delete the existing leases file and start dnsmasq lease_file = dhcp_file(network.vlan, 'leases') @@ -124,7 +139,10 @@ def stop_dnsmasq(network): pid = dnsmasq_pid_for(network) if pid: - os.kill(pid, signal.SIGTERM) + try: + os.kill(pid, signal.SIGTERM) + except Exception, e: + logging.debug("Killing dnsmasq threw %s", e) def dhcp_file(vlan, kind): """ return path to a pid, leases or conf file for a vlan """ diff --git a/nova/compute/model.py b/nova/compute/model.py index 78ed3a101..2754e9e6d 100644 --- a/nova/compute/model.py +++ b/nova/compute/model.py @@ -1,4 +1,4 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 expandtab +# vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ InstanceDirectory manager. True >>> inst = InstDir['i-123'] >>> inst['ip'] = "192.168.0.3" ->>> inst['owner_id'] = "projectA" +>>> inst['project_id'] = "projectA" >>> inst.save() True @@ -46,6 +46,8 @@ from nova import utils FLAGS = flags.FLAGS +flags.DEFINE_string('instances_prefix', 'compute-', + 'prefix for keepers for instances') # TODO(ja): singleton instance of the directory class InstanceDirectory(object): @@ -62,11 +64,12 @@ class InstanceDirectory(object): def by_project(self, project): """ returns a list of instance objects for a project """ - for instance_id in self.keeper['project:%s:instances' % project]: + for instance_id in self.keeper.smembers('project:%s:instances' % project): yield Instance(instance_id) def by_node(self, node_id): """ returns a list of instances for a node """ + for instance in self.all: if instance['node_name'] == node_id: yield instance @@ -83,17 +86,13 @@ class InstanceDirectory(object): pass def exists(self, instance_id): - if instance_id in self.keeper['instances']: - return True - return False + return self.keeper.set_is_member('instances', instance_id) @property def all(self): """ returns a list of all instances """ - instances = self.keeper['instances'] - if instances != None: - for instance_id in self.keeper['instances']: - yield Instance(instance_id) + for instance_id in self.keeper.set_members('instances'): + yield Instance(instance_id) def new(self): """ returns an empty Instance object, with ID """ @@ -114,10 +113,12 @@ class Instance(object): if self.state: self.initial_state = self.state else: - self.state = {'state' : 'pending', - 'instance_id' : instance_id, - 'node_name' : 'unassigned', - 'owner_id' : 'unassigned' } + self.state = {'state': 'pending', + 'instance_id': instance_id, + 'node_name': 'unassigned', + 'project_id': 'unassigned', + 'user_id': 'unassigned' + } @property def __redis_key(self): @@ -143,8 +144,8 @@ class Instance(object): def save(self): """ update the directory with the state from this instance - make sure you've set the owner_id before you call save - for the first time. + make sure you've set the project_id and user_id before you call save + for the first time. """ # TODO(ja): implement hmset in redis-py and use it # instead of multiple calls to hset @@ -157,17 +158,23 @@ class Instance(object): state[key] = val self.keeper[self.__redis_key] = state if self.initial_state == {}: - self.keeper.set_add('project:%s:instances' % self.state['owner_id'], + self.keeper.set_add('project:%s:instances' % self.project, self.instance_id) self.keeper.set_add('instances', self.instance_id) self.initial_state = self.state return True + @property + def project(self): + if self.state.get('project_id', None): + return self.state['project_id'] + return self.state.get('owner_id', 'unassigned') + def destroy(self): """ deletes all related records from datastore. - does NOT do anything to running libvirt state. + does NOT do anything to running libvirt state. """ - self.keeper.set_remove('project:%s:instances' % self.state['owner_id'], + self.keeper.set_remove('project:%s:instances' % self.project, self.instance_id) del self.keeper[self.__redis_key] self.keeper.set_remove('instances', self.instance_id) @@ -184,18 +191,18 @@ class Instance(object): pass # class Reservation(object): -# """ ORM wrapper for a batch of launched instances """ -# def __init__(self): -# pass +# """ ORM wrapper for a batch of launched instances """ +# def __init__(self): +# pass # -# def userdata(self): -# """ """ -# pass +# def userdata(self): +# """ """ +# pass # # # class NodeDirectory(object): -# def __init__(self): -# pass +# def __init__(self): +# pass # if __name__ == "__main__": diff --git a/nova/compute/network.py b/nova/compute/network.py index 612295f27..9a9bb9527 100644 --- a/nova/compute/network.py +++ b/nova/compute/network.py @@ -26,7 +26,6 @@ from nova import vendor import IPy from nova import datastore -import nova.exception from nova.compute import exception from nova import flags from nova import utils @@ -113,13 +112,13 @@ class Network(object): for idx in range(3, len(self.network)-2): yield self.network[idx] - def allocate_ip(self, user_id, mac): + def allocate_ip(self, user_id, project_id, mac): for ip in self.range(): address = str(ip) if not address in self.hosts.keys(): - logging.debug("Allocating IP %s to %s" % (address, user_id)) + logging.debug("Allocating IP %s to %s" % (address, project_id)) self.hosts[address] = { - "address" : address, "user_id" : user_id, 'mac' : mac + "address" : address, "user_id": user_id, "project_id" : project_id, 'mac' : mac } self.express(address=address) return address @@ -238,7 +237,6 @@ class DHCPNetwork(VirtNetwork): else: linux_net.start_dnsmasq(self) - class PrivateNetwork(DHCPNetwork): def __init__(self, **kwargs): super(PrivateNetwork, self).__init__(**kwargs) @@ -249,23 +247,18 @@ class PrivateNetwork(DHCPNetwork): 'network': self.network_str, 'hosts': self.hosts} - def express(self, *args, **kwargs): - super(PrivateNetwork, self).express(*args, **kwargs) - - - class PublicNetwork(Network): def __init__(self, network="192.168.216.0/24", **kwargs): super(PublicNetwork, self).__init__(network=network, **kwargs) self.express() - def allocate_ip(self, user_id, mac): + def allocate_ip(self, user_id, project_id, mac): for ip in self.range(): address = str(ip) if not address in self.hosts.keys(): - logging.debug("Allocating IP %s to %s" % (address, user_id)) + logging.debug("Allocating IP %s to %s" % (address, project_id)) self.hosts[address] = { - "address" : address, "user_id" : user_id, 'mac' : mac + "address" : address, "user_id": user_id, "project_id" : project_id, 'mac' : mac } self.express(address=address) return address @@ -354,8 +347,8 @@ class VlanPool(object): self.vlans = kwargs.get('vlans', {}) self.vlanpool = {} self.manager = users.UserManager.instance() - for user_id, vlan in self.vlans.iteritems(): - self.vlanpool[vlan] = user_id + for project_id, vlan in self.vlans.iteritems(): + self.vlanpool[vlan] = project_id def to_dict(self): return {'vlans': self.vlans} @@ -380,25 +373,25 @@ class VlanPool(object): parsed = json.loads(json_string) return cls.from_dict(parsed) - def assign_vlan(self, user_id, vlan): - logging.debug("Assigning vlan %s to user %s" % (vlan, user_id)) - self.vlans[user_id] = vlan - self.vlanpool[vlan] = user_id - return self.vlans[user_id] - - def next(self, user_id): - for old_user_id, vlan in self.vlans.iteritems(): - if not self.manager.get_user(old_user_id): - _get_keeper()["%s-default" % old_user_id] = {} - del _get_keeper()["%s-default" % old_user_id] - del self.vlans[old_user_id] - return self.assign_vlan(user_id, vlan) + def assign_vlan(self, project_id, vlan): + logging.debug("Assigning vlan %s to project %s" % (vlan, project_id)) + self.vlans[project_id] = vlan + self.vlanpool[vlan] = project_id + return self.vlans[project_id] + + def next(self, project_id): + for old_project_id, vlan in self.vlans.iteritems(): + if not self.manager.get_project(old_project_id): + _get_keeper()["%s-default" % old_project_id] = {} + del _get_keeper()["%s-default" % old_project_id] + del self.vlans[old_project_id] + return self.assign_vlan(project_id, vlan) vlans = self.vlanpool.keys() vlans.append(self.start) nextvlan = max(vlans) + 1 if nextvlan == self.end: raise exception.AddressNotAllocated("Out of VLANs") - return self.assign_vlan(user_id, nextvlan) + return self.assign_vlan(project_id, nextvlan) class NetworkController(object): @@ -442,37 +435,37 @@ class NetworkController(object): if address_record.get(u'instance_id', 'free') == instance_id: return address_record[u'address'] - def get_users_network(self, user_id): - """ get a user's private network, allocating one if needed """ + def get_project_network(self, project_id): + """ get a project's private network, allocating one if needed """ - user = self.manager.get_user(user_id) - if not user: - raise Exception("User %s doesn't exist, uhoh." % user_id) - usernet = self.get_network_from_name("%s-default" % user_id) - if not usernet: + project = self.manager.get_project(project_id) + if not project: + raise Exception("Project %s doesn't exist, uhoh." % project_id) + project_net = self.get_network_from_name("%s-default" % project_id) + if not project_net: pool = self.vlan_pool - vlan = pool.next(user_id) + vlan = pool.next(project_id) private_pool = NetworkPool() network_str = private_pool.get_from_vlan(vlan) - logging.debug("Constructing network %s and %s for %s" % (network_str, vlan, user_id)) - usernet = PrivateNetwork( + logging.debug("Constructing network %s and %s for %s" % (network_str, vlan, project_id)) + project_net = PrivateNetwork( network=network_str, vlan=vlan) - _get_keeper()["%s-default" % user_id] = usernet.to_dict() + _get_keeper()["%s-default" % project_id] = project_net.to_dict() _get_keeper()['vlans'] = pool.to_dict() - return usernet + return project_net - def allocate_address(self, user_id, mac=None, type=PrivateNetwork): + def allocate_address(self, user_id, project_id, mac=None, type=PrivateNetwork): ip = None net_name = None if type == PrivateNetwork: - net = self.get_users_network(user_id) - ip = net.allocate_ip(user_id, mac) + net = self.get_project_network(project_id) + ip = net.allocate_ip(user_id, project_id, mac) net_name = net.name - _get_keeper()["%s-default" % user_id] = net.to_dict() + _get_keeper()["%s-default" % project_id] = net.to_dict() else: net = self.public_net - ip = net.allocate_ip(user_id, mac) + ip = net.allocate_ip(user_id, project_id, mac) net_name = net.name _get_keeper()['public'] = net.to_dict() return (ip, net_name) @@ -483,19 +476,19 @@ class NetworkController(object): rv = net.deallocate_ip(str(address)) _get_keeper()['public'] = net.to_dict() return rv - for user in self.manager.get_users(): - if address in self.get_users_network(user.id).network: - net = self.get_users_network(user.id) + for project in self.manager.get_projects(): + if address in self.get_project_network(project.id).network: + net = self.get_project_network(project.id) rv = net.deallocate_ip(str(address)) - _get_keeper()["%s-default" % user.id] = net.to_dict() + _get_keeper()["%s-default" % project.id] = net.to_dict() return rv raise exception.AddressNotAllocated() def describe_addresses(self, type=PrivateNetwork): if type == PrivateNetwork: addresses = [] - for user in self.manager.get_users(): - addresses.extend(self.get_users_network(user.id).list_addresses()) + for project in self.manager.get_projects(): + addresses.extend(self.get_project_network(project.id).list_addresses()) return addresses return self.public_net.list_addresses() @@ -512,8 +505,8 @@ class NetworkController(object): return rv def express(self,address=None): - for user in self.manager.get_users(): - self.get_users_network(user.id).express() + for project in self.manager.get_projects(): + self.get_project_network(project.id).express() def report_state(self): pass diff --git a/nova/compute/node.py b/nova/compute/node.py index a4de0f98a..0f9a15672 100644 --- a/nova/compute/node.py +++ b/nova/compute/node.py @@ -57,8 +57,6 @@ flags.DEFINE_bool('use_s3', True, 'whether to get images from s3 or use local copy') flags.DEFINE_string('instances_path', utils.abspath('../instances'), 'where instances are stored on disk') -flags.DEFINE_string('instances_prefix', 'compute-', - 'prefix for keepers for instances') INSTANCE_TYPES = {} INSTANCE_TYPES['m1.tiny'] = {'memory_mb': 512, 'vcpus': 1, 'local_gb': 0} @@ -73,7 +71,6 @@ INSTANCE_TYPES['c1.medium'] = {'memory_mb': 2048, 'vcpus': 4, 'local_gb': 10} # be a singleton PROCESS_POOL_SIZE = 4 - class Node(object, service.Service): """ Manages the running instances. @@ -240,7 +237,6 @@ def _create_image(data, libvirt_xml): def image_url(path): return "%s:%s/_images/%s" % (FLAGS.s3_host, FLAGS.s3_port, path) - logging.info(basepath('disk')) try: os.makedirs(data['basepath']) @@ -309,15 +305,17 @@ class Instance(object): return (self.state == Instance.RUNNING or self.state == 'running') def __init__(self, conn, pool, name, data): - # TODO(termie): pool should probably be a singleton instead of being passed - # here and in the classmethods """ spawn an instance with a given name """ # TODO(termie): pool should probably be a singleton instead of being passed # here and in the classmethods self._pool = pool self._conn = conn + # TODO(vish): this can be removed after data has been updated + # data doesn't seem to have a working iterator so in doesn't work + if not data.get('owner_id', None) is None: + data['user_id'] = data['owner_id'] + data['project_id'] = data['owner_id'] self.datamodel = data - print data # NOTE(termie): to be passed to multiprocess self._s must be # pickle-able by cPickle @@ -344,7 +342,8 @@ class Instance(object): self._s['image_id'] = data.get('image_id', FLAGS.default_image) self._s['kernel_id'] = data.get('kernel_id', FLAGS.default_kernel) self._s['ramdisk_id'] = data.get('ramdisk_id', FLAGS.default_ramdisk) - self._s['owner_id'] = data.get('owner_id', '') + self._s['user_id'] = data.get('user_id', None) + self._s['project_id'] = data.get('project_id', self._s['user_id']) self._s['node_name'] = data.get('node_name', '') self._s['user_data'] = data.get('user_data', '') self._s['ami_launch_index'] = data.get('ami_launch_index', None) @@ -352,7 +351,6 @@ class Instance(object): self._s['reservation_id'] = data.get('reservation_id', None) # self._s['state'] = Instance.NOSTATE self._s['state'] = data.get('state', Instance.NOSTATE) - self._s['key_data'] = data.get('key_data', None) # TODO: we may not need to save the next few @@ -415,7 +413,6 @@ class Instance(object): def update_state(self): info = self.info() - self._s['state'] = info['state'] self.datamodel['state'] = info['state'] self.datamodel['node_name'] = FLAGS.node_name self.datamodel.save() @@ -427,7 +424,6 @@ class Instance(object): raise exception.Error('trying to destroy already destroyed' ' instance: %s' % self.name) - self._s['state'] = Instance.SHUTDOWN self.datamodel['state'] = 'shutting_down' self.datamodel.save() try: diff --git a/nova/crypto.py b/nova/crypto.py index 6add55ee5..1f35ffa39 100644 --- a/nova/crypto.py +++ b/nova/crypto.py @@ -39,21 +39,20 @@ flags.DEFINE_string('keys_path', utils.abspath('../keys'), 'Where we keep our ke flags.DEFINE_string('ca_path', utils.abspath('../CA'), 'Where we keep our root CA') flags.DEFINE_boolean('use_intermediate_ca', False, 'Should we use intermediate CAs for each project?') - -def ca_path(username): - if username: - return "%s/INTER/%s/cacert.pem" % (FLAGS.ca_path, username) +def ca_path(project_id): + if project_id: + return "%s/INTER/%s/cacert.pem" % (FLAGS.ca_path, project_id) return "%s/cacert.pem" % (FLAGS.ca_path) -def fetch_ca(username=None, chain=True): +def fetch_ca(project_id=None, chain=True): if not FLAGS.use_intermediate_ca: - username = None + project_id = None buffer = "" - if username: - with open(ca_path(username),"r") as cafile: + if project_id: + with open(ca_path(project_id),"r") as cafile: buffer += cafile.read() - if username and not chain: - return buffer + if not chain: + return buffer with open(ca_path(None),"r") as cafile: buffer += cafile.read() return buffer @@ -104,7 +103,6 @@ def generate_x509_cert(subject="/C=US/ST=California/L=The Mission/O=CloudFed/OU= shutil.rmtree(tmpdir) return (private_key, csr) - def sign_csr(csr_text, intermediate=None): if not FLAGS.use_intermediate_ca: intermediate = None @@ -118,7 +116,6 @@ def sign_csr(csr_text, intermediate=None): os.chdir(start) return _sign_csr(csr_text, user_ca) - def _sign_csr(csr_text, ca_folder): tmpfolder = tempfile.mkdtemp() csrfile = open("%s/inbound.csr" % (tmpfolder), "w") @@ -197,7 +194,7 @@ def mkcacert(subject='nova', years=1): # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. diff --git a/nova/datastore.py b/nova/datastore.py index 57940d98b..97c00264d 100644 --- a/nova/datastore.py +++ b/nova/datastore.py @@ -264,6 +264,12 @@ class SqliteKeeper(object): group.remove(value) self[item] = group + def set_members(self, item): + group = self[item] + if not group: + group = [] + return group + def set_fetch(self, item): # TODO(termie): I don't really know what set_fetch is supposed to do group = self[item] @@ -354,6 +360,10 @@ class RedisKeeper(object): item = slugify(item, self.prefix) return Redis.instance().srem(item, json.dumps(value)) + def set_members(self, item): + item = slugify(item, self.prefix) + return [json.loads(v) for v in Redis.instance().smembers(item)] + def set_fetch(self, item): item = slugify(item, self.prefix) for obj in Redis.instance().sinter([item]): diff --git a/nova/endpoint/admin.py b/nova/endpoint/admin.py index e9880acc5..b51929a83 100644 --- a/nova/endpoint/admin.py +++ b/nova/endpoint/admin.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -61,7 +61,6 @@ class AdminController(object): API Controller for users, node status, and worker mgmt. Trivial admin_only wrapper will be replaced with RBAC, allowing project managers to administer project users. - """ def __init__(self, user_manager, node_manager=None): self.user_manager = user_manager @@ -80,16 +79,14 @@ class AdminController(object): def describe_users(self, _context, **_kwargs): """Returns all users - should be changed to deal with a list. """ - return {'userSet': + return {'userSet': [user_dict(u) for u in self.user_manager.get_users()] } @admin_only def register_user(self, _context, name, **_kwargs): """ Creates a new user, and returns generated credentials. """ - self.user_manager.create_user(name) - - return user_dict(self.user_manager.get_user(name)) + return user_dict(self.user_manager.create_user(name)) @admin_only def deregister_user(self, _context, name, **_kwargs): @@ -102,14 +99,17 @@ class AdminController(object): return True @admin_only - def generate_x509_for_user(self, _context, name, **_kwargs): + def generate_x509_for_user(self, _context, name, project=None, **kwargs): """Generates and returns an x509 certificate for a single user. Is usually called from a client that will wrap this with access and secret key info, and return a zip file. """ + if project is None: + project = name + project = self.user_manager.get_project(project) user = self.user_manager.get_user(name) - return user_dict(user, base64.b64encode(user.get_credentials())) - + return user_dict(user, base64.b64encode(project.get_credentials(user))) + @admin_only def describe_nodes(self, _context, **_kwargs): """Returns status info for all nodes. Includes: @@ -120,12 +120,11 @@ class AdminController(object): * DHCP servers running * Iptables / bridges """ - return {'nodeSet': - [node_dict(n) for n in self.node_manager.get_nodes()] } - + return {'nodeSet': + [node_dict(n) for n in self.node_manager.get_nodes()] } + @admin_only def describe_node(self, _context, name, **_kwargs): """Returns status info for single node. """ return node_dict(self.node_manager.get_node(name)) - diff --git a/nova/endpoint/api.py b/nova/endpoint/api.py index 5bbda3f56..e70694210 100755 --- a/nova/endpoint/api.py +++ b/nova/endpoint/api.py @@ -1,13 +1,13 @@ #!/usr/bin/python # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -41,7 +41,6 @@ from nova.auth import users FLAGS = flags.FLAGS flags.DEFINE_integer('cc_port', 8773, 'cloud controller port') - _log = logging.getLogger("api") _log.setLevel(logging.DEBUG) @@ -63,9 +62,10 @@ def _underscore_to_xmlcase(str): class APIRequestContext(object): - def __init__(self, handler, user): + def __init__(self, handler, user, project): self.handler = handler self.user = user + self.project = project self.request_id = ''.join( [random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-') for x in xrange(20)] @@ -73,13 +73,11 @@ class APIRequestContext(object): class APIRequest(object): - def __init__(self, handler, controller, action): - self.handler = handler + def __init__(self, controller, action): self.controller = controller self.action = action - def send(self, user, **kwargs): - context = APIRequestContext(self.handler, user) + def send(self, context, **kwargs): try: method = getattr(self.controller, @@ -227,7 +225,6 @@ class MetadataRequestHandler(tornado.web.RequestHandler): self.print_data(data) self.finish() - class APIRequestHandler(tornado.web.RequestHandler): def get(self, controller_name): self.execute(controller_name) @@ -257,7 +254,7 @@ class APIRequestHandler(tornado.web.RequestHandler): # Get requested action and remove authentication args for final request. try: action = args.pop('Action')[0] - args.pop('AWSAccessKeyId') + access = args.pop('AWSAccessKeyId')[0] args.pop('SignatureMethod') args.pop('SignatureVersion') args.pop('Version') @@ -266,15 +263,18 @@ class APIRequestHandler(tornado.web.RequestHandler): raise tornado.web.HTTPError(400) # Authenticate the request. - user = self.application.user_manager.authenticate( - auth_params, - signature, - self.request.method, - self.request.host, - self.request.path - ) - - if not user: + try: + (user, project) = users.UserManager.instance().authenticate( + access, + signature, + auth_params, + self.request.method, + self.request.host, + self.request.path + ) + + except exception.Error, ex: + logging.debug("Authentication Failure: %s" % ex) raise tornado.web.HTTPError(403) _log.debug('action: %s' % action) @@ -282,8 +282,9 @@ class APIRequestHandler(tornado.web.RequestHandler): for key, value in args.items(): _log.debug('arg: %s\t\tval: %s' % (key, value)) - request = APIRequest(self, controller, action) - d = request.send(user, **args) + request = APIRequest(controller, action) + context = APIRequestContext(self, user, project) + d = request.send(context, **args) # d.addCallback(utils.debug) # TODO: Wrap response in AWS XML format diff --git a/nova/endpoint/cloud.py b/nova/endpoint/cloud.py index 27dd81aa2..53290e386 100644 --- a/nova/endpoint/cloud.py +++ b/nova/endpoint/cloud.py @@ -19,6 +19,7 @@ dispatched to other nodes via AMQP RPC. State is via distributed datastore. """ +import base64 import json import logging import os @@ -58,7 +59,6 @@ class CloudController(object): sent to the other nodes. """ def __init__(self): - self._instances = datastore.Keeper(FLAGS.instances_prefix) self.instdir = model.InstanceDirectory() self.network = network.NetworkController() self.setup() @@ -97,7 +97,7 @@ class CloudController(object): return self.instdir.by_ip(ip) def get_metadata(self, ip): - i = self.instdir.by_ip(ip) + i = self.get_instance_by_ip(ip) if i is None: return None if i['key_name']: @@ -145,7 +145,6 @@ class CloudController(object): data['product-codes'] = i['product_codes'] return data - def describe_availability_zones(self, context, **kwargs): return {'availabilityZoneInfo': [{'zoneName': 'nova', 'zoneState': 'available'}]} @@ -207,11 +206,9 @@ class CloudController(object): def get_console_output(self, context, instance_id, **kwargs): # instance_id is passed in as a list of instances - instance = self.instdir.get(instance_id[0]) + instance = self._get_instance(context, instance_id[0]) if instance['state'] == 'pending': raise exception.ApiError('Cannot get output for pending instance') - if not context.user.is_authorized(instance.get('owner_id', None)): - raise exception.ApiError('Not authorized to view output') return rpc.call('%s.%s' % (FLAGS.compute_topic, instance['node_name']), {"method": "get_console_output", "args" : {"instance_id": instance_id[0]}}) @@ -225,7 +222,7 @@ class CloudController(object): def describe_volumes(self, context, **kwargs): volumes = [] for volume in self.volumes: - if context.user.is_authorized(volume.get('user_id', None)): + if context.user.is_admin() or volume['project_id'] == context.project.id: v = self.format_volume(context, volume) volumes.append(v) return defer.succeed({'volumeSet': volumes}) @@ -252,36 +249,59 @@ class CloudController(object): "args" : {"size": size, "user_id": context.user.id}}) def _format_result(result): - volume = self._get_volume(result['result']) + volume = self._get_volume(context, result['result']) return {'volumeSet': [self.format_volume(context, volume)]} res.addCallback(_format_result) return res - def _get_by_id(self, nodes, id): - if nodes == {}: - raise exception.NotFound("%s not found" % id) - for node_name, node in nodes.iteritems(): - if node.has_key(id): - return node_name, node[id] - raise exception.NotFound("%s not found" % id) - - def _get_volume(self, volume_id): + def _convert_address(self, network_address): + # FIXME(vish): this should go away when network.py stores info properly + address = {} + address['public_ip'] == network_address[u'address'] + address['user_id'] == network_address[u'user_id'] + address['project_id'] == network_address.get(u'project_id', address['user_id']) + address['instance_id'] == network_address.get(u'instance_id', None) + return address + + def _get_address(self, context, public_ip): + # right now all addresses are allocated locally + # FIXME(vish) this should move into network.py + for network_address in self.network.describe_addresses(): + if network_address[u'address'] == public_ip: + address = self._convert_address(network_address) + if context.user.is_admin() or address['project_id'] == context.project.id: + return address + raise exception.NotFound("Address at ip %s not found" % public_ip) + + def _get_image(self, context, image_id): + """passes in context because + objectstore does its own authorization""" + result = images.list(context, [image_id]) + if not result: + raise exception.NotFound('Image %s could not be found' % image_id) + image = result[0] + return image + + def _get_instance(self, context, instance_id): + for instance in self.instances: + if instance['instance_id'] == instance_id: + if context.user.is_admin() or instance['project_id'] == context.project.id: + return instance + raise exception.NotFound('Instance %s could not be found' % instance_id) + + def _get_volume(self, context, volume_id): for volume in self.volumes: if volume['volume_id'] == volume_id: - return volume + if context.user.is_admin() or volume['project_id'] == context.project.id: + return volume + raise exception.NotFound('Volume %s could not be found' % volume_id) def attach_volume(self, context, volume_id, instance_id, device, **kwargs): - volume = self._get_volume(volume_id) + volume = self._get_volume(context, volume_id) storage_node = volume['node_name'] # TODO: (joshua) Fix volumes to store creator id - if not context.user.is_authorized(volume.get('user_id', None)): - raise exception.ApiError("%s not authorized for %s" % - (context.user.id, volume_id)) - instance = self.instdir.get(instance_id) + instance = self._get_instance(context, instance_id) compute_node = instance['node_name'] - if not context.user.is_authorized(instance.get('owner_id', None)): - raise exception.ApiError(message="%s not authorized for %s" % - (context.user.id, instance_id)) aoe_device = volume['aoe_device'] # Needs to get right node controller for attaching to # TODO: Maybe have another exchange that goes to everyone? @@ -297,24 +317,17 @@ class CloudController(object): "mountpoint" : device}}) return defer.succeed(True) + def detach_volume(self, context, volume_id, **kwargs): # TODO(joshua): Make sure the updated state has been received first - volume = self._get_volume(volume_id) + volume = self._get_volume(context, volume_id) storage_node = volume['node_name'] - if not context.user.is_authorized(volume.get('user_id', None)): - raise exception.ApiError("%s not authorized for %s" % - (context.user.id, volume_id)) if 'instance_id' in volume.keys(): instance_id = volume['instance_id'] try: - instance = self.instdir.get(instance_id) + instance = self._get_instance(context, instance_id) compute_node = instance['node_name'] mountpoint = volume['mountpoint'] - if not context.user.is_authorized( - instance.get('owner_id', None)): - raise exception.ApiError( - "%s not authorized for %s" % - (context.user.id, instance_id)) rpc.cast('%s.%s' % (FLAGS.compute_topic, compute_node), {"method": "detach_volume", "args" : {"instance_id": instance_id, @@ -332,16 +345,16 @@ class CloudController(object): return [{str: x} for x in lst] def describe_instances(self, context, **kwargs): - return defer.succeed(self.format_instances(context.user)) + return defer.succeed(self._format_instances(context)) - def format_instances(self, user, reservation_id = None): + def _format_instances(self, context, reservation_id = None): if self.instances == {}: return {'reservationSet': []} reservations = {} for inst in self.instances: instance = inst.values()[0] res_id = instance.get('reservation_id', 'Unknown') - if (user.is_authorized(instance.get('owner_id', None)) + if ((context.user.is_admin() or context.project.id == instance['project_id']) and (reservation_id == None or reservation_id == res_id)): i = {} i['instance_id'] = instance.get('instance_id', None) @@ -357,7 +370,7 @@ class CloudController(object): i['public_dns_name'] = i['private_dns_name'] i['dns_name'] = instance.get('dns_name', None) i['key_name'] = instance.get('key_name', None) - if user.is_admin(): + if context.user.is_admin(): i['key_name'] = '%s (%s, %s)' % (i['key_name'], instance.get('owner_id', None), instance.get('node_name','')) i['product_codes_set'] = self._convert_to_set( @@ -369,7 +382,7 @@ class CloudController(object): if not reservations.has_key(res_id): r = {} r['reservation_id'] = res_id - r['owner_id'] = instance.get('owner_id', None) + r['owner_id'] = instance.get('project_id', None) r['group_set'] = self._convert_to_set( instance.get('groups', None), 'group_id') r['instances_set'] = [] @@ -382,52 +395,52 @@ class CloudController(object): def describe_addresses(self, context, **kwargs): return self.format_addresses(context.user) - def format_addresses(self, user): + def format_addresses(self, context): addresses = [] # TODO(vish): move authorization checking into network.py - for address_record in self.network.describe_addresses( - type=network.PublicNetwork): + for network_address in self.network.describe_addresses(type=network.PublicNetwork): #logging.debug(address_record) - if user.is_authorized(address_record[u'user_id']): - address = { - 'public_ip': address_record[u'address'], - 'instance_id' : address_record.get(u'instance_id', 'free') - } - # FIXME: add another field for user id - if user.is_admin(): - address['instance_id'] = "%s (%s)" % ( - address['instance_id'], - address_record[u'user_id'], - ) - addresses.append(address) + address = self._convert_address(network_address) + address_rv = { + 'public_ip': address['public_ip'], + 'instance_id' : address.get('instance_id', 'free') + } + # FIXME: add another field for user id + if context.user.is_admin(): + address_rv['instance_id'] = "%s (%s, %s)" % ( + address['instance_id'], + address['user_id'], + address['project_id'], + ) + addresses.append(address_rv) # logging.debug(addresses) return {'addressesSet': addresses} def allocate_address(self, context, **kwargs): - # TODO: Verify user is valid? - kwargs['owner_id'] = context.user.id (address,network_name) = self.network.allocate_address( - context.user.id, type=network.PublicNetwork) + context.user.id, context.project_id, type=network.PublicNetwork) return defer.succeed({'addressSet': [{'publicIp' : address}]}) - def release_address(self, context, **kwargs): - self.network.deallocate_address(kwargs.get('public_ip', None)) + def release_address(self, context, public_ip, **kwargs): + address = self._get_address(public_ip) return defer.succeed({'releaseResponse': ["Address released."]}) def associate_address(self, context, instance_id, **kwargs): - instance = self.instdir.get(instance_id) + instance = self._get_instance(context, instance_id) rv = self.network.associate_address( kwargs['public_ip'], instance['private_dns_name'], instance_id) return defer.succeed({'associateResponse': ["Address associated."]}) - def disassociate_address(self, context, **kwargs): - rv = self.network.disassociate_address(kwargs['public_ip']) + def disassociate_address(self, context, public_ip, **kwargs): + address = self._get_address(public_ip) + rv = self.network.disassociate_address(public_ip) # TODO - Strip the IP from the instance - return rv + return defer.succeed({'disassociateResponse': ["Address disassociated."]}) def run_instances(self, context, **kwargs): + image = self._get_image(context, kwargs['image_id']) logging.debug("Going to run instances...") reservation_id = utils.generate_uid('r') launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) @@ -449,11 +462,14 @@ class CloudController(object): inst['launch_time'] = launch_time inst['key_data'] = key_data or '' inst['key_name'] = kwargs.get('key_name', '') - inst['owner_id'] = context.user.id + inst['user_id'] = context.user.id + inst['project_id'] = context.project.id inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = num address, _netname = self.network.allocate_address( - inst['owner_id'], mac=inst['mac_address']) + user_id=inst['user_id'], + project_id=inst['project_id'], + mac=inst['mac_address']) network = self.network.get_users_network(str(context.user.id)) inst['network_str'] = json.dumps(network.to_dict()) inst['bridge_name'] = network.bridge_name @@ -466,82 +482,77 @@ class CloudController(object): logging.debug("Casting to node for %s's instance with IP of %s" % (context.user.name, inst['private_dns_name'])) # TODO: Make the NetworkComputeNode figure out the network name from ip. - return defer.succeed(self.format_instances( + return defer.succeed(self._format_instances( context.user, reservation_id)) def terminate_instances(self, context, instance_id, **kwargs): logging.debug("Going to start terminating instances") - # TODO: return error if not authorized for i in instance_id: logging.debug("Going to try and terminate %s" % i) - instance = self.instdir.get(i) - #if instance['state'] == 'pending': - # raise exception.ApiError('Cannot terminate pending instance') - if context.user.is_authorized(instance.get('owner_id', None)): + try: + instance = self._get_instance(context, i) + except exception.NotFound: + logging.warning("Instance %s was not found during terminate" % i) + continue + try: + self.network.disassociate_address( + instance.get('public_dns_name', 'bork')) + except: + pass + if instance.get('private_dns_name', None): + logging.debug("Deallocating address %s" % instance.get('private_dns_name', None)) try: - self.network.disassociate_address( - instance.get('public_dns_name', 'bork')) - except: + self.network.deallocate_address(instance.get('private_dns_name', None)) + except Exception, _err: pass - if instance.get('private_dns_name', None): - logging.debug("Deallocating address %s" % instance.get('private_dns_name', None)) - try: - self.network.deallocate_address(instance.get('private_dns_name', None)) - except Exception, _err: - pass - if instance.get('node_name', 'unassigned') != 'unassigned': #It's also internal default - rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), + if instance.get('node_name', 'unassigned') != 'unassigned': #It's also internal default + rpc.cast('%s.%s' % (FLAGS.compute_topic, instance['node_name']), {"method": "terminate_instance", "args" : {"instance_id": i}}) - else: - instance.destroy() + else: + instance.destroy() return defer.succeed(True) def reboot_instances(self, context, instance_id, **kwargs): - # TODO: return error if not authorized + """instance_id is a list of instance ids""" for i in instance_id: - instance = self.instdir.get(i) + instance = self._get_instance(context, i) if instance['state'] == 'pending': raise exception.ApiError('Cannot reboot pending instance') - if context.user.is_authorized(instance.get('owner_id', None)): - rpc.cast('%s.%s' % (FLAGS.node_topic, instance['node_name']), + rpc.cast('%s.%s' % (FLAGS.node_topic, instance['node_name']), {"method": "reboot_instance", "args" : {"instance_id": i}}) return defer.succeed(True) def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized - volume = self._get_volume(volume_id) + volume = self._get_volume(context, volume_id) storage_node = volume['node_name'] - if context.user.is_authorized(volume.get('user_id', None)): - rpc.cast('%s.%s' % (FLAGS.storage_topic, storage_node), - {"method": "delete_volume", - "args" : {"volume_id": volume_id}}) + rpc.cast('%s.%s' % (FLAGS.storage_topic, storage_node), + {"method": "delete_volume", + "args" : {"volume_id": volume_id}}) return defer.succeed(True) def describe_images(self, context, image_id=None, **kwargs): - imageSet = images.list(context.user) - if not image_id is None: - imageSet = [i for i in imageSet if i['imageId'] in image_id] - + # The objectstore does its own authorization for describe + imageSet = images.list(context, image_id) return defer.succeed({'imagesSet': imageSet}) def deregister_image(self, context, image_id, **kwargs): - images.deregister(context.user, image_id) - + # FIXME: should the objectstore be doing these authorization checks? + images.deregister(context, image_id) return defer.succeed({'imageId': image_id}) def register_image(self, context, image_location=None, **kwargs): + # FIXME: should the objectstore be doing these authorization checks? if image_location is None and kwargs.has_key('name'): image_location = kwargs['name'] - - image_id = images.register(context.user, image_location) + image_id = images.register(context, image_location) logging.debug("Registered %s as %s" % (image_location, image_id)) return defer.succeed({'imageId': image_id}) - def modify_image_attribute(self, context, image_id, - attribute, operation_type, **kwargs): + def modify_image_attribute(self, context, image_id, attribute, operation_type, **kwargs): if attribute != 'launchPermission': raise exception.ApiError('only launchPermission is supported') if len(kwargs['user_group']) != 1 and kwargs['user_group'][0] != 'all': diff --git a/nova/endpoint/images.py b/nova/endpoint/images.py index f494ce892..673a108e9 100644 --- a/nova/endpoint/images.py +++ b/nova/endpoint/images.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,7 +14,7 @@ # limitations under the License. """ -Proxy AMI-related calls from the cloud controller, to the running +Proxy AMI-related calls from the cloud controller, to the running objectstore daemon. """ @@ -31,9 +31,8 @@ from nova import utils FLAGS = flags.FLAGS - -def modify(user, image_id, operation): - conn(user).make_request( +def modify(context, image_id, operation): + conn(context).make_request( method='POST', bucket='_images', query_args=qs({'image_id': image_id, 'operation': operation})) @@ -41,11 +40,11 @@ def modify(user, image_id, operation): return True -def register(user, image_location): +def register(context, image_location): """ rpc call to register a new image based from a manifest """ image_id = utils.generate_uid('ami') - conn(user).make_request( + conn(context).make_request( method='PUT', bucket='_images', query_args=qs({'image_location': image_location, @@ -53,32 +52,32 @@ def register(user, image_location): return image_id - -def list(user, filter_list=[]): +def list(context, filter_list=[]): """ return a list of all images that a user can see optionally filtered by a list of image_id """ # FIXME: send along the list of only_images to check for - response = conn(user).make_request( + response = conn(context).make_request( method='GET', bucket='_images') - return json.loads(response.read()) + result = json.loads(response.read()) + if not filter_list is None: + return [i for i in result if i['imageId'] in filter_list] + return result - -def deregister(user, image_id): +def deregister(context, image_id): """ unregister an image """ - conn(user).make_request( + conn(context).make_request( method='DELETE', bucket='_images', query_args=qs({'image_id': image_id})) - -def conn(user): +def conn(context): return boto.s3.connection.S3Connection ( - aws_access_key_id=user.access, - aws_secret_access_key=user.secret, + aws_access_key_id='%s:%s' % (context.user.access, context.project.name), + aws_secret_access_key=context.user.secret, is_secure=False, calling_format=boto.s3.connection.OrdinaryCallingFormat(), port=FLAGS.s3_port, diff --git a/nova/exception.py b/nova/exception.py index dc7b16cdb..82d08e840 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,7 +14,7 @@ # limitations under the License. """ -Nova base exception handling, including decorator for re-raising +Nova base exception handling, including decorator for re-raising Nova-type exceptions. SHOULD include dedicated exception logging. """ @@ -23,16 +23,21 @@ import traceback import sys class Error(Exception): - pass + def __init__(self, message=None): + super(Error, self).__init__(message) -class ApiError(Error): +class ApiError(Error): def __init__(self, message='Unknown', code='Unknown'): self.message = message self.code = code + super(ApiError, self).__init__('%s: %s'% (code, message)) class NotFound(Error): pass +class Duplicate(Error): + pass + class NotAuthorized(Error): pass @@ -42,12 +47,12 @@ def wrap_exception(f): return f(*args, **kw) except Exception, e: if not isinstance(e, Error): - # exc_type, exc_value, exc_traceback = sys.exc_info() + # exc_type, exc_value, exc_traceback = sys.exc_info() logging.exception('Uncaught exception') - # logging.debug(traceback.extract_stack(exc_traceback)) + # logging.debug(traceback.extract_stack(exc_traceback)) raise Error(str(e)) raise _wrap.func_name = f.func_name return _wrap - + diff --git a/nova/fakerabbit.py b/nova/fakerabbit.py index ec2e50791..13d432b45 100644 --- a/nova/fakerabbit.py +++ b/nova/fakerabbit.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -39,7 +39,7 @@ class Exchange(object): for f in self._routes[routing_key]: logging.debug('Publishing to route %s', f) f(message, routing_key=routing_key) - + def bind(self, callback, routing_key): self._routes.setdefault(routing_key, []) self._routes[routing_key].append(callback) @@ -52,7 +52,7 @@ class Queue(object): def __repr__(self): return '<Queue: %s>' % self.name - + def push(self, message, routing_key=None): self._queue.put(message) @@ -70,7 +70,7 @@ class Backend(object): #super(__impl, self).__init__(*args, **kwargs) self._exchanges = {} self._queues = {} - + def _reset_all(self): self._exchanges = {} self._queues = {} @@ -78,7 +78,7 @@ class Backend(object): def queue_declare(self, queue, **kwargs): if queue not in self._queues: logging.debug('Declaring queue %s', queue) - self._queues[queue] = Queue(queue) + self._queues[queue] = Queue(queue) def exchange_declare(self, exchange, type, *args, **kwargs): if exchange not in self._exchanges: @@ -92,7 +92,7 @@ class Backend(object): routing_key) def get(self, queue, no_ack=False): - if not self._queues[queue].size(): + if not queue in self._queues or not self._queues[queue].size(): return None (message_data, content_type, content_encoding) = \ self._queues[queue].pop() @@ -122,7 +122,7 @@ class Backend(object): def __getattr__(self, attr): return getattr(self.__instance, attr) - + def __setattr__(self, attr, value): return setattr(self.__instance, attr, value) diff --git a/nova/objectstore/bucket.py b/nova/objectstore/bucket.py index 0777c2f11..0bf102867 100644 --- a/nova/objectstore/bucket.py +++ b/nova/objectstore/bucket.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -28,12 +28,10 @@ from nova import flags from nova import utils from nova.objectstore import stored - FLAGS = flags.FLAGS flags.DEFINE_string('buckets_path', utils.abspath('../buckets'), 'path to s3 buckets') - class Bucket(object): def __init__(self, name): self.name = name @@ -62,11 +60,11 @@ class Bucket(object): return buckets @staticmethod - def create(bucket_name, user): - """Create a new bucket owned by a user. + def create(bucket_name, context): + """Create a new bucket owned by a project. @bucket_name: a string representing the name of the bucket to create - @user: a nova.auth.user who should own the bucket. + @context: a nova.auth.api.ApiContext object representing who owns the bucket. Raises: NotAuthorized: if the bucket is already exists or has invalid name @@ -80,7 +78,7 @@ class Bucket(object): os.makedirs(path) with open(path+'.json', 'w') as f: - json.dump({'ownerId': user.id}, f) + json.dump({'ownerId': context.project.id}, f) @property def metadata(self): @@ -101,9 +99,9 @@ class Bucket(object): except: return None - def is_authorized(self, user): + def is_authorized(self, context): try: - return user.is_admin() or self.owner_id == user.id + return context.user.is_admin() or self.owner_id == context.project.id except Exception, e: pass diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index c3e036a40..a7fff12fc 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -31,6 +31,7 @@ S3 client with this module:: print c.get("mybucket", "mykey").body """ + import datetime import os import urllib @@ -44,6 +45,7 @@ from tornado import escape, web from nova import exception from nova import flags +from nova.endpoint import api from nova.objectstore import bucket from nova.objectstore import image @@ -87,16 +89,18 @@ class BaseRequestHandler(web.RequestHandler): SUPPORTED_METHODS = ("PUT", "GET", "DELETE", "HEAD") @property - def user(self): - if not hasattr(self, '_user'): + def context(self): + if not hasattr(self, '_context'): try: - access = self.request.headers['Authorization'].split(' ')[1].split(':')[0] - user = self.application.user_manager.get_user_from_access_key(access) - user.secret # FIXME: check signature here! - self._user = user - except: + # Authorization Header format: 'AWS <access>:<secret>' + access, sep, secret = self.request.headers['Authorization'].split(' ')[1].rpartition(':') + (user, project) = self.application.user_manager.authenticate(access, secret, {}, self.request.method, self.request.host, self.request.path, False) + # FIXME: check signature here! + self._context = api.APIRequestContext(self, user, project) + except exception.Error, ex: + logging.debug("Authentication Failure: %s" % ex) raise web.HTTPError(403) - return self._user + return self._context def render_xml(self, value): assert isinstance(value, dict) and len(value) == 1 @@ -134,7 +138,7 @@ class BaseRequestHandler(web.RequestHandler): class RootHandler(BaseRequestHandler): def get(self): - buckets = [b for b in bucket.Bucket.all() if b.is_authorized(self.user)] + buckets = [b for b in bucket.Bucket.all() if b.is_authorized(self.context)] self.render_xml({"ListAllMyBucketsResult": { "Buckets": {"Bucket": [b.metadata for b in buckets]}, @@ -148,7 +152,7 @@ class BucketHandler(BaseRequestHandler): bucket_object = bucket.Bucket(bucket_name) - if not bucket_object.is_authorized(self.user): + if not bucket_object.is_authorized(self.context): raise web.HTTPError(403) prefix = self.get_argument("prefix", u"") @@ -162,7 +166,7 @@ class BucketHandler(BaseRequestHandler): @catch_nova_exceptions def put(self, bucket_name): logging.debug("Creating bucket %s" % (bucket_name)) - bucket.Bucket.create(bucket_name, self.user) + bucket.Bucket.create(bucket_name, self.context) self.finish() @catch_nova_exceptions @@ -170,7 +174,7 @@ class BucketHandler(BaseRequestHandler): logging.debug("Deleting bucket %s" % (bucket_name)) bucket_object = bucket.Bucket(bucket_name) - if not bucket_object.is_authorized(self.user): + if not bucket_object.is_authorized(self.context): raise web.HTTPError(403) bucket_object.delete() @@ -185,7 +189,7 @@ class ObjectHandler(BaseRequestHandler): bucket_object = bucket.Bucket(bucket_name) - if not bucket_object.is_authorized(self.user): + if not bucket_object.is_authorized(self.context): raise web.HTTPError(403) obj = bucket_object[urllib.unquote(object_name)] @@ -199,7 +203,7 @@ class ObjectHandler(BaseRequestHandler): logging.debug("Putting object: %s / %s" % (bucket_name, object_name)) bucket_object = bucket.Bucket(bucket_name) - if not bucket_object.is_authorized(self.user): + if not bucket_object.is_authorized(self.context): raise web.HTTPError(403) key = urllib.unquote(object_name) @@ -212,7 +216,7 @@ class ObjectHandler(BaseRequestHandler): logging.debug("Deleting object: %s / %s" % (bucket_name, object_name)) bucket_object = bucket.Bucket(bucket_name) - if not bucket_object.is_authorized(self.user): + if not bucket_object.is_authorized(self.context): raise web.HTTPError(403) del bucket_object[urllib.unquote(object_name)] @@ -228,7 +232,7 @@ class ImageHandler(BaseRequestHandler): """ returns a json listing of all images that a user has permissions to see """ - images = [i for i in image.Image.all() if i.is_authorized(self.user)] + images = [i for i in image.Image.all() if i.is_authorized(self.context)] self.finish(json.dumps([i.metadata for i in images])) @@ -247,11 +251,11 @@ class ImageHandler(BaseRequestHandler): bucket_object = bucket.Bucket(image_location.split("/")[0]) manifest = image_location[len(image_location.split('/')[0])+1:] - if not bucket_object.is_authorized(self.user): + if not bucket_object.is_authorized(self.context): raise web.HTTPError(403) p = multiprocessing.Process(target=image.Image.create,args= - (image_id, image_location, self.user)) + (image_id, image_location, self.context)) p.start() self.finish() @@ -264,7 +268,7 @@ class ImageHandler(BaseRequestHandler): image_object = image.Image(image_id) - if image_object.owner_id != self.user.id: + if not image.is_authorized(self.context): raise web.HTTPError(403) image_object.set_public(operation=='add') @@ -277,7 +281,7 @@ class ImageHandler(BaseRequestHandler): image_id = self.get_argument("image_id", u"") image_object = image.Image(image_id) - if image_object.owner_id != self.user.id: + if not image.is_authorized(self.context): raise web.HTTPError(403) image_object.delete() diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index 1878487f7..892ada00c 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -58,9 +58,9 @@ class Image(object): except: pass - def is_authorized(self, user): + def is_authorized(self, context): try: - return self.metadata['isPublic'] or self.metadata['imageOwnerId'] == user.id + return self.metadata['isPublic'] or context.user.is_admin() or self.metadata['imageOwnerId'] == context.project.id except: return False @@ -91,7 +91,7 @@ class Image(object): return json.load(f) @staticmethod - def create(image_id, image_location, user): + def create(image_id, image_location, context): image_path = os.path.join(FLAGS.images_path, image_id) os.makedirs(image_path) @@ -119,7 +119,7 @@ class Image(object): info = { 'imageId': image_id, 'imageLocation': image_location, - 'imageOwnerId': user.id, + 'imageOwnerId': context.project.id, 'isPublic': False, # FIXME: grab public from manifest 'architecture': 'x86_64', # FIXME: grab architecture from manifest 'type' : image_type diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py deleted file mode 100644 index ab0759c2d..000000000 --- a/nova/tests/access_unittest.py +++ /dev/null @@ -1,60 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright [2010] [Anso Labs, LLC] -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import os -import unittest - -from nova import flags -from nova import test -from nova.auth import users -from nova.endpoint import cloud - -FLAGS = flags.FLAGS - -class AccessTestCase(test.BaseTestCase): - def setUp(self): - FLAGS.fake_libvirt = True - FLAGS.fake_storage = True - self.users = users.UserManager.instance() - super(AccessTestCase, self).setUp() - # Make a test project - # Make a test user - self.users.create_user('test1', 'access', 'secret') - - # Make the test user a member of the project - - def tearDown(self): - # Delete the test user - # Delete the test project - self.users.delete_user('test1') - pass - - def test_001_basic_user_access(self): - user = self.users.get_user('test1') - # instance-foo, should be using object and not owner_id - instance_id = "i-12345678" - self.assertTrue(user.is_authorized(instance_id, action="describe_instances")) - - def test_002_sysadmin_access(self): - user = self.users.get_user('test1') - bucket = "foo/bar/image" - self.assertFalse(user.is_authorized(bucket, action="register")) - self.users.add_role(user, "sysadmin") - - -if __name__ == "__main__": - # TODO: Implement use_fake as an option - unittest.main() diff --git a/nova/tests/api_integration.py b/nova/tests/api_integration.py index d2e1026b8..cf84b9907 100644 --- a/nova/tests/api_integration.py +++ b/nova/tests/api_integration.py @@ -1,11 +1,11 @@ # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -33,14 +33,13 @@ def get_connection(): path='/services/Cloud', debug=99 ) - + class APIIntegrationTests(unittest.TestCase): def test_001_get_all_images(self): conn = get_connection() res = conn.get_all_images() - print res - - + + if __name__ == '__main__': unittest.main() diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 568a8dcd3..40eeb8a23 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,7 +20,6 @@ import unittest from xml.etree import ElementTree from nova import vendor -import mox from tornado import ioloop from twisted.internet import defer @@ -53,18 +52,24 @@ class CloudTestCase(test.BaseTestCase): topic=FLAGS.cloud_topic, proxy=self.cloud) self.injected.append(self.cloud_consumer.attach_to_tornado(self.ioloop)) - + # set up a node self.node = node.Node() self.node_consumer = rpc.AdapterConsumer(connection=self.conn, topic=FLAGS.compute_topic, proxy=self.node) self.injected.append(self.node_consumer.attach_to_tornado(self.ioloop)) - - user_mocker = mox.Mox() - self.admin = user_mocker.CreateMock(users.User) - self.admin.is_authorized(mox.IgnoreArg()).AndReturn(True) - self.context = api.APIRequestContext(handler=None,user=self.admin) + + try: + users.UserManager.instance().create_user('admin', 'admin', 'admin') + except: pass + admin = users.UserManager.instance().get_user('admin') + project = users.UserManager.instance().create_project('proj', 'admin', 'proj') + self.context = api.APIRequestContext(handler=None,project=project,user=admin) + + def tearDown(self): + users.UserManager.instance().delete_project('proj') + users.UserManager.instance().delete_user('admin') def test_console_output(self): if FLAGS.fake_libvirt: @@ -76,7 +81,7 @@ class CloudTestCase(test.BaseTestCase): logging.debug(output) self.assert_(output) rv = yield self.node.terminate_instance(instance_id) - + def test_run_instances(self): if FLAGS.fake_libvirt: logging.debug("Can't test instances without a real virtual env.") @@ -128,9 +133,7 @@ class CloudTestCase(test.BaseTestCase): 'state': 0x01, 'user_data': '' } - - rv = self.cloud.format_instances(self.admin) - print rv + rv = self.cloud._format_instances(self.context) self.assert_(len(rv['reservationSet']) == 0) # simulate launch of 5 instances @@ -139,19 +142,18 @@ class CloudTestCase(test.BaseTestCase): # inst = instance(i) # self.cloud.instances['pending'][inst['instance_id']] = inst - #rv = self.cloud.format_instances(self.admin) + #rv = self.cloud._format_instances(self.admin) #self.assert_(len(rv['reservationSet']) == 1) #self.assert_(len(rv['reservationSet'][0]['instances_set']) == 5) - # report 4 nodes each having 1 of the instances #for i in xrange(4): # self.cloud.update_state('instances', {('node-%s' % i): {('i-%s' % i): instance(i)}}) - + # one instance should be pending still #self.assert_(len(self.cloud.instances['pending'].keys()) == 1) # check that the reservations collapse - #rv = self.cloud.format_instances(self.admin) + #rv = self.cloud._format_instances(self.admin) #self.assert_(len(rv['reservationSet']) == 1) #self.assert_(len(rv['reservationSet'][0]['instances_set']) == 5) diff --git a/nova/tests/network_unittest.py b/nova/tests/network_unittest.py index 43c7831a7..a78d9075d 100644 --- a/nova/tests/network_unittest.py +++ b/nova/tests/network_unittest.py @@ -30,84 +30,86 @@ class NetworkTestCase(test.TrialTestCase): super(NetworkTestCase, self).setUp() logging.getLogger().setLevel(logging.DEBUG) self.manager = users.UserManager.instance() + try: + self.manager.create_user('netuser', 'netuser', 'netuser') + except: pass for i in range(0, 6): - name = 'user%s' % i - if not self.manager.get_user(name): - self.manager.create_user(name, name, name) + name = 'project%s' % i + if not self.manager.get_project(name): + self.manager.create_project(name, 'netuser', name) self.network = network.NetworkController(netsize=16) def tearDown(self): super(NetworkTestCase, self).tearDown() for i in range(0, 6): - name = 'user%s' % i - self.manager.delete_user(name) + name = 'project%s' % i + self.manager.delete_project(name) + self.manager.delete_user('netuser') def test_network_serialization(self): net1 = network.Network(vlan=100, network="192.168.100.0/24", conn=None) - address = net1.allocate_ip("user0", "01:24:55:36:f2:a0") + address = net1.allocate_ip("netuser", "project0", "01:24:55:36:f2:a0") net_json = str(net1) net2 = network.Network.from_json(net_json) self.assertEqual(net_json, str(net2)) self.assertTrue(IPy.IP(address) in net2.network) def test_allocate_deallocate_address(self): - for flag in flags.FLAGS: - print "%s=%s" % (flag, flags.FLAGS.get(flag, None)) - (address, net_name) = self.network.allocate_address( - "user0", "01:24:55:36:f2:a0") + (address, net_name) = self.network.allocate_address("netuser", + "project0", "01:24:55:36:f2:a0") logging.debug("Was allocated %s" % (address)) - self.assertEqual(True, address in self._get_user_addresses("user0")) + self.assertEqual(True, address in self._get_project_addresses("project0")) rv = self.network.deallocate_address(address) - self.assertEqual(False, address in self._get_user_addresses("user0")) + self.assertEqual(False, address in self._get_project_addresses("project0")) def test_range_allocation(self): - (address, net_name) = self.network.allocate_address( - "user0", "01:24:55:36:f2:a0") - (secondaddress, net_name) = self.network.allocate_address( - "user1", "01:24:55:36:f2:a0") - self.assertEqual(True, address in self._get_user_addresses("user0")) + (address, net_name) = self.network.allocate_address("netuser", + "project0", "01:24:55:36:f2:a0") + (secondaddress, net_name) = self.network.allocate_address("netuser", + "project1", "01:24:55:36:f2:a0") + self.assertEqual(True, address in self._get_project_addresses("project0")) self.assertEqual(True, - secondaddress in self._get_user_addresses("user1")) - self.assertEqual(False, address in self._get_user_addresses("user1")) + secondaddress in self._get_project_addresses("project1")) + self.assertEqual(False, address in self._get_project_addresses("project1")) rv = self.network.deallocate_address(address) - self.assertEqual(False, address in self._get_user_addresses("user0")) + self.assertEqual(False, address in self._get_project_addresses("project0")) rv = self.network.deallocate_address(secondaddress) self.assertEqual(False, - secondaddress in self._get_user_addresses("user1")) + secondaddress in self._get_project_addresses("project1")) def test_subnet_edge(self): - (secondaddress, net_name) = self.network.allocate_address("user0") - for user in range(1,5): - user_id = "user%s" % (user) - (address, net_name) = self.network.allocate_address( - user_id, "01:24:55:36:f2:a0") - (address2, net_name) = self.network.allocate_address( - user_id, "01:24:55:36:f2:a0") - (address3, net_name) = self.network.allocate_address( - user_id, "01:24:55:36:f2:a0") + (secondaddress, net_name) = self.network.allocate_address("netuser", "project0") + for project in range(1,5): + project_id = "project%s" % (project) + (address, net_name) = self.network.allocate_address("netuser", + project_id, "01:24:55:36:f2:a0") + (address2, net_name) = self.network.allocate_address("netuser", + project_id, "01:24:55:36:f2:a0") + (address3, net_name) = self.network.allocate_address("netuser", + project_id, "01:24:55:36:f2:a0") self.assertEqual(False, - address in self._get_user_addresses("user0")) + address in self._get_project_addresses("project0")) self.assertEqual(False, - address2 in self._get_user_addresses("user0")) + address2 in self._get_project_addresses("project0")) self.assertEqual(False, - address3 in self._get_user_addresses("user0")) + address3 in self._get_project_addresses("project0")) rv = self.network.deallocate_address(address) rv = self.network.deallocate_address(address2) rv = self.network.deallocate_address(address3) rv = self.network.deallocate_address(secondaddress) - def test_too_many_users(self): + def test_too_many_projects(self): for i in range(0, 30): - name = 'toomany-user%s' % i - self.manager.create_user(name, name, name) - (address, net_name) = self.network.allocate_address( + name = 'toomany-project%s' % i + self.manager.create_project(name, 'netuser', name) + (address, net_name) = self.network.allocate_address("netuser", name, "01:24:55:36:f2:a0") - self.manager.delete_user(name) + self.manager.delete_project(name) - def _get_user_addresses(self, user_id): + def _get_project_addresses(self, project_id): rv = self.network.describe_addresses() - user_addresses = [] + project_addresses = [] for item in rv: - if item['user_id'] == user_id: - user_addresses.append(item['address']) - return user_addresses + if item['project_id'] == project_id: + project_addresses.append(item['address']) + return project_addresses diff --git a/nova/tests/node_unittest.py b/nova/tests/node_unittest.py index 7a6115fcc..5ecd56d52 100644 --- a/nova/tests/node_unittest.py +++ b/nova/tests/node_unittest.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -14,14 +14,9 @@ # limitations under the License. import logging -import StringIO -import time -import unittest from xml.etree import ElementTree from nova import vendor -import mox -from tornado import ioloop from twisted.internet import defer from nova import exception @@ -39,21 +34,21 @@ class InstanceXmlTestCase(test.TrialTestCase): def test_serialization(self): # TODO: Reimplement this, it doesn't make sense in redis-land return - + # instance_id = 'foo' # first_node = node.Node() # inst = yield first_node.run_instance(instance_id) - # + # # # force the state so that we can verify that it changes # inst._s['state'] = node.Instance.NOSTATE # xml = inst.toXml() # self.assert_(ElementTree.parse(StringIO.StringIO(xml))) - # + # # second_node = node.Node() # new_inst = node.Instance.fromXml(second_node._conn, pool=second_node._pool, xml=xml) # self.assertEqual(new_inst.state, node.Instance.RUNNING) # rv = yield first_node.terminate_instance(instance_id) - + class NodeConnectionTestCase(test.TrialTestCase): def setUp(self): @@ -64,20 +59,22 @@ class NodeConnectionTestCase(test.TrialTestCase): fake_users=True, redis_db=8) self.node = node.Node() - + def create_instance(self): instdir = model.InstanceDirectory() inst = instdir.new() # TODO(ja): add ami, ari, aki, user_data inst['reservation_id'] = 'r-fakeres' inst['launch_time'] = '10' - inst['owner_id'] = 'fake' + inst['user_id'] = 'fake' + inst['project_id'] = 'fake' + inst['instance_type'] = 'm1.tiny' inst['node_name'] = FLAGS.node_name inst['mac_address'] = utils.generate_mac() inst['ami_launch_index'] = 0 inst.save() return inst['instance_id'] - + @defer.inlineCallbacks def test_run_describe_terminate(self): instance_id = self.create_instance() @@ -96,11 +93,10 @@ class NodeConnectionTestCase(test.TrialTestCase): def test_reboot(self): instance_id = self.create_instance() rv = yield self.node.run_instance(instance_id) - + rv = yield self.node.describe_instances() - logging.debug("describe_instances returns %s" % (rv)) self.assertEqual(rv[instance_id].name, instance_id) - + yield self.node.reboot_instance(instance_id) rv = yield self.node.describe_instances() @@ -111,7 +107,7 @@ class NodeConnectionTestCase(test.TrialTestCase): def test_console_output(self): instance_id = self.create_instance() rv = yield self.node.run_instance(instance_id) - + console = yield self.node.get_console_output(instance_id) self.assert_(console) rv = yield self.node.terminate_instance(instance_id) @@ -123,6 +119,6 @@ class NodeConnectionTestCase(test.TrialTestCase): rv = yield self.node.describe_instances() self.assertEqual(rv[instance_id].name, instance_id) - + self.assertRaises(exception.Error, self.node.run_instance, instance_id) rv = yield self.node.terminate_instance(instance_id) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index 5f41d47a0..812f5418b 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -56,8 +56,6 @@ class ObjectStoreTestCase(test.BaseTestCase): logging.getLogger().setLevel(logging.DEBUG) self.um = users.UserManager.instance() - - def test_buckets(self): try: self.um.create_user('user1') except: pass @@ -67,18 +65,41 @@ class ObjectStoreTestCase(test.BaseTestCase): try: self.um.create_user('admin_user', admin=True) except: pass + try: + self.um.create_project('proj1', 'user1', 'a proj', ['user1']) + except: pass + try: + self.um.create_project('proj2', 'user2', 'a proj', ['user2']) + except: pass + class Context(object): pass + self.context = Context() + + def tearDown(self): + self.um.delete_project('proj1') + self.um.delete_project('proj2') + self.um.delete_user('user1') + self.um.delete_user('user2') + self.um.delete_user('admin_user') + super(ObjectStoreTestCase, self).tearDown() - objectstore.bucket.Bucket.create('new_bucket', self.um.get_user('user1')) + def test_buckets(self): + self.context.user = self.um.get_user('user1') + self.context.project = self.um.get_project('proj1') + objectstore.bucket.Bucket.create('new_bucket', self.context) bucket = objectstore.bucket.Bucket('new_bucket') # creator is authorized to use bucket - self.assert_(bucket.is_authorized(self.um.get_user('user1'))) + self.assert_(bucket.is_authorized(self.context)) # another user is not authorized - self.assert_(bucket.is_authorized(self.um.get_user('user2')) == False) + self.context.user = self.um.get_user('user2') + self.context.project = self.um.get_project('proj2') + self.assert_(bucket.is_authorized(self.context) == False) # admin is authorized to use bucket - self.assert_(bucket.is_authorized(self.um.get_user('admin_user'))) + self.context.user = self.um.get_user('admin_user') + self.context.project = None + self.assert_(bucket.is_authorized(self.context)) # new buckets are empty self.assert_(bucket.list_keys()['Contents'] == []) @@ -116,18 +137,13 @@ class ObjectStoreTestCase(test.BaseTestCase): exception = True self.assert_(exception) - self.um.delete_user('user1') - self.um.delete_user('user2') - self.um.delete_user('admin_user') def test_images(self): - try: - self.um.create_user('image_creator') - except: pass - image_user = self.um.get_user('image_creator') + self.context.user = self.um.get_user('user1') + self.context.project = self.um.get_project('proj1') # create a bucket for our bundle - objectstore.bucket.Bucket.create('image_bucket', image_user) + objectstore.bucket.Bucket.create('image_bucket', self.context) bucket = objectstore.bucket.Bucket('image_bucket') # upload an image manifest/parts @@ -136,7 +152,7 @@ class ObjectStoreTestCase(test.BaseTestCase): bucket[os.path.basename(path)] = open(path, 'rb').read() # register an image - objectstore.image.Image.create('i-testing', 'image_bucket/1mb.manifest.xml', image_user) + objectstore.image.Image.create('i-testing', 'image_bucket/1mb.manifest.xml', self.context) # verify image my_img = objectstore.image.Image('i-testing') @@ -147,14 +163,9 @@ class ObjectStoreTestCase(test.BaseTestCase): self.assertEqual(sha, '3b71f43ff30f4b15b5cd85dd9e95ebc7e84eb5a3') # verify image permissions - try: - self.um.create_user('new_user') - except: pass - new_user = self.um.get_user('new_user') - self.assert_(my_img.is_authorized(new_user) == False) - - self.um.delete_user('new_user') - self.um.delete_user('image_creator') + self.context.user = self.um.get_user('user2') + self.context.project = self.um.get_project('proj2') + self.assert_(my_img.is_authorized(self.context) == False) # class ApiObjectStoreTestCase(test.BaseTestCase): # def setUp(self): diff --git a/nova/tests/users_unittest.py b/nova/tests/users_unittest.py index 70f508b35..ff34b8957 100644 --- a/nova/tests/users_unittest.py +++ b/nova/tests/users_unittest.py @@ -24,11 +24,9 @@ from M2Crypto import X509 from nova import crypto from nova import flags from nova import test -from nova import utils from nova.auth import users from nova.endpoint import cloud - FLAGS = flags.FLAGS @@ -40,8 +38,9 @@ class UserTestCase(test.BaseTestCase): redis_db=8) self.users = users.UserManager.instance() - def test_001_can_create_user(self): + def test_001_can_create_users(self): self.users.create_user('test1', 'access', 'secret') + self.users.create_user('test2') def test_002_can_get_user(self): user = self.users.get_user('test1') @@ -83,7 +82,6 @@ class UserTestCase(test.BaseTestCase): key.save_pub_key_bio(bio) converted = crypto.ssl_pub_to_ssh_pub(bio.read()) # assert key fields are equal - print converted self.assertEqual(public_key.split(" ")[1].strip(), converted.split(" ")[1].strip()) @@ -101,16 +99,44 @@ class UserTestCase(test.BaseTestCase): users = self.users.get_users() self.assertTrue(filter(lambda u: u.id == 'test1', users)) - def test_011_can_generate_x509(self): + def test_201_can_create_project(self): + project = self.users.create_project('testproj', 'test1', 'A test project', ['test1']) + self.assertTrue(filter(lambda p: p.name == 'testproj', self.users.get_projects())) + self.assertEqual(project.name, 'testproj') + self.assertEqual(project.description, 'A test project') + self.assertEqual(project.project_manager_id, 'test1') + self.assertTrue(project.has_member('test1')) + + def test_202_user1_is_project_member(self): + self.assertTrue(self.users.get_user('test1').is_project_member('testproj')) + + def test_203_user2_is_not_project_member(self): + self.assertFalse(self.users.get_user('test2').is_project_member('testproj')) + + def test_204_user1_is_project_manager(self): + self.assertTrue(self.users.get_user('test1').is_project_manager('testproj')) + + def test_205_user2_is_not_project_manager(self): + self.assertFalse(self.users.get_user('test2').is_project_manager('testproj')) + + def test_206_can_add_user_to_project(self): + self.users.add_to_project('test2', 'testproj') + self.assertTrue(self.users.get_project('testproj').has_member('test2')) + + def test_208_can_remove_user_from_project(self): + self.users.remove_from_project('test2', 'testproj') + self.assertFalse(self.users.get_project('testproj').has_member('test2')) + + def test_209_can_generate_x509(self): # MUST HAVE RUN CLOUD SETUP BY NOW self.cloud = cloud.CloudController() self.cloud.setup() - private_key, signed_cert_string = self.users.get_user('test1').generate_x509_cert() + private_key, signed_cert_string = self.users.get_project('testproj').generate_x509_cert('test1') logging.debug(signed_cert_string) # Need to verify that it's signed by the right intermediate CA - full_chain = crypto.fetch_ca(username='test1', chain=True) - int_cert = crypto.fetch_ca(username='test1', chain=False) + full_chain = crypto.fetch_ca(project_id='testproj', chain=True) + int_cert = crypto.fetch_ca(project_id='testproj', chain=False) cloud_cert = crypto.fetch_ca() logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain) signed_cert = X509.load_cert_string(signed_cert_string) @@ -125,11 +151,16 @@ class UserTestCase(test.BaseTestCase): else: self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey())) - def test_012_can_delete_user(self): + def test_299_can_delete_project(self): + self.users.delete_project('testproj') + self.assertFalse(filter(lambda p: p.name == 'testproj', self.users.get_projects())) + + def test_999_can_delete_users(self): self.users.delete_user('test1') users = self.users.get_users() - if users != None: - self.assertFalse(filter(lambda u: u.id == 'test1', users)) + self.assertFalse(filter(lambda u: u.id == 'test1', users)) + self.users.delete_user('test2') + self.assertEqual(self.users.get_user('test2'), None) if __name__ == "__main__": diff --git a/nova/volume/storage.py b/nova/volume/storage.py index 823e1390a..cf64b995f 100644 --- a/nova/volume/storage.py +++ b/nova/volume/storage.py @@ -1,12 +1,12 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 # Copyright [2010] [Anso Labs, LLC] -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,6 +19,7 @@ destroying persistent storage volumes, ala EBS. Currently uses Ata-over-Ethernet. """ +import glob import logging import random import socket @@ -52,7 +53,7 @@ flags.DEFINE_integer('shelf_id', flags.DEFINE_string('storage_availability_zone', 'nova', 'availability zone of this node') -flags.DEFINE_boolean('fake_storage', False, +flags.DEFINE_boolean('fake_storage', False, 'Should we make real storage volumes to attach?') class BlockStore(object): @@ -62,7 +63,7 @@ class BlockStore(object): if FLAGS.fake_storage: self.volume_class = FakeVolume self._init_volume_group() - self.keeper = datastore.Keeper('instances') + self.keeper = datastore.Keeper('storage-') def report_state(self): #TODO: aggregate the state of the system @@ -82,7 +83,7 @@ class BlockStore(object): def get_volume(self, volume_id): """ Returns a redis-backed volume object """ - if volume_id in self.keeper['volumes']: + if self.keeper.set_is_member('volumes', volume_id): return self.volume_class(volume_id=volume_id) raise exception.Error("Volume does not exist") diff --git a/run_tests.py b/run_tests.py index 535a0464a..886ab4bd0 100644 --- a/run_tests.py +++ b/run_tests.py @@ -44,7 +44,6 @@ from twisted.scripts import trial as trial_script from nova import flags from nova import twistd -from nova.tests.access_unittest import * from nova.tests.api_unittest import * from nova.tests.cloud_unittest import * from nova.tests.keeper_unittest import * |