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