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
|
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.
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 = [
|
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',
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user