Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / common.py @ d0bb677f

History | View | Annotate | Download (25.5 kB)

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
Common utils for burnin tests
36

37
"""
38

    
39
import re
40
import shutil
41
import unittest
42
import datetime
43
import tempfile
44
import traceback
45

    
46
from kamaki.clients.cyclades import CycladesClient, CycladesNetworkClient
47
from kamaki.clients.astakos import AstakosClient, parse_endpoints
48
from kamaki.clients.compute import ComputeClient
49
from kamaki.clients.pithos import PithosClient
50
from kamaki.clients.image import ImageClient
51

    
52
from synnefo_tools.burnin.logger import Log
53

    
54

    
55
# --------------------------------------------------------------------
56
# Global variables
57
logger = None   # pylint: disable=invalid-name
58
success = None  # pylint: disable=invalid-name
59
SNF_TEST_PREFIX = "snf-test-"
60
CONNECTION_RETRY_LIMIT = 2
61
SYSTEM_USERS = ["images@okeanos.grnet.gr", "images@demo.synnefo.org"]
62
KB = 2**10
63
MB = 2**20
64
GB = 2**30
65

    
66

    
67
# --------------------------------------------------------------------
68
# BurninTestResult class
69
class BurninTestResult(unittest.TestResult):
70
    """Modify the TextTestResult class"""
71
    def __init__(self):
72
        super(BurninTestResult, self).__init__()
73

    
74
        # Test parameters
75
        self.failfast = True
76

    
77
    def startTest(self, test):  # noqa
78
        """Called when the test case test is about to be run"""
79
        super(BurninTestResult, self).startTest(test)
80
        logger.log(test.__class__.__name__, test.shortDescription())
81

    
82
    # pylint: disable=no-self-use
83
    def _test_failed(self, test, err):
84
        """Test failed"""
85
        # Get class name
86
        if test.__class__.__name__ == "_ErrorHolder":
87
            class_name = test.id().split('.')[-1].rstrip(')')
88
        else:
89
            class_name = test.__class__.__name__
90
        err_msg = str(test) + "... failed (%s)."
91
        timestamp = datetime.datetime.strftime(
92
            datetime.datetime.now(), "%a %b %d %Y %H:%M:%S")
93
        logger.error(class_name, err_msg, timestamp)
94
        (err_type, err_value, err_trace) = err
95
        trcback = traceback.format_exception(err_type, err_value, err_trace)
96
        logger.info(class_name, trcback)
97

    
98
    def addError(self, test, err):  # noqa
99
        """Called when the test case test raises an unexpected exception"""
100
        super(BurninTestResult, self).addError(test, err)
101
        self._test_failed(test, err)
102

    
103
    def addFailure(self, test, err):  # noqa
104
        """Called when the test case test signals a failure"""
105
        super(BurninTestResult, self).addFailure(test, err)
106
        self._test_failed(test, err)
107

    
108
    # pylint: disable=fixme
109
    def addSkip(self, test, reason):  # noqa
110
        """Called when the test case test is skipped
111

112
        If reason starts with "__SkipClass__: " then
113
        we should stop the execution of all the TestSuite.
114

115
        TODO: There should be a better way to do this
116

