Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / common.py @ f772699c

History | View | Annotate | Download (16.9 kB)

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

    
34
"""
35
Common utils for burnin tests
36

37
"""
38

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

    
54
from kamaki.clients.astakos import AstakosClient
55
from kamaki.clients.compute import ComputeClient
56
from kamaki.clients.pithos import PithosClient
57
from kamaki.clients.image import ImageClient
58

    
59
from synnefo_tools.burnin.logger import Log
60

    
61

    
62
# --------------------------------------------------------------------
63
# Global variables
64
logger = None  # Invalid constant name. pylint: disable-msg=C0103
65
SNF_TEST_PREFIX = "snf-test-"
66
CONNECTION_RETRY_LIMIT = 2
67
SYSTEM_USERS = ["images@okeanos.grnet.gr", "images@demo.synnefo.org"]
68

    
69

    
70
# --------------------------------------------------------------------
71
# BurninTestResult class
72
class BurninTestResult(unittest.TestResult):
73
    """Modify the TextTestResult class"""
74
    def __init__(self):
75
        super(BurninTestResult, self).__init__()
76

    
77
        # Test parameters
78
        self.failfast = True
79

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

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

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

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

    
111

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

    
134

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

    
146
    @classmethod
147
    def setUpClass(cls):  # noqa
148
        """Initialize BurninTests"""
149
        cls.suite_name = cls.__name__
150
        logger.testsuite_start(cls.suite_name)
151

    
152
        # Set test parameters
153
        cls.longMessage = True
154

    
155
    def _setattr(self, attr, value):
156
        """Used by tests to set an attribute to TestCase
157

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

163
        """
164
        setattr(self.__class__, attr, value)
165

    
166
    def test_000_clients_setup(self):
167
        """Initializing astakos/cyclades/pithos clients"""
168
        # Update class attributes
169
        self.info("Astakos auth url is %s", self.clients.auth_url)
170
        self.clients.astakos = AstakosClient(
171
            self.clients.auth_url, self.clients.token)
172
        self.clients.astakos.CONNECTION_RETRY_LIMIT = self.clients.retry
173

    
174
        self.clients.compute_url = \
175
            self.clients.astakos.get_service_endpoints('compute')['publicURL']
176
        self.info("Cyclades url is %s", self.clients.compute_url)
177
        self.clients.compute = ComputeClient(
178
            self.clients.compute_url, self.clients.token)
179
        self.clients.compute.CONNECTION_RETRY_LIMIT = self.clients.retry
180

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

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

    
195
    # ----------------------------------
196
    # Loggers helper functions
197
    def log(self, msg, *args):
198
        """Pass the section value to logger"""
199
        logger.log(self.suite_name, msg, *args)
200

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

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

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

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

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

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

    
233
    def _create_tmp_directory(self):
234
        """Create a tmp directory
235

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

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

    
244
    def _remove_tmp_directory(self, tmp_dir):
245
        """Remove a tmp directory"""
246
        try:
247
            shutil.rmtree(tmp_dir)
248
            self.info("Temp directory %s deleted", tmp_dir)
249
        except OSError:
250
            pass
251

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

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

257
        """
258
        self.info("Getting the uuid of the system user")
259
        system_users = None
260
        if self.system_user is not None:
261
            parsed_su = parse_typed_option(self.system_user)
262
            if parsed_su is None:
263
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
264
                self.warning(msg, self.system_user)
265
            else:
266
                su_type, su_value = parsed_su
267
                if su_type == "name":
268
                    system_users = [su_value]
269
                elif su_type == "id":
270
                    self.info("System user's uuid is %s", su_value)
271
                    return su_value
272
                else:
273
                    self.error("Unrecognized system-user type %s", su_type)
274
                    self.fail("Unrecognized system-user type")
275

    
276
        if system_users is None:
277
            system_users = SYSTEM_USERS
278

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

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

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

    
300
    # ----------------------------------
301
    # Images
302
    def _get_list_of_images(self, detail=False):
303
        """Get (detailed) list of images"""
304
        if detail:
305
            self.info("Getting detailed list of images")
306
        else:
307
            self.info("Getting simple list of images")
308
        images = self.clients.image.list_public(detail=detail)
309
        # Remove images registered by burnin
