diff options
45 files changed, 1377 insertions, 99 deletions
@@ -24,8 +24,10 @@ Josh Kearney <josh.kearney@rackspace.com> Joshua McKenty <jmckenty@gmail.com> Justin Santa Barbara <justin@fathomdb.com> Ken Pepple <ken.pepple@gmail.com> +Lorin Hochstein <lorin@isi.edu> Matt Dietz <matt.dietz@rackspace.com> Michael Gundlach <michael.gundlach@rackspace.com> +Monsyne Dragon <mdragon@rackspace.com> Monty Taylor <mordred@inaugust.com> Paul Voccio <paul@openstack.org> Rick Clark <rick@openstack.org> diff --git a/bin/nova-console b/bin/nova-console new file mode 100755 index 000000000..802cc80b6 --- /dev/null +++ b/bin/nova-console @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# All Rights Reserved. +# +# 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. + +"""Starter script for Nova Console Proxy.""" + +import eventlet +eventlet.monkey_patch() + +import gettext +import os +import sys + +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + +gettext.install('nova', unicode=1) + +from nova import service +from nova import utils + +if __name__ == '__main__': + utils.default_flagfile() + service.serve() + service.wait() diff --git a/bin/nova-manage b/bin/nova-manage index 40f540e5b..3f5957190 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -333,6 +333,11 @@ class ProjectCommands(object): arguments: name project_manager [description]""" self.manager.create_project(name, project_manager, description) + def modify(self, name, project_manager, description=None): + """Modifies a project + arguments: name project_manager [description]""" + self.manager.modify_project(name, project_manager, description) + def delete(self, name): """Deletes an existing project arguments: name""" diff --git a/krm_mapping.json.sample b/krm_mapping.json.sample new file mode 100644 index 000000000..1ecfba635 --- /dev/null +++ b/krm_mapping.json.sample @@ -0,0 +1,3 @@ +{ + "machine" : ["kernel", "ramdisk"] +} diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b426710bc..39174d554 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -399,8 +399,8 @@ class CloudController(object): criteria = self._revoke_rule_args_to_dict(context, **kwargs) if criteria == None: - raise exception.ApiError(_("No rule for the specified " - "parameters.")) + raise exception.ApiError(_("Not enough parameters to build a " + "valid rule.")) for rule in security_group.rules: match = True @@ -427,6 +427,9 @@ class CloudController(object): group_name) values = self._revoke_rule_args_to_dict(context, **kwargs) + if values is None: + raise exception.ApiError(_("Not enough parameters to build a " + "valid rule.")) values['parent_group_id'] = security_group.id if self._security_group_rule_exists(security_group, values): diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index ad203c51f..f96e2af91 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -31,10 +31,11 @@ from nova import utils from nova import wsgi from nova.api.openstack import faults from nova.api.openstack import backup_schedules +from nova.api.openstack import consoles from nova.api.openstack import flavors from nova.api.openstack import images from nova.api.openstack import servers -from nova.api.openstack import sharedipgroups +from nova.api.openstack import shared_ip_groups LOG = logging.getLogger('nova.api.openstack') @@ -47,6 +48,10 @@ flags.DEFINE_string('os_api_ratelimiting', 'nova.api.openstack.ratelimiting.RateLimitingMiddleware', 'Default ratelimiting implementation for the Openstack API') +flags.DEFINE_string('os_krm_mapping_file', + 'krm_mapping.json', + 'Location of OpenStack Flavor/OS:EC2 Kernel/Ramdisk/Machine JSON file.') + flags.DEFINE_bool('allow_admin_api', False, 'When True, this API service will accept admin operations.') @@ -100,12 +105,18 @@ class APIRouter(wsgi.Router): parent_resource=dict(member_name='server', collection_name='servers')) + mapper.resource("console", "consoles", + controller=consoles.Controller(), + parent_resource=dict(member_name='server', + collection_name='servers')) + mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) mapper.resource("flavor", "flavors", controller=flavors.Controller(), collection={'detail': 'GET'}) - mapper.resource("sharedipgroup", "sharedipgroups", - controller=sharedipgroups.Controller()) + mapper.resource("shared_ip_group", "shared_ip_groups", + collection={'detail': 'GET'}, + controller=shared_ip_groups.Controller()) super(APIRouter, self).__init__(mapper) diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py index fcc07bdd3..197125d86 100644 --- a/nova/api/openstack/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -15,7 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. +import logging import time + from webob import exc from nova import wsgi @@ -46,8 +48,8 @@ class Controller(wsgi.Controller): def create(self, req, server_id): """ No actual update method required, since the existing API allows both create and update through a POST """ - return faults.Fault(exc.HTTPNotFound()) + return faults.Fault(exc.HTTPNotImplemented()) def delete(self, req, server_id, id): """ Deletes an existing backup schedule """ - return faults.Fault(exc.HTTPNotFound()) + return faults.Fault(exc.HTTPNotImplemented()) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index ac0572c96..037ed47a0 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +from nova import exception + def limited(items, req): """Return a slice of items according to requested offset and limit. @@ -34,3 +36,25 @@ def limited(items, req): limit = min(1000, limit) range_end = offset + limit return items[offset:range_end] + + +def get_image_id_from_image_hash(image_service, context, image_hash): + """Given an Image ID Hash, return an objectstore Image ID. + + image_service - reference to objectstore compatible image service. + context - security context for image service requests. + image_hash - hash of the image ID. + """ + + # FIX(sandy): This is terribly inefficient. It pulls all images + # from objectstore in order to find the match. ObjectStore + # should have a numeric counterpart to the string ID. + try: + items = image_service.detail(context) + except NotImplementedError: + items = image_service.index(context) + for image in items: + image_id = image['imageId'] + if abs(hash(image_id)) == int(image_hash): + return image_id + raise exception.NotFound(image_hash) diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py new file mode 100644 index 000000000..9ebdbe710 --- /dev/null +++ b/nova/api/openstack/consoles.py @@ -0,0 +1,96 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack LLC. +# All Rights Reserved. +# +# 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. + +from webob import exc + +from nova import console +from nova import exception +from nova import wsgi +from nova.api.openstack import faults + + +def _translate_keys(cons): + """Coerces a console instance into proper dictionary format """ + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type']} + return dict(console=info) + + +def _translate_detail_keys(cons): + """Coerces a console instance into proper dictionary format with + correctly mapped attributes """ + pool = cons['pool'] + info = {'id': cons['id'], + 'console_type': pool['console_type'], + 'password': cons['password'], + 'port': cons['port'], + 'host': pool['public_hostname']} + return dict(console=info) + + +class Controller(wsgi.Controller): + """The Consoles Controller for the Openstack API""" + + _serialization_metadata = { + 'application/xml': { + 'attributes': { + 'console': []}}} + + def __init__(self): + self.console_api = console.API() + super(Controller, self).__init__() + + def index(self, req, server_id): + """Returns a list of consoles for this instance""" + consoles = self.console_api.get_consoles( + req.environ['nova.context'], + int(server_id)) + return dict(consoles=[_translate_keys(console) + for console in consoles]) + + def create(self, req, server_id): + """Creates a new console""" + #info = self._deserialize(req.body, req) + self.console_api.create_console( + req.environ['nova.context'], + int(server_id)) + + def show(self, req, server_id, id): + """Shows in-depth information on a specific console""" + try: + console = self.console_api.get_console( + req.environ['nova.context'], + int(server_id), + int(id)) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return _translate_detail_keys(console) + + def update(self, req, server_id, id): + """You can't update a console""" + raise faults.Fault(exc.HTTPNotImplemented()) + + def delete(self, req, server_id, id): + """Deletes a console""" + try: + self.console_api.delete_console(req.environ['nova.context'], + int(server_id), + int(id)) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + return exc.HTTPAccepted() diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 0b239aab8..a5f55a489 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from webob import exc from nova import compute @@ -26,6 +28,7 @@ from nova.api.openstack import common from nova.api.openstack import faults import nova.image.service + FLAGS = flags.FLAGS @@ -88,6 +91,12 @@ def _filter_keys(item, keys): return dict((k, v) for k, v in item.iteritems() if k in keys) +def _convert_image_id_to_hash(image): + image_id = abs(hash(image['imageId'])) + image['imageId'] = image_id + image['id'] = image_id + + class Controller(wsgi.Controller): _serialization_metadata = { @@ -112,6 +121,9 @@ class Controller(wsgi.Controller): items = self._service.detail(req.environ['nova.context']) except NotImplementedError: items = self._service.index(req.environ['nova.context']) + for image in items: + _convert_image_id_to_hash(image) + items = common.limited(items, req) items = [_translate_keys(item) for item in items] items = [_translate_status(item) for item in items] @@ -119,7 +131,12 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given image id""" - return dict(image=self._service.show(req.environ['nova.context'], id)) + image_id = common.get_image_id_from_image_hash(self._service, + req.environ['nova.context'], id) + + image = self._service.show(req.environ['nova.context'], image_id) + _convert_image_id_to_hash(image) + return dict(image=image) def delete(self, req, id): # Only public images are supported for now. diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 4cc44d352..29af82533 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -15,14 +15,17 @@ # License for the specific language governing permissions and limitations # under the License. +import json import traceback from webob import exc from nova import compute from nova import exception +from nova import flags from nova import log as logging from nova import wsgi +from nova import utils from nova.api.openstack import common from nova.api.openstack import faults from nova.auth import manager as auth_manager @@ -35,6 +38,9 @@ LOG = logging.getLogger('server') LOG.setLevel(logging.DEBUG) +FLAGS = flags.FLAGS + + def _translate_detail_keys(inst): """ Coerces into dictionary format, mapping everything to Rackspace-like attributes for return""" @@ -44,7 +50,7 @@ def _translate_detail_keys(inst): power_state.RUNNING: 'active', power_state.BLOCKED: 'active', power_state.SUSPENDED: 'suspended', - power_state.PAUSED: 'error', + power_state.PAUSED: 'paused', power_state.SHUTDOWN: 'active', power_state.SHUTOFF: 'active', power_state.CRASHED: 'error'} @@ -81,6 +87,7 @@ class Controller(wsgi.Controller): def __init__(self): self.compute_api = compute.API() + self._image_service = utils.import_object(FLAGS.image_service) super(Controller, self).__init__() def index(self, req): @@ -117,6 +124,18 @@ class Controller(wsgi.Controller): return faults.Fault(exc.HTTPNotFound()) return exc.HTTPAccepted() + def _get_kernel_ramdisk_from_image(self, image_id): + mapping_filename = FLAGS.os_krm_mapping_file + + with open(mapping_filename) as f: + mapping = json.load(f) + if image_id in mapping: + return mapping[image_id] + + raise exception.NotFound( + _("No entry for image '%s' in mapping file '%s'") % + (image_id, mapping_filename)) + def create(self, req): """ Creates a new server for a given user """ env = self._deserialize(req.body, req) @@ -125,10 +144,15 @@ class Controller(wsgi.Controller): key_pair = auth_manager.AuthManager.get_key_pairs( req.environ['nova.context'])[0] + image_id = common.get_image_id_from_image_hash(self._image_service, + req.environ['nova.context'], env['server']['imageId']) + kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(image_id) instances = self.compute_api.create( req.environ['nova.context'], instance_types.get_by_flavor_id(env['server']['flavorId']), - env['server']['imageId'], + image_id, + kernel_id=kernel_id, + ramdisk_id=ramdisk_id, display_name=env['server']['name'], display_description=env['server']['name'], key_name=key_pair['name'], @@ -158,6 +182,7 @@ class Controller(wsgi.Controller): """ Multi-purpose method used to reboot, rebuild, and resize a server """ input_dict = self._deserialize(req.body, req) + #TODO(sandy): rebuild/resize not supported. try: reboot_type = input_dict['reboot']['type'] except Exception: diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/shared_ip_groups.py index 845f5bead..bd3cc23a8 100644 --- a/nova/api/openstack/sharedipgroups.py +++ b/nova/api/openstack/shared_ip_groups.py @@ -15,6 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. +import logging + from webob import exc from nova import wsgi @@ -29,7 +31,7 @@ def _translate_keys(inst): def _translate_detail_keys(inst): """ Coerces a shared IP group instance into proper dictionary format with correctly mapped attributes """ - return dict(sharedIpGroup=inst) + return dict(sharedIpGroups=inst) class Controller(wsgi.Controller): @@ -54,12 +56,12 @@ class Controller(wsgi.Controller): def delete(self, req, id): """ Deletes a Shared IP Group """ - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(exc.HTTPNotImplemented()) - def detail(self, req, id): + def detail(self, req): """ Returns a complete list of Shared IP Groups """ return _translate_detail_keys({}) def create(self, req): """ Creates a new Shared IP group """ - raise faults.Fault(exc.HTTPNotFound()) + raise faults.Fault(exc.HTTPNotImplemented()) diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index c8de20028..bc53e0ec6 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -119,8 +119,7 @@ class LdapDriver(object): def get_project(self, pid): """Retrieve project by id""" - dn = 'cn=%s,%s' % (pid, - FLAGS.ldap_project_subtree) + dn = self.__project_to_dn(pid) attr = self.__find_object(dn, LdapDriver.project_pattern) return self.__to_project(attr) @@ -228,7 +227,8 @@ class LdapDriver(object): ('description', [description]), (LdapDriver.project_attribute, [manager_dn]), ('member', members)] - self.conn.add_s('cn=%s,%s' % (name, FLAGS.ldap_project_subtree), attr) + dn = self.__project_to_dn(name, search=False) + self.conn.add_s(dn, attr) return self.__to_project(dict(attr)) def modify_project(self, project_id, manager_uid=None, description=None): @@ -246,23 +246,22 @@ class LdapDriver(object): manager_dn)) if description: attr.append((self.ldap.MOD_REPLACE, 'description', description)) - self.conn.modify_s('cn=%s,%s' % (project_id, - FLAGS.ldap_project_subtree), - attr) + dn = self.__project_to_dn(project_id) + self.conn.modify_s(dn, attr) def add_to_project(self, uid, project_id): """Add user to project""" - dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) + dn = self.__project_to_dn(project_id) return self.__add_to_group(uid, dn) def remove_from_project(self, uid, project_id): """Remove user from project""" - dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) + dn = self.__project_to_dn(project_id) return self.__remove_from_group(uid, dn) def is_in_project(self, uid, project_id): """Check if user is in project""" - dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) + dn = self.__project_to_dn(project_id) return self.__is_in_group(uid, dn) def has_role(self, uid, role, project_id=None): @@ -302,7 +301,7 @@ class LdapDriver(object): roles.append(role) return roles else: - project_dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) + project_dn = self.__project_to_dn(project_id) query = ('(&(&(objectclass=groupOfNames)(!%s))(member=%s))' % (LdapDriver.project_pattern, self.__uid_to_dn(uid))) roles = self.__find_objects(project_dn, query) @@ -335,7 +334,7 @@ class LdapDriver(object): def delete_project(self, project_id): """Delete a project""" - project_dn = 'cn=%s,%s' % (project_id, FLAGS.ldap_project_subtree) + project_dn = self.__project_to_dn(project_id) self.__delete_roles(project_dn) self.__delete_group(project_dn) @@ -367,9 +366,10 @@ class LdapDriver(object): def __get_ldap_user(self, uid): """Retrieve LDAP user entry by id""" - attr = self.__find_object(self.__uid_to_dn(uid), - '(objectclass=novaUser)') - return attr + dn = FLAGS.ldap_user_subtree + query = ('(&(%s=%s)(objectclass=novaUser))' % + (FLAGS.ldap_user_id_attribute, uid)) + return self.__find_object(dn, query) def __find_object(self, dn, query=None, scope=None): """Find an object by dn and query""" @@ -420,15 +420,13 @@ class LdapDriver(object): query = '(objectclass=groupOfNames)' return self.__find_object(dn, query) is not None - @staticmethod - def __role_to_dn(role, project_id=None): + def __role_to_dn(self, role, project_id=None): """Convert role to corresponding dn""" if project_id is None: return FLAGS.__getitem__("ldap_%s" % role).value else: - return 'cn=%s,cn=%s,%s' % (role, - project_id, - FLAGS.ldap_project_subtree) + project_dn = self.__project_to_dn(project_id) + return 'cn=%s,%s' % (role, project_dn) def __create_group(self, group_dn, name, uid, description, member_uids=None): @@ -534,6 +532,42 @@ class LdapDriver(object): for role_dn in self.__find_role_dns(project_dn): self.__delete_group(role_dn) + def __to_project(self, attr): + """Convert ldap attributes to Project object""" + if attr is None: + return None + member_dns = attr.get('member', []) + return { + 'id': attr['cn'][0], + 'name': attr['cn'][0], + 'project_manager_id': + self.__dn_to_uid(attr[LdapDriver.project_attribute][0]), + 'description': attr.get('description', [None])[0], + 'member_ids': [self.__dn_to_uid(x) for x in member_dns]} + + def __uid_to_dn(self, uid, search=True): + """Convert uid to dn""" + # By default return a generated DN + userdn = (FLAGS.ldap_user_id_attribute + '=%s,%s' + % (uid, FLAGS.ldap_user_subtree)) + if search: + query = ('%s=%s' % (FLAGS.ldap_user_id_attribute, uid)) + user = self.__find_dns(FLAGS.ldap_user_subtree, query) + if len(user) > 0: + userdn = user[0] + return userdn + + def __project_to_dn(self, pid, search=True): + """Convert pid to dn""" + # By default return a generated DN + projectdn = ('cn=%s,%s' % (pid, FLAGS.ldap_project_subtree)) + if search: + query = ('(&(cn=%s)%s)' % (pid, LdapDriver.project_pattern)) + project = self.__find_dns(FLAGS.ldap_project_subtree, query) + if len(project) > 0: + projectdn = project[0] + return projectdn + @staticmethod def __to_user(attr): """Convert ldap attributes to User object""" @@ -550,30 +584,11 @@ class LdapDriver(object): else: return None - def __to_project(self, attr): - """Convert ldap attributes to Project object""" - if attr is None: - return None - member_dns = attr.get('member', []) - return { - 'id': attr['cn'][0], - 'name': attr['cn'][0], - 'project_manager_id': - self.__dn_to_uid(attr[LdapDriver.project_attribute][0]), - 'description': attr.get('description', [None])[0], - 'member_ids': [self.__dn_to_uid(x) for x in member_dns]} - @staticmethod def __dn_to_uid(dn): """Convert user dn to uid""" return dn.split(',')[0].split('=')[1] - @staticmethod - def __uid_to_dn(uid): - """Convert uid to dn""" - return (FLAGS.ldap_user_id_attribute + '=%s,%s' - % (uid, FLAGS.ldap_user_subtree)) - class FakeLdapDriver(LdapDriver): """Fake Ldap Auth driver""" diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 5685ae5e2..89f02998d 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -684,8 +684,7 @@ class AuthManager(object): else: regions = {'nova': FLAGS.cc_host} for region, host in regions.iteritems(): - rc = self.__generate_rc(user.access, - user.secret, + rc = self.__generate_rc(user, pid, use_dmz, host) @@ -725,7 +724,7 @@ class AuthManager(object): return self.__generate_rc(user.access, user.secret, pid, use_dmz) @staticmethod - def __generate_rc(access, secret, pid, use_dmz=True, host=None): + def __generate_rc(user, pid, use_dmz=True, host=None): """Generate rc file for user""" if use_dmz: cc_host = FLAGS.cc_dmz @@ -738,14 +737,19 @@ class AuthManager(object): s3_host = host cc_host = host rc = open(FLAGS.credentials_template).read() - rc = rc % {'access': access, + rc = rc % {'access': user.access, 'project': pid, - 'secret': secret, + 'secret': user.secret, 'ec2': '%s://%s:%s%s' % (FLAGS.ec2_prefix, cc_host, FLAGS.cc_port, FLAGS.ec2_suffix), 's3': 'http://%s:%s' % (s3_host, FLAGS.s3_port), + 'os': '%s://%s:%s%s' % (FLAGS.os_prefix, + cc_host, + FLAGS.cc_port, + FLAGS.os_suffix), + 'user': user.name, 'nova': FLAGS.ca_file, 'cert': FLAGS.credential_cert_file, 'key': FLAGS.credential_key_file} diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template index 1b8ecb173..c53a4acdc 100644 --- a/nova/auth/novarc.template +++ b/nova/auth/novarc.template @@ -10,3 +10,7 @@ export NOVA_CERT=${NOVA_KEY_DIR}/%(nova)s export EUCALYPTUS_CERT=${NOVA_CERT} # euca-bundle-image seems to require this set alias ec2-bundle-image="ec2-bundle-image --cert ${EC2_CERT} --privatekey ${EC2_PRIVATE_KEY} --user 42 --ec2cert ${NOVA_CERT}" alias ec2-upload-bundle="ec2-upload-bundle -a ${EC2_ACCESS_KEY} -s ${EC2_SECRET_KEY} --url ${S3_URL} --ec2cert ${NOVA_CERT}" +export CLOUD_SERVERS_API_KEY="%(access)s" +export CLOUD_SERVERS_USERNAME="%(user)s" +export CLOUD_SERVERS_URL="%(os)s" + diff --git a/nova/compute/api.py b/nova/compute/api.py index 632ce0efb..56402c11b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -108,6 +108,8 @@ class API(base.Base): ramdisk_id = None LOG.debug(_("Creating a raw instance")) # Make sure we have access to kernel and ramdisk (if not raw) + logging.debug("Using Kernel=%s, Ramdisk=%s" % + (kernel_id, ramdisk_id)) if kernel_id: self.image_service.show(context, kernel_id) if ramdisk_id: @@ -171,7 +173,8 @@ class API(base.Base): # Set sane defaults if not specified updates = dict(hostname=generate_hostname(instance_id)) - if 'display_name' not in instance: + if (not hasattr(instance, 'display_name')) or \ + instance.display_name == None: updates['display_name'] = "Server %s" % instance_id instance = self.update(context, instance_id, **updates) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index fd1e983fa..6b2fc4adb 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -35,6 +35,8 @@ terminating it. """ import datetime +import logging +import socket import functools from nova import exception @@ -52,6 +54,9 @@ flags.DEFINE_string('compute_driver', 'nova.virt.connection.get_connection', 'Driver to use for controlling virtualization') flags.DEFINE_string('stub_network', False, 'Stub network related code') +flags.DEFINE_string('console_host', socket.gethostname(), + 'Console proxy host to use to connect to instances on' + 'this host.') LOG = logging.getLogger('nova.compute.manager') @@ -122,6 +127,15 @@ class ComputeManager(manager.Manager): state = power_state.NOSTATE self.db.instance_set_state(context, instance_id, state) + def get_console_topic(self, context, **_kwargs): + """Retrieves the console host for a project on this host + Currently this is just set in the flags for each compute + host.""" + #TODO(mdragon): perhaps make this variable by console_type? + return self.db.queue_get_for(context, + FLAGS.console_topic, + FLAGS.console_host) + def get_network_topic(self, context, **_kwargs): """Retrieves the network host for a project on this host""" # TODO(vish): This method should be memoized. This will make @@ -136,6 +150,9 @@ class ComputeManager(manager.Manager): FLAGS.network_topic, host) + def get_console_pool_info(self, context, console_type): + return self.driver.get_console_pool_info(console_type) + @exception.wrap_exception def refresh_security_group_rules(self, context, security_group_id, **_kwargs): diff --git a/nova/console/__init__.py b/nova/console/__init__.py new file mode 100644 index 000000000..dfc72cd61 --- /dev/null +++ b/nova/console/__init__.py @@ -0,0 +1,13 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +""" +:mod:`nova.console` -- Console Prxy to set up VM console access (i.e. with xvp) +===================================================== + +.. automodule:: nova.console + :platform: Unix + :synopsis: Wrapper around console proxies such as xvp to set up + multitenant VM console access +.. moduleauthor:: Monsyne Dragon <mdragon@rackspace.com> +""" +from nova.console.api import API diff --git a/nova/console/api.py b/nova/console/api.py new file mode 100644 index 000000000..3850d2c44 --- /dev/null +++ b/nova/console/api.py @@ -0,0 +1,75 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# All Rights Reserved. +# +# 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. + +""" +Handles ConsoleProxy API requests +""" + +from nova import exception +from nova.db import base + + +from nova import flags +from nova import rpc + + +FLAGS = flags.FLAGS + + +class API(base.Base): + """API for spining up or down console proxy connections""" + + def __init__(self, **kwargs): + super(API, self).__init__(**kwargs) + + def get_consoles(self, context, instance_id): + return self.db.console_get_all_by_instance(context, instance_id) + + def get_console(self, context, instance_id, console_id): + return self.db.console_get(context, console_id, instance_id) + + def delete_console(self, context, instance_id, console_id): + console = self.db.console_get(context, + console_id, + instance_id) + pool = console['pool'] + rpc.cast(context, + self.db.queue_get_for(context, + FLAGS.console_topic, + pool['host']), + {"method": "remove_console", + "args": {"console_id": console['id']}}) + + def create_console(self, context, instance_id): + instance = self.db.instance_get(context, instance_id) + #NOTE(mdragon): If we wanted to return this the console info + # here, as we would need to do a call. + # They can just do an index later to fetch + # console info. I am not sure which is better + # here. + rpc.cast(context, + self._get_console_topic(context, instance['host']), + {"method": "add_console", + "args": {"instance_id": instance_id}}) + + def _get_console_topic(self, context, instance_host): + topic = self.db.queue_get_for(context, + FLAGS.compute_topic, + instance_host) + return rpc.call(context, + topic, + {"method": "get_console_topic", "args": {'fake': 1}}) diff --git a/nova/console/fake.py b/nova/console/fake.py new file mode 100644 index 000000000..7a90d5221 --- /dev/null +++ b/nova/console/fake.py @@ -0,0 +1,58 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# All Rights Reserved. +# +# 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. + +""" +Fake ConsoleProxy driver for tests. +""" + +from nova import exception + + +class FakeConsoleProxy(object): + """Fake ConsoleProxy driver.""" + + @property + def console_type(self): + return "fake" + + def setup_console(self, context, console): + """Sets up actual proxies""" + pass + + def teardown_console(self, context, console): + """Tears down actual proxies""" + pass + + def init_host(self): + """Start up any config'ed consoles on start""" + pass + + def generate_password(self, length=8): + """Returns random console password""" + return "fakepass" + + def get_port(self, context): + """get available port for consoles that need one""" + return 5999 + + def fix_pool_password(self, password): + """Trim password to length, and any other massaging""" + return password + + def fix_console_password(self, password): + """Trim password to length, and any other massaging""" + return password diff --git a/nova/console/manager.py b/nova/console/manager.py new file mode 100644 index 000000000..c55ca8e8f --- /dev/null +++ b/nova/console/manager.py @@ -0,0 +1,127 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# All Rights Reserved. +# +# 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. + +""" +Console Proxy Service +""" + +import functools +import logging +import socket + +from nova import exception +from nova import flags +from nova import manager +from nova import rpc +from nova import utils + +FLAGS = flags.FLAGS +flags.DEFINE_string('console_driver', + 'nova.console.xvp.XVPConsoleProxy', + 'Driver to use for the console proxy') +flags.DEFINE_boolean('stub_compute', False, + 'Stub calls to compute worker for tests') +flags.DEFINE_string('console_public_hostname', + socket.gethostname(), + 'Publicly visable name for this console host') + + +class ConsoleProxyManager(manager.Manager): + + """ Sets up and tears down any proxy connections needed for accessing + instance consoles securely""" + + def __init__(self, console_driver=None, *args, **kwargs): + if not console_driver: + console_driver = FLAGS.console_driver + self.driver = utils.import_object(console_driver) + super(ConsoleProxyManager, self).__init__(*args, **kwargs) + self.driver.host = self.host + + def init_host(self): + self.driver.init_host() + + @exception.wrap_exception + def add_console(self, context, instance_id, password=None, + port=None, **kwargs): + instance = self.db.instance_get(context, instance_id) + host = instance['host'] + name = instance['name'] + pool = self.get_pool_for_instance_host(context, host) + try: + console = self.db.console_get_by_pool_instance(context, + pool['id'], + instance_id) + except exception.NotFound: + logging.debug("Adding console") + if not password: + password = self.driver.generate_password() + if not port: + port = self.driver.get_port(context) + console_data = {'instance_name': name, + 'instance_id': instance_id, + 'password': password, + 'pool_id': pool['id']} + if port: + console_data['port'] = port + console = self.db.console_create(context, console_data) + self.driver.setup_console(context, console) + return console['id'] + + @exception.wrap_exception + def remove_console(self, context, console_id, **_kwargs): + try: + console = self.db.console_get(context, console_id) + except exception.NotFound: + logging.debug(_('Tried to remove non-existant console ' + '%(console_id)s.') % + {'console_id': console_id}) + return + self.db.console_delete(context, console_id) + self.driver.teardown_console(context, console) + + def get_pool_for_instance_host(self, context, instance_host): + context = context.elevated() + console_type = self.driver.console_type + try: + pool = self.db.console_pool_get_by_host_type(context, + instance_host, + self.host, + console_type) + except exception.NotFound: + #NOTE(mdragon): Right now, the only place this info exists is the + # compute worker's flagfile, at least for + # xenserver. Thus we ned to ask. + if FLAGS.stub_compute: + pool_info = {'address': '127.0.0.1', + 'username': 'test', + 'password': '1234pass'} + else: + pool_info = rpc.call(context, + self.db.queue_get_for(context, + FLAGS.compute_topic, + instance_host), + {"method": "get_console_pool_info", + "args": {"console_type": console_type}}) + pool_info['password'] = self.driver.fix_pool_password( + pool_info['password']) + pool_info['host'] = self.host + pool_info['public_hostname'] = FLAGS.console_public_hostname + pool_info['console_type'] = self.driver.console_type + pool_info['compute_host'] = instance_host + pool = self.db.console_pool_create(context, pool_info) + return pool diff --git a/nova/console/xvp.conf.template b/nova/console/xvp.conf.template new file mode 100644 index 000000000..695ddbe96 --- /dev/null +++ b/nova/console/xvp.conf.template @@ -0,0 +1,16 @@ +# One time password use with time window +OTP ALLOW IPCHECK HTTP 60 +#if $multiplex_port +MULTIPLEX $multiplex_port +#end if + +#for $pool in $pools +POOL $pool.address + DOMAIN $pool.address + MANAGER root $pool.password + HOST $pool.address + VM - dummy 0123456789ABCDEF + #for $console in $pool.consoles + VM #if $multiplex_port then '-' else $console.port # $console.instance_name $pass_encode($console.password) + #end for +#end for diff --git a/nova/console/xvp.py b/nova/console/xvp.py new file mode 100644 index 000000000..2a76223da --- /dev/null +++ b/nova/console/xvp.py @@ -0,0 +1,194 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# All Rights Reserved. +# +# 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. + +""" +XVP (Xenserver VNC Proxy) driver. +""" + +import fcntl +import logging +import os +import signal +import subprocess + +from Cheetah.Template import Template + +from nova import context +from nova import db +from nova import exception +from nova import flags +from nova import utils + +flags.DEFINE_string('console_xvp_conf_template', + utils.abspath('console/xvp.conf.template'), + 'XVP conf template') +flags.DEFINE_string('console_xvp_conf', + '/etc/xvp.conf', + 'generated XVP conf file') +flags.DEFINE_string('console_xvp_pid', + '/var/run/xvp.pid', + 'XVP master process pid file') +flags.DEFINE_string('console_xvp_log', + '/var/log/xvp.log', + 'XVP log file') +flags.DEFINE_integer('console_xvp_multiplex_port', + 5900, + "port for XVP to multiplex VNC connections on") +FLAGS = flags.FLAGS + + +class XVPConsoleProxy(object): + """Sets up XVP config, and manages xvp daemon""" + + def __init__(self): + self.xvpconf_template = open(FLAGS.console_xvp_conf_template).read() + self.host = FLAGS.host # default, set by manager. + super(XVPConsoleProxy, self).__init__() + + @property + def console_type(self): + return "vnc+xvp" + + def get_port(self, context): + """get available port for consoles that need one""" + #TODO(mdragon): implement port selection for non multiplex ports, + # we are not using that, but someone else may want + # it. + return FLAGS.console_xvp_multiplex_port + + def setup_console(self, context, console): + """Sets up actual proxies""" + self._rebuild_xvp_conf(context.elevated()) + + def teardown_console(self, context, console): + """Tears down actual proxies""" + self._rebuild_xvp_conf(context.elevated()) + + def init_host(self): + """Start up any config'ed consoles on start""" + ctxt = context.get_admin_context() + self._rebuild_xvp_conf(ctxt) + + def fix_pool_password(self, password): + """Trim password to length, and encode""" + return self._xvp_encrypt(password, is_pool_password=True) + + def fix_console_password(self, password): + """Trim password to length, and encode""" + return self._xvp_encrypt(password) + + def generate_password(self, length=8): + """Returns random console password""" + return os.urandom(length * 2).encode('base64')[:length] + + def _rebuild_xvp_conf(self, context): + logging.debug("Rebuilding xvp conf") + pools = [pool for pool in + db.console_pool_get_all_by_host_type(context, self.host, + self.console_type) + if pool['consoles']] + if not pools: + logging.debug("No console pools!") + self._xvp_stop() + return + conf_data = {'multiplex_port': FLAGS.console_xvp_multiplex_port, + 'pools': pools, + 'pass_encode': self.fix_console_password} + config = str(Template(self.xvpconf_template, searchList=[conf_data])) + self._write_conf(config) + self._xvp_restart() + + def _write_conf(self, config): + logging.debug('Re-wrote %s' % FLAGS.console_xvp_conf) + with open(FLAGS.console_xvp_conf, 'w') as cfile: + cfile.write(config) + + def _xvp_stop(self): + logging.debug("Stopping xvp") + pid = self._xvp_pid() + if not pid: + return + try: + os.kill(pid, signal.SIGTERM) + except OSError: + #if it's already not running, no problem. + pass + + def _xvp_start(self): + if self._xvp_check_running(): + return + logging.debug("Starting xvp") + try: + utils.execute('xvp -p %s -c %s -l %s' % + (FLAGS.console_xvp_pid, + FLAGS.console_xvp_conf, + FLAGS.console_xvp_log)) + except exception.ProcessExecutionError, err: + logging.error("Error starting xvp: %s" % err) + + def _xvp_restart(self): + logging.debug("Restarting xvp") + if not self._xvp_check_running(): + logging.debug("xvp not running...") + self._xvp_start() + else: + pid = self._xvp_pid() + os.kill(pid, signal.SIGUSR1) + + def _xvp_pid(self): + try: + with open(FLAGS.console_xvp_pid, 'r') as pidfile: + pid = int(pidfile.read()) + except IOError: + return None + except ValueError: + return None + return pid + + def _xvp_check_running(self): + pid = self._xvp_pid() + if not pid: + return False + try: + os.kill(pid, 0) + except OSError: + return False + return True + + def _xvp_encrypt(self, password, is_pool_password=False): + """Call xvp to obfuscate passwords for config file. + + Args: + - password: the password to encode, max 8 char for vm passwords, + and 16 chars for pool passwords. passwords will + be trimmed to max len before encoding. + - is_pool_password: True if this this is the XenServer api password + False if it's a VM console password + (xvp uses different keys and max lengths for pool passwords) + + Note that xvp's obfuscation should not be considered 'real' encryption. + It simply DES encrypts the passwords with static keys plainly viewable + in the xvp source code.""" + maxlen = 8 + flag = '-e' + if is_pool_password: + maxlen = 16 + flag = '-x' + #xvp will blow up on passwords that are too long (mdragon) + password = password[:maxlen] + out, err = utils.execute('xvp %s' % flag, process_input=password) + return out.strip() diff --git a/nova/db/api.py b/nova/db/api.py index a4d26ec85..cf84157bc 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -906,3 +906,57 @@ def host_get_networks(context, host): """ return IMPL.host_get_networks(context, host) + + +################## + + +def console_pool_create(context, values): + """Create console pool.""" + return IMPL.console_pool_create(context, values) + + +def console_pool_get(context, pool_id): + """Get a console pool.""" + return IMPL.console_pool_get(context, pool_id) + + +def console_pool_get_by_host_type(context, compute_host, proxy_host, + console_type): + """Fetch a console pool for a given proxy host, compute host, and type.""" + return IMPL.console_pool_get_by_host_type(context, + compute_host, + proxy_host, + console_type) + + +def console_pool_get_all_by_host_type(context, host, console_type): + """Fetch all pools for given proxy host and type.""" + return IMPL.console_pool_get_all_by_host_type(context, + host, + console_type) + + +def console_create(context, values): + """Create a console.""" + return IMPL.console_create(context, values) + + +def console_delete(context, console_id): + """Delete a console.""" + return IMPL.console_delete(context, console_id) + + +def console_get_by_pool_instance(context, pool_id, instance_id): + """Get console entry for a given instance and pool.""" + return IMPL.console_get_by_pool_instance(context, pool_id, instance_id) + + +def console_get_all_by_instance(context, instance_id): + """Get consoles for a given instance.""" + return IMPL.console_get_all_by_instance(context, instance_id) + + +def console_get(context, console_id, instance_id=None): + """Get a specific console (possibly on a given instance).""" + return IMPL.console_get(context, console_id, instance_id) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e475b4d8c..4561fa219 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1863,3 +1863,111 @@ def host_get_networks(context, host): filter_by(deleted=False).\ filter_by(host=host).\ all() + + +################## + + +def console_pool_create(context, values): + pool = models.ConsolePool() + pool.update(values) + pool.save() + return pool + + +def console_pool_get(context, pool_id): + session = get_session() + result = session.query(models.ConsolePool).\ + filter_by(deleted=False).\ + filter_by(id=pool_id).\ + first() + if not result: + raise exception.NotFound(_("No console pool with id %(pool_id)s") % + {'pool_id': pool_id}) + + return result + + +def console_pool_get_by_host_type(context, compute_host, host, + console_type): + session = get_session() + result = session.query(models.ConsolePool).\ + filter_by(host=host).\ + filter_by(console_type=console_type).\ + filter_by(compute_host=compute_host).\ + filter_by(deleted=False).\ + options(joinedload('consoles')).\ + first() + if not result: + raise exception.NotFound(_('No console pool of type %(type)s ' + 'for compute host %(compute_host)s ' + 'on proxy host %(host)s') % + {'type': console_type, + 'compute_host': compute_host, + 'host': host}) + return result + + +def console_pool_get_all_by_host_type(context, host, console_type): + session = get_session() + return session.query(models.ConsolePool).\ + filter_by(host=host).\ + filter_by(console_type=console_type).\ + filter_by(deleted=False).\ + options(joinedload('consoles')).\ + all() + + +def console_create(context, values): + console = models.Console() + console.update(values) + console.save() + return console + + +def console_delete(context, console_id): + session = get_session() + with session.begin(): + # consoles are meant to be transient. (mdragon) + session.execute('delete from consoles ' + 'where id=:id', {'id': console_id}) + + +def console_get_by_pool_instance(context, pool_id, instance_id): + session = get_session() + result = session.query(models.Console).\ + filter_by(pool_id=pool_id).\ + filter_by(instance_id=instance_id).\ + options(joinedload('pool')).\ + first() + if not result: + raise exception.NotFound(_('No console for instance %(instance_id)s ' + 'in pool %(pool_id)s') % + {'instance_id': instance_id, + 'pool_id': pool_id}) + return result + + +def console_get_all_by_instance(context, instance_id): + session = get_session() + results = session.query(models.Console).\ + filter_by(instance_id=instance_id).\ + options(joinedload('pool')).\ + all() + return results + + +def console_get(context, console_id, instance_id=None): + session = get_session() + query = session.query(models.Console).\ + filter_by(id=console_id) + if instance_id: + query = query.filter_by(instance_id=instance_id) + result = query.options(joinedload('pool')).first() + if not result: + idesc = (_("on instance %s") % instance_id) if instance_id else "" + raise exception.NotFound(_("No console with id %(console_id)s" + " %(instance)s") % + {'instance': idesc, + 'console_id': console_id}) + return result diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 1ed366127..2a966448c 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -540,6 +540,31 @@ class FloatingIp(BASE, NovaBase): host = Column(String(255)) # , ForeignKey('hosts.id')) +class ConsolePool(BASE, NovaBase): + """Represents pool of consoles on the same physical node.""" + __tablename__ = 'console_pools' + id = Column(Integer, primary_key=True) + address = Column(String(255)) + username = Column(String(255)) + password = Column(String(255)) + console_type = Column(String(255)) + public_hostname = Column(String(255)) + host = Column(String(255)) + compute_host = Column(String(255)) + + +class Console(BASE, NovaBase): + """Represents a console session for an instance.""" + __tablename__ = 'consoles' + id = Column(Integer, primary_key=True) + instance_name = Column(String(255)) + instance_id = Column(Integer) + password = Column(String(255)) + port = Column(Integer, nullable=True) + pool_id = Column(Integer, ForeignKey('console_pools.id')) + pool = relationship(ConsolePool, backref=backref('consoles')) + + def register_models(): """Register Models and create metadata. @@ -552,7 +577,7 @@ def register_models(): Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp, Network, SecurityGroup, SecurityGroupIngressRule, SecurityGroupInstanceAssociation, AuthToken, User, - Project, Certificate) # , Image, Host + Project, Certificate, ConsolePool, Console) # , Image, Host engine = create_engine(FLAGS.sql_connection, echo=False) for model in models: model.metadata.create_all(engine) diff --git a/nova/flags.py b/nova/flags.py index 42f177cdc..76ab2f788 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -200,10 +200,22 @@ def DECLARE(name, module_string, flag_values=FLAGS): "%s not defined by %s" % (name, module_string)) +def _get_my_ip(): + """Returns the actual ip of the local machine.""" + try: + csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + csock.connect(('8.8.8.8', 80)) + (addr, port) = csock.getsockname() + csock.close() + return addr + except socket.gaierror as ex: + return "127.0.0.1" + + # __GLOBAL FLAGS ONLY__ # Define any app-specific flags in their own files, docs at: -# http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#39 - +# http://code.google.com/p/python-gflags/source/browse/trunk/gflags.py#a9 +DEFINE_string('my_ip', _get_my_ip(), 'host ip address') DEFINE_list('region_list', [], 'list of region=url pairs separated by commas') @@ -211,11 +223,13 @@ DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_string('aws_access_key_id', 'admin', 'AWS Access ID') DEFINE_string('aws_secret_access_key', 'admin', 'AWS Access Key') DEFINE_integer('glance_port', 9292, 'glance port') -DEFINE_string('glance_host', '127.0.0.1', 'glance host') +DEFINE_string('glance_host', '$my_ip', 'glance host') DEFINE_integer('s3_port', 3333, 's3 port') -DEFINE_string('s3_host', '127.0.0.1', 's3 host (for infrastructure)') -DEFINE_string('s3_dmz', '127.0.0.1', 's3 dmz ip (for instances)') +DEFINE_string('s3_host', '$my_ip', 's3 host (for infrastructure)') +DEFINE_string('s3_dmz', '$my_ip', 's3 dmz ip (for instances)') DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on') +DEFINE_string('console_topic', 'console', + 'the topic console proxy nodes listen on') DEFINE_string('scheduler_topic', 'scheduler', 'the topic scheduler nodes listen on') DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') @@ -241,10 +255,12 @@ DEFINE_integer('rabbit_retry_interval', 10, 'rabbit connection retry interval') DEFINE_integer('rabbit_max_retries', 12, 'rabbit connection attempts') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') DEFINE_string('ec2_prefix', 'http', 'prefix for ec2') -DEFINE_string('cc_host', '127.0.0.1', 'ip of api server') -DEFINE_string('cc_dmz', '127.0.0.1', 'internal ip of api server') +DEFINE_string('os_prefix', 'http', 'prefix for openstack') +DEFINE_string('cc_host', '$my_ip', 'ip of api server') +DEFINE_string('cc_dmz', '$my_ip', 'internal ip of api server') DEFINE_integer('cc_port', 8773, 'cloud controller port') DEFINE_string('ec2_suffix', '/services/Cloud', 'suffix for ec2') +DEFINE_string('os_suffix', '/v1.0/', 'suffix for openstack') DEFINE_string('default_project', 'openstack', 'default project for openstack') DEFINE_string('default_image', 'ami-11111', @@ -276,6 +292,8 @@ DEFINE_integer('sql_retry_interval', 10, 'sql connection retry interval') DEFINE_string('compute_manager', 'nova.compute.manager.ComputeManager', 'Manager for compute') +DEFINE_string('console_manager', 'nova.console.manager.ConsoleProxyManager', + 'Manager for console proxy') DEFINE_string('network_manager', 'nova.network.manager.VlanManager', 'Manager for network') DEFINE_string('volume_manager', 'nova.volume.manager.VolumeManager', diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index eba9502e9..3743fc7e8 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -46,7 +46,7 @@ flags.DEFINE_string('vlan_interface', 'eth0', 'network device for vlans') flags.DEFINE_string('dhcpbridge', _bin_file('nova-dhcpbridge'), 'location of nova-dhcpbridge') -flags.DEFINE_string('routing_source_ip', utils.get_my_ip(), +flags.DEFINE_string('routing_source_ip', '$my_ip', 'Public IP of network host') flags.DEFINE_bool('use_nova_chains', False, 'use the nova_ routing chains instead of default') diff --git a/nova/network/manager.py b/nova/network/manager.py index fd286f210..c75ecc671 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -74,7 +74,7 @@ flags.DEFINE_string('flat_network_dhcp_start', '10.0.0.2', 'Dhcp start for FlatDhcp') flags.DEFINE_integer('vlan_start', 100, 'First VLAN for private networks') flags.DEFINE_integer('num_networks', 1000, 'Number of networks to support') -flags.DEFINE_string('vpn_ip', utils.get_my_ip(), +flags.DEFINE_string('vpn_ip', '$my_ip', 'Public IP for the cloudpipe VPN servers') flags.DEFINE_integer('vpn_start', 1000, 'First Vpn port for private networks') flags.DEFINE_integer('network_size', 256, diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 291a0e468..194304e79 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -107,7 +107,7 @@ def stub_out_rate_limiting(stubs): def stub_out_networking(stubs): def get_my_ip(): return '127.0.0.1' - stubs.Set(nova.utils, 'get_my_ip', get_my_ip) + stubs.Set(nova.flags, '_get_my_ip', get_my_ip) def stub_out_compute_api_snapshot(stubs): diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index f5be9c94f..00ca739a5 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -172,6 +172,7 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase): IMAGE_FIXTURES = [ {'id': '23g2ogk23k4hhkk4k42l', + 'imageId': '23g2ogk23k4hhkk4k42l', 'name': 'public image #1', 'created_at': str(datetime.datetime.utcnow()), 'updated_at': str(datetime.datetime.utcnow()), @@ -181,6 +182,7 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase): 'status': 'available', 'image_type': 'kernel'}, {'id': 'slkduhfas73kkaskgdas', + 'imageId': 'slkduhfas73kkaskgdas', 'name': 'public image #2', 'created_at': str(datetime.datetime.utcnow()), 'updated_at': str(datetime.datetime.utcnow()), diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 6e611a55d..0396daf98 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -133,6 +133,12 @@ class ServersTest(unittest.TestCase): def queue_get_for(context, *args): return 'network_topic' + def kernel_ramdisk_mapping(*args, **kwargs): + return (1, 1) + + def image_id_from_hash(*args, **kwargs): + return 2 + self.stubs.Set(nova.db.api, 'project_get_network', project_get_network) self.stubs.Set(nova.db.api, 'instance_create', instance_create) self.stubs.Set(nova.rpc, 'cast', fake_method) @@ -142,6 +148,10 @@ class ServersTest(unittest.TestCase): self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for) self.stubs.Set(nova.network.manager.VlanManager, 'allocate_fixed_ip', fake_method) + self.stubs.Set(nova.api.openstack.servers.Controller, + "_get_kernel_ramdisk_from_image", kernel_ramdisk_mapping) + self.stubs.Set(nova.api.openstack.common, + "get_image_id_from_image_hash", image_id_from_hash) body = dict(server=dict( name='server_test', imageId=2, flavorId=2, metadata={}, diff --git a/nova/tests/api/openstack/test_sharedipgroups.py b/nova/tests/api/openstack/test_shared_ip_groups.py index d199951d8..c2fc3a203 100644 --- a/nova/tests/api/openstack/test_sharedipgroups.py +++ b/nova/tests/api/openstack/test_shared_ip_groups.py @@ -19,7 +19,7 @@ import unittest import stubout -from nova.api.openstack import sharedipgroups +from nova.api.openstack import shared_ip_groups class SharedIpGroupsTest(unittest.TestCase): diff --git a/nova/tests/test_console.py b/nova/tests/test_console.py new file mode 100644 index 000000000..31b5ca79c --- /dev/null +++ b/nova/tests/test_console.py @@ -0,0 +1,129 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2010 Openstack, LLC. +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. + +""" +Tests For Console proxy. +""" + +import datetime +import logging + +from nova import context +from nova import db +from nova import exception +from nova import flags +from nova import test +from nova import utils +from nova.auth import manager +from nova.console import manager as console_manager + +FLAGS = flags.FLAGS + + +class ConsoleTestCase(test.TestCase): + """Test case for console proxy""" + def setUp(self): + logging.getLogger().setLevel(logging.DEBUG) + super(ConsoleTestCase, self).setUp() + self.flags(console_driver='nova.console.fake.FakeConsoleProxy', + stub_compute=True) + self.console = utils.import_object(FLAGS.console_manager) + self.manager = manager.AuthManager() + self.user = self.manager.create_user('fake', 'fake', 'fake') + self.project = self.manager.create_project('fake', 'fake', 'fake') + self.context = context.get_admin_context() + self.host = 'test_compute_host' + + def tearDown(self): + self.manager.delete_user(self.user) + self.manager.delete_project(self.project) + super(ConsoleTestCase, self).tearDown() + + def _create_instance(self): + """Create a test instance""" + inst = {} + #inst['host'] = self.host + #inst['name'] = 'instance-1234' + inst['image_id'] = 'ami-test' + inst['reservation_id'] = 'r-fakeres' + inst['launch_time'] = '10' + inst['user_id'] = self.user.id + inst['project_id'] = self.project.id + inst['instance_type'] = 'm1.tiny' + inst['mac_address'] = utils.generate_mac() + inst['ami_launch_index'] = 0 + return db.instance_create(self.context, inst)['id'] + + def test_get_pool_for_instance_host(self): + pool = self.console.get_pool_for_instance_host(self.context, self.host) + self.assertEqual(pool['compute_host'], self.host) + + def test_get_pool_creates_new_pool_if_needed(self): + self.assertRaises(exception.NotFound, + db.console_pool_get_by_host_type, + self.context, + self.host, + self.console.host, + self.console.driver.console_type) + pool = self.console.get_pool_for_instance_host(self.context, + self.host) + pool2 = db.console_pool_get_by_host_type(self.context, + self.host, + self.console.host, + self.console.driver.console_type) + self.assertEqual(pool['id'], pool2['id']) + + def test_get_pool_does_not_create_new_pool_if_exists(self): + pool_info = {'address': '127.0.0.1', + 'username': 'test', + 'password': '1234pass', + 'host': self.console.host, + 'console_type': self.console.driver.console_type, + 'compute_host': 'sometesthostname'} + new_pool = db.console_pool_create(self.context, pool_info) + pool = self.console.get_pool_for_instance_host(self.context, + 'sometesthostname') + self.assertEqual(pool['id'], new_pool['id']) + + def test_add_console(self): + instance_id = self._create_instance() + self.console.add_console(self.context, instance_id) + instance = db.instance_get(self.context, instance_id) + pool = db.console_pool_get_by_host_type(self.context, + instance['host'], + self.console.host, + self.console.driver.console_type) + + console_instances = [con['instance_id'] for con in pool.consoles] + self.assert_(instance_id in console_instances) + + def test_add_console_does_not_duplicate(self): + instance_id = self._create_instance() + cons1 = self.console.add_console(self.context, instance_id) + cons2 = self.console.add_console(self.context, instance_id) + self.assertEqual(cons1, cons2) + + def test_remove_console(self): + instance_id = self._create_instance() + console_id = self.console.add_console(self.context, instance_id) + self.console.remove_console(self.context, console_id) + + self.assertRaises(exception.NotFound, + db.console_get, + self.context, + console_id) diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 59053f4d0..afdc89ba2 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -249,7 +249,7 @@ class IptablesFirewallTestCase(test.TestCase): '-A FORWARD -o virbr0 -j REJECT --reject-with icmp-port-unreachable ', '-A FORWARD -i virbr0 -j REJECT --reject-with icmp-port-unreachable ', 'COMMIT', - '# Completed on Mon Dec 6 11:54:13 2010' + '# Completed on Mon Dec 6 11:54:13 2010', ] def test_static_filters(self): @@ -278,6 +278,20 @@ class IptablesFirewallTestCase(test.TestCase): db.security_group_rule_create(admin_ctxt, {'parent_group_id': secgroup['id'], + 'protocol': 'icmp', + 'from_port': -1, + 'to_port': -1, + 'cidr': '192.168.11.0/24'}) + + db.security_group_rule_create(admin_ctxt, + {'parent_group_id': secgroup['id'], + 'protocol': 'icmp', + 'from_port': 8, + 'to_port': -1, + 'cidr': '192.168.11.0/24'}) + + db.security_group_rule_create(admin_ctxt, + {'parent_group_id': secgroup['id'], 'protocol': 'tcp', 'from_port': 80, 'to_port': 81, @@ -297,7 +311,35 @@ class IptablesFirewallTestCase(test.TestCase): self.assertTrue(rule in out_rules, 'Rule went missing: %s' % rule) - print '\n'.join(out_rules) + instance_chain = None + for rule in out_rules: + # This is pretty crude, but it'll do for now + if '-d 10.11.12.13 -j' in rule: + instance_chain = rule.split(' ')[-1] + break + self.assertTrue(instance_chain, "The instance chain wasn't added") + + security_group_chain = None + for rule in out_rules: + # This is pretty crude, but it'll do for now + if '-A %s -j' % instance_chain in rule: + security_group_chain = rule.split(' ')[-1] + break + self.assertTrue(security_group_chain, + "The security group chain wasn't added") + + self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -j ACCEPT' % \ + security_group_chain in out_rules, + "ICMP acceptance rule wasn't added") + + self.assertTrue('-A %s -p icmp -s 192.168.11.0/24 -m icmp --icmp-type' + ' 8 -j ACCEPT' % security_group_chain in out_rules, + "ICMP Echo Request acceptance rule wasn't added") + + self.assertTrue('-A %s -p tcp -s 192.168.10.0/24 -m multiport ' + '--dports 80:81 -j ACCEPT' % security_group_chain \ + in out_rules, + "TCP port 80/81 acceptance rule wasn't added") class NWFilterTestCase(test.TestCase): diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 55f751f11..292bd9ba9 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -41,9 +41,33 @@ def stubout_instance_snapshot(stubs): rv = done.wait() return rv + def fake_loop(self): + pass + stubs.Set(xenapi_conn.XenAPISession, 'wait_for_task', fake_wait_for_task) + stubs.Set(xenapi_conn.XenAPISession, '_stop_loop', fake_loop) + + from nova.virt.xenapi.fake import create_vdi + name_label = "instance-%s" % instance_id + #TODO: create fake SR record + sr_ref = "fakesr" + vdi_ref = create_vdi(name_label=name_label, read_only=False, + sr_ref=sr_ref, sharable=False) + vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref) + vdi_uuid = vdi_rec['uuid'] + return vdi_uuid + + stubs.Set(vm_utils.VMHelper, 'fetch_image', fake_fetch_image) + + def fake_parse_xmlrpc_value(val): + return val + + stubs.Set(xenapi_conn, '_parse_xmlrpc_value', fake_parse_xmlrpc_value) + + def fake_wait_for_vhd_coalesce(session, instance_id, sr_ref, vdi_ref, + original_parent_uuid): from nova.virt.xenapi.fake import create_vdi name_label = "instance-%s" % instance_id #TODO: create fake SR record diff --git a/nova/utils.py b/nova/utils.py index 21b12aaa9..45adb7b38 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -200,19 +200,6 @@ def last_octet(address): return int(address.split(".")[-1]) -def get_my_ip(): - """Returns the actual ip of the local machine.""" - try: - csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - csock.connect(('8.8.8.8', 80)) - (addr, port) = csock.getsockname() - csock.close() - return addr - except socket.gaierror as ex: - LOG.warn(_("Couldn't get IP, using 127.0.0.1 %s"), ex) - return "127.0.0.1" - - def utcnow(): """Overridable version of datetime.datetime.utcnow.""" if utcnow.override_time: diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 8abe08e41..9186d885e 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -292,6 +292,11 @@ class FakeConnection(object): def get_ajax_console(self, instance): return 'http://fakeajaxconsole.com/?token=FAKETOKEN' + def get_console_pool_info(self, console_type): + return {'address': '127.0.0.1', + 'username': 'fakeuser', + 'password': 'fakepassword'} + class FakeInstance(object): diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py index d71387ac0..30dc1c79b 100644 --- a/nova/virt/hyperv.py +++ b/nova/virt/hyperv.py @@ -92,7 +92,7 @@ REQ_POWER_STATE = { 'Reboot': 10, 'Reset': 11, 'Paused': 32768, - 'Suspended': 32769 + 'Suspended': 32769, } diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 4a907c88f..655c55fa1 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -752,6 +752,14 @@ class LibvirtConnection(object): domain = self._conn.lookupByName(instance_name) return domain.interfaceStats(interface) + def get_console_pool_info(self, console_type): + #TODO(mdragon): console proxy should be implemented for libvirt, + # in case someone wants to use it with kvm or + # such. For now return fake data. + return {'address': '127.0.0.1', + 'username': 'fakeuser', + 'password': 'fakepassword'} + def refresh_security_group_rules(self, security_group_id): self.firewall_driver.refresh_security_group_rules(security_group_id) @@ -1150,15 +1158,15 @@ class IptablesFirewallDriver(FirewallDriver): icmp_type = rule.from_port icmp_code = rule.to_port - if icmp_type == '-1': + if icmp_type == -1: icmp_type_arg = None else: icmp_type_arg = '%s' % icmp_type - if not icmp_code == '-1': + if not icmp_code == -1: icmp_type_arg += '/%s' % icmp_code if icmp_type_arg: - args += ['-m', 'icmp', '--icmp_type', icmp_type_arg] + args += ['-m', 'icmp', '--icmp-type', icmp_type_arg] args += ['-j ACCEPT'] our_rules += [' '.join(args)] diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index 1e9448a26..a91c8ea27 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -357,7 +357,9 @@ class VMHelper(HelperBase): if i >= 3 and i <= 11: ref = node.childNodes # Name and Value - diags[ref[0].firstChild.data] = ref[6].firstChild.data + if len(ref) > 6: + diags[ref[0].firstChild.data] = \ + ref[6].firstChild.data return diags except cls.XenAPI.Failure as e: return {"Unable to retrieve diagnostics": e} diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index fe1e16877..45d0738a5 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -52,6 +52,7 @@ reactor thread if the VM.get_by_name_label or VM.get_record calls block. """ import sys +import urlparse import xmlrpclib from eventlet import event @@ -194,6 +195,12 @@ class XenAPIConnection(object): """Detach volume storage to VM instance""" return self._volumeops.detach_volume(instance_name, mountpoint) + def get_console_pool_info(self, console_type): + xs_url = urlparse.urlparse(FLAGS.xenapi_connection_url) + return {'address': xs_url.netloc, + 'username': FLAGS.xenapi_connection_username, + 'password': FLAGS.xenapi_connection_password} + class XenAPISession(object): """The session to invoke XenAPI SDK calls""" @@ -202,6 +209,7 @@ class XenAPISession(object): self.XenAPI = self.get_imported_xenapi() self._session = self._create_session(url) self._session.login_with_password(user, pw) + self.loop = None def get_imported_xenapi(self): """Stubout point. This can be replaced with a mock xenapi module.""" @@ -238,14 +246,20 @@ class XenAPISession(object): def wait_for_task(self, id, task): """Return the result of the given task. The task is polled - until it completes.""" + until it completes. Not re-entrant.""" done = event.Event() - loop = utils.LoopingCall(self._poll_task, id, task, done) - loop.start(FLAGS.xenapi_task_poll_interval, now=True) + self.loop = utils.LoopingCall(self._poll_task, id, task, done) + self.loop.start(FLAGS.xenapi_task_poll_interval, now=True) rv = done.wait() - loop.stop() + self.loop.stop() return rv + def _stop_loop(self): + """Stop polling for task to finish.""" + #NOTE(sandy-walsh) Had to break this call out to support unit tests. + if self.loop: + self.loop.stop() + def _create_session(self, url): """Stubout point. This can be replaced with a mock session.""" return self.XenAPI.Session(url) @@ -282,6 +296,7 @@ class XenAPISession(object): except self.XenAPI.Failure, exc: LOG.warn(exc) done.send_exception(*sys.exc_info()) + self._stop_loop() def _unwrap_plugin_exceptions(self, func, *args, **kwargs): """Parse exception details""" diff --git a/smoketests/admin_smoketests.py b/smoketests/admin_smoketests.py index 50bb3fa2e..1ef1c1425 100644 --- a/smoketests/admin_smoketests.py +++ b/smoketests/admin_smoketests.py @@ -19,10 +19,17 @@ import os import random import sys -import time import unittest import zipfile +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + from nova import adminclient from smoketests import flags from smoketests import base diff --git a/smoketests/user_smoketests.py b/smoketests/user_smoketests.py index d29e3aea3..578c0722e 100644 --- a/smoketests/user_smoketests.py +++ b/smoketests/user_smoketests.py @@ -24,6 +24,14 @@ import sys import time import unittest +# If ../nova/__init__.py exists, add ../ to Python search path, so that +# it will override what happens to be installed in /usr/(local/)lib/python... +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), + os.pardir, + os.pardir)) +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): + sys.path.insert(0, possible_topdir) + from smoketests import flags from smoketests import base @@ -40,6 +48,7 @@ flags.DEFINE_string('bundle_image', 'openwrt-x86-ext2.image', TEST_PREFIX = 'test%s' % int (random.random()*1000000) TEST_BUCKET = '%s_bucket' % TEST_PREFIX TEST_KEY = '%s_key' % TEST_PREFIX +TEST_GROUP = '%s_group' % TEST_PREFIX TEST_DATA = {} @@ -137,7 +146,7 @@ class InstanceTests(UserSmokeTestCase): self.data['instance_id'] = reservation.instances[0].id def test_003_instance_runs_within_60_seconds(self): - reservations = self.conn.get_all_instances([data['instance_id']]) + reservations = self.conn.get_all_instances([self.data['instance_id']]) instance = reservations[0].instances[0] # allow 60 seconds to exit pending with IP for x in xrange(60): @@ -207,7 +216,7 @@ class InstanceTests(UserSmokeTestCase): def test_999_tearDown(self): self.delete_key_pair(self.conn, TEST_KEY) if self.data.has_key('instance_id'): - self.conn.terminate_instances([data['instance_id']]) + self.conn.terminate_instances([self.data['instance_id']]) class VolumeTests(UserSmokeTestCase): @@ -319,8 +328,80 @@ class VolumeTests(UserSmokeTestCase): self.conn.delete_key_pair(TEST_KEY) +class SecurityGroupTests(UserSmokeTestCase): + + def __public_instance_is_accessible(self): + id_url = "latest/meta-data/instance-id" + options = "-s --max-time 1" + command = "curl %s %s/%s" % (options, self.data['public_ip'], id_url) + instance_id = commands.getoutput(command).strip() + if not instance_id: + return False + if instance_id != self.data['instance_id']: + raise Exception("Wrong instance id") + return True + + def test_001_can_create_security_group(self): + self.conn.create_security_group(TEST_GROUP, description='test') + + groups = self.conn.get_all_security_groups() + self.assertTrue(TEST_GROUP in [group.name for group in groups]) + + def test_002_can_launch_instance_in_security_group(self): + self.create_key_pair(self.conn, TEST_KEY) + reservation = self.conn.run_instances(FLAGS.test_image, + key_name=TEST_KEY, + security_groups=[TEST_GROUP], + instance_type='m1.tiny') + + self.data['instance_id'] = reservation.instances[0].id + + def test_003_can_authorize_security_group_ingress(self): + self.assertTrue(self.conn.authorize_security_group(TEST_GROUP, + ip_protocol='tcp', + from_port=80, + to_port=80)) + + def test_004_can_access_instance_over_public_ip(self): + result = self.conn.allocate_address() + self.assertTrue(hasattr(result, 'public_ip')) + self.data['public_ip'] = result.public_ip + + result = self.conn.associate_address(self.data['instance_id'], + self.data['public_ip']) + start_time = time.time() + while not self.__public_instance_is_accessible(): + # 1 minute to launch + if time.time() - start_time > 60: + raise Exception("Timeout") + time.sleep(1) + + def test_005_can_revoke_security_group_ingress(self): + self.assertTrue(self.conn.revoke_security_group(TEST_GROUP, + ip_protocol='tcp', + from_port=80, + to_port=80)) + start_time = time.time() + while self.__public_instance_is_accessible(): + # 1 minute to teardown + if time.time() - start_time > 60: + raise Exception("Timeout") + time.sleep(1) + + + def test_999_tearDown(self): + self.conn.delete_key_pair(TEST_KEY) + self.conn.delete_security_group(TEST_GROUP) + groups = self.conn.get_all_security_groups() + self.assertFalse(TEST_GROUP in [group.name for group in groups]) + self.conn.terminate_instances([self.data['instance_id']]) + self.assertTrue(self.conn.release_address(self.data['public_ip'])) + + if __name__ == "__main__": suites = {'image': unittest.makeSuite(ImageTests), 'instance': unittest.makeSuite(InstanceTests), - 'volume': unittest.makeSuite(VolumeTests)} + 'security_group': unittest.makeSuite(SecurityGroupTests), + 'volume': unittest.makeSuite(VolumeTests) + } sys.exit(base.run_tests(suites)) diff --git a/tools/install_venv.py b/tools/install_venv.py index 32c372352..4e3941210 100644 --- a/tools/install_venv.py +++ b/tools/install_venv.py @@ -66,7 +66,8 @@ def check_dependencies(): # Try installing it via easy_install... if HAS_EASY_INSTALL: print 'Installing virtualenv via easy_install...', - if not run_command(['which', 'easy_install']): + if not (run_command(['which', 'easy_install']) and + run_command(['easy_install', 'virtualenv'])): die('ERROR: virtualenv not found.\n\nNova development requires virtualenv,' ' please install it using your favorite package management tool') print 'done.' |