[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:
commit
66b838dcdf
14
config.yaml
14
config.yaml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -165,3 +165,6 @@ enabled=True
|
|||
|
||||
[conductor]
|
||||
workers = {{ workers }}
|
||||
|
||||
[spice]
|
||||
{% include "parts/spice" %}
|
||||
|
|
|
@ -160,3 +160,6 @@ enabled=True
|
|||
|
||||
[conductor]
|
||||
workers = {{ workers }}
|
||||
|
||||
[spice]
|
||||
{% include "parts/spice" %}
|
||||
|
|
|
@ -155,3 +155,6 @@ workers = {{ workers }}
|
|||
|
||||
[oslo_concurrency]
|
||||
lock_path=/var/lock/nova
|
||||
|
||||
[spice]
|
||||
{% include "parts/spice" %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{% if html5proxy_base_url -%}
|
||||
html5proxy_base_url = {{ html5proxy_base_url }}
|
||||
{% endif -%}
|
|
@ -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')
|
||||
|
|
|
@ -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']
|
||||
|
|
Loading…
Reference in New Issue