[xianghu,r=hopem]

Add Console SSL support. This allows SSL to be used for
instance console sessions independently of whether SSL
is used for the Nova API endpoint.

Closes-Bug: 1476455
This commit is contained in:
Edward Hope-Morley 2015-07-24 13:29:29 +01:00
commit 66b838dcdf
11 changed files with 281 additions and 10 deletions

View File

@ -317,6 +317,20 @@ options:
default: 'en-us'
description: |
Console keymap
console-ssl-cert:
type: string
default:
description: |
Used for encrypted console connections. This differs from the SSL
certificate used for API endpoints and is used for console sessions only.
Setting this value along with console-ssl-key will enable encrypted
console sessions. This has nothing to do with Nova API SSL and can be
used independently. This can be used in conjunction when
console-access-protocol is set to 'novnc' or 'spice'.
console-ssl-key:
type: string
default:
description: SSL key to use with certificate specified as console-ssl-cert.
worker-multiplier:
type: int
default: 2

View File

@ -1,14 +1,17 @@
import os
from base64 import b64decode
from charmhelpers.core.hookenv import (
config,
relation_ids,
relation_set,
log,
DEBUG,
ERROR,
related_units,
relations_for_id,
relation_get,
unit_get,
)
from charmhelpers.fetch import (
apt_install,
@ -23,6 +26,7 @@ from charmhelpers.contrib.hahelpers.cluster import (
determine_apache_port,
determine_api_port,
https,
is_clustered,
)
from charmhelpers.contrib.network.ip import (
format_ipv6_addr,
@ -30,6 +34,7 @@ from charmhelpers.contrib.network.ip import (
from charmhelpers.contrib.openstack.ip import (
resolve_address,
INTERNAL,
PUBLIC,
)
@ -350,3 +355,54 @@ class InstanceConsoleContext(context.OSContextGenerator):
ctxt['ssl_key'] = key
return ctxt
class ConsoleSSLContext(context.OSContextGenerator):
interfaces = []
def __call__(self):
ctxt = {}
from nova_cc_utils import console_attributes
if (config('console-ssl-cert') and
config('console-ssl-key') and
config('console-access-protocol')):
ssl_dir = '/etc/nova/ssl/'
if not os.path.exists(ssl_dir):
log('Creating %s.' % ssl_dir, level=DEBUG)
os.mkdir(ssl_dir)
cert_path = os.path.join(ssl_dir, 'nova_cert.pem')
decode_ssl_cert = b64decode(config('console-ssl-cert'))
key_path = os.path.join(ssl_dir, 'nova_key.pem')
decode_ssl_key = b64decode(config('console-ssl-key'))
with open(cert_path, 'w') as fh:
fh.write(decode_ssl_cert)
with open(key_path, 'w') as fh:
fh.write(decode_ssl_key)
ctxt['ssl_only'] = True
ctxt['ssl_cert'] = cert_path
ctxt['ssl_key'] = key_path
if is_clustered():
ip_addr = resolve_address(endpoint_type=PUBLIC)
else:
ip_addr = unit_get('private-address')
ip_addr = format_ipv6_addr(ip_addr) or ip_addr
_proto = config('console-access-protocol')
url = "https://%s:%s%s" % (
ip_addr,
console_attributes('proxy-port', proto=_proto),
console_attributes('proxy-page', proto=_proto))
if _proto == 'novnc':
ctxt['novncproxy_base_url'] = url
elif _proto == 'spice':
ctxt['html5proxy_base_url'] = url
return ctxt

View File

@ -106,21 +106,24 @@ from nova_cc_utils import (
from charmhelpers.contrib.hahelpers.cluster import (
is_elected_leader,
get_hacluster_config,
https,
)
from charmhelpers.payload.execd import execd_preinstall
from charmhelpers.contrib.openstack.ip import (
canonical_url,
PUBLIC, INTERNAL, ADMIN
PUBLIC, INTERNAL, ADMIN,
resolve_address,
)
from charmhelpers.contrib.network.ip import (
format_ipv6_addr,
get_iface_for_address,
get_netmask_for_address,
get_address_in_network,
get_ipv6_addr,
is_ipv6
is_ipv6,
)
from charmhelpers.contrib.openstack.context import ADDRESS_TYPES
@ -511,10 +514,27 @@ def console_settings():
return {}
rel_settings['console_keymap'] = config('console-keymap')
rel_settings['console_access_protocol'] = proto
console_ssl = False
if config('console-ssl-cert') and config('console-ssl-key'):
console_ssl = True
if config('console-proxy-ip') == 'local':
proxy_base_addr = canonical_url(CONFIGS, PUBLIC)
if console_ssl:
address = resolve_address(endpoint_type=PUBLIC)
address = format_ipv6_addr(address) or address
proxy_base_addr = 'https://%s' % address
else:
# canonical_url will only return 'https:' if API SSL are enabled.
proxy_base_addr = canonical_url(CONFIGS, PUBLIC)
else:
proxy_base_addr = "http://" + config('console-proxy-ip')
if console_ssl or https():
schema = "https"
else:
schema = "http"
proxy_base_addr = "%s://%s" % (schema, config('console-proxy-ip'))
if proto == 'vnc':
protocols = ['novnc', 'xvpvnc']
else:

View File

@ -194,7 +194,8 @@ BASE_RESOURCE_MAP = OrderedDict([
nova_cc_context.NovaIPv6Context(),
nova_cc_context.NeutronCCContext(),
nova_cc_context.NovaConfigContext(),
nova_cc_context.InstanceConsoleContext()],
nova_cc_context.InstanceConsoleContext(),
nova_cc_context.ConsoleSSLContext()],
}),
(NOVA_API_PASTE, {
'services': [s for s in BASE_SERVICES if 'api' in s],

View File

@ -165,3 +165,6 @@ enabled=True
[conductor]
workers = {{ workers }}
[spice]
{% include "parts/spice" %}

View File

@ -160,3 +160,6 @@ enabled=True
[conductor]
workers = {{ workers }}
[spice]
{% include "parts/spice" %}

View File

@ -155,3 +155,6 @@ workers = {{ workers }}
[oslo_concurrency]
lock_path=/var/lock/nova
[spice]
{% include "parts/spice" %}

View File

@ -7,3 +7,6 @@ cert={{ ssl_cert }}
{% if ssl_key -%}
key={{ ssl_key }}
{% endif %}
{% if novncproxy_base_url -%}
novncproxy_base_url={{ novncproxy_base_url }}
{%- endif %}

3
templates/parts/spice Normal file
View File

@ -0,0 +1,3 @@
{% if html5proxy_base_url -%}
html5proxy_base_url = {{ html5proxy_base_url }}
{% endif -%}

View File

@ -148,3 +148,151 @@ class NovaComputeContextTests(CharmTestCase):
self.assertTrue(context.use_local_neutron_api())
self.related_units.return_value = ['unit/0']
self.assertFalse(context.use_local_neutron_api())
@mock.patch.object(context, 'config')
def test_console_ssl_disabled(self, mock_config):
config = {'console-ssl-cert': 'LS0tLS1CRUdJTiBDRV',
'console-ssl-key': 'LS0tLS1CRUdJTiBQUk'}
mock_config.side_effect = lambda key: config.get(key)
ctxt = context.ConsoleSSLContext()()
self.assertEqual(ctxt, {})
config = {'console-ssl-cert': None,
'console-ssl-key': None}
mock_config.side_effect = lambda key: config.get(key)
ctxt = context.ConsoleSSLContext()()
self.assertEqual(ctxt, {})
config = {'console-access-protocol': 'novnc',
'console-ssl-cert': None,
'console-ssl-key': None}
mock_config.side_effect = lambda key: config.get(key)
ctxt = context.ConsoleSSLContext()()
self.assertEqual(ctxt, {})
@mock.patch('__builtin__.open')
@mock.patch('os.path.exists')
@mock.patch.object(context, 'config')
@mock.patch.object(context, 'unit_get')
@mock.patch.object(context, 'is_clustered')
@mock.patch.object(context, 'resolve_address')
@mock.patch.object(context, 'b64decode')
def test_noVNC_ssl_enabled(self, mock_b64decode,
mock_resolve_address,
mock_is_clustered, mock_unit_get,
mock_config, mock_exists, mock_open):
config = {'console-ssl-cert': 'LS0tLS1CRUdJTiBDRV',
'console-ssl-key': 'LS0tLS1CRUdJTiBQUk',
'console-access-protocol': 'novnc'}
mock_config.side_effect = lambda key: config.get(key)
mock_exists.return_value = True
mock_unit_get.return_value = '127.0.0.1'
mock_is_clustered.return_value = True
mock_resolve_address.return_value = '10.5.100.1'
mock_b64decode.return_value = 'decode_success'
mock_open.return_value.__enter__ = lambda s: s
mock_open.return_value.__exit__ = mock.Mock()
ctxt = context.ConsoleSSLContext()()
self.assertTrue(ctxt['ssl_only'])
self.assertEqual(ctxt['ssl_cert'], '/etc/nova/ssl/nova_cert.pem')
self.assertEqual(ctxt['ssl_key'], '/etc/nova/ssl/nova_key.pem')
self.assertEqual(ctxt['novncproxy_base_url'],
'https://10.5.100.1:6080/vnc_auto.html')
@mock.patch('__builtin__.open')
@mock.patch('os.path.exists')
@mock.patch.object(context, 'config')
@mock.patch.object(context, 'unit_get')
@mock.patch.object(context, 'is_clustered')
@mock.patch.object(context, 'resolve_address')
@mock.patch.object(context, 'b64decode')
def test_noVNC_ssl_enabled_no_cluster(self, mock_b64decode,
mock_resolve_address,
mock_is_clustered, mock_unit_get,
mock_config, mock_exists, mock_open):
config = {'console-ssl-cert': 'LS0tLS1CRUdJTiBDRV',
'console-ssl-key': 'LS0tLS1CRUdJTiBQUk',
'console-access-protocol': 'novnc'}
mock_config.side_effect = lambda key: config.get(key)
mock_exists.return_value = True
mock_unit_get.return_value = '10.5.0.1'
mock_is_clustered.return_value = False
mock_b64decode.return_value = 'decode_success'
mock_open.return_value.__enter__ = lambda s: s
mock_open.return_value.__exit__ = mock.Mock()
ctxt = context.ConsoleSSLContext()()
self.assertTrue(ctxt['ssl_only'])
self.assertEqual(ctxt['ssl_cert'], '/etc/nova/ssl/nova_cert.pem')
self.assertEqual(ctxt['ssl_key'], '/etc/nova/ssl/nova_key.pem')
self.assertEqual(ctxt['novncproxy_base_url'],
'https://10.5.0.1:6080/vnc_auto.html')
@mock.patch('__builtin__.open')
@mock.patch('os.path.exists')
@mock.patch.object(context, 'config')
@mock.patch.object(context, 'unit_get')
@mock.patch.object(context, 'is_clustered')
@mock.patch.object(context, 'resolve_address')
@mock.patch.object(context, 'b64decode')
def test_spice_html5_ssl_enabled(self, mock_b64decode,
mock_resolve_address,
mock_is_clustered, mock_unit_get,
mock_config, mock_exists, mock_open):
config = {'console-ssl-cert': 'LS0tLS1CRUdJTiBDRV',
'console-ssl-key': 'LS0tLS1CRUdJTiBQUk',
'console-access-protocol': 'spice'}
mock_config.side_effect = lambda key: config.get(key)
mock_exists.return_value = True
mock_unit_get.return_value = '127.0.0.1'
mock_is_clustered.return_value = True
mock_resolve_address.return_value = '10.5.100.1'
mock_b64decode.return_value = 'decode_success'
mock_open.return_value.__enter__ = lambda s: s
mock_open.return_value.__exit__ = mock.Mock()
ctxt = context.ConsoleSSLContext()()
self.assertTrue(ctxt['ssl_only'])
self.assertEqual(ctxt['ssl_cert'], '/etc/nova/ssl/nova_cert.pem')
self.assertEqual(ctxt['ssl_key'], '/etc/nova/ssl/nova_key.pem')
self.assertEqual(ctxt['html5proxy_base_url'],
'https://10.5.100.1:6082/spice_auto.html')
@mock.patch('__builtin__.open')
@mock.patch('os.path.exists')
@mock.patch.object(context, 'config')
@mock.patch.object(context, 'unit_get')
@mock.patch.object(context, 'is_clustered')
@mock.patch.object(context, 'resolve_address')
@mock.patch.object(context, 'b64decode')
def test_spice_html5_ssl_enabled_no_cluster(self, mock_b64decode,
mock_resolve_address,
mock_is_clustered,
mock_unit_get,
mock_config, mock_exists,
mock_open):
config = {'console-ssl-cert': 'LS0tLS1CRUdJTiBDRV',
'console-ssl-key': 'LS0tLS1CRUdJTiBQUk',
'console-access-protocol': 'spice'}
mock_config.side_effect = lambda key: config.get(key)
mock_exists.return_value = True
mock_unit_get.return_value = '10.5.0.1'
mock_is_clustered.return_value = False
mock_b64decode.return_value = 'decode_success'
mock_open.return_value.__enter__ = lambda s: s
mock_open.return_value.__exit__ = mock.Mock()
ctxt = context.ConsoleSSLContext()()
self.assertTrue(ctxt['ssl_only'])
self.assertEqual(ctxt['ssl_cert'], '/etc/nova/ssl/nova_cert.pem')
self.assertEqual(ctxt['ssl_key'], '/etc/nova/ssl/nova_key.pem')
self.assertEqual(ctxt['html5proxy_base_url'],
'https://10.5.0.1:6082/spice_auto.html')

View File

@ -681,16 +681,14 @@ class NovaCCHooksTests(CharmTestCase):
}
self.assertEqual(_con_sets, console_settings)
@patch.object(hooks, 'canonical_url')
@patch.object(hooks, 'https')
@patch.object(utils, 'config')
def test_console_settings_explicit_ip(self, _utils_config,
_canonical_url):
def test_console_settings_explicit_ip(self, _utils_config, _https):
_utils_config.return_value = 'spice'
_https.return_value = False
_cc_public_host = "public-host"
_cc_private_host = "private-host"
self.test_config.set('console-proxy-ip', _cc_public_host)
_con_sets = hooks.console_settings()
_canonical_url.return_value = 'http://' + _cc_private_host
console_settings = {
'console_proxy_spice_address': 'http://%s:6082/spice_auto.html' %
(_cc_public_host),
@ -701,6 +699,25 @@ class NovaCCHooksTests(CharmTestCase):
}
self.assertEqual(_con_sets, console_settings)
@patch.object(hooks, 'https')
@patch.object(utils, 'config')
def test_console_settings_explicit_ip_with_https(self, _utils_config,
_https):
_utils_config.return_value = 'spice'
_https.return_value = True
_cc_public_host = "public-host"
self.test_config.set('console-proxy-ip', _cc_public_host)
_con_sets = hooks.console_settings()
console_settings = {
'console_proxy_spice_address': 'https://%s:6082/spice_auto.html' %
(_cc_public_host),
'console_proxy_spice_host': _cc_public_host,
'console_proxy_spice_port': 6082,
'console_access_protocol': 'spice',
'console_keymap': 'en-us'
}
self.assertEqual(_con_sets, console_settings)
def test_conditional_neutron_migration(self):
self.os_release.return_value = 'juno'
self.services.return_value = ['neutron-server']