common.py 29.2 KB
Newer Older
Vangelis Koukis's avatar
Vangelis Koukis committed
1
# Copyright (C) 2010-2014 GRNET S.A.
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
2
#
Vangelis Koukis's avatar
Vangelis Koukis committed
3
4
5
6
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
7
#
Vangelis Koukis's avatar
Vangelis Koukis committed
8
9
10
11
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
12
#
Vangelis Koukis's avatar
Vangelis Koukis committed
13
14
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
15
16
17
18
19
20

"""
Common utils for burnin tests

"""

21
import hashlib
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
22
23
import re
import shutil
24
import unittest
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
25
import datetime
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
26
import tempfile
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
27
import traceback
28
29
from tempfile import NamedTemporaryFile
from os import urandom
30
from string import ascii_letters
31
32
from StringIO import StringIO
from binascii import hexlify
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
33

34
from kamaki.cli import config as kamaki_config
35
from kamaki.clients.cyclades import CycladesClient, CycladesNetworkClient
36
from kamaki.clients.astakos import AstakosClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
37
from kamaki.clients.compute import ComputeClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
38
from kamaki.clients.pithos import PithosClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
39
from kamaki.clients.image import ImageClient
40
from kamaki.clients.utils import https
41
from kamaki.clients.blockstorage import BlockStorageClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
42

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
43
from synnefo_tools.burnin.logger import Log
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
44
45
46
47


# --------------------------------------------------------------------
# Global variables
48
49
logger = None   # pylint: disable=invalid-name
success = None  # pylint: disable=invalid-name
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
50
SNF_TEST_PREFIX = "snf-test-"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
51
CONNECTION_RETRY_LIMIT = 2
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
52
SYSTEM_USERS = ["images@okeanos.grnet.gr", "images@demo.synnefo.org"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
53
54
55
KB = 2**10
MB = 2**20
GB = 2**30
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
56

57
58
59
60
61
62
63
64
65
66
67
QADD = 1
QREMOVE = -1

QDISK = "cyclades.disk"
QVM = "cyclades.vm"
QPITHOS = "pithos.diskspace"
QRAM = "cyclades.ram"
QIP = "cyclades.floating_ip"
QCPU = "cyclades.cpu"
QNET = "cyclades.network.private"

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
68
69
70
71
72
73
74
75
76
77
78
79
80
81

# --------------------------------------------------------------------
# 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)
82
83
84
        logger.log(
            test.__class__.__name__,
            test.shortDescription() or 'Test %s' % test.__class__.__name__)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
85

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

    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)

112
113
114
115
116
117
118
119
120
121
122
123
124
125
    # pylint: disable=fixme
    def addSkip(self, test, reason):  # noqa
        """Called when the test case test is skipped

        If reason starts with "__SkipClass__: " then
        we should stop the execution of all the TestSuite.

        TODO: There should be a better way to do this

        """
        super(BurninTestResult, self).addSkip(test, reason)
        if reason.startswith("__SkipClass__: "):
            self.stop()

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
126
127

# --------------------------------------------------------------------
128
# Helper Classes
129
130
# pylint: disable=too-few-public-methods
# pylint: disable=too-many-instance-attributes
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
131
132
133
134
class Clients(object):
    """Our kamaki clients"""
    auth_url = None
    token = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
135
    # Astakos
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
136
137
    astakos = None
    retry = CONNECTION_RETRY_LIMIT
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
138
    # Compute
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
139
140
    compute = None
    compute_url = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
141
142
    # Cyclades
    cyclades = None
143
144
145
    # Network
    network = None
    network_url = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
146
147
148
    # Pithos
    pithos = None
    pithos_url = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
149
150
151
    # Image
    image = None
    image_url = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
152

