Statistics
| Branch: | Tag: | Revision:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
266
        if system_users is None:
267
            system_users = SYSTEM_USERS
268

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

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

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

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

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

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

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

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

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

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

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

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

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

    
363
        return ret_images
364

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

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

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

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

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

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

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

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

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

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

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

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

434
        If the container exists, nothing will happen
435

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

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

    
443

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

449
    Initialize our logger and burnin state
450

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

    
458
    # Initialize clients
459
    Clients.auth_url = opts.auth_url
460
    Clients.token = opts.token
461

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

    
473
    # Choose tests to run
474
    if opts.tests != "all":
475
        testsuites = opts.tests
476
    if opts.exclude_tests is not None:
477
        testsuites = [tsuite for tsuite in testsuites
478
                      if tsuite not in opts.exclude_tests]
479

    
480
    return testsuites
481

    
482

    
483
# --------------------------------------------------------------------
484
# Run Burnin
485
def run_burnin(testsuites, failfast=False, final_report=False):
486
    """Run burnin testsuites"""
487
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
488

    
489
    success = True
490
    for tcase in testsuites:
491
        was_success = run_test(tcase)
492
        success = success and was_success
493
        if failfast and not success:
494
            break
495

    
496
    # Are we going to print final report?
497
    if final_report:
498
        logger.print_logfile_to_stdout()
499
    # Clean up our logger
500
    del(logger)
501

    
502
    # Return
503
    return 0 if success else 1
504

    
505

    
506
def run_test(tcase):
507
    """Run a testcase"""
508
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
509
    results = tsuite.run(BurninTestResult())
510

    
511
    return was_successful(tcase.__name__, results.wasSuccessful())
512

    
513

    
514
# --------------------------------------------------------------------
515
# Helper functions
516
def was_successful(tsuite, success):
517
    """Handle whether a testsuite was succesful or not"""
518
    if success:
519
        logger.testsuite_success(tsuite)
520
        return True
521
    else:
522
        logger.testsuite_failure(tsuite)
523
        return False
524

    
525

    
526
def parse_typed_option(value):
527
    """Parse typed options (flavors and images)
528

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

531
    """
532
    try:
533
        [type_, val] = value.strip().split(':')
534
        if type_ not in ["id", "name"]:
535
            raise ValueError
536
        return type_, val
537
    except ValueError:
538
        return None
539

    
540

    
541
class Proper(object):
542
    """A descriptor used by tests implementing the TestCase class
543

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

548
    """
549
    def __init__(self, value=None):
550
        self.val = value
551

    
552
    def __get__(self, obj, objtype=None):
553
        return self.val
554

    
555
    def __set__(self, obj, value):
556
        self.val = value