Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / common.py @ 24d1788b

History | View | Annotate | Download (22.3 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 re
40
import shutil
41
import unittest
42
import datetime
43
import tempfile
44
import traceback
45

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

    
52
from synnefo_tools.burnin.logger import Log
53

    
54

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

    
65

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

    
73
        # Test parameters
74
        self.failfast = True
75

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

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

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

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

    
107

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

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

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

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

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

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

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

    
162

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

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

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

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

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

    
180

    
181
# --------------------------------------------------------------------
182
# BurninTests class
183
# Too many public methods (45/20). pylint: disable-msg=R0904
184
class BurninTests(unittest.TestCase):
185
    """Common class that all burnin tests should implement"""
186
    clients = Clients()
187
    run_id = None
188
    use_ipv6 = None
189
    action_timeout = None
190
    action_warning = None
191
    query_interval = None
192
    system_user = None
193
    images = None
194
    flavors = None
195
    delete_stale = False
196
    temp_directory = None
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
        temp_dir = tempfile.mkdtemp(dir=self.temp_directory)
276
        self.info("Temp directory %s created", temp_dir)
277
        return temp_dir
278

    
279
    def _remove_tmp_directory(self, tmp_dir):
280
        """Remove a tmp directory"""
281
        try:
282
            shutil.rmtree(tmp_dir)
283
            self.info("Temp directory %s deleted", tmp_dir)
284
        except OSError:
285
            pass
286

    
287
    def _get_uuid_of_system_user(self):
288
        """Get the uuid of the system user
289

290
        This is the user that upload the 'official' images.
291

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

    
311
        if system_users is None:
312
            system_users = SYSTEM_USERS
313

    
314
        uuids = self.clients.astakos.usernames2uuids(system_users)
315
        for su_name in system_users:
316
            self.info("Trying username %s", su_name)
317
            if su_name in uuids:
318
                self.info("System user's uuid is %s", uuids[su_name])
319
                return uuids[su_name]
320

    
321
        self.warning("No system user found")
322
        return None
323

    
324
    def _skip_if(self, condition, msg):
325
        """Skip tests"""
326
        if condition:
327
            self.info("Test skipped: %s" % msg)
328
            self.skipTest(msg)
329

    
330
    # ----------------------------------
331
    # Flavors
332
    def _get_list_of_flavors(self, detail=False):
333
        """Get (detailed) list of flavors"""
334
        if detail:
335
            self.info("Getting detailed list of flavors")
336
        else:
337
            self.info("Getting simple list of flavors")
338
        flavors = self.clients.compute.list_flavors(detail=detail)
339
        return flavors
340

    
341
    def _find_flavors(self, patterns, flavors=None):
342
        """Find a list of suitable flavors to use
343

344
        The patterns is a list of `typed_options'. A list of all flavors
345
        matching this patterns will be returned.
346

347
        """
348
        if flavors is None:
349
            flavors = self._get_list_of_flavors(detail=True)
350

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

    
376
            # Append and continue
377
            ret_flavors.extend(filtered_flvs)
378

    
379
        self.assertGreater(len(ret_flavors), 0,
380
                           "No matching flavors found")
381
        return ret_flavors
382

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

    
397
    def _get_list_of_sys_images(self, images=None):
398
        """Get (detailed) list of images registered by system user or by me"""
399
        self.info("Getting list of images registered by system user or by me")
400
        if images is None:
401
            images = self._get_list_of_images(detail=True)
402

    
403
        su_uuid = self._get_uuid_of_system_user()
404
        my_uuid = self._get_uuid()
405
        ret_images = [i for i in images
406
                      if i['owner'] == su_uuid or i['owner'] == my_uuid]
407

    
408
        return ret_images
409

    
410
    def _find_images(self, patterns, images=None):
411
        """Find a list of suitable images to use
412

413
        The patterns is a list of `typed_options'. A list of all images
414
        matching this patterns will be returned.
415

416
        """
417
        if images is None:
418
            images = self._get_list_of_sys_images()
419

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

    
446
            # Append and continue
447
            ret_images.extend(filtered_imgs)
448

    
449
        self.assertGreater(len(ret_images), 0,
450
                           "No matching images found")
451
        return ret_images
452

    
453
    # ----------------------------------
454
    # Pithos
455
    def _set_pithos_account(self, account):
456
        """Set the Pithos account"""
457
        assert account, "No pithos account was given"
458

    
459
        self.info("Setting Pithos account to %s", account)
460
        self.clients.pithos.account = account
461

    
462
    def _set_pithos_container(self, container):
463
        """Set the Pithos container"""
464
        assert container, "No pithos container was given"
465

    
466
        self.info("Setting Pithos container to %s", container)
467
        self.clients.pithos.container = container
468

    
469
    def _get_list_of_containers(self, account=None):
470
        """Get list of containers"""
471
        if account is not None:
472
            self._set_pithos_account(account)
473
        self.info("Getting list of containers")
474
        return self.clients.pithos.list_containers()
475

    
476
    def _create_pithos_container(self, container):
477
        """Create a pithos container
478

479
        If the container exists, nothing will happen
480

481
        """
482
        assert container, "No pithos container was given"
483

    
484
        self.info("Creating pithos container %s", container)
485
        self.clients.pithos.container = container
486
        self.clients.pithos.container_put()
487

    
488
    # ----------------------------------
489
    # Quotas
490
    def _get_quotas(self):
491
        """Get quotas"""
492
        self.info("Getting quotas")
493
        astakos_client = self.clients.astakos.get_client()
494
        return astakos_client.get_quotas()
495

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

    
505
        self.info("Check that quotas' changes are consistent")
506
        old_quotas = self.quotas
507
        new_quotas = self._get_quotas()
508
        self.quotas = new_quotas
509

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

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

    
543

    
544
# --------------------------------------------------------------------
545
# Initialize Burnin
546
def initialize(opts, testsuites, stale_testsuites):
547
    """Initalize burnin
548

549
    Initialize our logger and burnin state
550

551
    """
552
    # Initialize logger
553
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
554
    curr_time = datetime.datetime.now()
555
    logger = Log(opts.log_folder, verbose=opts.verbose,
556
                 use_colors=opts.use_colors, in_parallel=False,
557
                 log_level=opts.log_level, curr_time=curr_time)
558

    
559
    # Initialize clients
560
    Clients.auth_url = opts.auth_url
561
    Clients.token = opts.token
562

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

    
576
    # Choose tests to run
577
    if opts.show_stale:
578
        # We will run the stale_testsuites
579
        return (stale_testsuites, True)
580

    
581
    if opts.tests != "all":
582
        testsuites = opts.tests
583
    if opts.exclude_tests is not None:
584
        testsuites = [tsuite for tsuite in testsuites
585
                      if tsuite not in opts.exclude_tests]
586

    
587
    return (testsuites, opts.failfast)
588

    
589

    
590
# --------------------------------------------------------------------
591
# Run Burnin
592
def run_burnin(testsuites, failfast=False):
593
    """Run burnin testsuites"""
594
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
595

    
596
    success = True
597
    for tcase in testsuites:
598
        was_success = run_test(tcase)
599
        success = success and was_success
600
        if failfast and not success:
601
            break
602

    
603
    # Clean up our logger
604
    del(logger)
605

    
606
    # Return
607
    return 0 if success else 1
608

    
609

    
610
def run_test(tcase):
611
    """Run a testcase"""
612
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
613
    results = tsuite.run(BurninTestResult())
614

    
615
    return was_successful(tcase.__name__, results.wasSuccessful())
616

    
617

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

    
629

    
630
def parse_typed_option(value):
631
    """Parse typed options (flavors and images)
632

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

635
    """
636
    try:
637
        [type_, val] = value.strip().split(':')
638
        if type_ not in ["id", "name"]:
639
            raise ValueError
640
        return type_, val
641
    except ValueError:
642
        return None