153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
    def _kamaki_ssl(self, ignore_ssl=None):
        """Patch kamaki to use the correct CA certificates

        Read kamaki's config file and decide if we are going to use
        CA certificates and patch kamaki clients accordingly.

        """
        config = kamaki_config.Config()
        if ignore_ssl is None:
            ignore_ssl = config.get("global", "ignore_ssl").lower() == "on"
        ca_file = config.get("global", "ca_certs")

        if ignore_ssl:
            # Skip SSL verification
            https.patch_ignore_ssl()
        else:
            # Use ca_certs path found in kamakirc
            https.patch_with_certs(ca_file)

    def initialize_clients(self, ignore_ssl=False):
173
        """Initialize all the Kamaki Clients"""
174
175
176
177
178

        # Path kamaki for SSL verification
        self._kamaki_ssl(ignore_ssl=ignore_ssl)

        # Initialize kamaki Clients
179
        self.astakos = AstakosClient(self.auth_url, self.token)
180
181
        self.astakos.CONNECTION_RETRY_LIMIT = self.retry

182
183
        self.compute_url = self.astakos.get_endpoint_url(
            ComputeClient.service_type)
184
185
186
        self.compute = ComputeClient(self.compute_url, self.token)
        self.compute.CONNECTION_RETRY_LIMIT = self.retry

187
188
189
        self.cyclades_url = self.astakos.get_endpoint_url(
            CycladesClient.service_type)
        self.cyclades = CycladesClient(self.cyclades_url, self.token)
190
191
        self.cyclades.CONNECTION_RETRY_LIMIT = self.retry

192
193
194
195
196
197
198
199
        self.block_storage_url = self.astakos.get_endpoint_url(
            BlockStorageClient.service_type)
        self.block_storage = BlockStorageClient(self.block_storage_url,
                                                self.token)
        self.block_storage.CONNECTION_RETRY_LIMIT = self.retry

        self.network_url = self.astakos.get_endpoint_url(
            CycladesNetworkClient.service_type)
200
201
202
        self.network = CycladesNetworkClient(self.network_url, self.token)
        self.network.CONNECTION_RETRY_LIMIT = self.retry

203
204
        self.pithos_url = self.astakos.get_endpoint_url(
            PithosClient.service_type)
205
206
207
        self.pithos = PithosClient(self.pithos_url, self.token)
        self.pithos.CONNECTION_RETRY_LIMIT = self.retry

208
209
        self.image_url = self.astakos.get_endpoint_url(
            ImageClient.service_type)
210
211
212
213
214
215
216
217
218
219
        self.image = ImageClient(self.image_url, self.token)
        self.image.CONNECTION_RETRY_LIMIT = self.retry


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

    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 instances. Instead we use descriptors.
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
220

221
222
223
224
225
226
227
228
229
230
231
    """
    def __init__(self, value=None):
        self.val = value

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

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


232
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
def file_read_iterator(fp, size=1024):
    while True:
        data = fp.read(size)
        if not data:
            break
        yield data


class HashMap(list):

    def __init__(self, blocksize, blockhash):
        super(HashMap, self).__init__()
        self.blocksize = blocksize
        self.blockhash = blockhash

    def _hash_raw(self, v):
        h = hashlib.new(self.blockhash)
        h.update(v)
        return h.digest()

    def _hash_block(self, v):
        return self._hash_raw(v.rstrip('\x00'))

    def hash(self):
        if len(self) == 0:
            return self._hash_raw('')
        if len(self) == 1:
            return self.__getitem__(0)

        h = list(self)
        s = 2
        while s < len(h):
            s = s * 2
        h += [('\x00' * len(h[0]))] * (s - len(h))
        while len(h) > 1:
            h = [self._hash_raw(h[x] + h[x + 1]) for x in range(0, len(h), 2)]
        return h[0]

    def load(self, data):
        self.size = 0
        fp = StringIO(data)
        for block in file_read_iterator(fp, self.blocksize):
            self.append(self._hash_block(block))
            self.size += len(block)


def merkle(data, blocksize, blockhash):
    hashes = HashMap(blocksize, blockhash)
    hashes.load(data)
    return hexlify(hashes.hash())


284
285
# --------------------------------------------------------------------
# BurninTests class
286
# pylint: disable=too-many-public-methods
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
287
288
class BurninTests(unittest.TestCase):
    """Common class that all burnin tests should implement"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
