Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / common.py @ 6c78720b

History | View | Annotate | Download (25.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 os
40
import re
41
import time
42
import shutil
43
import socket
44
import random
45
import unittest
46
import datetime
47
import tempfile
48
import traceback
49
import subprocess
50

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

    
57
from synnefo_tools.burnin.logger import Log
58

    
59

    
60
# --------------------------------------------------------------------
61
# Global variables
62
logger = 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

    
67

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

    
75
        # Test parameters
76
        self.failfast = True
77

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

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

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

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

    
109

    
110
# --------------------------------------------------------------------
111
# BurninTests class
112
# Too few public methods. pylint: disable-msg=R0903
113
# Too many instance attributes. pylint: disable-msg=R0902
114
class Clients(object):
115
    """Our kamaki clients"""
116
    auth_url = None
117
    token = None
118
    # Astakos
119
    astakos = None
120
    retry = CONNECTION_RETRY_LIMIT
121
    # Compute
122
    compute = None
123
    compute_url = None
124
    # Cyclades
125
    cyclades = None
126
    # Pithos
127
    pithos = None
128
    pithos_url = None
129
    # Image
130
    image = None
131
    image_url = None
132

    
133

    
134
# Too many public methods (45/20). pylint: disable-msg=R0904
135
class BurninTests(unittest.TestCase):
136
    """Common class that all burnin tests should implement"""
137
    clients = Clients()
138
    run_id = None
139
    use_ipv6 = None
140
    action_timeout = None
141
    action_warning = None
142
    query_interval = None
143
    system_user = None
144
    images = None
145
    flavors = None
146

    
147
    @classmethod
148
    def setUpClass(cls):  # noqa
149
        """Initialize BurninTests"""
150
        cls.suite_name = cls.__name__
151
        logger.testsuite_start(cls.suite_name)
152

    
153
        # Set test parameters
154
        cls.longMessage = True
155

    
156
    def test_000_clients_setup(self):
157
        """Initializing astakos/cyclades/pithos clients"""
158
        # Update class attributes
159
        self.info("Astakos auth url is %s", self.clients.auth_url)
160
        self.clients.astakos = AstakosClient(
161
            self.clients.auth_url, self.clients.token)
162
        self.clients.astakos.CONNECTION_RETRY_LIMIT = self.clients.retry
163

    
164
        self.clients.compute_url = \
165
            self.clients.astakos.get_service_endpoints('compute')['publicURL']
166
        self.info("Cyclades url is %s", self.clients.compute_url)
167
        self.clients.compute = ComputeClient(
168
            self.clients.compute_url, self.clients.token)
169
        self.clients.compute.CONNECTION_RETRY_LIMIT = self.clients.retry
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

    
175
        self.clients.pithos_url = self.clients.astakos.\
176
            get_service_endpoints('object-store')['publicURL']
177
        self.info("Pithos url is %s", self.clients.pithos_url)
178
        self.clients.pithos = PithosClient(
179
            self.clients.pithos_url, self.clients.token)
180
        self.clients.pithos.CONNECTION_RETRY_LIMIT = self.clients.retry
181

    
182
        self.clients.image_url = \
183
            self.clients.astakos.get_service_endpoints('image')['publicURL']
184
        self.info("Image url is %s", self.clients.image_url)
185
        self.clients.image = ImageClient(
186
            self.clients.image_url, self.clients.token)
187
        self.clients.image.CONNECTION_RETRY_LIMIT = self.clients.retry
188

    
189
    # ----------------------------------
190
    # Loggers helper functions
191
    def log(self, msg, *args):
192
        """Pass the section value to logger"""
193
        logger.log(self.suite_name, msg, *args)
194

    
195
    def info(self, msg, *args):
196
        """Pass the section value to logger"""
197
        logger.info(self.suite_name, msg, *args)
198

    
199
    def debug(self, msg, *args):
200
        """Pass the section value to logger"""
201
        logger.debug(self.suite_name, msg, *args)
202

    
203
    def warning(self, msg, *args):
204
        """Pass the section value to logger"""
205
        logger.warning(self.suite_name, msg, *args)
206

    
207
    def error(self, msg, *args):
208
        """Pass the section value to logger"""
209
        logger.error(self.suite_name, msg, *args)
210

    
211
    # ----------------------------------
212
    # Helper functions that every testsuite may need
213
    def _get_uuid(self):
214
        """Get our uuid"""
215
        authenticate = self.clients.astakos.authenticate()
216
        uuid = authenticate['access']['user']['id']
217
        self.info("User's uuid is %s", uuid)
218
        return uuid
219

    
220
    def _get_username(self):
221
        """Get our User Name"""
222
        authenticate = self.clients.astakos.authenticate()
223
        username = authenticate['access']['user']['name']
224
        self.info("User's name is %s", username)
225
        return username
226

    
227
    def _create_tmp_directory(self):
228
        """Create a tmp directory
229

230
        In my machine /tmp has not enough space for an image
231
        to be saves, so we are going to use the current directory.
232

233
        """
234
        temp_dir = tempfile.mkdtemp(dir=os.getcwd())
235
        self.info("Temp directory %s created", temp_dir)
236
        return temp_dir
237

    
238
    def _remove_tmp_directory(self, tmp_dir):
239
        """Remove a tmp directory"""
240
        try:
241
            shutil.rmtree(tmp_dir)
242
            self.info("Temp directory %s deleted", tmp_dir)
243
        except OSError:
244
            pass
245

    
246
    def _get_uuid_of_system_user(self):
247
        """Get the uuid of the system user
248

249
        This is the user that upload the 'official' images.
250

251
        """
252
        self.info("Getting the uuid of the system user")
253
        system_users = None
254
        if self.system_user is not None:
255
            parsed_su = parse_typed_option(self.system_user)
256
            if parsed_su is None:
257
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
258
                self.warning(msg, self.system_user)
259
            else:
260
                su_type, su_value = parsed_su
261
                if su_type == "name":
262
                    system_users = [su_value]
263
                elif su_type == "id":
264
                    self.info("System user's uuid is %s", su_value)
265
                    return su_value
266
                else:
267
                    self.error("Unrecognized system-user type %s", su_type)
268
                    self.fail("Unrecognized system-user type")
269

    
270
        if system_users is None:
271
            system_users = SYSTEM_USERS
272

    
273
        uuids = self.clients.astakos.usernames2uuids(system_users)
274
        for su_name in system_users:
275
            self.info("Trying username %s", su_name)
276
            if su_name in uuids:
277
                self.info("System user's uuid is %s", uuids[su_name])
278
                return uuids[su_name]
279

    
280
        self.warning("No system user found")
281
        return None
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

    
313
    # ----------------------------------
314
    # Flavors
315
    def _get_list_of_flavors(self, detail=False):
316
        """Get (detailed) list of flavors"""
317
        if detail:
318
            self.info("Getting detailed list of flavors")
319
        else:
320
            self.info("Getting simple list of flavors")
321
        flavors = self.clients.compute.list_flavors(detail=detail)
322
        return flavors
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

    
366
    # ----------------------------------
367
    # Images
368
    def _get_list_of_images(self, detail=False):
369
        """Get (detailed) list of images"""
370
        if detail:
371
            self.info("Getting detailed list of images")
372
        else:
373
            self.info("Getting simple list of images")
374
        images = self.clients.image.list_public(detail=detail)
375
        # Remove images registered by burnin
376
        images = [img for img in images
377
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
378
        return images
379

    
380
    def _get_list_of_sys_images(self, images=None):
381
        """Get (detailed) list of images registered by system user or by me"""
382
        self.info("Getting list of images registered by system user or by me")
383
        if images is None:
384
            images = self._get_list_of_images(detail=True)
385

    
386
        su_uuid = self._get_uuid_of_system_user()
387
        my_uuid = self._get_uuid()
388
        ret_images = [i for i in images
389
                      if i['owner'] == su_uuid or i['owner'] == my_uuid]
390

    
391
        return ret_images
392

    
393
    def _find_images(self, patterns, images=None):
394
        """Find a list of suitable images to use
395

396
        The patterns is a list of `typed_options'. A list of all images
397
        matching this patterns will be returned.
398

399
        """
400
        if images is None:
401
            images = self._get_list_of_sys_images()
402

    
403
        ret_images = []
404
        for ptrn in patterns:
405
            parsed_ptrn = parse_typed_option(ptrn)
406
            if parsed_ptrn is None:
407
                msg = "Invalid image format: %s. Must be [id|name]:.+"
408
                self.warning(msg, ptrn)
409
                continue
410
            img_type, img_value = parsed_ptrn
411
            if img_type == "name":
412
                # Filter image by name
413
                msg = "Trying to find an image with name %s"
414
                self.info(msg, img_value)
415
                filtered_imgs = \
416
                    [i for i in images if
417
                     re.search(img_value, i['name'], flags=re.I) is not None]
418
            elif img_type == "id":
419
                # Filter images by id
420
                msg = "Trying to find an image with id %s"
421
                self.info(msg, img_value)
422
                filtered_imgs = \
423
                    [i for i in images if
424
                     i['id'].lower() == img_value.lower()]
425
            else:
426
                self.error("Unrecognized image type %s", img_type)
427
                self.fail("Unrecognized image type")
428

    
429
            # Append and continue
430
            ret_images.extend(filtered_imgs)
431

    
432
        self.assertGreater(len(ret_images), 0,
433
                           "No matching images found")
434
        return ret_images
435

    
436
    # ----------------------------------
437
    # Pithos
438
    def _set_pithos_account(self, account):
439
        """Set the Pithos account"""
440
        assert account, "No pithos account was given"
441

    
442
        self.info("Setting Pithos account to %s", account)
443
        self.clients.pithos.account = account
444

    
445
    def _set_pithos_container(self, container):
446
        """Set the Pithos container"""
447
        assert container, "No pithos container was given"
448

    
449
        self.info("Setting Pithos container to %s", container)
450
        self.clients.pithos.container = container
451

    
452
    def _get_list_of_containers(self, account=None):
453
        """Get list of containers"""
454
        if account is not None:
455
            self._set_pithos_account(account)
456
        self.info("Getting list of containers")
457
        return self.clients.pithos.list_containers()
458

    
459
    def _create_pithos_container(self, container):
460
        """Create a pithos container
461

462
        If the container exists, nothing will happen
463

464
        """
465
        assert container, "No pithos container was given"
466

    
467
        self.info("Creating pithos container %s", container)
468
        self.clients.pithos.container = container
469
        self.clients.pithos.container_put()
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

    
613

    
614
# --------------------------------------------------------------------
615
# Initialize Burnin
616
def initialize(opts, testsuites):
617
    """Initalize burnin
618

619
    Initialize our logger and burnin state
620

621
    """
622
    # Initialize logger
623
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
624
    logger = Log(opts.log_folder, verbose=opts.verbose,
625
                 use_colors=opts.use_colors, in_parallel=False,
626
                 quiet=opts.quiet)
627

    
628
    # Initialize clients
629
    Clients.auth_url = opts.auth_url
630
    Clients.token = opts.token
631

    
632
    # Pass the rest options to BurninTests
633
    BurninTests.use_ipv6 = opts.use_ipv6
634
    BurninTests.action_timeout = opts.action_timeout
635
    BurninTests.action_warning = opts.action_warning
636
    BurninTests.query_interval = opts.query_interval
637
    BurninTests.system_user = opts.system_user
638
    BurninTests.flavors = opts.flavors
639
    BurninTests.images = opts.images
640
    BurninTests.run_id = SNF_TEST_PREFIX + \
641
        datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d%H%M%S")
642

    
643
    # Choose tests to run
644
    if opts.tests != "all":
645
        testsuites = opts.tests
646
    if opts.exclude_tests is not None:
647
        testsuites = [tsuite for tsuite in testsuites
648
                      if tsuite not in opts.exclude_tests]
649

    
650
    return testsuites
651

    
652

    
653
# --------------------------------------------------------------------
654
# Run Burnin
655
def run_burnin(testsuites, failfast=False, final_report=False):
656
    """Run burnin testsuites"""
657
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
658

    
659
    success = True
660
    for tcase in testsuites:
661
        was_success = run_test(tcase)
662
        success = success and was_success
663
        if failfast and not success:
664
            break
665

    
666
    # Are we going to print final report?
667
    if final_report:
668
        logger.print_logfile_to_stdout()
669
    # Clean up our logger
670
    del(logger)
671

    
672
    # Return
673
    return 0 if success else 1
674

    
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

    
684
# --------------------------------------------------------------------
685
# Helper functions
686
def was_successful(tsuite, success):
687
    """Handle whether a testsuite was succesful or not"""
688
    if success:
689
        logger.testsuite_success(tsuite)
690
        return True
691
    else:
692
        logger.testsuite_failure(tsuite)
693
        return False
694

    
695

    
696
def parse_typed_option(value):
697
    """Parse typed options (flavors and images)
698

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

701
    """
702
    try:
703
        [type_, val] = value.strip().split(':')
704
        if type_ not in ["id", "name"]:
705
            raise ValueError
706
        return type_, val
707
    except ValueError:
708
        return None
709

    
710

    
711
class Proper(object):
712
    """A descriptor used by tests implementing the TestCase class
713

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

718
    """
719
    def __init__(self, value=None):
720
        self.val = value
721

    
722
    def __get__(self, obj, objtype=None):
723
        return self.val
724

    
725
    def __set__(self, obj, value):
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
    """