Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (23.7 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

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

    
55
from synnefo_tools.burnin.logger import Log
56

    
57

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

    
69

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

    
77
        # Test parameters
78
        self.failfast = True
79

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

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

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

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

    
113

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

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

    
145
        endpoints = self.astakos.authenticate()
146

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

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

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

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

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

    
166

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

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

    
173

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

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

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

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

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

    
191

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

    
211

    
212
    quotas = Proper(value=None)
213

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
328
    def _get_uuid_of_system_user(self):
329
        """Get the uuid of the system user
330

331
        This is the user that upload the 'official' images.
332

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

    
351
        if system_users is None:
352
            system_users = SYSTEM_USERS
353

    
354
        uuids = self.clients.astakos.get_uuids(system_users)
355
        for su_name in system_users:
356
            self.info("Trying username %s", su_name)
357
            if su_name in uuids:
358
                self.info("System user's uuid is %s", uuids[su_name])
359
                return uuids[su_name]
360

    
361
        self.warning("No system user found")
362
        return None
363

    
364
    def _skip_if(self, condition, msg):
365
        """Skip tests"""
366
        if condition:
367
            self.info("Test skipped: %s" % msg)
368
            self.skipTest(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.create_container(container)
528
        self.temp_containers.append(container)
529

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

    
537
    # Invalid argument name. pylint: disable-msg=C0103
538
    # Too many arguments. pylint: disable-msg=R0913
539
    def _check_quotas(self, disk=None, vm=None, diskspace=None,
540
                      ram=None, ip=None, cpu=None, network=None):
541
        """Check that quotas' changes are consistent"""
542
        assert any(v is None for v in
543
                   [disk, vm, diskspace, ram, ip, cpu, network]), \
544
            "_check_quotas require arguments"
545

    
546
        self.info("Check that quotas' changes are consistent")
547
        old_quotas = self.quotas
548
        new_quotas = self._get_quotas()
549
        self.quotas = new_quotas
550

    
551
        # Check Disk usage
552
        self._check_quotas_aux(
553
            old_quotas, new_quotas, 'cyclades.disk', disk)
554
        # Check VM usage
555
        self._check_quotas_aux(
556
            old_quotas, new_quotas, 'cyclades.vm', vm)
557
        # Check DiskSpace usage
558
        self._check_quotas_aux(
559
            old_quotas, new_quotas, 'pithos.diskspace', diskspace)
560
        # Check Ram usage
561
        self._check_quotas_aux(
562
            old_quotas, new_quotas, 'cyclades.ram', ram)
563
        # Check Floating IPs usage
564
        self._check_quotas_aux(
565
            old_quotas, new_quotas, 'cyclades.floating_ip', ip)
566
        # Check CPU usage
567
        self._check_quotas_aux(
568
            old_quotas, new_quotas, 'cyclades.cpu', cpu)
569
        # Check Network usage
570
        self._check_quotas_aux(
571
            old_quotas, new_quotas, 'cyclades.network.private', network)
572

    
573
    def _check_quotas_aux(self, old_quotas, new_quotas, resource, value):
574
        """Auxiliary function for _check_quotas"""
575
        old_value = old_quotas['system'][resource]['usage']
576
        new_value = new_quotas['system'][resource]['usage']
577
        if value is not None:
578
            assert isinstance(value, int), \
579
                "%s value has to be integer" % resource
580
            old_value += value
581
        self.assertEqual(old_value, new_value,
582
                         "%s quotas don't match" % resource)
583

    
584

    
585
# --------------------------------------------------------------------
586
# Initialize Burnin
587
def initialize(opts, testsuites, stale_testsuites):
588
    """Initalize burnin
589

590
    Initialize our logger and burnin state
591

592
    """
593
    # Initialize logger
594
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
595
    curr_time = datetime.datetime.now()
596
    logger = Log(opts.log_folder, verbose=opts.verbose,
597
                 use_colors=opts.use_colors, in_parallel=False,
598
                 log_level=opts.log_level, curr_time=curr_time)
599

    
600
    # Initialize clients
601
    Clients.auth_url = opts.auth_url
602
    Clients.token = opts.token
603

    
604
    # Pass the rest options to BurninTests
605
    BurninTests.use_ipv6 = opts.use_ipv6
606
    BurninTests.action_timeout = opts.action_timeout
607
    BurninTests.action_warning = opts.action_warning
608
    BurninTests.query_interval = opts.query_interval
609
    BurninTests.system_user = opts.system_user
610
    BurninTests.flavors = opts.flavors
611
    BurninTests.images = opts.images
612
    BurninTests.delete_stale = opts.delete_stale
613
    BurninTests.temp_directory = opts.temp_directory
614
    BurninTests.failfast = opts.failfast
615
    BurninTests.run_id = SNF_TEST_PREFIX + \
616
        datetime.datetime.strftime(curr_time, "%Y%m%d%H%M%S")
617

    
618
    # Choose tests to run
619
    if opts.show_stale:
620
        # We will run the stale_testsuites
621
        return (stale_testsuites, True)
622

    
623
    if opts.tests != "all":
624
        testsuites = opts.tests
625
    if opts.exclude_tests is not None:
626
        testsuites = [tsuite for tsuite in testsuites
627
                      if tsuite not in opts.exclude_tests]
628

    
629
    return (testsuites, opts.failfast)
630

    
631

    
632
# --------------------------------------------------------------------
633
# Run Burnin
634
def run_burnin(testsuites, failfast=False):
635
    """Run burnin testsuites"""
636
    # Using global. pylint: disable-msg=C0103,W0603,W0602
637
    global logger, success
638

    
639
    success = True
640
    run_tests(testsuites, failfast=failfast)
641

    
642
    # Clean up our logger
643
    del logger
644

    
645
    # Return
646
    return 0 if success else 1
647

    
648

    
649
def run_tests(tcases, failfast=False):
650
    """Run some testcases"""
651
    global success  # Using global. pylint: disable-msg=C0103,W0603,W0602
652

    
653
    for tcase in tcases:
654
        was_success = run_test(tcase)
655
        success = success and was_success
656
        if failfast and not success:
657
            break
658

    
659

    
660
def run_test(tcase):
661
    """Run a testcase"""
662
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
663
    results = tsuite.run(BurninTestResult())
664

    
665
    return was_successful(tcase.__name__, results.wasSuccessful())
666

    
667

    
668
# --------------------------------------------------------------------
669
# Helper functions
670
def was_successful(tsuite, successful):
671
    """Handle whether a testsuite was succesful or not"""
672
    if successful:
673
        logger.testsuite_success(tsuite)
674
        return True
675
    else:
676
        logger.testsuite_failure(tsuite)
677
        return False
678

    
679

    
680
def parse_typed_option(value):
681
    """Parse typed options (flavors and images)
682

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

685
    """
686
    try:
687
        [type_, val] = value.strip().split(':')
688
        if type_ not in ["id", "name"]:
689
            raise ValueError
690
        return type_, val
691
    except ValueError:
692
        raise