Implementation of xs-console blueprint (adds support for console proxies like xvp)
If you spin up the nova-console service, you should be able to see the xvp.conf being edited, and the xvp daemon started/stopped if you exercise the openstack console api (consoles sub-resource on servers)
This commit is contained in:
1
Authors
1
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>
|
||||
|
||||
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()
|
||||
@@ -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(),
|
||||
|
||||
96
nova/api/openstack/consoles.py
Normal file
96
nova/api/openstack/consoles.py
Normal file
@@ -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()
|
||||
@@ -35,6 +35,8 @@ terminating it.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import socket
|
||||
import functools
|
||||
|
||||
from nova import exception
|
||||
@@ -52,6 +54,9 @@ flags.DEFINE_string('compute_driver', 'nova.virt.connection.get_connection',
|
||||
'Driver to use for controlling virtualization')
|
||||
flags.DEFINE_string('stub_network', False,
|
||||
'Stub network related code')
|
||||
flags.DEFINE_string('console_host', socket.gethostname(),
|
||||
'Console proxy host to use to connect to instances on'
|
||||
'this host.')
|
||||
|
||||
LOG = logging.getLogger('nova.compute.manager')
|
||||
|
||||
@@ -122,6 +127,15 @@ class ComputeManager(manager.Manager):
|
||||
state = power_state.NOSTATE
|
||||
self.db.instance_set_state(context, instance_id, state)
|
||||
|
||||
def get_console_topic(self, context, **_kwargs):
|
||||
"""Retrieves the console host for a project on this host
|
||||
Currently this is just set in the flags for each compute
|
||||
host."""
|
||||
#TODO(mdragon): perhaps make this variable by console_type?
|
||||
return self.db.queue_get_for(context,
|
||||
FLAGS.console_topic,
|
||||
FLAGS.console_host)
|
||||
|
||||
def get_network_topic(self, context, **_kwargs):
|
||||
"""Retrieves the network host for a project on this host"""
|
||||
# TODO(vish): This method should be memoized. This will make
|
||||
@@ -136,6 +150,9 @@ class ComputeManager(manager.Manager):
|
||||
FLAGS.network_topic,
|
||||
host)
|
||||
|
||||
def get_console_pool_info(self, context, console_type):
|
||||
return self.driver.get_console_pool_info(console_type)
|
||||
|
||||
@exception.wrap_exception
|
||||
def refresh_security_group_rules(self, context,
|
||||
security_group_id, **_kwargs):
|
||||
|
||||
13
nova/console/__init__.py
Normal file
13
nova/console/__init__.py
Normal file
@@ -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
|
||||
75
nova/console/api.py
Normal file
75
nova/console/api.py
Normal file
@@ -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}})
|
||||
58
nova/console/fake.py
Normal file
58
nova/console/fake.py
Normal file
@@ -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
|
||||
127
nova/console/manager.py
Normal file
127
nova/console/manager.py
Normal file
@@ -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
|
||||
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
|
||||
194
nova/console/xvp.py
Normal file
194
nova/console/xvp.py
Normal file
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -228,6 +228,8 @@ DEFINE_integer('s3_port', 3333, 's3 port')
|
||||
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')
|
||||
@@ -281,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',
|
||||
|
||||
129
nova/tests/test_console.py
Normal file
129
nova/tests/test_console.py
Normal file
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -289,6 +289,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):
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ REQ_POWER_STATE = {
|
||||
'Reboot': 10,
|
||||
'Reset': 11,
|
||||
'Paused': 32768,
|
||||
'Suspended': 32769
|
||||
'Suspended': 32769,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -190,6 +191,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"""
|
||||
|
||||
Reference in New Issue
Block a user