summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Authors2
-rwxr-xr-xbin/nova-console44
-rwxr-xr-xbin/nova-manage5
-rw-r--r--krm_mapping.json.sample3
-rw-r--r--nova/api/ec2/cloud.py7
-rw-r--r--nova/api/openstack/__init__.py17
-rw-r--r--nova/api/openstack/backup_schedules.py6
-rw-r--r--nova/api/openstack/common.py24
-rw-r--r--nova/api/openstack/consoles.py96
-rw-r--r--nova/api/openstack/images.py19
-rw-r--r--nova/api/openstack/servers.py29
-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.py5
-rw-r--r--nova/compute/manager.py17
-rw-r--r--nova/console/__init__.py13
-rw-r--r--nova/console/api.py75
-rw-r--r--nova/console/fake.py58
-rw-r--r--nova/console/manager.py127
-rw-r--r--nova/console/xvp.conf.template16
-rw-r--r--nova/console/xvp.py194
-rw-r--r--nova/db/api.py54
-rw-r--r--nova/db/sqlalchemy/api.py108
-rw-r--r--nova/db/sqlalchemy/models.py27
-rw-r--r--nova/flags.py32
-rw-r--r--nova/network/linux_net.py2
-rw-r--r--nova/network/manager.py2
-rw-r--r--nova/tests/api/openstack/fakes.py2
-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_console.py129
-rw-r--r--nova/tests/test_virt.py46
-rw-r--r--nova/tests/xenapi/stubs.py24
-rw-r--r--nova/utils.py13
-rw-r--r--nova/virt/fake.py5
-rw-r--r--nova/virt/hyperv.py2
-rw-r--r--nova/virt/libvirt_conn.py14
-rw-r--r--nova/virt/xenapi/vm_utils.py4
-rw-r--r--nova/virt/xenapi_conn.py23
-rw-r--r--smoketests/admin_smoketests.py9
-rw-r--r--smoketests/user_smoketests.py87
-rw-r--r--tools/install_venv.py3
45 files changed, 1377 insertions, 99 deletions
diff --git a/Authors b/Authors
index 47101e272..6e6ecfe1b 100644
--- a/Authors
+++ b/Authors
@@ -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.'