Update Nova server APIs an

- ensure there quota limits before creating a new server
- ensure a server is in desired state (ACTIVE or SHUTOFF)

Change-Id: Id91123fd4ca4114d92bc7b836257e64505b10404
This commit is contained in:
Federico Ressi 2021-02-01 12:39:39 +01:00
parent cba8d571bf
commit 9aaa10c9d7
8 changed files with 418 additions and 126 deletions

View File

@ -16,6 +16,7 @@ from __future__ import absolute_import
from tobiko.openstack.keystone import _client from tobiko.openstack.keystone import _client
from tobiko.openstack.keystone import _clouds_file from tobiko.openstack.keystone import _clouds_file
from tobiko.openstack.keystone import _credentials from tobiko.openstack.keystone import _credentials
from tobiko.openstack.keystone import _resource
from tobiko.openstack.keystone import _services from tobiko.openstack.keystone import _services
from tobiko.openstack.keystone import _session from tobiko.openstack.keystone import _session
@ -47,6 +48,13 @@ InvalidKeystoneCredentials = _credentials.InvalidKeystoneCredentials
DEFAULT_KEYSTONE_CREDENTIALS_FIXTURES = \ DEFAULT_KEYSTONE_CREDENTIALS_FIXTURES = \
_credentials.DEFAULT_KEYSTONE_CREDENTIALS_FIXTURES _credentials.DEFAULT_KEYSTONE_CREDENTIALS_FIXTURES
get_keystone_resource_id = _resource.get_keystone_resource_id
get_project_id = _resource.get_project_id
get_user_id = _resource.get_user_id
KeystoneResourceType = _resource.KeystoneResourceType
ProjectType = _resource.ProjectType
UserType = _resource.UserType
has_service = _services.has_service has_service = _services.has_service
is_service_missing = _services.is_service_missing is_service_missing = _services.is_service_missing
skip_if_missing_service = _services.skip_if_missing_service skip_if_missing_service = _services.skip_if_missing_service

View File

@ -13,6 +13,8 @@
# under the License. # under the License.
from __future__ import absolute_import from __future__ import absolute_import
import typing
from keystoneclient import base from keystoneclient import base
from keystoneclient import client as keystoneclient from keystoneclient import client as keystoneclient
from keystoneclient.v2_0 import client as v2_client from keystoneclient.v2_0 import client as v2_client
@ -36,13 +38,16 @@ class KeystoneClientManager(_client.OpenstackClientManager):
CLIENTS = KeystoneClientManager() CLIENTS = KeystoneClientManager()
CLIENT_CLASSES = (v2_client.Client, v3_client.Client) CLIENT_CLASSES = (v2_client.Client, v3_client.Client)
KeystoneClient = typing.Union[v2_client.Client, v3_client.Client]
KeystoneClientType = typing.Union[KeystoneClient,
KeystoneClientFixture,
typing.Type[KeystoneClientFixture],
None]
def keystone_client(obj): def keystone_client(obj: KeystoneClientType) -> KeystoneClient:
if not obj: if obj is None:
return get_keystone_client() return get_keystone_client()
if isinstance(obj, CLIENT_CLASSES): if isinstance(obj, CLIENT_CLASSES):
@ -50,14 +55,13 @@ def keystone_client(obj):
fixture = tobiko.setup_fixture(obj) fixture = tobiko.setup_fixture(obj)
if isinstance(fixture, KeystoneClientFixture): if isinstance(fixture, KeystoneClientFixture):
return fixture.client return tobiko.setup_fixture(obj).client
message = "Object {!r} is not a KeystoneClientFixture".format(obj) raise TypeError(f"Object {obj} is not a KeystoneClientFixture")
raise TypeError(message)
def get_keystone_client(session=None, shared=True, init_client=None, def get_keystone_client(session=None, shared=True, init_client=None,
manager=None): manager=None) -> KeystoneClient:
manager = manager or CLIENTS manager = manager or CLIENTS
client = manager.get_client(session=session, shared=shared, client = manager.get_client(session=session, shared=shared,
init_client=init_client) init_client=init_client)

