Add sync-compute-availability-zones Juju action
This action should be used to sync the Juju availability zones, from the nova-compute units, with the OpenStack availability zones. The action is meant to be used post-deployment by the operator. It will setup OpenStack aggregates for each availability zone, and add the proper compute hosts to them. Co-Authored-By: Billy Olsen <billy.olsen@canonical.com> Change-Id: Ibd71cd61e51b04599eadf21b3ef46e47544b8814
This commit is contained in:
parent
77a1bee1f2
commit
b56572cf6b
15
actions.yaml
15
actions.yaml
@ -49,3 +49,18 @@ clear-unit-knownhost-cache:
|
||||
not set; caching of hosts occurs regardless of that setting, and so this
|
||||
action can be used to force an update if DNS has changed in the system, or
|
||||
for a particular host (although this scenario is unlikely).
|
||||
sync-compute-availability-zones:
|
||||
description: |
|
||||
Update Nova host aggregates to match the availability zone defined in the
|
||||
related nova-compute units. This action will create any missing host
|
||||
aggregates in Nova and add hypervisors to the appropriate host aggregates.
|
||||
This action will not remove any hypervisors from host aggregates already
|
||||
configured in nova.
|
||||
.
|
||||
This action requires that the nova-cloud-controller application be fully
|
||||
related to keystone. This action will fail if the Nova API is unavailable.
|
||||
Successful completion of this action will report a list of each hypervisor
|
||||
added to an availability zone. Successful completion with no output means
|
||||
that all hypervisors were associated with their host aggregates.
|
||||
.
|
||||
This action is only available for OpenStack Stein and newer.
|
||||
|
@ -30,6 +30,7 @@ _add_path(_root)
|
||||
|
||||
|
||||
import charmhelpers.core.hookenv as hookenv
|
||||
import charmhelpers.contrib.openstack.utils as ch_utils
|
||||
import hooks.nova_cc_utils as utils
|
||||
import hooks.nova_cc_hooks as ncc_hooks
|
||||
|
||||
@ -122,6 +123,73 @@ def clear_knownhost_cache(target):
|
||||
return affected_units
|
||||
|
||||
|
||||
def sync_compute_availability_zones(args):
|
||||
"""Sync the nova-compute Juju units' availability zones with the OpenStack
|
||||
hypervisors' availability zones."""
|
||||
# Due to python3 issues, we do a check here to see which version of
|
||||
# OpenStack is installed and gate the availability of the action on
|
||||
# that. See note below.
|
||||
release = ch_utils.CompareOpenStackReleases(
|
||||
ch_utils.os_release('nova-common'))
|
||||
if release < 'stein':
|
||||
msg = ('The sync_compute_availability_zones action is not available'
|
||||
'for the {} release.'.format(release))
|
||||
hookenv.action_fail(msg)
|
||||
return
|
||||
|
||||
# Note (wolsen): There's a problem with the action script using only
|
||||
# python3 (/usr/bin/env python3) above, however on versions lower than
|
||||
# rocky, the python2 versions of the following python packages are
|
||||
# installed. The imports are moved to here to avoid causing actions
|
||||
# to fail outright.
|
||||
import hooks.nova_cc_context as ncc_context
|
||||
from keystoneauth1 import session
|
||||
from keystoneauth1.identity import v3
|
||||
from novaclient import client as nova_client
|
||||
from novaclient import exceptions as nova_exceptions
|
||||
|
||||
ctxt = ncc_context.IdentityServiceContext()()
|
||||
if not ctxt:
|
||||
hookenv.action_fail("Identity service context cannot be generated")
|
||||
return
|
||||
|
||||
keystone_auth = ctxt['keystone_authtoken']
|
||||
keystone_creds = {
|
||||
'auth_url': keystone_auth.get('auth_url'),
|
||||
'username': keystone_auth.get('username'),
|
||||
'password': keystone_auth.get('password'),
|
||||
'user_domain_name': keystone_auth.get('user_domain_name'),
|
||||
'project_domain_name': keystone_auth.get('project_domain_name'),
|
||||
'project_name': keystone_auth.get('project_name'),
|
||||
}
|
||||
keystone_session = session.Session(auth=v3.Password(**keystone_creds))
|
||||
client = nova_client.Client(2, session=keystone_session)
|
||||
output_str = ''
|
||||
for r_id in hookenv.relation_ids('cloud-compute'):
|
||||
units = hookenv.related_units(r_id)
|
||||
for unit in units:
|
||||
rel_data = hookenv.relation_get(rid=r_id, unit=unit)
|
||||
unit_az = rel_data.get('availability_zone')
|
||||
if not unit_az:
|
||||
continue
|
||||
aggregate_name = '{}_az'.format(unit_az)
|
||||
try:
|
||||
aggregate = client.aggregates.find(
|
||||
name=aggregate_name, availability_zone=unit_az)
|
||||
except nova_exceptions.NotFound:
|
||||
aggregate = client.aggregates.create(
|
||||
aggregate_name, availability_zone=unit_az)
|
||||
unit_ip = rel_data.get('private-address')
|
||||
hypervisor = client.hypervisors.find(host_ip=unit_ip)
|
||||
if hypervisor.hypervisor_hostname not in aggregate.hosts:
|
||||
client.aggregates.add_host(
|
||||
aggregate, hypervisor.hypervisor_hostname)
|
||||
output_str += \
|
||||
"Hypervisor {} added to availability zone {}\n".format(
|
||||
hypervisor.hypervisor_hostname, unit_az)
|
||||
hookenv.action_set({'output': output_str})
|
||||
|
||||
|
||||
# A dictionary of all the defined actions to callables (which take
|
||||
# parsed arguments).
|
||||
ACTIONS = {
|
||||
@ -129,6 +197,7 @@ ACTIONS = {
|
||||
"resume": resume,
|
||||
"archive-data": archive_data,
|
||||
"clear-unit-knownhost-cache": clear_unit_knownhost_cache,
|
||||
"sync-compute-availability-zones": sync_compute_availability_zones,
|
||||
}
|
||||
|
||||
|
||||
|
1
actions/sync-compute-availability-zones
Symbolic link
1
actions/sync-compute-availability-zones
Symbolic link
@ -0,0 +1 @@
|
||||
actions.py
|
@ -75,6 +75,7 @@ BASE_PACKAGES = [
|
||||
PY3_PACKAGES = [
|
||||
'libapache2-mod-wsgi-py3',
|
||||
'python3-nova',
|
||||
'python3-novaclient',
|
||||
'python3-keystoneclient',
|
||||
'python3-psutil',
|
||||
'python3-six',
|
||||
|
@ -5,9 +5,9 @@ gate_bundles:
|
||||
- vault: groovy-victoria
|
||||
- vault: focal-victoria
|
||||
- vault: focal-ussuri
|
||||
- bionic-ussuri
|
||||
- bionic-train
|
||||
- bionic-stein
|
||||
- sync_az: bionic-ussuri
|
||||
- sync_az: bionic-train
|
||||
- sync_az: bionic-stein
|
||||
- bionic-queens
|
||||
- xenial-mitaka
|
||||
dev_bundles:
|
||||
@ -32,6 +32,12 @@ configure:
|
||||
- zaza.openstack.charm_tests.neutron.setup.basic_overcloud_network
|
||||
- zaza.openstack.charm_tests.nova.setup.create_flavors
|
||||
- zaza.openstack.charm_tests.nova.setup.manage_ssh_key
|
||||
- sync_az:
|
||||
- zaza.openstack.charm_tests.glance.setup.add_cirros_image
|
||||
- zaza.openstack.charm_tests.keystone.setup.add_demo_user
|
||||
- zaza.openstack.charm_tests.neutron.setup.basic_overcloud_network
|
||||
- zaza.openstack.charm_tests.nova.setup.create_flavors
|
||||
- zaza.openstack.charm_tests.nova.setup.manage_ssh_key
|
||||
tests:
|
||||
- zaza.openstack.charm_tests.nova.tests.CirrosGuestCreateTest
|
||||
- zaza.openstack.charm_tests.nova.tests.SecurityTests
|
||||
@ -40,6 +46,12 @@ tests:
|
||||
- zaza.openstack.charm_tests.nova.tests.CirrosGuestCreateTest
|
||||
- zaza.openstack.charm_tests.nova.tests.SecurityTests
|
||||
- zaza.openstack.charm_tests.nova.tests.NovaCloudController
|
||||
- zaza.openstack.charm_tests.nova.tests.NovaCloudControllerActionTest
|
||||
- sync_az:
|
||||
- zaza.openstack.charm_tests.nova.tests.CirrosGuestCreateTest
|
||||
- zaza.openstack.charm_tests.nova.tests.SecurityTests
|
||||
- zaza.openstack.charm_tests.nova.tests.NovaCloudController
|
||||
- zaza.openstack.charm_tests.nova.tests.NovaCloudControllerActionTest
|
||||
tests_options:
|
||||
force_deploy:
|
||||
- groovy-victoria
|
||||
|
@ -151,6 +151,139 @@ class ClearUnitKnownhostCacheTestCase(CharmTestCase):
|
||||
mock.call(rid="r:2", unit="bservice/3")])
|
||||
|
||||
|
||||
class SyncComputeAvailabilityZonesTestCase(CharmTestCase):
|
||||
|
||||
@staticmethod
|
||||
def _relation_get(attribute=None, unit=None, rid=None):
|
||||
return {
|
||||
'aservice/0': {
|
||||
'private-address': '10.0.0.1',
|
||||
'availability_zone': 'site-a',
|
||||
},
|
||||
'aservice/1': {
|
||||
'private-address': '10.0.0.2',
|
||||
'availability_zone': 'site-b',
|
||||
},
|
||||
'aservice/2': {
|
||||
'private-address': '10.0.0.3',
|
||||
},
|
||||
'bservice/0': {
|
||||
'private-address': '10.0.1.1',
|
||||
'availability_zone': 'site-c',
|
||||
},
|
||||
'bservice/1': {
|
||||
'private-address': '10.0.1.2',
|
||||
'availability_zone': 'site-d',
|
||||
},
|
||||
}.get(unit)
|
||||
|
||||
def setUp(self):
|
||||
super(SyncComputeAvailabilityZonesTestCase, self).setUp(
|
||||
actions, [
|
||||
"charmhelpers.core.hookenv.action_fail",
|
||||
"charmhelpers.core.hookenv.action_set",
|
||||
"charmhelpers.core.hookenv.relation_ids",
|
||||
"charmhelpers.core.hookenv.related_units",
|
||||
"charmhelpers.core.hookenv.relation_get",
|
||||
"charmhelpers.contrib.openstack.utils.os_release",
|
||||
"hooks.nova_cc_context.IdentityServiceContext",
|
||||
"keystoneauth1.session.Session",
|
||||
"keystoneauth1.identity.v3.Password",
|
||||
"novaclient.client.Client",
|
||||
])
|
||||
self.relation_ids.return_value = ["r:1", "r:2"]
|
||||
self.related_units.side_effect = [
|
||||
['aservice/0', 'aservice/1', 'aservice/2'],
|
||||
['bservice/0', 'bservice/1'],
|
||||
]
|
||||
self.relation_get.side_effect = \
|
||||
SyncComputeAvailabilityZonesTestCase._relation_get
|
||||
self.os_release.return_value = 'ussuri'
|
||||
|
||||
def test_failing_action(self):
|
||||
self.IdentityServiceContext.return_value.return_value = {}
|
||||
actions.sync_compute_availability_zones([])
|
||||
self.IdentityServiceContext.assert_called_once()
|
||||
self.action_fail.assert_called_once()
|
||||
self.action_set.assert_not_called()
|
||||
|
||||
def test_early_release(self):
|
||||
self.os_release.return_value = 'queens'
|
||||
actions.sync_compute_availability_zones([])
|
||||
self.action_fail.assert_called_once()
|
||||
self.action_set.assert_not_called()
|
||||
self.IdentityServiceContext.assert_not_called()
|
||||
|
||||
def test_sync_compute_az(self):
|
||||
keystone_auth = {
|
||||
'auth_url': 'http://127.0.0.1:5000/v3',
|
||||
'username': 'test-user',
|
||||
'password': 'test-password',
|
||||
'user_domain_name': 'test-user-domain',
|
||||
'project_domain_name': 'test-project-domain',
|
||||
'project_name': 'test-project',
|
||||
}
|
||||
self.IdentityServiceContext.return_value.return_value = {
|
||||
'keystone_authtoken': keystone_auth
|
||||
}
|
||||
self.Password.return_value = 'v3-password-instance'
|
||||
self.Session.return_value = 'keystone-session'
|
||||
aggregate_mocks = [
|
||||
mock.MagicMock(hosts=[]),
|
||||
mock.MagicMock(hosts=[]),
|
||||
mock.MagicMock(hosts=[]),
|
||||
mock.MagicMock(hosts=[]),
|
||||
]
|
||||
self.Client.return_value.aggregates.find.side_effect = aggregate_mocks
|
||||
self.Client.return_value.hypervisors.find.side_effect = [
|
||||
mock.MagicMock(hypervisor_hostname='node-1'),
|
||||
mock.MagicMock(hypervisor_hostname='node-2'),
|
||||
mock.MagicMock(hypervisor_hostname='node-3'),
|
||||
mock.MagicMock(hypervisor_hostname='node-4'),
|
||||
]
|
||||
actions.sync_compute_availability_zones([])
|
||||
self.IdentityServiceContext.assert_called_once()
|
||||
self.Password.assert_called_once_with(**keystone_auth)
|
||||
self.Session.assert_called_once_with(auth='v3-password-instance')
|
||||
self.Client.assert_called_once_with(2, session='keystone-session')
|
||||
self.relation_ids.assert_called_once_with('cloud-compute')
|
||||
self.related_units.has_calls([
|
||||
mock.call('r:1'),
|
||||
mock.call('r:2')
|
||||
])
|
||||
self.relation_get.has_calls([
|
||||
mock.call(rid='r:1', unit='aservice/0'),
|
||||
mock.call(rid='r:1', unit='aservice/1'),
|
||||
mock.call(rid='r:1', unit='aservice/2'),
|
||||
mock.call(rid='r:2', unit='bservice/0'),
|
||||
mock.call(rid='r:2', unit='bservice/1'),
|
||||
])
|
||||
self.Client.aggregates.find.has_calls([
|
||||
mock.call(name='site-a_az', availability_zone='site-a'),
|
||||
mock.call(name='site-b_az', availability_zone='site-b'),
|
||||
mock.call(name='site-c_az', availability_zone='site-c'),
|
||||
mock.call(name='site-d_az', availability_zone='site-d'),
|
||||
])
|
||||
self.Client.hypervisors.find.has_calls([
|
||||
mock.call(host_ip='10.0.0.1'),
|
||||
mock.call(host_ip='10.0.0.2'),
|
||||
mock.call(host_ip='10.0.1.1'),
|
||||
mock.call(host_ip='10.0.1.2'),
|
||||
])
|
||||
self.Client.aggregates.add_host.has_calls([
|
||||
mock.call(aggregate_mocks[0], 'node-1'),
|
||||
mock.call(aggregate_mocks[1], 'node-2'),
|
||||
mock.call(aggregate_mocks[2], 'node-3'),
|
||||
mock.call(aggregate_mocks[3], 'node-4'),
|
||||
])
|
||||
self.action_set.assert_called_with({
|
||||
'output': ('Hypervisor node-1 added to availability zone site-a\n'
|
||||
'Hypervisor node-2 added to availability zone site-b\n'
|
||||
'Hypervisor node-3 added to availability zone site-c\n'
|
||||
'Hypervisor node-4 added to availability zone site-d\n')
|
||||
})
|
||||
|
||||
|
||||
class MainTestCase(CharmTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
Loading…
Reference in New Issue
Block a user