289
    clients = Clients()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
290
    run_id = None
291
    ignore_ssl = False
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
292
293
294
295
    use_ipv6 = None
    action_timeout = None
    action_warning = None
    query_interval = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
296
    system_user = None
297
298
    images = None
    flavors = None
299
    delete_stale = False
300
    temp_directory = None
301
    failfast = None
302
    temp_containers = []
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
303

304
    quotas = Proper(value=None)
305
    uuid = Proper(value=None)
306

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
307
308
309
310
311
312
313
314
315
    @classmethod
    def setUpClass(cls):  # noqa
        """Initialize BurninTests"""
        cls.suite_name = cls.__name__
        logger.testsuite_start(cls.suite_name)

        # Set test parameters
        cls.longMessage = True

316
    def test_000_clients_setup(self):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
317
318
        """Initializing astakos/cyclades/pithos clients"""
        # Update class attributes
319
        self.clients.initialize_clients(ignore_ssl=self.ignore_ssl)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
320
321
        self.info("Astakos auth url is %s", self.clients.auth_url)
        self.info("Cyclades url is %s", self.clients.compute_url)
322
        self.info("Network url is %s", self.clients.network_url)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
323
        self.info("Pithos url is %s", self.clients.pithos_url)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
324
        self.info("Image url is %s", self.clients.image_url)
325
326

        self.quotas = self._get_quotas()
327
        for puuid, quotas in self.quotas.items():
328
            project_name = self._get_project_name(puuid)
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
            self.info("  Project %s:", project_name)
            self.info("    Disk usage is         %s bytes",
                      quotas['cyclades.disk']['usage'])
            self.info("    VM usage is           %s",
                      quotas['cyclades.vm']['usage'])
            self.info("    DiskSpace usage is    %s bytes",
                      quotas['pithos.diskspace']['usage'])
            self.info("    Ram usage is          %s bytes",
                      quotas['cyclades.ram']['usage'])
            self.info("    Floating IPs usage is %s",
                      quotas['cyclades.floating_ip']['usage'])
            self.info("    CPU usage is          %s",
                      quotas['cyclades.cpu']['usage'])
            self.info("    Network usage is      %s",
                      quotas['cyclades.network.private']['usage'])
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
344

345
346
    def _run_tests(self, tcases):
        """Run some generated testcases"""
347
        global success  # pylint: disable=invalid-name, global-statement
348
349
350
351
352
353
354

        for tcase in tcases:
            self.info("Running testsuite %s", tcase.__name__)
            success = run_test(tcase) and success
            if self.failfast and not success:
                break

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
355
356
    # ----------------------------------
    # Loggers helper functions
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
    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)
376
        self.fail(msg % args)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
377

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
378
379
380
381
    # ----------------------------------
    # Helper functions that every testsuite may need
    def _get_uuid(self):
        """Get our uuid"""
382
383
384
385
386
        if self.uuid is None:
            authenticate = self.clients.astakos.authenticate()
            self.uuid = authenticate['access']['user']['id']
            self.info("User's uuid is %s", self.uuid)
        return self.uuid
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
387
388
389
390
391
392
393
394

    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
395
    def _create_tmp_directory(self):
396
397
        """Create a tmp directory"""
        temp_dir = tempfile.mkdtemp(dir=self.temp_directory)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
398
399
400
401
402
403
404
405
406
407
408
        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

409
410
411
412
413
414
415
416
417
418
419
420
    def _create_large_file(self, size):
        """Create a large file at fs"""
        named_file = NamedTemporaryFile()
        seg = size / 8
        self.debug('Create file %s  ', named_file.name)
        for sbytes in [b * seg for b in range(size / seg)]:
            named_file.seek(sbytes)
            named_file.write(urandom(seg))
            named_file.flush()
        named_file.seek(0)
        return named_file

