Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.8 kB)

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

    
34
"""
35
Common utils for burnin tests
36

37
"""
38

    
39
import os
40
import re
41
import shutil
42
import unittest
43
import datetime
44
import tempfile
45
import traceback
46

    
47
from kamaki.clients.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

    
62

    
63
# --------------------------------------------------------------------
64
# BurninTestResult class
65
class BurninTestResult(unittest.TestResult):
66
    """Modify the TextTestResult class"""
67
    def __init__(self):
68
        super(BurninTestResult, self).__init__()
69

    
70
        # Test parameters
71
        self.failfast = True
72

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

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

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

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

    
104

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

    
127

    
128
# Too many public methods (45/20). pylint: disable-msg=R0904
129
class BurninTests(unittest.TestCase):
130
    """Common class that all burnin tests should implement"""
131
    clients = Clients()
132
    run_id = None
133
    use_ipv6 = None
134
    action_timeout = None
135
    action_warning = None
136
    query_interval = None
137
    system_user = None
138

    
139
    @classmethod
140
    def setUpClass(cls):  # noqa
141
        """Initialize BurninTests"""
142
        cls.suite_name = cls.__name__
143
        logger.testsuite_start(cls.suite_name)
144

    
145
        # Set test parameters
146
        cls.longMessage = True
147

    
148
    def test_000_clients_setup(self):
149
        """Initializing astakos/cyclades/pithos clients"""
150
        # Update class attributes
151
        self.info("Astakos auth url is %s", self.clients.auth_url)
152
        self.clients.astakos = AstakosClient(
153
            self.clients.auth_url, self.clients.token)
154
        self.clients.astakos.CONNECTION_RETRY_LIMIT = self.clients.retry
155

    
156
        self.clients.compute_url = \
157
            self.clients.astakos.get_service_endpoints('compute')['publicURL']
158
        self.info("Cyclades url is %s", self.clients.compute_url)
159
        self.clients.compute = ComputeClient(
160
            self.clients.compute_url, self.clients.token)
161
        self.clients.compute.CONNECTION_RETRY_LIMIT = self.clients.retry
162

    
163
        self.clients.pithos_url = self.clients.astakos.\
164
            get_service_endpoints('object-store')['publicURL']
165
        self.info("Pithos url is %s", self.clients.pithos_url)
166
        self.clients.pithos = PithosClient(
167
            self.clients.pithos_url, self.clients.token)
168
        self.clients.pithos.CONNECTION_RETRY_LIMIT = self.clients.retry
169

    
170
        self.clients.image_url = \
171
            self.clients.astakos.get_service_endpoints('image')['publicURL']
172
        self.info("Image url is %s", self.clients.image_url)
173
        self.clients.image = ImageClient(
174
            self.clients.image_url, self.clients.token)
175
        self.clients.image.CONNECTION_RETRY_LIMIT = self.clients.retry
176

    
177
    # ----------------------------------
178
    # Loggers helper functions
179
    def log(self, msg, *args):
180
        """Pass the section value to logger"""
181
        logger.log(self.suite_name, msg, *args)
182

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

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

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

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

    
199
    # ----------------------------------
200
    # Helper functions that every testsuite may need
201
    def _get_uuid(self):
202
        """Get our uuid"""
203
        authenticate = self.clients.astakos.authenticate()
204
        uuid = authenticate['access']['user']['id']
205
        self.info("User's uuid is %s", uuid)
206
        return uuid
207

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

    
215
    def _create_tmp_directory(self):
216
        """Create a tmp directory
217

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

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

    
226
    def _remove_tmp_directory(self, tmp_dir):
227
        """Remove a tmp directory"""
228
        try:
229
            shutil.rmtree(tmp_dir)
230
            self.info("Temp directory %s deleted", tmp_dir)
231
        except OSError:
232
            pass
233

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

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

239
        """
240
        self.info("Getting the uuid of the system user")
241
        system_users = None
242
        if self.system_user is not None:
243
            parsed_su = parse_typed_option(self.system_user)
244
            if parsed_su is None:
245
                msg = "Invalid system-user format: %s. Must be [id|name]:.+"
246
                self.warning(msg, self.system_user)
247
            else:
248
                su_type, su_value = parsed_su
249
                if su_type == "name":
250
                    system_users = [su_value]
251
                elif su_type == "id":
252
                    self.info("System user's uuid is %s", su_value)
253
                    return su_value
254
                else:
255
                    self.error("Unrecognized system-user type %s", su_type)
256
                    self.fail("Unrecognized system-user type")
257

    
258
        if system_users is None:
259
            system_users = SYSTEM_USERS
260

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

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

    
271
    # ----------------------------------
272
    # Flavors
273
    def _get_list_of_flavors(self, detail=False):
274
        """Get (detailed) list of flavors"""
275
        if detail:
276
            self.info("Getting detailed list of flavors")
277
        else:
278
            self.info("Getting simple list of flavors")
279
        flavors = self.clients.compute.list_flavors(detail=detail)
280
        return flavors
281

    
282
    # ----------------------------------
283
    # Images
284
    def _get_list_of_images(self, detail=False):
285
        """Get (detailed) list of images"""
286
        if detail:
287
            self.info("Getting detailed list of images")
288
        else:
289
            self.info("Getting simple list of images")
290
        images = self.clients.image.list_public(detail=detail)
