Add functionality for vendor_data

Using vendor metadata helps alleviate the need to spin custom images
for things like package mirrors, timezones, or network proxies.

Adds new config option 'vendor-data' which takes a JSON formated
string to be used as static vendor metadata.

Adds new config option 'vendor-data-url' which takes a URL which
serves dynamic JSON formatted vendor metadata.

Adds new NovaMetadataContext class which writes
/etc/nova/vendor_data.json and enables it via nova.conf.

Closes-Bug: 1777714

Change-Id: I1d70804e59d42b0651a462c81e01d9c95626f27d
This commit is contained in:
Shane Peters 2018-06-25 12:07:49 -04:00
parent 2a7749333c
commit b355ea0473
8 changed files with 194 additions and 2 deletions

View File

@ -251,3 +251,19 @@ options:
description: |
IPFIX target wit the format "IP_Address:Port". This will enable IPFIX
exporting on all OVS bridges to the target, including br-int and br-ext.
vendor-data:
type: string
default:
description: |
A JSON-formatted string that will serve as vendor metadata
(via "StaticJSON" provider) to all VM's within an OpenStack deployment,
regardless of project or domain.
vendor-data-url:
type: string
default:
description: |
A URL serving JSON-formatted data that will serve as vendor metadata
(via "DynamicJSON" provider) to all VM's within an OpenStack deployment,
regardless of project or domain.
.
Only supported in OpenStack Newton and higher.

View File