421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
    def _create_file(self, size):
        """Create a file and compute its merkle hash"""

        tmp_file = NamedTemporaryFile()
        self.debug('\tCreate file %s  ' % tmp_file.name)
        meta = self.clients.pithos.get_container_info()
        block_size = int(meta['x-container-block-size'])
        block_hash_algorithm = meta['x-container-block-hash']
        num_of_blocks = size / block_size
        hashmap = HashMap(block_size, block_hash_algorithm)
        s = 0
        for i in range(num_of_blocks):
            seg = urandom(block_size)
            tmp_file.write(seg)
            hashmap.load(seg)
            s += len(seg)
        else:
            rest = size - s
            if rest:
                seg = urandom(rest)
                tmp_file.write(seg)
                hashmap.load(seg)
                s += len(seg)
        tmp_file.seek(0)
        tmp_file.hash = hexlify(hashmap.hash())
        return tmp_file

448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
    def _create_boring_file(self, num_of_blocks):
        """Create a file with some blocks being the same"""

        def chargen():
            """10 + 2 * 26 + 26 = 88"""
            while True:
                for char in xrange(10):
                    yield '%s' % char
                for char in ascii_letters:
                    yield char
                for char in '~!@#$%^&*()_+`-=:";|<>?,./':
                    yield char

        tmp_file = NamedTemporaryFile()
        self.debug('\tCreate file %s  ' % tmp_file.name)
        block_size = 4 * 1024 * 1024
        chars = chargen()
        while num_of_blocks:
            fslice = 3 if num_of_blocks > 3 else num_of_blocks
            tmp_file.write(fslice * block_size * chars.next())
            num_of_blocks -= fslice
        tmp_file.seek(0)
        return tmp_file

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
472
473
474
475
476
477
478
479
480
    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:
481
482
            try:
                su_type, su_value = parse_typed_option(self.system_user)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
483
484
485
486
487
488
489
                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)
490
491
492
            except ValueError:
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
                self.warning(msg, self.system_user)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
493
494
495
496

        if system_users is None:
            system_users = SYSTEM_USERS

497
        uuids = self.clients.astakos.get_uuids(system_users)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
498
499
500
501
502
503
504
505
506
        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

507
508
509
510
511
512
    def _skip_if(self, condition, msg):
        """Skip tests"""
        if condition:
            self.info("Test skipped: %s" % msg)
            self.skipTest(msg)

513
514
515
516
517
518
    def _skip_suite_if(self, condition, msg):
        """Skip the whole testsuite"""
        if condition:
            self.info("TestSuite skipped: %s" % msg)
            self.skipTest("__SkipClass__: %s" % msg)

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
519
520
    # ----------------------------------
    # Flavors
521
522
523
524
525
526
527
528
529
    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

530
531
532
533
534
535
536
    def _find_flavors(self, patterns, flavors=None):
        """Find a list of suitable flavors to use

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

        """
537
538
539
540
541
542
543
544
545
546
        def _is_true(value):
            """Boolean or string value that represents a bool"""
            if isinstance(value, bool):
                return value
            elif isinstance(value, str):
                return value in ["True", "true"]
            else:
                self.warning("Unrecognized boolean value %s", value)
                return False

547
548
549
550
551
        if flavors is None:
            flavors = self._get_list_of_flavors(detail=True)

        ret_flavors = []
        for ptrn in patterns:
552
553
554
            try:
                flv_type, flv_value = parse_typed_option(ptrn)
            except ValueError:
555
556
557
                msg = "Invalid flavor format: %s. Must be [id|name]:.+"
                self.warning(msg, ptrn)
                continue
558

559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
            if flv_type == "name":
                # Filter flavor by name
                msg = "Trying to find a flavor with name %s"
                self.info(msg, flv_value)
                filtered_flvs = \
                    [f for f in flavors if
                     re.search(flv_value, f['name'], flags=re.I) is not None]
            elif flv_type == "id":
                # Filter flavors by id
                msg = "Trying to find a flavor with id %s"
                self.info(msg, flv_value)
                filtered_flvs = \
                    [f for f in flavors if str(f['id']) == flv_value]
            else:
                self.error("Unrecognized flavor type %s", flv_type)

