# Copyright (c) 2019 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. from __future__ import absolute_import import abc import os import typing import netaddr import six from oslo_log import log import tobiko from tobiko import config from tobiko.openstack import glance from tobiko.openstack import heat from tobiko.openstack import keystone from tobiko.openstack import neutron from tobiko.openstack import nova from tobiko.openstack.stacks import _hot from tobiko.openstack.stacks import _neutron from tobiko.shell import curl from tobiko.shell import sh from tobiko.shell import ssh CONF = config.CONF LOG = log.getLogger(__name__) class KeyPairStackFixture(heat.HeatStackFixture): template = _hot.heat_template_file('nova/key_pair.yaml') key_file = tobiko.tobiko_config_path(CONF.tobiko.nova.key_file) public_key = None private_key = None def setup_fixture(self): self.create_key_file() self.read_keys() super(KeyPairStackFixture, self).setup_fixture() def read_keys(self): with open(self.key_file, 'r') as fd: self.private_key = as_str(fd.read()) with open(self.key_file + '.pub', 'r') as fd: self.public_key = as_str(fd.read()) def create_key_file(self): key_file = self.key_file if not os.path.isfile(key_file): key_dir = os.path.dirname(key_file) tobiko.makedirs(key_dir) try: sh.local_execute(['ssh-keygen', '-f', key_file, '-P', '']) except sh.ShellCommandFailed: if not os.path.isfile(key_file): raise else: assert os.path.isfile(key_file) class FlavorStackFixture(heat.HeatStackFixture): template = _hot.heat_template_file('nova/flavor.yaml') disk: typing.Optional[int] = None ephemeral = None extra_specs = None is_public = None name = None rxtx_factor = None swap = None vcpus = None @neutron.skip_if_missing_networking_extensions('port-security') class ServerStackFixture(heat.HeatStackFixture, abc.ABC): #: Heat template file template = _hot.heat_template_file('nova/server.yaml') #: stack with the key pair for the server instance key_pair_stack = tobiko.required_setup_fixture(KeyPairStackFixture) #: stack with the internal where the server port is created network_stack = tobiko.required_setup_fixture(_neutron.NetworkStackFixture) #: whenever the server relies only on DHCP for address assignation @property def need_dhcp(self) -> bool: return not self.config_drive #: whenever the server will use config-drive to get metadata config_drive = False def create_stack(self, retry=None): self.ensure_quota_limits() super(ServerStackFixture, self).create_stack(retry=retry) @property def image_fixture(self) -> glance.GlanceImageFixture: """Glance image used to create a Nova server instance""" raise NotImplementedError def delete_stack(self, stack_id=None): if self._outputs: tobiko.cleanup_fixture(self.ssh_client) super(ServerStackFixture, self).delete_stack(stack_id=stack_id) @property def image(self) -> str: return self.image_fixture.image_id @property def username(self) -> str: """username used to login to a Nova server instance""" return self.image_fixture.username or 'root' @property def password(self) -> typing.Optional[str]: """password used to login to a Nova server instance""" return self.image_fixture.password @property def connection_timeout(self) -> tobiko.Seconds: return self.image_fixture.connection_timeout @property def flavor_stack(self) -> FlavorStackFixture: """stack used to create flavor for Nova server instance""" raise NotImplementedError @property def flavor(self) -> str: """Flavor for Nova server instance""" return self.flavor_stack.flavor_id #: Whenever port security on internal network is enable port_security_enabled = False #: Security groups to be associated to network ports security_groups: typing.List[str] = [] @property def key_name(self) -> str: return self.key_pair_stack.key_name @property def network(self) -> str: return self.network_stack.network_id #: Floating IP network where the Neutron floating IP are created @property def floating_network(self) -> str: return self.network_stack.floating_network @property def has_floating_ip(self) -> bool: return bool(self.floating_network) @property def ssh_client(self) -> ssh.SSHClientFixture: return ssh.ssh_client(host=self.ip_address, username=self.username, password=self.password, connection_timeout=self.connection_timeout) @property def peer_ssh_client(self) -> typing.Optional[ssh.SSHClientFixture]: """Nearest SSH client to an host that can see server fixed IPs ports """ return self.ssh_client @property def ssh_command(self) -> sh.ShellCommand: return ssh.ssh_command(host=self.ip_address, username=self.username) @property def ip_address(self) -> str: if self.has_floating_ip: return self.floating_ip_address else: return self.outputs.fixed_ips[0]['ip_address'] def list_fixed_ips(self, ip_version: typing.Optional[int] = None) -> \ tobiko.Selection[netaddr.IPAddress]: fixed_ips: tobiko.Selection[netaddr.IPAddress] = tobiko.Selection( netaddr.IPAddress(fixed_ip['ip_address']) for fixed_ip in self.fixed_ips) if ip_version is not None: fixed_ips.with_attributes(version=ip_version) return fixed_ips def find_fixed_ip(self, ip_version: typing.Optional[int] = None, unique=False) -> netaddr.IPAddress: fixed_ips = self.list_fixed_ips(ip_version=ip_version) if unique: return fixed_ips.unique else: return fixed_ips.first @property def fixed_ipv4(self): return self.find_fixed_ip(ip_version=4) @property def fixed_ipv6(self): return self.find_fixed_ip(ip_version=6) #: Schedule on different host that this Nova server instance ID different_host = None #: Schedule on same host as this Nova server instance ID same_host = None #: Scheduler group in which this Nova server is attached @property def scheduler_group(self): return None @property def scheduler_hints(self): scheduler_hints = {} if self.different_host: scheduler_hints.update(different_host=list(self.different_host)) if self.same_host: scheduler_hints.update(same_host=list(self.same_host)) if self.scheduler_group: scheduler_hints.update(group=self.scheduler_group) return scheduler_hints #: allow to retry creating server in case scheduler hits are not respected retry_create = 3 expected_creted_status = {heat.CREATE_COMPLETE} def validate_created_stack(self): stack = super(ServerStackFixture, self).validate_created_stack() self.validate_scheduler_hints() return stack @property def hypervisor_host(self): return getattr(self.server_details, 'OS-EXT-SRV-ATTR:host') def validate_scheduler_hints(self): if self.scheduler_hints: hypervisor = self.hypervisor_host self.validate_same_host_scheduler_hints(hypervisor=hypervisor) self.validate_different_host_scheduler_hints(hypervisor=hypervisor) def validate_same_host_scheduler_hints(self, hypervisor): if self.same_host: different_host_hypervisors = nova.get_different_host_hypervisors( self.same_host, hypervisor) if different_host_hypervisors: tobiko.skip_test(f"Server {self.server_id} of stack " f"{self.stack_name} created on different " "hypervisor host from servers:\n" f"{different_host_hypervisors}") def validate_different_host_scheduler_hints(self, hypervisor): if self.different_host: same_host_hypervisors = nova.get_same_host_hypervisors( self.different_host, hypervisor) if same_host_hypervisors: tobiko.skip_test(f"Server {self.server_id} of stack " f"{self.stack_name} created on the same " "hypervisor host as servers:\n" f"{same_host_hypervisors}") @property def server_details(self): return nova.get_server(self.server_id) @property def port_details(self): return neutron.get_port(self.port_id) def getDetails(self): # pylint: disable=W0212 details = super(ServerStackFixture, self).getDetails() stack = self.get_stack() if stack: details[self.fixture_name + '.stack'] = ( self.details_content(get_json=lambda: stack._info)) if stack.stack_status == 'CREATE_COMPLETE': details[self.fixture_name + '.server_details'] = ( self.details_content( get_json=lambda: self.server_details._info)) details[self.fixture_name + '.console_output'] = ( self.details_content( get_text=lambda: self.console_output)) return details def details_content(self, **kwargs): return tobiko.details_content(content_id=self.fixture_name, **kwargs) max_console_output_length = 64 * 1024 @property def console_output(self): return nova.get_console_output(server=self.server_id, length=self.max_console_output_length) @property def user_data(self): return nova.user_data(self.cloud_config) #: SWAP file name swap_filename: str = '/swap.img' #: SWAP file size in bytes swap_size: typing.Optional[int] = None #: nax SWAP file size in bytes swap_maxsize: typing.Optional[int] = None @property def cloud_config(self): cloud_config = nova.cloud_config() # default is to not create any swap files, # because 'swap_file_max_size' is set to None if self.swap_maxsize is not None: cloud_config = nova.cloud_config( cloud_config, swap={'filename': self.swap_filename, 'size': self.swap_size or 'auto', 'maxsize': self.swap_maxsize}) return cloud_config def ensure_server_status( self, status: str, retry_count: typing.Optional[int] = None, retry_timeout: tobiko.Seconds = None, retry_interval: tobiko.Seconds = None): self.ssh_client.close() for attempt in tobiko.retry(count=retry_count, timeout=retry_timeout, interval=retry_interval, default_count=3, default_timeout=900., default_interval=5.): tobiko.setup_fixture(self) server_id = self.server_id try: 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): # pylint: disable=abstract-method #: stack with the network where the server port is created network_stack = tobiko.required_setup_fixture( _neutron.ExternalNetworkStackFixture) # external servers doesn't need floating IPs has_floating_ip = False config_drive = True peer_ssh_client = None @property def floating_network(self): return self.network_stack.network_id class QosServerStackFixture(ServerStackFixture, abc.ABC): #: stack with the network with a qos policy network_stack = tobiko.required_setup_fixture( _neutron.NetworkQosPolicyStackFixture) class PeerServerStackFixture(ServerStackFixture, abc.ABC): """Server witch networking access requires passing by another Nova server """ has_floating_ip = False @property def peer_stack(self) -> ServerStackFixture: """Peer server used to reach this one""" raise NotImplementedError @property def ssh_client(self) -> ssh.SSHClientFixture: return ssh.ssh_client(host=self.ip_address, username=self.username, password=self.password, connection_timeout=self.connection_timeout, proxy_jump=self.peer_ssh_client) @property def peer_ssh_client(self) -> ssh.SSHClientFixture: return self.peer_stack.ssh_client @property def ssh_command(self) -> sh.ShellCommand: proxy_command = self.peer_stack.ssh_command + [ 'nc', self.ip_address, '22'] return ssh.ssh_command(host=self.ip_address, username=self.username, proxy_command=proxy_command) @property def network(self) -> str: return self.peer_stack.network @nova.skip_if_missing_hypervisors(count=2, state='up', status='enabled') class DifferentHostServerStackFixture(PeerServerStackFixture, abc.ABC): # pylint: disable=abstract-method @property def different_host(self): return [self.peer_stack.server_id] class SameHostServerStackFixture(PeerServerStackFixture, abc.ABC): @property def same_host(self): return [self.peer_stack.server_id] def as_str(text): if isinstance(text, six.string_types): return text else: return text.decode() class HttpServerStackFixture(PeerServerStackFixture, abc.ABC): http_server_port = 80 http_request_scheme = 'http' http_request_path = '' def send_http_request( self, hostname: typing.Union[str, netaddr.IPAddress, None] = None, ip_version: typing.Optional[int] = None, port: typing.Optional[int] = None, path: typing.Optional[str] = None, retry_count: typing.Optional[int] = None, retry_timeout: tobiko.Seconds = None, retry_interval: tobiko.Seconds = None, ssh_client: typing.Optional[ssh.SSHClientFixture] = None, **curl_parameters) -> str: if hostname is None: hostname = self.find_fixed_ip(ip_version=ip_version) if port is None: port = self.http_server_port if path is None: path = self.http_request_path if ssh_client is None: ssh_client = self.peer_stack.ssh_client return curl.execute_curl(scheme='http', hostname=hostname, port=port, path=path, retry_count=retry_count, retry_timeout=retry_timeout, retry_interval=retry_interval, ssh_client=ssh_client, **curl_parameters) class ServerGroupStackFixture(heat.HeatStackFixture): template = _hot.heat_template_file('nova/server_group.yaml') class AffinityServerGroupStackFixture(tobiko.SharedFixture): server_group_stack = tobiko.required_setup_fixture( ServerGroupStackFixture) @property def scheduler_group(self): return self.server_group_stack.affinity_server_group_id class AntiAffinityServerGroupStackFixture(tobiko.SharedFixture): server_group_stack = tobiko.required_setup_fixture( ServerGroupStackFixture) @property def scheduler_group(self): return self.server_group_stack.anti_affinity_server_group_id