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:
Ionut Balutoiu 2021-02-03 15:32:06 +00:00 committed by Billy Olsen
parent 77a1bee1f2
commit b56572cf6b
6 changed files with 234 additions and 3 deletions

View File

@ -49,3 +49,18 @@ clear-unit-knownhost-cache:
not set; caching of hosts occurs regardless of that setting, and so this 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 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). 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.

View File

@ -30,6 +30,7 @@ _add_path(_root)
import charmhelpers.core.hookenv as hookenv 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_utils as utils
import hooks.nova_cc_hooks as ncc_hooks import hooks.nova_cc_hooks as ncc_hooks
@ -122,6 +123,73 @@ def clear_knownhost_cache(target):
return affected_units 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 # A dictionary of all the defined actions to callables (which take
# parsed arguments). # parsed arguments).
ACTIONS = { ACTIONS = {
@ -129,6 +197,7 @@ ACTIONS = {
"resume": resume, "resume": resume,
"archive-data": archive_data, "archive-data": archive_data,
"clear-unit-knownhost-cache": clear_unit_knownhost_cache, "clear-unit-knownhost-cache": clear_unit_knownhost_cache,
"sync-compute-availability-zones": sync_compute_availability_zones,
} }

View File

@ -0,0 +1 @@
actions.py

View File

@ -75,6 +75,7 @@ BASE_PACKAGES = [
PY3_PACKAGES = [ PY3_PACKAGES = [
'libapache2-mod-wsgi-py3', 'libapache2-mod-wsgi-py3',
'python3-nova', 'python3-nova',
'python3-novaclient',
'python3-keystoneclient', 'python3-keystoneclient',
'python3-psutil', 'python3-psutil',
'python3-six', 'python3-six',

View File

@ -5,9 +5,9 @@ gate_bundles:
- vault: groovy-victoria - vault: groovy-victoria
- vault: focal-victoria - vault: focal-victoria
- vault: focal-ussuri - vault: focal-ussuri
- bionic-ussuri - sync_az: bionic-ussuri
- bionic-train - sync_az: bionic-train
- bionic-stein - sync_az: bionic-stein
- bionic-queens - bionic-queens
- xenial-mitaka - xenial-mitaka
dev_bundles: dev_bundles:
@ -32,6 +32,12 @@ configure:
- zaza.openstack.charm_tests.neutron.setup.basic_overcloud_network - zaza.openstack.charm_tests.neutron.setup.basic_overcloud_network
- zaza.openstack.charm_tests.nova.setup.create_flavors - zaza.openstack.charm_tests.nova.setup.create_flavors
- zaza.openstack.charm_tests.nova.setup.manage_ssh_key - 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: tests:
- zaza.openstack.charm_tests.nova.tests.CirrosGuestCreateTest - zaza.openstack.charm_tests.nova.tests.CirrosGuestCreateTest
- zaza.openstack.charm_tests.nova.tests.SecurityTests - 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.CirrosGuestCreateTest
- zaza.openstack.charm_tests.nova.tests.SecurityTests - zaza.openstack.charm_tests.nova.tests.SecurityTests
- zaza.openstack.charm_tests.nova.tests.NovaCloudController - 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: tests_options:
force_deploy: force_deploy:
- groovy-victoria - groovy-victoria

View File

@ -151,6 +151,139 @@ class ClearUnitKnownhostCacheTestCase(CharmTestCase):
mock.call(rid="r:2", unit="bservice/3")]) 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): class MainTestCase(CharmTestCase):
def setUp(self): def setUp(self):