View File

@ -0,0 +1,60 @@
# Copyright 2021 Red Hat
#
# 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.
from __future__ import absolute_import
import typing
from keystoneclient import base
from keystoneclient.v2_0 import tenants as tenants_v2
from keystoneclient.v2_0 import users as users_v2
from keystoneclient.v3 import projects as projects_v3
from keystoneclient.v3 import users as users_v3
KeystoneResourceType = typing.Union[str, base.Resource]
ProjectType = typing.Union[str, tenants_v2.Tenant, projects_v3.Project]
UserType = typing.Union[str, users_v2.User, users_v3.User]
def get_project_id(
project: typing.Optional[ProjectType] = None,
session=None) -> str:
if project is not None:
return get_keystone_resource_id(project)
if session is not None:
return session.auth.auth_ref.project_id
raise ValueError("'project' and 'session' can't be None ata the same "
"time.")
def get_user_id(
user: typing.Optional[UserType] = None,
session=None) -> str:
if user is not None:
return get_keystone_resource_id(user)
if session is not None:
return session.auth.auth_ref.user_id
raise ValueError("'project' and 'session' can't be None ata the same "
"time.")
def get_keystone_resource_id(resource: KeystoneResourceType) -> str:
if isinstance(resource, str):
return resource
if isinstance(resource, base.Resource):
return resource.id
raise TypeError(f"Object {resource} is not a valid Keystone resource "
"type")

View File

@ -16,11 +16,13 @@ from __future__ import absolute_import
from tobiko.openstack.nova import _client from tobiko.openstack.nova import _client
from tobiko.openstack.nova import _cloud_init from tobiko.openstack.nova import _cloud_init
from tobiko.openstack.nova import _hypervisor from tobiko.openstack.nova import _hypervisor
from tobiko.openstack.nova import _quota_set
from tobiko.openstack.nova import _server from tobiko.openstack.nova import _server
from tobiko.openstack.nova import _service from tobiko.openstack.nova import _service
CLIENT_CLASSES = _client.CLIENT_CLASSES CLIENT_CLASSES = _client.CLIENT_CLASSES
NovaClient = _client.NovaClient
NovaClientType = _client.NovaClientType
get_console_output = _client.get_console_output get_console_output = _client.get_console_output
get_nova_client = _client.get_nova_client get_nova_client = _client.get_nova_client
get_server = _client.get_server get_server = _client.get_server
@ -38,8 +40,10 @@ WaitForServerStatusError = _client.WaitForServerStatusError
WaitForServerStatusTimeout = _client.WaitForServerStatusTimeout WaitForServerStatusTimeout = _client.WaitForServerStatusTimeout
shutoff_server = _client.shutoff_server shutoff_server = _client.shutoff_server
activate_server = _client.activate_server activate_server = _client.activate_server
ensure_server_status = _client.ensure_server_status
migrate_server = _client.migrate_server migrate_server = _client.migrate_server
confirm_resize = _client.confirm_resize confirm_resize = _client.confirm_resize
NovaServer = _client.NovaServer
WaitForCloudInitTimeoutError = _cloud_init.WaitForCloudInitTimeoutError WaitForCloudInitTimeoutError = _cloud_init.WaitForCloudInitTimeoutError
cloud_config = _cloud_init.cloud_config cloud_config = _cloud_init.cloud_config
@ -54,6 +58,10 @@ get_different_host_hypervisors = _hypervisor.get_different_host_hypervisors
get_server_hypervisor = _hypervisor.get_server_hypervisor get_server_hypervisor = _hypervisor.get_server_hypervisor
get_servers_hypervisors = _hypervisor.get_servers_hypervisors get_servers_hypervisors = _hypervisor.get_servers_hypervisors
get_nova_quota_set = _quota_set.get_nova_quota_set
ensure_nova_quota_limits = _quota_set.ensure_nova_quota_limits
set_nova_quota_set = _quota_set.set_nova_quota_set
find_server_ip_address = _server.find_server_ip_address find_server_ip_address = _server.find_server_ip_address
HasServerMixin = _server.HasServerMixin HasServerMixin = _server.HasServerMixin
list_server_ip_addresses = _server.list_server_ip_addresses list_server_ip_addresses = _server.list_server_ip_addresses

