Merge "Introduce new BaseResourceFixture class"

This commit is contained in:
Zuul 2024-03-19 22:13:00 +00:00 committed by Gerrit Code Review
commit 33596a954d
7 changed files with 118 additions and 65 deletions

View File

@ -0,0 +1,15 @@
# Copyright (c) 2024 Red Hat, Inc.
#
# All Rights Reserved.
#
# 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.

View File

@ -14,18 +14,94 @@
from __future__ import absolute_import
import abc
import collections
import typing
from oslo_log import log
import tobiko
from tobiko import config
from tobiko.openstack import keystone
from tobiko.openstack.neutron import _quota_set as neutron_quota
from tobiko.openstack.nova import _quota_set as nova_quota
LOG = log.getLogger(__name__)
class ResourceFixture(tobiko.SharedFixture, abc.ABC):
class InvalidFixtureError(tobiko.TobikoException):
message = "invalid fixture {name!r}"
class BaseResourceFixture(tobiko.SharedFixture):
"""Base class for fixtures both types: those which uses heat stacks and
those which are not.
"""
client: keystone.KeystoneClient = None
project: typing.Optional[str] = None
user: typing.Optional[str] = None
def setup_fixture(self):
self.setup_client()
self.setup_project()
self.setup_user()
def setup_project(self):
if self.project is None:
self.project = keystone.get_project_id(session=self.session)
def setup_user(self):
if self.user is None:
self.user = keystone.get_user_id(session=self.session)
@property
def session(self):
return self.setup_client().session
def setup_client(self) -> keystone.KeystoneClient:
client = self.client
# NOTE(slaweq): it seems that due to bug
# https://github.com/python/mypy/issues/11673
# in mypy this line is causing arg-type error so lets
# ignore it for now
if not isinstance(
client, keystone.KeystoneClient): # type: ignore[arg-type]
self.client = client = keystone.keystone_client(self.client)
return client
def ensure_quota_limits(self):
"""Ensures quota limits before creating a new stack
"""
try:
self.ensure_neutron_quota_limits()
self.ensure_nova_quota_limits()
except (nova_quota.EnsureNovaQuotaLimitsError,
neutron_quota.EnsureNeutronQuotaLimitsError) as ex:
raise InvalidFixtureError(name=self.fixture_name) from ex
def ensure_neutron_quota_limits(self):
required_quota_set = self.neutron_required_quota_set
if required_quota_set:
neutron_quota.ensure_neutron_quota_limits(project=self.project,
**required_quota_set)
def ensure_nova_quota_limits(self):
required_quota_set = self.nova_required_quota_set
if required_quota_set:
nova_quota.ensure_nova_quota_limits(project=self.project,
user=self.user,
**required_quota_set)
@property
def neutron_required_quota_set(self) -> typing.Dict[str, int]:
return collections.defaultdict(int)
@property
def nova_required_quota_set(self) -> typing.Dict[str, int]:
return collections.defaultdict(int)
class ResourceFixture(BaseResourceFixture, abc.ABC):
"""Base class for fixtures intended to manage Openstack resources not
created using Heat, but with openstacksdk or other component clients (such
as neutronclient, novaclient, manilaclient, etc).
@ -41,11 +117,11 @@ class ResourceFixture(tobiko.SharedFixture, abc.ABC):
Child classes must define any other attributes required by the
resource_create, resource_delete and resource_find methods. Examples:
prefixes and default_prefixlen are needed for subnet_pools; description and
rules are needed for secutiry_groups; etc.
rules are needed for security_groups; etc.
Child classes must define the resource_create, resource_delete and
resource_find methods. In case of resource_create and resource_find, they
should return an object with the type defined for self._resouce.
should return an object with the type defined for self._resource.
Child classes may optionally implement simple properties to access to
resource_id and resource using a more representative name (these properties
@ -84,6 +160,7 @@ class ResourceFixture(tobiko.SharedFixture, abc.ABC):
pass
def setup_fixture(self):
super().setup_fixture()
self.name = self.fixture_name
if config.get_bool_env('TOBIKO_PREVENT_CREATE'):
LOG.debug("%r should have been already created: %r",
@ -98,6 +175,9 @@ class ResourceFixture(tobiko.SharedFixture, abc.ABC):
tobiko.addme_to_shared_resource(__name__, self.name)
def try_create_resource(self):
# Ensure quota limits are OK just in time before start creating
# a new stack
self.ensure_quota_limits()
if not self.resource:
self._resource = self.resource_create()

View File

@ -41,7 +41,6 @@ HeatStackNotFound = _stack.HeatStackNotFound
heat_stack_parameters = _stack.heat_stack_parameters
find_stack = _stack.find_stack
list_stacks = _stack.list_stacks
InvalidStackError = _stack.InvalidStackError
INIT_IN_PROGRESS = _stack.INIT_IN_PROGRESS
INIT_COMPLETE = _stack.INIT_COMPLETE
CREATE_IN_PROGRESS = _stack.CREATE_IN_PROGRESS

View File

@ -13,7 +13,6 @@
# under the License.
from __future__ import absolute_import
import collections
from collections import abc
import random
import time
@ -28,8 +27,7 @@ from tobiko import config
from tobiko.openstack.heat import _client
from tobiko.openstack.heat import _template
from tobiko.openstack import keystone
from tobiko.openstack import neutron
from tobiko.openstack import nova
from tobiko.openstack.base import _fixture as base_fixture
LOG = log.getLogger(__name__)
@ -110,7 +108,7 @@ def find_stack(client: _client.HeatClientType = None,
@keystone.skip_unless_has_keystone_credentials()
class HeatStackFixture(tobiko.SharedFixture):
class HeatStackFixture(base_fixture.BaseResourceFixture):
"""Manages Heat stacks."""
client: _client.HeatClientType = None
@ -124,8 +122,6 @@ class HeatStackFixture(tobiko.SharedFixture):
stack: typing.Optional[StackType] = None
stack_name: typing.Optional[str] = None
parameters: typing.Optional['HeatStackParametersFixture'] = None
project: typing.Optional[str] = None
user: typing.Optional[str] = None
output_needs_stack_complete: bool = True
def __init__(
@ -150,12 +146,10 @@ class HeatStackFixture(tobiko.SharedFixture):
self.wait_interval = wait_interval
def setup_fixture(self):
super().setup_fixture()
self.setup_stack_name()
self.setup_template()
self.setup_parameters()
self.setup_client()
self.setup_project()
self.setup_user()
self.setup_stack()
def setup_template(self):
@ -180,14 +174,6 @@ class HeatStackFixture(tobiko.SharedFixture):
def session(self):
return self.setup_client().http_client.session
def setup_project(self):
if self.project is None:
self.project = keystone.get_project_id(session=self.session)
def setup_user(self):
if self.user is None:
self.user = keystone.get_user_id(session=self.session)
def setup_stack(self) -> stacks.Stack:
stack = self.create_stack()
tobiko.addme_to_shared_resource(__name__, stack.stack_name)
@ -207,7 +193,7 @@ class HeatStackFixture(tobiko.SharedFixture):
try:
stack = self.try_create_stack()
break
except InvalidStackError:
except base_fixture.InvalidFixtureError:
LOG.exception(f"Error creating stack '{self.stack_name}'",
exc_info=1)
if attempt.is_last:
@ -290,7 +276,7 @@ class HeatStackFixture(tobiko.SharedFixture):
f"id='{stack_id}'.")
try:
stack = self.validate_created_stack()
except InvalidStackError as ex:
except base_fixture.InvalidFixtureError as ex:
LOG.debug(f'Deleting invalid stack (name={self.stack_name}, "'
f'"id={stack_id}): {ex}')
# the stack shelf counter does not need to be decreased here,
@ -517,37 +503,6 @@ class HeatStackFixture(tobiko.SharedFixture):
message = "Object {!r} has no attribute {!r}".format(self, name)
raise AttributeError(message)
def ensure_quota_limits(self):
"""Ensures quota limits before creating a new stack
"""
try:
self.ensure_neutron_quota_limits()
self.ensure_nova_quota_limits()
except (nova.EnsureNovaQuotaLimitsError,
neutron.EnsureNeutronQuotaLimitsError) as ex:
raise InvalidStackError(name=self.stack_name) from ex
def ensure_neutron_quota_limits(self):
required_quota_set = self.neutron_required_quota_set
if required_quota_set:
neutron.ensure_neutron_quota_limits(project=self.project,
**required_quota_set)
def ensure_nova_quota_limits(self):
required_quota_set = self.nova_required_quota_set
if required_quota_set:
nova.ensure_nova_quota_limits(project=self.project,
user=self.user,
**required_quota_set)
@property
def neutron_required_quota_set(self) -> typing.Dict[str, int]:
return collections.defaultdict(int)
@property
def nova_required_quota_set(self) -> typing.Dict[str, int]:
return collections.defaultdict(int)
class HeatStackKeyError(tobiko.TobikoException):
message = "key {key!r} not found in stack {name!r}"
@ -717,11 +672,7 @@ class HeatStackNotFound(HeatStackError):
message = "stack {name!r} not found"
class InvalidStackError(HeatStackError):
message = "invalid stack {name!r}"
class InvalidHeatStackStatus(InvalidStackError):
class InvalidHeatStackStatus(base_fixture.InvalidFixtureError):
message = ("stack {name!r} status {observed!r} not in {expected!r}\n"
"{status_reason!s}")

View File

@ -26,8 +26,8 @@ import tobiko
from tobiko import config
from tobiko.openstack import heat
from tobiko.openstack import neutron
from tobiko.openstack.base import _fixture as base_fixture
from tobiko.openstack.stacks import _hot
from tobiko.openstack.stacks import _fixture
from tobiko.shell import ip
from tobiko.shell import sh
from tobiko.shell import ssh
@ -284,7 +284,7 @@ class RouterNoSnatStackFixture(RouterStackFixture):
@neutron.skip_if_missing_networking_extensions('subnet_allocation')
class SubnetPoolFixture(_fixture.ResourceFixture):
class SubnetPoolFixture(base_fixture.ResourceFixture):
"""Neutron Subnet Pool Fixture.
A subnet pool is a dependency of network fixtures with either IPv4 or
@ -583,7 +583,7 @@ class SecurityGroupsFixture(heat.HeatStackFixture):
@neutron.skip_if_missing_networking_extensions('stateful-security-group')
class StatelessSecurityGroupFixture(_fixture.ResourceFixture):
class StatelessSecurityGroupFixture(base_fixture.ResourceFixture):
"""Neutron Stateless Security Group Fixture.
This SG will by default allow SSH and ICMP to the instance and also
@ -615,6 +615,12 @@ class StatelessSecurityGroupFixture(_fixture.ResourceFixture):
self.description = description or self.description
self.rules = rules or self.rules
@property
def neutron_required_quota_set(self) -> typing.Dict[str, int]:
requirements = super().neutron_required_quota_set
requirements['security_group'] += 1
return requirements
@property
def security_group_id(self):
return self.resource_id

View File

@ -28,6 +28,7 @@ from tobiko.openstack import glance
from tobiko.openstack import heat
from tobiko.openstack import neutron
from tobiko.openstack import nova
from tobiko.openstack.base import _fixture as base_fixture
from tobiko.openstack.stacks import _hot
from tobiko.openstack.stacks import _neutron
from tobiko.shell import curl
@ -271,7 +272,8 @@ class ServerStackFixture(heat.HeatStackFixture, abc.ABC):
self.validate_different_host_scheduler_hints(
hypervisor=hypervisor)
except nova.MigrateServerError as ex:
raise heat.InvalidStackError(name=self.stack_name) from ex
raise base_fixture.InvalidFixtureError(
name=self.stack_name) from ex
def validate_same_host_scheduler_hints(self, hypervisor):
if self.same_host:

View File

@ -22,10 +22,10 @@ import pytest
import testtools
import tobiko
from tobiko.openstack import heat
from tobiko.openstack import keystone
from tobiko.openstack import nova
from tobiko.openstack import stacks
from tobiko.openstack.base import _fixture as base_fixture
from tobiko.shell import ping
@ -177,7 +177,7 @@ class CirrosServerStackFixture(stacks.CirrosServerStackFixture):
try:
nova.activate_server(server)
except nova.WaitForServerStatusTimeout as ex:
raise heat.InvalidStackError(
raise base_fixture.InvalidFixtureError(
name=self.stack_name) from ex
return stack