117
        """
118
        super(BurninTestResult, self).addSkip(test, reason)
119
        if reason.startswith("__SkipClass__: "):
120
            self.stop()
121

    
122

    
123
# --------------------------------------------------------------------
124
# Helper Classes
125
# pylint: disable=too-few-public-methods
126
# pylint: disable=too-many-instance-attributes
127
class Clients(object):
128
    """Our kamaki clients"""
129
    auth_url = None
130
    token = None
131
    # Astakos
132
    astakos = None
133
    retry = CONNECTION_RETRY_LIMIT
134
    # Compute
135
    compute = None
136
    compute_url = None
137
    # Cyclades
138
    cyclades = None
139
    # Network
140
    network = None
141
    network_url = None
142
    # Pithos
143
    pithos = None
144
    pithos_url = None
145
    # Image
146
    image = None
147
    image_url = None
148

    
149
    def initialize_clients(self):
150
        """Initialize all the Kamaki Clients"""
151
        self.astakos = AstakosClient(self.auth_url, self.token)
152
        self.astakos.CONNECTION_RETRY_LIMIT = self.retry
153

    
154
        endpoints = self.astakos.authenticate()
155

    
156
        self.compute_url = _get_endpoint_url(endpoints, "compute")
157
        self.compute = ComputeClient(self.compute_url, self.token)
158
        self.compute.CONNECTION_RETRY_LIMIT = self.retry
159

    
160
        self.cyclades = CycladesClient(self.compute_url, self.token)
161
        self.cyclades.CONNECTION_RETRY_LIMIT = self.retry
162

    
163
        self.network_url = _get_endpoint_url(endpoints, "network")
164
        self.network = CycladesNetworkClient(self.network_url, self.token)
165
        self.network.CONNECTION_RETRY_LIMIT = self.retry
166

    
167
        self.pithos_url = _get_endpoint_url(endpoints, "object-store")
168
        self.pithos = PithosClient(self.pithos_url, self.token)
169
        self.pithos.CONNECTION_RETRY_LIMIT = self.retry
170

    
171
        self.image_url = _get_endpoint_url(endpoints, "image")
172
        self.image = ImageClient(self.image_url, self.token)
173
        self.image.CONNECTION_RETRY_LIMIT = self.retry
174

    
175

    
176
def _get_endpoint_url(endpoints, endpoint_type):
177
    """Get the publicURL for the specified endpoint"""
178

    
179
    service_catalog = parse_endpoints(endpoints, ep_type=endpoint_type)
180
    return service_catalog[0]['endpoints'][0]['publicURL']
181

    
182

    
183
class Proper(object):
184
    """A descriptor used by tests implementing the TestCase class
185

186
    Since each instance of the TestCase will only be used to run a single
187
    test method (a new fixture is created for each test) the attributes can
188
    not be saved in the class instances. Instead we use descriptors.
189