View File

@ -13,37 +13,45 @@
# under the License. # under the License.
from __future__ import absolute_import from __future__ import absolute_import
import time import typing
from novaclient import client as novaclient import novaclient
from novaclient.v2 import client as client_v2 import novaclient.v2.client
from oslo_log import log from oslo_log import log
import tobiko import tobiko
from tobiko.openstack import _client from tobiko.openstack import _client
CLIENT_CLASSES = (client_v2.Client,)
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
CLIENT_CLASSES = (novaclient.v2.client.Client,)
NovaClient = typing.Union[novaclient.v2.client.Client]
NovaServer = typing.Union[novaclient.v2.servers.Server]
class NovaClientFixture(_client.OpenstackClientFixture): class NovaClientFixture(_client.OpenstackClientFixture):
def init_client(self, session): def init_client(self, session) -> NovaClient:
return novaclient.Client('2', session=session) return novaclient.client.Client('2', session=session)
class NovaClientManager(_client.OpenstackClientManager): class NovaClientManager(_client.OpenstackClientManager):
def create_client(self, session): def create_client(self, session) -> NovaClientFixture:
return NovaClientFixture(session=session) return NovaClientFixture(session=session)
CLIENTS = NovaClientManager() CLIENTS = NovaClientManager()
NovaClientType = typing.Union[NovaClient,
NovaClientFixture,
typing.Type[NovaClientFixture],
None]
def nova_client(obj):
if not obj: def nova_client(obj: NovaClientType) -> NovaClient:
if obj is None:
return get_nova_client() return get_nova_client()
if isinstance(obj, CLIENT_CLASSES): if isinstance(obj, CLIENT_CLASSES):
@ -51,14 +59,15 @@ def nova_client(obj):
fixture = tobiko.setup_fixture(obj) fixture = tobiko.setup_fixture(obj)
if isinstance(fixture, NovaClientFixture): if isinstance(fixture, NovaClientFixture):
assert fixture.client is not None
return fixture.client return fixture.client
message = "Object {!r} is not a NovaClientFixture".format(obj) message = f"Object '{obj}' is not a NovaClientFixture"
raise TypeError(message) raise TypeError(message)
def get_nova_client(session=None, shared=True, init_client=None, def get_nova_client(session=None, shared=True, init_client=None,
manager=None): manager=None) -> NovaClient:
manager = manager or CLIENTS manager = manager or CLIENTS
client = manager.get_client(session=session, shared=shared, client = manager.get_client(session=session, shared=shared,
init_client=init_client) init_client=init_client)
@ -66,13 +75,13 @@ def get_nova_client(session=None, shared=True, init_client=None,
return client.client return client.client
def list_hypervisors(client=None, detailed=True, **params): def list_hypervisors(client: NovaClientType = None, detailed=True, **params):
client = nova_client(client) client = nova_client(client)
hypervisors = client.hypervisors.list(detailed=detailed) hypervisors = client.hypervisors.list(detailed=detailed)
return tobiko.select(hypervisors).with_attributes(**params) return tobiko.select(hypervisors).with_attributes(**params)
def find_hypervisor(client=None, unique=False, **params): def find_hypervisor(client: NovaClientType = None, unique=False, **params):
hypervisors = list_hypervisors(client=client, **params) hypervisors = list_hypervisors(client=client, **params)
if unique: if unique:
return hypervisors.unique return hypervisors.unique
@ -80,13 +89,14 @@ def find_hypervisor(client=None, unique=False, **params):
return hypervisors.first return hypervisors.first
def list_servers(client=None, **params): def list_servers(client: NovaClientType = None, **params) -> \
client = nova_client(client) tobiko.Selection[NovaServer]:
servers = client.servers.list() servers = nova_client(client).servers.list()
return tobiko.select(servers).with_attributes(**params) return tobiko.select(servers).with_attributes(**params)
def find_server(client=None, unique=False, **params): def find_server(client: NovaClientType = None, unique=False, **params) -> \
NovaServer:
servers = list_servers(client=client, **params) servers = list_servers(client=client, **params)
if unique: if unique:
return servers.unique return servers.unique
@ -94,13 +104,13 @@ def find_server(client=None, unique=False, **params):
return servers.first return servers.first
def list_services(client=None, **params) -> tobiko.Selection: def list_services(client: NovaClientType = None, **params) -> tobiko.Selection:
client = nova_client(client) client = nova_client(client)
services = client.services.list() services = client.services.list()
return tobiko.select(services).with_attributes(**params) return tobiko.select(services).with_attributes(**params)
def find_service(client=None, unique=False, **params): def find_service(client: NovaClientType = None, unique=False, **params):
services = list_services(client=client, **params) services = list_services(client=client, **params)
if unique: if unique:
return services.unique return services.unique
@ -108,29 +118,42 @@ def find_service(client=None, unique=False, **params):
return services.first return services.first
def get_server_id(server): ServerType = typing.Union[str, NovaServer]
if isinstance(server, str):
return server
else:
return server.id
def get_server(server, client=None, **params): def get_server_id(server: typing.Optional[ServerType] = None,
server_id = get_server_id(server) server_id: typing.Optional[str] = None) -> str:
if server_id is None:
if isinstance(server, str):
server_id = server
else:
assert server is not None
server_id = server.id
return server_id
def get_server(server: typing.Optional[ServerType] = None,
server_id: typing.Optional[str] = None,
client: NovaClientType = None, **params) -> NovaServer:
server_id = get_server_id(server=server, server_id=server_id)
return nova_client(client).servers.get(server_id, **params) return nova_client(client).servers.get(server_id, **params)
def migrate_server(server, client=None, **params): def migrate_server(server: typing.Optional[ServerType] = None,
server_id: typing.Optional[str] = None,
client: NovaClientType = None, **params):
# pylint: disable=protected-access # pylint: disable=protected-access
server_id = get_server_id(server) server_id = get_server_id(server=server, server_id=server_id)
LOG.debug(f"Start server migration (server_id='{server_id}', " LOG.debug(f"Start server migration (server_id='{server_id}', "
f"info={params})") f"info={params})")
return nova_client(client).servers._action('migrate', server_id, return nova_client(client).servers._action('migrate', server_id,
info=params) info=params)
def confirm_resize(server, client=None, **params): def confirm_resize(server: typing.Optional[ServerType] = None,
server_id = get_server_id(server) server_id: typing.Optional[str] = None,
client: NovaClientType = None, **params):
server_id = get_server_id(server=server, server_id=server_id)
LOG.debug(f"Confirm server resize (server_id='{server_id}', " LOG.debug(f"Confirm server resize (server_id='{server_id}', "
f"info={params})") f"info={params})")
return nova_client(client).servers.confirm_resize(server_id, **params) return nova_client(client).servers.confirm_resize(server_id, **params)
@ -139,46 +162,67 @@ def confirm_resize(server, client=None, **params):
MAX_SERVER_CONSOLE_OUTPUT_LENGTH = 1024 * 256 MAX_SERVER_CONSOLE_OUTPUT_LENGTH = 1024 * 256
def get_console_output(server, timeout=None, interval=1., length=None, def get_console_output(server: typing.Optional[ServerType] = None,
client=None): server_id: typing.Optional[str] = None,
client = nova_client(client) timeout: tobiko.Seconds = None,
start_time = time.time() interval: tobiko.Seconds = None,
length: typing.Optional[int] = None,
client: NovaClientType = None) -> \
typing.Optional[str]:
if length is not None: if length is not None:
length = min(length, MAX_SERVER_CONSOLE_OUTPUT_LENGTH) length = min(length, MAX_SERVER_CONSOLE_OUTPUT_LENGTH)
else: else:
length = MAX_SERVER_CONSOLE_OUTPUT_LENGTH length = MAX_SERVER_CONSOLE_OUTPUT_LENGTH
while True:
server_id = get_server_id(server=server, server_id=server_id)
for attempt in tobiko.retry(timeout=timeout,
interval=interval,
default_timeout=60.,
default_interval=5.):
try: try:
output = client.servers.get_console_output(server=server, output = nova_client(client).servers.get_console_output(
length=length) server=server_id, length=length)
except TypeError: except (TypeError, novaclient.exceptions.NotFound):
# For some reason it could happen resulting body cannot be # Only active servers have console output
# translated to json object and it is converted to None server = get_server(server_id=server_id)
# on such case get_console_output would raise a TypeError if server.status != 'ACTIVE':
return None LOG.debug(f"Server '{server_id}' has no console output "
f"(status = '{server.status}').")
break
else:
# For some reason it could happen resulting body cannot be
# translated to json object and it is converted to None
# on such case get_console_output would raise a TypeError
LOG.exception(f"Error getting server '{server_id}' console "
"output")
else:
if output:
LOG.debug(f"got server '{server_id}' console output "
f"(length = {len(output)}).")
return output
if timeout is None or output: try:
attempt.check_limits()
except tobiko.RetryLimitError:
LOG.info(f"No console output produced by server '{server_id}') "
f" after {attempt.elapsed_time} seconds")
break break
else:
LOG.debug(f"Waiting for server '{server_id}' console output...")
if time.time() - start_time > timeout: return None
LOG.warning("No console output produced by server (%r) after "
"%r seconds", server, timeout)
break
LOG.debug('Waiting for server (%r) console output...', server)
time.sleep(interval)
return output
class HasNovaClientMixin(object): class HasNovaClientMixin(object):
nova_client = None nova_client: NovaClientType = None
def get_server(self, server, **params): def get_server(self, server: ServerType, **params) -> NovaServer:
return get_server(server=server, client=self.nova_client, **params) return get_server(server=server, client=self.nova_client, **params)
def get_server_console_output(self, server, **params): def get_server_console_output(self, server: ServerType, **params) -> \
typing.Optional[str]:
return get_console_output(server=server, client=self.nova_client, return get_console_output(server=server, client=self.nova_client,
**params) **params)
@ -193,76 +237,113 @@ class WaitForServerStatusTimeout(WaitForServerStatusError):
"{server_status} to {status} status after {timeout} seconds") "{server_status} to {status} status after {timeout} seconds")
NOVA_SERVER_TRANSIENT_STATUS = { NOVA_SERVER_TRANSIENT_STATUS: typing.Dict[str, typing.List[str]] = {
'ACTIVE': ('BUILD', 'SHUTOFF'), 'ACTIVE': ['BUILD', 'SHUTOFF'],
'SHUTOFF': ('ACTIVE'), 'SHUTOFF': ['ACTIVE'],
'VERIFY_RESIZE': ('RESIZE'), 'VERIFY_RESIZE': ['RESIZE'],
} }
def wait_for_server_status(server, status, client=None, timeout=None, def wait_for_server_status(
sleep_time=None, transient_status=None): server: ServerType,
if timeout is None: status: str,
timeout = 300. client: NovaClientType = None,
if sleep_time is None: timeout: tobiko.Seconds = None,
sleep_time = 5. sleep_time: tobiko.Seconds = None,
start_time = time.time() transient_status: typing.Optional[typing.List[str]] = None) -> \
NovaServer:
if transient_status is None: if transient_status is None:
transient_status = NOVA_SERVER_TRANSIENT_STATUS.get(status) or tuple() transient_status = NOVA_SERVER_TRANSIENT_STATUS.get(status) or []
while True: server_id = get_server_id(server)
server = get_server(server=server, client=client) for attempt in tobiko.retry(timeout=timeout,
if server.status == status: interval=sleep_time,
default_timeout=300.,
default_interval=5.):
_server = get_server(server_id=server_id, client=client)
if _server.status == status:
break break
if server.status not in transient_status: if _server.status not in transient_status:
raise WaitForServerStatusError(server_id=server.id, raise WaitForServerStatusError(server_id=server_id,
server_status=server.status, server_status=_server.status,
status=status) status=status)
try:
if time.time() - start_time >= timeout: attempt.check_time_left()
raise WaitForServerStatusTimeout(server_id=server.id, except tobiko.RetryTimeLimitError as ex:
server_status=server.status, raise WaitForServerStatusTimeout(server_id=server_id,
server_status=_server.status,
status=status, status=status,
timeout=timeout) timeout=timeout) from ex
progress = getattr(server, 'progress', None) progress = getattr(server, 'progress', None)
LOG.debug(f"Waiting for server {server.id} status to get from " LOG.debug(f"Waiting for server {server_id} status to get from "
f"{server.status} to {status} " f"{_server.status} to {status} "
f"(progress={progress}%)") f"(progress={progress}%)")
time.sleep(sleep_time)
return server return _server
def shutoff_server(server, client=None, timeout=None, sleep_time=None): def shutoff_server(server: ServerType = None,
client: NovaClientType = None,
timeout: tobiko.Seconds = None,
sleep_time: tobiko.Seconds = None) -> NovaServer:
client = nova_client(client) client = nova_client(client)
server = get_server(server=server, client=client) server = get_server(server=server, client=client)
if server.status == 'SHUTOFF': if server.status == 'SHUTOFF':
return server return server
LOG.info(f"stop server '{server.id}' (status='{server.status}').")
client.servers.stop(server.id) client.servers.stop(server.id)
return wait_for_server_status(server=server.id, status='SHUTOFF', return wait_for_server_status(server=server.id,
client=client, timeout=timeout, status='SHUTOFF',
client=client,
timeout=timeout,
sleep_time=sleep_time) sleep_time=sleep_time)
def activate_server(server, client=None, timeout=None, sleep_time=None): def activate_server(server: ServerType,
client: NovaClientType = None,
timeout: tobiko.Seconds = None,
sleep_time: tobiko.Seconds = None) -> NovaServer:
client = nova_client(client) client = nova_client(client)
server = get_server(server=server, client=client) server = get_server(server=server, client=client)
if server.status == 'ACTIVE': if server.status == 'ACTIVE':
return server return server
if server.status == 'SHUTOFF': if server.status == 'SHUTOFF':
LOG.info(f"Start server '{server.id}' (status='{server.status}').")
client.servers.start(server.id) client.servers.start(server.id)
elif server.status == 'RESIZE': elif server.status == 'RESIZE':
wait_for_server_status(server=server.id, status='VERIFY_RESIZE', server = wait_for_server_status(
client=client, timeout=timeout, server=server.id, status='VERIFY_RESIZE', client=client,
sleep_time=sleep_time) timeout=timeout, sleep_time=sleep_time)
LOG.info(f"Confirm resize of server '{server.id}' "
f"(status='{server.status}').")
client.servers.confirm_resize(server) client.servers.confirm_resize(server)
elif server.status == 'VERIFY_RESIZE': elif server.status == 'VERIFY_RESIZE':
LOG.info(f"Confirm resize of server '{server.id}' "
f"(status='{server.status}').")
client.servers.confirm_resize(server) client.servers.confirm_resize(server)
else: else:
LOG.warning(f"Try activating server '{server.id}' by rebooting "
f"it (status='{server.status}').")
client.servers.reboot(server.id, reboot_type='HARD') client.servers.reboot(server.id, reboot_type='HARD')
return wait_for_server_status(server=server.id, status='ACTIVE', return wait_for_server_status(server=server.id, status='ACTIVE',
client=client, timeout=timeout, client=client, timeout=timeout,
sleep_time=sleep_time) sleep_time=sleep_time)
def ensure_server_status(server: ServerType,
status: str,
client: NovaClientType = None,
timeout: tobiko.Seconds = None,
sleep_time: tobiko.Seconds = None) -> NovaServer:
if status == 'ACTIVE':
return activate_server(server=server, client=client, timeout=timeout,
sleep_time=sleep_time)
elif status == 'SHUTOFF':
return shutoff_server(server=server, client=client, timeout=timeout,
sleep_time=sleep_time)
else:
raise ValueError(f"Unsupported server status: '{status}'")