291
        # Remove images registered by burnin
292
        images = [img for img in images
293
                  if not img['name'].startswith(SNF_TEST_PREFIX)]
294
        return images
295

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

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

    
307
        return ret_images
308

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

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

315
        """
316
        if images is None:
317
            images = self._get_list_of_sys_images()
318

    
319
        for ptrn in patterns:
320
            parsed_ptrn = parse_typed_option(ptrn)
321
            if parsed_ptrn is None:
322
                msg = "Invalid image format: %s. Must be [id|name]:.+"
323
                self.warning(msg, ptrn)
324
                continue
325
            img_type, img_value = parsed_ptrn
326
            if img_type == "name":
327
                # Filter image by name
328
                msg = "Trying to find an image with name %s"
329
                self.info(msg, img_value)
330
                filtered_imgs = \
331
                    [i for i in images if
332
                     re.search(img_value, i['name'], flags=re.I) is not None]
333
            elif img_type == "id":
334
                # Filter images by id
335
                msg = "Trying to find an image with id %s"
336
                self.info(msg, img_value)
337
                filtered_imgs = \
338
                    [i for i in images if
339
                     i['id'].lower() == img_value.lower()]
340
            else:
341
                self.error("Unrecognized image type %s", img_type)
342
                self.fail("Unrecognized image type")
343

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

    
350
        # We didn't found one
351
        err = "No matching image found"
352
        self.error(err)
353
        self.fail(err)
354

    
355
    # ----------------------------------
356
    # Pithos
357
    def _set_pithos_account(self, account):
358
        """Set the Pithos account"""
359
        assert account, "No pithos account was given"
360

    
361
        self.info("Setting Pithos account to %s", account)
362
        self.clients.pithos.account = account
363

    
364
    def _set_pithos_container(self, container):
365
        """Set the Pithos container"""
366
        assert container, "No pithos container was given"
367

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

    
371
    def _get_list_of_containers(self, account=None):
372
        """Get list of containers"""
373
        if account is not None:
374
            self._set_pithos_account(account)
375
        self.info("Getting list of containers")
376
        return self.clients.pithos.list_containers()
377

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

381
        If the container exists, nothing will happen
382

383
        """
384
        assert container, "No pithos container was given"
385

    
386
        self.info("Creating pithos container %s", container)
387
        self.clients.pithos.container = container
388
        self.clients.pithos.container_put()
389

    
390

    
391
# --------------------------------------------------------------------
392
# Initialize Burnin
393
def initialize(opts, testsuites):
394
    """Initalize burnin
395

396
    Initialize our logger and burnin state
397

398
    """
399
    # Initialize logger
400
    global logger  # Using global statement. pylint: disable-msg=C0103,W0603
401
    logger = Log(opts.log_folder, verbose=opts.verbose,
402
                 use_colors=opts.use_colors, in_parallel=False,
403
                 quiet=opts.quiet)
404

    
405
    # Initialize clients
406
    Clients.auth_url = opts.auth_url
407
    Clients.token = opts.token
408

    
409
    # Pass the rest options to BurninTests
410
    BurninTests.use_ipv6 = opts.use_ipv6
411
    BurninTests.action_timeout = opts.action_timeout
412
    BurninTests.action_warning = opts.action_warning
413
    BurninTests.query_interval = opts.query_interval
414
    BurninTests.system_user = opts.system_user
415
    BurninTests.run_id = SNF_TEST_PREFIX + \
416
        datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d%H%M%S")
417

    
418
    # Choose tests to run
419
    if opts.tests != "all":
420
        testsuites = opts.tests
421
    if opts.exclude_tests is not None:
422
        testsuites = [tsuite for tsuite in testsuites
423
                      if tsuite not in opts.exclude_tests]
424

    
425
    return testsuites
426

    
427

    
428
# --------------------------------------------------------------------
429
# Run Burnin
430
def run(testsuites, failfast=False, final_report=False):
431
    """Run burnin testsuites"""
432
    global logger  # Using global. pylint: disable-msg=C0103,W0603,W0602
433

    
434
    success = True
435
    for tcase in testsuites:
436
        tsuite = unittest.TestLoader().loadTestsFromTestCase(tcase)
437
        results = tsuite.run(BurninTestResult())
438

    
439
        was_success = was_successful(tcase.__name__, results.wasSuccessful())
440
        success = success and was_success
441
        if failfast and not success:
442
            break
443

    
444
    # Are we going to print final report?
445
    if final_report:
446
        logger.print_logfile_to_stdout()
447
    # Clean up our logger
448
    del(logger)
449

    
450
    # Return
451
    return 0 if success else 1
452

    
453

    
454
# --------------------------------------------------------------------
455
# Helper functions
456
def was_successful(tsuite, success):
457
    """Handle whether a testsuite was succesful or not"""
458
    if success:
459
        logger.testsuite_success(tsuite)
460
        return True
461
    else:
462
        logger.testsuite_failure(tsuite)
463
        return False
464

    
465

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

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

471
    """
472
    try:
473
        [type_, val] = value.strip().split(':')
474
        if type_ not in ["id", "name"]:
475
            raise ValueError
476
        return type_, val
477
    except ValueError:
478
        return None
479

    
480

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

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

488
    """
489
    def __init__(self, value=None):
490
        self.val = value
491

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

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