blob: 54fa4f827b26f80237e7b8f64e25727c8769477d [file] [log] [blame]
Jude Cross638c4ef2017-07-24 14:57:20 -07001# 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.
14import pkg_resources
15import random
16import shlex
17import string
18import subprocess
19import tempfile
20import time
21
22from oslo_log import log as logging
23from oslo_utils import excutils
24from tempest import config
25from tempest.lib.common import fixed_network
26from tempest.lib.common import rest_client
27from tempest.lib.common.utils.linux import remote_client
28from tempest.lib.common.utils import test_utils
29from tempest.lib import exceptions
30
31CONF = config.CONF
32LOG = logging.getLogger(__name__)
33
34SERVER_BINARY = pkg_resources.resource_filename(
35 'octavia_tempest_plugin.contrib.httpd', 'httpd.bin')
36
37
38class BuildErrorException(exceptions.TempestException):
39 message = "Server %(server_id)s failed to build and is in ERROR status"
40
41
42def _get_task_state(body):
43 return body.get('OS-EXT-STS:task_state', None)
44
45
46def 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
113def 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
144def 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 Kongbf966a92018-01-16 00:20:37 +1300211 body = clients.servers_client.create_server(
212 name=name,
213 imageRef=image_id,
214 flavorRef=flavor,
215 config_drive=True,
216 **kwargs
217 )
Jude Cross638c4ef2017-07-24 14:57:20 -0700218 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
270def 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
278def _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
292def 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
309def 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)