Jude Cross | 638c4ef | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 1 | # Copyright 2017 Catalyst IT Ltd |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | import pkg_resources |
| 15 | import random |
| 16 | import shlex |
| 17 | import string |
| 18 | import subprocess |
| 19 | import tempfile |
| 20 | import time |
| 21 | |
| 22 | from oslo_log import log as logging |
| 23 | from oslo_utils import excutils |
| 24 | from tempest import config |
| 25 | from tempest.lib.common import fixed_network |
| 26 | from tempest.lib.common import rest_client |
| 27 | from tempest.lib.common.utils.linux import remote_client |
| 28 | from tempest.lib.common.utils import test_utils |
| 29 | from tempest.lib import exceptions |
| 30 | |
| 31 | CONF = config.CONF |
| 32 | LOG = logging.getLogger(__name__) |
| 33 | |
| 34 | SERVER_BINARY = pkg_resources.resource_filename( |
| 35 | 'octavia_tempest_plugin.contrib.httpd', 'httpd.bin') |
| 36 | |
| 37 | |
| 38 | class BuildErrorException(exceptions.TempestException): |
| 39 | message = "Server %(server_id)s failed to build and is in ERROR status" |
| 40 | |
| 41 | |
| 42 | def _get_task_state(body): |
| 43 | return body.get('OS-EXT-STS:task_state', None) |
| 44 | |
| 45 | |
| 46 | def wait_for_server_status(client, server_id, status, ready_wait=True, |
| 47 | extra_timeout=0, raise_on_error=True): |
| 48 | """Waits for a server to reach a given status.""" |
| 49 | |
| 50 | # NOTE(afazekas): UNKNOWN status possible on ERROR |
| 51 | # or in a very early stage. |
| 52 | body = client.show_server(server_id)['server'] |
| 53 | old_status = server_status = body['status'] |
| 54 | old_task_state = task_state = _get_task_state(body) |
| 55 | start_time = int(time.time()) |
| 56 | timeout = client.build_timeout + extra_timeout |
| 57 | while True: |
| 58 | # NOTE(afazekas): Now the BUILD status only reached |
| 59 | # between the UNKNOWN->ACTIVE transition. |
| 60 | # TODO(afazekas): enumerate and validate the stable status set |
| 61 | if status == 'BUILD' and server_status != 'UNKNOWN': |
| 62 | return |
| 63 | if server_status == status: |
| 64 | if ready_wait: |
| 65 | if status == 'BUILD': |
| 66 | return |
| 67 | # NOTE(afazekas): The instance is in "ready for action state" |
| 68 | # when no task in progress |
| 69 | if task_state is None: |
| 70 | # without state api extension 3 sec usually enough |
| 71 | time.sleep(CONF.compute.ready_wait) |
| 72 | return |
| 73 | else: |
| 74 | return |
| 75 | |
| 76 | time.sleep(client.build_interval) |
| 77 | body = client.show_server(server_id)['server'] |
| 78 | server_status = body['status'] |
| 79 | task_state = _get_task_state(body) |
| 80 | if (server_status != old_status) or (task_state != old_task_state): |
| 81 | LOG.info('State transition "%s" ==> "%s" after %d second wait', |
| 82 | '/'.join((old_status, str(old_task_state))), |
| 83 | '/'.join((server_status, str(task_state))), |
| 84 | time.time() - start_time) |
| 85 | if (server_status == 'ERROR') and raise_on_error: |
| 86 | if 'fault' in body: |
| 87 | raise BuildErrorException(body['fault'], |
| 88 | server_id=server_id) |
| 89 | else: |
| 90 | raise BuildErrorException(server_id=server_id) |
| 91 | |
| 92 | timed_out = int(time.time()) - start_time >= timeout |
| 93 | |
| 94 | if timed_out: |
| 95 | expected_task_state = 'None' if ready_wait else 'n/a' |
| 96 | message = ('Server %(server_id)s failed to reach %(status)s ' |
| 97 | 'status and task state "%(expected_task_state)s" ' |
| 98 | 'within the required time (%(timeout)s s).' % |
| 99 | {'server_id': server_id, |
| 100 | 'status': status, |
| 101 | 'expected_task_state': expected_task_state, |
| 102 | 'timeout': timeout}) |
| 103 | message += ' Current status: %s.' % server_status |
| 104 | message += ' Current task state: %s.' % task_state |
| 105 | caller = test_utils.find_test_caller() |
| 106 | if caller: |
| 107 | message = '(%s) %s' % (caller, message) |
| 108 | raise exceptions.TimeoutException(message) |
| 109 | old_status = server_status |
| 110 | old_task_state = task_state |
| 111 | |
| 112 | |
| 113 | def wait_for_server_termination(client, server_id, ignore_error=False): |
| 114 | """Waits for server to reach termination.""" |
| 115 | try: |
| 116 | body = client.show_server(server_id)['server'] |
| 117 | except exceptions.NotFound: |
| 118 | return |
| 119 | old_status = body['status'] |
| 120 | old_task_state = _get_task_state(body) |
| 121 | start_time = int(time.time()) |
| 122 | while True: |
| 123 | time.sleep(client.build_interval) |
| 124 | try: |
| 125 | body = client.show_server(server_id)['server'] |
| 126 | except exceptions.NotFound: |
| 127 | return |
| 128 | server_status = body['status'] |
| 129 | task_state = _get_task_state(body) |
| 130 | if (server_status != old_status) or (task_state != old_task_state): |
| 131 | LOG.info('State transition "%s" ==> "%s" after %d second wait', |
| 132 | '/'.join((old_status, str(old_task_state))), |
| 133 | '/'.join((server_status, str(task_state))), |
| 134 | time.time() - start_time) |
| 135 | if server_status == 'ERROR' and not ignore_error: |
| 136 | raise exceptions.DeleteErrorException(resource_id=server_id) |
| 137 | |
| 138 | if int(time.time()) - start_time >= client.build_timeout: |
| 139 | raise exceptions.TimeoutException |
| 140 | old_status = server_status |
| 141 | old_task_state = task_state |
| 142 | |
| 143 | |
| 144 | def create_server(clients, name=None, flavor=None, image_id=None, |
| 145 | validatable=False, validation_resources=None, |
| 146 | tenant_network=None, wait_until=None, availability_zone=None, |
| 147 | **kwargs): |
| 148 | """Common wrapper utility returning a test server. |
| 149 | |
| 150 | This method is a common wrapper returning a test server that can be |
| 151 | pingable or sshable. |
| 152 | |
| 153 | :param name: Name of the server to be provisioned. If not defined a random |
| 154 | string ending with '-instance' will be generated. |
| 155 | :param flavor: Flavor of the server to be provisioned. If not defined, |
| 156 | CONF.compute.flavor_ref will be used instead. |
| 157 | :param image_id: ID of the image to be used to provision the server. If not |
| 158 | defined, CONF.compute.image_ref will be used instead. |
| 159 | :param clients: Client manager which provides OpenStack Tempest clients. |
| 160 | :param validatable: Whether the server will be pingable or sshable. |
| 161 | :param validation_resources: Resources created for the connection to the |
| 162 | server. Include a keypair, a security group and an IP. |
| 163 | :param tenant_network: Tenant network to be used for creating a server. |
| 164 | :param wait_until: Server status to wait for the server to reach after |
| 165 | its creation. |
| 166 | :returns: a tuple |
| 167 | """ |
| 168 | if name is None: |
| 169 | r = random.SystemRandom() |
| 170 | name = "m{}".format("".join( |
| 171 | [r.choice(string.ascii_uppercase + string.digits) |
| 172 | for i in range( |
| 173 | CONF.loadbalancer.random_server_name_length - 1)] |
| 174 | )) |
| 175 | if flavor is None: |
| 176 | flavor = CONF.compute.flavor_ref |
| 177 | if image_id is None: |
| 178 | image_id = CONF.compute.image_ref |
| 179 | if availability_zone is None: |
| 180 | availability_zone = CONF.loadbalancer.availability_zone |
| 181 | |
| 182 | kwargs = fixed_network.set_networks_kwarg( |
| 183 | tenant_network, kwargs) or {} |
| 184 | |
| 185 | if availability_zone: |
| 186 | kwargs.update({'availability_zone': availability_zone}) |
| 187 | |
| 188 | if CONF.validation.run_validation and validatable: |
| 189 | LOG.debug("Provisioning test server with validation resources %s", |
| 190 | validation_resources) |
| 191 | if 'security_groups' in kwargs: |
| 192 | kwargs['security_groups'].append( |
| 193 | {'name': validation_resources['security_group']['name']}) |
| 194 | else: |
| 195 | try: |
| 196 | kwargs['security_groups'] = [ |
| 197 | {'name': validation_resources['security_group']['name']}] |
| 198 | except KeyError: |
| 199 | LOG.debug("No security group provided.") |
| 200 | |
| 201 | if 'key_name' not in kwargs: |
| 202 | try: |
| 203 | kwargs['key_name'] = validation_resources['keypair']['name'] |
| 204 | except KeyError: |
| 205 | LOG.debug("No key provided.") |
| 206 | |
| 207 | if CONF.validation.connect_method == 'floating': |
| 208 | if wait_until is None: |
| 209 | wait_until = 'ACTIVE' |
| 210 | |
Lingxian Kong | bf966a9 | 2018-01-16 00:20:37 +1300 | [diff] [blame] | 211 | body = clients.servers_client.create_server( |
| 212 | name=name, |
| 213 | imageRef=image_id, |
| 214 | flavorRef=flavor, |
| 215 | config_drive=True, |
| 216 | **kwargs |
| 217 | ) |
Jude Cross | 638c4ef | 2017-07-24 14:57:20 -0700 | [diff] [blame] | 218 | server = rest_client.ResponseBody(body.response, body['server']) |
| 219 | |
| 220 | def _setup_validation_fip(): |
| 221 | if CONF.service_available.neutron: |
| 222 | ifaces = clients.interfaces_client.list_interfaces(server['id']) |
| 223 | validation_port = None |
| 224 | for iface in ifaces['interfaceAttachments']: |
| 225 | if not tenant_network or (iface['net_id'] == |
| 226 | tenant_network['id']): |
| 227 | validation_port = iface['port_id'] |
| 228 | break |
| 229 | if not validation_port: |
| 230 | # NOTE(artom) This will get caught by the catch-all clause in |
| 231 | # the wait_until loop below |
| 232 | raise ValueError('Unable to setup floating IP for validation: ' |
| 233 | 'port not found on tenant network') |
| 234 | clients.floating_ips_client.update_floatingip( |
| 235 | validation_resources['floating_ip']['id'], |
| 236 | port_id=validation_port) |
| 237 | else: |
| 238 | fip_client = clients.compute_floating_ips_client |
| 239 | fip_client.associate_floating_ip_to_server( |
| 240 | floating_ip=validation_resources['floating_ip']['ip'], |
| 241 | server_id=server['id']) |
| 242 | |
| 243 | if wait_until: |
| 244 | try: |
| 245 | wait_for_server_status( |
| 246 | clients.servers_client, server['id'], wait_until) |
| 247 | |
| 248 | # Multiple validatable servers are not supported for now. Their |
| 249 | # creation will fail with the condition above (l.58). |
| 250 | if CONF.validation.run_validation and validatable: |
| 251 | if CONF.validation.connect_method == 'floating': |
| 252 | _setup_validation_fip() |
| 253 | |
| 254 | except Exception: |
| 255 | with excutils.save_and_reraise_exception(): |
| 256 | try: |
| 257 | clients.servers_client.delete_server(server['id']) |
| 258 | except Exception: |
| 259 | LOG.exception('Deleting server %s failed', server['id']) |
| 260 | try: |
| 261 | wait_for_server_termination(clients.servers_client, |
| 262 | server['id']) |
| 263 | except Exception: |
| 264 | LOG.exception('Server %s failed to delete in time', |
| 265 | server['id']) |
| 266 | |
| 267 | return server |
| 268 | |
| 269 | |
| 270 | def clear_server(servers_client, id): |
| 271 | try: |
| 272 | servers_client.delete_server(id) |
| 273 | except exceptions.NotFound: |
| 274 | pass |
| 275 | wait_for_server_termination(servers_client, id) |
| 276 | |
| 277 | |
| 278 | def _execute(cmd, cwd=None): |
| 279 | args = shlex.split(cmd) |
| 280 | subprocess_args = {'stdout': subprocess.PIPE, |
| 281 | 'stderr': subprocess.STDOUT, |
| 282 | 'cwd': cwd} |
| 283 | proc = subprocess.Popen(args, **subprocess_args) |
| 284 | stdout, stderr = proc.communicate() |
| 285 | if proc.returncode != 0: |
| 286 | LOG.error('Command %s returned with exit status %s, output %s, ' |
| 287 | 'error %s', cmd, proc.returncode, stdout, stderr) |
| 288 | raise exceptions.CommandFailed(proc.returncode, cmd, stdout, stderr) |
| 289 | return stdout |
| 290 | |
| 291 | |
| 292 | def copy_file(floating_ip, private_key, local_file, remote_file): |
| 293 | """Copy web server script to instance.""" |
| 294 | with tempfile.NamedTemporaryFile() as key: |
| 295 | key.write(private_key.encode('utf-8')) |
| 296 | key.flush() |
| 297 | dest = ( |
| 298 | "%s@%s:%s" % |
| 299 | (CONF.validation.image_ssh_user, floating_ip, remote_file) |
| 300 | ) |
| 301 | cmd = ("scp -v -o UserKnownHostsFile=/dev/null " |
| 302 | "-o StrictHostKeyChecking=no " |
| 303 | "-i %(key_file)s %(file)s %(dest)s" % {'key_file': key.name, |
| 304 | 'file': local_file, |
| 305 | 'dest': dest}) |
| 306 | return _execute(cmd) |
| 307 | |
| 308 | |
| 309 | def run_webserver(connect_ip, private_key): |
| 310 | httpd = "/dev/shm/httpd.bin" |
| 311 | |
| 312 | linux_client = remote_client.RemoteClient( |
| 313 | connect_ip, |
| 314 | CONF.validation.image_ssh_user, |
| 315 | pkey=private_key, |
| 316 | ) |
| 317 | linux_client.validate_authentication() |
| 318 | |
| 319 | # TODO(kong): We may figure out an elegant way to copy file to instance |
| 320 | # in future. |
| 321 | LOG.debug("Copying the webserver binary to the server.") |
| 322 | copy_file(connect_ip, private_key, SERVER_BINARY, httpd) |
| 323 | |
| 324 | LOG.debug("Starting services on the server.") |
| 325 | linux_client.exec_command('sudo screen -d -m %s -port 80 -id 1' % httpd) |
| 326 | linux_client.exec_command('sudo screen -d -m %s -port 81 -id 2' % httpd) |