From b437a98738c7a564205d1b27e36b844cd54445d1 Mon Sep 17 00:00:00 2001 From: Monsyne Dragon Date: Wed, 5 Jan 2011 14:16:14 -0600 Subject: [PATCH] add in xs-console worker and tests. --- Authors | 1 + bin/nova-combined | 5 +- bin/nova-console | 44 ++++++++ nova/compute/manager.py | 5 + nova/console/__init__.py | 11 ++ nova/console/driver.py | 59 ++++++++++ nova/console/fake.py | 59 ++++++++++ nova/console/manager.py | 130 ++++++++++++++++++++++ nova/console/xvp.conf.template | 16 +++ nova/console/xvp.py | 193 +++++++++++++++++++++++++++++++++ nova/db/api.py | 41 +++++++ nova/db/sqlalchemy/api.py | 81 ++++++++++++++ nova/db/sqlalchemy/models.py | 23 +++- nova/flags.py | 3 + nova/tests/test_console.py | 134 +++++++++++++++++++++++ nova/virt/fake.py | 5 + nova/virt/libvirt_conn.py | 8 ++ nova/virt/xenapi_conn.py | 7 ++ 18 files changed, 822 insertions(+), 3 deletions(-) create mode 100755 bin/nova-console create mode 100644 nova/console/__init__.py create mode 100644 nova/console/driver.py create mode 100644 nova/console/fake.py create mode 100644 nova/console/manager.py create mode 100644 nova/console/xvp.conf.template create mode 100644 nova/console/xvp.py create mode 100644 nova/tests/test_console.py diff --git a/Authors b/Authors index 963cd6704c56..03dcf149c16c 100644 --- a/Authors +++ b/Authors @@ -22,6 +22,7 @@ Joshua McKenty Justin Santa Barbara Matt Dietz Michael Gundlach +Monsyne Dragon Monty Taylor Paul Voccio Rick Clark diff --git a/bin/nova-combined b/bin/nova-combined index 53322f1a0828..a1d2cdc6650a 100755 --- a/bin/nova-combined +++ b/bin/nova-combined @@ -56,11 +56,12 @@ if __name__ == '__main__': compute = service.Service.create(binary='nova-compute') network = service.Service.create(binary='nova-network') - volume = service.Service.create(binary='nova-volume') +# volume = service.Service.create(binary='nova-volume') scheduler = service.Service.create(binary='nova-scheduler') #objectstore = service.Service.create(binary='nova-objectstore') - service.serve(compute, network, volume, scheduler) +# service.serve(compute, network, volume, scheduler) + service.serve(compute, network, scheduler) server = wsgi.Server() server.start(api.API('os'), FLAGS.osapi_port, host=FLAGS.osapi_host) diff --git a/bin/nova-console b/bin/nova-console new file mode 100755 index 000000000000..802cc80b608e --- /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/nova/compute/manager.py b/nova/compute/manager.py index 70b175e7c682..295e75eca4c8 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -99,6 +99,11 @@ 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(self, context, security_group_id, **_kwargs): """This call passes stright through to the virtualization driver.""" diff --git a/nova/console/__init__.py b/nova/console/__init__.py new file mode 100644 index 000000000000..adce8621e331 --- /dev/null +++ b/nova/console/__init__.py @@ -0,0 +1,11 @@ +# 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 +""" diff --git a/nova/console/driver.py b/nova/console/driver.py new file mode 100644 index 000000000000..b92765b3451a --- /dev/null +++ b/nova/console/driver.py @@ -0,0 +1,59 @@ +# 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. + +""" +ConsoleProxy base class that all ConsoleProxies should inherit from +""" + +from nova import exception + + +class ConsoleProxy(object): + """The base class for all ConsoleProxy driver classes.""" + + @property + def console_type(self): + raise NotImplementedError("Must specify type in subclass") + + def setup_console(self, context, console): + """Sets up actual proxies""" + raise NotImplementedError("Must implement setup in subclass") + + def teardown_console(self, context, console): + """Tears down actual proxies""" + raise NotImplementedError("Must implement teardown in subclass") + + def init_host(self): + """Start up any config'ed consoles on start""" + pass + + def generate_password(self, length=8): + """Returns random console password""" + return os.urandom(length*2).encode('base64')[:length] + + def get_port(self, context): + """get available port for consoles that need one""" + return None + + 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/fake.py b/nova/console/fake.py new file mode 100644 index 000000000000..4a9f1244c94d --- /dev/null +++ b/nova/console/fake.py @@ -0,0 +1,59 @@ +# 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 +from nova.console import driver + +class FakeConsoleProxy(driver.ConsoleProxy): + """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 000000000000..93c6fabce2cf --- /dev/null +++ b/nova/console/manager.py @@ -0,0 +1,130 @@ +# 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 logging +import functools + +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') + +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, instance_id, **_kwargs): + instance = self.db.instance_get(context, instance_id) + host = instance['host'] + 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(_('Tried to remove non-existant console in pool ' + '%(pool_id)s for instance %(instance_id)s.' % + {'instance_id' : instance_id, + 'pool_id' : pool['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['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 000000000000..695ddbe964df --- /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 000000000000..62ad3b2bba31 --- /dev/null +++ b/nova/console/xvp.py @@ -0,0 +1,193 @@ +# 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 +from nova.console import driver + +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(driver.ConsoleProxy): + """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 _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) + #p = subprocess.Popen(['xvp', flag], stdin=subprocess.PIPE, stdout=subprocess.PIPE) + #out,err = p.communicate(password) + return out.strip() + diff --git a/nova/db/api.py b/nova/db/api.py index fde3f0852a87..af9856cb6887 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -884,3 +884,44 @@ 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) + + diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 7e945e4cb3d8..25a3922c7acc 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1863,3 +1863,84 @@ 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 + + diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index 693db8d23d97..e7f2d427e202 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -553,6 +553,27 @@ class FloatingIp(BASE, NovaBase): project_id = Column(String(255)) 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)) + 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. @@ -565,7 +586,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 76a98d35a859..447cc6c6c906 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -216,6 +216,7 @@ DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', utils.get_my_ip(), 's3 host (for infrastructure)') DEFINE_string('s3_dmz', utils.get_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') @@ -263,6 +264,8 @@ DEFINE_string('sql_connection', 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/tests/test_console.py b/nova/tests/test_console.py new file mode 100644 index 000000000000..9f06a17716a8 --- /dev/null +++ b/nova/tests/test_console.py @@ -0,0 +1,134 @@ +# 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() + self.console.add_console(self.context, instance_id) + self.console.remove_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 not in console_instances) + diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 706888b0d197..acabb8034980 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -272,6 +272,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/libvirt_conn.py b/nova/virt/libvirt_conn.py index 65cf6509852f..51353147fb17 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -671,6 +671,14 @@ class LibvirtConnection(object): fw = NWFilterFirewall(self._conn) fw.ensure_security_group_filter(security_group_id) + 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'} + class NWFilterFirewall(object): """ diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 7f03d6c2bb34..abad0a08a2ad 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 logging import sys +import urlparse import xmlrpclib from eventlet import event @@ -177,6 +178,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"""