summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEd Leafe <ed@leafe.com>2011-01-11 12:19:29 -0600
committerEd Leafe <ed@leafe.com>2011-01-11 12:19:29 -0600
commit5b5fa0eb4b9ffd597b6e49b2cef5a2ad9028d55f (patch)
tree9599772ed7a24b09ddd3c04e7909e3e6bc5f4b74
parentd91a06b4fea7e45fd2e9abe35803cd9deb5d8e92 (diff)
parentb8de5221368c4055fc593c6d0d7164f2be956924 (diff)
downloadnova-5b5fa0eb4b9ffd597b6e49b2cef5a2ad9028d55f.tar.gz
nova-5b5fa0eb4b9ffd597b6e49b2cef5a2ad9028d55f.tar.xz
nova-5b5fa0eb4b9ffd597b6e49b2cef5a2ad9028d55f.zip
merged trunk changes
-rw-r--r--Authors1
-rwxr-xr-xbin/nova-console44
-rwxr-xr-xbin/nova-manage5
-rw-r--r--nova/api/openstack/__init__.py6
-rw-r--r--nova/api/openstack/consoles.py96
-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.py30
-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/test_console.py129
-rw-r--r--nova/tests/test_virt.py2
-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.py8
-rw-r--r--nova/virt/xenapi_conn.py7
-rw-r--r--smoketests/admin_smoketests.py9
-rw-r--r--smoketests/user_smoketests.py87
28 files changed, 1109 insertions, 30 deletions
diff --git a/Authors b/Authors
index 47101e272..a482c34f0 100644
--- a/Authors
+++ b/Authors
@@ -26,6 +26,7 @@ Justin Santa Barbara <justin@fathomdb.com>
Ken Pepple <ken.pepple@gmail.com>
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/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index ad203c51f..7b999c87b 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -31,6 +31,7 @@ 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
@@ -100,6 +101,11 @@ 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(),
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/compute/manager.py b/nova/compute/manager.py
index 3d22ee432..9872cc65b 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -37,6 +37,8 @@ terminating it.
import datetime
import random
import string
+import logging
+import socket
import functools
from nova import exception
@@ -56,6 +58,9 @@ flags.DEFINE_string('stub_network', False,
'Stub network related code')
flags.DEFINE_integer('password_length', 12,
'Length of generated admin passwords')
+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')
@@ -126,6 +131,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
@@ -140,6 +154,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 f5c2d4233..ab3a2b5f8 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')
@@ -234,8 +248,8 @@ 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('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')
@@ -269,6 +283,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/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..2ef9ee9c1 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):
diff --git a/nova/utils.py b/nova/utils.py
index cc632b835..aadbec532 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -195,19 +195,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 2d4b0a3d7..038857e81 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -302,6 +302,11 @@ class FakeConnection(object):
def get_console_output(self, instance):
return 'FAKE CONSOLE OUTPUT'
+ 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 3a4b6d469..19f79e19f 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -707,6 +707,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)
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index bee7a5110..8189e40db 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"""
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))