190
    """
191
    def __init__(self, value=None):
192
        self.val = value
193

    
194
    def __get__(self, obj, objtype=None):
195
        return self.val
196

    
197
    def __set__(self, obj, value):
198
        self.val = value
199

    
200

    
201
# --------------------------------------------------------------------
202
# BurninTests class
203
# pylint: disable=too-many-public-methods
204
class BurninTests(unittest.TestCase):
205
    """Common class that all burnin tests should implement"""
206
    clients = Clients()
207
    run_id = None
208
    use_ipv6 = None
209
    action_timeout = None
210
    action_warning = None
211
    query_interval = None
212
    system_user = None
213
    images = None
214
    flavors = None
215
    delete_stale = False
216
    temp_directory = None
217
    failfast = None
218

    
219
    quotas = Proper(value=None)
220

    
221
    @classmethod
222
    def setUpClass(cls):  # noqa
223
        """Initialize BurninTests"""
224
        cls.suite_name = cls.__name__
225
        logger.testsuite_start(cls.suite_name)
226

    
227
        # Set test parameters
228
        cls.longMessage = True
229

    
230
    def test_000_clients_setup(self):
231
        """Initializing astakos/cyclades/pithos clients"""
232
        # Update class attributes
233
        self.clients.initialize_clients()
234
        self.info("Astakos auth url is %s", self.clients.auth_url)
235
        self.info("Cyclades url is %s", self.clients.compute_url)
236
        self.info("Network url is %s", self.clients.network_url)
237
        self.info("Pithos url is %s", self.clients.pithos_url)
238
        self.info("Image url is %s", self.clients.image_url)
239

    
240
        user_uuid = self._get_uuid()
241
        self.quotas = self._get_quotas()
242
        for puuid, quotas in self.quotas.items():
243
            project_name = self._get_project_name(puuid, user_uuid)
244
            self.info("  Project %s:", project_name)
245
            self.info("    Disk usage is         %s bytes",
246
                      quotas['cyclades.disk']['usage'])
247
            self.info("    VM usage is           %s",
248
                      quotas['cyclades.vm']['usage'])
249
            self.info("    DiskSpace usage is    %s bytes",
250
                      quotas['pithos.diskspace']['usage'])
251
            self.info("    Ram usage is          %s bytes",
252
                      quotas['cyclades.ram']['usage'])
253
            self.info("    Floating IPs usage is %s",
254
                      quotas['cyclades.floating_ip']['usage'])
255
            self.info("    CPU usage is          %s",
256
                      quotas['cyclades.cpu']['usage'])
257
            self.info("    Network usage is      %s",
258
                      quotas['cyclades.network.private']['usage'])
259

    
260
    def _run_tests(self, tcases):
261
        """Run some generated testcases"""
262
        global success  # pylint: disable=invalid-name, global-statement
263

    
264
        for tcase in tcases:
265
            self.info("Running testsuite %s", tcase.__name__)
266
            success = run_test(tcase) and success
267
            if self.failfast and not success:
268
                break
269

    
270
    # ----------------------------------
271
    # Loggers helper functions
272
    def log(self, msg, *args):
273
        """Pass the section value to logger"""
274
        logger.log(self.suite_name, msg, *args)
275

    
276
    def info(self, msg, *args):
277
        """Pass the section value to logger"""
278
        logger.info(self.suite_name, msg, *args)
279

    
280
    def debug(self, msg, *args):
281
        """Pass the section value to logger"""
282
        logger.debug(self.suite_name, msg, *args)
283

    
284
    def warning(self, msg, *args):
285
        """Pass the section value to logger"""
286
        logger.warning(self.suite_name, msg, *args)
287

    
288
    def error(self, msg, *args):
289
        """Pass the section value to logger"""
290
        logger.error(self.suite_name, msg, *args)
291

    
292
    # ----------------------------------
293
    # Helper functions that every testsuite may need
294
    def _get_uuid(self):
295
        """Get our uuid"""
296
        authenticate = self.clients.astakos.authenticate()
297
        uuid = authenticate['access']['user']['id']
298
        self.info("User's uuid is %s", uuid)
299
        return uuid
300

    
301
    def _get_username(self):
302
        """Get our User Name"""
303
        authenticate = self.clients.astakos.authenticate()
304
        username = authenticate['access']['user']['name']
305
        self.info("User's name is %s", username)
306
        return username
307

    
308
    def _create_tmp_directory(self):
309
        """Create a tmp directory"""
310
        temp_dir = tempfile.mkdtemp(dir=self.temp_directory)
311
        self.info("Temp directory %s created", temp_dir)
312
        return temp_dir
313

    
314
    def _remove_tmp_directory(self, tmp_dir):
315
        """Remove a tmp directory"""
316
        try:
317
            shutil.rmtree(tmp_dir)
318
            self.info("Temp directory %s deleted", tmp_dir)
319
        except OSError:
320
            pass
321

    
322
    def _get_uuid_of_system_user(self):
323
        """Get the uuid of the system user
324

325
        This is the user that upload the 'official' images.
326

327
        """
328
        self.info("Getting the uuid of the system user")
329
        system_users = None
330
        if self.system_user is not None:
331
            try:
332
                su_type, su_value = parse_typed_option(self.system_user)
333
                if su_type == "name":
334
                    system_users = [su_value]
335
                elif su_type == "id":
336
                    self.info("System user's uuid is %s", su_value)
337
                    return su_value
338
                else:
339
                    self.error("Unrecognized system-user type %s", su_type)
340
                    self.fail("Unrecognized system-user type")
341
            except ValueError:
342
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
343
                self.warning(msg, self.system_user)
344

    
345
        if system_users is None:
346
            system_users = SYSTEM_USERS
347

    
348
        uuids = self.clients.astakos.get_uuids(system_users)
349
        for su_name in system_users:
350
            self.info("Trying username %s", su_name)
351
            if su_name in uuids:
352
                self.info("System user's uuid is %s", uuids[su_name])
353
                return uuids[su_name]
354

    
355
        self.warning("No system user found")
356
        return None
357

    
358
    def _skip_if(self, condition, msg):
359
        """Skip tests"""
360
        if condition:
361
            self.info("Test skipped: %s" % msg)
362
            self.skipTest(msg)
363

    
364
    def _skip_suite_if(self, condition, msg):
365
        """Skip the whole testsuite"""
366
        if condition:
367
            self.info("TestSuite skipped: %s" % msg)
368
            self.skipTest("__SkipClass__: %s" % msg)
369

    
370
    # ----------------------------------
371
    # Flavors
372
    def _get_list_of_flavors(self, detail=False):
373
        """Get (detailed) list of flavors"""
374
        if detail:
375
            self.info("Getting detailed list of flavors")
376
        else:
377
            self.info("Getting simple list of flavors")
378
        flavors = self.clients.compute.list_flavors(detail=detail)
379
        return flavors
380

    
381
    def _find_flavors(self, patterns, flavors=None):
382
        """Find a list of suitable flavors to use