575
576
577
578
            # Get only flavors that are allowed to create a machine
            filtered_flvs = [f for f in filtered_flvs
                             if _is_true(f['SNF:allow_create'])]

579
580
581
582
583
584
585
            # Append and continue
            ret_flavors.extend(filtered_flvs)

        self.assertGreater(len(ret_flavors), 0,
                           "No matching flavors found")
        return ret_flavors

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
    # ----------------------------------
    # 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
609
610
                      if (i['owner'] == su_uuid or i['owner'] == my_uuid)
                      and not i['is_snapshot']]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
611
612
613

        return ret_images

614
615
    def _find_images(self, patterns, images=None):
        """Find a list of suitable images to use
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
616

617
618
        The patterns is a list of `typed_options'. A list of all images
        matching this patterns will be returned.
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
619
620
621
622
623

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

624
        ret_images = []
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
625
        for ptrn in patterns:
626
627
628
            try:
                img_type, img_value = parse_typed_option(ptrn)
            except ValueError:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
629
630
631
                msg = "Invalid image format: %s. Must be [id|name]:.+"
                self.warning(msg, ptrn)
                continue
632

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
            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)

650
651
            # Append and continue
            ret_images.extend(filtered_imgs)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
652

653
654
655
        self.assertGreater(len(ret_images), 0,
                           "No matching images found")
        return ret_images
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
656
657
658

    # ----------------------------------
    # Pithos
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
659
    def _set_pithos_account(self, account):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
660
        """Set the Pithos account"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
661
662
        assert account, "No pithos account was given"

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
663
        self.info("Setting Pithos account to %s", account)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
664
665
        self.clients.pithos.account = account

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
666
667
668
669
670
671
672
    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
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
    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)
689
690
        self.clients.pithos.create_container(container)
        self.temp_containers.append(container)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
691

692
693
694
695
    # ----------------------------------
    # Quotas
    def _get_quotas(self):
        """Get quotas"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
696
        self.info("Getting quotas")
697
        return dict(self.clients.astakos.get_quotas())
698

699
700
    # pylint: disable=invalid-name
    # pylint: disable=too-many-arguments
701
    def _check_quotas(self, changes):
702
703
        """Check that quotas' changes are consistent

704
        @param changes: A dict of the changes that have been made in quotas
