Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / common.py @ 60a80953

History | View | Annotate | Download (22.4 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, CycladesNetworkClient
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
KB = 2**10
63
MB = 2**20
64
GB = 2**30
65

    
66

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

    
74
        # Test parameters
75
        self.failfast = True
76

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

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

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

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

    
108

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

    
135
    def initialize_clients(self):
136
        """Initialize all the Kamaki Clients"""
137
        self.astakos = AstakosClient(self.auth_url, self.token)
138
        self.astakos.CONNECTION_RETRY_LIMIT = self.retry
139

    
140
        self.compute_url = \
141
            self.astakos.get_service_endpoints('compute')['publicURL']
142
        self.compute = ComputeClient(self.compute_url, self.token)
143
        self.compute.CONNECTION_RETRY_LIMIT = self.retry
144

    
145
        self.cyclades = CycladesClient(self.compute_url, self.token)
146
        self.cyclades.CONNECTION_RETRY_LIMIT = self.retry
147

    
148
        self.network_url = \
149
            self.astakos.get_service_endpoints('network')['publicURL']
150
        self.network = CycladesNetworkClient(self.network_url, self.token)
151
        self.network.CONNECTION_RETRY_LIMIT = self.retry
152

    
153
        self.pithos_url = self.astakos.\
154
            get_service_endpoints('object-store')['publicURL']
155
        self.pithos = PithosClient(self.pithos_url, self.token)
156
        self.pithos.CONNECTION_RETRY_LIMIT = self.retry
157

    
158
        self.image_url = \
159
            self.astakos.get_service_endpoints('image')['publicURL']
160
        self.image = ImageClient(self.image_url, self.token)
161
        self.image.CONNECTION_RETRY_LIMIT = self.retry
162

    
163

    
164
class Proper(object):
165
    """A descriptor used by tests implementing the TestCase class
166

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

171
    """
172
    def __init__(self, value=None):
173
        self.val = value
174

    
175
    def __get__(self, obj, objtype=None):
176
        return self.val
177

    
178
    def __set__(self, obj, value):
179
        self.val = value
180

    
181

    
182
# --------------------------------------------------------------------
183
# BurninTests class
184
# Too many public methods (45/20). pylint: disable-msg=R0904
185
class BurninTests(unittest.TestCase):
186
    """Common class that all burnin tests should implement"""
187
    clients = Clients()
188
    run_id = None
189
    use_ipv6 = None
190
    action_timeout = None
191
    action_warning = None
192
    query_interval = None
193
    system_user = None
194
    images = None
195
    flavors = None
196
    delete_stale = False
197

    
198
    quotas = Proper(value=None)
199

    
200
    @classmethod
201
    def setUpClass(cls):  # noqa
202
        """Initialize BurninTests"""
203
        cls.suite_name = cls.__name__
204
        logger.testsuite_start(cls.suite_name)
205

    
206
        # Set test parameters
207
        cls.longMessage = True
208

    
209
    def test_000_clients_setup(self):
210
        """Initializing astakos/cyclades/pithos clients"""
211
        # Update class attributes
212
        self.clients.initialize_clients()
213
        self.info("Astakos auth url is %s", self.clients.auth_url)
214
        self.info("Cyclades url is %s", self.clients.compute_url)
215
        self.info("Network url is %s", self.clients.network_url)
216
        self.info("Pithos url is %s", self.clients.pithos_url)
217
        self.info("Image url is %s", self.clients.image_url)
218

    
219
        self.quotas = self._get_quotas()
220
        self.info("  Disk usage is %s bytes",
221
                  self.quotas['system']['cyclades.disk']['usage'])
222
        self.info("  VM usage is %s",
223
                  self.quotas['system']['cyclades.vm']['usage'])
224
        self.info("  DiskSpace usage is %s bytes",
225
                  self.quotas['system']['pithos.diskspace']['usage'])
226
        self.info("  Ram usage is %s bytes",
227
                  self.quotas['system']['cyclades.ram']['usage'])
228
        self.info("  Floating IPs usage is %s",
229
                  self.quotas['system']['cyclades.floating_ip']['usage'])
230
        self.info("  CPU usage is %s",
231
                  self.quotas['system']['cyclades.cpu']['usage'])
232
        self.info("  Network usage is %s",
233
                  self.quotas['system']['cyclades.network.private']['usage'])
234

    
235
    # ----------------------------------
236
    # Loggers helper functions
237
    def log(self, msg, *args):
238
        """Pass the section value to logger"""
239
        logger.log(self.suite_name, msg, *args)
240

    
241
    def info(self, msg, *args):
242
        """Pass the section value to logger"""
243
        logger.info(self.suite_name, msg, *args)
244

    
245
    def debug(self, msg, *args):
246
        """Pass the section value to logger"""
247
        logger.debug(self.suite_name, msg, *args)
248

    
249
    def warning(self, msg, *args):
250
        """Pass the section value to logger"""
251
        logger.warning(self.suite_name, msg, *args)
252

    
253
    def error(self, msg, *args):
254
        """Pass the section value to logger"""
255
        logger.error(self.suite_name, msg, *args)
256

    
257
    # ----------------------------------
258
    # Helper functions that every testsuite may need
259
    def _get_uuid(self):
260
        """Get our uuid"""
261
        authenticate = self.clients.astakos.authenticate()
262
        uuid = authenticate['access']['user']['id']
263
        self.info("User's uuid is %s", uuid)
264
        return uuid
265

    
266
    def _get_username(self):
267
        """Get our User Name"""
268
        authenticate = self.clients.astakos.authenticate()
269
        username = authenticate['access']['user']['name']
270
        self.info("User's name is %s", username)
271
        return username
272

    
273
    def _create_tmp_directory(self):
274
        """Create a tmp directory
275

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

279
        """
280
        temp_dir = tempfile.mkdtemp(dir=os.getcwd())
281
        self.info("Temp directory %s created", temp_dir)
282
        return temp_dir
283

    
284
    def _remove_tmp_directory(self, tmp_dir):
285
        """Remove a tmp directory"""
286
        try:
287
            shutil.rmtree(tmp_dir)
288
            self.info("Temp directory %s deleted", tmp_dir)
289
        except OSError:
290
            pass
291

    
292
    def _get_uuid_of_system_user(self):
293
        """Get the uuid of the system user
294

295
        This is the user that upload the 'official' images.
296

297
        """
298
        self.info("Getting the uuid of the system user")
299
        system_users = None
300
        if self.system_user is not None:
301
            parsed_su = parse_typed_option(self.system_user)
302
            if parsed_su is None:
303
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
304
                self.warning(msg, self.system_user)
305
            else:
306
                su_type, su_value = parsed_su
307
                if su_type == "name":
308
                    system_users = [su_value]
309
                elif su_type == "id":
310
                    self.info("System user's uuid is %s", su_value)
311
                    return su_value
312
                else:
313
                    self.error("Unrecognized system-user type %s", su_type)
314
                    self.fail("Unrecognized system-user type")
315

    
316
        if system_users is None:
317
            system_users = SYSTEM_USERS
318

    
319
        uuids = self.clients.astakos.usernames2uuids(system_users)
320
        for su_name in system_users:
321
            self.info("Trying username %s", su_name)
322
            if su_name in uuids:
323
                self.info("System user's uuid is %s", uuids[su_name])
324
                return uuids[su_name]
325

    
326
        self.warning("No system user found")
327
        return None
328

    
329
    def _skip_if(self, condition, msg):
330
        """Skip tests"""
331
        if condition:
332
            self.info("Test skipped: %s" % msg)
333
            self.skipTest(msg)
334

    
335
    # ----------------------------------
336
    # Flavors
337
    def _get_list_of_flavors(self, detail=False):
338
        """Get (detailed) list of flavors"""
339
        if detail:
340
            self.info("Getting detailed list of flavors")
341
        else:
342
            self.info("Getting simple list of flavors")
343
        flavors = self.clients.compute.list_flavors(detail=detail)
344
        return flavors
345

    
346
    def _find_flavors(self, patterns, flavors=None):
347
        """Find a list of suitable flavors to use
348

349
        The patterns is a list of `typed_options'. A list of all flavors
350
        matching this patterns will be returned.
351

352
        """
353
        if flavors is None:
354
            flavors = self._get_list_of_flavors(detail=True)
355

    
356
        ret_flavors = []
357
        for ptrn in patterns:
358
            parsed_ptrn = parse_typed_option(ptrn)
359
            if parsed_ptrn is None:
360
                msg = "Invalid flavor format: %s. Must be [id|name]:.+"
361
                self.warning(msg, ptrn)
362
                continue
363
            flv_type, flv_value = parsed_ptrn
364
            if flv_type == "name":
365
                # Filter flavor by name
366
                msg = "Trying to find a flavor with name %s"
367
                self.info(msg, flv_value)
368
                filtered_flvs = \
369
                    [f for f in flavors if
370
                     re.search(flv_value, f['name'], flags=re.I) is not None]
371
            elif flv_type == "id":
372
                # Filter flavors by id
373
                msg = "Trying to find a flavor with id %s"
374
                self.info(msg, flv_value)
375
                filtered_flvs = \
376
                    [f for f in flavors if str(f['id']) == flv_value]
377
            else:
378
                self.error("Unrecognized flavor type %s", flv_type)
379
                self.fail("Unrecognized flavor type")
380

    
381
            # Append and continue
382
            ret_flavors.extend(filtered_flvs)
383

    
384
        self.assertGreater(len(ret_flavors), 0,
385
                           "No matching flavors found")
386
        return ret_flavors
387

    
388
    # ----------------------------------
389
    # Images
390
    def _get_list_of_images(self, detail=False):
391
        """Get (detailed) list of images"""
392
        if detail:
393
            self.info("Getting detailed list of images")
394
        else:
395
            self.info("Getting simple list of images")
396
        images = self.clients.image.list_public(detail=detail)
397
        # Remove images registered by burnin
398
        images = [img for img in images
399
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
400
        return images
401

    
402
    def _get_list_of_sys_images(self, images=None):
403
        """Get (detailed) list of images registered by system user or by me"""
404
        self.info("Getting list of images registered by system user or by me")
405
        if images is None:
406
            images = self._get_list_of_images(detail=True)
407

    
408
        su_uuid = self._get_uuid_of_system_user()
409
        my_uuid = self._get_uuid()
410
        ret_images = [i for i in images
411
                      if i['owner'] == su_uuid or i['owner'] == my_uuid]
412

    
413
        return ret_images
414

    
415
    def _find_images(self, patterns, images=None):
416
        """Find a list of suitable images to use
417

418
        The patterns is a list of `typed_options'. A list of all images
419
        matching this patterns will be returned.
420

421
        """
422
        if images is None:
423
            images = self._get_list_of_sys_images()
424

    
425
        ret_images = []
426
        for ptrn in patterns:
427
            parsed_ptrn = parse_typed_option(ptrn)
428
            if parsed_ptrn is None:
429
                msg = "Invalid image format: %s. Must be [id|name]:.+"
430
                self.warning(msg, ptrn)
431
                continue
432
            img_type, img_value = parsed_ptrn
433
            if img_type == "name":
434
                # Filter image by name
435
                msg = "Trying to find an image with name %s"
436
                self.info(msg, img_value)
437
                filtered_imgs = \
438
                    [i for i in images if
439
                     re.search(img_value, i['name'], flags=re.I) is not None]
440
            elif img_type == "id":
441
                # Filter images by id
442
                msg = "Trying to find an image with id %s"
443
                self.info(msg, img_value)
444
                filtered_imgs = \
445
                    [i for i in images if
446
                     i['id'].lower() == img_value.lower()]
447
            else:
448
                self.error("Unrecognized image type %s", img_type)
449
                self.fail("Unrecognized image type")
450

    
451
            # Append and continue
452
            ret_images.extend(filtered_imgs)
453

    
454
        self.assertGreater(len(ret_images), 0,
455
                           "No matching images found")
456
        return ret_images
457

    
458
    # ----------------------------------
459
    # Pithos
460
    def _set_pithos_account(self, account):
461
        """Set the Pithos account"""
462
        assert account, "No pithos account was given"
463

    
464
        self.info("Setting Pithos account to %s", account)
465
        self.clients.pithos.account = account
466

    
467
    def _set_pithos_container(self, container):
468
        """Set the Pithos container"""
469
        assert container, "No pithos container was given"
470

    
471
        self.info("Setting Pithos container to %s", container)
472
        self.clients.pithos.container = container
473

    
474
    def _get_list_of_containers(self, account=None):
475
        """Get list of containers"""
476
        if account is not None:
477
            self._set_pithos_account(account)
478
        self.info("Getting list of containers")
479
        return self.clients.pithos.list_containers()
480

    
481
    def _create_pithos_container(self, container):
482
        """Create a pithos container
483

484
        If the container exists, nothing will happen
485

486
        """
487
        assert container, "No pithos container was given"
488

    
489
        self.info("Creating pithos container %s", container)
490
        self.clients.pithos.container = container
491
        self.clients.pithos.container_put()
492

    
493
    # ----------------------------------
494
    # Quotas
495
    def _get_quotas(self):
496
        """Get quotas"""
497
        self.info("Getting quotas")
498
        astakos_client = self.clients.astakos.get_client()
499
        return astakos_client.get_quotas()
500

    
501
    # Invalid argument name. pylint: disable-msg=C0103
502
    # Too many arguments. pylint: disable-msg=R0913
503
    def _check_quotas(self, disk=None, vm=None, diskspace=None,
504
                      ram=None, ip=None, cpu=None, network=None):
505
        """Check that quotas' changes are consistent"""
506
        assert any(v is None for v in
507
                   [disk, vm, diskspace, ram, ip, cpu, network]), \
508
            "_check_quotas require arguments"
509

    
510
        self.info("Check that quotas' changes are consistent")
511
        old_quotas = self.quotas
512
        new_quotas = self._get_quotas()
513
        self.quotas = new_quotas
514

    
515
        # Check Disk usage
516
        self._check_quotas_aux(
517
            old_quotas, new_quotas, 'cyclades.disk', disk)
518
        # Check VM usage
519
        self._check_quotas_aux(
520
            old_quotas, new_quotas, 'cyclades.vm', vm)
521
        # Check DiskSpace usage
522
        self._check_quotas_aux(
523
            old_quotas, new_quotas, 'pithos.diskspace', diskspace)
524
        # Check Ram usage
525
        self._check_quotas_aux(
526
            old_quotas, new_quotas, 'cyclades.ram', ram)
527
        # Check Floating IPs usage
528
        self._check_quotas_aux(
529
            old_quotas, new_quotas, 'cyclades.floating_ip', ip)
530
        # Check CPU usage
531
        self._check_quotas_aux(
532
            old_quotas, new_quotas, 'cyclades.cpu', cpu)
533
        # Check Network usage
534
        self._check_quotas_aux(
535
            old_quotas, new_quotas, 'cyclades.network.private', network)
536

    
537
    def _check_quotas_aux(self, old_quotas, new_quotas, resource, value):
538
        """Auxiliary function for _check_quotas"""
539
        old_value = old_quotas['system'][resource]['usage']
540
        new_value = new_quotas['system'][resource]['usage']
541
        if value is not None:
542
            assert isinstance(value, int), \
543
                "%s value has to be integer" % resource
544
            old_value += value
545
        self.assertEqual(old_value, new_value,
546
                         "%s quotas don't match" % resource)
547

    
548

    
549
# --------------------------------------------------------------------
550
# Initialize Burnin
551
def initialize(opts, testsuites, stale_testsuites):
552
    """Initalize burnin
553

554
    Initialize our logger and burnin state
555

556
    """
557
    # Initialize logger
558
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
559
    curr_time = datetime.datetime.now()
560
    logger = Log(opts.log_folder, verbose=opts.verbose,
561
                 use_colors=opts.use_colors, in_parallel=False,
562
                 log_level=opts.log_level, curr_time=curr_time)
563

    
564
    # Initialize clients
565
    Clients.auth_url = opts.auth_url
566
    Clients.token = opts.token
567

    
568
    # Pass the rest options to BurninTests
569
    BurninTests.use_ipv6 = opts.use_ipv6
570
    BurninTests.action_timeout = opts.action_timeout
571
    BurninTests.action_warning = opts.action_warning
572
    BurninTests.query_interval = opts.query_interval
573
    BurninTests.system_user = opts.system_user
574
    BurninTests.flavors = opts.flavors
575
    BurninTests.images = opts.images
576
    BurninTests.delete_stale = opts.delete_stale
577
    BurninTests.run_id = SNF_TEST_PREFIX + \
578
        datetime.datetime.strftime(curr_time, "%Y%m%d%H%M%S")
579

    
580
    # Choose tests to run
581
    if opts.show_stale:
582
        # We will run the stale_testsuites
583
        return (stale_testsuites, True)
584

    
585
    if opts.tests != "all":
586
        testsuites = opts.tests
587
    if opts.exclude_tests is not None:
588
        testsuites = [tsuite for tsuite in testsuites
589
                      if tsuite not in opts.exclude_tests]
590

    
591
    return (testsuites, opts.failfast)
592

    
593

    
594
# --------------------------------------------------------------------
595
# Run Burnin
596
def run_burnin(testsuites, failfast=False):
597
    """Run burnin testsuites"""
598
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
599

    
600
    success = True
601
    for tcase in testsuites:
602
        was_success = run_test(tcase)
603
        success = success and was_success
604
        if failfast and not success:
605
            break
606

    
607
    # Clean up our logger
608
    del(logger)
609

    
610
    # Return
611
    return 0 if success else 1
612

    
613

    
614
def run_test(tcase):
615
    """Run a testcase"""
616
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
617
    results = tsuite.run(BurninTestResult())
618

    
619
    return was_successful(tcase.__name__, results.wasSuccessful())
620

    
621

    
622
# --------------------------------------------------------------------
623
# Helper functions
624
def was_successful(tsuite, success):
625
    """Handle whether a testsuite was succesful or not"""
626
    if success:
627
        logger.testsuite_success(tsuite)
628
        return True
629
    else:
630
        logger.testsuite_failure(tsuite)
631
        return False
632

    
633

    
634
def parse_typed_option(value):
635
    """Parse typed options (flavors and images)
636

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

639
    """
640
    try:
641
        [type_, val] = value.strip().split(':')
642
        if type_ not in ["id", "name"]:
643
            raise ValueError
644
        return type_, val
645
    except ValueError:
646
        return None