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 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. 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 diff --git a/blazar/config.py b/blazar/config.py index 621dc8927..83feb1252 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 = [ @@ -24,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'), @@ -91,18 +88,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/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/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) diff --git a/blazar/manager/service.py b/blazar/manager/service.py index 22dbe5a40..b8cdaee90 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, @@ -508,6 +509,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, @@ -534,6 +542,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)) 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/blazar/plugins/flavor/flavor_plugin.py b/blazar/plugins/flavor/flavor_plugin.py index eb6bcfe76..3c0af75c1 100644 --- a/blazar/plugins/flavor/flavor_plugin.py +++ b/blazar/plugins/flavor/flavor_plugin.py @@ -133,8 +133,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) @@ -391,6 +425,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/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..6cf73f1e0 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 @@ -549,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) @@ -770,8 +772,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..dd6c5f9b6 100644 --- a/blazar/plugins/oshosts/host_plugin.py +++ b/blazar/plugins/oshosts/host_plugin.py @@ -15,13 +15,14 @@ # under the License. import datetime -from random import Random +import random import retrying from novaclient import exceptions as nova_exceptions 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 @@ -653,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 [] @@ -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/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/policy.py b/blazar/policy.py index a4e2f0c58..d8f6dcbac 100644 --- a/blazar/policy.py +++ b/blazar/policy.py @@ -19,10 +19,10 @@ 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 +from blazar.db import api as db_api from blazar import exceptions from blazar import policies @@ -30,13 +30,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 @@ -109,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/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/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 old mode 100755 new mode 100644 index 35e321e72..ab8eb20cc --- 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 old mode 100755 new mode 100644 index 0a76d437f..4d1d42fc7 --- 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 704b6ce83..0ebc31a78 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 @@ -188,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', @@ -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'], @@ -538,7 +537,7 @@ def test_create_lease_dry_run(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) @@ -548,11 +547,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) @@ -583,11 +582,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') @@ -619,7 +618,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') @@ -632,30 +631,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, @@ -681,13 +680,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) @@ -725,7 +724,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) @@ -733,7 +732,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( @@ -741,8 +740,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) @@ -775,21 +774,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' } @@ -816,10 +815,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) @@ -853,10 +850,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( @@ -909,10 +904,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( @@ -956,10 +949,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) @@ -981,10 +972,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) @@ -1013,10 +1002,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) @@ -1047,10 +1034,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( @@ -1097,10 +1082,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( @@ -1162,10 +1145,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( @@ -1232,10 +1213,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) @@ -1272,10 +1251,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) @@ -1311,10 +1288,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) @@ -1332,10 +1307,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) @@ -1346,10 +1319,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) @@ -1360,10 +1331,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) @@ -1374,10 +1343,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) @@ -1412,10 +1379,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) @@ -1460,10 +1425,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, @@ -1743,10 +1706,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, @@ -1777,10 +1738,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, @@ -1811,10 +1770,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/flavor/test_flavor_plugin.py b/blazar/tests/plugins/flavor/test_flavor_plugin.py index 283dfb978..6450b32bf 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) @@ -354,16 +356,19 @@ 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): + @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', @@ -412,6 +417,64 @@ 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') + @mock.patch.object(flavor_plugin.FlavorPlugin, 'get_enforcement_resources') @mock.patch.object(flavors.FlavorManager, 'create') def test_get_enforcement_resources(self, mock_create_flavor, 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..18b27d2ec 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) @@ -2433,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': @@ -2467,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': @@ -2501,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': @@ -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/blazar/tests/test_policy.py b/blazar/tests/test_policy.py index df680324c..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 @@ -32,7 +34,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, @@ -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/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/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/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", 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 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/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 `__ 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 `__ 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 `__ 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. 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. 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). 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. 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 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 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/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/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 351c12ebb..a054fcc06 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -8,6 +8,9 @@ Contents :maxdepth: 2 unreleased + 2026.1 + 2025.2 + 2025.1 2024.2 2024.1 2023.2 diff --git a/requirements.txt b/requirements.txt index ab3de6491..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 @@ -22,11 +18,11 @@ 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 -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 diff --git a/setup.cfg b/setup.cfg index 98bda8baf..9b4f32870 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,16 +3,16 @@ 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 + Programming Language :: Python :: 3.13 Environment :: OpenStack Development Status :: 3 - Alpha Framework :: Setuptools Plugin @@ -54,12 +54,9 @@ 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 wsgi_scripts = blazar-api-wsgi = blazar.api.wsgi_app:init_app - 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 diff --git a/tox.ini b/tox.ini index c4544d263..3f59bebfe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,8 @@ [tox] -envlist = py39,py38,pep8 +envlist = py3,pep8 minversion = 3.18.0 -ignore_basepython_conflict = True [testenv] -basepython = python3 usedevelop = True allowlist_externals = rm deps =