common.py 16.9 KB
Newer Older
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

"""
Common utils for burnin tests

"""

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
39
40
import os
import re
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
41
import sys
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
42
import shutil
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
43
import datetime
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
44
import tempfile
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
45
46
47
48
49
50
51
52
53
54
55
import traceback
# Use backported unittest functionality if Python < 2.7
try:
    import unittest2 as unittest
except ImportError:
    if sys.version_info < (2, 7):
        raise Exception("The unittest2 package is required for Python < 2.7")
    import unittest

from kamaki.clients.astakos import AstakosClient
from kamaki.clients.compute import ComputeClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
56
from kamaki.clients.pithos import PithosClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
57
from kamaki.clients.image import ImageClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
58

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
59
from synnefo_tools.burnin.logger import Log
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
60
61
62
63
64


# --------------------------------------------------------------------
# Global variables
logger = None  # Invalid constant name. pylint: disable-msg=C0103
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
65
SNF_TEST_PREFIX = "snf-test-"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
66
CONNECTION_RETRY_LIMIT = 2
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
67
SYSTEM_USERS = ["images@okeanos.grnet.gr", "images@demo.synnefo.org"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82


# --------------------------------------------------------------------
# BurninTestResult class
class BurninTestResult(unittest.TestResult):
    """Modify the TextTestResult class"""
    def __init__(self):
        super(BurninTestResult, self).__init__()

        # Test parameters
        self.failfast = True

    def startTest(self, test):  # noqa
        """Called when the test case test is about to be run"""
        super(BurninTestResult, self).startTest(test)
83
        logger.log(test.__class__.__name__, test.shortDescription())
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
84
85
86
87

    # Method could be a function. pylint: disable-msg=R0201
    def _test_failed(self, test, err):
        """Test failed"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
88
89
90
91
92
        # Get class name
        if test.__class__.__name__ == "_ErrorHolder":
            class_name = test.id().split('.')[-1].rstrip(')')
        else:
            class_name = test.__class__.__name__
93
        err_msg = str(test) + "... failed (%s)."
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
94
95
        timestamp = datetime.datetime.strftime(
            datetime.datetime.now(), "%a %b %d %Y %H:%M:%S")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
96
        logger.error(class_name, err_msg, timestamp)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
97
98
        (err_type, err_value, err_trace) = err
        trcback = traceback.format_exception(err_type, err_value, err_trace)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
99
        logger.info(class_name, trcback)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
100
101
102
103
104
105
106
107
108
109
110
111
112
113

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

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


# --------------------------------------------------------------------
# BurninTests class
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
114
115
116
117
118
# Too few public methods (0/2). pylint: disable-msg=R0903
class Clients(object):
    """Our kamaki clients"""
    auth_url = None
    token = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
119
    # Astakos
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
120
121
    astakos = None
    retry = CONNECTION_RETRY_LIMIT
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
122
    # Compute
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
123
124
    compute = None
    compute_url = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
125
126
127
128
129
    # Cyclades
    cyclades = None
    # Pithos
    pithos = None
    pithos_url = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
130
131
132
    # Image
    image = None
    image_url = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
133
134


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
135
136
137
# Too many public methods (45/20). pylint: disable-msg=R0904
class BurninTests(unittest.TestCase):
    """Common class that all burnin tests should implement"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
138
    clients = Clients()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
139
    run_id = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
140
141
142
143
    use_ipv6 = None
    action_timeout = None
    action_warning = None
    query_interval = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
144
    system_user = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
145

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
146
147
148
149
150
151
152
153
154
    @classmethod
    def setUpClass(cls):  # noqa
        """Initialize BurninTests"""
        cls.suite_name = cls.__name__
        logger.testsuite_start(cls.suite_name)

        # Set test parameters
        cls.longMessage = True

155
156
157
158
159
160
161
162
163
164
165
166
    def _setattr(self, attr, value):
        """Used by tests to set an attribute to TestCase

        Since each instance of the TestCase will only be used to run a single
        test method (a new fixture is created for each test) the attributes can
        not be saved in the class instance. Instead the class itself should be
        used.

        """
        setattr(self.__class__, attr, value)

    def test_000_clients_setup(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
167
168
        """Initializing astakos/cyclades/pithos clients"""
        # Update class attributes
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
169
170
171
172
173
174
175
176
177
178
179
        self.info("Astakos auth url is %s", self.clients.auth_url)
        self.clients.astakos = AstakosClient(
            self.clients.auth_url, self.clients.token)
        self.clients.astakos.CONNECTION_RETRY_LIMIT = self.clients.retry

        self.clients.compute_url = \
            self.clients.astakos.get_service_endpoints('compute')['publicURL']
        self.info("Cyclades url is %s", self.clients.compute_url)
        self.clients.compute = ComputeClient(
            self.clients.compute_url, self.clients.token)
        self.clients.compute.CONNECTION_RETRY_LIMIT = self.clients.retry
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
180

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
181
182
183
184
185
186
187
        self.clients.pithos_url = self.clients.astakos.\
            get_service_endpoints('object-store')['publicURL']
        self.info("Pithos url is %s", self.clients.pithos_url)
        self.clients.pithos = PithosClient(
            self.clients.pithos_url, self.clients.token)
        self.clients.pithos.CONNECTION_RETRY_LIMIT = self.clients.retry

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
188
189
190
191
192
193
194
        self.clients.image_url = \
            self.clients.astakos.get_service_endpoints('image')['publicURL']
        self.info("Image url is %s", self.clients.image_url)
        self.clients.image = ImageClient(
            self.clients.image_url, self.clients.token)
        self.clients.image.CONNECTION_RETRY_LIMIT = self.clients.retry

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
195
196
    # ----------------------------------
    # Loggers helper functions
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
    def log(self, msg, *args):
        """Pass the section value to logger"""
        logger.log(self.suite_name, msg, *args)

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

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

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

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
    # ----------------------------------
    # Helper functions that every testsuite may need
    def _get_uuid(self):
        """Get our uuid"""
        authenticate = self.clients.astakos.authenticate()
        uuid = authenticate['access']['user']['id']
        self.info("User's uuid is %s", uuid)
        return uuid

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
    def _create_tmp_directory(self):
        """Create a tmp directory

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

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

    def _remove_tmp_directory(self, tmp_dir):
        """Remove a tmp directory"""
        try:
            shutil.rmtree(tmp_dir)
            self.info("Temp directory %s deleted", tmp_dir)
        except OSError:
            pass

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

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

        """
        self.info("Getting the uuid of the system user")
        system_users = None
        if self.system_user is not None:
            parsed_su = parse_typed_option(self.system_user)
            if parsed_su is None:
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
                self.warning(msg, self.system_user)
            else:
                su_type, su_value = parsed_su
                if su_type == "name":
                    system_users = [su_value]
                elif su_type == "id":
                    self.info("System user's uuid is %s", su_value)
                    return su_value
                else:
                    self.error("Unrecognized system-user type %s", su_type)
                    self.fail("Unrecognized system-user type")

        if system_users is None:
            system_users = SYSTEM_USERS

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

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

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
    # ----------------------------------
    # Images
    def _get_list_of_images(self, detail=False):
        """Get (detailed) list of images"""
        if detail:
            self.info("Getting detailed list of images")
        else:
            self.info("Getting simple list of images")
        images = self.clients.image.list_public(detail=detail)
        # Remove images registered by burnin
        images = [img for img in images
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
        return images

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

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

        return ret_images

    def _find_image(self, patterns, images=None):
        """Find a suitable image to use

        The patterns is a list of `typed_options'. The first pattern to
        match an image will be the one that will be returned.

        """
        if images is None:
            images = self._get_list_of_sys_images()

        for ptrn in patterns:
            parsed_ptrn = parse_typed_option(ptrn)
            if parsed_ptrn is None:
                msg = "Invalid image format: %s. Must be [id|name]:.+"
                self.warning(msg, ptrn)
                continue
            img_type, img_value = parsed_ptrn
            if img_type == "name":
                # Filter image by name
                msg = "Trying to find an image with name %s"
                self.info(msg, img_value)
                filtered_imgs = \
                    [i for i in images if
                     re.search(img_value, i['name'], flags=re.I) is not None]
            elif img_type == "id":
                # Filter images by id
                msg = "Trying to find an image with id %s"
                self.info(msg, img_value)
                filtered_imgs = \
                    [i for i in images if
                     i['id'].lower() == img_value.lower()]
            else:
                self.error("Unrecognized image type %s", img_type)
                self.fail("Unrecognized image type")

            # Check if we found one
            if filtered_imgs:
                img = filtered_imgs[0]
                self.info("Will use %s with id %s", img['name'], img['id'])
                return img

        # We didn't found one
        err = "No matching image found"
        self.error(err)
        self.fail(err)

    # ----------------------------------
    # Pithos
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
375
    def _set_pithos_account(self, account):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
376
        """Set the Pithos account"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
377
378
        assert account, "No pithos account was given"

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
379
        self.info("Setting Pithos account to %s", account)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
380
381
        self.clients.pithos.account = account

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
382
383
384
385
386
387
388
    def _set_pithos_container(self, container):
        """Set the Pithos container"""
        assert container, "No pithos container was given"

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
    def _get_list_of_containers(self, account=None):
        """Get list of containers"""
        if account is not None:
            self._set_pithos_account(account)
        self.info("Getting list of containers")
        return self.clients.pithos.list_containers()

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

        If the container exists, nothing will happen

        """
        assert container, "No pithos container was given"

        self.info("Creating pithos container %s", container)
        self.clients.pithos.container = container
        self.clients.pithos.container_put()

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
408

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
409
410
411
412
413
414
415
416
417
418
419
# --------------------------------------------------------------------
# Initialize Burnin
def initialize(opts, testsuites):
    """Initalize burnin

    Initialize our logger and burnin state

    """
    # Initialize logger
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
    logger = Log(opts.log_folder, verbose=opts.verbose,
420
421
                 use_colors=opts.use_colors, in_parallel=False,
                 quiet=opts.quiet)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
422
423
424
425
426
427

    # Initialize clients
    Clients.auth_url = opts.auth_url
    Clients.token = opts.token

    # Pass the rest options to BurninTests
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
428
429
430
431
    BurninTests.use_ipv6 = opts.use_ipv6
    BurninTests.action_timeout = opts.action_timeout
    BurninTests.action_warning = opts.action_warning
    BurninTests.query_interval = opts.query_interval
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
432
    BurninTests.system_user = opts.system_user
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
433
434
    BurninTests.run_id = SNF_TEST_PREFIX + \
        datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d%H%M%S")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
435
436
437
438
439
440
441
442
443
444
445
446
447

    # Choose tests to run
    if opts.tests != "all":
        testsuites = opts.tests
    if opts.exclude_tests is not None:
        testsuites = [tsuite for tsuite in testsuites
                      if tsuite not in opts.exclude_tests]

    return testsuites


# --------------------------------------------------------------------
# Run Burnin
448
def run(testsuites, failfast=False, final_report=False):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
449
450
451
452
453
454
455
    """Run burnin testsuites"""
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602

    success = True
    for tcase in testsuites:
        tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
        results = tsuite.run(BurninTestResult())
456
457
458

        was_success = was_successful(tcase.__name__, results.wasSuccessful())
        success = success and was_success
459
460
        if failfast and not success:
            break
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
461

462
463
464
    # Are we going to print final report?
    if final_report:
        logger.print_logfile_to_stdout()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
465
466
467
468
469
470
471
    # Clean up our logger
    del(logger)

    # Return
    return 0 if success else 1


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
472
473
# --------------------------------------------------------------------
# Helper functions
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
474
def was_successful(tsuite, success):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
475
476
477
    """Handle whether a testsuite was succesful or not"""
    if success:
        logger.testsuite_success(tsuite)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
478
        return True
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
479
480
    else:
        logger.testsuite_failure(tsuite)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
481
        return False
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496


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

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

    """
    try:
        [type_, val] = value.strip().split(':')
        if type_ not in ["id", "name"]:
            raise ValueError
        return type_, val
    except ValueError:
        return None