summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorEd Leafe <ed@leafe.com>2011-01-12 13:22:01 -0600
committerEd Leafe <ed@leafe.com>2011-01-12 13:22:01 -0600
commitea1f5ad611a84931dc4a02b3c4bf2c6ef89f7360 (patch)
treefd74b5aa6b37916037e6f41fb9c6145ffe37f387 /nova
parent5b5fa0eb4b9ffd597b6e49b2cef5a2ad9028d55f (diff)
parent3d57735caf78fd421da6e660c4d56c635706fa7d (diff)
cleaned up prior merge mess
Diffstat (limited to 'nova')
-rw-r--r--nova/api/ec2/cloud.py57
-rw-r--r--nova/api/openstack/__init__.py11
-rw-r--r--nova/api/openstack/backup_schedules.py6
-rw-r--r--nova/api/openstack/common.py24
-rw-r--r--nova/api/openstack/images.py19
-rw-r--r--nova/api/openstack/servers.py38
-rw-r--r--nova/api/openstack/shared_ip_groups.py (renamed from nova/api/openstack/sharedipgroups.py)10
-rw-r--r--nova/auth/ldapdriver.py91
-rw-r--r--nova/auth/manager.py14
-rw-r--r--nova/auth/novarc.template4
-rw-r--r--nova/compute/api.py27
-rw-r--r--nova/compute/manager.py8
-rw-r--r--nova/db/api.py13
-rw-r--r--nova/db/sqlalchemy/api.py17
-rw-r--r--nova/db/sqlalchemy/models.py1
-rw-r--r--nova/flags.py12
-rw-r--r--nova/scheduler/zone.py56
-rw-r--r--nova/service.py4
-rw-r--r--nova/tests/api/openstack/test_images.py2
-rw-r--r--nova/tests/api/openstack/test_servers.py10
-rw-r--r--nova/tests/api/openstack/test_shared_ip_groups.py (renamed from nova/tests/api/openstack/test_sharedipgroups.py)2
-rw-r--r--nova/tests/test_cloud.py59
-rw-r--r--nova/tests/test_compute.py10
-rw-r--r--nova/tests/test_scheduler.py54
-rw-r--r--nova/tests/test_service.py15
-rw-r--r--nova/tests/test_virt.py44
-rw-r--r--nova/utils.py5
-rw-r--r--nova/virt/fake.py3
-rw-r--r--nova/virt/libvirt.xml.template13
-rw-r--r--nova/virt/libvirt_conn.py51
-rw-r--r--nova/virt/xenapi/vm_utils.py4
-rw-r--r--nova/virt/xenapi/vmops.py24
-rw-r--r--nova/virt/xenapi_conn.py4
33 files changed, 620 insertions, 92 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 99a9677c4..832426b94 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -132,6 +132,21 @@ class CloudController(object):
result[key] = [line]
return result
+ def _trigger_refresh_security_group(self, context, security_group):
+ nodes = set([instance['host'] for instance in security_group.instances
+ if instance['host'] is not None])
+ for node in nodes:
+ rpc.cast(context,
+ '%s.%s' % (FLAGS.compute_topic, node),
+ {"method": "refresh_security_group",
+ "args": {"security_group_id": security_group.id}})
+
+ def _get_availability_zone_by_host(self, context, host):
+ services = db.service_get_all_by_host(context, host)
+ if len(services) > 0:
+ return services[0]['availability_zone']
+ return 'unknown zone'
+
def get_metadata(self, address):
ctxt = context.get_admin_context()
instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address)
@@ -144,6 +159,8 @@ class CloudController(object):
else:
keys = ''
hostname = instance_ref['hostname']
+ host = instance_ref['host']
+ availability_zone = self._get_availability_zone_by_host(ctxt, host)
floating_ip = db.instance_get_floating_address(ctxt,
instance_ref['id'])
ec2_id = id_to_ec2_id(instance_ref['id'])
@@ -166,8 +183,7 @@ class CloudController(object):
'local-hostname': hostname,
'local-ipv4': address,
'kernel-id': instance_ref['kernel_id'],
- # TODO(vish): real zone
- 'placement': {'availability-zone': 'nova'},
+ 'placement': {'availability-zone': availability_zone},
'public-hostname': hostname,
'public-ipv4': floating_ip or '',
'public-keys': keys,
@@ -191,8 +207,26 @@ class CloudController(object):
return self._describe_availability_zones(context, **kwargs)
def _describe_availability_zones(self, context, **kwargs):
- return {'availabilityZoneInfo': [{'zoneName': 'nova',
- 'zoneState': 'available'}]}
+ enabled_services = db.service_get_all(context)
+ disabled_services = db.service_get_all(context, True)
+ available_zones = []
+ for zone in [service.availability_zone for service
+ in enabled_services]:
+ if not zone in available_zones:
+ available_zones.append(zone)
+ not_available_zones = []
+ for zone in [service.availability_zone for service in disabled_services
+ if not service['availability_zone'] in available_zones]:
+ if not zone in not_available_zones:
+ not_available_zones.append(zone)
+ result = []
+ for zone in available_zones:
+ result.append({'zoneName': zone,
+ 'zoneState': "available"})
+ for zone in not_available_zones:
+ result.append({'zoneName': zone,
+ 'zoneState': "not available"})
+ return {'availabilityZoneInfo': result}
def _describe_availability_zones_verbose(self, context, **kwargs):
rv = {'availabilityZoneInfo': [{'zoneName': 'nova',
@@ -399,8 +433,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 +461,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):
@@ -498,6 +535,11 @@ class CloudController(object):
"Timestamp": now,
"output": base64.b64encode(output)}
+ def get_ajax_console(self, context, instance_id, **kwargs):
+ ec2_id = instance_id[0]
+ internal_id = ec2_id_to_id(ec2_id)
+ return self.compute_api.get_ajax_console(context, internal_id)
+
def describe_volumes(self, context, volume_id=None, **kwargs):
volumes = self.volume_api.get_all(context)
# NOTE(vish): volume_id is an optional list of volume ids to filter by.
@@ -646,6 +688,9 @@ class CloudController(object):
i['amiLaunchIndex'] = instance['launch_index']
i['displayName'] = instance['display_name']
i['displayDescription'] = instance['display_description']
+ host = instance['host']
+ zone = self._get_availability_zone_by_host(context, host)
+ i['placement'] = {'availabilityZone': zone}
if instance['reservation_id'] not in reservations:
r = {}
r['reservationId'] = instance['reservation_id']
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 7b999c87b..f96e2af91 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -35,7 +35,7 @@ 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')
@@ -48,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.')
@@ -110,8 +114,9 @@ class APIRouter(wsgi.Router):
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/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 764c843ac..8cbcebed2 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'],
@@ -161,6 +185,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:
@@ -261,6 +286,15 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ def get_ajax_console(self, req, id):
+ """ Returns a url to an instance's ajaxterm console. """
+ try:
+ self.compute_api.get_ajax_console(req.environ['nova.context'],
+ int(id))
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return exc.HTTPAccepted()
+
def diagnostics(self, req, id):
"""Permit Admins to retrieve server diagnostics."""
ctxt = req.environ["nova.context"]
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 10d7b67cf..90273da36 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)
@@ -183,7 +186,8 @@ class API(base.Base):
FLAGS.scheduler_topic,
{"method": "run_instance",
"args": {"topic": FLAGS.compute_topic,
- "instance_id": instance_id}})
+ "instance_id": instance_id,
+ "availability_zone": availability_zone}})
for group_id in security_groups:
self.trigger_security_group_members_refresh(elevated, group_id)
@@ -379,6 +383,25 @@ class API(base.Base):
"""Set the root/admin password for the given instance."""
self._cast_compute_message('set_admin_password', context, instance_id)
+ def get_ajax_console(self, context, instance_id):
+ """Get a url to an AJAX Console"""
+
+ instance = self.get(context, instance_id)
+
+ output = rpc.call(context,
+ '%s.%s' % (FLAGS.compute_topic,
+ instance['host']),
+ {'method': 'get_ajax_console',
+ 'args': {'instance_id': instance['id']}})
+
+ rpc.cast(context, '%s' % FLAGS.ajax_console_proxy_topic,
+ {'method': 'authorize_ajax_console',
+ 'args': {'token': output['token'], 'host': output['host'],
+ 'port': output['port']}})
+
+ return {'url': '%s?token=%s' % (FLAGS.ajax_console_proxy_url,
+ output['token'])}
+
def lock(self, context, instance_id):
"""lock the instance with instance_id"""
self._cast_compute_message('lock_instance', context, instance_id)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 9872cc65b..267beca45 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -504,6 +504,14 @@ class ComputeManager(manager.Manager):
return self.driver.get_console_output(instance_ref)
@exception.wrap_exception
+ def get_ajax_console(self, context, instance_id):
+ """Return connection information for an ajax console"""
+ context = context.elevated()
+ logging.debug(_("instance %s: getting ajax console"), instance_id)
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ return self.driver.get_ajax_console(instance_ref)
+
@checks_instance_lock
def attach_volume(self, context, instance_id, volume_id, mountpoint):
"""Attach a volume to an instance."""
diff --git a/nova/db/api.py b/nova/db/api.py
index cf84157bc..1f81ef145 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -81,16 +81,21 @@ def service_get(context, service_id):
return IMPL.service_get(context, service_id)
-def service_get_all(context):
- """Get a list of all services on any machine on any topic of any type"""
- return IMPL.service_get_all(context)
+def service_get_all(context, disabled=False):
+ """Get all service."""
+ return IMPL.service_get_all(context, None, disabled)
def service_get_all_by_topic(context, topic):
- """Get all compute services for a given topic."""
+ """Get all services for a given topic."""
return IMPL.service_get_all_by_topic(context, topic)
+def service_get_all_by_host(context, host):
+ """Get all services for a given host."""
+ return IMPL.service_get_all_by_host(context, host)
+
+
def service_get_all_compute_sorted(context):
"""Get all compute services sorted by instance count.
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 4561fa219..2e4f8fc39 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -135,14 +135,14 @@ def service_get(context, service_id, session=None):
@require_admin_context
-def service_get_all(context, session=None):
+def service_get_all(context, session=None, disabled=False):
if not session:
session = get_session()
result = session.query(models.Service).\
- filter_by(deleted=can_read_deleted(context)).\
- all()
-
+ filter_by(deleted=can_read_deleted(context)).\
+ filter_by(disabled=disabled).\
+ all()
return result
@@ -157,6 +157,15 @@ def service_get_all_by_topic(context, topic):
@require_admin_context
+def service_get_all_by_host(context, host):
+ session = get_session()
+ return session.query(models.Service).\
+ filter_by(deleted=False).\
+ filter_by(host=host).\
+ all()
+
+
+@require_admin_context
def _service_get_all_topic_subquery(context, session, topic, subq, label):
sort_value = getattr(subq.c, label)
return session.query(models.Service, func.coalesce(sort_value, 0)).\
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 2a966448c..1dc46fe78 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -149,6 +149,7 @@ class Service(BASE, NovaBase):
topic = Column(String(255))
report_count = Column(Integer, nullable=False, default=0)
disabled = Column(Boolean, default=False)
+ availability_zone = Column(String(255), default='nova')
class Certificate(BASE, NovaBase):
diff --git a/nova/flags.py b/nova/flags.py
index ab3a2b5f8..fdcba6c72 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -234,7 +234,14 @@ DEFINE_string('scheduler_topic', 'scheduler',
'the topic scheduler nodes listen on')
DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on')
DEFINE_string('network_topic', 'network', 'the topic network nodes listen on')
-
+DEFINE_string('ajax_console_proxy_topic', 'ajax_proxy',
+ 'the topic ajax proxy nodes listen on')
+DEFINE_string('ajax_console_proxy_url',
+ 'http://127.0.0.1:8000',
+ 'location of ajax console proxy, \
+ in the form "http://127.0.0.1:8000"')
+DEFINE_string('ajax_console_proxy_port',
+ 8000, 'port that ajax_console_proxy binds')
DEFINE_bool('verbose', False, 'show debug output')
DEFINE_boolean('fake_rabbit', False, 'use a fake rabbit')
DEFINE_bool('fake_network', False,
@@ -248,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('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',
@@ -299,6 +308,5 @@ DEFINE_string('image_service', 'nova.image.s3.S3ImageService',
DEFINE_string('host', socket.gethostname(),
'name of this node')
-# UNUSED
DEFINE_string('node_availability_zone', 'nova',
'availability zone of this node')
diff --git a/nova/scheduler/zone.py b/nova/scheduler/zone.py
new file mode 100644
index 000000000..49786cd32
--- /dev/null
+++ b/nova/scheduler/zone.py
@@ -0,0 +1,56 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2010 Openstack, LLC.
+# Copyright 2010 United States Government as represented by the
+# 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.
+
+"""
+Availability Zone Scheduler implementation
+"""
+
+import random
+
+from nova.scheduler import driver
+from nova import db
+
+
+class ZoneScheduler(driver.Scheduler):
+ """Implements Scheduler as a random node selector."""
+
+ def hosts_up_with_zone(self, context, topic, zone):
+ """Return the list of hosts that have a running service
+ for topic and availability zone (if defined).
+ """
+
+ if zone is None:
+ return self.hosts_up(context, topic)
+
+ services = db.service_get_all_by_topic(context, topic)
+ return [service.host
+ for service in services
+ if self.service_is_up(service)
+ and service.availability_zone == zone]
+
+ def schedule(self, context, topic, *_args, **_kwargs):
+ """Picks a host that is up at random in selected
+ availability zone (if defined).
+ """
+
+ zone = _kwargs.get('availability_zone')
+ hosts = self.hosts_up_with_zone(context, topic, zone)
+ if not hosts:
+ raise driver.NoValidHost(_("No hosts found"))
+ return hosts[int(random.random() * len(hosts))]
diff --git a/nova/service.py b/nova/service.py
index 523c1a8d7..8b2a22ce0 100644
--- a/nova/service.py
+++ b/nova/service.py
@@ -113,11 +113,13 @@ class Service(object):
self.timers.append(periodic)
def _create_service_ref(self, context):
+ zone = FLAGS.node_availability_zone
service_ref = db.service_create(context,
{'host': self.host,
'binary': self.binary,
'topic': self.topic,
- 'report_count': 0})
+ 'report_count': 0,
+ 'availability_zone': zone})
self.service_id = service_ref['id']
def __getattr__(self, key):
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_cloud.py b/nova/tests/test_cloud.py
index b8a15c7b2..fdacb04f6 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -133,10 +133,35 @@ class CloudTestCase(test.TestCase):
db.volume_destroy(self.context, vol1['id'])
db.volume_destroy(self.context, vol2['id'])
+ def test_describe_availability_zones(self):
+ """Makes sure describe_availability_zones works and filters results."""
+ service1 = db.service_create(self.context, {'host': 'host1_zones',
+ 'binary': "nova-compute",
+ 'topic': 'compute',
+ 'report_count': 0,
+ 'availability_zone': "zone1"})
+ service2 = db.service_create(self.context, {'host': 'host2_zones',
+ 'binary': "nova-compute",
+ 'topic': 'compute',
+ 'report_count': 0,
+ 'availability_zone': "zone2"})
+ result = self.cloud.describe_availability_zones(self.context)
+ self.assertEqual(len(result['availabilityZoneInfo']), 3)
+ db.service_destroy(self.context, service1['id'])
+ db.service_destroy(self.context, service2['id'])
+
def test_describe_instances(self):
"""Makes sure describe_instances works and filters results."""
- inst1 = db.instance_create(self.context, {'reservation_id': 'a'})
- inst2 = db.instance_create(self.context, {'reservation_id': 'a'})
+ inst1 = db.instance_create(self.context, {'reservation_id': 'a',
+ 'host': 'host1'})
+ inst2 = db.instance_create(self.context, {'reservation_id': 'a',
+ 'host': 'host2'})
+ comp1 = db.service_create(self.context, {'host': 'host1',
+ 'availability_zone': 'zone1',
+ 'topic': "compute"})
+ comp2 = db.service_create(self.context, {'host': 'host2',
+ 'availability_zone': 'zone2',
+ 'topic': "compute"})
result = self.cloud.describe_instances(self.context)
result = result['reservationSet'][0]
self.assertEqual(len(result['instancesSet']), 2)
@@ -147,8 +172,12 @@ class CloudTestCase(test.TestCase):
self.assertEqual(len(result['instancesSet']), 1)
self.assertEqual(result['instancesSet'][0]['instanceId'],
instance_id)
+ self.assertEqual(result['instancesSet'][0]
+ ['placement']['availabilityZone'], 'zone2')
db.instance_destroy(self.context, inst1['id'])
db.instance_destroy(self.context, inst2['id'])
+ db.service_destroy(self.context, comp1['id'])
+ db.service_destroy(self.context, comp2['id'])
def test_console_output(self):
image_id = FLAGS.default_image
@@ -167,6 +196,19 @@ class CloudTestCase(test.TestCase):
greenthread.sleep(0.3)
rv = self.cloud.terminate_instances(self.context, [instance_id])
+ def test_ajax_console(self):
+ kwargs = {'image_id': image_id}
+ rv = yield self.cloud.run_instances(self.context, **kwargs)
+ instance_id = rv['instancesSet'][0]['instanceId']
+ output = yield self.cloud.get_console_output(context=self.context,
+ instance_id=[instance_id])
+ self.assertEquals(b64decode(output['output']),
+ 'http://fakeajaxconsole.com/?token=FAKETOKEN')
+ # TODO(soren): We need this until we can stop polling in the rpc code
+ # for unit tests.
+ greenthread.sleep(0.3)
+ rv = yield self.cloud.terminate_instances(self.context, [instance_id])
+
def test_key_generation(self):
result = self._create_key('test')
private_key = result['private_key']
@@ -228,6 +270,19 @@ class CloudTestCase(test.TestCase):
LOG.debug(_("Terminating instance %s"), instance_id)
rv = self.compute.terminate_instance(instance_id)
+ def test_describe_instances(self):
+ """Makes sure describe_instances works."""
+ instance1 = db.instance_create(self.context, {'host': 'host2'})
+ comp1 = db.service_create(self.context, {'host': 'host2',
+ 'availability_zone': 'zone1',
+ 'topic': "compute"})
+ result = self.cloud.describe_instances(self.context)
+ self.assertEqual(result['reservationSet'][0]
+ ['instancesSet'][0]
+ ['placement']['availabilityZone'], 'zone1')
+ db.instance_destroy(self.context, instance1['id'])
+ db.service_destroy(self.context, comp1['id'])
+
def test_instance_update_state(self):
def instance(num):
return {
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 7a384f1da..a7d47961c 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -176,6 +176,16 @@ class ComputeTestCase(test.TestCase):
self.assert_(console)
self.compute.terminate_instance(self.context, instance_id)
+ def test_ajax_console(self):
+ """Make sure we can get console output from instance"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+
+ console = self.compute.get_ajax_console(self.context,
+ instance_id)
+ self.assert_(console)
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_run_instance_existing(self):
"""Ensure failure when running an instance that already exists"""
instance_id = self._create_instance()
diff --git a/nova/tests/test_scheduler.py b/nova/tests/test_scheduler.py
index a9937d797..9d458244b 100644
--- a/nova/tests/test_scheduler.py
+++ b/nova/tests/test_scheduler.py
@@ -21,6 +21,7 @@ Tests For Scheduler
import datetime
+from mox import IgnoreArg
from nova import context
from nova import db
from nova import flags
@@ -76,6 +77,59 @@ class SchedulerTestCase(test.TestCase):
scheduler.named_method(ctxt, 'topic', num=7)
+class ZoneSchedulerTestCase(test.TestCase):
+ """Test case for zone scheduler"""
+ def setUp(self):
+ super(ZoneSchedulerTestCase, self).setUp()
+ self.flags(scheduler_driver='nova.scheduler.zone.ZoneScheduler')
+
+ def _create_service_model(self, **kwargs):
+ service = db.sqlalchemy.models.Service()
+ service.host = kwargs['host']
+ service.disabled = False
+ service.deleted = False
+ service.report_count = 0
+ service.binary = 'nova-compute'
+ service.topic = 'compute'
+ service.id = kwargs['id']
+ service.availability_zone = kwargs['zone']
+ service.created_at = datetime.datetime.utcnow()
+ return service
+
+ def test_with_two_zones(self):
+ scheduler = manager.SchedulerManager()
+ ctxt = context.get_admin_context()
+ service_list = [self._create_service_model(id=1,
+ host='host1',
+ zone='zone1'),
+ self._create_service_model(id=2,
+ host='host2',
+ zone='zone2'),
+ self._create_service_model(id=3,
+ host='host3',
+ zone='zone2'),
+ self._create_service_model(id=4,
+ host='host4',
+ zone='zone2'),
+ self._create_service_model(id=5,
+ host='host5',
+ zone='zone2')]
+ self.mox.StubOutWithMock(db, 'service_get_all_by_topic')
+ arg = IgnoreArg()
+ db.service_get_all_by_topic(arg, arg).AndReturn(service_list)
+ self.mox.StubOutWithMock(rpc, 'cast', use_mock_anything=True)
+ rpc.cast(ctxt,
+ 'compute.host1',
+ {'method': 'run_instance',
+ 'args': {'instance_id': 'i-ffffffff',
+ 'availability_zone': 'zone1'}})
+ self.mox.ReplayAll()
+ scheduler.run_instance(ctxt,
+ 'compute',
+ instance_id='i-ffffffff',
+ availability_zone='zone1')
+
+
class SimpleDriverTestCase(test.TestCase):
"""Test case for simple driver"""
def setUp(self):
diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py
index 9f1a181a0..a67c8d1e8 100644
--- a/nova/tests/test_service.py
+++ b/nova/tests/test_service.py
@@ -133,7 +133,8 @@ class ServiceTestCase(test.TestCase):
service_create = {'host': host,
'binary': binary,
'topic': topic,
- 'report_count': 0}
+ 'report_count': 0,
+ 'availability_zone': 'nova'}
service_ref = {'host': host,
'binary': binary,
'report_count': 0,
@@ -161,11 +162,13 @@ class ServiceTestCase(test.TestCase):
service_create = {'host': host,
'binary': binary,
'topic': topic,
- 'report_count': 0}
+ 'report_count': 0,
+ 'availability_zone': 'nova'}
service_ref = {'host': host,
'binary': binary,
'topic': topic,
'report_count': 0,
+ 'availability_zone': 'nova',
'id': 1}
service.db.service_get_by_args(mox.IgnoreArg(),
@@ -193,11 +196,13 @@ class ServiceTestCase(test.TestCase):
service_create = {'host': host,
'binary': binary,
'topic': topic,
- 'report_count': 0}
+ 'report_count': 0,
+ 'availability_zone': 'nova'}
service_ref = {'host': host,
'binary': binary,
'topic': topic,
'report_count': 0,
+ 'availability_zone': 'nova',
'id': 1}
service.db.service_get_by_args(mox.IgnoreArg(),
@@ -224,11 +229,13 @@ class ServiceTestCase(test.TestCase):
service_create = {'host': host,
'binary': binary,
'topic': topic,
- 'report_count': 0}
+ 'report_count': 0,
+ 'availability_zone': 'nova'}
service_ref = {'host': host,
'binary': binary,
'topic': topic,
'report_count': 0,
+ 'availability_zone': 'nova',
'id': 1}
service.db.service_get_by_args(mox.IgnoreArg(),
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py
index 2ef9ee9c1..afdc89ba2 100644
--- a/nova/tests/test_virt.py
+++ b/nova/tests/test_virt.py
@@ -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/utils.py b/nova/utils.py
index aadbec532..45adb7b38 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -153,6 +153,11 @@ def abspath(s):
return os.path.join(os.path.dirname(__file__), s)
+def novadir():
+ import nova
+ return os.path.abspath(nova.__file__).split('nova/__init__.pyc')[0]
+
+
def default_flagfile(filename='nova.conf'):
for arg in sys.argv:
if arg.find('flagfile') != -1:
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 038857e81..a57a8f43b 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -302,6 +302,9 @@ class FakeConnection(object):
def get_console_output(self, instance):
return 'FAKE CONSOLE OUTPUT'
+ 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',
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index 3fb2243da..2eb7d9488 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -71,9 +71,22 @@
#end if
</filterref>
</interface>
+
+ <!-- The order is significant here. File must be defined first -->
<serial type="file">
<source path='${basepath}/console.log'/>
<target port='1'/>
</serial>
+
+ <console type='pty' tty='/dev/pts/2'>
+ <source path='/dev/pts/2'/>
+ <target port='0'/>
+ </console>
+
+ <serial type='pty'>
+ <source path='/dev/pts/2'/>
+ <target port='0'/>
+ </serial>
+
</devices>
</domain>
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 19f79e19f..655c55fa1 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -38,6 +38,11 @@ Supports KVM, QEMU, UML, and XEN.
import os
import shutil
+import random
+import subprocess
+import uuid
+from xml.dom import minidom
+
from eventlet import greenthread
from eventlet import event
@@ -86,6 +91,9 @@ flags.DEFINE_string('libvirt_uri',
flags.DEFINE_bool('allow_project_net_traffic',
True,
'Whether to allow in project network traffic')
+flags.DEFINE_string('ajaxterm_portrange',
+ '10000-12000',
+ 'Range of ports that ajaxterm should randomly try to bind')
flags.DEFINE_string('firewall_driver',
'nova.virt.libvirt_conn.IptablesFirewallDriver',
'Firewall driver (defaults to iptables)')
@@ -433,6 +441,43 @@ class LibvirtConnection(object):
return self._dump_file(fpath)
+ @exception.wrap_exception
+ def get_ajax_console(self, instance):
+ def get_open_port():
+ start_port, end_port = FLAGS.ajaxterm_portrange.split("-")
+ for i in xrange(0, 100): # don't loop forever
+ port = random.randint(int(start_port), int(end_port))
+ # netcat will exit with 0 only if the port is in use,
+ # so a nonzero return value implies it is unused
+ cmd = 'netcat 0.0.0.0 %s -w 1 </dev/null || echo free' % (port)
+ stdout, stderr = utils.execute(cmd)
+ if stdout.strip() == 'free':
+ return port
+ raise Exception(_('Unable to find an open port'))
+
+ def get_pty_for_instance(instance_name):
+ virt_dom = self._conn.lookupByName(instance_name)
+ xml = virt_dom.XMLDesc(0)
+ dom = minidom.parseString(xml)
+
+ for serial in dom.getElementsByTagName('serial'):
+ if serial.getAttribute('type') == 'pty':
+ source = serial.getElementsByTagName('source')[0]
+ return source.getAttribute('path')
+
+ port = get_open_port()
+ token = str(uuid.uuid4())
+ host = instance['host']
+
+ ajaxterm_cmd = 'sudo socat - %s' \
+ % get_pty_for_instance(instance['name'])
+
+ cmd = '%s/tools/ajaxterm/ajaxterm.py --command "%s" -t %s -p %s' \
+ % (utils.novadir(), ajaxterm_cmd, token, port)
+
+ subprocess.Popen(cmd, shell=True)
+ return {'token': token, 'host': host, 'port': port}
+
def _create_image(self, inst, libvirt_xml, prefix='', disk_images=None):
# syntactic nicety
basepath = lambda fname = '', prefix = prefix: os.path.join(
@@ -1113,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/vmops.py b/nova/virt/xenapi/vmops.py
index c10943aa1..99583bee1 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -64,13 +64,13 @@ class VMOps(object):
"""Create VM instance"""
vm = VMHelper.lookup(self._session, instance.name)
if vm is not None:
- msg = _('Attempted to create non-unique name %s') % instance.name
- raise exception.Duplicate(msg)
+ raise exception.Duplicate(_('Attempted to create'
+ ' non-unique name %s') % instance.name)
bridge = db.network_get_by_instance(context.get_admin_context(),
instance['id'])['bridge']
- network_ref = NetworkHelper.find_network_with_bridge(self._session,
- bridge)
+ network_ref = \
+ NetworkHelper.find_network_with_bridge(self._session, bridge)
user = AuthManager().get_user(instance.user_id)
project = AuthManager().get_project(instance.project_id)
@@ -145,8 +145,15 @@ class VMOps(object):
# Not a string; must be an ID or a vm instance
if isinstance(instance_or_vm, (int, long)):
ctx = context.get_admin_context()
- instance_obj = db.instance_get_by_id(ctx, instance_or_vm)
- instance_name = instance_obj.name
+ try:
+ instance_obj = db.instance_get_by_id(ctx, instance_or_vm)
+ instance_name = instance_obj.name
+ except exception.NotFound:
+ # The unit tests screw this up, as they use an integer for
+ # the vm name. I'd fix that up, but that's a matter for
+ # another bug report. So for now, just try with the passed
+ # value
+ instance_name = instance_or_vm
else:
instance_name = instance_or_vm.name
vm = VMHelper.lookup(self._session, instance_name)
@@ -328,6 +335,11 @@ class VMOps(object):
# TODO: implement this to fix pylint!
return 'FAKE CONSOLE OUTPUT of instance'
+ def get_ajax_console(self, instance):
+ """Return link to instance's ajax console"""
+ # TODO: implement this!
+ return 'http://fakeajaxconsole/fake_url'
+
def list_from_xenstore(self, vm, path):
"""Runs the xenstore-ls command to get a listing of all records
from 'path' downward. Returns a dict with the sub-paths as keys,
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 8189e40db..689844f34 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -185,6 +185,10 @@ class XenAPIConnection(object):
"""Return snapshot of console"""
return self._vmops.get_console_output(instance)
+ def get_ajax_console(self, instance):
+ """Return link to instance's ajax console"""
+ return self._vmops.get_ajax_console(instance)
+
def attach_volume(self, instance_name, device_path, mountpoint):
"""Attach volume storage to VM instance"""
return self._volumeops.attach_volume(instance_name,