@ -2,6 +2,7 @@
import os
import uuid
from charmhelpers.core.hookenv import (
log, ERROR,
config,
unit_get,
network_get_primary_address,
@ -11,7 +12,11 @@ from charmhelpers.contrib.openstack.context import (
NeutronAPIContext,
config_flags_parser,
)
from charmhelpers.contrib.hahelpers.cluster import(
from charmhelpers.contrib.openstack.utils import (
os_release,
CompareOpenStackReleases,
)
from charmhelpers.contrib.hahelpers.cluster import (
eligible_leader
)
from charmhelpers.contrib.network.ip import (
@ -145,6 +150,30 @@ class NeutronGatewayContext(NeutronAPIContext):
return ctxt
class NovaMetadataContext(OSContextGenerator):
def __call__(self):
ctxt = {}
ctxt['vendordata_providers'] = []
vdata = config('vendor-data')
vdata_url = config('vendor-data-url')
cmp_os_release = CompareOpenStackReleases(os_release('neutron-common'))
if vdata:
ctxt['vendor_data'] = True
ctxt['vendordata_providers'].append('StaticJSON')
if vdata_url:
if cmp_os_release > 'mitaka':
ctxt['vendor_data_url'] = vdata_url
ctxt['vendordata_providers'].append('DynamicJSON')
else:
log('Dynamic vendor data unsupported'
' for {}.'.format(cmp_os_release), level=ERROR)
return ctxt
SHARED_SECRET = "/etc/{}/secret.txt"

View File

@ -64,6 +64,7 @@ from neutron_utils import (
assess_status,
install_systemd_override,
configure_apparmor,
write_vendordata,
)
hooks = Hooks()
@ -118,6 +119,9 @@ def config_changed():
if sysctl_dict:
create_sysctl(sysctl_dict, '/etc/sysctl.d/50-quantum-gateway.conf')
if config('vendor-data'):
write_vendordata(config('vendor-data'))
# Re-run joined hooks as config might have changed
for r_id in relation_ids('amqp'):
amqp_joined(relation_id=r_id)

View File

@ -1,4 +1,5 @@
import os
import json
import shutil
import subprocess
from shutil import copy2
@ -68,6 +69,7 @@ from neutron_contexts import (
CORE_PLUGIN, OVS, NSX, N1KV, OVS_ODL,
NeutronGatewayContext,
L3AgentContext,
NovaMetadataContext,
)
from charmhelpers.contrib.openstack.neutron import (
parse_bridge_mappings,
@ -79,6 +81,7 @@ from copy import deepcopy
def valid_plugin():
return config('plugin') in CORE_PLUGIN
NEUTRON_COMMON = 'neutron-common'
VERSION_PACKAGE = NEUTRON_COMMON
@ -262,6 +265,7 @@ def determine_l3ha_packages():
def use_l3ha():
return NeutronAPIContext()()['enable_l3ha']
EXT_PORT_CONF = '/etc/init/ext-port.conf'
PHY_NIC_MTU_CONF = '/etc/init/os-charm-phy-nic-mtu.conf'
STOPPED_SERVICES = ['os-charm-phy-nic-mtu', 'ext-port']
@ -293,7 +297,8 @@ NOVA_CONFIG_FILES = {
SyslogContext(),
context.WorkerConfigContext(),
context.ZeroMQContext(),
context.NotificationDriverContext()],
context.NotificationDriverContext(),
NovaMetadataContext()],
'services': ['nova-api-metadata']
},
NOVA_API_METADATA_AA_PROFILE_PATH: {
@ -968,3 +973,17 @@ def configure_apparmor():
profiles.append(NEUTRON_LBAASV2_AA_PROFILE)
for profile in profiles:
context.AppArmorContext(profile).setup_aa_profile()
VENDORDATA_FILE = '/etc/nova/vendor_data.json'
def write_vendordata(vdata):
try:
json_vdata = json.loads(vdata)
except (TypeError, json.decoder.JSONDecodeError) as e:
log('Error decoding vendor-data. {}'.format(e), level=ERROR)
return False
with open(VENDORDATA_FILE, 'w') as vdata_file:
vdata_file.write(json.dumps(json_vdata, sort_keys=True, indent=2))
return True

View File

@ -18,6 +18,11 @@ network_api_class=nova.network.neutronv2.api.API
use_neutron = True
metadata_workers = {{ workers }}
{% if vendor_data -%}
vendordata_driver = nova.api.metadata.vendordata_json.JsonFileVendorData
vendordata_jsonfile_path = /etc/nova/vendor_data.json
{% endif -%}
[neutron]
url={{ quantum_url }}
auth_url={{ auth_protocol }}://{{ keystone_host }}:{{ auth_port }}

View File

@ -0,0 +1,48 @@
# newton
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[DEFAULT]
logdir=/var/log/nova
state_path=/var/lib/nova
root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf
debug = {{ debug }}
verbose= {{ verbose }}
use_syslog = {{ use_syslog }}
api_paste_config=/etc/nova/api-paste.ini
enabled_apis=metadata
multi_host=True
# Access to neutron API services
network_api_class=nova.network.neutronv2.api.API
use_neutron = True
metadata_workers = {{ workers }}
{% if vendor_data or vendor_data_url -%}
[api]
vendordata_providers = {{ vendordata_providers }}
{% if vendor_data -%}
vendordata_jsonfile_path = /etc/nova/vendor_data.json
{% endif -%}
{% if vendor_data_url -%}
vendordata_dynamic_targets = {{ vendor_data_url }}
{% endif -%}
{% endif -%}
[neutron]
url={{ quantum_url }}
auth_url={{ auth_protocol }}://{{ keystone_host }}:{{ auth_port }}
auth_type=password
project_domain_name=default
user_domain_name=default
region={{ region }}
project_name={{ service_tenant }}
username={{ service_username }}
password={{ service_password }}
service_metadata_proxy=True
metadata_proxy_shared_secret={{ shared_secret }}
{% include "section-rabbitmq-oslo" %}
[oslo_concurrency]
lock_path=/var/lock/nova

View File

@ -18,6 +18,7 @@ TO_PATCH = [
'eligible_leader',
'unit_get',
'network_get_primary_address',
'os_release',
]
@ -335,3 +336,54 @@ class TestMisc(CharmTestCase):
self.config.return_value = 'ovs'
self.assertEqual(neutron_contexts.core_plugin(),
neutron_contexts.NEUTRON_ML2_PLUGIN)
class TestNovaMetadataContext(CharmTestCase):
def setUp(self):
super(TestNovaMetadataContext, self).setUp(neutron_contexts,
TO_PATCH)
self.config.side_effect = self.test_config.get
def test_vendordata_static(self):
_vdata = '{"good": "json"}'
self.os_release.return_value = 'pike'
self.test_config.set('vendor-data', _vdata)
ctxt = neutron_contexts.NovaMetadataContext()()
self.assertTrue(ctxt['vendor_data'])
self.assertEqual(ctxt['vendordata_providers'], ['StaticJSON'])
def test_vendordata_dynamic(self):
_vdata_url = 'http://example.org/vdata'
self.os_release.return_value = 'pike'
self.test_config.set('vendor-data-url', _vdata_url)
ctxt = neutron_contexts.NovaMetadataContext()()
self.assertEqual(ctxt['vendor_data_url'], _vdata_url)
self.assertEqual(ctxt['vendordata_providers'], ['DynamicJSON'])
def test_vendordata_static_and_dynamic(self):
_vdata = '{"good": "json"}'
_vdata_url = 'http://example.org/vdata'
self.os_release.return_value = 'pike'
self.test_config.set('vendor-data', _vdata)
self.test_config.set('vendor-data-url', _vdata_url)
ctxt = neutron_contexts.NovaMetadataContext()()
self.assertTrue(ctxt['vendor_data'])
self.assertEqual(ctxt['vendor_data_url'], _vdata_url)
self.assertEqual(ctxt['vendordata_providers'], ['StaticJSON',
'DynamicJSON'])
def test_vendordata_mitaka(self):
_vdata_url = 'http://example.org/vdata'
self.os_release.return_value = 'mitaka'
self.test_config.set('vendor-data-url', _vdata_url)
ctxt = neutron_contexts.NovaMetadataContext()()
self.assertEqual(ctxt, {'vendordata_providers': []})

View File

@ -11,6 +11,10 @@ from test_utils import (
CharmTestCase
)
from test_neutron_contexts import (
patch_open
)
TO_PATCH = [
'config',
'get_os_codename_install_source',
@ -717,6 +721,21 @@ class TestNeutronUtils(CharmTestCase):
for config in EXC_CONFIG:
self.assertTrue(config not in actual_configs)
def test_write_valid_json_vendordata(self):
_jdata = '{"good": "json"}'
_tdata = '{\n "good": "json"\n}'
with patch_open() as (_open, _file):
self.assertEqual(neutron_utils.write_vendordata(_jdata), True)
_open.assert_called_with(neutron_utils.VENDORDATA_FILE, 'w')
_file.write.assert_called_with(_tdata)
@patch('json.loads')
def test_write_invalid_json_vendordata(self, _json_loads):
_jdata = '{ bad json }'
_json_loads.side_effect = TypeError
with patch_open() as (_open, _file):
self.assertEqual(neutron_utils.write_vendordata(_jdata), False)
network_context = {
'service_username': 'foo',