View File

@ -0,0 +1,87 @@
# Copyright 2021 Red Hat
#
# 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.
from __future__ import absolute_import
from oslo_log import log
from tobiko.openstack import keystone
from tobiko.openstack.nova import _client
LOG = log.getLogger(__name__)
def get_nova_quota_set(project: keystone.ProjectType = None,
user: keystone.UserType = None,
client: _client.NovaClientType = None,
**params):
client = _client.nova_client(client)
project_id = keystone.get_project_id(project=project,
session=client.client.session)
user_id = user and keystone.get_user_id(user=user) or None
return client.quotas.get(project_id, user_id=user_id, **params)
def set_nova_quota_set(project: keystone.ProjectType = None,
user: keystone.UserType = None,
client: _client.NovaClientType = None,
**params):
client = _client.nova_client(client)
project_id = keystone.get_project_id(project=project,
session=client.client.session)
user_id = user and keystone.get_user_id(user=user) or None
return client.quotas.update(project_id, user_id=user_id, **params)
def ensure_nova_quota_limits(project: keystone.ProjectType = None,
user: keystone.UserType = None,
client: _client.NovaClientType = None,
**required):
client = _client.nova_client(client)
project = keystone.get_project_id(project=project,
session=client.client.session)
user = user and keystone.get_user_id(user=user) or None
if user:
# Must increase project limits before user ones
ensure_nova_quota_limits(project=project, client=client,
**required)
quota_set = get_nova_quota_set(project=project, user=user,
client=client, detail=True)
actual_limits = {}
increment_limits = {}
for name, needed in required.items():
quota = getattr(quota_set, name)
limit: int = quota['limit']
if limit > 0:
in_use: int = max(0, quota['in_use']) + max(0, quota['reserved'])
if in_use + needed >= limit:
actual_limits[name] = limit
increment_limits[name] = max(10, limit * 2)
if increment_limits:
LOG.info(f"Increment Nova quota limits (project={project}, "
f"user={user}): {actual_limits} -> {increment_limits}...")
set_nova_quota_set(project=project, user=user, client=client,
**increment_limits)
quota_set = get_nova_quota_set(project=project, user=user,
client=client, detail=True)
new_limits = {
name: getattr(quota_set, name)['limit']
for name in increment_limits.keys()}
LOG.info(f"Nova quota limit increased (project={project}, "
f"user={user}): {actual_limits} -> {new_limits}...")
return quota_set

