Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / common.py @ 2afd10bf

History | View | Annotate | Download (19.2 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 shutil
42
import unittest
43
import datetime
44
import tempfile
45
import traceback
46

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

    
53
from synnefo_tools.burnin.logger import Log
54

    
55

    
56
# --------------------------------------------------------------------
57
# Global variables
58
logger = None  # Invalid constant name. pylint: disable-msg=C0103
59
SNF_TEST_PREFIX = "snf-test-"
60
CONNECTION_RETRY_LIMIT = 2
61
SYSTEM_USERS = ["images@okeanos.grnet.gr", "images@demo.synnefo.org"]
62

    
63

    
64
# --------------------------------------------------------------------
65
# BurninTestResult class
66
class BurninTestResult(unittest.TestResult):
67
    """Modify the TextTestResult class"""
68
    def __init__(self):
69
        super(BurninTestResult, self).__init__()
70

    
71
        # Test parameters
72
        self.failfast = True
73

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

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

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

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

    
105

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

    
129

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

    
144
    @classmethod
145
    def setUpClass(cls):  # noqa
146
        """Initialize BurninTests"""
147
        cls.suite_name = cls.__name__
148
        logger.testsuite_start(cls.suite_name)
149

    
150
        # Set test parameters
151
        cls.longMessage = True
152

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

    
161
        self.clients.compute_url = \
162
            self.clients.astakos.get_service_endpoints('compute')['publicURL']
163
        self.info("Cyclades url is %s", self.clients.compute_url)
164
        self.clients.compute = ComputeClient(
165
            self.clients.compute_url, self.clients.token)
166
        self.clients.compute.CONNECTION_RETRY_LIMIT = self.clients.retry
167

    
168
        self.clients.cyclades = CycladesClient(
169
            self.clients.compute_url, self.clients.token)
170
        self.clients.cyclades.CONNECTION_RETRY_LIMIT = self.clients.retry
171

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

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

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

    
192
    def info(self, msg, *args):
193
        """Pass the section value to logger"""
194
        logger.info(self.suite_name, msg, *args)
195

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

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

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

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

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

    
224
    def _create_tmp_directory(self):
225
        """Create a tmp directory
226

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

230
        """
231
        temp_dir = tempfile.mkdtemp(dir=os.getcwd())
232
        self.info("Temp directory %s created", temp_dir)
233
        return temp_dir
234

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

    
243
    def _get_uuid_of_system_user(self):
244
        """Get the uuid of the system user
245

246
        This is the user that upload the 'official' images.
247

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

    
267
        if system_users is None:
268
            system_users = SYSTEM_USERS
269

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

    
277
        self.warning("No system user found")
278
        return None
279

    
280
    def _skip_if(self, condition, msg):
281
        """Skip tests"""
282
        if condition:
283
            self.info("Test skipped: %s" % msg)
284
            self.skipTest(msg)
285

    
286
    # ----------------------------------
287
    # Flavors
288
    def _get_list_of_flavors(self, detail=False):
289
        """Get (detailed) list of flavors"""
290
        if detail:
291
            self.info("Getting detailed list of flavors")
292
        else:
293
            self.info("Getting simple list of flavors")
294
        flavors = self.clients.compute.list_flavors(detail=detail)
295
        return flavors
296

    
297
    def _find_flavors(self, patterns, flavors=None):
298
        """Find a list of suitable flavors to use
299

300
        The patterns is a list of `typed_options'. A list of all flavors
301
        matching this patterns will be returned.
302

303
        """
304
        if flavors is None:
305
            flavors = self._get_list_of_flavors(detail=True)
306

    
307
        ret_flavors = []
308
        for ptrn in patterns:
309
            parsed_ptrn = parse_typed_option(ptrn)
310
            if parsed_ptrn is None:
311
                msg = "Invalid flavor format: %s. Must be [id|name]:.+"
312
                self.warning(msg, ptrn)
313
                continue
314
            flv_type, flv_value = parsed_ptrn
315
            if flv_type == "name":
316
                # Filter flavor by name
317
                msg = "Trying to find a flavor with name %s"
318
                self.info(msg, flv_value)
319
                filtered_flvs = \
320
                    [f for f in flavors if
321
                     re.search(flv_value, f['name'], flags=re.I) is not None]
322
            elif flv_type == "id":
323
                # Filter flavors by id
324
                msg = "Trying to find a flavor with id %s"
325
                self.info(msg, flv_value)
326
                filtered_flvs = \
327
                    [f for f in flavors if str(f['id']) == flv_value]
328
            else:
329
                self.error("Unrecognized flavor type %s", flv_type)
330
                self.fail("Unrecognized flavor type")
331

    
332
            # Append and continue
333
            ret_flavors.extend(filtered_flvs)
334

    
335
        self.assertGreater(len(ret_flavors), 0,
336
                           "No matching flavors found")
337
        return ret_flavors
338

    
339
    # ----------------------------------
340
    # Images
341
    def _get_list_of_images(self, detail=False):
342
        """Get (detailed) list of images"""
343
        if detail:
344
            self.info("Getting detailed list of images")
345
        else:
346
            self.info("Getting simple list of images")
347
        images = self.clients.image.list_public(detail=detail)
348
        # Remove images registered by burnin
349
        images = [img for img in images
350
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
351
        return images
352

    
353
    def _get_list_of_sys_images(self, images=None):
354
        """Get (detailed) list of images registered by system user or by me"""
355
        self.info("Getting list of images registered by system user or by me")
356
        if images is None:
357
            images = self._get_list_of_images(detail=True)
358

    
359
        su_uuid = self._get_uuid_of_system_user()
360
        my_uuid = self._get_uuid()
361
        ret_images = [i for i in images
362
                      if i['owner'] == su_uuid or i['owner'] == my_uuid]
363

    
364
        return ret_images
365

    
366
    def _find_images(self, patterns, images=None):
367
        """Find a list of suitable images to use
368

369
        The patterns is a list of `typed_options'. A list of all images
370
        matching this patterns will be returned.
371

372
        """
373
        if images is None:
374
            images = self._get_list_of_sys_images()
375

    
376
        ret_images = []
377
        for ptrn in patterns:
378
            parsed_ptrn = parse_typed_option(ptrn)
379
            if parsed_ptrn is None:
380
                msg = "Invalid image format: %s. Must be [id|name]:.+"
381
                self.warning(msg, ptrn)
382
                continue
383
            img_type, img_value = parsed_ptrn
384
            if img_type == "name":
385
                # Filter image by name
386
                msg = "Trying to find an image with name %s"
387
                self.info(msg, img_value)
388
                filtered_imgs = \
389
                    [i for i in images if
390
                     re.search(img_value, i['name'], flags=re.I) is not None]
391
            elif img_type == "id":
392
                # Filter images by id
393
                msg = "Trying to find an image with id %s"
394
                self.info(msg, img_value)
395
                filtered_imgs = \
396
                    [i for i in images if
397
                     i['id'].lower() == img_value.lower()]
398
            else:
399
                self.error("Unrecognized image type %s", img_type)
400
                self.fail("Unrecognized image type")
401

    
402
            # Append and continue
403
            ret_images.extend(filtered_imgs)
404

    
405
        self.assertGreater(len(ret_images), 0,
406
                           "No matching images found")
407
        return ret_images
408

    
409
    # ----------------------------------
410
    # Pithos
411
    def _set_pithos_account(self, account):
412
        """Set the Pithos account"""
413
        assert account, "No pithos account was given"
414

    
415
        self.info("Setting Pithos account to %s", account)
416
        self.clients.pithos.account = account
417

    
418
    def _set_pithos_container(self, container):
419
        """Set the Pithos container"""
420
        assert container, "No pithos container was given"
421

    
422
        self.info("Setting Pithos container to %s", container)
423
        self.clients.pithos.container = container
424

    
425
    def _get_list_of_containers(self, account=None):
426
        """Get list of containers"""
427
        if account is not None:
428
            self._set_pithos_account(account)
429
        self.info("Getting list of containers")
430
        return self.clients.pithos.list_containers()
431

    
432
    def _create_pithos_container(self, container):
433
        """Create a pithos container
434

435
        If the container exists, nothing will happen
436

437
        """
438
        assert container, "No pithos container was given"
439

    
440
        self.info("Creating pithos container %s", container)
441
        self.clients.pithos.container = container
442
        self.clients.pithos.container_put()
443

    
444

    
445
# --------------------------------------------------------------------
446
# Initialize Burnin
447
def initialize(opts, testsuites, stale_testsuites):
448
    """Initalize burnin
449

450
    Initialize our logger and burnin state
451

452
    """
453
    # Initialize logger
454
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
455
    curr_time = datetime.datetime.now()
456
    logger = Log(opts.log_folder, verbose=opts.verbose,
457
                 use_colors=opts.use_colors, in_parallel=False,
458
                 quiet=opts.quiet, curr_time=curr_time)
459

    
460
    # Initialize clients
461
    Clients.auth_url = opts.auth_url
462
    Clients.token = opts.token
463

    
464
    # Pass the rest options to BurninTests
465
    BurninTests.use_ipv6 = opts.use_ipv6
466
    BurninTests.action_timeout = opts.action_timeout
467
    BurninTests.action_warning = opts.action_warning
468
    BurninTests.query_interval = opts.query_interval
469
    BurninTests.system_user = opts.system_user
470
    BurninTests.flavors = opts.flavors
471
    BurninTests.images = opts.images
472
    BurninTests.delete_stale = opts.delete_stale
473
    BurninTests.run_id = SNF_TEST_PREFIX + \
474
        datetime.datetime.strftime(curr_time, "%Y%m%d%H%M%S")
475

    
476
    # Choose tests to run
477
    if opts.show_stale:
478
        # We will run the stale_testsuites
479
        return (stale_testsuites, True)
480

    
481
    if opts.tests != "all":
482
        testsuites = opts.tests
483
    if opts.exclude_tests is not None:
484
        testsuites = [tsuite for tsuite in testsuites
485
                      if tsuite not in opts.exclude_tests]
486

    
487
    return (testsuites, opts.failfast)
488

    
489

    
490
# --------------------------------------------------------------------
491
# Run Burnin
492
def run_burnin(testsuites, failfast=False, final_report=False):
493
    """Run burnin testsuites"""
494
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
495

    
496
    success = True
497
    for tcase in testsuites:
498
        was_success = run_test(tcase)
499
        success = success and was_success
500
        if failfast and not success:
501
            break
502

    
503
    # Are we going to print final report?
504
    if final_report:
505
        logger.print_logfile_to_stdout()
506
    # Clean up our logger
507
    del(logger)
508

    
509
    # Return
510
    return 0 if success else 1
511

    
512

    
513
def run_test(tcase):
514
    """Run a testcase"""
515
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
516
    results = tsuite.run(BurninTestResult())
517

    
518
    return was_successful(tcase.__name__, results.wasSuccessful())
519

    
520

    
521
# --------------------------------------------------------------------
522
# Helper functions
523
def was_successful(tsuite, success):
524
    """Handle whether a testsuite was succesful or not"""
525
    if success:
526
        logger.testsuite_success(tsuite)
527
        return True
528
    else:
529
        logger.testsuite_failure(tsuite)
530
        return False
531

    
532

    
533
def parse_typed_option(value):
534
    """Parse typed options (flavors and images)
535

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

538
    """
539
    try:
540
        [type_, val] = value.strip().split(':')
541
        if type_ not in ["id", "name"]:
542
            raise ValueError
543
        return type_, val
544
    except ValueError:
545
        return None
546

    
547

    
548
class Proper(object):
549
    """A descriptor used by tests implementing the TestCase class
550

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

555
    """
556
    def __init__(self, value=None):
557
        self.val = value
558

    
559
    def __get__(self, obj, objtype=None):
560
        return self.val
561

    
562
    def __set__(self, obj, value):
563
        self.val = value