Add support for serial console proxy access

Support access to instances via optionally enabled serial console
feature provided in Nova.

Seria console access is enabled using a new config flag; this flag
plus the required base_url for the nova-serialproxy are also passed
over the cloud-compute relation for use in nova-compute units.

This is only supported in OpenStack Juno or later, and replaces
the standard output to the nova console-log.

Change-Id: I3bfcca88bd6147be337e6d770db7348170b914e6
This commit is contained in:
James Page 2016-09-15 11:31:51 +01:00
parent 71e74cd0b0
commit 469ef8dced
11 changed files with 158 additions and 18 deletions

View File

@ -326,6 +326,13 @@ options:
type: string
default:
description: SSL key to use with certificate specified as console-ssl-cert.
enable-serial-console:
type: boolean
default: false
description: |
Enable serial console access to instances using websockets (insecure).
This is only supported on OpenStack Juno or later, and will disable the
normal console-log output for an instance.
worker-multiplier:
type: float
default: 2.0

View File

@ -357,6 +357,21 @@ class ConsoleSSLContext(context.OSContextGenerator):
return ctxt
class SerialConsoleContext(context.OSContextGenerator):
interfaces = []
def __call__(self):
ip_addr = resolve_address(endpoint_type=PUBLIC)
ip_addr = format_ipv6_addr(ip_addr) or ip_addr
ctxt = {
'enable_serial_console':
str(config('enable-serial-console')).lower(),
'serial_console_base_url': 'ws://{}:6083/'.format(ip_addr)
}
return ctxt
class APIRateLimitingContext(context.OSContextGenerator):
def __call__(self):
ctxt = {}

View File

@ -112,6 +112,7 @@ from nova_cc_utils import (
is_db_initialised,
assess_status,
update_aws_compat_services,
serial_console_settings,
)
from charmhelpers.contrib.hahelpers.cluster import (
@ -263,23 +264,21 @@ def config_changed():
save_script_rc()
configure_https()
CONFIGS.write_all()
if console_attributes('protocol'):
if not git_install_requested():
status_set('maintenance', 'Configuring guest console access')
apt_update()
packages = console_attributes('packages') or []
filtered = filter_installed_packages(packages)
if filtered:
apt_install(filtered, fatal=True)
[compute_joined(rid=rid)
for rid in relation_ids('cloud-compute')]
# NOTE(jamespage): deal with any changes to the console and serial
# console configuration options
if not git_install_requested():
filtered = filter_installed_packages(determine_packages())
if filtered:
apt_install(filtered, fatal=True)
for r_id in relation_ids('identity-service'):
identity_joined(rid=r_id)
for rid in relation_ids('zeromq-configuration'):
zeromq_configuration_relation_joined(rid)
[cluster_joined(rid) for rid in relation_ids('cluster')]
[compute_joined(rid=rid) for rid in relation_ids('cloud-compute')]
update_nrpe_config()
# If the region value has changed, notify the cloud-compute relations
@ -590,8 +589,9 @@ def compute_joined(rid=None, remote_restart=False):
# (comment from bash vers) XXX Should point to VIP if clustered, or
# this may not even be needed.
'ec2_host': unit_get('private-address'),
'region': config('region')
'region': config('region'),
}
rel_settings.update(serial_console_settings())
# update relation setting if we're attempting to restart remote
# services
if remote_restart:

View File