View File

@ -27,6 +27,7 @@ import tobiko
from tobiko import config from tobiko import config
from tobiko.openstack import glance from tobiko.openstack import glance
from tobiko.openstack import heat from tobiko.openstack import heat
from tobiko.openstack import keystone
from tobiko.openstack import neutron from tobiko.openstack import neutron
from tobiko.openstack import nova from tobiko.openstack import nova
from tobiko.openstack.stacks import _hot from tobiko.openstack.stacks import _hot
@ -96,6 +97,10 @@ class ServerStackFixture(heat.HeatStackFixture, abc.ABC):
#: stack with the internal where the server port is created #: stack with the internal where the server port is created
network_stack = tobiko.required_setup_fixture(_neutron.NetworkStackFixture) network_stack = tobiko.required_setup_fixture(_neutron.NetworkStackFixture)
def create_stack(self, retry=None):
self.ensure_quota_limits()
super(ServerStackFixture, self).create_stack(retry=retry)
@property @property
def image_fixture(self) -> glance.GlanceImageFixture: def image_fixture(self) -> glance.GlanceImageFixture:
"""Glance image used to create a Nova server instance""" """Glance image used to create a Nova server instance"""
@ -320,22 +325,50 @@ class ServerStackFixture(heat.HeatStackFixture, abc.ABC):
'maxsize': self.swap_maxsize}) 'maxsize': self.swap_maxsize})
return cloud_config return cloud_config
def ensure_server_status(self, status): def ensure_server_status(
tobiko.setup_fixture(self) self, status: str,
try: retry_count: typing.Optional[int] = None,
server = nova.wait_for_server_status(self.server_id, status) retry_timeout: tobiko.Seconds = None,
except nova.WaitForServerStatusError: retry_interval: tobiko.Seconds = None):
server = nova.get_server(self.server_id) self.ssh_client.close()
LOG.debug(f"Server {server.id} status is {server.status} instead " for attempt in tobiko.retry(count=retry_count,
f"of {status}", exc_info=1) timeout=retry_timeout,
if server.status == status: interval=retry_interval,
return server default_count=3,
elif status == "ACTIVE": default_timeout=900.,
tobiko.reset_fixture(self) default_interval=5.):
return nova.wait_for_server_status(self.server_id, 'ACTIVE') tobiko.setup_fixture(self)
else: server_id = self.server_id
tobiko.skip_test(f"{type(self).__name__}.ensure_server_status " try:
"method not implemented") server = nova.ensure_server_status(
server=server_id,
status=status,
timeout=attempt.time_left)
except nova.WaitForServerStatusError:
attempt.check_limits()
LOG.warning(
f"Unable to change server '{server_id}' status to "
f"'{status}'",
exc_info=1)
tobiko.cleanup_fixture(self)
else:
assert server.status == status
break
return server
def ensure_quota_limits(self):
"""Ensures Nova quota limits before creating a new server
"""
project = keystone.get_project_id(
session=self.client.http_client.session)
user = keystone.get_user_id(
session=self.client.http_client.session)
nova.ensure_nova_quota_limits(
project=project,
user=user,
instances=1,
cores=self.flavor_stack.vcpus or 1)
class ExternalServerStackFixture(ServerStackFixture, abc.ABC): class ExternalServerStackFixture(ServerStackFixture, abc.ABC):