383

384
        The patterns is a list of `typed_options'. A list of all flavors
385
        matching this patterns will be returned.
386

387
        """
388
        if flavors is None:
389
            flavors = self._get_list_of_flavors(detail=True)
390

    
391
        ret_flavors = []
392
        for ptrn in patterns:
393
            try:
394
                flv_type, flv_value = parse_typed_option(ptrn)
395
            except ValueError:
396
                msg = "Invalid flavor format: %s. Must be [id|name]:.+"
397
                self.warning(msg, ptrn)
398
                continue
399

    
400
            if flv_type == "name":
401
                # Filter flavor by name
402
                msg = "Trying to find a flavor with name %s"
403
                self.info(msg, flv_value)
404
                filtered_flvs = \
405
                    [f for f in flavors if
406
                     re.search(flv_value, f['name'], flags=re.I) is not None]
407
            elif flv_type == "id":
408
                # Filter flavors by id
409
                msg = "Trying to find a flavor with id %s"
410
                self.info(msg, flv_value)
411
                filtered_flvs = \
412
                    [f for f in flavors if str(f['id']) == flv_value]
413
            else:
414
                self.error("Unrecognized flavor type %s", flv_type)
415
                self.fail("Unrecognized flavor type")
416

    
417
            # Append and continue
418
            ret_flavors.extend(filtered_flvs)
419

    
420
        self.assertGreater(len(ret_flavors), 0,
421
                           "No matching flavors found")
422
        return ret_flavors
423

    
424
    # ----------------------------------
425
    # Images
426
    def _get_list_of_images(self, detail=False):
427
        """Get (detailed) list of images"""
428
        if detail:
429
            self.info("Getting detailed list of images")
430
        else:
431
            self.info("Getting simple list of images")
432
        images = self.clients.image.list_public(detail=detail)
433
        # Remove images registered by burnin
434
        images = [img for img in images
435
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
436
        return images
437

    
438
    def _get_list_of_sys_images(self, images=None):
439
        """Get (detailed) list of images registered by system user or by me"""
440
        self.info("Getting list of images registered by system user or by me")
441
        if images is None:
442
            images = self._get_list_of_images(detail=True)
443

    
444
        su_uuid = self._get_uuid_of_system_user()
445
        my_uuid = self._get_uuid()
446
        ret_images = [i for i in images
447
                      if i['owner'] == su_uuid or i['owner'] == my_uuid]
448

    
449
        return ret_images
450

    
451
    def _find_images(self, patterns, images=None):
452
        """Find a list of suitable images to use
453

454
        The patterns is a list of `typed_options'. A list of all images
455
        matching this patterns will be returned.
456

457
        """
458
        if images is None:
459
            images = self._get_list_of_sys_images()
460

    
461
        ret_images = []
462
        for ptrn in patterns:
463
            try:
464
                img_type, img_value = parse_typed_option(ptrn)
465
            except ValueError:
466
                msg = "Invalid image format: %s. Must be [id|name]:.+"
467
                self.warning(msg, ptrn)
468
                continue
469

    
470
            if img_type == "name":
471
                # Filter image by name
472
                msg = "Trying to find an image with name %s"
473
                self.info(msg, img_value)
474
                filtered_imgs = \
475
                    [i for i in images if
476
                     re.search(img_value, i['name'], flags=re.I) is not None]
477
            elif img_type == "id":
478
                # Filter images by id
479
                msg = "Trying to find an image with id %s"
480
                self.info(msg, img_value)
481
                filtered_imgs = \
482
                    [i for i in images if
483
                     i['id'].lower() == img_value.lower()]
484
            else:
485
                self.error("Unrecognized image type %s", img_type)
486
                self.fail("Unrecognized image type")
487

    
488
            # Append and continue
489
            ret_images.extend(filtered_imgs)
490

    
491
        self.assertGreater(len(ret_images), 0,
492
                           "No matching images found")
493
        return ret_images
494

    
495
    # ----------------------------------
496
    # Pithos
497
    def _set_pithos_account(self, account):
498
        """Set the Pithos account"""
499
        assert account, "No pithos account was given"
500

    
501
        self.info("Setting Pithos account to %s", account)
502
        self.clients.pithos.account = account
503

    
504
    def _set_pithos_container(self, container):
505
        """Set the Pithos container"""
506
        assert container, "No pithos container was given"
507

    
508
        self.info("Setting Pithos container to %s", container)
509
        self.clients.pithos.container = container
510

    
511
    def _get_list_of_containers(self, account=None):
512
        """Get list of containers"""
513
        if account is not None:
514
            self._set_pithos_account(account)
515
        self.info("Getting list of containers")
516
        return self.clients.pithos.list_containers()
517

    
518
    def _create_pithos_container(self, container):
519
        """Create a pithos container
