From 4fb124310091008b3f650ac389d010d530b6831e Mon Sep 17 00:00:00 2001 From: Dmitrii Shcherbakov Date: Thu, 4 Oct 2018 19:22:43 +0300 Subject: [PATCH] Allow Juju AZ context information to be used The change adds an option to the charm to use JUJU_AVAILABILITY_ZONE environment variable set by Juju for the hook environment based on the underlying provider's availability zone information for a given machine. This information is used to configure default_availability_zone for nova and availability_zone for subordinate networking charms. Change-Id: Idc7112e7fe7b76d15cf9c4896b702b8ffd8c0e8e Closes-Bug: #1796068 --- README.md | 31 +++++++++++ config.yaml | 15 +++++- hooks/nova_compute_context.py | 10 ++-- hooks/nova_compute_hooks.py | 3 +- hooks/nova_compute_utils.py | 7 +++ unit_tests/test_nova_compute_contexts.py | 65 ++++++++++++++++++++++++ unit_tests/test_nova_compute_hooks.py | 23 +++++++++ unit_tests/test_nova_compute_utils.py | 52 +++++++++++++++++++ 8 files changed, 201 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 90aa66b7..251738ad 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,37 @@ different cephx keys and user names. See LP Bug [#1671422](https://bugs.launchpad.net/charm-cinder-ceph/+bug/1671422) for more information. +Availability Zones +================== + +There are two options to provide default_availability_zone config +for nova nodes: + + - default-availability-zone + - customize-failure-domain + +The order of precedence is as follows: + + 1. Information from a Juju provider (JUJU_AVAILABILITY_ZONE) + if customize-failure-domain is set to True and Juju + has set the JUJU_AVAILABILITY_ZONE to a non-empty value; + 2. The value of default-availability-zone will be used + if customize-failure-domain is set to True but no + JUJU_AVAILABILITY_ZONE is provided via hook + context by the Juju provider; + 3. Otherwise, the value of default-availability-zone + charm option will be used. + +The default_availability_zone in Nova affects scheduling if a +given Nova node was not placed into an aggregate with an +availability zone present as a property by an operator. Using +customize-failure-domain is recommended as it provides AZ-aware +scheduling out of the box if an operator specifies an AZ during +instance creation. + +These options also affect the AZ propagated down to networking +subordinates which is useful for AZ-aware Neutron agent scheduling. + NFV support =========== diff --git a/config.yaml b/config.yaml index 5660bec5..6fb5293a 100644 --- a/config.yaml +++ b/config.yaml @@ -373,11 +373,24 @@ options: . This option determines the availability zone to be used when it is not specified in the VM creation request. If this option is not set, the - default availability zone 'nova' is used. + default availability zone 'nova' is used. If customize-failure-domain + is set to True, it will override this option only if an AZ is set by + the Juju provider. If JUJU_AVAILABILITY_ZONE is not set, the value + specified by this option will be used regardless of + customize-failure-domain's setting. . NOTE: Availability zones must be created manually using the 'openstack aggregate create' command. . + customize-failure-domain: + type: boolean + default: False + description: | + Juju propagates availability zone information to charms from the + underlying machine provider such as MAAS and this option allows the + charm to use JUJU_AVAILABILITY_ZONE to set default_availability_zone for + Nova nodes. This option overrides the default-availability-zone charm + config setting only when the Juju provider sets JUJU_AVAILABILITY_ZONE. resume-guests-state-on-host-boot: type: boolean default: False diff --git a/hooks/nova_compute_context.py b/hooks/nova_compute_context.py index 61922c18..d11938a4 100644 --- a/hooks/nova_compute_context.py +++ b/hooks/nova_compute_context.py @@ -51,6 +51,7 @@ from charmhelpers.contrib.network.ip import ( get_relation_ip, ) + # This is just a label and it must be consistent across # nova-compute nodes to support live migration. CEPH_SECRET_UUID = '514c9fca-8cbe-11e2-9c52-3bc8c7819472' @@ -86,6 +87,11 @@ def _network_manager(): return manager() +def _get_availability_zone(): + from nova_compute_utils import get_availability_zone as get_az + return get_az() + + def _neutron_security_groups(): ''' Inspects current cloud-compute relation and determine if nova-c-c has @@ -722,7 +728,5 @@ class NovaComputeAvailabilityZoneContext(context.OSContextGenerator): def __call__(self): ctxt = {} - if config('default-availability-zone'): - ctxt['default_availability_zone'] = config( - 'default-availability-zone') + ctxt['default_availability_zone'] = _get_availability_zone() return ctxt diff --git a/hooks/nova_compute_hooks.py b/hooks/nova_compute_hooks.py index 9a075a3e..ce2d9575 100755 --- a/hooks/nova_compute_hooks.py +++ b/hooks/nova_compute_hooks.py @@ -109,6 +109,7 @@ from nova_compute_utils import ( configure_local_ephemeral_storage, pause_unit_helper, resume_unit_helper, + get_availability_zone, ) from charmhelpers.contrib.network.ip import ( @@ -464,7 +465,7 @@ def update_nrpe_config(): def neutron_plugin_joined(relid=None, remote_restart=False): rel_settings = { 'hugepage_number': get_hugepage_number(), - 'default_availability_zone': config('default-availability-zone') + 'default_availability_zone': get_availability_zone() } if remote_restart: rel_settings['restart-trigger'] = str(uuid.uuid4()) diff --git a/hooks/nova_compute_utils.py b/hooks/nova_compute_utils.py index edfc985d..ac90ec43 100644 --- a/hooks/nova_compute_utils.py +++ b/hooks/nova_compute_utils.py @@ -907,3 +907,10 @@ def configure_local_ephemeral_storage(): # storage is never reconfigured by mistake, losing instance disks db.set('storage-configured', True) db.flush() + + +def get_availability_zone(): + use_juju_az = config('customize-failure-domain') + juju_az = os.environ.get('JUJU_AVAILABILITY_ZONE') + return (juju_az if use_juju_az and juju_az + else config('default-availability-zone')) diff --git a/unit_tests/test_nova_compute_contexts.py b/unit_tests/test_nova_compute_contexts.py index 803111ef..743e547d 100644 --- a/unit_tests/test_nova_compute_contexts.py +++ b/unit_tests/test_nova_compute_contexts.py @@ -563,3 +563,68 @@ class SerialConsoleContextTests(CharmTestCase): 'host_uuid': self.host_uuid, 'force_raw_images': True, 'reserved_host_memory': 512}, libvirt()) + + +class NovaComputeAvailabilityZoneContextTests(CharmTestCase): + + def setUp(self): + super(NovaComputeAvailabilityZoneContextTests, + self).setUp(context, TO_PATCH) + self.os_release.return_value = 'kilo' + + @patch('nova_compute_utils.config') + @patch('os.environ.get') + def test_availability_zone_no_juju_with_env(self, mock_get, + mock_config): + def environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': 'az1', + }[key] + mock_get.side_effect = environ_get_side_effect + + def config_side_effect(key): + return { + 'customize-failure-domain': False, + 'default-availability-zone': 'nova', + }[key] + + mock_config.side_effect = config_side_effect + az_context = context.NovaComputeAvailabilityZoneContext() + self.assertEqual( + {'default_availability_zone': 'nova'}, az_context()) + + @patch('nova_compute_utils.config') + @patch('os.environ.get') + def test_availability_zone_no_juju_no_env(self, mock_get, + mock_config): + def environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': '', + }[key] + mock_get.side_effect = environ_get_side_effect + + def config_side_effect(key): + return { + 'customize-failure-domain': False, + 'default-availability-zone': 'nova', + }[key] + + mock_config.side_effect = config_side_effect + az_context = context.NovaComputeAvailabilityZoneContext() + + self.assertEqual( + {'default_availability_zone': 'nova'}, az_context()) + + @patch('os.environ.get') + def test_availability_zone_juju(self, mock_get): + def environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': 'az1', + }[key] + mock_get.side_effect = environ_get_side_effect + + self.config.side_effect = self.test_config.get + self.test_config.set('customize-failure-domain', True) + az_context = context.NovaComputeAvailabilityZoneContext() + self.assertEqual( + {'default_availability_zone': 'az1'}, az_context()) diff --git a/unit_tests/test_nova_compute_hooks.py b/unit_tests/test_nova_compute_hooks.py index 269b26b4..87d9e225 100644 --- a/unit_tests/test_nova_compute_hooks.py +++ b/unit_tests/test_nova_compute_hooks.py @@ -542,6 +542,29 @@ class NovaComputeRelationsTests(CharmTestCase): **expect_rel_settings ) + @patch('os.environ.get') + @patch.object(hooks, 'get_hugepage_number') + def test_neutron_plugin_joined_relid_juju_az(self, + get_hugepage_number, + mock_env_get): + self.test_config.set('customize-failure-domain', True) + + def environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': 'az1', + }[key] + mock_env_get.side_effect = environ_get_side_effect + get_hugepage_number.return_value = None + hooks.neutron_plugin_joined(relid='relid23') + expect_rel_settings = { + 'hugepage_number': None, + 'default_availability_zone': 'az1', + } + self.relation_set.assert_called_with( + relation_id='relid23', + **expect_rel_settings + ) + @patch.object(hooks, 'get_hugepage_number') def test_neutron_plugin_joined_huge(self, get_hugepage_number): get_hugepage_number.return_value = 12 diff --git a/unit_tests/test_nova_compute_utils.py b/unit_tests/test_nova_compute_utils.py index d30cc2ad..ee9dabd6 100644 --- a/unit_tests/test_nova_compute_utils.py +++ b/unit_tests/test_nova_compute_utils.py @@ -952,3 +952,55 @@ class NovaComputeUtilsTests(CharmTestCase): priority=80 ) self.is_block_device.assert_not_called() + + @patch.object(utils.os.environ, 'get') + def test_get_az_customize_with_env(self, os_environ_get_mock): + self.test_config.set('customize-failure-domain', True) + self.test_config.set('default-availability-zone', 'nova') + + def os_environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': 'az1', + }[key] + os_environ_get_mock.side_effect = os_environ_get_side_effect + az = utils.get_availability_zone() + self.assertEqual('az1', az) + + @patch.object(utils.os.environ, 'get') + def test_get_az_customize_without_env(self, os_environ_get_mock): + self.test_config.set('customize-failure-domain', True) + self.test_config.set('default-availability-zone', 'mynova') + + def os_environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': '', + }[key] + os_environ_get_mock.side_effect = os_environ_get_side_effect + az = utils.get_availability_zone() + self.assertEqual('mynova', az) + + @patch.object(utils.os.environ, 'get') + def test_get_az_no_customize_without_env(self, os_environ_get_mock): + self.test_config.set('customize-failure-domain', False) + self.test_config.set('default-availability-zone', 'nova') + + def os_environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': '', + }[key] + os_environ_get_mock.side_effect = os_environ_get_side_effect + az = utils.get_availability_zone() + self.assertEqual('nova', az) + + @patch.object(utils.os.environ, 'get') + def test_get_az_no_customize_with_env(self, os_environ_get_mock): + self.test_config.set('customize-failure-domain', False) + self.test_config.set('default-availability-zone', 'nova') + + def os_environ_get_side_effect(key): + return { + 'JUJU_AVAILABILITY_ZONE': 'az1', + }[key] + os_environ_get_mock.side_effect = os_environ_get_side_effect + az = utils.get_availability_zone() + self.assertEqual('nova', az)