View File

@ -103,21 +103,32 @@ class ClientTest(testtools.TestCase):
self.assertEqual(server_id, server.id) self.assertEqual(server_id, server.id)
self.assertEqual('ACTIVE', server.status) self.assertEqual('ACTIVE', server.status)
def test_shutof_and_activate_server(self):
server_id = self.useFixture(stacks.CirrosServerStackFixture(
stack_name=self.id())).server_id
server = nova.wait_for_server_status(server=server_id, status='ACTIVE') class ServerActionsStack(stacks.CirrosServerStackFixture):
self.assertEqual(server_id, server.id) pass
class ServerActionsTest(testtools.TestCase):
stack = tobiko.required_setup_fixture(ServerActionsStack)
def test_activate_server(self, initial_status='SHUTOFF'):
self.stack.ensure_server_status(initial_status)
server = nova.activate_server(self.stack.server_id)
self.assertEqual('ACTIVE', server.status) self.assertEqual('ACTIVE', server.status)
ping.assert_reachable_hosts([self.stack.ip_address])
server = nova.shutoff_server(server=server_id) def test_activate_server_when_shutoff(self):
self.assertEqual(server_id, server.id) self.test_activate_server(initial_status='SHUTOFF')
def test_shutoff_server(self, initial_status='ACTIVE'):
self.stack.ensure_server_status(initial_status)
server = nova.shutoff_server(self.stack.server_id)
self.assertEqual('SHUTOFF', server.status) self.assertEqual('SHUTOFF', server.status)
ping.assert_unreachable_hosts([self.stack.ip_address])
server = nova.activate_server(server=server_id) def test_shutoff_server_when_shutoff(self):
self.assertEqual(server_id, server.id) self.test_shutoff_server(initial_status='SHUTOFF')
self.assertEqual('ACTIVE', server.status)
@keystone.skip_unless_has_keystone_credentials() @keystone.skip_unless_has_keystone_credentials()
@ -194,8 +205,8 @@ class MigrateServerTest(testtools.TestCase):
target_hypervisor = hypervisor.hypervisor_hostname target_hypervisor = hypervisor.hypervisor_hostname
break break
else: else:
self.skip("Cannot find a valid hypervisor host to migrate server " self.skipTest("Cannot find a valid hypervisor host to migrate "
"to") "server to")
server = self.migrate_server(server=server, host=target_hypervisor) server = self.migrate_server(server=server, host=target_hypervisor)