520

521
        If the container exists, nothing will happen
522

523
        """
524
        assert container, "No pithos container was given"
525

    
526
        self.info("Creating pithos container %s", container)
527
        self.clients.pithos.container = container
528
        self.clients.pithos.container_put()
529

    
530
    # ----------------------------------
531
    # Quotas
532
    def _get_quotas(self):
533
        """Get quotas"""
534
        self.info("Getting quotas")
535
        return dict(self.clients.astakos.get_quotas())
536

    
537
    # pylint: disable=invalid-name
538
    # pylint: disable=too-many-arguments
539
    def _check_quotas(self, puuid=None, disk=None, vm=None, diskspace=None,
540
                      ram=None, ip=None, cpu=None, network=None):
541
        """Check that quotas' changes are consistent
542

543
        @param puuid: The uuid of the project, quotas are assigned to
544

545
        """
546

    
547
        assert any(v is None for v in
548
                   [disk, vm, diskspace, ram, ip, cpu, network]), \
549
            "_check_quotas require arguments"
550

    
551
        self.info("Check that quotas' changes are consistent")
552
        old_quotas = self.quotas
553
        new_quotas = self._get_quotas()
554
        self.quotas = new_quotas
555

    
556
        user_uuid = self._get_uuid()
557
        if puuid is None:
558
            puuid = user_uuid
559

    
560
        self.assertListEqual(sorted(old_quotas.keys()),
561
                             sorted(new_quotas.keys()))
562
        for project in old_quotas.keys():
563
            # Check Disk usage
564
            project_name = self._get_project_name(project, user_uuid)
565
            self._check_quotas_aux(old_quotas[project], new_quotas[project],
566
                                   project_name, "cyclades.disk",
567
                                   disk, project == puuid)
568
            # Check VM usage
569
            self._check_quotas_aux(old_quotas[project], new_quotas[project],
570
                                   project_name, "cyclades.vm",
571
                                   vm, project == puuid)
572
            # Check DiskSpace usage
573
            self._check_quotas_aux(old_quotas[project], new_quotas[project],
574
                                   project_name, "pithos.diskspace",
575
                                   diskspace, project == puuid)
576
            # Check Ram usage
577
            self._check_quotas_aux(old_quotas[project], new_quotas[project],
578
                                   project_name, "cyclades.ram",
579
                                   ram, project == puuid)
580
            # Check Floating IPs usage
581
            self._check_quotas_aux(old_quotas[project], new_quotas[project],
582
                                   project_name, "cyclades.floating_ip",
583
                                   ip, project == puuid)
584
            # Check CPU usage
585
            self._check_quotas_aux(old_quotas[project], new_quotas[project],
586
                                   project_name, "cyclades.cpu",
587
                                   cpu, project == puuid)
588
            # Check Network usage
589
            self._check_quotas_aux(old_quotas[project], new_quotas[project],
590
                                   project_name, "cyclades.network.private",
591
                                   network, project == puuid)
592

    
593
    def _check_quotas_aux(self, old_quotas, new_quotas,
594
                          project_name, resource, value, check):
595
        """Auxiliary function for _check_quotas"""
596
        old_value = old_quotas[resource]['usage']
597
        new_value = new_quotas[resource]['usage']
598
        if check and value is not None:
599
            assert isinstance(value, int), \
600
                "%s value has to be integer" % resource
601
            old_value += value
602
        self.assertEqual(old_value, new_value,
603
                         "Project %s: %s quotas don't match" %
604
                         (project_name, resource))
605

    
606
    # ----------------------------------
607
    # Projects
608
    def _get_project_name(self, puuid, uuid=None):
609
        """Get the name of a project"""
610
        if uuid is None:
611
            uuid = self._get_uuid()
612
        if puuid == uuid:
613
            return "base"
614
        else:
615
            project_info = self.clients.astakos.get_project(puuid)
616
            return project_info['name']
617

    
618

    
619
# --------------------------------------------------------------------
620
# Initialize Burnin
621
def initialize(opts, testsuites, stale_testsuites):
622
    """Initalize burnin
