Revision 6c78720b
b/snf-tools/synnefo_tools/burnin/__init__.py | ||
---|---|---|
45 | 45 |
from synnefo_tools.burnin.images_tests import \ |
46 | 46 |
FlavorsTestSuite, ImagesTestSuite |
47 | 47 |
from synnefo_tools.burnin.pithos_tests import PithosTestSuite |
48 |
from synnefo_tools.burnin.server_tests import ServerTestSuite |
|
48 | 49 |
|
49 | 50 |
|
50 | 51 |
# -------------------------------------------------------------------- |
... | ... | |
54 | 55 |
FlavorsTestSuite, |
55 | 56 |
ImagesTestSuite, |
56 | 57 |
PithosTestSuite, |
58 |
ServerTestSuite, |
|
57 | 59 |
] |
58 | 60 |
|
59 | 61 |
TSUITES_NAMES = [tsuite.__name__ for tsuite in TESTSUITES] |
... | ... | |
68 | 70 |
# Parse arguments |
69 | 71 |
def parse_comma(option, _, value, parser): |
70 | 72 |
"""Parse comma separated arguments""" |
71 |
tests = set(TSUITES_NAMES) |
|
72 |
parse_input = value.split(',') |
|
73 |
|
|
74 |
if not (set(parse_input)).issubset(tests): |
|
75 |
raise optparse.OptionValueError("The selected set of tests is invalid") |
|
76 |
|
|
77 |
setattr(parser.values, option.dest, value.split(',')) |
|
73 |
parse_input = [p.strip() for p in value.split(',')] |
|
74 |
setattr(parser.values, option.dest, parse_input) |
|
78 | 75 |
|
79 | 76 |
|
80 | 77 |
def parse_arguments(args): |
... | ... | |
120 | 117 |
help="Query server status when requests are pending " |
121 | 118 |
"every INTERVAL seconds") |
122 | 119 |
parser.add_option( |
123 |
"--force-flavor", action="store",
|
|
124 |
type="string", default=None, dest="force_flavor", metavar="FLAVOR",
|
|
125 |
help="Force all server creations to use the specified FLAVOR "
|
|
120 |
"--flavors", action="callback", callback=parse_comma,
|
|
121 |
type="string", default=None, dest="flavors", metavar="FLAVORS",
|
|
122 |
help="Force all server creations to use one of the specified FLAVORS "
|
|
126 | 123 |
"instead of a randomly chosen one. Supports both search by name " |
127 | 124 |
"(reg expression) with \"name:flavor name\" or by id with " |
128 | 125 |
"\"id:flavor id\"") |
129 | 126 |
parser.add_option( |
130 |
"--force-image", action="store",
|
|
131 |
type="string", default=None, dest="force_image", metavar="IMAGE",
|
|
132 |
help="Force all server creations to use the specified IMAGE "
|
|
127 |
"--images", action="callback", callback=parse_comma,
|
|
128 |
type="string", default=None, dest="images", metavar="IMAGES",
|
|
129 |
help="Force all server creations to use one of the specified IMAGES "
|
|
133 | 130 |
"instead of the default one (a Debian Base image). Just like the " |
134 |
"--force-flavor option, it supports both search by name and id")
|
|
131 |
"--flavors option, it supports both search by name and id")
|
|
135 | 132 |
parser.add_option( |
136 | 133 |
"--system-user", action="store", |
137 | 134 |
type="string", default=None, dest="system_user", |
... | ... | |
203 | 200 |
if opts.final_report: |
204 | 201 |
opts.quiet = True |
205 | 202 |
|
203 |
# Check `--set-tests' and `--exclude-tests' options |
|
204 |
if opts.tests != "all" and \ |
|
205 |
not (set(opts.tests)).issubset(set(TSUITES_NAMES)): |
|
206 |
raise optparse.OptionValueError("The selected set of tests is invalid") |
|
207 |
if opts.exclude_tests is not None and \ |
|
208 |
not (set(opts.exclude_tests)).issubset(set(TSUITES_NAMES)): |
|
209 |
raise optparse.OptionValueError("The selected set of tests is invalid") |
|
210 |
|
|
206 | 211 |
# `token' is mandatory |
207 | 212 |
mandatory_argument(opts.token, "--token") |
208 | 213 |
# `auth_url' is mandatory |
... | ... | |
245 | 250 |
|
246 | 251 |
# Run burnin |
247 | 252 |
# The return value denotes the success status |
248 |
return common.run(testsuites, failfast=opts.failfast, |
|
249 |
final_report=opts.final_report) |
|
253 |
return common.run_burnin(testsuites, failfast=opts.failfast,
|
|
254 |
final_report=opts.final_report)
|
|
250 | 255 |
|
251 | 256 |
|
252 | 257 |
if __name__ == "__main__": |
b/snf-tools/synnefo_tools/burnin/common.py | ||
---|---|---|
38 | 38 |
|
39 | 39 |
import os |
40 | 40 |
import re |
41 |
import time |
|
41 | 42 |
import shutil |
43 |
import socket |
|
44 |
import random |
|
42 | 45 |
import unittest |
43 | 46 |
import datetime |
44 | 47 |
import tempfile |
45 | 48 |
import traceback |
49 |
import subprocess |
|
46 | 50 |
|
51 |
from kamaki.clients.cyclades import CycladesClient |
|
47 | 52 |
from kamaki.clients.astakos import AstakosClient |
48 | 53 |
from kamaki.clients.compute import ComputeClient |
49 | 54 |
from kamaki.clients.pithos import PithosClient |
... | ... | |
104 | 109 |
|
105 | 110 |
# -------------------------------------------------------------------- |
106 | 111 |
# BurninTests class |
107 |
# Too few public methods (0/2). pylint: disable-msg=R0903 |
|
112 |
# Too few public methods. pylint: disable-msg=R0903 |
|
113 |
# Too many instance attributes. pylint: disable-msg=R0902 |
|
108 | 114 |
class Clients(object): |
109 | 115 |
"""Our kamaki clients""" |
110 | 116 |
auth_url = None |
... | ... | |
135 | 141 |
action_warning = None |
136 | 142 |
query_interval = None |
137 | 143 |
system_user = None |
144 |
images = None |
|
145 |
flavors = None |
|
138 | 146 |
|
139 | 147 |
@classmethod |
140 | 148 |
def setUpClass(cls): # noqa |
... | ... | |
160 | 168 |
self.clients.compute_url, self.clients.token) |
161 | 169 |
self.clients.compute.CONNECTION_RETRY_LIMIT = self.clients.retry |
162 | 170 |
|
171 |
self.clients.cyclades = CycladesClient( |
|
172 |
self.clients.compute_url, self.clients.token) |
|
173 |
self.clients.cyclades.CONNECTION_RETRY_LIMIT = self.clients.retry |
|
174 |
|
|
163 | 175 |
self.clients.pithos_url = self.clients.astakos.\ |
164 | 176 |
get_service_endpoints('object-store')['publicURL'] |
165 | 177 |
self.info("Pithos url is %s", self.clients.pithos_url) |
... | ... | |
268 | 280 |
self.warning("No system user found") |
269 | 281 |
return None |
270 | 282 |
|
283 |
def _try_until_timeout_expires(self, opmsg, check_fun): |
|
284 |
"""Try to perform an action until timeout expires""" |
|
285 |
assert callable(check_fun), "Not a function" |
|
286 |
|
|
287 |
action_timeout = self.action_timeout |
|
288 |
action_warning = self.action_warning |
|
289 |
if action_warning > action_timeout: |
|
290 |
action_warning = action_timeout |
|
291 |
|
|
292 |
start_time = time.time() |
|
293 |
while (start_time + action_warning) > time.time(): |
|
294 |
try: |
|
295 |
return check_fun() |
|
296 |
except Retry: |
|
297 |
time.sleep(self.query_interval) |
|
298 |
self.warning("Operation `%s' is taking too long", opmsg) |
|
299 |
while (start_time + action_timeout) > time.time(): |
|
300 |
try: |
|
301 |
return check_fun() |
|
302 |
except Retry: |
|
303 |
time.sleep(self.query_interval) |
|
304 |
self.error("Operation `%s' timed out", opmsg) |
|
305 |
self.fail("time out") |
|
306 |
|
|
307 |
def _skip_if(self, condition, msg): |
|
308 |
"""Skip tests""" |
|
309 |
if condition: |
|
310 |
self.info("Test skipped: %s" % msg) |
|
311 |
self.skipTest(msg) |
|
312 |
|
|
271 | 313 |
# ---------------------------------- |
272 | 314 |
# Flavors |
273 | 315 |
def _get_list_of_flavors(self, detail=False): |
... | ... | |
279 | 321 |
flavors = self.clients.compute.list_flavors(detail=detail) |
280 | 322 |
return flavors |
281 | 323 |
|
324 |
def _find_flavors(self, patterns, flavors=None): |
|
325 |
"""Find a list of suitable flavors to use |
|
326 |
|
|
327 |
The patterns is a list of `typed_options'. A list of all flavors |
|
328 |
matching this patterns will be returned. |
|
329 |
|
|
330 |
""" |
|
331 |
if flavors is None: |
|
332 |
flavors = self._get_list_of_flavors(detail=True) |
|
333 |
|
|
334 |
ret_flavors = [] |
|
335 |
for ptrn in patterns: |
|
336 |
parsed_ptrn = parse_typed_option(ptrn) |
|
337 |
if parsed_ptrn is None: |
|
338 |
msg = "Invalid flavor format: %s. Must be [id|name]:.+" |
|
339 |
self.warning(msg, ptrn) |
|
340 |
continue |
|
341 |
flv_type, flv_value = parsed_ptrn |
|
342 |
if flv_type == "name": |
|
343 |
# Filter flavor by name |
|
344 |
msg = "Trying to find a flavor with name %s" |
|
345 |
self.info(msg, flv_value) |
|
346 |
filtered_flvs = \ |
|
347 |
[f for f in flavors if |
|
348 |
re.search(flv_value, f['name'], flags=re.I) is not None] |
|
349 |
elif flv_type == "id": |
|
350 |
# Filter flavors by id |
|
351 |
msg = "Trying to find a flavor with id %s" |
|
352 |
self.info(msg, flv_value) |
|
353 |
filtered_flvs = \ |
|
354 |
[f for f in flavors if str(f['id']) == flv_value] |
|
355 |
else: |
|
356 |
self.error("Unrecognized flavor type %s", flv_type) |
|
357 |
self.fail("Unrecognized flavor type") |
|
358 |
|
|
359 |
# Append and continue |
|
360 |
ret_flavors.extend(filtered_flvs) |
|
361 |
|
|
362 |
self.assertGreater(len(ret_flavors), 0, |
|
363 |
"No matching flavors found") |
|
364 |
return ret_flavors |
|
365 |
|
|
282 | 366 |
# ---------------------------------- |
283 | 367 |
# Images |
284 | 368 |
def _get_list_of_images(self, detail=False): |
... | ... | |
306 | 390 |
|
307 | 391 |
return ret_images |
308 | 392 |
|
309 |
def _find_image(self, patterns, images=None): |
|
310 |
"""Find a suitable image to use
|
|
393 |
def _find_images(self, patterns, images=None):
|
|
394 |
"""Find a list of suitable images to use
|
|
311 | 395 |
|
312 |
The patterns is a list of `typed_options'. The first pattern to
|
|
313 |
match an image will be the one that will be returned.
|
|
396 |
The patterns is a list of `typed_options'. A list of all images
|
|
397 |
matching this patterns will be returned.
|
|
314 | 398 |
|
315 | 399 |
""" |
316 | 400 |
if images is None: |
317 | 401 |
images = self._get_list_of_sys_images() |
318 | 402 |
|
403 |
ret_images = [] |
|
319 | 404 |
for ptrn in patterns: |
320 | 405 |
parsed_ptrn = parse_typed_option(ptrn) |
321 | 406 |
if parsed_ptrn is None: |
... | ... | |
341 | 426 |
self.error("Unrecognized image type %s", img_type) |
342 | 427 |
self.fail("Unrecognized image type") |
343 | 428 |
|
344 |
# Check if we found one |
|
345 |
if filtered_imgs: |
|
346 |
img = filtered_imgs[0] |
|
347 |
self.info("Will use %s with id %s", img['name'], img['id']) |
|
348 |
return img |
|
429 |
# Append and continue |
|
430 |
ret_images.extend(filtered_imgs) |
|
349 | 431 |
|
350 |
# We didn't found one |
|
351 |
err = "No matching image found" |
|
352 |
self.error(err) |
|
353 |
self.fail(err) |
|
432 |
self.assertGreater(len(ret_images), 0, |
|
433 |
"No matching images found") |
|
434 |
return ret_images |
|
354 | 435 |
|
355 | 436 |
# ---------------------------------- |
356 | 437 |
# Pithos |
... | ... | |
387 | 468 |
self.clients.pithos.container = container |
388 | 469 |
self.clients.pithos.container_put() |
389 | 470 |
|
471 |
# ---------------------------------- |
|
472 |
# Servers |
|
473 |
def _get_list_of_servers(self, detail=False): |
|
474 |
"""Get (detailed) list of servers""" |
|
475 |
if detail: |
|
476 |
self.info("Getting detailed list of servers") |
|
477 |
else: |
|
478 |
self.info("Getting simple list of servers") |
|
479 |
return self.clients.cyclades.list_servers(detail=detail) |
|
480 |
|
|
481 |
def _get_server_details(self, server): |
|
482 |
"""Get details for a server""" |
|
483 |
self.info("Getting details for server %s with id %s", |
|
484 |
server['name'], server['id']) |
|
485 |
return self.clients.cyclades.get_server_details(server['id']) |
|
486 |
|
|
487 |
def _create_server(self, name, image, flavor): |
|
488 |
"""Create a new server""" |
|
489 |
self.info("Creating a server with name %s", name) |
|
490 |
self.info("Using image %s with id %s", image['name'], image['id']) |
|
491 |
self.info("Using flavor %s with id %s", flavor['name'], flavor['id']) |
|
492 |
server = self.clients.cyclades.create_server( |
|
493 |
name, flavor['id'], image['id']) |
|
494 |
|
|
495 |
self.info("Server id: %s", server['id']) |
|
496 |
self.info("Server password: %s", server['adminPass']) |
|
497 |
|
|
498 |
self.assertEqual(server['name'], name) |
|
499 |
self.assertEqual(server['flavor']['id'], flavor['id']) |
|
500 |
self.assertEqual(server['image']['id'], image['id']) |
|
501 |
self.assertEqual(server['status'], "BUILD") |
|
502 |
|
|
503 |
return server |
|
504 |
|
|
505 |
def _get_connection_username(self, server): |
|
506 |
"""Determine the username to use to connect to the server""" |
|
507 |
users = server['metadata'].get("users", None) |
|
508 |
ret_user = None |
|
509 |
if users is not None: |
|
510 |
user_list = users.split() |
|
511 |
if "root" in user_list: |
|
512 |
ret_user = "root" |
|
513 |
else: |
|
514 |
ret_user = random.choice(user_list) |
|
515 |
else: |
|
516 |
# Return the login name for connections based on the server OS |
|
517 |
self.info("Could not find `users' metadata in server. Let's guess") |
|
518 |
os_value = server['metadata'].get("os") |
|
519 |
if os_value in ("Ubuntu", "Kubuntu", "Fedora"): |
|
520 |
ret_user = "user" |
|
521 |
elif os_value in ("windows", "windows_alpha1"): |
|
522 |
ret_user = "Administrator" |
|
523 |
else: |
|
524 |
ret_user = "root" |
|
525 |
|
|
526 |
self.assertIsNotNone(ret_user) |
|
527 |
self.info("User's login name: %s", ret_user) |
|
528 |
return ret_user |
|
529 |
|
|
530 |
def _insist_on_server_transition(self, server, curr_status, new_status): |
|
531 |
"""Insist on server transiting from curr_status to new_status""" |
|
532 |
def check_fun(): |
|
533 |
"""Check server status""" |
|
534 |
srv = self.clients.cyclades.get_server_details(server['id']) |
|
535 |
if srv['status'] == curr_status: |
|
536 |
raise Retry() |
|
537 |
elif srv['status'] == new_status: |
|
538 |
return |
|
539 |
else: |
|
540 |
msg = "Server %s went to unexpected status %s" |
|
541 |
self.error(msg, server['name'], srv['status']) |
|
542 |
self.fail(msg % (server['name'], srv['status'])) |
|
543 |
opmsg = "Waiting for server %s to transit from %s to %s" |
|
544 |
self.info(opmsg, server['name'], curr_status, new_status) |
|
545 |
opmsg = opmsg % (server['name'], curr_status, new_status) |
|
546 |
self._try_until_timeout_expires(opmsg, check_fun) |
|
547 |
|
|
548 |
def _insist_on_tcp_connection(self, family, host, port): |
|
549 |
"""Insist on tcp connection""" |
|
550 |
def check_fun(): |
|
551 |
"""Get a connected socket from the specified family to host:port""" |
|
552 |
sock = None |
|
553 |
for res in socket.getaddrinfo(host, port, family, |
|
554 |
socket.SOCK_STREAM, 0, |
|
555 |
socket.AI_PASSIVE): |
|
556 |
fam, socktype, proto, _, saddr = res |
|
557 |
try: |
|
558 |
sock = socket.socket(fam, socktype, proto) |
|
559 |
except socket.error: |
|
560 |
sock = None |
|
561 |
continue |
|
562 |
try: |
|
563 |
sock.connect(saddr) |
|
564 |
except socket.error: |
|
565 |
sock.close() |
|
566 |
sock = None |
|
567 |
continue |
|
568 |
if sock is None: |
|
569 |
raise Retry |
|
570 |
return sock |
|
571 |
familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6", |
|
572 |
socket.AF_UNSPEC: "Unspecified-IPv4/6"} |
|
573 |
opmsg = "Connecting over %s to %s:%s" |
|
574 |
self.info(opmsg, familystr.get(family, "Unknown"), host, port) |
|
575 |
opmsg = opmsg % (familystr.get(family, "Unknown"), host, port) |
|
576 |
return self._try_until_timeout_expires(opmsg, check_fun) |
|
577 |
|
|
578 |
def _get_ip(self, server, version): |
|
579 |
"""Get the public IP of a server from the detailed server info""" |
|
580 |
assert version in (4, 6) |
|
581 |
|
|
582 |
nics = server['attachments'] |
|
583 |
public_addrs = None |
|
584 |
for nic in nics: |
|
585 |
net_id = nic['network_id'] |
|
586 |
if self.clients.cyclades.get_network_details(net_id)['public']: |
|
587 |
public_addrs = nic['ipv' + str(version)] |
|
588 |
|
|
589 |
self.assertIsNotNone(public_addrs) |
|
590 |
msg = "Servers %s public IPv%s is %s" |
|
591 |
self.info(msg, server['name'], version, public_addrs) |
|
592 |
return public_addrs |
|
593 |
|
|
594 |
def _insist_on_ping(self, ip_addr, version): |
|
595 |
"""Test server responds to a single IPv4 of IPv6 ping""" |
|
596 |
def check_fun(): |
|
597 |
"""Ping to server""" |
|
598 |
assert version in (4, 6) |
|
599 |
cmd = ("ping%s -c 3 -w 20 %s" % |
|
600 |
("6" if version == 6 else "", ip_addr)) |
|
601 |
ping = subprocess.Popen( |
|
602 |
cmd, shell=True, stdout=subprocess.PIPE, |
|
603 |
stderr=subprocess.PIPE) |
|
604 |
ping.communicate() |
|
605 |
ret = ping.wait() |
|
606 |
if ret != 0: |
|
607 |
raise Retry |
|
608 |
opmsg = "Sent IPv%s ping requests to %s" |
|
609 |
self.info(opmsg, version, ip_addr) |
|
610 |
opmsg = opmsg % (version, ip_addr) |
|
611 |
self._try_until_timeout_expires(opmsg, check_fun) |
|
612 |
|
|
390 | 613 |
|
391 | 614 |
# -------------------------------------------------------------------- |
392 | 615 |
# Initialize Burnin |
... | ... | |
412 | 635 |
BurninTests.action_warning = opts.action_warning |
413 | 636 |
BurninTests.query_interval = opts.query_interval |
414 | 637 |
BurninTests.system_user = opts.system_user |
638 |
BurninTests.flavors = opts.flavors |
|
639 |
BurninTests.images = opts.images |
|
415 | 640 |
BurninTests.run_id = SNF_TEST_PREFIX + \ |
416 | 641 |
datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d%H%M%S") |
417 | 642 |
|
... | ... | |
427 | 652 |
|
428 | 653 |
# -------------------------------------------------------------------- |
429 | 654 |
# Run Burnin |
430 |
def run(testsuites, failfast=False, final_report=False): |
|
655 |
def run_burnin(testsuites, failfast=False, final_report=False):
|
|
431 | 656 |
"""Run burnin testsuites""" |
432 | 657 |
global logger # Using global. pylint: disable-msg=C0103,W0603,W0602 |
433 | 658 |
|
434 | 659 |
success = True |
435 | 660 |
for tcase in testsuites: |
436 |
tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase) |
|
437 |
results = tsuite.run(BurninTestResult()) |
|
438 |
|
|
439 |
was_success = was_successful(tcase.__name__, results.wasSuccessful()) |
|
661 |
was_success = run_test(tcase) |
|
440 | 662 |
success = success and was_success |
441 | 663 |
if failfast and not success: |
442 | 664 |
break |
... | ... | |
451 | 673 |
return 0 if success else 1 |
452 | 674 |
|
453 | 675 |
|
676 |
def run_test(tcase): |
|
677 |
"""Run a testcase""" |
|
678 |
tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase) |
|
679 |
results = tsuite.run(BurninTestResult()) |
|
680 |
|
|
681 |
return was_successful(tcase.__name__, results.wasSuccessful()) |
|
682 |
|
|
683 |
|
|
454 | 684 |
# -------------------------------------------------------------------- |
455 | 685 |
# Helper functions |
456 | 686 |
def was_successful(tsuite, success): |
... | ... | |
494 | 724 |
|
495 | 725 |
def __set__(self, obj, value): |
496 | 726 |
self.val = value |
727 |
|
|
728 |
|
|
729 |
class Retry(Exception): |
|
730 |
"""Retry the action |
|
731 |
|
|
732 |
This is used by _try_unit_timeout_expires method. |
|
733 |
|
|
734 |
""" |
/dev/null | ||
---|---|---|
1 |
# Copyright 2013 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
""" |
|
35 |
This is the burnin class that tests the Cyclades functionality |
|
36 |
|
|
37 |
""" |
|
38 |
|
|
39 |
from synnefo_tools.burnin.common import BurninTests, Proper |
|
40 |
|
|
41 |
|
|
42 |
# Too many public methods. pylint: disable-msg=R0904 |
|
43 |
class FlavorsTestSuite(BurninTests): |
|
44 |
"""Test flavor lists for consistency""" |
|
45 |
simple_flavors = Proper(value=None) |
|
46 |
detailed_flavors = Proper(value=None) |
|
47 |
simple_names = Proper(value=None) |
|
48 |
|
|
49 |
def test_001_simple_flavors(self): |
|
50 |
"""Test flavor list actually returns flavors""" |
|
51 |
self.simple_flavors = self._get_list_of_flavors(detail=False) |
|
52 |
self.assertGreater(len(self.simple_flavors), 0) |
|
53 |
|
|
54 |
def test_002_get_detailed_flavors(self): |
|
55 |
"""Test detailed flavor list is the same length as list""" |
|
56 |
self.detailed_flavors = self._get_list_of_flavors(detail=True) |
|
57 |
self.assertEquals(len(self.simple_flavors), len(self.detailed_flavors)) |
|
58 |
|
|
59 |
def test_003_same_flavor_names(self): |
|
60 |
"""Test detailed and simple flavor list contain same names""" |
|
61 |
names = sorted([flv['name'] for flv in self.simple_flavors]) |
|
62 |
self.simple_names = names |
|
63 |
detailed_names = sorted([flv['name'] for flv in self.detailed_flavors]) |
|
64 |
self.assertEqual(self.simple_names, detailed_names) |
|
65 |
|
|
66 |
def test_004_unique_flavor_names(self): |
|
67 |
"""Test flavors have unique names""" |
|
68 |
self.assertEqual(sorted(list(set(self.simple_names))), |
|
69 |
self.simple_names) |
|
70 |
|
|
71 |
def test_005_well_formed_names(self): |
|
72 |
"""Test flavors have well formed names |
|
73 |
|
|
74 |
Test flavors have names of the form CxxRyyDzz, where xx is vCPU count, |
|
75 |
yy is RAM in MiB, zz is Disk in GiB |
|
76 |
|
|
77 |
""" |
|
78 |
for flv in self.detailed_flavors: |
|
79 |
flavor = (flv['vcpus'], flv['ram'], flv['disk'], |
|
80 |
flv['SNF:disk_template']) |
|
81 |
self.assertEqual("C%dR%dD%d%s" % flavor, flv['name'], |
|
82 |
"Flavor %s doesn't match its specs" % flv['name']) |
b/snf-tools/synnefo_tools/burnin/images_tests.py | ||
---|---|---|
135 | 135 |
def test_007_download_image(self): |
136 | 136 |
"""Download image from Pithos""" |
137 | 137 |
# Find the 'Debian Base' image |
138 |
image = self._find_image(["name:^Debian Base$"], |
|
139 |
images=self.system_images) |
|
138 |
images = self._find_images(["name:^Debian Base$"], |
|
139 |
images=self.system_images) |
|
140 |
image = images[0] |
|
141 |
self.info("Will use %s with id %s", image['name'], image['id']) |
|
140 | 142 |
image_location = \ |
141 | 143 |
image['location'].replace("://", " ").replace("/", " ").split() |
142 | 144 |
image_owner = image_location[1] |
b/snf-tools/synnefo_tools/burnin/logger.py | ||
---|---|---|
99 | 99 |
|
100 | 100 |
def _format_message(msg, *args): |
101 | 101 |
"""Format the message using the args""" |
102 |
return (msg % args) + "\n" |
|
102 |
if args: |
|
103 |
return (msg % args) + "\n" |
|
104 |
else: |
|
105 |
return msg + "\n" |
|
103 | 106 |
|
104 | 107 |
|
105 | 108 |
def _list_to_string(lst, append=""): |
... | ... | |
439 | 442 |
""" |
440 | 443 |
if self.use_colors: |
441 | 444 |
if callable(color_fun): |
442 |
return color_fun((msg % args)) + "\n" |
|
445 |
if args: |
|
446 |
return color_fun((msg % args)) + "\n" |
|
447 |
else: |
|
448 |
return color_fun(msg) + "\n" |
|
443 | 449 |
else: |
444 | 450 |
args = tuple([_blue(arg) for arg in args]) |
445 | 451 |
return _format_message(msg, *args) |
b/snf-tools/synnefo_tools/burnin/server_tests.py | ||
---|---|---|
1 |
# Copyright 2013 GRNET S.A. All rights reserved. |
|
2 |
# |
|
3 |
# Redistribution and use in source and binary forms, with or |
|
4 |
# without modification, are permitted provided that the following |
|
5 |
# conditions are met: |
|
6 |
# |
|
7 |
# 1. Redistributions of source code must retain the above |
|
8 |
# copyright notice, this list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, this list of conditions and the following |
|
13 |
# disclaimer in the documentation and/or other materials |
|
14 |
# provided with the distribution. |
|
15 |
# |
|
16 |
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS |
|
17 |
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
18 |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
|
19 |
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR |
|
20 |
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|
21 |
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|
22 |
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF |
|
23 |
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED |
|
24 |
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
|
25 |
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
|
27 |
# POSSIBILITY OF SUCH DAMAGE. |
|
28 |
# |
|
29 |
# The views and conclusions contained in the software and |
|
30 |
# documentation are those of the authors and should not be |
|
31 |
# interpreted as representing official policies, either expressed |
|
32 |
# or implied, of GRNET S.A. |
|
33 |
|
|
34 |
""" |
|
35 |
This is the burnin class that tests the Servers' functionality |
|
36 |
|
|
37 |
""" |
|
38 |
|
|
39 |
import sys |
|
40 |
import IPy |
|
41 |
import random |
|
42 |
import socket |
|
43 |
|
|
44 |
from vncauthproxy.d3des import generate_response as d3des_generate_response |
|
45 |
|
|
46 |
from synnefo_tools.burnin.common import BurninTests, Proper, run_test |
|
47 |
|
|
48 |
|
|
49 |
# Too many public methods. pylint: disable-msg=R0904 |
|
50 |
# This class gets replicated into actual TestCases dynamically |
|
51 |
class GeneratedServerTestSuite(BurninTests): |
|
52 |
"""Test Spawning Serverfunctionality""" |
|
53 |
use_image = Proper(value=None) |
|
54 |
avail_flavors = Proper(value=None) |
|
55 |
use_flavor = Proper(value=None) |
|
56 |
server = Proper(value=None) |
|
57 |
ipv4 = Proper(value=None) |
|
58 |
ipv6 = Proper(value=None) |
|
59 |
|
|
60 |
def test_001_submit_create_server(self): |
|
61 |
"""Submit a create server request""" |
|
62 |
servername = "%s for %s" % (self.run_id, self.use_image['name']) |
|
63 |
self.use_flavor = random.choice(self.avail_flavors) |
|
64 |
|
|
65 |
self.server = self._create_server( |
|
66 |
servername, self.use_image, self.use_flavor) |
|
67 |
|
|
68 |
def test_002_server_build_list(self): |
|
69 |
"""Test server is in BUILD state, in server list""" |
|
70 |
servers = self._get_list_of_servers(detail=True) |
|
71 |
servers = [s for s in servers if s['id'] == self.server['id']] |
|
72 |
|
|
73 |
self.assertEqual(len(servers), 1) |
|
74 |
server = servers[0] |
|
75 |
self.assertEqual(server['name'], self.server['name']) |
|
76 |
self.assertEqual(server['flavor']['id'], self.use_flavor['id']) |
|
77 |
self.assertEqual(server['image']['id'], self.use_image['id']) |
|
78 |
self.assertEqual(server['status'], "BUILD") |
|
79 |
|
|
80 |
def test_003_server_build_details(self): |
|
81 |
"""Test server is in BUILD state, in details""" |
|
82 |
server = self._get_server_details(self.server) |
|
83 |
self.assertEqual(server['name'], self.server['name']) |
|
84 |
self.assertEqual(server['flavor']['id'], self.use_flavor['id']) |
|
85 |
self.assertEqual(server['image']['id'], self.use_image['id']) |
|
86 |
self.assertEqual(server['status'], "BUILD") |
|
87 |
|
|
88 |
def test_004_set_server_metadata(self): |
|
89 |
"""Test setting some of the server's metadata""" |
|
90 |
image = self.clients.cyclades.get_image_details(self.use_image['id']) |
|
91 |
os_value = image['metadata']['os'] |
|
92 |
self.clients.cyclades.update_server_metadata( |
|
93 |
self.server['id'], OS=os_value) |
|
94 |
|
|
95 |
servermeta = \ |
|
96 |
self.clients.cyclades.get_server_metadata(self.server['id']) |
|
97 |
imagemeta = \ |
|
98 |
self.clients.cyclades.get_image_metadata(self.use_image['id']) |
|
99 |
self.assertEqual(servermeta['OS'], imagemeta['os']) |
|
100 |
|
|
101 |
def test_005_server_becomes_active(self): |
|
102 |
"""Test server becomes ACTIVE""" |
|
103 |
self._insist_on_server_transition(self.server, "BUILD", "ACTIVE") |
|
104 |
|
|
105 |
def test_006_get_server_oob_console(self): |
|
106 |
"""Test getting OOB server console over VNC |
|
107 |
|
|
108 |
Implementation of RFB protocol follows |
|
109 |
http://www.realvnc.com/docs/rfbproto.pdf. |
|
110 |
|
|
111 |
""" |
|
112 |
console = self.clients.cyclades.get_server_console(self.server['id']) |
|
113 |
self.assertEquals(console['type'], "vnc") |
|
114 |
sock = self._insist_on_tcp_connection( |
|
115 |
socket.AF_INET, console['host'], console['port']) |
|
116 |
|
|
117 |
# Step 1. ProtocolVersion message (par. 6.1.1) |
|
118 |
version = sock.recv(1024) |
|
119 |
self.assertEquals(version, 'RFB 003.008\n') |
|
120 |
sock.send(version) |
|
121 |
|
|
122 |
# Step 2. Security (par 6.1.2): Only VNC Authentication supported |
|
123 |
sec = sock.recv(1024) |
|
124 |
self.assertEquals(list(sec), ['\x01', '\x02']) |
|
125 |
|
|
126 |
# Step 3. Request VNC Authentication (par 6.1.2) |
|
127 |
sock.send('\x02') |
|
128 |
|
|
129 |
# Step 4. Receive Challenge (par 6.2.2) |
|
130 |
challenge = sock.recv(1024) |
|
131 |
self.assertEquals(len(challenge), 16) |
|
132 |
|
|
133 |
# Step 5. DES-Encrypt challenge, use password as key (par 6.2.2) |
|
134 |
response = d3des_generate_response( |
|
135 |
(console["password"] + '\0' * 8)[:8], challenge) |
|
136 |
sock.send(response) |
|
137 |
|
|
138 |
# Step 6. SecurityResult (par 6.1.3) |
|
139 |
result = sock.recv(4) |
|
140 |
self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00']) |
|
141 |
sock.close() |
|
142 |
|
|
143 |
def test_007_server_has_ipv4(self): |
|
144 |
"""Test active server has a valid IPv4 address""" |
|
145 |
server = self.clients.cyclades.get_server_details(self.server['id']) |
|
146 |
# Update the server attribute |
|
147 |
self.server = server |
|
148 |
|
|
149 |
self.ipv4 = self._get_ip(server, 4) |
|
150 |
self.assertEquals(IPy.IP(self.ipv4).version(), 4) |
|
151 |
|
|
152 |
def test_008_server_has_ipv6(self): |
|
153 |
"""Test active server has a valid IPv6 address""" |
|
154 |
self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled") |
|
155 |
|
|
156 |
self.ipv6 = self._get_ip(self.server, 6) |
|
157 |
self.assertEquals(IPy.IP(self.ipv6).version(), 6) |
|
158 |
|
|
159 |
def test_009_server_ping_ipv4(self): |
|
160 |
"""Test server responds to ping on IPv4 address""" |
|
161 |
self._insist_on_ping(self.ipv4, 4) |
|
162 |
|
|
163 |
def test_010_server_ping_ipv6(self): |
|
164 |
"""Test server responds to ping on IPv6 address""" |
|
165 |
self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled") |
|
166 |
self._insist_on_ping(self.ipv6, 6) |
|
167 |
|
|
168 |
def test_011_submit_shutdown(self): |
|
169 |
"""Test submit request to shutdown server""" |
|
170 |
self.clients.cyclades.shutdown_server(self.server['id']) |
|
171 |
|
|
172 |
def test_012_server_becomes_stopped(self): |
|
173 |
"""Test server becomes STOPPED""" |
|
174 |
self._insist_on_server_transition(self.server, "ACTIVE", "STOPPED") |
|
175 |
|
|
176 |
def test_013_submit_start(self): |
|
177 |
"""Test submit start server request""" |
|
178 |
self.clients.cyclades.start_server(self.server['id']) |
|
179 |
|
|
180 |
def test_014_server_becomes_active(self): |
|
181 |
"""Test server becomes ACTIVE again""" |
|
182 |
self._insist_on_server_transition(self.server, "STOPPED", "ACTIVE") |
|
183 |
|
|
184 |
def test_015_server_ping_ipv4(self): |
|
185 |
"""Test server OS is actually up and running again""" |
|
186 |
self.test_009_server_ping_ipv4() |
|
187 |
|
|
188 |
|
|
189 |
# -------------------------------------------------------------------- |
|
190 |
# The actuall test class. We use this class to dynamically create |
|
191 |
# tests from the GeneratedServerTestSuite class. Each of these classes |
|
192 |
# will run the same tests using different images and or flavors. |
|
193 |
# The creation and running of our GeneratedServerTestSuite class will |
|
194 |
# happen as a testsuite itself (everything here is a test!). |
|
195 |
class ServerTestSuite(BurninTests): |
|
196 |
"""Generate and run the GeneratedServerTestSuite |
|
197 |
|
|
198 |
We will generate as many testsuites as the number of images given. |
|
199 |
Each of these testsuites will use the given flavors at will (random). |
|
200 |
|
|
201 |
""" |
|
202 |
avail_images = Proper(value=None) |
|
203 |
avail_flavors = Proper(value=None) |
|
204 |
gen_classes = Proper(value=None) |
|
205 |
|
|
206 |
def test_001_images_to_use(self): |
|
207 |
"""Find images to be used by GeneratedServerTestSuite""" |
|
208 |
if self.images is None: |
|
209 |
self.info("No --images given. Will use the default %s", |
|
210 |
"^Debian Base$") |
|
211 |
filters = ["name:^Debian Base$"] |
|
212 |
else: |
|
213 |
filters = self.images |
|
214 |
|
|
215 |
self.avail_images = self._find_images(filters) |
|
216 |
self.info("Found %s images. Let's create an equal number of tests", |
|
217 |
len(self.avail_images)) |
|
218 |
|
|
219 |
def test_002_flavors_to_use(self): |
|
220 |
"""Find flavors to be used by GeneratedServerTestSuite""" |
|
221 |
flavors = self._get_list_of_flavors(detail=True) |
|
222 |
|
|
223 |
if self.flavors is None: |
|
224 |
self.info("No --flavors given. Will use all of them") |
|
225 |
self.avail_flavors = flavors |
|
226 |
else: |
|
227 |
self.avail_flavors = self._find_flavors( |
|
228 |
patterns=self.flavors, flavors=flavors) |
|
229 |
self.info("Found %s flavors to choose from", len(self.avail_flavors)) |
|
230 |
|
|
231 |
def test_003_create_testsuites(self): |
|
232 |
"""Generate the GeneratedServerTestSuite tests""" |
|
233 |
gen_classes = [] |
|
234 |
for img in self.avail_images: |
|
235 |
name = (str("GeneratedServerTestSuite_(%s)" % |
|
236 |
img['name']).replace(" ", "_")) |
|
237 |
self.info("Constructing class %s", name) |
|
238 |
class_dict = { |
|
239 |
'use_image': Proper(value=img), |
|
240 |
'avail_flavors': Proper(value=self.avail_flavors) |
|
241 |
} |
|
242 |
cls = type(name, (GeneratedServerTestSuite,), class_dict) |
|
243 |
# Make sure the class can be pickled, by listing it among |
|
244 |
# the attributes of __main__. A PicklingError is raised otherwise. |
|
245 |
thismodule = sys.modules[__name__] |
|
246 |
setattr(thismodule, name, cls) |
|
247 |
# Append the generated class |
|
248 |
gen_classes.append(cls) |
|
249 |
|
|
250 |
self.gen_classes = gen_classes |
|
251 |
|
|
252 |
def test_004_run_testsuites(self): |
|
253 |
"""Run the generated tests""" |
|
254 |
for gen_cls in self.gen_classes: |
|
255 |
self.info("Running testsuite %s", gen_cls.__name__) |
|
256 |
run_test(gen_cls) |
Also available in: Unified diff