add in xs-console worker and tests.
This commit is contained in:
parent
675ca7c5f3
commit
b437a98738
1
Authors
1
Authors
@ -22,6 +22,7 @@ Joshua McKenty <jmckenty@gmail.com>
|
||||
Justin Santa Barbara <justin@fathomdb.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>
|
||||
|
@ -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)
|
||||
|
44
bin/nova-console
Executable file
44
bin/nova-console
Executable file
@ -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()
|
@ -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."""
|
||||
|
11
nova/console/__init__.py
Normal file
11
nova/console/__init__.py
Normal file
@ -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 <mdragon@rackspace.com>
|
||||
"""
|
59
nova/console/driver.py
Normal file
59
nova/console/driver.py
Normal file
@ -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
|
||||
|
59
nova/console/fake.py
Normal file
59
nova/console/fake.py
Normal file
@ -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
|
||||
|
130
nova/console/manager.py
Normal file
130
nova/console/manager.py
Normal file
@ -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
|
||||
|
||||
|
16
nova/console/xvp.conf.template
Normal file
16
nova/console/xvp.conf.template
Normal file
@ -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
|
193
nova/console/xvp.py
Normal file
193
nova/console/xvp.py
Normal file
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
134
nova/tests/test_console.py
Normal file
134
nova/tests/test_console.py
Normal file
@ -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)
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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"""
|
||||
|
Loading…
Reference in New Issue
Block a user