@ -250,7 +250,8 @@ BASE_RESOURCE_MAP = OrderedDict([
nova_cc_context.ConsoleSSLContext(),
nova_cc_context.CloudComputeContext(),
context.InternalEndpointContext(),
nova_cc_context.NeutronAPIContext()],
nova_cc_context.NeutronAPIContext(),
nova_cc_context.SerialConsoleContext()],
}),
(NOVA_API_PASTE, {
'services': [s for s in resolve_services() if 'api' in s],
@ -297,6 +298,12 @@ CONSOLE_CONFIG = {
},
}
SERIAL_CONSOLE = {
'packages': ['nova-serialproxy', 'nova-consoleauth',
'websockify'],
'services': ['nova-serialproxy', 'nova-consoleauth'],
}
def resource_map():
'''
@ -324,6 +331,11 @@ def resource_map():
resource_map[NOVA_CONF]['services'] += \
console_attributes('services')
if (config('enable-serial-console') and
os_release('nova-common') >= 'juno'):
resource_map[NOVA_CONF]['services'] += \
SERIAL_CONSOLE['services']
# also manage any configs that are being updated by subordinates.
vmware_ctxt = context.SubordinateConfigContext(interface='nova-vmware',
service='nova',
@ -399,11 +411,14 @@ def console_attributes(attr, proto=None):
def determine_packages():
# currently all packages match service names
packages = [] + BASE_PACKAGES
packages = deepcopy(BASE_PACKAGES)
for v in resource_map().values():
packages.extend(v['services'])
if console_attributes('packages'):
packages.extend(console_attributes('packages'))
if (config('enable-serial-console') and
os_release('nova-common') >= 'juno'):
packages.extend(SERIAL_CONSOLE['packages'])
if git_install_requested():
packages = list(set(packages))
@ -1370,3 +1385,10 @@ def update_aws_compat_services():
else:
for service_ in AWS_COMPAT_SERVICES:
service_resume(service_)
def serial_console_settings():
'''Utility wrapper to retrieve serial console settings
for use in cloud-compute relation
'''
return nova_cc_context.SerialConsoleContext()()

View File

@ -169,3 +169,5 @@ lock_path=/var/lock/nova
[spice]
{% include "parts/spice" %}
{% include "parts/section-serial-console" %}

View File

@ -167,3 +167,5 @@ lock_path=/var/lock/nova
[spice]
{% include "parts/spice" %}
{% include "parts/section-serial-console" %}

View File

@ -161,3 +161,5 @@ lock_path=/var/lock/nova
[spice]
{% include "parts/spice" %}
{% include "parts/section-serial-console" %}

View File

@ -0,0 +1,3 @@
[serial_console]
enabled = {{ enable_serial_console }}
base_url = {{ serial_console_base_url }}

View File

@ -311,3 +311,38 @@ class NovaComputeContextTests(CharmTestCase):
self.config('cpu-allocation-ratio'))
self.assertEqual(ctxt['ram_allocation_ratio'],
self.config('ram-allocation-ratio'))
@mock.patch.object(context, 'format_ipv6_addr')
@mock.patch.object(context, 'resolve_address')
@mock.patch.object(context, 'config')
def test_serial_console_context(self, mock_config,
mock_resolve_address,
mock_format_ipv6_address):
mock_config.side_effect = self.test_config.get
mock_format_ipv6_address.return_value = None
mock_resolve_address.return_value = '10.10.10.1'
ctxt = context.SerialConsoleContext()()
self.assertEqual(
ctxt,
{'serial_console_base_url': 'ws://10.10.10.1:6083/',
'enable_serial_console': 'false'}
)
mock_resolve_address.assert_called_with(endpoint_type=context.PUBLIC)
@mock.patch.object(context, 'format_ipv6_addr')
@mock.patch.object(context, 'resolve_address')
@mock.patch.object(context, 'config')
def test_serial_console_context_enabled(self, mock_config,
mock_resolve_address,
mock_format_ipv6_address):
mock_config.side_effect = self.test_config.get
self.test_config.set('enable-serial-console', True)
mock_format_ipv6_address.return_value = None
mock_resolve_address.return_value = '10.10.10.1'
ctxt = context.SerialConsoleContext()()
self.assertEqual(
ctxt,
{'serial_console_base_url': 'ws://10.10.10.1:6083/',
'enable_serial_console': 'true'}
)
mock_resolve_address.assert_called_with(endpoint_type=context.PUBLIC)

View File

@ -91,6 +91,7 @@ TO_PATCH = [
'status_set',
'network_get_primary_address',
'update_dns_ha_resource_params',
'serial_console_settings',
]
@ -164,12 +165,15 @@ class NovaCCHooksTests(CharmTestCase):
self.assertTrue(self.disable_services.called)
self.cmd_all_services.assert_called_with('stop')
@patch.object(hooks, 'determine_packages')
@patch.object(utils, 'service_resume')
@patch.object(utils, 'config')
@patch.object(hooks, 'filter_installed_packages')
@patch.object(hooks, 'configure_https')
def test_config_changed_no_upgrade(self, conf_https, mock_filter_packages,
utils_config, mock_service_resume):
utils_config, mock_service_resume,
mock_determine_packages):
mock_determine_packages.return_value = []
utils_config.side_effect = self.test_config.get
self.test_config.set('console-access-protocol', 'dummy')
self.git_install_requested.return_value = False
@ -202,6 +206,7 @@ class NovaCCHooksTests(CharmTestCase):
self.git_install.assert_called_with(projects_yaml)
self.assertFalse(self.do_openstack_upgrade.called)
@patch.object(hooks, 'determine_packages')
@patch.object(utils, 'service_resume')
@patch('charmhelpers.contrib.openstack.ip.unit_get')
@patch('charmhelpers.contrib.hahelpers.cluster.relation_ids')
@ -219,7 +224,9 @@ class NovaCCHooksTests(CharmTestCase):
mock_filter_packages, db_joined,
utils_config, mock_relids,
mock_unit_get,
mock_service_resume):
mock_service_resume,
mock_determine_packages):
mock_determine_packages.return_value = []
self.git_install_requested.return_value = False
self.openstack_upgrade_available.return_value = True
self.relation_ids.return_value = ['generic_rid']
@ -331,6 +338,10 @@ class NovaCCHooksTests(CharmTestCase):
self.is_elected_leader = True
self.keystone_ca_cert_b64.return_value = 'foocert64'
self.unit_get.return_value = 'nova-cc-host1'
self.serial_console_settings.return_value = {
'enable_serial_console': 'false',
'serial_console_base_url': 'ws://controller:6803',
}
_canonical_url.return_value = 'http://nova-cc-host1'
auth_config.return_value = FAKE_KS_AUTH_CFG
hooks.compute_joined()
@ -341,7 +352,10 @@ class NovaCCHooksTests(CharmTestCase):
region='RegionOne',
volume_service='cinder',
ec2_host='nova-cc-host1',
network_manager='neutron', **FAKE_KS_AUTH_CFG)
network_manager='neutron',
enable_serial_console='false',
serial_console_base_url='ws://controller:6803',
**FAKE_KS_AUTH_CFG)
@patch.object(hooks, 'canonical_url')
@patch.object(utils, 'config')
@ -362,6 +376,10 @@ class NovaCCHooksTests(CharmTestCase):
self.is_elected_leader = True
self.keystone_ca_cert_b64.return_value = 'foocert64'
self.unit_get.return_value = 'nova-cc-host1'
self.serial_console_settings.return_value = {
'enable_serial_console': 'false',
'serial_console_base_url': 'ws://controller:6803',
}
_canonical_url.return_value = 'http://nova-cc-host1'
auth_config.return_value = FAKE_KS_AUTH_CFG
hooks.compute_joined()
@ -376,7 +394,10 @@ class NovaCCHooksTests(CharmTestCase):
volume_service='cinder',
ec2_host='nova-cc-host1',
quantum_plugin='bob',
network_manager='neutron', **FAKE_KS_AUTH_CFG)
network_manager='neutron',
enable_serial_console='false',
serial_console_base_url='ws://controller:6803',
**FAKE_KS_AUTH_CFG)
@patch.object(hooks, 'canonical_url')
@patch.object(hooks, '_auth_config')
@ -961,6 +982,7 @@ class NovaCCHooksTests(CharmTestCase):
call(**args),
])
@patch.object(hooks, 'determine_packages')
@patch.object(utils, 'service_pause')
@patch.object(hooks, 'filter_installed_packages')
@patch('nova_cc_hooks.configure_https')
@ -968,7 +990,9 @@ class NovaCCHooksTests(CharmTestCase):
def test_config_changed_single_consoleauth(self, mock_config,
mock_configure_https,
mock_filter_packages,
mock_service_pause):
mock_service_pause,
mock_determine_packages):
mock_determine_packages.return_value = []
self.config_value_changed.return_value = False
self.git_install_requested.return_value = False
config.return_value = 'novnc'

View File

@ -292,6 +292,34 @@ class NovaCCUtilsTests(CharmTestCase):
ex = list(set(utils.BASE_PACKAGES + utils.BASE_SERVICES))
self.assertEquals(ex, pkgs)
@patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext')
@patch.object(utils, 'git_install_requested')
def test_determine_packages_serial_console(self,
git_requested,
subcontext):
git_requested.return_value = False
self.test_config.set('enable-serial-console', True)
self.relation_ids.return_value = []
self.os_release.return_value = 'juno'
pkgs = utils.determine_packages()
console_pkgs = ['nova-serialproxy', 'nova-consoleauth']
for console_pkg in console_pkgs:
self.assertIn(console_pkg, pkgs)
@patch('charmhelpers.contrib.openstack.context.SubordinateConfigContext')
@patch.object(utils, 'git_install_requested')
def test_determine_packages_serial_console_icehouse(self,
git_requested,
subcontext):
git_requested.return_value = False
self.test_config.set('enable-serial-console', True)
self.relation_ids.return_value = []
self.os_release.return_value = 'icehouse'
pkgs = utils.determine_packages()
console_pkgs = ['nova-serialproxy', 'nova-consoleauth']
for console_pkg in console_pkgs:
self.assertNotIn(console_pkg, pkgs)
@patch.object(utils, 'restart_map')
def test_determine_ports(self, restart_map):
restart_map.return_value = {