Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / common.py @ 5bef1f49

History | View | Annotate | Download (19.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
# Helper Classes
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
    def initialize_clients(self):
130
        """Initialize all the Kamaki Clients"""
131
        self.astakos = AstakosClient(self.auth_url, self.token)
132
        self.astakos.CONNECTION_RETRY_LIMIT = self.retry
133

    
134
        self.compute_url = \
135
            self.astakos.get_service_endpoints('compute')['publicURL']
136
        self.compute = ComputeClient(self.compute_url, self.token)
137
        self.compute.CONNECTION_RETRY_LIMIT = self.retry
138

    
139
        self.cyclades = CycladesClient(self.compute_url, self.token)
140
        self.cyclades.CONNECTION_RETRY_LIMIT = self.retry
141

    
142
        self.pithos_url = self.astakos.\
143
            get_service_endpoints('object-store')['publicURL']
144
        self.pithos = PithosClient(self.pithos_url, self.token)
145
        self.pithos.CONNECTION_RETRY_LIMIT = self.retry
146

    
147
        self.image_url = \
148
            self.astakos.get_service_endpoints('image')['publicURL']
149
        self.image = ImageClient(self.image_url, self.token)
150
        self.image.CONNECTION_RETRY_LIMIT = self.retry
151

    
152

    
153
class Proper(object):
154
    """A descriptor used by tests implementing the TestCase class
155

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

160
    """
161
    def __init__(self, value=None):
162
        self.val = value
163

    
164
    def __get__(self, obj, objtype=None):
165
        return self.val
166

    
167
    def __set__(self, obj, value):
168
        self.val = value
169

    
170

    
171
# --------------------------------------------------------------------
172
# BurninTests class
173
# Too many public methods (45/20). pylint: disable-msg=R0904
174
class BurninTests(unittest.TestCase):
175
    """Common class that all burnin tests should implement"""
176
    clients = Clients()
177
    run_id = None
178
    use_ipv6 = None
179
    action_timeout = None
180
    action_warning = None
181
    query_interval = None
182
    system_user = None
183
    images = None
184
    flavors = None
185
    delete_stale = False
186

    
187
    quotas = Proper(value=None)
188

    
189
    @classmethod
190
    def setUpClass(cls):  # noqa
191
        """Initialize BurninTests"""
192
        cls.suite_name = cls.__name__
193
        logger.testsuite_start(cls.suite_name)
194

    
195
        # Set test parameters
196
        cls.longMessage = True
197

    
198
    def test_000_clients_setup(self):
199
        """Initializing astakos/cyclades/pithos clients"""
200
        # Update class attributes
201
        self.clients.initialize_clients()
202
        self.info("Astakos auth url is %s", self.clients.auth_url)
203
        self.info("Cyclades url is %s", self.clients.compute_url)
204
        self.info("Pithos url is %s", self.clients.pithos_url)
205
        self.info("Image url is %s", self.clients.image_url)
206

    
207
        self.quotas = self._get_quotas()
208
        self.info("  Disk usage is %s",
209
                  self.quotas['system']['cyclades.disk']['usage'])
210
        self.info("  VM usage is %s",
211
                  self.quotas['system']['cyclades.vm']['usage'])
212
        self.info("  DiskSpace usage is %s",
213
                  self.quotas['system']['pithos.diskspace']['usage'])
214
        self.info("  Ram usage is %s",
215
                  self.quotas['system']['cyclades.ram']['usage'])
216
        self.info("  CPU usage is %s",
217
                  self.quotas['system']['cyclades.cpu']['usage'])
218
        self.info("  Network usage is %s",
219
                  self.quotas['system']['cyclades.network.private']['usage'])
220

    
221
    # ----------------------------------
222
    # Loggers helper functions
223
    def log(self, msg, *args):
224
        """Pass the section value to logger"""
225
        logger.log(self.suite_name, msg, *args)
226

    
227
    def info(self, msg, *args):
228
        """Pass the section value to logger"""
229
        logger.info(self.suite_name, msg, *args)
230

    
231
    def debug(self, msg, *args):
232
        """Pass the section value to logger"""
233
        logger.debug(self.suite_name, msg, *args)
234

    
235
    def warning(self, msg, *args):
236
        """Pass the section value to logger"""
237
        logger.warning(self.suite_name, msg, *args)
238

    
239
    def error(self, msg, *args):
240
        """Pass the section value to logger"""
241
        logger.error(self.suite_name, msg, *args)
242

    
243
    # ----------------------------------
244
    # Helper functions that every testsuite may need
245
    def _get_uuid(self):
246
        """Get our uuid"""
247
        authenticate = self.clients.astakos.authenticate()
248
        uuid = authenticate['access']['user']['id']
249
        self.info("User's uuid is %s", uuid)
250
        return uuid
251

    
252
    def _get_username(self):
253
        """Get our User Name"""
254
        authenticate = self.clients.astakos.authenticate()
255
        username = authenticate['access']['user']['name']
256
        self.info("User's name is %s", username)
257
        return username
258

    
259
    def _create_tmp_directory(self):
260
        """Create a tmp directory
261

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

265
        """
266
        temp_dir = tempfile.mkdtemp(dir=os.getcwd())
267
        self.info("Temp directory %s created", temp_dir)
268
        return temp_dir
269

    
270
    def _remove_tmp_directory(self, tmp_dir):
271
        """Remove a tmp directory"""
272
        try:
273
            shutil.rmtree(tmp_dir)
274
            self.info("Temp directory %s deleted", tmp_dir)
275
        except OSError:
276
            pass
277

    
278
    def _get_uuid_of_system_user(self):
279
        """Get the uuid of the system user
280

281
        This is the user that upload the 'official' images.
282

283
        """
284
        self.info("Getting the uuid of the system user")
285
        system_users = None
286
        if self.system_user is not None:
287
            parsed_su = parse_typed_option(self.system_user)
288
            if parsed_su is None:
289
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
290
                self.warning(msg, self.system_user)
291
            else:
292
                su_type, su_value = parsed_su
293
                if su_type == "name":
294
                    system_users = [su_value]
295
                elif su_type == "id":
296
                    self.info("System user's uuid is %s", su_value)
297
                    return su_value
298
                else:
299
                    self.error("Unrecognized system-user type %s", su_type)
300
                    self.fail("Unrecognized system-user type")
301

    
302
        if system_users is None:
303
            system_users = SYSTEM_USERS
304

    
305
        uuids = self.clients.astakos.usernames2uuids(system_users)
306
        for su_name in system_users:
307
            self.info("Trying username %s", su_name)
308
            if su_name in uuids:
309
                self.info("System user's uuid is %s", uuids[su_name])
310
                return uuids[su_name]
311

    
312
        self.warning("No system user found")
313
        return None
314

    
315
    def _skip_if(self, condition, msg):
316
        """Skip tests"""
317
        if condition:
318
            self.info("Test skipped: %s" % msg)
319
            self.skipTest(msg)
320

    
321
    # ----------------------------------
322
    # Flavors
323
    def _get_list_of_flavors(self, detail=False):
324
        """Get (detailed) list of flavors"""
325
        if detail:
326
            self.info("Getting detailed list of flavors")
327
        else:
328
            self.info("Getting simple list of flavors")
329
        flavors = self.clients.compute.list_flavors(detail=detail)
330
        return flavors
331

    
332
    def _find_flavors(self, patterns, flavors=None):
333
        """Find a list of suitable flavors to use
334

335
        The patterns is a list of `typed_options'. A list of all flavors
336
        matching this patterns will be returned.
337

338
        """
339
        if flavors is None:
340
            flavors = self._get_list_of_flavors(detail=True)
341

    
342
        ret_flavors = []
343
        for ptrn in patterns:
344
            parsed_ptrn = parse_typed_option(ptrn)
345
            if parsed_ptrn is None:
346
                msg = "Invalid flavor format: %s. Must be [id|name]:.+"
347
                self.warning(msg, ptrn)
348
                continue
349
            flv_type, flv_value = parsed_ptrn
350
            if flv_type == "name":
351
                # Filter flavor by name
352
                msg = "Trying to find a flavor with name %s"
353
                self.info(msg, flv_value)
354
                filtered_flvs = \
355
                    [f for f in flavors if
356
                     re.search(flv_value, f['name'], flags=re.I) is not None]
357
            elif flv_type == "id":
358
                # Filter flavors by id
359
                msg = "Trying to find a flavor with id %s"
360
                self.info(msg, flv_value)
361
                filtered_flvs = \
362
                    [f for f in flavors if str(f['id']) == flv_value]
363
            else:
364
                self.error("Unrecognized flavor type %s", flv_type)
365
                self.fail("Unrecognized flavor type")
366

    
367
            # Append and continue
368
            ret_flavors.extend(filtered_flvs)
369

    
370
        self.assertGreater(len(ret_flavors), 0,
371
                           "No matching flavors found")
372
        return ret_flavors
373

    
374
    # ----------------------------------
375
    # Images
376
    def _get_list_of_images(self, detail=False):
377
        """Get (detailed) list of images"""
378
        if detail:
379
            self.info("Getting detailed list of images")
380
        else:
381
            self.info("Getting simple list of images")
382
        images = self.clients.image.list_public(detail=detail)
383
        # Remove images registered by burnin
384
        images = [img for img in images
385
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
386
        return images
387

    
388
    def _get_list_of_sys_images(self, images=None):
389
        """Get (detailed) list of images registered by system user or by me"""
390
        self.info("Getting list of images registered by system user or by me")
391
        if images is None:
392
            images = self._get_list_of_images(detail=True)
393

    
394
        su_uuid = self._get_uuid_of_system_user()
395
        my_uuid = self._get_uuid()
396
        ret_images = [i for i in images
397
                      if i['owner'] == su_uuid or i['owner'] == my_uuid]
398

    
399
        return ret_images
400

    
401
    def _find_images(self, patterns, images=None):
402
        """Find a list of suitable images to use
403

404
        The patterns is a list of `typed_options'. A list of all images
405
        matching this patterns will be returned.
406

407
        """
408
        if images is None:
409
            images = self._get_list_of_sys_images()
410

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

    
437
            # Append and continue
438
            ret_images.extend(filtered_imgs)
439

    
440
        self.assertGreater(len(ret_images), 0,
441
                           "No matching images found")
442
        return ret_images
443

    
444
    # ----------------------------------
445
    # Pithos
446
    def _set_pithos_account(self, account):
447
        """Set the Pithos account"""
448
        assert account, "No pithos account was given"
449

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

    
453
    def _set_pithos_container(self, container):
454
        """Set the Pithos container"""
455
        assert container, "No pithos container was given"
456

    
457
        self.info("Setting Pithos container to %s", container)
458
        self.clients.pithos.container = container
459

    
460
    def _get_list_of_containers(self, account=None):
461
        """Get list of containers"""
462
        if account is not None:
463
            self._set_pithos_account(account)
464
        self.info("Getting list of containers")
465
        return self.clients.pithos.list_containers()
466

    
467
    def _create_pithos_container(self, container):
468
        """Create a pithos container
469

470
        If the container exists, nothing will happen
471

472
        """
473
        assert container, "No pithos container was given"
474

    
475
        self.info("Creating pithos container %s", container)
476
        self.clients.pithos.container = container
477
        self.clients.pithos.container_put()
478

    
479
    # ----------------------------------
480
    # Quotas
481
    def _get_quotas(self):
482
        """Get quotas"""
483
        self.info("Getting quotas for user %s", self._get_uuid())
484
        astakos_client = self.clients.astakos.get_client()
485
        return astakos_client.get_quotas()
486

    
487

    
488
# --------------------------------------------------------------------
489
# Initialize Burnin
490
def initialize(opts, testsuites, stale_testsuites):
491
    """Initalize burnin
492

493
    Initialize our logger and burnin state
494

495
    """
496
    # Initialize logger
497
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
498
    curr_time = datetime.datetime.now()
499
    logger = Log(opts.log_folder, verbose=opts.verbose,
500
                 use_colors=opts.use_colors, in_parallel=False,
501
                 log_level=opts.log_level, curr_time=curr_time)
502

    
503
    # Initialize clients
504
    Clients.auth_url = opts.auth_url
505
    Clients.token = opts.token
506

    
507
    # Pass the rest options to BurninTests
508
    BurninTests.use_ipv6 = opts.use_ipv6
509
    BurninTests.action_timeout = opts.action_timeout
510
    BurninTests.action_warning = opts.action_warning
511
    BurninTests.query_interval = opts.query_interval
512
    BurninTests.system_user = opts.system_user
513
    BurninTests.flavors = opts.flavors
514
    BurninTests.images = opts.images
515
    BurninTests.delete_stale = opts.delete_stale
516
    BurninTests.run_id = SNF_TEST_PREFIX + \
517
        datetime.datetime.strftime(curr_time, "%Y%m%d%H%M%S")
518

    
519
    # Choose tests to run
520
    if opts.show_stale:
521
        # We will run the stale_testsuites
522
        return (stale_testsuites, True)
523

    
524
    if opts.tests != "all":
525
        testsuites = opts.tests
526
    if opts.exclude_tests is not None:
527
        testsuites = [tsuite for tsuite in testsuites
528
                      if tsuite not in opts.exclude_tests]
529

    
530
    return (testsuites, opts.failfast)
531

    
532

    
533
# --------------------------------------------------------------------
534
# Run Burnin
535
def run_burnin(testsuites, failfast=False):
536
    """Run burnin testsuites"""
537
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
538

    
539
    success = True
540
    for tcase in testsuites:
541
        was_success = run_test(tcase)
542
        success = success and was_success
543
        if failfast and not success:
544
            break
545

    
546
    # Clean up our logger
547
    del(logger)
548

    
549
    # Return
550
    return 0 if success else 1
551

    
552

    
553
def run_test(tcase):
554
    """Run a testcase"""
555
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
556
    results = tsuite.run(BurninTestResult())
557

    
558
    return was_successful(tcase.__name__, results.wasSuccessful())
559

    
560

    
561
# --------------------------------------------------------------------
562
# Helper functions
563
def was_successful(tsuite, success):
564
    """Handle whether a testsuite was succesful or not"""
565
    if success:
566
        logger.testsuite_success(tsuite)
567
        return True
568
    else:
569
        logger.testsuite_failure(tsuite)
570
        return False
571

    
572

    
573
def parse_typed_option(value):
574
    """Parse typed options (flavors and images)
575

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

578
    """
579
    try:
580
        [type_, val] = value.strip().split(':')
581
        if type_ not in ["id", "name"]:
582
            raise ValueError
583
        return type_, val
584
    except ValueError:
585
        return None