From 55cb9a9f538a39ee367c0f21425654241d367096 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Mon, 21 Oct 2024 18:38:46 -0700 Subject: [PATCH 01/31] Update gate jobs as per the 2025.1 cycle testing runtime As per 2025.1 testing runtime[1], we need to test on Ubuntu Noble (which will be taken care by depends-on tempest and devstack patches to move base jobs to Noble) and at least single job to run on Ubuntu Jammy (for smooth upgrade from previous releases). This commit adds a new job to run on Jammy which can be removed in future cycle when testing runtime test next version of Ubuntu as default. Depends-On: https://review.opendev.org/c/openstack/blazar-tempest-plugin/+/932947 [1] https://governance.openstack.org/tc/reference/runtimes/2025.1.html Change-Id: I539b02f54e1207e1e9a0109f50aa89ac15b74fa4 --- .zuul.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.zuul.yaml b/.zuul.yaml index 755c6f6e6..214105d1c 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -9,10 +9,12 @@ check: jobs: - blazar-tempest-plugin-base + - blazar-tempest-plugin-jammy - blazar-tempest-plugin-ipv6-only - openstack-tox-pylint: voting: false gate: jobs: - blazar-tempest-plugin-base + - blazar-tempest-plugin-jammy - blazar-tempest-plugin-ipv6-only From 0db9a32e70ba3805d48d0b5e1122bc0879a65401 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Sun, 10 Nov 2024 21:38:05 -0800 Subject: [PATCH 02/31] Remove default override for config options policy_file olso.policy 4.5.0[1] changed the config options policy_file default value to 'policy.yaml', which means it is changed for all the OpenStack services and they do not need to override the default anymore. NOTE: There is no change in behaviour here, oslo.policy provides the same configuration that services have overridden till now. [1] https://review.opendev.org/c/openstack/releases/+/934012 [2] https://review.opendev.org/c/openstack/requirements/+/934295 Change-Id: I4783f751c8ec775e862f378dc0b212a416198c38 --- blazar/config.py | 16 ---------------- blazar/policy.py | 8 -------- requirements.txt | 2 +- setup.cfg | 2 -- 4 files changed, 1 insertion(+), 27 deletions(-) diff --git a/blazar/config.py b/blazar/config.py index 621dc8927..67a4f5041 100644 --- a/blazar/config.py +++ b/blazar/config.py @@ -16,7 +16,6 @@ from oslo_config import cfg from oslo_log import log as logging -from oslo_policy import opts cli_opts = [ @@ -91,18 +90,3 @@ CONF.register_opts(api_opts) CONF.register_opts(lease_opts) logging.register_options(cfg.CONF) - - -def set_lib_defaults(): - """Update default value for configuration options from other namespace. - - Example, oslo lib config options. This is needed for - config generator tool to pick these default value changes. - https://docs.openstack.org/oslo.config/latest/cli/ - generator.html#modifying-defaults-from-other-namespaces - """ - - # TODO(gmann): Remove setting the default value of config policy_file - # once oslo_policy change the default value to 'policy.yaml'. - # https://github.com/openstack/oslo.policy/blob/a626ad12fe5a3abd49d70e3e5b95589d279ab578/oslo_policy/opts.py#L49 - opts.set_defaults(CONF, 'policy.yaml') diff --git a/blazar/policy.py b/blazar/policy.py index a4e2f0c58..7d37f3fb1 100644 --- a/blazar/policy.py +++ b/blazar/policy.py @@ -19,7 +19,6 @@ from oslo_config import cfg from oslo_log import log as logging -from oslo_policy import opts from oslo_policy import policy from blazar import context @@ -30,13 +29,6 @@ LOG = logging.getLogger(__name__) -# TODO(gmann): Remove setting the default value of config policy_file -# once oslo_policy change the default value to 'policy.yaml'. -# https://github.com/openstack/oslo.policy/blob/a626ad12fe5a3abd49d70e3e5b95589d279ab578/oslo_policy/opts.py#L49 -DEFAULT_POLICY_FILE = 'policy.yaml' -opts.set_defaults(CONF, DEFAULT_POLICY_FILE) - - _ENFORCER = None diff --git a/requirements.txt b/requirements.txt index ab3de6491..d1321be63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ oslo.i18n>=3.15.3 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0 oslo.messaging>=5.29.0 # Apache-2.0 oslo.middleware>=3.31.0 # Apache-2.0 -oslo.policy>=3.6.0 # Apache-2.0 +oslo.policy>=4.5.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.service>=1.34.0 # Apache-2.0 oslo.upgradecheck>=1.3.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 98bda8baf..81ed03ea2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,8 +54,6 @@ blazar.api.v2.controllers.extensions = oslo.config.opts = blazar = blazar.opts:list_opts -oslo.config.opts.defaults = - blazar = blazar.config:set_lib_defaults oslo.policy.policies = blazar = blazar.policies:list_rules From 99d93c8a97bc4c5fa91fced0668f0d0114420fdf Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Wed, 13 Nov 2024 10:40:22 +0000 Subject: [PATCH 03/31] reno: Update master for unmaintained/2023.1 Update the 2023.1 release notes configuration to build from unmaintained/2023.1. Change-Id: Ie1e38bc85f247ec2a97d391104bdfc852e067f23 --- releasenotes/source/2023.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst index d1238479b..2c9a36fae 100644 --- a/releasenotes/source/2023.1.rst +++ b/releasenotes/source/2023.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2023.1 + :branch: unmaintained/2023.1 From 5df5277756a6d832bcdf0107ce9df859ed2a1b03 Mon Sep 17 00:00:00 2001 From: Takashi Natsume Date: Sat, 28 Sep 2024 20:45:53 +0900 Subject: [PATCH 04/31] Replace deprecated datetime.utcnow() The datetime.utcnow() is deprecated in Python 3.12. Replace datetime.utcnow() with oslo_utils.timeutils.utcnow(). Change-Id: I4d3382b142c1c9225cad29f9e33abf539ecf0f30 Signed-off-by: Takashi Natsume --- blazar/manager/service.py | 7 +- .../plugins/floatingips/floatingip_plugin.py | 3 +- blazar/plugins/instances/instance_plugin.py | 6 +- blazar/plugins/oshosts/host_plugin.py | 9 +- .../db/sqlalchemy/test_sqlalchemy_api.py | 5 +- .../filters/test_max_lease_duration_filter.py | 24 ++- blazar/tests/enforcement/test_enforcement.py | 6 +- blazar/tests/manager/test_service.py | 143 ++++++------------ .../plugins/instances/test_instance_plugin.py | 41 ++--- .../oshosts/test_physical_host_plugin.py | 25 ++- requirements.txt | 2 +- 11 files changed, 108 insertions(+), 163 deletions(-) diff --git a/blazar/manager/service.py b/blazar/manager/service.py index d19ac59f6..69b3bfc30 100644 --- a/blazar/manager/service.py +++ b/blazar/manager/service.py @@ -21,6 +21,7 @@ from oslo_config import cfg from oslo_log import log as logging from oslo_utils.excutils import save_and_reraise_exception +from oslo_utils import timeutils from stevedore import enabled from blazar import context @@ -236,7 +237,7 @@ def _process_events(self): sort_dir='asc', filters={'status': status.event.UNDONE, 'time': {'op': 'le', - 'border': datetime.datetime.utcnow()}} + 'border': timeutils.utcnow()}} ) for batch in self._select_for_execution(events): @@ -252,7 +253,7 @@ def _exec_event(self, event): try: event_fn(lease_id=event['lease_id'], event_id=event['id']) except common_ex.InvalidStatus: - now = datetime.datetime.utcnow() + now = timeutils.utcnow() if now < event['time'] + datetime.timedelta( seconds=CONF.manager.event_max_retries * 10): # Set the event status UNDONE for retrying the event @@ -284,7 +285,7 @@ def _date_from_string(self, date_string, date_format=LEASE_DATE_FORMAT): return date def _parse_lease_dates(self, start_date, end_date): - now = datetime.datetime.utcnow() + now = timeutils.utcnow() now = datetime.datetime(now.year, now.month, now.day, diff --git a/blazar/plugins/floatingips/floatingip_plugin.py b/blazar/plugins/floatingips/floatingip_plugin.py index 2f3ce9cad..8f3ec1027 100644 --- a/blazar/plugins/floatingips/floatingip_plugin.py +++ b/blazar/plugins/floatingips/floatingip_plugin.py @@ -19,6 +19,7 @@ from oslo_utils.excutils import save_and_reraise_exception from oslo_utils import netutils from oslo_utils import strutils +from oslo_utils import timeutils from blazar import context from blazar.db import api as db_api @@ -422,7 +423,7 @@ def query_fip_allocations(self, fips, detail=None, lease_id=None, ] }. """ - start = datetime.datetime.utcnow() + start = timeutils.utcnow() end = datetime.date.max reservations = db_utils.get_reservation_allocations_by_fip_ids( diff --git a/blazar/plugins/instances/instance_plugin.py b/blazar/plugins/instances/instance_plugin.py index ab51841cd..882ca51cf 100644 --- a/blazar/plugins/instances/instance_plugin.py +++ b/blazar/plugins/instances/instance_plugin.py @@ -21,6 +21,7 @@ from oslo_log import log as logging from oslo_utils import strutils from oslo_utils.strutils import bool_from_string +from oslo_utils import timeutils from blazar import context from blazar.db import api as db_api @@ -167,7 +168,7 @@ def query_allocations(self, hosts, lease_id=None, reservation_id=None): ] }. """ - start = datetime.datetime.utcnow() + start = timeutils.utcnow() end = datetime.date.max # To reduce overhead, this method only executes one query @@ -770,8 +771,7 @@ def _heal_reservation(self, reservation, host_ids): def _select_host(self, reservation, lease): """Returns the alternative host id or None if not found.""" values = {} - values['start_date'] = max(datetime.datetime.utcnow(), - lease['start_date']) + values['start_date'] = max(timeutils.utcnow(), lease['start_date']) values['end_date'] = lease['end_date'] specs = ['vcpus', 'memory_mb', 'disk_gb', 'affinity', 'amount', 'resource_properties'] diff --git a/blazar/plugins/oshosts/host_plugin.py b/blazar/plugins/oshosts/host_plugin.py index cd28f88e9..2bf5d2275 100644 --- a/blazar/plugins/oshosts/host_plugin.py +++ b/blazar/plugins/oshosts/host_plugin.py @@ -22,6 +22,7 @@ from oslo_config import cfg from oslo_log import log as logging from oslo_utils import strutils +from oslo_utils import timeutils from blazar.db import api as db_api from blazar.db import exceptions as db_ex @@ -304,7 +305,7 @@ def _reallocate(self, allocation): host['service_name']) # Allocate an alternative host. - start_date = max(datetime.datetime.utcnow(), lease['start_date']) + start_date = max(timeutils.utcnow(), lease['start_date']) new_hostids = self._matching_hosts( reservation['hypervisor_properties'], reservation['resource_properties'], @@ -457,7 +458,7 @@ def create_computehost(self, host_values): def is_updatable_extra_capability(self, capability, property_name): reservations = db_utils.get_reservations_by_host_id( - capability['computehost_id'], datetime.datetime.utcnow(), + capability['computehost_id'], timeutils.utcnow(), datetime.date.max) for r in reservations: @@ -588,7 +589,7 @@ def query_allocations(self, hosts, lease_id=None, reservation_id=None): ] }. """ - start = datetime.datetime.utcnow() + start = timeutils.utcnow() end = datetime.date.max # To reduce overhead, this method only executes one query @@ -969,7 +970,7 @@ def heal(self): reservation_flags = {} hosts = db_api.unreservable_host_get_all_by_queries([]) - interval_begin = datetime.datetime.utcnow() + interval_begin = timeutils.utcnow() interval = self.get_healing_interval() if interval == 0: interval_end = datetime.date.max diff --git a/blazar/tests/db/sqlalchemy/test_sqlalchemy_api.py b/blazar/tests/db/sqlalchemy/test_sqlalchemy_api.py index 05ade5384..d8c21ff4e 100644 --- a/blazar/tests/db/sqlalchemy/test_sqlalchemy_api.py +++ b/blazar/tests/db/sqlalchemy/test_sqlalchemy_api.py @@ -16,6 +16,7 @@ import datetime import operator +from oslo_utils import timeutils from oslo_utils import uuidutils from blazar.db import exceptions as db_exceptions @@ -964,12 +965,12 @@ def test_event_get_sorted_asc_by_event_type_filter(self): db_api.event_create(_get_fake_event_values( id='1', event_type=fake_event_type, - time=datetime.datetime.utcnow() + time=timeutils.utcnow() )) db_api.event_create(_get_fake_event_values( id='2', event_type=fake_event_type, - time=datetime.datetime.utcnow() + time=timeutils.utcnow() )) filtered_events = db_api.event_get_all_sorted_by_filters( diff --git a/blazar/tests/enforcement/filters/test_max_lease_duration_filter.py b/blazar/tests/enforcement/filters/test_max_lease_duration_filter.py index 35e321e72..ab8eb20cc 100755 --- a/blazar/tests/enforcement/filters/test_max_lease_duration_filter.py +++ b/blazar/tests/enforcement/filters/test_max_lease_duration_filter.py @@ -17,6 +17,8 @@ from unittest import mock import ddt +from oslo_config import cfg +from oslo_utils import timeutils from blazar import context from blazar import enforcement @@ -24,8 +26,6 @@ from blazar.enforcement import filters from blazar import tests -from oslo_config import cfg - def get_fake_host(host_id): return { @@ -158,9 +158,8 @@ def test_check_update_allowed(self): del new_lease_values['reservations'] ctx = context.current() - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime(2014, 1, 1, 1, 1) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2014, 1, 1, 1, 1) self.enforcement.check_update( ctx, lease, new_lease_values, current_allocations, allocation_candidates, reservations, new_reservations) @@ -178,9 +177,8 @@ def test_check_update_denied(self): del new_lease_values['reservations'] ctx = context.current() - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime(2014, 1, 1, 1, 1) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2014, 1, 1, 1, 1) self.assertRaises(exceptions.MaxLeaseDurationException, self.enforcement.check_update, ctx, lease, new_lease_values, current_allocations, @@ -199,9 +197,8 @@ def test_check_update_active_lease_allowed(self): del new_lease_values['reservations'] ctx = context.current() - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime(2014, 1, 1, 1, 50) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2014, 1, 1, 1, 50) self.enforcement.check_update( ctx, lease, new_lease_values, current_allocations, allocation_candidates, reservations, new_reservations) @@ -235,9 +232,8 @@ def test_check_update_exempt(self): del new_lease_values['reservations'] ctx = context.current() - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime(2014, 1, 1, 1, 1) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2014, 1, 1, 1, 1) self.enforcement.check_update( ctx, lease, new_lease_values, current_allocations, allocation_candidates, reservations, new_reservations) diff --git a/blazar/tests/enforcement/test_enforcement.py b/blazar/tests/enforcement/test_enforcement.py index 08208abde..531387651 100755 --- a/blazar/tests/enforcement/test_enforcement.py +++ b/blazar/tests/enforcement/test_enforcement.py @@ -16,6 +16,8 @@ import datetime import ddt +from oslo_utils import timeutils + from blazar import context from blazar import enforcement from blazar.enforcement import filters @@ -44,10 +46,10 @@ def get_fake_lease(**kwargs): fake_lease = { 'id': '1', 'name': 'lease_test', - 'start_date': datetime.datetime.utcnow().strftime( + 'start_date': timeutils.utcnow().strftime( service.LEASE_DATE_FORMAT), 'end_date': ( - datetime.datetime.utcnow() + datetime.timedelta(days=1)).strftime( + timeutils.utcnow() + datetime.timedelta(days=1)).strftime( service.LEASE_DATE_FORMAT), 'user_id': '111', 'project_id': '222', diff --git a/blazar/tests/manager/test_service.py b/blazar/tests/manager/test_service.py index 7a6a5769a..2effcc07e 100644 --- a/blazar/tests/manager/test_service.py +++ b/blazar/tests/manager/test_service.py @@ -22,6 +22,7 @@ import importlib from oslo_config import cfg import oslo_messaging as messaging +from oslo_utils import timeutils from stevedore import enabled import testtools @@ -424,10 +425,9 @@ def test_exec_event_retry(self): start_lease.side_effect = exceptions.InvalidStatus event_update = self.patch(self.db_api, 'event_update') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = (self.good_date - + datetime.timedelta(seconds=1)) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = (self.good_date + datetime.timedelta( + seconds=1)) self.manager._exec_event(event) start_lease.assert_called_once_with(lease_id=event['lease_id'], @@ -445,10 +445,9 @@ def test_exec_event_no_more_retry(self): start_lease.side_effect = exceptions.InvalidStatus event_update = self.patch(self.db_api, 'event_update') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = (self.good_date - + datetime.timedelta(days=1)) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = (self.good_date + datetime.timedelta( + days=1)) self.manager._exec_event(event) start_lease.assert_called_once_with(lease_id=event['lease_id'], @@ -701,7 +700,7 @@ def test_create_lease_wrong_format_before_end_date(self): def test_create_lease_start_date_in_past(self): lease_values = self.lease_values.copy() lease_values['start_date'] = datetime.datetime.strftime( - datetime.datetime.utcnow() - datetime.timedelta(days=1), + timeutils.utcnow() - datetime.timedelta(days=1), service.LEASE_DATE_FORMAT) self.assertRaises( @@ -782,10 +781,8 @@ def test_create_lease_with_filter_exception(self): def test_update_lease_completed_lease_rename(self): lease_values = {'name': 'renamed'} target = datetime.datetime(2015, 1, 1) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target lease = self.manager.update_lease(lease_id=self.lease_id, values=lease_values) self.lease_update.assert_called_once_with(self.lease_id, lease_values) @@ -819,10 +816,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.manager.update_lease(lease_id=self.lease_id, values=lease_values) self.fake_plugin.update_reservation.assert_called_with( @@ -875,10 +870,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.manager.update_lease(lease_id=self.lease_id, values=lease_values) self.fake_plugin.update_reservation.assert_called_with( @@ -922,10 +915,8 @@ def test_update_modify_reservations_with_invalid_param(self): } ] target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( manager_ex.CantUpdateParameter, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -947,10 +938,8 @@ def test_update_modify_reservations_without_reservation_id(self): } ] target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( manager_ex.MissingParameter, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -979,10 +968,8 @@ def test_update_reservations_with_invalid_reservation_id(self, } ] target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( exceptions.InvalidInput, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1013,10 +1000,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 20, 14, 00) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.manager.update_lease(lease_id=self.lease_id, values=lease_values) self.fake_plugin.update_reservation.assert_called_with( @@ -1063,10 +1048,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 20, 14, 00) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.manager.update_lease(lease_id=self.lease_id, values=lease_values) self.fake_plugin.update_reservation.assert_called_with( @@ -1128,10 +1111,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 20, 14, 00) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.manager.update_lease(lease_id=self.lease_id, values=lease_values) self.fake_plugin.update_reservation.assert_called_with( @@ -1198,10 +1179,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 20, 14, 00) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( exceptions.NotAuthorized, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1238,10 +1217,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 20, 14, 00) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( exceptions.NotAuthorized, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1277,10 +1254,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 20, 14, 00) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( manager_ex.InvalidDate, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1298,10 +1273,8 @@ def test_update_lease_started_modify_start_date(self): 'start_date': '2013-12-20 16:00' } target = datetime.datetime(2013, 12, 20, 14, 00) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( exceptions.InvalidInput, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1312,10 +1285,8 @@ def test_update_lease_not_started_start_date_before_current_time(self): 'start_date': '2013-12-14 13:00' } target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( exceptions.InvalidInput, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1326,10 +1297,8 @@ def test_update_lease_end_date_before_current_time(self): 'end_date': '2013-12-14 13:00' } target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( exceptions.InvalidInput, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1340,10 +1309,8 @@ def test_update_lease_completed_modify_dates(self): 'end_date': '2013-12-15 20:00' } target = datetime.datetime(2015, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( exceptions.InvalidInput, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1378,10 +1345,8 @@ def fake_event_get(sort_key, sort_dir, filters): 'end_date': '2013-12-25 20:00' } target = datetime.datetime(2013, 12, 10) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises(exceptions.BlazarException, self.manager.update_lease, lease_id=self.lease_id, values=lease_values) @@ -1426,10 +1391,8 @@ def fake_event_get(sort_key, sort_dir, filters): event_get = self.patch(db_api, 'event_get_first_sorted_by_filters') event_get.side_effect = fake_event_get target = datetime.datetime(2013, 12, 15) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises(exceptions.NotAuthorized, self.manager.update_lease, @@ -1709,10 +1672,8 @@ def test_update_non_fatal_max_lease_duration_exception(self): 'prolong_for': '8d' } target = datetime.datetime(2013, 12, 14) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( enforcement_ex.MaxLeaseDurationException, manager.update_lease, @@ -1743,10 +1704,8 @@ def test_update_non_fatal_external_service_filter_exception(self): 'prolong_for': '8d' } target = datetime.datetime(2013, 12, 14) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( enforcement.exceptions.ExternalServiceFilterException, manager.update_lease, @@ -1777,10 +1736,8 @@ def test_update_fatal_extra_capability_too_long_exception(self): 'prolong_for': '8d' } target = datetime.datetime(2013, 12, 14) - with mock.patch.object(datetime, - 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = target + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = target self.assertRaises( manager_ex.ExtraCapabilityTooLong, manager.update_lease, diff --git a/blazar/tests/plugins/instances/test_instance_plugin.py b/blazar/tests/plugins/instances/test_instance_plugin.py index ade9ddbc4..6818b3561 100644 --- a/blazar/tests/plugins/instances/test_instance_plugin.py +++ b/blazar/tests/plugins/instances/test_instance_plugin.py @@ -19,6 +19,9 @@ import ddt from novaclient import exceptions as nova_exceptions +from oslo_config import cfg +from oslo_config import fixture as conf_fixture +from oslo_utils import timeutils from blazar import context from blazar.db import api as db_api @@ -29,8 +32,6 @@ from blazar.plugins import oshosts from blazar import tests from blazar.utils.openstack import nova -from oslo_config import cfg -from oslo_config import fixture as conf_fixture CONF = cfg.CONF @@ -1417,10 +1418,8 @@ def test_reallocate_before_start(self): pickup_hosts.return_value = {'added': [new_host['id']], 'removed': []} alloc_update = self.patch(db_api, 'host_allocation_update') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( - 2020, 1, 1, 11, 00) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2020, 1, 1, 11, 00) result = plugin._heal_reservation( dummy_reservation, list(failed_host.values())) @@ -1476,10 +1475,8 @@ def test_reallocate_active(self): mock_update_reservation_inventory = self.patch( plugin.placement_client, 'update_reservation_inventory') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( - 2020, 1, 1, 13, 00) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2020, 1, 1, 13, 00) result = plugin._heal_reservation( dummy_reservation, list(failed_host.values())) @@ -1533,10 +1530,8 @@ def test_reallocate_missing_resources(self): pickup_hosts.side_effect = mgr_exceptions.NotEnoughHostsAvailable alloc_destroy = self.patch(db_api, 'host_allocation_destroy') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( - 2020, 1, 1, 11, 00) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2020, 1, 1, 11, 00) result = plugin._heal_reservation( dummy_reservation, list(failed_host.values())) @@ -1579,10 +1574,8 @@ def test_reallocate_before_start_affinity(self): pickup_hosts.return_value = {'added': [new_host['id']], 'removed': []} alloc_update = self.patch(db_api, 'host_allocation_update') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( - 2020, 1, 1, 11, 00) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2020, 1, 1, 11, 00) result = plugin._heal_reservation( dummy_reservation, list(failed_host.values())) @@ -1642,10 +1635,8 @@ def test_reallocate_active_affinity(self): mock_update_reservation_inventory = self.patch( plugin.placement_client, 'update_reservation_inventory') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( - 2020, 1, 1, 13, 00) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2020, 1, 1, 13, 00) result = plugin._heal_reservation( dummy_reservation, list(failed_host.values())) @@ -1703,10 +1694,8 @@ def test_reallocate_missing_resources_with_affinity(self): pickup_hosts.side_effect = mgr_exceptions.NotEnoughHostsAvailable alloc_destroy = self.patch(db_api, 'host_allocation_destroy') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( - 2020, 1, 1, 11, 00) + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime(2020, 1, 1, 11, 00) result = plugin._heal_reservation( dummy_reservation, list(failed_host.values())) diff --git a/blazar/tests/plugins/oshosts/test_physical_host_plugin.py b/blazar/tests/plugins/oshosts/test_physical_host_plugin.py index e3441fb73..86ebf5106 100644 --- a/blazar/tests/plugins/oshosts/test_physical_host_plugin.py +++ b/blazar/tests/plugins/oshosts/test_physical_host_plugin.py @@ -15,6 +15,7 @@ import collections import datetime +import random from unittest import mock import ddt @@ -22,7 +23,7 @@ from novaclient import exceptions as nova_exceptions from oslo_config import cfg from oslo_config import fixture as conf_fixture -import random +from oslo_utils import timeutils import testtools from blazar import context @@ -908,7 +909,7 @@ def test_get_allocations_with_invalid_host(self): self.assertDictEqual(expected, ret) def test_create_reservation_no_hosts_available(self): - now = datetime.datetime.utcnow() + now = timeutils.utcnow() values = { 'lease_id': '018c1b43-e69e-4aef-a543-09681539cf4c', 'min': 1, @@ -2215,9 +2216,8 @@ def test_reallocate_before_start(self): matching_hosts.return_value = [new_host['id']] alloc_update = self.patch(self.db_api, 'host_allocation_update') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime( 2020, 1, 1, 11, 00) result = self.fake_phys_plugin._reallocate(dummy_allocation) @@ -2271,9 +2271,8 @@ def test_reallocate_active(self): matching_hosts.return_value = [new_host['id']] alloc_update = self.patch(self.db_api, 'host_allocation_update') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime( 2020, 1, 1, 13, 00) result = self.fake_phys_plugin._reallocate(dummy_allocation) @@ -2329,9 +2328,8 @@ def test_reallocate_missing_resources(self): matching_hosts.return_value = [] alloc_destroy = self.patch(self.db_api, 'host_allocation_destroy') - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = datetime.datetime( + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = datetime.datetime( 2020, 1, 1, 11, 00) result = self.fake_phys_plugin._reallocate(dummy_allocation) @@ -2902,9 +2900,8 @@ def test_heal(self): self.host_monitor_plugin.healing_handlers = [healing_handler] start_date = datetime.datetime(2020, 1, 1, 12, 00) - with mock.patch.object(datetime, 'datetime', - mock.Mock(wraps=datetime.datetime)) as patched: - patched.utcnow.return_value = start_date + with mock.patch.object(timeutils, 'utcnow') as patched: + patched.return_value = start_date result = self.host_monitor_plugin.heal() healing_handler.assert_called_once_with( diff --git a/requirements.txt b/requirements.txt index d1321be63..1dd9d5e38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ oslo.policy>=4.5.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.service>=1.34.0 # Apache-2.0 oslo.upgradecheck>=1.3.0 # Apache-2.0 -oslo.utils>=4.5.0 # Apache-2.0 +oslo.utils>=7.0.0 # Apache-2.0 python-neutronclient>=6.0.0 # Apache-2.0 python-novaclient>=9.1.0 # Apache-2.0 netaddr>=0.7.18 # BSD From 7ed419fbf71b8851227c31331c31feff360ad9f3 Mon Sep 17 00:00:00 2001 From: sd109 Date: Fri, 6 Dec 2024 10:05:16 +0000 Subject: [PATCH 05/31] Support Nova ephemeral disks in flavor-based reservations Change-Id: Ia0ef7ba891bfbdbfcd2abb3c46df26f34899435f --- blazar/plugins/flavor/flavor_plugin.py | 1 + blazar/tests/plugins/flavor/test_flavor_plugin.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/blazar/plugins/flavor/flavor_plugin.py b/blazar/plugins/flavor/flavor_plugin.py index b80f26cea..bddb00b7b 100644 --- a/blazar/plugins/flavor/flavor_plugin.py +++ b/blazar/plugins/flavor/flavor_plugin.py @@ -391,6 +391,7 @@ def _create_flavor(self, instance_reservation): 'vcpus': source_flavor['vcpus'], 'ram': source_flavor['ram'], 'disk': source_flavor['disk'], + 'ephemeral': source_flavor.get('OS-FLV-EXT-DATA:ephemeral', 0), 'is_public': False } # create flavor using admin access diff --git a/blazar/tests/plugins/flavor/test_flavor_plugin.py b/blazar/tests/plugins/flavor/test_flavor_plugin.py index 20194480e..7ae4067cb 100644 --- a/blazar/tests/plugins/flavor/test_flavor_plugin.py +++ b/blazar/tests/plugins/flavor/test_flavor_plugin.py @@ -347,7 +347,7 @@ def test_create_flavor(self, mock_create): }) mock_create.assert_called_once_with( flavorid='12345', name='reservation:12345', vcpus=2, ram=1024, - disk=10, is_public=False) + disk=10, ephemeral=100, is_public=False) def test__query_available_hosts(self): get_reservations = self.patch(db_utils, From 92cba0c60676644fe2fda34c288858b85c2b4967 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Tue, 28 Jan 2025 19:41:21 -0800 Subject: [PATCH 06/31] Add secure RBAC role as new default This add new RBAC defaults in the blazar API policy. There is no change in the admin policy except they are scoped to the 'project'. Adding project reader role in the read APIs which continue to be allow by the member and admin role. Change-Id: I3246638e03ab7ddae366b4423b9397db7fe6051e --- blazar/policies/base.py | 41 ++++++++++++++++++- blazar/policies/floatingips.py | 11 +++-- blazar/policies/leases.py | 20 +++++---- blazar/policies/oshosts.py | 21 ++++++---- blazar/tests/api/__init__.py | 4 +- blazar/tests/test_policy.py | 2 +- ...secure-rbac-defaults-2c46da839de3d59c.yaml | 17 ++++++++ 7 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/secure-rbac-defaults-2c46da839de3d59c.yaml diff --git a/blazar/policies/base.py b/blazar/policies/base.py index 2bd7f64a0..e8ec63dd9 100644 --- a/blazar/policies/base.py +++ b/blazar/policies/base.py @@ -16,6 +16,22 @@ RULE_ADMIN_OR_OWNER = 'rule:admin_or_owner' RULE_ANY = '@' +DEPRECATED_REASON = """ +Blazar API policies are introducing new default roles with scope_type +capabilities. Old policies are deprecated and silently going to be ignored +in future release. +""" + +DEPRECATED_ADMIN_OR_OWNER_POLICY = policy.DeprecatedRule( + name=RULE_ADMIN_OR_OWNER, + check_str="rule:admin or project_id:%(project_id)s", + deprecated_reason=DEPRECATED_REASON, + deprecated_since='15.0.0' +) + +PROJECT_MEMBER_OR_ADMIN = 'rule:project_member_or_admin' +PROJECT_READER_OR_ADMIN = 'rule:project_reader_or_admin' + rules = [ policy.RuleDefault( name="admin", @@ -24,7 +40,30 @@ policy.RuleDefault( name="admin_or_owner", check_str="rule:admin or project_id:%(project_id)s", - description="Default rule for most non-Admin APIs.") + description="Default rule for most non-Admin APIs.", + deprecated_for_removal=True, + deprecated_reason=DEPRECATED_REASON, + deprecated_since='15.0.0'), + policy.RuleDefault( + "project_member_api", + "role:member and project_id:%(project_id)s", + "Default rule for Project Member (non-Admin) APIs.", + deprecated_rule=DEPRECATED_ADMIN_OR_OWNER_POLICY), + policy.RuleDefault( + "project_reader_api", + "role:reader and project_id:%(project_id)s", + "Default rule for Project Reader (read-only) APIs.", + deprecated_rule=DEPRECATED_ADMIN_OR_OWNER_POLICY), + policy.RuleDefault( + "project_member_or_admin", + "rule:project_member_api or rule:admin", + "Default rule for Project Member or Admin APIs.", + deprecated_rule=DEPRECATED_ADMIN_OR_OWNER_POLICY), + policy.RuleDefault( + "project_reader_or_admin", + "rule:project_reader_api or rule:admin", + "Default rule for Project Reader or Admin APIs.", + deprecated_rule=DEPRECATED_ADMIN_OR_OWNER_POLICY) ] diff --git a/blazar/policies/floatingips.py b/blazar/policies/floatingips.py index c946b6621..e3b863d6f 100644 --- a/blazar/policies/floatingips.py +++ b/blazar/policies/floatingips.py @@ -19,7 +19,7 @@ floatingips_policies = [ policy.DocumentedRuleDefault( name=POLICY_ROOT % 'get', - check_str=base.RULE_ADMIN, + check_str=base.PROJECT_READER_OR_ADMIN, description='Policy rule for List/Show FloatingIP(s) API.', operations=[ { @@ -30,7 +30,8 @@ 'path': '/{api_version}/floatingips/{floatingip_id}', 'method': 'GET' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'post', @@ -41,7 +42,8 @@ 'path': '/{api_version}/floatingips', 'method': 'POST' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'delete', @@ -52,7 +54,8 @@ 'path': '/{api_version}/floatingips/{floatingip_id}', 'method': 'DELETE' } - ] + ], + scope_types=['project'] ) ] diff --git a/blazar/policies/leases.py b/blazar/policies/leases.py index e209f69a3..e0dce1f50 100644 --- a/blazar/policies/leases.py +++ b/blazar/policies/leases.py @@ -19,7 +19,7 @@ leases_policies = [ policy.DocumentedRuleDefault( name=POLICY_ROOT % 'get', - check_str=base.RULE_ADMIN_OR_OWNER, + check_str=base.PROJECT_READER_OR_ADMIN, description='Policy rule for List/Show Lease(s) API.', operations=[ { @@ -30,40 +30,44 @@ 'path': '/{api_version}/leases/{lease_id}', 'method': 'GET' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'post', - check_str=base.RULE_ADMIN_OR_OWNER, + check_str=base.PROJECT_MEMBER_OR_ADMIN, description='Policy rule for Create Lease API.', operations=[ { 'path': '/{api_version}/leases', 'method': 'POST' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'put', - check_str=base.RULE_ADMIN_OR_OWNER, + check_str=base.PROJECT_MEMBER_OR_ADMIN, description='Policy rule for Update Lease API.', operations=[ { 'path': '/{api_version}/leases/{lease_id}', 'method': 'PUT' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'delete', - check_str=base.RULE_ADMIN_OR_OWNER, + check_str=base.PROJECT_MEMBER_OR_ADMIN, description='Policy rule for Delete Lease API.', operations=[ { 'path': '/{api_version}/leases/{lease_id}', 'method': 'DELETE' } - ] + ], + scope_types=['project'] ) ] diff --git a/blazar/policies/oshosts.py b/blazar/policies/oshosts.py index 05b30d03c..02bdce2d2 100644 --- a/blazar/policies/oshosts.py +++ b/blazar/policies/oshosts.py @@ -30,7 +30,8 @@ 'path': '/{api_version}/os-hosts/{host_id}', 'method': 'GET' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'post', @@ -41,7 +42,8 @@ 'path': '/{api_version}/os-hosts', 'method': 'POST' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'put', @@ -52,7 +54,8 @@ 'path': '/{api_version}/os-hosts/{host_id}', 'method': 'PUT' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'delete', @@ -63,7 +66,8 @@ 'path': '/{api_version}/os-hosts/{host_id}', 'method': 'DELETE' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'get_allocations', @@ -78,7 +82,8 @@ 'path': '/{api_version}/os-hosts/{host_id}/allocation', 'method': 'GET' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'get_resource_properties', @@ -89,7 +94,8 @@ 'path': '/{api_version}/os-hosts/resource_properties', 'method': 'GET' } - ] + ], + scope_types=['project'] ), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'update_resource_properties', @@ -101,7 +107,8 @@ '{property_name}'), 'method': 'PATCH' } - ] + ], + scope_types=['project'] ), ] diff --git a/blazar/tests/api/__init__.py b/blazar/tests/api/__init__.py index 4e3e537fd..6f0f9bc1c 100644 --- a/blazar/tests/api/__init__.py +++ b/blazar/tests/api/__init__.py @@ -44,8 +44,8 @@ def fake_ctx_from_headers(headers): if not headers: return context.BlazarContext( user_id=FAKE_USER, project_id=FAKE_PROJECT, - roles=['member']) - roles = headers.get('X-Roles', str('member')).split(',') + roles=['member', 'reader']) + roles = headers.get('X-Roles', str('member,reader')).split(',') return context.BlazarContext( user_id=headers.get('X-User-Id', FAKE_USER), project_id=headers.get('X-Project-Id', FAKE_PROJECT), diff --git a/blazar/tests/test_policy.py b/blazar/tests/test_policy.py index df680324c..5288be7da 100644 --- a/blazar/tests/test_policy.py +++ b/blazar/tests/test_policy.py @@ -32,7 +32,7 @@ def setUp(self): self.context = context.BlazarContext(user_id='fake', project_id='fake', - roles=['member']) + roles=['member', 'reader']) def test_standardpolicy(self): target_good = {'user_id': self.context.user_id, diff --git a/releasenotes/notes/secure-rbac-defaults-2c46da839de3d59c.yaml b/releasenotes/notes/secure-rbac-defaults-2c46da839de3d59c.yaml new file mode 100644 index 000000000..0860d3581 --- /dev/null +++ b/releasenotes/notes/secure-rbac-defaults-2c46da839de3d59c.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + The Blazar policies implemented the scope concept and new default roles + (``admin``, ``member``, and ``reader``) provided by keystone. +upgrade: + - | + All the policies implement the ``scope_type`` and new defaults. + + * **Scope** + + Each policy is protected with ``project`` ``scope_type``. + + * **New Defaults(Admin, Member and Reader)** + + Policies are default to Admin, Member and Reader roles. Old roles + are also supported. There is no change in the legacy admin access. From dbdf1b894ff898bf4909789704120056677f3796 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Mon, 17 Feb 2025 22:20:50 +0000 Subject: [PATCH 07/31] Fix host randomization Previously, this was implemented using the shuffle method of the Random class without instantiating a Random object, which did not work. This changes swaps to the random.shuffle method, which works as expected. Closes-Bug: #2099927 Change-Id: Ib288091af9cc035ccb0535fbc1748c17bb3cb1e9 --- blazar/plugins/oshosts/host_plugin.py | 6 +++--- blazar/tests/plugins/oshosts/test_physical_host_plugin.py | 6 +++--- .../notes/fix-host-randomization-bcab5276ef6199e6.yaml | 5 +++++ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/fix-host-randomization-bcab5276ef6199e6.yaml diff --git a/blazar/plugins/oshosts/host_plugin.py b/blazar/plugins/oshosts/host_plugin.py index 2bf5d2275..dd6c5f9b6 100644 --- a/blazar/plugins/oshosts/host_plugin.py +++ b/blazar/plugins/oshosts/host_plugin.py @@ -15,7 +15,7 @@ # under the License. import datetime -from random import Random +import random import retrying from novaclient import exceptions as nova_exceptions @@ -654,12 +654,12 @@ def _matching_hosts(self, hypervisor_properties, resource_properties, allocated_host_ids.append(host['id']) if len(not_allocated_host_ids) >= int(min_host): if CONF[self.resource_type].randomize_host_selection: - Random.shuffle(not_allocated_host_ids) + random.shuffle(not_allocated_host_ids) return not_allocated_host_ids[:int(max_host)] all_host_ids = allocated_host_ids + not_allocated_host_ids if len(all_host_ids) >= int(min_host): if CONF[self.resource_type].randomize_host_selection: - Random.shuffle(all_host_ids) + random.shuffle(all_host_ids) return all_host_ids[:int(max_host)] else: return [] diff --git a/blazar/tests/plugins/oshosts/test_physical_host_plugin.py b/blazar/tests/plugins/oshosts/test_physical_host_plugin.py index 86ebf5106..18b27d2ec 100644 --- a/blazar/tests/plugins/oshosts/test_physical_host_plugin.py +++ b/blazar/tests/plugins/oshosts/test_physical_host_plugin.py @@ -2431,7 +2431,7 @@ def host_allocation_get_all_by_values(**kwargs): self.addCleanup(CONF.clear_override, 'cleaning_time') self.assertEqual(['host1', 'host2', 'host3'], result) - @mock.patch.object(random.Random, "shuffle") + @mock.patch.object(random, "shuffle") def test_random_matching_hosts_not_allocated_hosts(self, mock_shuffle): def host_allocation_get_all_by_values(**kwargs): if kwargs['compute_host_id'] == 'host1': @@ -2465,7 +2465,7 @@ def host_allocation_get_all_by_values(**kwargs): group=plugin.RESOURCE_TYPE) mock_shuffle.assert_called_once_with(['host2', 'host3']) - @mock.patch.object(random.Random, "shuffle") + @mock.patch.object(random, "shuffle") def test_random_matching_hosts_allocated_hosts(self, mock_shuffle): def host_allocation_get_all_by_values(**kwargs): if kwargs['compute_host_id'] == 'host1': @@ -2499,7 +2499,7 @@ def host_allocation_get_all_by_values(**kwargs): group=plugin.RESOURCE_TYPE) mock_shuffle.assert_called_once_with(['host1', 'host2', 'host3']) - @mock.patch.object(random.Random, "shuffle") + @mock.patch.object(random, "shuffle") def test_random_matching_hosts_allocated_cleaning_time(self, mock_shuffle): def host_allocation_get_all_by_values(**kwargs): if kwargs['compute_host_id'] == 'host1': diff --git a/releasenotes/notes/fix-host-randomization-bcab5276ef6199e6.yaml b/releasenotes/notes/fix-host-randomization-bcab5276ef6199e6.yaml new file mode 100644 index 000000000..e6ed412fc --- /dev/null +++ b/releasenotes/notes/fix-host-randomization-bcab5276ef6199e6.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixes functionality of host randomization feature. + `LP#2099927 `__ From 199f69b8a7e87a61020e7e835719e22882e653c3 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 13 Mar 2025 13:30:34 +0000 Subject: [PATCH 08/31] Update master for stable/2025.1 Add file to the reno documentation build to show release notes for stable/2025.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2025.1. Sem-Ver: feature Change-Id: Ie566be732d06d70c7732290f55e4e28c1ba27a48 --- releasenotes/source/2025.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2025.1.rst diff --git a/releasenotes/source/2025.1.rst b/releasenotes/source/2025.1.rst new file mode 100644 index 000000000..3add0e53a --- /dev/null +++ b/releasenotes/source/2025.1.rst @@ -0,0 +1,6 @@ +=========================== +2025.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index 351c12ebb..c8daf7844 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2025.1 2024.2 2024.1 2023.2 From 2fc005e0e6af780dd7b56f026f1d0c1e2a8915c5 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Wed, 16 Apr 2025 12:58:12 +0900 Subject: [PATCH 09/31] Remove unnecessary +x mode These files are not actually executable. Change-Id: Ia11e696ffa804172cb4b6814f1813f3ad0ee1a00 --- .../versions/9593f3656974_no_affinity_instance_reservation.py | 0 .../tests/enforcement/filters/test_max_lease_duration_filter.py | 0 blazar/tests/enforcement/test_enforcement.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 blazar/db/migration/alembic_migrations/versions/9593f3656974_no_affinity_instance_reservation.py mode change 100755 => 100644 blazar/tests/enforcement/filters/test_max_lease_duration_filter.py mode change 100755 => 100644 blazar/tests/enforcement/test_enforcement.py diff --git a/blazar/db/migration/alembic_migrations/versions/9593f3656974_no_affinity_instance_reservation.py b/blazar/db/migration/alembic_migrations/versions/9593f3656974_no_affinity_instance_reservation.py old mode 100755 new mode 100644 diff --git a/blazar/tests/enforcement/filters/test_max_lease_duration_filter.py b/blazar/tests/enforcement/filters/test_max_lease_duration_filter.py old mode 100755 new mode 100644 diff --git a/blazar/tests/enforcement/test_enforcement.py b/blazar/tests/enforcement/test_enforcement.py old mode 100755 new mode 100644 From 51234621087c60ef30ba6a360fe168c672af6548 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Mon, 12 May 2025 12:55:07 +0200 Subject: [PATCH 10/31] Fix lease date Currently, tests are using 2026 as date in the future. Unfortunately, if we set the date of the machine running the tests in let's say 2027, then tests are failing, as per Debian report here: https://bugs.debian.org/1104885 This patch fixes this by using 2046 instead of 2026 in the tests. Closes-Bug: #2110472 Change-Id: I190daa89d134a4440dd914a9a935000aaa87d4f2 --- blazar/tests/manager/test_service.py | 80 ++++++++++++++-------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/blazar/tests/manager/test_service.py b/blazar/tests/manager/test_service.py index 2effcc07e..b6a82f4c8 100644 --- a/blazar/tests/manager/test_service.py +++ b/blazar/tests/manager/test_service.py @@ -189,8 +189,8 @@ def setUp(self): 'resource_id': '111', 'resource_type': 'virtual:instance', 'status': 'FAKE PROGRESS'}], - 'start_date': '2026-11-13 13:13', - 'end_date': '2026-12-13 13:13', + 'start_date': '2046-11-13 13:13', + 'end_date': '2046-12-13 13:13', 'trust_id': 'exxee111qwwwwe'} self.good_date = datetime.datetime.strptime('2012-12-13 13:13', @@ -505,7 +505,7 @@ def test_create_lease_now(self): def test_create_lease_some_time(self): lease_values = self.lease_values.copy() - self.lease['start_date'] = '2026-11-13 13:13' + self.lease['start_date'] = '2046-11-13 13:13' lease = self.manager.create_lease(lease_values) @@ -515,11 +515,11 @@ def test_create_lease_some_time(self): def test_create_lease_validate_created_events(self): lease_values = self.lease_values.copy() - self.lease['start_date'] = '2026-11-13 13:13:00' - self.lease['end_date'] = '2026-12-13 13:13:00' - self.lease['events'][0]['time'] = '2026-11-13 13:13:00' - self.lease['events'][1]['time'] = '2026-12-13 13:13:00' - self.lease['events'][2]['time'] = '2026-12-13 12:13:00' + self.lease['start_date'] = '2046-11-13 13:13:00' + self.lease['end_date'] = '2046-12-13 13:13:00' + self.lease['events'][0]['time'] = '2046-11-13 13:13:00' + self.lease['events'][1]['time'] = '2046-12-13 13:13:00' + self.lease['events'][2]['time'] = '2046-12-13 12:13:00' lease = self.manager.create_lease(lease_values) @@ -550,11 +550,11 @@ def test_create_lease_validate_created_events(self): def test_create_lease_before_end_event_is_before_lease_start(self): lease_values = self.lease_values.copy() - self.lease['start_date'] = '2026-11-13 13:13:00' - self.lease['end_date'] = '2026-12-13 13:13:00' - self.lease['events'][0]['time'] = '2026-11-13 13:13:00' - self.lease['events'][1]['time'] = '2026-12-13 13:13:00' - self.lease['events'][2]['time'] = '2026-11-13 13:13:00' + self.lease['start_date'] = '2046-11-13 13:13:00' + self.lease['end_date'] = '2046-12-13 13:13:00' + self.lease['events'][0]['time'] = '2046-11-13 13:13:00' + self.lease['events'][1]['time'] = '2046-12-13 13:13:00' + self.lease['events'][2]['time'] = '2046-11-13 13:13:00' self.cfg.CONF.set_override('minutes_before_end_lease', 120, group='manager') @@ -586,7 +586,7 @@ def test_create_lease_before_end_event_is_before_lease_start(self): def test_create_lease_before_end_event_before_start_without_lease_id(self): lease_values = self.lease_values.copy() - self.lease['start_date'] = '2026-11-13 13:13' + self.lease['start_date'] = '2046-11-13 13:13' self.cfg.CONF.set_override('minutes_before_end_lease', 120, group='manager') @@ -599,30 +599,30 @@ def test_create_lease_before_end_event_before_start_without_lease_id(self): def test_create_lease_before_end_param_is_before_lease_start(self): lease_values = self.lease_values.copy() - lease_values['before_end_date'] = '2026-11-11 13:13' - lease_values['start_date'] = '2026-11-13 13:13' + lease_values['before_end_date'] = '2046-11-11 13:13' + lease_values['start_date'] = '2046-11-13 13:13' - self.lease['start_date'] = '2026-11-13 13:13' + self.lease['start_date'] = '2046-11-13 13:13' self.assertRaises( exceptions.NotAuthorized, self.manager.create_lease, lease_values) def test_create_lease_before_end_param_is_past_lease_ending(self): lease_values = self.lease_values.copy() - lease_values['start_date'] = '2026-11-13 13:13' - lease_values['end_date'] = '2026-11-14 13:13' - lease_values['before_end_date'] = '2026-11-15 13:13' - self.lease['start_date'] = '2026-11-13 13:13' + lease_values['start_date'] = '2046-11-13 13:13' + lease_values['end_date'] = '2046-11-14 13:13' + lease_values['before_end_date'] = '2046-11-15 13:13' + self.lease['start_date'] = '2046-11-13 13:13' self.assertRaises( exceptions.NotAuthorized, self.manager.create_lease, lease_values) def test_create_lease_no_before_end_event(self): lease_values = self.lease_values.copy() - self.lease['start_date'] = '2026-11-13 13:13:00' - self.lease['end_date'] = '2026-11-14 13:13:00' - self.lease['events'][0]['time'] = '2026-11-13 13:13:00' - self.lease['events'][1]['time'] = '2026-11-14 13:13:00' + self.lease['start_date'] = '2046-11-13 13:13:00' + self.lease['end_date'] = '2046-11-14 13:13:00' + self.lease['events'][0]['time'] = '2046-11-13 13:13:00' + self.lease['events'][1]['time'] = '2046-11-14 13:13:00' self.lease['events'].pop() self.cfg.CONF.set_override('minutes_before_end_lease', 0, @@ -648,13 +648,13 @@ def test_create_lease_no_before_end_event(self): def test_create_lease_with_before_end_date_param(self): lease_values = self.lease_values.copy() - lease_values['before_end_date'] = '2026-11-14 10:13' + lease_values['before_end_date'] = '2046-11-14 10:13' - self.lease['start_date'] = '2026-11-13 13:13:00' - self.lease['end_date'] = '2026-11-14 13:13:00' - self.lease['events'][0]['time'] = '2026-11-13 13:13:00' - self.lease['events'][1]['time'] = '2026-11-14 13:13:00' - self.lease['events'][2]['time'] = '2026-11-14 10:13:00' + self.lease['start_date'] = '2046-11-13 13:13:00' + self.lease['end_date'] = '2046-11-14 13:13:00' + self.lease['events'][0]['time'] = '2046-11-13 13:13:00' + self.lease['events'][1]['time'] = '2046-11-14 13:13:00' + self.lease['events'][2]['time'] = '2046-11-14 10:13:00' lease = self.manager.create_lease(lease_values) @@ -692,7 +692,7 @@ def test_create_lease_wrong_date(self): def test_create_lease_wrong_format_before_end_date(self): lease_values = self.lease_values.copy() - lease_values['before_end_date'] = '2026-14 10:13' + lease_values['before_end_date'] = '2046-14 10:13' self.assertRaises( manager_ex.InvalidDate, self.manager.create_lease, lease_values) @@ -708,8 +708,8 @@ def test_create_lease_start_date_in_past(self): def test_create_lease_end_before_start(self): lease_values = self.lease_values.copy() - lease_values['start_date'] = '2026-11-13 13:13' - lease_values['end_date'] = '2026-11-13 12:13' + lease_values['start_date'] = '2046-11-13 13:13' + lease_values['end_date'] = '2046-11-13 12:13' self.assertRaises( exceptions.InvalidInput, self.manager.create_lease, lease_values) @@ -742,21 +742,21 @@ def test_create_lease_without_trust_id(self): def test_create_lease_without_required_params(self): name_missing_values = { - 'start_date': '2026-11-13 13:13', - 'end_date': '2026-12-13 13:13', + 'start_date': '2046-11-13 13:13', + 'end_date': '2046-12-13 13:13', 'trust_id': 'trust1'} start_missing_values = { 'name': 'name', - 'end_date': '2026-12-13 13:13', + 'end_date': '2046-12-13 13:13', 'trust_id': 'trust1'} end_missing_values = { 'name': 'name', - 'start_date': '2026-11-13 13:13', + 'start_date': '2046-11-13 13:13', 'trust_id': 'trust1'} resource_type_missing_value = { 'name': 'name', - 'start_date': '2026-11-13 13:13', - 'end_date': '2026-12-13 13:13', + 'start_date': '2046-11-13 13:13', + 'end_date': '2046-12-13 13:13', 'reservations': [{'min': 1, 'max': 3}], 'trust_id': 'trust1' } From 3ccbf2ea5c7b28a8914f44da0be2704d07477343 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Tue, 13 May 2025 09:09:52 +0200 Subject: [PATCH 11/31] Add wsgi module to blazar Changes in python packaging tooling mean that the wsgi_scripts functionality via PBR may not longer function. This patch switches blazar from using the PBR wsgi_scripts method to using a new wsgi module that provides the same behavior as the generated wsgi scripts provided. A related devstack patch enables devstack to setup uWSGI to use the new module path instead of the generated wsgi scripts. For more details see the proposed OpenStack goal [1]. [1] https://review.opendev.org/c/openstack/governance/+/902807 Change-Id: Ib16d5d1ea5d7416d7b1f026f771d6c2b77657970 --- blazar/wsgi/__init__.py | 0 blazar/wsgi/api.py | 24 +++++++++++++++ devstack/plugin.sh | 2 +- devstack/settings | 2 +- .../remove-wsgi-scripts-1e2011c1d40df1c7.yaml | 29 +++++++++++++++++++ 5 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 blazar/wsgi/__init__.py create mode 100644 blazar/wsgi/api.py create mode 100644 releasenotes/notes/remove-wsgi-scripts-1e2011c1d40df1c7.yaml diff --git a/blazar/wsgi/__init__.py b/blazar/wsgi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/blazar/wsgi/api.py b/blazar/wsgi/api.py new file mode 100644 index 000000000..e418a4068 --- /dev/null +++ b/blazar/wsgi/api.py @@ -0,0 +1,24 @@ +# 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. + +"""WSGI application entry-point for the Blazar API.""" + +import threading + +from blazar.api import wsgi_app + +application = None + +lock = threading.Lock() +with lock: + if application is None: + application = wsgi_app.init_app() diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 0e5bdeb29..e1cf4f837 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -80,7 +80,7 @@ function configure_blazar { iniadd $NOVA_CONF filter_scheduler available_filters "blazarnova.scheduler.filters.blazar_filter.BlazarFilter" if [[ "$BLAZAR_USE_MOD_WSGI" == "True" ]]; then - write_uwsgi_config "$BLAZAR_UWSGI_CONF" "$BLAZAR_UWSGI" "/reservation" + write_uwsgi_config "$BLAZAR_UWSGI_CONF" "$BLAZAR_UWSGI" "/reservation" "" "blazar" fi # Database diff --git a/devstack/settings b/devstack/settings index e04a78b62..ae78fcca3 100644 --- a/devstack/settings +++ b/devstack/settings @@ -31,7 +31,7 @@ BLAZAR_DASHBOARD_DIR=$DEST/blazar-dashboard # wsgi deployment BLAZAR_USE_MOD_WSGI=${BLAZAR_USE_MOD_WSGI:-True} BLAZAR_BIN_DIR=$(get_python_exec_prefix) -BLAZAR_UWSGI=$BLAZAR_BIN_DIR/blazar-api-wsgi +BLAZAR_UWSGI=blazar.wsgi.api:application BLAZAR_UWSGI_CONF=$BLAZAR_CONF_DIR/blazar-api-uwsgi.ini BLAZAR_SERVICE_HOST=${BLAZAR_SERVICE_HOST:-$SERVICE_HOST} diff --git a/releasenotes/notes/remove-wsgi-scripts-1e2011c1d40df1c7.yaml b/releasenotes/notes/remove-wsgi-scripts-1e2011c1d40df1c7.yaml new file mode 100644 index 000000000..e4df6f089 --- /dev/null +++ b/releasenotes/notes/remove-wsgi-scripts-1e2011c1d40df1c7.yaml @@ -0,0 +1,29 @@ +--- +features: + - | + A new module, ``blazar.wsgi``, has been added as a place to gather WSGI + ``application`` objects. This is intended to ease deployment by providing a + consistent location for these objects. For example, if using uWSGI then + instead of: + + .. code-block:: ini + + [uwsgi] + wsgi-file = /bin/blazar-api-wsgi + + You can now use: + + .. code-block:: ini + + [uwsgi] + module = blazar.wsgi.api:application + + This also simplifies deployment with other WSGI servers that expect module + paths such as gunicorn. +upgrade: + - | + The WSGI script ``blazar-api-wsgi`` has been removed. Deployment tooling + should instead reference the Python module path for the wsgi module in + Blazar, ``blazar.wsgi.api:application`` if their chosen WSGI server + supports this (gunicorn, uWSGI, etc.) or implement a .wsgi script + themselves if not (mod_wsgi). From f00448f09edafcf733c39f148064ae1230955a71 Mon Sep 17 00:00:00 2001 From: Winicius Silva Date: Fri, 16 May 2025 10:08:28 -0300 Subject: [PATCH 12/31] Fix JSON api examples Some syntax errors in JSON make it unparsable for tools like `jq` Change-Id: Idd3ba27933868a3eaa53f9ce8c8db94a49558e74 --- doc/api_samples/floatingips/floatingip-list-resp.json | 2 +- doc/api_samples/leases/lease-create-resp.json | 4 ++-- doc/api_samples/leases/lease-details-resp.json | 4 ++-- doc/api_samples/leases/lease-list-resp.json | 4 ++-- doc/api_samples/leases/lease-update-resp.json | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/api_samples/floatingips/floatingip-list-resp.json b/doc/api_samples/floatingips/floatingip-list-resp.json index da30d2a91..740eb6a50 100644 --- a/doc/api_samples/floatingips/floatingip-list-resp.json +++ b/doc/api_samples/floatingips/floatingip-list-resp.json @@ -17,7 +17,7 @@ "floating_ip_address": "172.24.4.102", "reservable": true, "created_at": "2019-01-28 08:08:22", - "updated_at": null, + "updated_at": null } ] } diff --git a/doc/api_samples/leases/lease-create-resp.json b/doc/api_samples/leases/lease-create-resp.json index 722dd6e0f..03ca8f057 100644 --- a/doc/api_samples/leases/lease-create-resp.json +++ b/doc/api_samples/leases/lease-create-resp.json @@ -51,7 +51,7 @@ ], "events": [ { - "id": "188a8584-f832-4df9-9a4a-51e6364420ff" + "id": "188a8584-f832-4df9-9a4a-51e6364420ff", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "event_type": "start_lease", @@ -69,7 +69,7 @@ "updated_at": null }, { - "id": "f583af71-ca21-4b66-87de-52211d118029" + "id": "f583af71-ca21-4b66-87de-52211d118029", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "time": "2017-12-27T11:00:00.000000", diff --git a/doc/api_samples/leases/lease-details-resp.json b/doc/api_samples/leases/lease-details-resp.json index 722dd6e0f..03ca8f057 100644 --- a/doc/api_samples/leases/lease-details-resp.json +++ b/doc/api_samples/leases/lease-details-resp.json @@ -51,7 +51,7 @@ ], "events": [ { - "id": "188a8584-f832-4df9-9a4a-51e6364420ff" + "id": "188a8584-f832-4df9-9a4a-51e6364420ff", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "event_type": "start_lease", @@ -69,7 +69,7 @@ "updated_at": null }, { - "id": "f583af71-ca21-4b66-87de-52211d118029" + "id": "f583af71-ca21-4b66-87de-52211d118029", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "time": "2017-12-27T11:00:00.000000", diff --git a/doc/api_samples/leases/lease-list-resp.json b/doc/api_samples/leases/lease-list-resp.json index f138a9129..71795f08c 100644 --- a/doc/api_samples/leases/lease-list-resp.json +++ b/doc/api_samples/leases/lease-list-resp.json @@ -52,7 +52,7 @@ ], "events": [ { - "id": "188a8584-f832-4df9-9a4a-51e6364420ff" + "id": "188a8584-f832-4df9-9a4a-51e6364420ff", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "event_type": "start_lease", @@ -70,7 +70,7 @@ "updated_at": null }, { - "id": "f583af71-ca21-4b66-87de-52211d118029" + "id": "f583af71-ca21-4b66-87de-52211d118029", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "time": "2017-12-27T11:00:00.000000", diff --git a/doc/api_samples/leases/lease-update-resp.json b/doc/api_samples/leases/lease-update-resp.json index 14ed3f9e5..978c5b12f 100644 --- a/doc/api_samples/leases/lease-update-resp.json +++ b/doc/api_samples/leases/lease-update-resp.json @@ -51,7 +51,7 @@ ], "events": [ { - "id": "188a8584-f832-4df9-9a4a-51e6364420ff" + "id": "188a8584-f832-4df9-9a4a-51e6364420ff", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "event_type": "start_lease", @@ -69,7 +69,7 @@ "updated_at": null }, { - "id": "f583af71-ca21-4b66-87de-52211d118029" + "id": "f583af71-ca21-4b66-87de-52211d118029", "lease_id": "6ee55c78-ac52-41a6-99af-2d2d73bcc466", "status": "UNDONE", "time": "2017-12-27T11:00:00.000000", From 3b235baf312fe32ccba9e931c0750afd91b59eb7 Mon Sep 17 00:00:00 2001 From: Pierre Riteau Date: Thu, 12 Jun 2025 18:38:23 +0200 Subject: [PATCH 13/31] Remove support for Python 3.8 and 3.9 Python 3.9 is no longer part of the tested runtimes [1]. Python 3.8 testing was removed in the 2024.2 release [2]. Add Python 3.12 to classifiers. [1] https://governance.openstack.org/tc/reference/runtimes/2025.2.html [2] https://governance.openstack.org/tc/reference/runtimes/2024.2.html Change-Id: I5a62b7b6ffdefafa1966a74faa2725ff88fdec3a --- .../notes/drop-python-3-8-and-3-9-232826086bf12ac5.yaml | 5 +++++ setup.cfg | 5 ++--- tox.ini | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/drop-python-3-8-and-3-9-232826086bf12ac5.yaml diff --git a/releasenotes/notes/drop-python-3-8-and-3-9-232826086bf12ac5.yaml b/releasenotes/notes/drop-python-3-8-and-3-9-232826086bf12ac5.yaml new file mode 100644 index 000000000..ffb6cc491 --- /dev/null +++ b/releasenotes/notes/drop-python-3-8-and-3-9-232826086bf12ac5.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + Python 3.8 and 3.9 support has been dropped. The minimum version of Python + now supported is Python 3.10. diff --git a/setup.cfg b/setup.cfg index 81ed03ea2..94a03cad3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,16 +3,15 @@ name = blazar summary = Reservation Service for OpenStack clouds description_file = README.rst license = Apache Software License -python_requires = >=3.8 +python_requires = >=3.10 classifiers = Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Environment :: OpenStack Development Status :: 3 - Alpha Framework :: Setuptools Plugin diff --git a/tox.ini b/tox.ini index 52374cd75..f75032d80 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py39,py38,pep8 +envlist = py3,pep8 minversion = 3.18.0 ignore_basepython_conflict = True From db0680a4c8dd5fa80e0ec90eccf03354161586f5 Mon Sep 17 00:00:00 2001 From: Ghanshyam Date: Thu, 12 Jun 2025 12:23:54 -0700 Subject: [PATCH 14/31] Remove Jammy job Jammy testing is not needed in 2025.2 release. Change-Id: I5f6de2316e42685a4f80aed990327ade61ccb50a --- .zuul.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 214105d1c..755c6f6e6 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -9,12 +9,10 @@ check: jobs: - blazar-tempest-plugin-base - - blazar-tempest-plugin-jammy - blazar-tempest-plugin-ipv6-only - openstack-tox-pylint: voting: false gate: jobs: - blazar-tempest-plugin-base - - blazar-tempest-plugin-jammy - blazar-tempest-plugin-ipv6-only From c2ced4020244b9fae112a42c9c7f055bc30f2093 Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Mon, 30 Jun 2025 22:20:14 +0900 Subject: [PATCH 15/31] sqlalchemy: Use built-in declarative sqlalchemy.ext.declarative was deprecated in sqlalchemy 1.4.0, due to the built-in implementations[1]. [1] https://github.com/sqlalchemy/sqlalchemy/commit/450f5c0d6519a439f40 Change-Id: I2bc3a39313e2534ca0a5638e08fcac736bac66ed --- blazar/db/sqlalchemy/model_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blazar/db/sqlalchemy/model_base.py b/blazar/db/sqlalchemy/model_base.py index 8990c9b40..6d78227c9 100644 --- a/blazar/db/sqlalchemy/model_base.py +++ b/blazar/db/sqlalchemy/model_base.py @@ -14,8 +14,8 @@ # limitations under the License. from oslo_db.sqlalchemy import models -from sqlalchemy.ext import declarative from sqlalchemy.orm import attributes +from sqlalchemy.orm import declarative_base class _BlazarBase(models.ModelBase, models.TimestampMixin): @@ -49,4 +49,4 @@ def datetime_to_str(dct, attr_name): dct[attr_name] = dct[attr_name].isoformat(' ') -BlazarBase = declarative.declarative_base(cls=_BlazarBase) +BlazarBase = declarative_base(cls=_BlazarBase) From e407e8da0225e319807e96bf709720b0ee2257fb Mon Sep 17 00:00:00 2001 From: Matt Crees Date: Thu, 14 Aug 2025 11:42:56 +0100 Subject: [PATCH 16/31] Fix owner policy enforcement Previously, target was always None, and so ownership check essentially compared the current context against itself. Under this implementation, if the target is None, we look up the relevant object based on kwargs, and then use this object as the target. At the moment "lease" is the only type of object that has ownership, and so it is the only case. Co-Authored-By: Mark Powers Closes-Bug: #2120655 Change-Id: I4bb85d650f4d7f3534206f1c4690cef8eca0a4c2 Signed-off-by: Matt Crees --- blazar/policy.py | 19 +++++++++++++-- blazar/tests/test_policy.py | 23 +++++++++++++++++++ ...r-policy-enforcement-57a6d71c37ffec3d.yaml | 10 ++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-owner-policy-enforcement-57a6d71c37ffec3d.yaml diff --git a/blazar/policy.py b/blazar/policy.py index 7d37f3fb1..d8f6dcbac 100644 --- a/blazar/policy.py +++ b/blazar/policy.py @@ -22,6 +22,7 @@ from oslo_policy import policy from blazar import context +from blazar.db import api as db_api from blazar import exceptions from blazar import policies @@ -101,8 +102,22 @@ def decorator(func): @functools.wraps(func) def wrapped(self, *args, **kwargs): cur_ctx = ctx or context.current() - tgt = target or {'project_id': cur_ctx.project_id, - 'user_id': cur_ctx.user_id} + tgt = target + if tgt is None: + obj = None + if kwargs.get("lease_id"): + obj = db_api.lease_get(kwargs.get("lease_id")) + if obj: + tgt = { + 'project_id': obj.get("project_id"), + 'user_id': obj.get("user_id"), + } + else: + tgt = { + 'project_id': cur_ctx.project_id, + 'user_id': cur_ctx.user_id, + } + if action is None: act = '%s:%s' % (api, extension) else: diff --git a/blazar/tests/test_policy.py b/blazar/tests/test_policy.py index 5288be7da..b1cc649b0 100644 --- a/blazar/tests/test_policy.py +++ b/blazar/tests/test_policy.py @@ -15,6 +15,8 @@ """Test of Policy Engine For Blazar.""" +import unittest.mock as mock + from oslo_config import cfg from blazar import context @@ -65,3 +67,24 @@ def adminonly_method_with_action(self): self.assertTrue(user_method_with_action(self)) self.assertRaises(exceptions.PolicyNotAuthorized, adminonly_method_with_action, self) + + @mock.patch( + 'blazar.db.api.lease_get', + mock.MagicMock(return_value={'id': '1', 'user_id': 'alt_fake', + 'project_id': 'alt_fake'})) + @policy.authorize('leases', 'get', ctx=self.context) + def alt_user_get_lease(self, **kwargs): + return True + + self.assertRaises(exceptions.PolicyNotAuthorized, alt_user_get_lease, + self, lease_id='1') + + @mock.patch( + 'blazar.db.api.lease_get', + mock.MagicMock(return_value={'id': '1', 'user_id': 'fake', + 'project_id': 'fake'})) + @policy.authorize('leases', 'get', ctx=self.context) + def user_get_lease(self, **kwargs): + return True + + self.assertTrue(user_get_lease(self, lease_id='1')) diff --git a/releasenotes/notes/fix-owner-policy-enforcement-57a6d71c37ffec3d.yaml b/releasenotes/notes/fix-owner-policy-enforcement-57a6d71c37ffec3d.yaml new file mode 100644 index 000000000..b473f55a0 --- /dev/null +++ b/releasenotes/notes/fix-owner-policy-enforcement-57a6d71c37ffec3d.yaml @@ -0,0 +1,10 @@ +--- +security: + - | + Fixes a bug where any user could update/delete a lease from any project, + provided that they had the lease ID. When using the default policy + configuration, there is no way for a regular user to see lease IDs of + another project. However, operators that are running the Resource + Availability Calendar may have overridden this policy, and so may be + vulnerable without this fix. + `LP#2120655 `__ From 752440329fa5de826c5fc34f865e12748e92fa0c Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sun, 24 Aug 2025 15:37:33 +0900 Subject: [PATCH 17/31] Drop description for ZeroMQ As is described, ZeroMQ support by oslo.messaging was removed during Stein cycle. Change-Id: I871426ef37db699e79a211ffdc91bb366a394918 Signed-off-by: Takashi Kajinami --- blazar/config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/blazar/config.py b/blazar/config.py index 67a4f5041..83feb1252 100644 --- a/blazar/config.py +++ b/blazar/config.py @@ -23,9 +23,7 @@ help='Name of this node. This can be an opaque ' 'identifier. It is not necessarily a hostname, ' 'FQDN, or IP address. However, the node name must ' - 'be valid within an AMQP key, and if using ' - 'ZeroMQ (will be removed in the Stein release), a ' - 'valid hostname, FQDN, or IP address'), + 'be valid within an AMQP key.'), cfg.BoolOpt('log_exchange', default=False, help='Log request/response exchange details: environ, ' 'headers and bodies'), From 85d6566684895a5b95622dc7fa5ed568ff3470a6 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Thu, 11 Sep 2025 12:35:03 +0000 Subject: [PATCH 18/31] Update master for stable/2025.2 Add file to the reno documentation build to show release notes for stable/2025.2. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2025.2. Sem-Ver: feature Change-Id: I382b5a722b9fe141584fad7ee0d12caa236cf326 Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh --- releasenotes/source/2025.2.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2025.2.rst diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 000000000..4dae18d86 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index c8daf7844..edf54f55f 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2025.2 2025.1 2024.2 2024.1 From a96d93e487bc74f598e99c0be6d213e6310283f0 Mon Sep 17 00:00:00 2001 From: Ivan Anfimov Date: Sat, 13 Sep 2025 22:46:46 +0000 Subject: [PATCH 19/31] Remove url tags from README The tags framework has been discontinued for a long time. https://governance.openstack.org/tc/reference/tags/ https://governance.openstack.org/tc/resolutions/20211224-tags-framework-removal.html Change-Id: I8f7940030afede4391b614f7c242bc3ccfb7daf0 Signed-off-by: Ivan Anfimov --- README.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 656394140..a4fc85ae4 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,11 @@ -Team and repository tags -======================== +====== +Blazar +====== .. image:: https://governance.openstack.org/tc/badges/blazar.svg - :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on -Blazar -====== - Blazar is a resource reservation service for OpenStack. Blazar enables users to reserve a specific type/amount of resources for a specific time period and it leases these resources to users based on their reservations. From 5a9baa587688532b67fb466c4d19d8e39c8ec151 Mon Sep 17 00:00:00 2001 From: Ivan Anfimov Date: Sat, 13 Sep 2025 22:45:23 +0000 Subject: [PATCH 20/31] tox: Drop basepython Python 2 reached its EOL long time ago and we no longer expect any user may attempt to run tox in Python 2. Removing the option allows us to remove ignore_basepython_conflict. Change-Id: Ifb7db1e4c5dc928346ce0704951d9fb538f9a679 Co-authored-by: Takashi Kajinami Signed-off-by: Ivan Anfimov --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index f75032d80..15e9368e8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,8 @@ [tox] envlist = py3,pep8 minversion = 3.18.0 -ignore_basepython_conflict = True [testenv] -basepython = python3 usedevelop = True allowlist_externals = rm deps = From e65316e0f2c21e1c872ceab11c5a50c4145bec42 Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Fri, 31 Oct 2025 12:12:54 +0000 Subject: [PATCH 21/31] reno: Update master for unmaintained/2024.1 Update the 2024.1 release notes configuration to build from unmaintained/2024.1. Change-Id: I1cde60ec473743089f8e13fa41c36bc0f9c5ab5b Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/change_reno_branch_to_unmaintained.sh --- releasenotes/source/2024.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst index 4977a4f1a..6896656be 100644 --- a/releasenotes/source/2024.1.rst +++ b/releasenotes/source/2024.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2024.1 + :branch: unmaintained/2024.1 From 1c175feaa081606af6fb5edf7d7dff03247dc0d7 Mon Sep 17 00:00:00 2001 From: nitin-29-gupta Date: Fri, 31 Oct 2025 08:33:52 +0000 Subject: [PATCH 22/31] feat(blazar): Extending the payload of a notification, to send the entire lease data. Change-Id: I6b6b971fc5df9721f474020965abdf7d34d6e913 Signed-off-by: nitin-29-gupta --- blazar/notification/api.py | 10 +++++++++- .../lease-entire-data-in-payload-8828ef1288c436fe.yaml | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/lease-entire-data-in-payload-8828ef1288c436fe.yaml diff --git a/blazar/notification/api.py b/blazar/notification/api.py index e829fa6a6..75fcc481a 100644 --- a/blazar/notification/api.py +++ b/blazar/notification/api.py @@ -27,6 +27,14 @@ def format_lease_payload(lease): 'lease_id': lease['id'], 'user_id': lease['user_id'], 'project_id': lease['project_id'], + 'trust_id': lease['trust_id'], 'start_date': lease['start_date'], - 'end_date': lease['end_date'] + 'end_date': lease['end_date'], + 'status': lease['status'], + 'reservations': lease['reservations'], + 'events': lease['events'], + 'name': lease.get('name'), + 'created_at': lease.get('created_at'), + 'updated_at': lease.get('updated_at'), + 'degraded': lease.get('degraded') } diff --git a/releasenotes/notes/lease-entire-data-in-payload-8828ef1288c436fe.yaml b/releasenotes/notes/lease-entire-data-in-payload-8828ef1288c436fe.yaml new file mode 100644 index 000000000..25243efeb --- /dev/null +++ b/releasenotes/notes/lease-entire-data-in-payload-8828ef1288c436fe.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + The payload of a notification has been extended to send the entire lease data. From 3e7cf24571e425ab2b806fe3419c9c9c59701f94 Mon Sep 17 00:00:00 2001 From: Ivan Anfimov Date: Thu, 8 Jan 2026 15:19:47 +0000 Subject: [PATCH 23/31] Remove pylint pylint is no longer in use and can be removed. Change-Id: I118c61cf56a9761f0afb3ed26905237b5d4a0152 Co-authored-by: Takashi Kajinami Signed-off-by: Ivan Anfimov --- pylintrc | 253 ------------------------------------------------------- 1 file changed, 253 deletions(-) delete mode 100644 pylintrc diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 17c687691..000000000 --- a/pylintrc +++ /dev/null @@ -1,253 +0,0 @@ -[MASTER] - -# Specify a configuration file. -rcfile=pylintrc - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Profiled execution. -profile=no - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=common - -# To exclude openstack/common. - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). -disable=C, R, W, E1002, E1101, E1103, F0401 - -# Disabled F0401 because of that old pylint bastard. Will be enabled when global-reqs will have pylint-1.1.0 support. - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=text - -# Include message's id in output -include-ids=yes - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (RP0004). -comment=no - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes=SQLObject - -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=no - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E0201 when accessed. Python regular -# expressions are accepted. -generated-members=REQUEST,acl_users,aq_parent - - -[BASIC] - -# Required attributes for module, separated by a comma -required-attributes= - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=__.*__ - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the beginning of the name of dummy variables -# (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branchs=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - - -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception From 48d3239171f615d4c1fe8a222e3b9770268a9329 Mon Sep 17 00:00:00 2001 From: Matt Crees Date: Wed, 14 Jan 2026 16:49:16 +0000 Subject: [PATCH 24/31] Fix updating amount in active instance leases Since 'resource_properties' was added, 'amount' is no longer the last item in the updatable list, so updating the amount would be denied. Correct this by instead explicitly checking for 'amount'. Closes-Bug: #2138386 Change-Id: Idd3ee49f173a62392eb5aec7ea664921a36d5068 Signed-off-by: Matt Crees --- blazar/plugins/instances/instance_plugin.py | 3 ++- ...ng-amount-in-instance-reservations-52e6e3a413a0176f.yaml | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-updating-amount-in-instance-reservations-52e6e3a413a0176f.yaml diff --git a/blazar/plugins/instances/instance_plugin.py b/blazar/plugins/instances/instance_plugin.py index 882ca51cf..6cf73f1e0 100644 --- a/blazar/plugins/instances/instance_plugin.py +++ b/blazar/plugins/instances/instance_plugin.py @@ -550,7 +550,8 @@ def update_reservation(self, reservation_id, new_values): return if (reservation['status'] == 'active' and - any([k in updatable[:-1] for k in new_values.keys()])): + any([k in updatable for k in new_values.keys() + if k != 'amount'])): msg = "An active reservation only accepts to update amount." raise mgr_exceptions.InvalidStateUpdate(msg) diff --git a/releasenotes/notes/fix-updating-amount-in-instance-reservations-52e6e3a413a0176f.yaml b/releasenotes/notes/fix-updating-amount-in-instance-reservations-52e6e3a413a0176f.yaml new file mode 100644 index 000000000..05bc5f828 --- /dev/null +++ b/releasenotes/notes/fix-updating-amount-in-instance-reservations-52e6e3a413a0176f.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes an issue where the amount of instances in an instance reservation + could not be updated when the lease is active. + `LP#2138386 `__ From b1cab52dcb5c244b390752ac3b4a3510d668bbe3 Mon Sep 17 00:00:00 2001 From: Matt Crees Date: Thu, 15 Jan 2026 10:03:14 +0000 Subject: [PATCH 25/31] Add flavor-based reservations to API reference Change-Id: I2f98ffe2cfbfb20c1c26be37bf54705b6666d01f Signed-off-by: Matt Crees --- api-ref/source/v1/leases.inc | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/api-ref/source/v1/leases.inc b/api-ref/source/v1/leases.inc index b3d13bdc7..0d85fa527 100644 --- a/api-ref/source/v1/leases.inc +++ b/api-ref/source/v1/leases.inc @@ -85,6 +85,24 @@ Parameters for Instance Reservation The following parameters are returned for instance reservation. All parameters are in the ``reservation`` object. +.. rest_parameters:: parameters.yaml + + - reservation.amount: reservation_amount + - reservation.vcpus: reservation_vcpus + - reservation.memory_mb: reservation_memory_mb + - reservation.disk_gb: reservation_disk_gb + - reservation.affinity : reservation_affinity + - reservation.resource_properties: reservation_resource_properties + - reservation.flavor_id: reservation_flavor_id + - reservation.server_group_id: reservation_server_group_id + - reservation.aggregate_id: reservation_aggregate_id + +Parameters for Flavor-based Instance Reservation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following parameters are returned for flavor-based instance reservation. +All parameters are in the ``reservation`` object. + .. rest_parameters:: parameters.yaml - reservation.amount: reservation_amount @@ -161,6 +179,19 @@ are in the ``reservation`` object. - reservation.affinity : reservation_affinity - reservation.resource_properties: reservation_resource_properties +Parameters for Flavor-based Instance Reservation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following parameters are required for flavor-based instance reservation. +All parameters are in the ``reservation`` object. + +.. rest_parameters:: parameters.yaml + + - reservation.amount: reservation_amount + - reservation.flavor_id: reservation_flavor_id + - reservation.affinity : reservation_affinity + - reservation.resource_properties: reservation_resource_properties + **Example of Create Lease Request** .. literalinclude:: ../../../doc/api_samples/leases/lease-create-req.json @@ -225,6 +256,21 @@ Parameters for Instance Reservation The following parameters are returned for instance reservation. All parameters are in the ``reservation`` object. +.. rest_parameters:: parameters.yaml + + - reservation.amount: reservation_amount + - reservation.vcpus: reservation_vcpus + - reservation.memory_mb: reservation_memory_mb + - reservation.disk_gb: reservation_disk_gb + - reservation.affinity : reservation_affinity + - reservation.resource_properties: reservation_resource_properties + - reservation.flavor_id: reservation_flavor_id + - reservation.server_group_id: reservation_server_group_id + - reservation.aggregate_id: reservation_aggregate_id + +Parameters for Flavor-based Instance Reservation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + .. rest_parameters:: parameters.yaml - reservation.amount: reservation_amount @@ -326,6 +372,21 @@ Parameters for Instance Reservation The following parameters are returned for instance reservation. All parameters are in the ``reservation`` object. +.. rest_parameters:: parameters.yaml + + - reservation.amount: reservation_amount + - reservation.vcpus: reservation_vcpus + - reservation.memory_mb: reservation_memory_mb + - reservation.disk_gb: reservation_disk_gb + - reservation.affinity : reservation_affinity + - reservation.resource_properties: reservation_resource_properties + - reservation.flavor_id: reservation_flavor_id + - reservation.server_group_id: reservation_server_group_id + - reservation.aggregate_id: reservation_aggregate_id + +Parameters for Flavor-based Instance Reservation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + .. rest_parameters:: parameters.yaml - reservation.amount: reservation_amount @@ -406,6 +467,12 @@ are in the ``reservation`` object. - reservation.affinity : reservation_affinity_optional - reservation.resource_properties: reservation_resource_properties_optional +Parameters for Flavor-based Instance Reservation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Lease updates are not currently supported for flavor-based instance +reservations. + **Example of Update Lease Request** .. literalinclude:: ../../../doc/api_samples/leases/lease-update-req.json @@ -482,6 +549,12 @@ are in the ``reservation`` object. - reservation.server_group_id: reservation_server_group_id - reservation.aggregate_id: reservation_aggregate_id +Parameters for Flavor-based Instance Reservation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Lease updates are not currently supported for flavor-based instance +reservations. + **Example of Update Lease Response** .. literalinclude:: ../../../doc/api_samples/leases/lease-update-resp.json From 068a3c54453d276b29052631c443ac70c5ccf70d Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Fri, 23 Jan 2026 00:43:56 +0900 Subject: [PATCH 26/31] Remove note about old pip's behavior Recent versions of pip no longer requires that the requirements are listed in specific orders and can resolve dependencies automatically. Change-Id: Icb4310abaf3bf85777d1e4ea2a885f22b684496b Signed-off-by: Takashi Kajinami --- requirements.txt | 4 ---- test-requirements.txt | 3 --- 2 files changed, 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1dd9d5e38..2141adb07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,6 @@ # Requirements lower bounds listed here are our best effort to keep them up to # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. - -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 alembic>=0.9.6 # MIT diff --git a/test-requirements.txt b/test-requirements.txt index 373463a2b..7b1d477ba 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,3 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. hacking>=7.0.0,<7.1.0 # Apache-2.0 ddt>=1.0.1 # MIT From 929ecf59f89ac860e6f6ac3f899f7ea2fed60d72 Mon Sep 17 00:00:00 2001 From: Ivan Anfimov Date: Thu, 8 Jan 2026 15:24:00 +0000 Subject: [PATCH 27/31] Add support for Python 3.13 Change-Id: Ic81e7c0601d62f7bec7d32d3e68227539aa417b1 Signed-off-by: Ivan Anfimov --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 94a03cad3..9b4f32870 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,6 +12,7 @@ classifiers = Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Environment :: OpenStack Development Status :: 3 - Alpha Framework :: Setuptools Plugin @@ -59,4 +60,3 @@ oslo.policy.policies = wsgi_scripts = blazar-api-wsgi = blazar.api.wsgi_app:init_app - From cbb8776ce8ac4ee53c0f945c2b6030473d429751 Mon Sep 17 00:00:00 2001 From: Mark Powers Date: Fri, 18 Jul 2025 12:39:01 -0500 Subject: [PATCH 28/31] Add support for traits in flavor plugin Previously, traits could not be included with a flavor reservation. This commit implements checking flavor traits when querying available hosts. This check is done via the placement API to get resource providers based on a trait query only when traits are specified on the flavor. Co-Authored-By: Michael Sherman Change-Id: I5905d97d85955113d07b418cbe65fbafe132cd4b Signed-off-by: Mark Powers --- blazar/plugins/flavor/flavor_plugin.py | 37 +++++++++- .../plugins/flavor/test_flavor_plugin.py | 71 +++++++++++++++++-- blazar/utils/openstack/exceptions.py | 4 ++ blazar/utils/openstack/placement.py | 27 +++++++ ...flavor-trait-support-da9f68a3bf600fe7.yaml | 4 ++ 5 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/flavor-trait-support-da9f68a3bf600fe7.yaml diff --git a/blazar/plugins/flavor/flavor_plugin.py b/blazar/plugins/flavor/flavor_plugin.py index bddb00b7b..b648e023b 100644 --- a/blazar/plugins/flavor/flavor_plugin.py +++ b/blazar/plugins/flavor/flavor_plugin.py @@ -116,9 +116,6 @@ def _query_available_hosts(self, start_date, end_date, # we should be able to exclude hosts that don't match the # resource requests, e.g. baremetal vs virtual # or missing traits - if resource_traits: - raise mgr_exceptions.NotImplemented( - error="Resource traits not supported yet") hosts = db_api.reservable_host_get_all_by_queries([]) # find reservations for each host in our time period @@ -129,8 +126,42 @@ def _query_available_hosts(self, start_date, end_date, end_date + datetime.timedelta(minutes=CONF.cleaning_time), []) + placement_rps_matching_traits = None + # Only query placement if we have traits to match + if resource_traits: + traits_list = [] + for trait, value in resource_traits.items(): + # We've already validated resource_traits at this point + if value == "required": + traits_list.append(trait) + elif value == "forbidden": + # prefix forbidden traits with `!` + traits_list.append(f"!{trait}") + required_string = ",".join(traits_list) + placement_rps_matching_traits = \ + self._placement_client.list_resource_providers( + query=f"required={required_string}", + microversion="1.22", + ) + placement_rps_matching_traits_hostnames = { + rp['name'] for rp in placement_rps_matching_traits + } if placement_rps_matching_traits else set() + available_hosts = [] for host_info in (reserved_hosts + free_hosts): + hypervisor_hostname = host_info['host']['hypervisor_hostname'] + if ( + resource_traits and + ( + hypervisor_hostname + not in placement_rps_matching_traits_hostnames + ) + ): + LOG.debug( + "Placement filtered out host %s based on traits", + hypervisor_hostname + ) + continue # check how many instances can fit on this host hosts_list = self._get_hosts_list(host_info, resource_request) available_hosts.extend(hosts_list) diff --git a/blazar/tests/plugins/flavor/test_flavor_plugin.py b/blazar/tests/plugins/flavor/test_flavor_plugin.py index 7ae4067cb..69f1f3519 100644 --- a/blazar/tests/plugins/flavor/test_flavor_plugin.py +++ b/blazar/tests/plugins/flavor/test_flavor_plugin.py @@ -29,8 +29,10 @@ class TestFlavorPlugin(tests.DBTestCase): - def _create_fake_host(self): - host_values = fake._get_fake_host_values(id=123) + def _create_fake_host(self, id=123, hypervisor_hostname=None): + host_values = fake._get_fake_host_values(id=id) + if hypervisor_hostname: + host_values['hypervisor_hostname'] = hypervisor_hostname host_values["reservable"] = 1 db_api.host_create(host_values) @@ -349,14 +351,17 @@ def test_create_flavor(self, mock_create): flavorid='12345', name='reservation:12345', vcpus=2, ram=1024, disk=10, ephemeral=100, is_public=False) - def test__query_available_hosts(self): + @mock.patch.object(placement.BlazarPlacementClient, + 'list_resource_providers') + def test__query_available_hosts(self, mock_list_resource_providers): + mock_list_resource_providers.return_value = [] get_reservations = self.patch(db_utils, 'get_reservations_by_host_id') get_reservations.return_value = [] plugin = flavor_plugin.FlavorPlugin() # Check that we can fit 4 VCPU resource requests - self._create_fake_host() + self._create_fake_host(hypervisor_hostname="abc") fake_inventory_values = { 'computehost_id': 123, 'resource_class': 'VCPU', @@ -404,3 +409,61 @@ def test__query_available_hosts(self): } ret = plugin._query_available_hosts(**query_params) self.assertEqual(2, len(ret)) + + # No instances fit when traits do not match request + query_params = { + 'start_date': datetime.datetime(2020, 7, 7, 18, 0), + 'end_date': datetime.datetime(2020, 7, 7, 19, 0), + 'resource_request': { + 'VCPU': 1, + 'MEMORY_MB': 1024 + }, + 'resource_traits': { + "CUSTOM_1": "required" + } + } + ret = plugin._query_available_hosts(**query_params) + self.assertEqual(0, len(ret)) + + # Only hosts with the required trait are returned + self._create_fake_host(id=456, hypervisor_hostname="def") + fake_inventory_values = { + 'computehost_id': 456, + 'resource_class': 'VCPU', + 'total': 3, + 'reserved': 0, + 'min_unit': 1, + 'max_unit': 4, + 'step_size': 1, + 'allocation_ratio': 1.0 + } + db_api.host_resource_inventory_create(fake_inventory_values) + self._create_fake_host(id=789, hypervisor_hostname="ghi") + fake_inventory_values = { + 'computehost_id': 789, + 'resource_class': 'VCPU', + 'total': 3, + 'reserved': 0, + 'min_unit': 1, + 'max_unit': 4, + 'step_size': 1, + 'allocation_ratio': 1.0 + } + db_api.host_resource_inventory_create(fake_inventory_values) + mock_list_resource_providers.return_value = [{"name": "def"}] + query_params = { + 'start_date': datetime.datetime(2020, 7, 7, 18, 0), + 'end_date': datetime.datetime(2020, 7, 7, 19, 0), + 'resource_request': { + 'VCPU': 1, + }, + 'resource_traits': { + "CUSTOM_1": "forbidden", + "CUSTOM_2": "required", + } + } + ret = plugin._query_available_hosts(**query_params) + # 3 available slots on the second host + self.assertEqual(3, len(ret)) + for host in ret: + self.assertEqual(host["id"], '456') diff --git a/blazar/utils/openstack/exceptions.py b/blazar/utils/openstack/exceptions.py index 46eb54f65..5664be916 100644 --- a/blazar/utils/openstack/exceptions.py +++ b/blazar/utils/openstack/exceptions.py @@ -19,6 +19,10 @@ class ResourceProviderRetrievalFailed(exceptions.BlazarException): msg_fmt = _("Failed to get resource provider %(name)s") +class ResourceProviderListFailed(exceptions.BlazarException): + msg_fmt = _("Failed to list resource providers") + + class ResourceProviderCreationFailed(exceptions.BlazarException): msg_fmt = _("Failed to create resource provider %(name)s") diff --git a/blazar/utils/openstack/placement.py b/blazar/utils/openstack/placement.py index 5668d45f6..a4efe4358 100644 --- a/blazar/utils/openstack/placement.py +++ b/blazar/utils/openstack/placement.py @@ -146,6 +146,33 @@ def get_resource_provider(self, rp_name): LOG.error(msg, args) raise exceptions.ResourceProviderRetrievalFailed(name=rp_name) + def list_resource_providers( + self, query="", microversion=PLACEMENT_MICROVERSION): + """Get all resource providers. + + :param query: A string of query parameters. + :param microversion: The microversion to use for the request. + :return: A list of resource provider information + :raise: ResourceProviderListFailed on error. + """ + resp = self.get( + f'/resource_providers?{query}', microversion=microversion) + if resp: + json_resp = resp.json() + if json_resp['resource_providers']: + return json_resp['resource_providers'] + else: + return [] + + msg = ("Failed to get resource providers. " + "Got %(status_code)d: %(err_text)s.") + args = { + 'status_code': resp.status_code, + 'err_text': resp.text, + } + LOG.error(msg, args) + raise exceptions.ResourceProviderListFailed() + def create_resource_provider(self, rp_name, rp_uuid=None, parent_uuid=None): """Calls the placement API to create a new resource provider record. diff --git a/releasenotes/notes/flavor-trait-support-da9f68a3bf600fe7.yaml b/releasenotes/notes/flavor-trait-support-da9f68a3bf600fe7.yaml new file mode 100644 index 000000000..72235f58e --- /dev/null +++ b/releasenotes/notes/flavor-trait-support-da9f68a3bf600fe7.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add support for traits with ``flavor:instance`` reservations. From bca58ddbd79fed38c1cd88d8298bd7cb61d946ec Mon Sep 17 00:00:00 2001 From: Takashi Kajinami Date: Sat, 7 Feb 2026 16:51:47 +0900 Subject: [PATCH 29/31] Remove MANIFEST.in This file isn't needed as far as we use pbr. Trivial-Fix Change-Id: Ifaf86dd72193d2fa41acaab918a78bf3ba3b802e Signed-off-by: Takashi Kajinami --- MANIFEST.in | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index d785bec56..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -include AUTHORS -include README.rst -include ChangeLog -include LICENSE - -recursive-include blazar/locale * - -exclude .gitignore -exclude .gitreview - -global-exclude *.pyc From 084a6788451b80b7fb7bee3d62de8ff0ba152084 Mon Sep 17 00:00:00 2001 From: Matt Crees Date: Thu, 15 Jan 2026 10:52:42 +0000 Subject: [PATCH 30/31] Raise exception in updating flavor:instance leases Makes it clear to the user that this feature is not yet supported. Change-Id: I26c1af3f02a0afc98329b0f9840a2d7b56d7ea90 Signed-off-by: Matt Crees --- blazar/manager/service.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/blazar/manager/service.py b/blazar/manager/service.py index 69b3bfc30..85f5eb5a6 100644 --- a/blazar/manager/service.py +++ b/blazar/manager/service.py @@ -472,6 +472,13 @@ def _add_resource_type(self, reservations, existing_reservations): return reservations + def _handle_resource_type_exception(self, lease): + for reservation in lease['reservations']: + if reservation['resource_type'] == 'flavor:instance': + raise exceptions.NotImplemented( + error="Updating leases for 'flavor:instance' type " + "reservations is not yet supported.") + @status.lease.lease_status( transition=status.lease.UPDATING, result_in=status.lease.STABLE, @@ -498,6 +505,9 @@ def update_lease(self, lease_id, values): return db_api.lease_get(lease_id) lease = db_api.lease_get(lease_id) + + self._handle_resource_type_exception(lease) + start_date = values.get( 'start_date', datetime.datetime.strftime(lease['start_date'], LEASE_DATE_FORMAT)) From 78eb2a7dd36ebb82bbb272814846c5a6ebba9d0a Mon Sep 17 00:00:00 2001 From: OpenStack Release Bot Date: Wed, 11 Mar 2026 10:12:00 +0000 Subject: [PATCH 31/31] Update master for stable/2026.1 Add file to the reno documentation build to show release notes for stable/2026.1. Use pbr instruction to increment the minor version number automatically so that master versions are higher than the versions on stable/2026.1. Sem-Ver: feature Change-Id: I9a28f797f062b9c626d852600662fdc19ee2b24e Signed-off-by: OpenStack Release Bot Generated-By: openstack/project-config:roles/copy-release-tools-scripts/files/release-tools/add_release_note_page.sh --- releasenotes/source/2026.1.rst | 6 ++++++ releasenotes/source/index.rst | 1 + 2 files changed, 7 insertions(+) create mode 100644 releasenotes/source/2026.1.rst diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 000000000..3d2861580 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index edf54f55f..a054fcc06 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,7 @@ Contents :maxdepth: 2 unreleased + 2026.1 2025.2 2025.1 2024.2