Introduce new BaseResourceFixture class
This new class is base for both: HeatStackFixture and ResourceFixture classes. It contains some common pieces of code like e.g. ensuring that quota is set as required by the fixture. This is done because fixture StatelessSecurityGroup, which don't use HeatStackFixture needs to set quota for security groups in Neutron but ensuring neutron (and nova) quotas are set correctly were done only in the HeatStackFixture class so far. Change-Id: Id3d3207f098853469bef87020fc017bec5aaba93
This commit is contained in:
parent
7733488f00
commit
26b6909ceb
15
tobiko/openstack/base/__init__.py
Normal file
15
tobiko/openstack/base/__init__.py
Normal 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.
|
@ -14,18 +14,94 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import collections
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
|
|
||||||
import tobiko
|
import tobiko
|
||||||
from tobiko import config
|
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__)
|
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
|
"""Base class for fixtures intended to manage Openstack resources not
|
||||||
created using Heat, but with openstacksdk or other component clients (such
|
created using Heat, but with openstacksdk or other component clients (such
|
||||||
as neutronclient, novaclient, manilaclient, etc).
|
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
|
Child classes must define any other attributes required by the
|
||||||
resource_create, resource_delete and resource_find methods. Examples:
|
resource_create, resource_delete and resource_find methods. Examples:
|
||||||
prefixes and default_prefixlen are needed for subnet_pools; description and
|
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
|
Child classes must define the resource_create, resource_delete and
|
||||||
resource_find methods. In case of resource_create and resource_find, they
|
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
|
Child classes may optionally implement simple properties to access to
|
||||||
resource_id and resource using a more representative name (these properties
|
resource_id and resource using a more representative name (these properties
|
||||||
@ -84,6 +160,7 @@ class ResourceFixture(tobiko.SharedFixture, abc.ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def setup_fixture(self):
|
def setup_fixture(self):
|
||||||
|
super().setup_fixture()
|
||||||
self.name = self.fixture_name
|
self.name = self.fixture_name
|
||||||
if config.get_bool_env('TOBIKO_PREVENT_CREATE'):
|
if config.get_bool_env('TOBIKO_PREVENT_CREATE'):
|
||||||
LOG.debug("%r should have been already created: %r",
|
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)
|
tobiko.addme_to_shared_resource(__name__, self.name)
|
||||||
|
|
||||||
def try_create_resource(self):
|
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:
|
if not self.resource:
|
||||||
self._resource = self.resource_create()
|
self._resource = self.resource_create()
|
||||||
|
|
@ -41,7 +41,6 @@ HeatStackNotFound = _stack.HeatStackNotFound
|
|||||||
heat_stack_parameters = _stack.heat_stack_parameters
|
heat_stack_parameters = _stack.heat_stack_parameters
|
||||||
find_stack = _stack.find_stack
|
find_stack = _stack.find_stack
|
||||||
list_stacks = _stack.list_stacks
|
list_stacks = _stack.list_stacks
|
||||||
InvalidStackError = _stack.InvalidStackError
|
|
||||||
INIT_IN_PROGRESS = _stack.INIT_IN_PROGRESS
|
INIT_IN_PROGRESS = _stack.INIT_IN_PROGRESS
|
||||||
INIT_COMPLETE = _stack.INIT_COMPLETE
|
INIT_COMPLETE = _stack.INIT_COMPLETE
|
||||||
CREATE_IN_PROGRESS = _stack.CREATE_IN_PROGRESS
|
CREATE_IN_PROGRESS = _stack.CREATE_IN_PROGRESS
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import collections
|
|
||||||
from collections import abc
|
from collections import abc
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
@ -28,8 +27,7 @@ from tobiko import config
|
|||||||
from tobiko.openstack.heat import _client
|
from tobiko.openstack.heat import _client
|
||||||
from tobiko.openstack.heat import _template
|
from tobiko.openstack.heat import _template
|
||||||
from tobiko.openstack import keystone
|
from tobiko.openstack import keystone
|
||||||
from tobiko.openstack import neutron
|
from tobiko.openstack.base import _fixture as base_fixture
|
||||||
from tobiko.openstack import nova
|
|
||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
@ -110,7 +108,7 @@ def find_stack(client: _client.HeatClientType = None,
|
|||||||
|
|
||||||
|
|
||||||
@keystone.skip_unless_has_keystone_credentials()
|
@keystone.skip_unless_has_keystone_credentials()
|
||||||
class HeatStackFixture(tobiko.SharedFixture):
|
class HeatStackFixture(base_fixture.BaseResourceFixture):
|
||||||
"""Manages Heat stacks."""
|
"""Manages Heat stacks."""
|
||||||
|
|
||||||
client: _client.HeatClientType = None
|
client: _client.HeatClientType = None
|
||||||
@ -124,8 +122,6 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||||||
stack: typing.Optional[StackType] = None
|
stack: typing.Optional[StackType] = None
|
||||||
stack_name: typing.Optional[str] = None
|
stack_name: typing.Optional[str] = None
|
||||||
parameters: typing.Optional['HeatStackParametersFixture'] = None
|
parameters: typing.Optional['HeatStackParametersFixture'] = None
|
||||||
project: typing.Optional[str] = None
|
|
||||||
user: typing.Optional[str] = None
|
|
||||||
output_needs_stack_complete: bool = True
|
output_needs_stack_complete: bool = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -150,12 +146,10 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||||||
self.wait_interval = wait_interval
|
self.wait_interval = wait_interval
|
||||||
|
|
||||||
def setup_fixture(self):
|
def setup_fixture(self):
|
||||||
|
super().setup_fixture()
|
||||||
self.setup_stack_name()
|
self.setup_stack_name()
|
||||||
self.setup_template()
|
self.setup_template()
|
||||||
self.setup_parameters()
|
self.setup_parameters()
|
||||||
self.setup_client()
|
|
||||||
self.setup_project()
|
|
||||||
self.setup_user()
|
|
||||||
self.setup_stack()
|
self.setup_stack()
|
||||||
|
|
||||||
def setup_template(self):
|
def setup_template(self):
|
||||||
@ -180,14 +174,6 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||||||
def session(self):
|
def session(self):
|
||||||
return self.setup_client().http_client.session
|
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:
|
def setup_stack(self) -> stacks.Stack:
|
||||||
stack = self.create_stack()
|
stack = self.create_stack()
|
||||||
tobiko.addme_to_shared_resource(__name__, stack.stack_name)
|
tobiko.addme_to_shared_resource(__name__, stack.stack_name)
|
||||||
@ -207,7 +193,7 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||||||
try:
|
try:
|
||||||
stack = self.try_create_stack()
|
stack = self.try_create_stack()
|
||||||
break
|
break
|
||||||
except InvalidStackError:
|
except base_fixture.InvalidFixtureError:
|
||||||
LOG.exception(f"Error creating stack '{self.stack_name}'",
|
LOG.exception(f"Error creating stack '{self.stack_name}'",
|
||||||
exc_info=1)
|
exc_info=1)
|
||||||
if attempt.is_last:
|
if attempt.is_last:
|
||||||
@ -290,7 +276,7 @@ class HeatStackFixture(tobiko.SharedFixture):
|
|||||||
f"id='{stack_id}'.")
|
f"id='{stack_id}'.")
|
||||||
try:
|
try:
|
||||||
stack = self.validate_created_stack()
|
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}, "'
|
LOG.debug(f'Deleting invalid stack (name={self.stack_name}, "'
|
||||||
f'"id={stack_id}): {ex}')
|
f'"id={stack_id}): {ex}')
|
||||||
# the stack shelf counter does not need to be decreased here,
|
# 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)
|
message = "Object {!r} has no attribute {!r}".format(self, name)
|
||||||
raise AttributeError(message)
|
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):
|
class HeatStackKeyError(tobiko.TobikoException):
|
||||||
message = "key {key!r} not found in stack {name!r}"
|
message = "key {key!r} not found in stack {name!r}"
|
||||||
@ -717,11 +672,7 @@ class HeatStackNotFound(HeatStackError):
|
|||||||
message = "stack {name!r} not found"
|
message = "stack {name!r} not found"
|
||||||
|
|
||||||
|
|
||||||
class InvalidStackError(HeatStackError):
|
class InvalidHeatStackStatus(base_fixture.InvalidFixtureError):
|
||||||
message = "invalid stack {name!r}"
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidHeatStackStatus(InvalidStackError):
|
|
||||||
message = ("stack {name!r} status {observed!r} not in {expected!r}\n"
|
message = ("stack {name!r} status {observed!r} not in {expected!r}\n"
|
||||||
"{status_reason!s}")
|
"{status_reason!s}")
|
||||||
|
|
||||||
|
@ -26,8 +26,8 @@ import tobiko
|
|||||||
from tobiko import config
|
from tobiko import config
|
||||||
from tobiko.openstack import heat
|
from tobiko.openstack import heat
|
||||||
from tobiko.openstack import neutron
|
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 _hot
|
||||||
from tobiko.openstack.stacks import _fixture
|
|
||||||
from tobiko.shell import ip
|
from tobiko.shell import ip
|
||||||
from tobiko.shell import sh
|
from tobiko.shell import sh
|
||||||
from tobiko.shell import ssh
|
from tobiko.shell import ssh
|
||||||
@ -284,7 +284,7 @@ class RouterNoSnatStackFixture(RouterStackFixture):
|
|||||||
|
|
||||||
|
|
||||||
@neutron.skip_if_missing_networking_extensions('subnet_allocation')
|
@neutron.skip_if_missing_networking_extensions('subnet_allocation')
|
||||||
class SubnetPoolFixture(_fixture.ResourceFixture):
|
class SubnetPoolFixture(base_fixture.ResourceFixture):
|
||||||
"""Neutron Subnet Pool Fixture.
|
"""Neutron Subnet Pool Fixture.
|
||||||
|
|
||||||
A subnet pool is a dependency of network fixtures with either IPv4 or
|
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')
|
@neutron.skip_if_missing_networking_extensions('stateful-security-group')
|
||||||
class StatelessSecurityGroupFixture(_fixture.ResourceFixture):
|
class StatelessSecurityGroupFixture(base_fixture.ResourceFixture):
|
||||||
"""Neutron Stateless Security Group Fixture.
|
"""Neutron Stateless Security Group Fixture.
|
||||||
|
|
||||||
This SG will by default allow SSH and ICMP to the instance and also
|
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.description = description or self.description
|
||||||
self.rules = rules or self.rules
|
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
|
@property
|
||||||
def security_group_id(self):
|
def security_group_id(self):
|
||||||
return self.resource_id
|
return self.resource_id
|
||||||
|
@ -28,6 +28,7 @@ from tobiko.openstack import glance
|
|||||||
from tobiko.openstack import heat
|
from tobiko.openstack import heat
|
||||||
from tobiko.openstack import neutron
|
from tobiko.openstack import neutron
|
||||||
from tobiko.openstack import nova
|
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 _hot
|
||||||
from tobiko.openstack.stacks import _neutron
|
from tobiko.openstack.stacks import _neutron
|
||||||
from tobiko.shell import curl
|
from tobiko.shell import curl
|
||||||
@ -271,7 +272,8 @@ class ServerStackFixture(heat.HeatStackFixture, abc.ABC):
|
|||||||
self.validate_different_host_scheduler_hints(
|
self.validate_different_host_scheduler_hints(
|
||||||
hypervisor=hypervisor)
|
hypervisor=hypervisor)
|
||||||
except nova.MigrateServerError as ex:
|
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):
|
def validate_same_host_scheduler_hints(self, hypervisor):
|
||||||
if self.same_host:
|
if self.same_host:
|
||||||
|
@ -22,10 +22,10 @@ import pytest
|
|||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
import tobiko
|
import tobiko
|
||||||
from tobiko.openstack import heat
|
|
||||||
from tobiko.openstack import keystone
|
from tobiko.openstack import keystone
|
||||||
from tobiko.openstack import nova
|
from tobiko.openstack import nova
|
||||||
from tobiko.openstack import stacks
|
from tobiko.openstack import stacks
|
||||||
|
from tobiko.openstack.base import _fixture as base_fixture
|
||||||
from tobiko.shell import ping
|
from tobiko.shell import ping
|
||||||
|
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ class CirrosServerStackFixture(stacks.CirrosServerStackFixture):
|
|||||||
try:
|
try:
|
||||||
nova.activate_server(server)
|
nova.activate_server(server)
|
||||||
except nova.WaitForServerStatusTimeout as ex:
|
except nova.WaitForServerStatusTimeout as ex:
|
||||||
raise heat.InvalidStackError(
|
raise base_fixture.InvalidFixtureError(
|
||||||
name=self.stack_name) from ex
|
name=self.stack_name) from ex
|
||||||
return stack
|
return stack
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user