diff options
-rw-r--r-- | heat/common/environment_util.py | 6 | ||||
-rw-r--r-- | heat/common/pluginutils.py | 20 | ||||
-rw-r--r-- | heat/engine/resources/openstack/neutron/qos.py | 87 | ||||
-rw-r--r-- | heat/policies/resource_types.py | 3 | ||||
-rw-r--r-- | heat/tests/openstack/neutron/test_qos.py | 128 | ||||
-rw-r--r-- | heat/tests/test_common_pluginutils.py | 31 | ||||
-rw-r--r-- | releasenotes/notes/neutron-qos-minimum-packet-rate-rule-e58e9ced636320f1.yaml | 7 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | roles/run-heat-tests/defaults/main.yaml | 3 | ||||
-rw-r--r-- | roles/run-heat-tests/tasks/main.yaml | 23 | ||||
-rw-r--r-- | tox.ini | 10 |
11 files changed, 306 insertions, 14 deletions
diff --git a/heat/common/environment_util.py b/heat/common/environment_util.py index 5143719d8..eb3b8cde1 100644 --- a/heat/common/environment_util.py +++ b/heat/common/environment_util.py @@ -12,6 +12,7 @@ # under the License. import collections +from oslo_log import log as logging from oslo_serialization import jsonutils from heat.common import environment_format as env_fmt @@ -21,6 +22,8 @@ from heat.common.i18n import _ ALLOWED_PARAM_MERGE_STRATEGIES = (OVERWRITE, MERGE, DEEP_MERGE) = ( 'overwrite', 'merge', 'deep_merge') +LOG = logging.getLogger(__name__) + def get_param_merge_strategy(merge_strategies, param_key, available_strategies=None): @@ -129,12 +132,15 @@ def merge_parameters(old, new, param_schemata, strategies_in_file, param=key, env_file=env_file) if param_merge_strategy == DEEP_MERGE: + LOG.debug("Deep Merging Parameter: %s", key) param_merge(key, value, param_schemata[key], deep_merge=True) elif param_merge_strategy == MERGE: + LOG.debug("Merging Parameter: %s", key) param_merge(key, value, param_schemata[key]) else: + LOG.debug("Overriding Parameter: %s", key) old[key] = value return old, new_strategies diff --git a/heat/common/pluginutils.py b/heat/common/pluginutils.py index c4da0ec06..1fc9618ee 100644 --- a/heat/common/pluginutils.py +++ b/heat/common/pluginutils.py @@ -18,9 +18,17 @@ LOG = logging.getLogger(__name__) def log_fail_msg(manager, entrypoint, exception): - LOG.warning('Encountered exception while loading %(module_name)s: ' - '"%(message)s". Not using %(name)s.', - {'module_name': entrypoint.module, - 'message': getattr(exception, 'message', - str(exception)), - 'name': entrypoint.name}) + # importlib.metadata in Python 3.8 is quite old and the EntryPoint class + # does not have module. This logic is required to workaround AttributeError + # caused by that old implementation. + if hasattr(entrypoint, 'module'): + LOG.warning('Encountered exception while loading %(module_name)s: ' + '"%(message)s". Not using %(name)s.', + {'module_name': entrypoint.module, + 'message': getattr(exception, 'message', str(exception)), + 'name': entrypoint.name}) + else: + LOG.warning('Encountered exception: "%(message)s". ' + 'Not using %(name)s.', + {'message': getattr(exception, 'message', str(exception)), + 'name': entrypoint.name}) diff --git a/heat/engine/resources/openstack/neutron/qos.py b/heat/engine/resources/openstack/neutron/qos.py index d767050ae..a2a65adb9 100644 --- a/heat/engine/resources/openstack/neutron/qos.py +++ b/heat/engine/resources/openstack/neutron/qos.py @@ -378,10 +378,97 @@ class QoSMinimumBandwidthRule(QoSRule): return [self.resource_id, self.policy_id] +class QoSMinimumPacketRateRule(QoSRule): + """A resource for guaranteeing packet rate. + + This rule can be associated with a QoS policy, and then the policy + can be used by a neutron port to provide guaranteed packet rate QoS + capabilities. + + Depending on drivers the guarantee may be enforced on two levels. + First when a server is placed (scheduled) on physical infrastructure + and/or second in the data plane of the physical hypervisor. For details + please see Neutron documentation: + + https://docs.openstack.org/neutron/latest/admin/config-qos-min-pps.html + + The default policy usage of this resource is limited to + administrators only. + """ + + entity = 'minimum_packet_rate_rule' + + required_service_extension = 'qos-pps-minimum' + + support_status = support.SupportStatus( + status=support.SUPPORTED, + version='19.0.0', + ) + + PROPERTIES = ( + MIN_PACKET_RATE, DIRECTION + ) = ( + 'min_kpps', 'direction' + ) + + properties_schema = { + MIN_PACKET_RATE: properties.Schema( + properties.Schema.INTEGER, + _('Min packet rate in kpps.'), + required=True, + update_allowed=True, + constraints=[ + constraints.Range(min=0), + ], + ), + DIRECTION: properties.Schema( + properties.Schema.STRING, + _('Traffic direction from the point of view of the port.'), + update_allowed=True, + constraints=[ + constraints.AllowedValues(['any', 'egress', 'ingress']), + ], + default='egress', + ), + } + + properties_schema.update(QoSRule.properties_schema) + + def handle_create(self): + props = self.prepare_properties(self.properties, + self.physical_resource_name()) + props.pop(self.POLICY) + + rule = self.client().create_minimum_packet_rate_rule( + self.policy_id, + {'minimum_packet_rate_rule': props})['minimum_packet_rate_rule'] + + self.resource_id_set(rule['id']) + + def handle_delete(self): + if self.resource_id is None: + return + + with self.client_plugin().ignore_not_found: + self.client().delete_minimum_packet_rate_rule( + self.resource_id, self.policy_id) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if prop_diff: + self.client().update_minimum_packet_rate_rule( + self.resource_id, + self.policy_id, + {'minimum_packet_rate_rule': prop_diff}) + + def _res_get_args(self): + return [self.resource_id, self.policy_id] + + def resource_mapping(): return { 'OS::Neutron::QoSPolicy': QoSPolicy, 'OS::Neutron::QoSBandwidthLimitRule': QoSBandwidthLimitRule, 'OS::Neutron::QoSDscpMarkingRule': QoSDscpMarkingRule, 'OS::Neutron::QoSMinimumBandwidthRule': QoSMinimumBandwidthRule, + 'OS::Neutron::QoSMinimumPacketRateRule': QoSMinimumPacketRateRule, } diff --git a/heat/policies/resource_types.py b/heat/policies/resource_types.py index 39e6d2596..3bb3fc6f1 100644 --- a/heat/policies/resource_types.py +++ b/heat/policies/resource_types.py @@ -57,6 +57,9 @@ resource_types_policies = [ name=POLICY_ROOT % 'OS::Neutron::QoSMinimumBandwidthRule', check_str=base.RULE_PROJECT_ADMIN), policy.RuleDefault( + name=POLICY_ROOT % 'OS::Neutron::QoSMinimumPacketRateRule', + check_str=base.RULE_PROJECT_ADMIN), + policy.RuleDefault( name=POLICY_ROOT % 'OS::Neutron::Segment', check_str=base.RULE_PROJECT_ADMIN), policy.RuleDefault( diff --git a/heat/tests/openstack/neutron/test_qos.py b/heat/tests/openstack/neutron/test_qos.py index d46eb64f4..306b15b4f 100644 --- a/heat/tests/openstack/neutron/test_qos.py +++ b/heat/tests/openstack/neutron/test_qos.py @@ -70,6 +70,19 @@ resources: tenant_id: d66c74c01d6c41b9846088c1ad9634d0 ''' +minimum_packet_rate_rule_template = ''' +heat_template_version: 2021-04-16 +description: This template to define a neutron minimum packet rate rule. +resources: + my_minimum_packet_rate_rule: + type: OS::Neutron::QoSMinimumPacketRateRule + properties: + policy: 477e8273-60a7-4c41-b683-fdb0bc7cd151 + min_kpps: 1000 + direction: any + tenant_id: d66c74c01d6c41b9846088c1ad9634d0 +''' + class NeutronQoSPolicyTest(common.HeatTestCase): def setUp(self): @@ -515,3 +528,118 @@ class NeutronQoSMinimumBandwidthRuleTest(common.HeatTestCase): self.neutronclient.show_minimum_bandwidth_rule.assert_called_once_with( self.minimum_bandwidth_rule.resource_id, self.policy_id) + + +class NeutronQoSMinimumPacketRateRuleTest(common.HeatTestCase): + def setUp(self): + super(NeutronQoSMinimumPacketRateRuleTest, self).setUp() + + self.ctx = utils.dummy_context() + tpl = template_format.parse(minimum_packet_rate_rule_template) + self.stack = stack.Stack( + self.ctx, + 'neutron_minimum_packet_rate_rule_test', + template.Template(tpl) + ) + + self.neutronclient = mock.MagicMock() + self.patchobject(neutron.NeutronClientPlugin, 'has_extension', + return_value=True) + self.minimum_packet_rate_rule = self.stack[ + 'my_minimum_packet_rate_rule'] + self.minimum_packet_rate_rule.client = mock.MagicMock( + return_value=self.neutronclient) + self.find_mock = self.patchobject( + neutron.neutronV20, + 'find_resourceid_by_name_or_id') + self.policy_id = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + self.find_mock.return_value = self.policy_id + + def test_rule_handle_create(self): + rule = { + 'minimum_packet_rate_rule': { + 'id': 'cf0eab12-ef8b-4a62-98d0-70576583c17a', + 'min_kpps': 1000, + 'direction': 'any', + 'tenant_id': 'd66c74c01d6c41b9846088c1ad9634d0' + } + } + + create_props = {'min_kpps': 1000, + 'direction': 'any', + 'tenant_id': 'd66c74c01d6c41b9846088c1ad9634d0'} + self.neutronclient.create_minimum_packet_rate_rule.return_value = rule + + self.minimum_packet_rate_rule.handle_create() + self.assertEqual('cf0eab12-ef8b-4a62-98d0-70576583c17a', + self.minimum_packet_rate_rule.resource_id) + self.neutronclient.create_minimum_packet_rate_rule.\ + assert_called_once_with( + self.policy_id, + {'minimum_packet_rate_rule': create_props}) + + def test_rule_handle_delete(self): + rule_id = 'cf0eab12-ef8b-4a62-98d0-70576583c17a' + self.minimum_packet_rate_rule.resource_id = rule_id + self.neutronclient.delete_minimum_packet_rate_rule.return_value = None + + self.assertIsNone(self.minimum_packet_rate_rule.handle_delete()) + self.neutronclient.delete_minimum_packet_rate_rule.\ + assert_called_once_with(rule_id, self.policy_id) + + def test_rule_handle_delete_not_found(self): + rule_id = 'cf0eab12-ef8b-4a62-98d0-70576583c17a' + self.minimum_packet_rate_rule.resource_id = rule_id + not_found = self.neutronclient.NotFound + self.neutronclient.delete_minimum_packet_rate_rule.side_effect =\ + not_found + + self.assertIsNone(self.minimum_packet_rate_rule.handle_delete()) + self.neutronclient.delete_minimum_packet_rate_rule.\ + assert_called_once_with(rule_id, self.policy_id) + + def test_rule_handle_delete_resource_id_is_none(self): + self.minimum_packet_rate_rule.resource_id = None + self.assertIsNone(self.minimum_packet_rate_rule.handle_delete()) + self.assertEqual( + 0, + self.neutronclient.minimum_packet_rate_rule.call_count) + + def test_rule_handle_update(self): + rule_id = 'cf0eab12-ef8b-4a62-98d0-70576583c17a' + self.minimum_packet_rate_rule.resource_id = rule_id + + prop_diff = { + 'min_kpps': 500 + } + + self.minimum_packet_rate_rule.handle_update( + json_snippet={}, + tmpl_diff={}, + prop_diff=prop_diff.copy()) + + self.neutronclient.update_minimum_packet_rate_rule.\ + assert_called_once_with( + rule_id, + self.policy_id, + {'minimum_packet_rate_rule': prop_diff}) + + def test_rule_get_attr(self): + self.minimum_packet_rate_rule.resource_id = 'test rule' + rule = { + 'minimum_packet_rate_rule': { + 'id': 'cf0eab12-ef8b-4a62-98d0-70576583c17a', + 'min_kpps': 1000, + 'direction': 'egress', + 'tenant_id': 'd66c74c01d6c41b9846088c1ad9634d0' + } + } + self.neutronclient.show_minimum_packet_rate_rule.return_value = rule + + self.assertEqual(rule['minimum_packet_rate_rule'], + self.minimum_packet_rate_rule.FnGetAtt('show')) + + self.neutronclient.show_minimum_packet_rate_rule.\ + assert_called_once_with( + self.minimum_packet_rate_rule.resource_id, + self.policy_id) diff --git a/heat/tests/test_common_pluginutils.py b/heat/tests/test_common_pluginutils.py new file mode 100644 index 000000000..7a6264543 --- /dev/null +++ b/heat/tests/test_common_pluginutils.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import importlib.metadata as importlib_metadata +from unittest import mock + +from heat.common import pluginutils +from heat.tests import common + + +class TestPluginUtil(common.HeatTestCase): + + def test_log_fail_msg(self): + ep = importlib_metadata.EntryPoint( + name=None, group=None, + value='package.module:attr [extra1, extra2]') + + exc = Exception('Something went wrong') + pluginutils.log_fail_msg(mock.Mock(), ep, exc) + self.assertIn("Something went wrong", self.LOG.output) diff --git a/releasenotes/notes/neutron-qos-minimum-packet-rate-rule-e58e9ced636320f1.yaml b/releasenotes/notes/neutron-qos-minimum-packet-rate-rule-e58e9ced636320f1.yaml new file mode 100644 index 000000000..2e8af66ee --- /dev/null +++ b/releasenotes/notes/neutron-qos-minimum-packet-rate-rule-e58e9ced636320f1.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added ``OS::Neutron::QoSMinimumPacketRateRule`` resource to support + ``minimum_packet_rate_rule`` in Neutron QoS. This resource depends + on Neutron API extension ``qos-pps-minimum`` and according + to the default policy it is admin-only. diff --git a/requirements.txt b/requirements.txt index a6a85292a..457724754 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,7 +50,7 @@ python-magnumclient>=2.3.0 # Apache-2.0 python-manilaclient>=1.16.0 # Apache-2.0 python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 python-monascaclient>=1.12.0 # Apache-2.0 -python-neutronclient>=6.14.0 # Apache-2.0 +python-neutronclient>=7.7.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 python-octaviaclient>=1.8.0 # Apache-2.0 python-openstackclient>=3.12.0 # Apache-2.0 diff --git a/roles/run-heat-tests/defaults/main.yaml b/roles/run-heat-tests/defaults/main.yaml index b601d49d9..22ce4a490 100644 --- a/roles/run-heat-tests/defaults/main.yaml +++ b/roles/run-heat-tests/defaults/main.yaml @@ -1,2 +1,5 @@ devstack_base_dir: /opt/stack tempest_test_timeout: '' +tempest_tox_environment: {} +heat_tempest_plugin: /opt/stack/heat-tempest-plugin +constraints_file: /opt/stack/requirements/upper-constraints.txt diff --git a/roles/run-heat-tests/tasks/main.yaml b/roles/run-heat-tests/tasks/main.yaml index 75122f2a1..4af1d0e03 100644 --- a/roles/run-heat-tests/tasks/main.yaml +++ b/roles/run-heat-tests/tasks/main.yaml @@ -1,3 +1,23 @@ +- name: Set OS_TEST_TIMEOUT if requested + set_fact: + tempest_tox_environment: "{{ tempest_tox_environment | combine({'OS_TEST_TIMEOUT': tempest_test_timeout}) }}" + when: tempest_test_timeout != '' + +- name: Set TOX_CONSTRAINTS_FILE + set_fact: + tempest_tox_environment: "{{ tempest_tox_environment | combine({'UPPER_CONSTRAINTS_FILE': constraints_file}) | combine({'TOX_CONSTRAINTS_FILE': constraints_file}) }}" + +- name: Allow git to read plugin directories + become: true + command: git config --system --add safe.directory {{heat_tempest_plugin}} + +- name: Install plugins + command: tox -evenv-tempest -- pip install -c{{constraints_file}} {{heat_tempest_plugin}} + become: true + args: + chdir: "{{devstack_base_dir}}/tempest" + environment: "{{ tempest_tox_environment }}" + - name: Run heat tests command: tox -evenv-tempest -- stestr --test-path={{devstack_base_dir}}/heat/heat_integrationtests \ --top-dir={{devstack_base_dir}}/heat \ @@ -5,5 +25,4 @@ args: chdir: "{{devstack_base_dir}}/tempest" become: true - become_user: tempest - environment: '{{ {"OS_TEST_TIMEOUT": tempest_test_timeout} if tempest_test_timeout else {} }}' + environment: "{{ tempest_tox_environment }}" @@ -1,7 +1,7 @@ [tox] envlist = py38,py39,pep8 ignore_basepython_conflict = True -minversion = 3.1.0 +minversion = 3.18.0 skipsdist = True [testenv] @@ -55,7 +55,7 @@ commands = coverage report [testenv:docs] -whitelist_externals = +allowlist_externals = rm deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} @@ -66,7 +66,7 @@ commands = [testenv:pdf-docs] deps = {[testenv:docs]deps} -whitelist_externals = +allowlist_externals = make commands = sphinx-build -W -b latex doc/source doc/build/pdf @@ -76,7 +76,7 @@ commands = # This environment is called from CI scripts to test and publish # the API Ref to docs.openstack.org. deps = -r{toxinidir}/doc/requirements.txt -whitelist_externals = rm +allowlist_externals = rm commands = rm -rf api-ref/build sphinx-build -W --keep-going -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html @@ -136,7 +136,7 @@ paths = ./heat/hacking commands = oslo_debug_helper {posargs} [testenv:releasenotes] -whitelist_externals = +allowlist_externals = rm deps = -r{toxinidir}/doc/requirements.txt commands = |