310
        images = [img for img in images
311
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
312
        return images
313

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

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

    
325
        return ret_images
326

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

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

333
        """
334
        if images is None:
335
            images = self._get_list_of_sys_images()
336

    
337
        for ptrn in patterns:
338
            parsed_ptrn = parse_typed_option(ptrn)
339
            if parsed_ptrn is None:
340
                msg = "Invalid image format: %s. Must be [id|name]:.+"
341
                self.warning(msg, ptrn)
342
                continue
343
            img_type, img_value = parsed_ptrn
344
            if img_type == "name":
345
                # Filter image by name
346
                msg = "Trying to find an image with name %s"
347
                self.info(msg, img_value)
348
                filtered_imgs = \
349
                    [i for i in images if
350
                     re.search(img_value, i['name'], flags=re.I) is not None]
351
            elif img_type == "id":
352
                # Filter images by id
353
                msg = "Trying to find an image with id %s"
354
                self.info(msg, img_value)
355
                filtered_imgs = \
356
                    [i for i in images if
357
                     i['id'].lower() == img_value.lower()]
358
            else:
359
                self.error("Unrecognized image type %s", img_type)
360
                self.fail("Unrecognized image type")
361

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

    
368
        # We didn't found one
369
        err = "No matching image found"
370
        self.error(err)
371
        self.fail(err)
372

    
373
    # ----------------------------------
374
    # Pithos
375
    def _set_pithos_account(self, account):
376
        """Set the Pithos account"""
377
        assert account, "No pithos account was given"
378

    
379
        self.info("Setting Pithos account to %s", account)
380
        self.clients.pithos.account = account
381

    
382
    def _set_pithos_container(self, container):
383
        """Set the Pithos container"""
384
        assert container, "No pithos container was given"
385

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

    
389
    def _get_list_of_containers(self, account=None):
390
        """Get list of containers"""
391
        if account is not None:
392
            self._set_pithos_account(account)
393
        self.info("Getting list of containers")
394
        return self.clients.pithos.list_containers()
395

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

399
        If the container exists, nothing will happen
400

401
        """
402
        assert container, "No pithos container was given"
403

    
404
        self.info("Creating pithos container %s", container)
405
        self.clients.pithos.container = container
406
        self.clients.pithos.container_put()
407

    
408

    
409
# --------------------------------------------------------------------
410
# Initialize Burnin
411
def initialize(opts, testsuites):
412
    """Initalize burnin
413

414
    Initialize our logger and burnin state
415

416
    """
417
    # Initialize logger
418
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
419
    logger = Log(opts.log_folder, verbose=opts.verbose,
420
                 use_colors=opts.use_colors, in_parallel=False,
421
                 quiet=opts.quiet)
422

    
423
    # Initialize clients
424
    Clients.auth_url = opts.auth_url
425
    Clients.token = opts.token
426

    
427
    # Pass the rest options to BurninTests
428
    BurninTests.use_ipv6 = opts.use_ipv6
429
    BurninTests.action_timeout = opts.action_timeout
430
    BurninTests.action_warning = opts.action_warning
431
    BurninTests.query_interval = opts.query_interval
432
    BurninTests.system_user = opts.system_user
433
    BurninTests.run_id = SNF_TEST_PREFIX + \
434
        datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d%H%M%S")
435

    
436
    # Choose tests to run
437
    if opts.tests != "all":
438
        testsuites = opts.tests
439
    if opts.exclude_tests is not None:
440
        testsuites = [tsuite for tsuite in testsuites
441
                      if tsuite not in opts.exclude_tests]
442

    
443
    return testsuites
444

    
445

    
446
# --------------------------------------------------------------------
447
# Run Burnin
448
def run(testsuites, failfast=False, final_report=False):
449
    """Run burnin testsuites"""
450
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
451

    
452
    success = True
453
    for tcase in testsuites:
454
        tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
455
        results = tsuite.run(BurninTestResult())
456

    
457
        was_success = was_successful(tcase.__name__, results.wasSuccessful())
458
        success = success and was_success
459
        if failfast and not success:
460
            break
461

    
462
    # Are we going to print final report?
463
    if final_report:
464
        logger.print_logfile_to_stdout()
465
    # Clean up our logger
466
    del(logger)
467

    
468
    # Return
469
    return 0 if success else 1
470

    
471

    
472
# --------------------------------------------------------------------
473
# Helper functions
474
def was_successful(tsuite, success):
475
    """Handle whether a testsuite was succesful or not"""
476
    if success:
477
        logger.testsuite_success(tsuite)
478
        return True
479
    else:
480
        logger.testsuite_failure(tsuite)
481
        return False
482

    
483

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

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

489
    """
490
    try:
491
        [type_, val] = value.strip().split(':')
492
        if type_ not in ["id", "name"]:
493
            raise ValueError
494
        return type_, val
495
    except ValueError:
496
        return None