623

624
    Initialize our logger and burnin state
625

626
    """
627
    # Initialize logger
628
    global logger  # pylint: disable=invalid-name, global-statement
629
    curr_time = datetime.datetime.now()
630
    logger = Log(opts.log_folder, verbose=opts.verbose,
631
                 use_colors=opts.use_colors, in_parallel=False,
632
                 log_level=opts.log_level, curr_time=curr_time)
633

    
634
    # Initialize clients
635
    Clients.auth_url = opts.auth_url
636
    Clients.token = opts.token
637

    
638
    # Pass the rest options to BurninTests
639
    BurninTests.use_ipv6 = opts.use_ipv6
640
    BurninTests.action_timeout = opts.action_timeout
641
    BurninTests.action_warning = opts.action_warning
642
    BurninTests.query_interval = opts.query_interval
643
    BurninTests.system_user = opts.system_user
644
    BurninTests.flavors = opts.flavors
645
    BurninTests.images = opts.images
646
    BurninTests.delete_stale = opts.delete_stale
647
    BurninTests.temp_directory = opts.temp_directory
648
    BurninTests.failfast = opts.failfast
649
    BurninTests.run_id = SNF_TEST_PREFIX + \
650
        datetime.datetime.strftime(curr_time, "%Y%m%d%H%M%S")
651

    
652
    # Choose tests to run
653
    if opts.show_stale:
654
        # We will run the stale_testsuites
655
        return (stale_testsuites, True)
656

    
657
    if opts.tests != "all":
658
        testsuites = opts.tests
659
    if opts.exclude_tests is not None:
660
        testsuites = [tsuite for tsuite in testsuites
661
                      if tsuite not in opts.exclude_tests]
662

    
663
    return (testsuites, opts.failfast)
664

    
665

    
666
# --------------------------------------------------------------------
667
# Run Burnin
668
def run_burnin(testsuites, failfast=False):
669
    """Run burnin testsuites"""
670
    # pylint: disable=invalid-name,global-statement
671
    # pylint: disable=global-variable-not-assigned
672
    global logger, success
673

    
674
    success = True
675
    run_tests(testsuites, failfast=failfast)
676

    
677
    # Clean up our logger
678
    del logger
679

    
680
    # Return
681
    return 0 if success else 1
682

    
683

    
684
def run_tests(tcases, failfast=False):
685
    """Run some testcases"""
686
    # pylint: disable=invalid-name,global-statement
687
    # pylint: disable=global-variable-not-assigned
688
    global success
689

    
690
    for tcase in tcases:
691
        was_success = run_test(tcase)
692
        success = success and was_success
693
        if failfast and not success:
694
            break
695

    
696

    
697
def run_test(tcase):
698
    """Run a testcase"""
699
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
700
    results = tsuite.run(BurninTestResult())
701

    
702
    return was_successful(tcase.__name__, results.wasSuccessful())
703

    
704

    
705
# --------------------------------------------------------------------
706
# Helper functions
707
def was_successful(tsuite, successful):
708
    """Handle whether a testsuite was succesful or not"""
709
    if successful:
710
        logger.testsuite_success(tsuite)
711
        return True
712
    else:
713
        logger.testsuite_failure(tsuite)
714
        return False
715

    
716

    
717
def parse_typed_option(value):
718
    """Parse typed options (flavors and images)
719

720
    The options are in the form 'id:123-345' or 'name:^Debian Base$'
721

722
    """
723
    try:
724
        [type_, val] = value.strip().split(':')
725
        if type_ not in ["id", "name"]:
726
            raise ValueError
727
        return type_, val
728
    except ValueError:
729
        raise