Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (24.8 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
from tempfile import NamedTemporaryFile
46
from os import urandom
47
from sys import stderr
48
from string import ascii_letters
49

    
50
from kamaki.clients.cyclades import CycladesClient, CycladesNetworkClient
51
from kamaki.clients.astakos import AstakosClient, parse_endpoints
52
from kamaki.clients.compute import ComputeClient
53
from kamaki.clients.pithos import PithosClient
54
from kamaki.clients.image import ImageClient
55

    
56
from synnefo_tools.burnin.logger import Log
57

    
58

    
59
# --------------------------------------------------------------------
60
# Global variables
61
logger = None   # Invalid constant name. pylint: disable-msg=C0103
62
success = None  # Invalid constant name. pylint: disable-msg=C0103
63
SNF_TEST_PREFIX = "snf-test-"
64
CONNECTION_RETRY_LIMIT = 2
65
SYSTEM_USERS = ["images@okeanos.grnet.gr", "images@demo.synnefo.org"]
66
KB = 2**10
67
MB = 2**20
68
GB = 2**30
69

    
70

    
71
# --------------------------------------------------------------------
72
# BurninTestResult class
73
class BurninTestResult(unittest.TestResult):
74
    """Modify the TextTestResult class"""
75
    def __init__(self):
76
        super(BurninTestResult, self).__init__()
77

    
78
        # Test parameters
79
        self.failfast = True
80

    
81
    def startTest(self, test):  # noqa
82
        """Called when the test case test is about to be run"""
83
        super(BurninTestResult, self).startTest(test)
84
        logger.log(
85
            test.__class__.__name__,
86
            test.shortDescription() or 'Test %s' % test.__class__.__name__)
87

    
88
    # Method could be a function. pylint: disable-msg=R0201
89
    def _test_failed(self, test, err):
90
        """Test failed"""
91
        # Get class name
92
        if test.__class__.__name__ == "_ErrorHolder":
93
            class_name = test.id().split('.')[-1].rstrip(')')
94
        else:
95
            class_name = test.__class__.__name__
96
        err_msg = str(test) + "... failed (%s)."
97
        timestamp = datetime.datetime.strftime(
98
            datetime.datetime.now(), "%a %b %d %Y %H:%M:%S")
99
        logger.error(class_name, err_msg, timestamp)
100
        (err_type, err_value, err_trace) = err
101
        trcback = traceback.format_exception(err_type, err_value, err_trace)
102
        logger.info(class_name, trcback)
103

    
104
    def addError(self, test, err):  # noqa
105
        """Called when the test case test raises an unexpected exception"""
106
        super(BurninTestResult, self).addError(test, err)
107
        self._test_failed(test, err)
108

    
109
    def addFailure(self, test, err):  # noqa
110
        """Called when the test case test signals a failure"""
111
        super(BurninTestResult, self).addFailure(test, err)
112
        self._test_failed(test, err)
113

    
114

    
115
# --------------------------------------------------------------------
116
# Helper Classes
117
# Too few public methods. pylint: disable-msg=R0903
118
# Too many instance attributes. pylint: disable-msg=R0902
119
class Clients(object):
120
    """Our kamaki clients"""
121
    auth_url = None
122
    token = None
123
    # Astakos
124
    astakos = None
125
    retry = CONNECTION_RETRY_LIMIT
126
    # Compute
127
    compute = None
128
    compute_url = None
129
    # Cyclades
130
    cyclades = None
131
    # Network
132
    network = None
133
    network_url = None
134
    # Pithos
135
    pithos = None
136
    pithos_url = None
137
    # Image
138
    image = None
139
    image_url = None
140

    
141
    def initialize_clients(self):
142
        """Initialize all the Kamaki Clients"""
143
        self.astakos = AstakosClient(self.auth_url, self.token)
144
        self.astakos.CONNECTION_RETRY_LIMIT = self.retry
145

    
146
        endpoints = self.astakos.authenticate()
147

    
148
        self.compute_url = _get_endpoint_url(endpoints, "compute")
149
        self.compute = ComputeClient(self.compute_url, self.token)
150
        self.compute.CONNECTION_RETRY_LIMIT = self.retry
151

    
152
        self.cyclades = CycladesClient(self.compute_url, self.token)
153
        self.cyclades.CONNECTION_RETRY_LIMIT = self.retry
154

    
155
        self.network_url = _get_endpoint_url(endpoints, "network")
156
        self.network = CycladesNetworkClient(self.network_url, self.token)
157
        self.network.CONNECTION_RETRY_LIMIT = self.retry
158

    
159
        self.pithos_url = _get_endpoint_url(endpoints, "object-store")
160
        self.pithos = PithosClient(self.pithos_url, self.token)
161
        self.pithos.CONNECTION_RETRY_LIMIT = self.retry
162

    
163
        self.image_url = _get_endpoint_url(endpoints, "image")
164
        self.image = ImageClient(self.image_url, self.token)
165
        self.image.CONNECTION_RETRY_LIMIT = self.retry
166

    
167

    
168
def _get_endpoint_url(endpoints, endpoint_type):
169
    """Get the publicURL for the specified endpoint"""
170

    
171
    service_catalog = parse_endpoints(endpoints, ep_type=endpoint_type)
172
    return service_catalog[0]['endpoints'][0]['publicURL']
173

    
174

    
175
class Proper(object):
176
    """A descriptor used by tests implementing the TestCase class
177

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

182
    """
183
    def __init__(self, value=None):
184
        self.val = value
185

    
186
    def __get__(self, obj, objtype=None):
187
        return self.val
188

    
189
    def __set__(self, obj, value):
190
        self.val = value
191

    
192

    
193
# --------------------------------------------------------------------
194
# BurninTests class
195
# Too many public methods (45/20). pylint: disable-msg=R0904
196
class BurninTests(unittest.TestCase):
197
    """Common class that all burnin tests should implement"""
198
    clients = Clients()
199
    run_id = None
200
    use_ipv6 = None
201
    action_timeout = None
202
    action_warning = None
203
    query_interval = None
204
    system_user = None
205
    images = None
206
    flavors = None
207
    delete_stale = False
208
    temp_directory = None
209
    failfast = None
210
    temp_containers = []
211

    
212

    
213
    quotas = Proper(value=None)
214

    
215
    @classmethod
216
    def setUpClass(cls):  # noqa
217
        """Initialize BurninTests"""
218
        cls.suite_name = cls.__name__
219
        logger.testsuite_start(cls.suite_name)
220

    
221
        # Set test parameters
222
        cls.longMessage = True
223

    
224
    def test_000_clients_setup(self):
225
        """Initializing astakos/cyclades/pithos clients"""
226
        # Update class attributes
227
        self.clients.initialize_clients()
228
        self.info("Astakos auth url is %s", self.clients.auth_url)
229
        self.info("Cyclades url is %s", self.clients.compute_url)
230
        self.info("Network url is %s", self.clients.network_url)
231
        self.info("Pithos url is %s", self.clients.pithos_url)
232
        self.info("Image url is %s", self.clients.image_url)
233

    
234
        self.quotas = self._get_quotas()
235
        self.info("  Disk usage is %s bytes",
236
                  self.quotas['system']['cyclades.disk']['usage'])
237
        self.info("  VM usage is %s",
238
                  self.quotas['system']['cyclades.vm']['usage'])
239
        self.info("  DiskSpace usage is %s bytes",
240
                  self.quotas['system']['pithos.diskspace']['usage'])
241
        self.info("  Ram usage is %s bytes",
242
                  self.quotas['system']['cyclades.ram']['usage'])
243
        self.info("  Floating IPs usage is %s",
244
                  self.quotas['system']['cyclades.floating_ip']['usage'])
245
        self.info("  CPU usage is %s",
246
                  self.quotas['system']['cyclades.cpu']['usage'])
247
        self.info("  Network usage is %s",
248
                  self.quotas['system']['cyclades.network.private']['usage'])
249

    
250
    def _run_tests(self, tcases):
251
        """Run some generated testcases"""
252
        global success  # Using global. pylint: disable-msg=C0103,W0603,W0602
253

    
254
        for tcase in tcases:
255
            self.info("Running testsuite %s", tcase.__name__)
256
            success = run_test(tcase) and success
257
            if self.failfast and not success:
258
                break
259

    
260
    # ----------------------------------
261
    # Loggers helper functions
262
    def log(self, msg, *args):
263
        """Pass the section value to logger"""
264
        logger.log(self.suite_name, msg, *args)
265

    
266
    def info(self, msg, *args):
267
        """Pass the section value to logger"""
268
        logger.info(self.suite_name, msg, *args)
269

    
270
    def debug(self, msg, *args):
271
        """Pass the section value to logger"""
272
        logger.debug(self.suite_name, msg, *args)
273

    
274
    def warning(self, msg, *args):
275
        """Pass the section value to logger"""
276
        logger.warning(self.suite_name, msg, *args)
277

    
278
    def error(self, msg, *args):
279
        """Pass the section value to logger"""
280
        logger.error(self.suite_name, msg, *args)
281

    
282
    # ----------------------------------
283
    # Helper functions that every testsuite may need
284
    def _get_uuid(self):
285
        """Get our uuid"""
286
        authenticate = self.clients.astakos.authenticate()
287
        uuid = authenticate['access']['user']['id']
288
        self.info("User's uuid is %s", uuid)
289
        return uuid
290

    
291
    def _get_username(self):
292
        """Get our User Name"""
293
        authenticate = self.clients.astakos.authenticate()
294
        username = authenticate['access']['user']['name']
295
        self.info("User's name is %s", username)
296
        return username
297

    
298
    def _create_tmp_directory(self):
299
        """Create a tmp directory"""
300
        temp_dir = tempfile.mkdtemp(dir=self.temp_directory)
301
        self.info("Temp directory %s created", temp_dir)
302
        return temp_dir
303

    
304
    def _remove_tmp_directory(self, tmp_dir):
305
        """Remove a tmp directory"""
306
        try:
307
            shutil.rmtree(tmp_dir)
308
            self.info("Temp directory %s deleted", tmp_dir)
309
        except OSError:
310
            pass
311

    
312
    def _create_large_file(self, size):
313
        """Create a large file at fs"""
314
        f = NamedTemporaryFile()
315
        Ki = size / 8
316
        c = ['|', '/', '-', '\\']
317
        stderr.write('\tCreate file %s  ' % f.name)
318
        for i, bytes in enumerate([b * Ki for b in range(size / Ki)]):
319
            f.seek(bytes)
320
            f.write(urandom(Ki))
321
            f.flush()
322
            stderr.write('\b' + c[i % 4])
323
            stderr.flush()
324
        stderr.write('\n')
325
        stderr.flush()
326
        f.seek(0)
327
        return f
328

    
329
    def _create_boring_file(self, num_of_blocks):
330
        """Create a file with some blocks being the same"""
331

    
332
        def chargen():
333
            """10 + 2 * 26 + 26 = 88"""
334
            while True:
335
                for CH in xrange(10):
336
                    yield '%s' % CH
337
                for CH in ascii_letters:
338
                    yield CH
339
                for CH in '~!@#$%^&*()_+`-=:";|<>?,./':
340
                    yield CH
341

    
342
        c = ['|', '/', '-', '\\']
343
        tmpFile = NamedTemporaryFile()
344
        stderr.write('\tCreate file %s  ' % tmpFile.name)
345
        block_size = 4 * 1024 * 1024
346
        chars, i = chargen(), 0
347
        while num_of_blocks:
348
            fslice = 3 if num_of_blocks > 3 else num_of_blocks
349
            tmpFile.write(fslice * block_size * chars.next())
350
            num_of_blocks -= fslice
351
            stderr.write('\b' + c[i % 4])
352
            stderr.flush()
353
            i += 1
354
        tmpFile.seek(0)
355
        stderr.write('\n')
356
        stderr.flush()
357
        return tmpFile
358

    
359
    def _get_uuid_of_system_user(self):
360
        """Get the uuid of the system user
361

362
        This is the user that upload the 'official' images.
363

364
        """
365
        self.info("Getting the uuid of the system user")
366
        system_users = None
367
        if self.system_user is not None:
368
            try:
369
                su_type, su_value = parse_typed_option(self.system_user)
370
                if su_type == "name":
371
                    system_users = [su_value]
372
                elif su_type == "id":
373
                    self.info("System user's uuid is %s", su_value)
374
                    return su_value
375
                else:
376
                    self.error("Unrecognized system-user type %s", su_type)
377
                    self.fail("Unrecognized system-user type")
378
            except ValueError:
379
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
380
                self.warning(msg, self.system_user)
381

    
382
        if system_users is None:
383
            system_users = SYSTEM_USERS
384

    
385
        uuids = self.clients.astakos.get_uuids(system_users)
386
        for su_name in system_users:
387
            self.info("Trying username %s", su_name)
388
            if su_name in uuids:
389
                self.info("System user's uuid is %s", uuids[su_name])
390
                return uuids[su_name]
391

    
392
        self.warning("No system user found")
393
        return None
394

    
395
    def _skip_if(self, condition, msg):
396
        """Skip tests"""
397
        if condition:
398
            self.info("Test skipped: %s" % msg)
399
            self.skipTest(msg)
400

    
401
    # ----------------------------------
402
    # Flavors
403
    def _get_list_of_flavors(self, detail=False):
404
        """Get (detailed) list of flavors"""
405
        if detail:
406
            self.info("Getting detailed list of flavors")
407
        else:
408
            self.info("Getting simple list of flavors")
409
        flavors = self.clients.compute.list_flavors(detail=detail)
410
        return flavors
411

    
412
    def _find_flavors(self, patterns, flavors=None):
413
        """Find a list of suitable flavors to use
414

415
        The patterns is a list of `typed_options'. A list of all flavors
416
        matching this patterns will be returned.
417

418
        """
419
        if flavors is None:
420
            flavors = self._get_list_of_flavors(detail=True)
421

    
422
        ret_flavors = []
423
        for ptrn in patterns:
424
            try:
425
                flv_type, flv_value = parse_typed_option(ptrn)
426
            except ValueError:
427
                msg = "Invalid flavor format: %s. Must be [id|name]:.+"
428
                self.warning(msg, ptrn)
429
                continue
430

    
431
            if flv_type == "name":
432
                # Filter flavor by name
433
                msg = "Trying to find a flavor with name %s"
434
                self.info(msg, flv_value)
435
                filtered_flvs = \
436
                    [f for f in flavors if
437
                     re.search(flv_value, f['name'], flags=re.I) is not None]
438
            elif flv_type == "id":
439
                # Filter flavors by id
440
                msg = "Trying to find a flavor with id %s"
441
                self.info(msg, flv_value)
442
                filtered_flvs = \
443
                    [f for f in flavors if str(f['id']) == flv_value]
444
            else:
445
                self.error("Unrecognized flavor type %s", flv_type)
446
                self.fail("Unrecognized flavor type")
447

    
448
            # Append and continue
449
            ret_flavors.extend(filtered_flvs)
450

    
451
        self.assertGreater(len(ret_flavors), 0,
452
                           "No matching flavors found")
453
        return ret_flavors
454

    
455
    # ----------------------------------
456
    # Images
457
    def _get_list_of_images(self, detail=False):
458
        """Get (detailed) list of images"""
459
        if detail:
460
            self.info("Getting detailed list of images")
461
        else:
462
            self.info("Getting simple list of images")
463
        images = self.clients.image.list_public(detail=detail)
464
        # Remove images registered by burnin
465
        images = [img for img in images
466
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
467
        return images
468

    
469
    def _get_list_of_sys_images(self, images=None):
470
        """Get (detailed) list of images registered by system user or by me"""
471
        self.info("Getting list of images registered by system user or by me")
472
        if images is None:
473
            images = self._get_list_of_images(detail=True)
474

    
475
        su_uuid = self._get_uuid_of_system_user()
476
        my_uuid = self._get_uuid()
477
        ret_images = [i for i in images
478
                      if i['owner'] == su_uuid or i['owner'] == my_uuid]
479

    
480
        return ret_images
481

    
482
    def _find_images(self, patterns, images=None):
483
        """Find a list of suitable images to use
484

485
        The patterns is a list of `typed_options'. A list of all images
486
        matching this patterns will be returned.
487

488
        """
489
        if images is None:
490
            images = self._get_list_of_sys_images()
491

    
492
        ret_images = []
493
        for ptrn in patterns:
494
            try:
495
                img_type, img_value = parse_typed_option(ptrn)
496
            except ValueError:
497
                msg = "Invalid image format: %s. Must be [id|name]:.+"
498
                self.warning(msg, ptrn)
499
                continue
500

    
501
            if img_type == "name":
502
                # Filter image by name
503
                msg = "Trying to find an image with name %s"
504
                self.info(msg, img_value)
505
                filtered_imgs = \
506
                    [i for i in images if
507
                     re.search(img_value, i['name'], flags=re.I) is not None]
508
            elif img_type == "id":
509
                # Filter images by id
510
                msg = "Trying to find an image with id %s"
511
                self.info(msg, img_value)
512
                filtered_imgs = \
513
                    [i for i in images if
514
                     i['id'].lower() == img_value.lower()]
515
            else:
516
                self.error("Unrecognized image type %s", img_type)
517
                self.fail("Unrecognized image type")
518

    
519
            # Append and continue
520
            ret_images.extend(filtered_imgs)
521

    
522
        self.assertGreater(len(ret_images), 0,
523
                           "No matching images found")
524
        return ret_images
525

    
526
    # ----------------------------------
527
    # Pithos
528
    def _set_pithos_account(self, account):
529
        """Set the Pithos account"""
530
        assert account, "No pithos account was given"
531

    
532
        self.info("Setting Pithos account to %s", account)
533
        self.clients.pithos.account = account
534

    
535
    def _set_pithos_container(self, container):
536
        """Set the Pithos container"""
537
        assert container, "No pithos container was given"
538

    
539
        self.info("Setting Pithos container to %s", container)
540
        self.clients.pithos.container = container
541

    
542
    def _get_list_of_containers(self, account=None):
543
        """Get list of containers"""
544
        if account is not None:
545
            self._set_pithos_account(account)
546
        self.info("Getting list of containers")
547
        return self.clients.pithos.list_containers()
548

    
549
    def _create_pithos_container(self, container):
550
        """Create a pithos container
551

552
        If the container exists, nothing will happen
553

554
        """
555
        assert container, "No pithos container was given"
556

    
557
        self.info("Creating pithos container %s", container)
558
        self.clients.pithos.create_container(container)
559
        self.temp_containers.append(container)
560

    
561
    # ----------------------------------
562
    # Quotas
563
    def _get_quotas(self):
564
        """Get quotas"""
565
        self.info("Getting quotas")
566
        return self.clients.astakos.get_quotas()
567

    
568
    # Invalid argument name. pylint: disable-msg=C0103
569
    # Too many arguments. pylint: disable-msg=R0913
570
    def _check_quotas(self, disk=None, vm=None, diskspace=None,
571
                      ram=None, ip=None, cpu=None, network=None):
572
        """Check that quotas' changes are consistent"""
573
        assert any(v is None for v in
574
                   [disk, vm, diskspace, ram, ip, cpu, network]), \
575
            "_check_quotas require arguments"
576

    
577
        self.info("Check that quotas' changes are consistent")
578
        old_quotas = self.quotas
579
        new_quotas = self._get_quotas()
580
        self.quotas = new_quotas
581

    
582
        # Check Disk usage
583
        self._check_quotas_aux(
584
            old_quotas, new_quotas, 'cyclades.disk', disk)
585
        # Check VM usage
586
        self._check_quotas_aux(
587
            old_quotas, new_quotas, 'cyclades.vm', vm)
588
        # Check DiskSpace usage
589
        self._check_quotas_aux(
590
            old_quotas, new_quotas, 'pithos.diskspace', diskspace)
591
        # Check Ram usage
592
        self._check_quotas_aux(
593
            old_quotas, new_quotas, 'cyclades.ram', ram)
594
        # Check Floating IPs usage
595
        self._check_quotas_aux(
596
            old_quotas, new_quotas, 'cyclades.floating_ip', ip)
597
        # Check CPU usage
598
        self._check_quotas_aux(
599
            old_quotas, new_quotas, 'cyclades.cpu', cpu)
600
        # Check Network usage
601
        self._check_quotas_aux(
602
            old_quotas, new_quotas, 'cyclades.network.private', network)
603

    
604
    def _check_quotas_aux(self, old_quotas, new_quotas, resource, value):
605
        """Auxiliary function for _check_quotas"""
606
        old_value = old_quotas['system'][resource]['usage']
607
        new_value = new_quotas['system'][resource]['usage']
608
        if value is not None:
609
            assert isinstance(value, int), \
610
                "%s value has to be integer" % resource
611
            old_value += value
612
        self.assertEqual(old_value, new_value,
613
                         "%s quotas don't match" % resource)
614

    
615

    
616
# --------------------------------------------------------------------
617
# Initialize Burnin
618
def initialize(opts, testsuites, stale_testsuites):
619
    """Initalize burnin
620

621
    Initialize our logger and burnin state
622

623
    """
624
    # Initialize logger
625
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
626
    curr_time = datetime.datetime.now()
627
    logger = Log(opts.log_folder, verbose=opts.verbose,
628
                 use_colors=opts.use_colors, in_parallel=False,
629
                 log_level=opts.log_level, curr_time=curr_time)
630

    
631
    # Initialize clients
632
    Clients.auth_url = opts.auth_url
633
    Clients.token = opts.token
634

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

    
649
    # Choose tests to run
650
    if opts.show_stale:
651
        # We will run the stale_testsuites
652
        return (stale_testsuites, True)
653

    
654
    if opts.tests != "all":
655
        testsuites = opts.tests
656
    if opts.exclude_tests is not None:
657
        testsuites = [tsuite for tsuite in testsuites
658
                      if tsuite not in opts.exclude_tests]
659

    
660
    return (testsuites, opts.failfast)
661

    
662

    
663
# --------------------------------------------------------------------
664
# Run Burnin
665
def run_burnin(testsuites, failfast=False):
666
    """Run burnin testsuites"""
667
    # Using global. pylint: disable-msg=C0103,W0603,W0602
668
    global logger, success
669

    
670
    success = True
671
    run_tests(testsuites, failfast=failfast)
672

    
673
    # Clean up our logger
674
    del logger
675

    
676
    # Return
677
    return 0 if success else 1
678

    
679

    
680
def run_tests(tcases, failfast=False):
681
    """Run some testcases"""
682
    global success  # Using global. pylint: disable-msg=C0103,W0603,W0602
683

    
684
    for tcase in tcases:
685
        was_success = run_test(tcase)
686
        success = success and was_success
687
        if failfast and not success:
688
            break
689

    
690

    
691
def run_test(tcase):
692
    """Run a testcase"""
693
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
694
    results = tsuite.run(BurninTestResult())
695

    
696
    return was_successful(tcase.__name__, results.wasSuccessful())
697

    
698

    
699
# --------------------------------------------------------------------
700
# Helper functions
701
def was_successful(tsuite, successful):
702
    """Handle whether a testsuite was succesful or not"""
703
    if successful:
704
        logger.testsuite_success(tsuite)
705
        return True
706
    else:
707
        logger.testsuite_failure(tsuite)
708
        return False
709

    
710

    
711
def parse_typed_option(value):
712
    """Parse typed options (flavors and images)
713

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

716
    """
717
    try:
718
        [type_, val] = value.strip().split(':')
719
        if type_ not in ["id", "name"]:
720
            raise ValueError
721
        return type_, val
722
    except ValueError:
723
        raise