705
706

        """
707
708
709
710
711
712
713
714
715
716
717
        def dicts_are_equal(d1, d2):
            """Helper function to check dict equality"""
            self.assertEqual(set(d1), set(d2))
            for key, val in d1.items():
                if isinstance(val, (list, tuple)):
                    self.assertEqual(set(val), set(d2[key]))
                elif isinstance(val, dict):
                    dicts_are_equal(val, d2[key])
                else:
                    self.assertEqual(val, d2[key])

718
719
        if not changes:
            return
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
720
721
722
723
724
725

        self.info("Check that quotas' changes are consistent")
        old_quotas = self.quotas
        new_quotas = self._get_quotas()
        self.quotas = new_quotas

726
727
        self.assertListEqual(sorted(old_quotas.keys()),
                             sorted(new_quotas.keys()))
728
729
730
731
732
733
734
735
736
737
738
739
740

        # Take old_quotas and apply changes
        for prj, values in changes.items():
            self.assertIn(prj, old_quotas.keys())
            for q_name, q_mult, q_value, q_unit in values:
                if q_unit is None:
                    q_unit = 1
                q_value = q_mult*int(q_value)*q_unit
                assert isinstance(q_value, int), \
                    "Project %s: %s value has to be integer" % (prj, q_name)
                old_quotas[prj][q_name]['usage'] += q_value
                old_quotas[prj][q_name]['project_usage'] += q_value

741
        dicts_are_equal(old_quotas, new_quotas)
742
743
744

    # ----------------------------------
    # Projects
745
    def _get_project_name(self, puuid):
746
        """Get the name of a project"""
747
        uuid = self._get_uuid()
748
749
750
751
752
        if puuid == uuid:
            return "base"
        else:
            project_info = self.clients.astakos.get_project(puuid)
            return project_info['name']
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
753

754
755
756
757
758
759
760
761
762
    def _get_merkle_hash(self, data):
        self.clients.pithos._assert_account()
        meta = self.clients.pithos.get_container_info()
        block_size = int(meta['x-container-block-size'])
        block_hash_algorithm = meta['x-container-block-hash']
        hashes = HashMap(block_size, block_hash_algorithm)
        hashes.load(data)
        return hexlify(hashes.hash())

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
763

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
764
765
# --------------------------------------------------------------------
# Initialize Burnin
766
def initialize(opts, testsuites, stale_testsuites):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
767
768
769
770
771
772
    """Initalize burnin

    Initialize our logger and burnin state

    """
    # Initialize logger
773
    global logger  # pylint: disable=invalid-name, global-statement
774
    curr_time = datetime.datetime.now()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
775
    logger = Log(opts.log_folder, verbose=opts.verbose,
776
                 use_colors=opts.use_colors, in_parallel=False,
777
                 log_level=opts.log_level, curr_time=curr_time)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
778
779
780
781
782
783

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

    # Pass the rest options to BurninTests
784
    BurninTests.ignore_ssl = opts.ignore_ssl
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
785
786
787
788
    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
789
    BurninTests.system_user = opts.system_user
790
791
    BurninTests.flavors = opts.flavors
    BurninTests.images = opts.images
792
    BurninTests.delete_stale = opts.delete_stale
793
    BurninTests.temp_directory = opts.temp_directory
794
    BurninTests.failfast = opts.failfast
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
795
    BurninTests.run_id = SNF_TEST_PREFIX + \
796
        datetime.datetime.strftime(curr_time, "%Y%m%d%H%M%S")
797
798
799
    BurninTests.obj_upload_num = opts.obj_upload_num
    BurninTests.obj_upload_min_size = opts.obj_upload_min_size
    BurninTests.obj_upload_max_size = opts.obj_upload_max_size
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
800
801

    # Choose tests to run
802
803
    if opts.show_stale:
        # We will run the stale_testsuites
804
        return (stale_testsuites, True)
805

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
806
807
808
809
810
811
    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]

812
    return (testsuites, opts.failfast)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
813
814
815
816


# --------------------------------------------------------------------
# Run Burnin
817
def run_burnin(testsuites, failfast=False):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
818
    """Run burnin testsuites"""
819
820
    # pylint: disable=invalid-name,global-statement
    # pylint: disable=global-variable-not-assigned
821
    global logger, success
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
822
823

    success = True
824
    run_tests(testsuites, failfast=failfast)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
825
826

    # Clean up our logger
827
    del logger
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
828
829
830
831
832

    # Return
    return 0 if success else 1


833
834
def run_tests(tcases, failfast=False):
    """Run some testcases"""
835
836
837
    # pylint: disable=invalid-name,global-statement
    # pylint: disable=global-variable-not-assigned
    global success
838
839
840
841
842
843
844
845

    for tcase in tcases:
        was_success = run_test(tcase)
        success = success and was_success
        if failfast and not success:
            break


846
847
848
849
850
851
852
853
def run_test(tcase):
    """Run a testcase"""
    tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
    results = tsuite.run(BurninTestResult())

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


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
854
855
# --------------------------------------------------------------------
# Helper functions
856
def was_successful(tsuite, successful):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
857
    """Handle whether a testsuite was succesful or not"""
858
    if successful:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
859
        logger.testsuite_success(tsuite)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
860
        return True
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
861
862
    else:
        logger.testsuite_failure(tsuite)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
863
        return False
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
864
865
866
867
868
869
870
871
872
873
874
875
876
877


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:
878
        raise