Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin.py @ 4fed2379

History | View | Annotate | Download (78.7 kB)

1
#!/usr/bin/env python
2

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

    
36
"""Perform integration testing on a running Synnefo deployment"""
37

    
38
#import __main__
39
import datetime
40
import inspect
41
import logging
42
import os
43
import os.path
44
import paramiko
45
import prctl
46
import subprocess
47
import signal
48
import socket
49
import sys
50
import time
51
import tempfile
52
from base64 import b64encode
53
from IPy import IP
54
from multiprocessing import Process, Queue
55
from random import choice, randint
56
from optparse import OptionParser, OptionValueError
57

    
58
from kamaki.clients.compute import ComputeClient
59
from kamaki.clients.cyclades import CycladesClient
60
from kamaki.clients.image import ImageClient
61
from kamaki.clients.pithos import PithosClient
62
from kamaki.clients.astakos import AstakosClient
63
from kamaki.clients import ClientError
64

    
65
from vncauthproxy.d3des import generate_response as d3des_generate_response
66

    
67
# Use backported unittest functionality if Python < 2.7
68
try:
69
    import unittest2 as unittest
70
except ImportError:
71
    if sys.version_info < (2, 7):
72
        raise Exception("The unittest2 package is required for Python < 2.7")
73
    import unittest
74

    
75
# --------------------------------------------------------------------
76
# Global Variables
77
API = None
78
TOKEN = None
79
PLANKTON = None
80
PLANKTON_USER = None
81
PITHOS = None
82
ASTAKOS = None
83
NO_IPV6 = None
84
DEFAULT_PLANKTON_USER = "images@okeanos.grnet.gr"
85
NOFAILFAST = None
86
VERBOSE = None
87

    
88
# A unique id identifying this test run
89
TEST_RUN_ID = datetime.datetime.strftime(datetime.datetime.now(),
90
                                         "%Y%m%d%H%M%S")
91
SNF_TEST_PREFIX = "snf-test-"
92

    
93
red = '\x1b[31m'
94
yellow = '\x1b[33m'
95
green = '\x1b[32m'
96
normal = '\x1b[0m'
97

    
98

    
99
# --------------------------------------------------------------------
100
# Global functions
101
def _ssh_execute(hostip, username, password, command):
102
    """Execute a command via ssh"""
103
    ssh = paramiko.SSHClient()
104
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
105
    try:
106
        ssh.connect(hostip, username=username, password=password)
107
    except socket.error, err:
108
        raise AssertionError(err)
109
    try:
110
        stdin, stdout, stderr = ssh.exec_command(command)
111
    except paramiko.SSHException, err:
112
        raise AssertionError(err)
113
    status = stdout.channel.recv_exit_status()
114
    output = stdout.readlines()
115
    ssh.close()
116
    return output, status
117

    
118

    
119
def _get_user_id():
120
    """Authenticate to astakos and get unique users id"""
121
    astakos = AstakosClient(ASTAKOS, TOKEN)
122
    authenticate = astakos.authenticate()
123
    if 'uuid' in authenticate:
124
        return authenticate['uuid']
125
    else:
126
        return authenticate['uniq']
127

    
128

    
129
# --------------------------------------------------------------------
130
# BurninTestReulst class
131
class BurninTestResult(unittest.TextTestResult):
132
    def addSuccess(self, test):
133
        super(BurninTestResult, self).addSuccess(test)
134
        if self.showAll:
135
            if hasattr(test, 'result_dict'):
136
                run_details = test.result_dict
137

    
138
                self.stream.write("\n")
139
                for i in run_details:
140
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
141
                self.stream.write("\n")
142

    
143
        elif self.dots:
144
            self.stream.write('.')
145
            self.stream.flush()
146

    
147
    def addError(self, test, err):
148
        super(BurninTestResult, self).addError(test, err)
149
        if self.showAll:
150
            self.stream.writeln("ERROR")
151
            if hasattr(test, 'result_dict'):
152
                run_details = test.result_dict
153

    
154
                self.stream.write("\n")
155
                for i in run_details:
156
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
157
                self.stream.write("\n")
158

    
159
        elif self.dots:
160
            self.stream.write('E')
161
            self.stream.flush()
162

    
163
    def addFailure(self, test, err):
164
        super(BurninTestResult, self).addFailure(test, err)
165
        if self.showAll:
166
            self.stream.writeln("FAIL")
167
            if hasattr(test, 'result_dict'):
168
                run_details = test.result_dict
169

    
170
                self.stream.write("\n")
171
                for i in run_details:
172
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
173
                self.stream.write("\n")
174

    
175
        elif self.dots:
176
            self.stream.write('F')
177
            self.stream.flush()
178

    
179

    
180
# --------------------------------------------------------------------
181
# Format Results
182
class burninFormatter(logging.Formatter):
183
    err_fmt = red + "ERROR: %(msg)s" + normal
184
    dbg_fmt = green + "* %(msg)s" + normal
185
    info_fmt = "%(msg)s"
186

    
187
    def __init__(self, fmt="%(levelno)s: %(msg)s"):
188
        logging.Formatter.__init__(self, fmt)
189

    
190
    def format(self, record):
191
        format_orig = self._fmt
192
        # Replace the original format with one customized by logging level
193
        if record.levelno == 10:    # DEBUG
194
            self._fmt = burninFormatter.dbg_fmt
195
        elif record.levelno == 20:  # INFO
196
            self._fmt = burninFormatter.info_fmt
197
        elif record.levelno == 40:  # ERROR
198
            self._fmt = burninFormatter.err_fmt
199
        result = logging.Formatter.format(self, record)
200
        self._fmt = format_orig
201
        return result
202

    
203
log = logging.getLogger("burnin")
204
log.setLevel(logging.DEBUG)
205
handler = logging.StreamHandler()
206
handler.setFormatter(burninFormatter())
207
log.addHandler(handler)
208

    
209

    
210
# --------------------------------------------------------------------
211
# UnauthorizedTestCase class
212
class UnauthorizedTestCase(unittest.TestCase):
213
    """Test unauthorized access"""
214
    @classmethod
215
    def setUpClass(cls):
216
        cls.result_dict = dict()
217

    
218
    def test_unauthorized_access(self):
219
        """Test access without a valid token fails"""
220
        log.info("Authentication test")
221
        falseToken = '12345'
222
        c = ComputeClient(API, falseToken)
223

    
224
        with self.assertRaises(ClientError) as cm:
225
            c.list_servers()
226
            self.assertEqual(cm.exception.status, 401)
227

    
228

    
229
# --------------------------------------------------------------------
230
# This class gest replicated into Images TestCases dynamically
231
class ImagesTestCase(unittest.TestCase):
232
    """Test image lists for consistency"""
233
    @classmethod
234
    def setUpClass(cls):
235
        """Initialize kamaki, get (detailed) list of images"""
236
        log.info("Getting simple and detailed list of images")
237
        cls.client = ComputeClient(API, TOKEN)
238
        cls.plankton = ImageClient(PLANKTON, TOKEN)
239
        cls.images = \
240
            filter(lambda x: not x['name'].startswith(SNF_TEST_PREFIX),
241
                   cls.plankton.list_public())
242
        cls.dimages = \
243
            filter(lambda x: not x['name'].startswith(SNF_TEST_PREFIX),
244
                   cls.plankton.list_public(detail=True))
245
        cls.result_dict = dict()
246
        # Get uniq user id
247
        cls.uuid = _get_user_id()
248
        log.info("Uniq user id = %s" % cls.uuid)
249
        # Create temp directory and store it inside our class
250
        # XXX: In my machine /tmp has not enough space
251
        #      so use current directory to be sure.
252
        cls.temp_dir = tempfile.mkdtemp(dir=os.getcwd())
253
        cls.temp_image_name = \
254
            SNF_TEST_PREFIX + cls.imageid + ".diskdump"
255

    
256
    @classmethod
257
    def tearDownClass(cls):
258
        """Remove local files"""
259
        try:
260
            temp_file = os.path.join(cls.temp_dir, cls.temp_image_name)
261
            os.unlink(temp_file)
262
        except:
263
            pass
264
        try:
265
            os.rmdir(cls.temp_dir)
266
        except:
267
            pass
268

    
269
    def test_001_list_images(self):
270
        """Test image list actually returns images"""
271
        self.assertGreater(len(self.images), 0)
272

    
273
    def test_002_list_images_detailed(self):
274
        """Test detailed image list is the same length as list"""
275
        self.assertEqual(len(self.dimages), len(self.images))
276

    
277
    def test_003_same_image_names(self):
278
        """Test detailed and simple image list contain same names"""
279
        names = sorted(map(lambda x: x["name"], self.images))
280
        dnames = sorted(map(lambda x: x["name"], self.dimages))
281
        self.assertEqual(names, dnames)
282

    
283
    def test_004_unique_image_names(self):
284
        """Test system images have unique names"""
285
        sys_images = filter(lambda x: x['owner'] == PLANKTON_USER,
286
                            self.dimages)
287
        names = sorted(map(lambda x: x["name"], sys_images))
288
        self.assertEqual(sorted(list(set(names))), names)
289

    
290
    def test_005_image_metadata(self):
291
        """Test every image has specific metadata defined"""
292
        keys = frozenset(["osfamily", "root_partition"])
293
        details = self.client.list_images(detail=True)
294
        for i in details:
295
            self.assertTrue(keys.issubset(i["metadata"].keys()))
296

    
297
    def test_006_download_image(self):
298
        """Download image from pithos+"""
299
        # Get image location
300
        image = filter(
301
            lambda x: x['id'] == self.imageid, self.dimages)[0]
302
        image_location = \
303
            image['location'].replace("://", " ").replace("/", " ").split()
304
        log.info("Download image, with owner %s\n\tcontainer %s, and name %s"
305
                 % (image_location[1], image_location[2], image_location[3]))
306
        pithos_client = PithosClient(PITHOS, TOKEN, image_location[1])
307
        pithos_client.container = image_location[2]
308
        temp_file = os.path.join(self.temp_dir, self.temp_image_name)
309
        with open(temp_file, "wb+") as f:
310
            pithos_client.download_object(image_location[3], f)
311

    
312
    def test_007_upload_image(self):
313
        """Upload and register image"""
314
        temp_file = os.path.join(self.temp_dir, self.temp_image_name)
315
        log.info("Upload image to pithos+")
316
        # Create container `images'
317
        pithos_client = PithosClient(PITHOS, TOKEN, self.uuid)
318
        pithos_client.container = "images"
319
        pithos_client.container_put()
320
        with open(temp_file, "rb+") as f:
321
            pithos_client.upload_object(self.temp_image_name, f)
322
        log.info("Register image to plankton")
323
        location = "pithos://" + self.uuid + \
324
            "/images/" + self.temp_image_name
325
        params = {'is_public': True}
326
        properties = {'OSFAMILY': "linux", 'ROOT_PARTITION': 1}
327
        self.plankton.register(self.temp_image_name, location,
328
                               params, properties)
329
        # Get image id
330
        details = self.plankton.list_public(detail=True)
331
        detail = filter(lambda x: x['location'] == location, details)
332
        self.assertEqual(len(detail), 1)
333
        cls = type(self)
334
        cls.temp_image_id = detail[0]['id']
335
        log.info("Image registered with id %s" % detail[0]['id'])
336

    
337
    def test_008_cleanup_image(self):
338
        """Cleanup image test"""
339
        log.info("Cleanup image test")
340
        # Remove image from pithos+
341
        pithos_client = PithosClient(PITHOS, TOKEN, self.uuid)
342
        pithos_client.container = "images"
343
        pithos_client.del_object(self.temp_image_name)
344

    
345

    
346
# --------------------------------------------------------------------
347
# FlavorsTestCase class
348
class FlavorsTestCase(unittest.TestCase):
349
    """Test flavor lists for consistency"""
350
    @classmethod
351
    def setUpClass(cls):
352
        """Initialize kamaki, get (detailed) list of flavors"""
353
        log.info("Getting simple and detailed list of flavors")
354
        cls.client = ComputeClient(API, TOKEN)
355
        cls.flavors = cls.client.list_flavors()
356
        cls.dflavors = cls.client.list_flavors(detail=True)
357
        cls.result_dict = dict()
358

    
359
    def test_001_list_flavors(self):
360
        """Test flavor list actually returns flavors"""
361
        self.assertGreater(len(self.flavors), 0)
362

    
363
    def test_002_list_flavors_detailed(self):
364
        """Test detailed flavor list is the same length as list"""
365
        self.assertEquals(len(self.dflavors), len(self.flavors))
366

    
367
    def test_003_same_flavor_names(self):
368
        """Test detailed and simple flavor list contain same names"""
369
        names = sorted(map(lambda x: x["name"], self.flavors))
370
        dnames = sorted(map(lambda x: x["name"], self.dflavors))
371
        self.assertEqual(names, dnames)
372

    
373
    def test_004_unique_flavor_names(self):
374
        """Test flavors have unique names"""
375
        names = sorted(map(lambda x: x["name"], self.flavors))
376
        self.assertEqual(sorted(list(set(names))), names)
377

    
378
    def test_005_well_formed_flavor_names(self):
379
        """Test flavors have names of the form CxxRyyDzz
380
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
381
        """
382
        for f in self.dflavors:
383
            flavor = (f["cpu"], f["ram"], f["disk"], f["SNF:disk_template"])
384
            self.assertEqual("C%dR%dD%d%s" % flavor,
385
                             f["name"],
386
                             "Flavor %s does not match its specs." % f["name"])
387

    
388

    
389
# --------------------------------------------------------------------
390
# ServersTestCase class
391
class ServersTestCase(unittest.TestCase):
392
    """Test server lists for consistency"""
393
    @classmethod
394
    def setUpClass(cls):
395
        """Initialize kamaki, get (detailed) list of servers"""
396
        log.info("Getting simple and detailed list of servers")
397

    
398
        cls.client = ComputeClient(API, TOKEN)
399
        cls.servers = cls.client.list_servers()
400
        cls.dservers = cls.client.list_servers(detail=True)
401
        cls.result_dict = dict()
402

    
403
    # def test_001_list_servers(self):
404
    #     """Test server list actually returns servers"""
405
    #     self.assertGreater(len(self.servers), 0)
406

    
407
    def test_002_list_servers_detailed(self):
408
        """Test detailed server list is the same length as list"""
409
        self.assertEqual(len(self.dservers), len(self.servers))
410

    
411
    def test_003_same_server_names(self):
412
        """Test detailed and simple flavor list contain same names"""
413
        names = sorted(map(lambda x: x["name"], self.servers))
414
        dnames = sorted(map(lambda x: x["name"], self.dservers))
415
        self.assertEqual(names, dnames)
416

    
417

    
418
# --------------------------------------------------------------------
419
# Pithos Test Cases
420
class PithosTestCase(unittest.TestCase):
421
    """Test pithos functionality"""
422
    @classmethod
423
    def setUpClass(cls):
424
        """Initialize kamaki, get list of containers"""
425
        # Get uniq user id
426
        cls.uuid = _get_user_id()
427
        log.info("Uniq user id = %s" % cls.uuid)
428
        log.info("Getting list of containers")
429
        cls.client = PithosClient(PITHOS, TOKEN, cls.uuid)
430
        cls.containers = cls.client.list_containers()
431
        cls.result_dict = dict()
432

    
433
    def test_001_list_containers(self):
434
        """Test container list actually returns containers"""
435
        self.assertGreater(len(self.containers), 0)
436

    
437
    def test_002_unique_containers(self):
438
        """Test if containers have unique names"""
439
        names = [n['name'] for n in self.containers]
440
        names = sorted(names)
441
        self.assertEqual(sorted(list(set(names))), names)
442

    
443
    def test_003_create_container(self):
444
        """Test create a container"""
445
        rand_num = randint(1000, 9999)
446
        rand_name = "%s%s" % (SNF_TEST_PREFIX, rand_num)
447
        names = [n['name'] for n in self.containers]
448
        while rand_name in names:
449
            rand_num = randint(1000, 9999)
450
            rand_name = "%s%s" % (SNF_TEST_PREFIX, rand_num)
451
        # Create container
452
        self.client.container = rand_name
453
        self.client.container_put()
454
        # Get list of containers
455
        new_containers = self.client.list_containers()
456
        new_container_names = [n['name'] for n in new_containers]
457
        self.assertIn(rand_name, new_container_names)
458

    
459
    def test_004_upload(self):
460
        """Test uploading something to pithos+"""
461
        # Create a tmp file
462
        with tempfile.TemporaryFile() as f:
463
            f.write("This is a temp file")
464
            f.seek(0, 0)
465
            # Where to save file
466
            self.client.upload_object("test.txt", f)
467

    
468
    def test_005_download(self):
469
        """Test download something from pithos+"""
470
        # Create tmp directory to save file
471
        tmp_dir = tempfile.mkdtemp()
472
        tmp_file = os.path.join(tmp_dir, "test.txt")
473
        with open(tmp_file, "wb+") as f:
474
            self.client.download_object("test.txt", f)
475
            # Read file
476
            f.seek(0, 0)
477
            content = f.read()
478
        # Remove files
479
        os.unlink(tmp_file)
480
        os.rmdir(tmp_dir)
481
        # Compare results
482
        self.assertEqual(content, "This is a temp file")
483

    
484
    def test_006_remove(self):
485
        """Test removing files and containers"""
486
        cont_name = self.client.container
487
        self.client.del_object("test.txt")
488
        self.client.purge_container()
489
        # List containers
490
        containers = self.client.list_containers()
491
        cont_names = [n['name'] for n in containers]
492
        self.assertNotIn(cont_name, cont_names)
493

    
494

    
495
# --------------------------------------------------------------------
496
# This class gets replicated into actual TestCases dynamically
497
class SpawnServerTestCase(unittest.TestCase):
498
    """Test scenario for server of the specified image"""
499
    @classmethod
500
    def setUpClass(cls):
501
        """Initialize a kamaki instance"""
502
        log.info("Spawning server for image `%s'" % cls.imagename)
503
        cls.client = ComputeClient(API, TOKEN)
504
        cls.cyclades = CycladesClient(API, TOKEN)
505
        cls.result_dict = dict()
506

    
507
    def _get_ipv4(self, server):
508
        """Get the public IPv4 of a server from the detailed server info"""
509

    
510
        nics = server["attachments"]
511

    
512
        for nic in nics:
513
            net_id = nic["network_id"]
514
            if self.cyclades.get_network_details(net_id)["public"]:
515
                public_addrs = nic["ipv4"]
516

    
517
        self.assertTrue(public_addrs is not None)
518

    
519
        return public_addrs
520

    
521
    def _get_ipv6(self, server):
522
        """Get the public IPv6 of a server from the detailed server info"""
523

    
524
        nics = server["attachments"]
525

    
526
        for nic in nics:
527
            net_id = nic["network_id"]
528
            if self.cyclades.get_network_details(net_id)["public"]:
529
                public_addrs = nic["ipv6"]
530

    
531
        self.assertTrue(public_addrs is not None)
532

    
533
        return public_addrs
534

    
535
    def _connect_loginname(self, os_value):
536
        """Return the login name for connections based on the server OS"""
537
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
538
            return "user"
539
        elif os_value in ("windows", "windows_alpha1"):
540
            return "Administrator"
541
        else:
542
            return "root"
543

    
544
    def _verify_server_status(self, current_status, new_status):
545
        """Verify a server has switched to a specified status"""
546
        server = self.client.get_server_details(self.serverid)
547
        if server["status"] not in (current_status, new_status):
548
            return None  # Do not raise exception, return so the test fails
549
        self.assertEquals(server["status"], new_status)
550

    
551
    def _get_connected_tcp_socket(self, family, host, port):
552
        """Get a connected socket from the specified family to host:port"""
553
        sock = None
554
        for res in \
555
            socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
556
                               socket.AI_PASSIVE):
557
            af, socktype, proto, canonname, sa = res
558
            try:
559
                sock = socket.socket(af, socktype, proto)
560
            except socket.error:
561
                sock = None
562
                continue
563
            try:
564
                sock.connect(sa)
565
            except socket.error:
566
                sock.close()
567
                sock = None
568
                continue
569
        self.assertIsNotNone(sock)
570
        return sock
571

    
572
    def _ping_once(self, ipv6, ip):
573
        """Test server responds to a single IPv4 or IPv6 ping"""
574
        cmd = "ping%s -c 2 -w 3 %s" % ("6" if ipv6 else "", ip)
575
        ping = subprocess.Popen(cmd, shell=True,
576
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
577
        (stdout, stderr) = ping.communicate()
578
        ret = ping.wait()
579
        self.assertEquals(ret, 0)
580

    
581
    def _get_hostname_over_ssh(self, hostip, username, password):
582
        lines, status = _ssh_execute(
583
            hostip, username, password, "hostname")
584
        self.assertEqual(len(lines), 1)
585
        return lines[0]
586

    
587
    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
588
                                   opmsg, callable, *args, **kwargs):
589
        if warn_timeout == fail_timeout:
590
            warn_timeout = fail_timeout + 1
591
        warn_tmout = time.time() + warn_timeout
592
        fail_tmout = time.time() + fail_timeout
593
        while True:
594
            self.assertLess(time.time(), fail_tmout,
595
                            "operation `%s' timed out" % opmsg)
596
            if time.time() > warn_tmout:
597
                log.warning("Server %d: `%s' operation `%s' not done yet",
598
                            self.serverid, self.servername, opmsg)
599
            try:
600
                log.info("%s... " % opmsg)
601
                return callable(*args, **kwargs)
602
            except AssertionError:
603
                pass
604
            time.sleep(self.query_interval)
605

    
606
    def _insist_on_tcp_connection(self, family, host, port):
607
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
608
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
609
        msg = "connect over %s to %s:%s" % \
610
              (familystr.get(family, "Unknown"), host, port)
611
        sock = self._try_until_timeout_expires(
612
            self.action_timeout, self.action_timeout,
613
            msg, self._get_connected_tcp_socket,
614
            family, host, port)
615
        return sock
616

    
617
    def _insist_on_status_transition(self, current_status, new_status,
618
                                     fail_timeout, warn_timeout=None):
619
        msg = "Server %d: `%s', waiting for %s -> %s" % \
620
              (self.serverid, self.servername, current_status, new_status)
621
        if warn_timeout is None:
622
            warn_timeout = fail_timeout
623
        self._try_until_timeout_expires(warn_timeout, fail_timeout,
624
                                        msg, self._verify_server_status,
625
                                        current_status, new_status)
626
        # Ensure the status is actually the expected one
627
        server = self.client.get_server_details(self.serverid)
628
        self.assertEquals(server["status"], new_status)
629

    
630
    def _insist_on_ssh_hostname(self, hostip, username, password):
631
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
632
        hostname = self._try_until_timeout_expires(
633
            self.action_timeout, self.action_timeout,
634
            msg, self._get_hostname_over_ssh,
635
            hostip, username, password)
636

    
637
        # The hostname must be of the form 'prefix-id'
638
        self.assertTrue(hostname.endswith("-%d\n" % self.serverid))
639

    
640
    def _check_file_through_ssh(self, hostip, username, password,
641
                                remotepath, content):
642
        msg = "Trying file injection through SSH to %s, as %s/%s" % \
643
            (hostip, username, password)
644
        log.info(msg)
645
        try:
646
            ssh = paramiko.SSHClient()
647
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
648
            ssh.connect(hostip, username=username, password=password)
649
            ssh.close()
650
        except socket.error, err:
651
            raise AssertionError(err)
652

    
653
        transport = paramiko.Transport((hostip, 22))
654
        transport.connect(username=username, password=password)
655

    
656
        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
657
        sftp = paramiko.SFTPClient.from_transport(transport)
658
        sftp.get(remotepath, localpath)
659
        sftp.close()
660
        transport.close()
661

    
662
        f = open(localpath)
663
        remote_content = b64encode(f.read())
664

    
665
        # Check if files are the same
666
        return (remote_content == content)
667

    
668
    def _skipIf(self, condition, msg):
669
        if condition:
670
            self.skipTest(msg)
671

    
672
    def test_001_submit_create_server(self):
673
        """Test submit create server request"""
674

    
675
        log.info("Submit new server request")
676
        server = self.client.create_server(self.servername, self.flavorid,
677
                                           self.imageid, self.personality)
678

    
679
        log.info("Server id: " + str(server["id"]))
680
        log.info("Server password: " + server["adminPass"])
681
        self.assertEqual(server["name"], self.servername)
682
        self.assertEqual(server["flavor"], self.flavorid)
683
        self.assertEqual(server["image"], self.imageid)
684
        self.assertEqual(server["status"], "BUILD")
685

    
686
        # Update class attributes to reflect data on building server
687
        cls = type(self)
688
        cls.serverid = server["id"]
689
        cls.username = None
690
        cls.passwd = server["adminPass"]
691

    
692
        self.result_dict["Server ID"] = str(server["id"])
693
        self.result_dict["Password"] = str(server["adminPass"])
694

    
695
    def test_002a_server_is_building_in_list(self):
696
        """Test server is in BUILD state, in server list"""
697
        log.info("Server in BUILD state in server list")
698

    
699
        self.result_dict.clear()
700

    
701
        servers = self.client.list_servers(detail=True)
702
        servers = filter(lambda x: x["name"] == self.servername, servers)
703

    
704
        server = servers[0]
705
        self.assertEqual(server["name"], self.servername)
706
        self.assertEqual(server["flavor"], self.flavorid)
707
        self.assertEqual(server["image"], self.imageid)
708
        self.assertEqual(server["status"], "BUILD")
709

    
710
    def test_002b_server_is_building_in_details(self):
711
        """Test server is in BUILD state, in details"""
712

    
713
        log.info("Server in BUILD state in details")
714

    
715
        server = self.client.get_server_details(self.serverid)
716
        self.assertEqual(server["name"], self.servername)
717
        self.assertEqual(server["flavor"], self.flavorid)
718
        self.assertEqual(server["image"], self.imageid)
719
        self.assertEqual(server["status"], "BUILD")
720

    
721
    def test_002c_set_server_metadata(self):
722

    
723
        log.info("Creating server metadata")
724

    
725
        image = self.client.get_image_details(self.imageid)
726
        os_value = image["metadata"]["os"]
727
        users = image["metadata"].get("users", None)
728
        self.client.update_server_metadata(self.serverid, OS=os_value)
729

    
730
        userlist = users.split()
731

    
732
        # Determine the username to use for future connections
733
        # to this host
734
        cls = type(self)
735

    
736
        if "root" in userlist:
737
            cls.username = "root"
738
        elif users is None:
739
            cls.username = self._connect_loginname(os_value)
740
        else:
741
            cls.username = choice(userlist)
742

    
743
        self.assertIsNotNone(cls.username)
744

    
745
    def test_002d_verify_server_metadata(self):
746
        """Test server metadata keys are set based on image metadata"""
747

    
748
        log.info("Verifying image metadata")
749

    
750
        servermeta = self.client.get_server_metadata(self.serverid)
751
        imagemeta = self.client.get_image_metadata(self.imageid)
752

    
753
        self.assertEqual(servermeta["OS"], imagemeta["os"])
754

    
755
    def test_003_server_becomes_active(self):
756
        """Test server becomes ACTIVE"""
757

    
758
        log.info("Waiting for server to become ACTIVE")
759

    
760
        self._insist_on_status_transition(
761
            "BUILD", "ACTIVE", self.build_fail, self.build_warning)
762

    
763
    def test_003a_get_server_oob_console(self):
764
        """Test getting OOB server console over VNC
765

766
        Implementation of RFB protocol follows
767
        http://www.realvnc.com/docs/rfbproto.pdf.
768

769
        """
770
        console = self.cyclades.get_server_console(self.serverid)
771
        self.assertEquals(console['type'], "vnc")
772
        sock = self._insist_on_tcp_connection(
773
            socket.AF_INET, console["host"], console["port"])
774

    
775
        # Step 1. ProtocolVersion message (par. 6.1.1)
776
        version = sock.recv(1024)
777
        self.assertEquals(version, 'RFB 003.008\n')
778
        sock.send(version)
779

    
780
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
781
        sec = sock.recv(1024)
782
        self.assertEquals(list(sec), ['\x01', '\x02'])
783

    
784
        # Step 3. Request VNC Authentication (par 6.1.2)
785
        sock.send('\x02')
786

    
787
        # Step 4. Receive Challenge (par 6.2.2)
788
        challenge = sock.recv(1024)
789
        self.assertEquals(len(challenge), 16)
790

    
791
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
792
        response = d3des_generate_response(
793
            (console["password"] + '\0' * 8)[:8], challenge)
794
        sock.send(response)
795

    
796
        # Step 6. SecurityResult (par 6.1.3)
797
        result = sock.recv(4)
798
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
799
        sock.close()
800

    
801
    def test_004_server_has_ipv4(self):
802
        """Test active server has a valid IPv4 address"""
803

    
804
        log.info("Validate server's IPv4")
805

    
806
        server = self.client.get_server_details(self.serverid)
807
        ipv4 = self._get_ipv4(server)
808

    
809
        self.result_dict.clear()
810
        self.result_dict["IPv4"] = str(ipv4)
811

    
812
        self.assertEquals(IP(ipv4).version(), 4)
813

    
814
    def test_005_server_has_ipv6(self):
815
        """Test active server has a valid IPv6 address"""
816
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
817

    
818
        log.info("Validate server's IPv6")
819

    
820
        server = self.client.get_server_details(self.serverid)
821
        ipv6 = self._get_ipv6(server)
822

    
823
        self.result_dict.clear()
824
        self.result_dict["IPv6"] = str(ipv6)
825

    
826
        self.assertEquals(IP(ipv6).version(), 6)
827

    
828
    def test_006_server_responds_to_ping_IPv4(self):
829
        """Test server responds to ping on IPv4 address"""
830

    
831
        log.info("Testing if server responds to pings in IPv4")
832
        self.result_dict.clear()
833

    
834
        server = self.client.get_server_details(self.serverid)
835
        ip = self._get_ipv4(server)
836
        self._try_until_timeout_expires(self.action_timeout,
837
                                        self.action_timeout,
838
                                        "PING IPv4 to %s" % ip,
839
                                        self._ping_once,
840
                                        False, ip)
841

    
842
    def test_007_server_responds_to_ping_IPv6(self):
843
        """Test server responds to ping on IPv6 address"""
844
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
845
        log.info("Testing if server responds to pings in IPv6")
846

    
847
        server = self.client.get_server_details(self.serverid)
848
        ip = self._get_ipv6(server)
849
        self._try_until_timeout_expires(self.action_timeout,
850
                                        self.action_timeout,
851
                                        "PING IPv6 to %s" % ip,
852
                                        self._ping_once,
853
                                        True, ip)
854

    
855
    def test_008_submit_shutdown_request(self):
856
        """Test submit request to shutdown server"""
857

    
858
        log.info("Shutting down server")
859

    
860
        self.cyclades.shutdown_server(self.serverid)
861

    
862
    def test_009_server_becomes_stopped(self):
863
        """Test server becomes STOPPED"""
864

    
865
        log.info("Waiting until server becomes STOPPED")
866
        self._insist_on_status_transition(
867
            "ACTIVE", "STOPPED", self.action_timeout, self.action_timeout)
868

    
869
    def test_010_submit_start_request(self):
870
        """Test submit start server request"""
871

    
872
        log.info("Starting server")
873

    
874
        self.cyclades.start_server(self.serverid)
875

    
876
    def test_011_server_becomes_active(self):
877
        """Test server becomes ACTIVE again"""
878

    
879
        log.info("Waiting until server becomes ACTIVE")
880
        self._insist_on_status_transition(
881
            "STOPPED", "ACTIVE", self.action_timeout, self.action_timeout)
882

    
883
    def test_011a_server_responds_to_ping_IPv4(self):
884
        """Test server OS is actually up and running again"""
885

    
886
        log.info("Testing if server is actually up and running")
887

    
888
        self.test_006_server_responds_to_ping_IPv4()
889

    
890
    def test_012_ssh_to_server_IPv4(self):
891
        """Test SSH to server public IPv4 works, verify hostname"""
892

    
893
        self._skipIf(self.is_windows, "only valid for Linux servers")
894
        server = self.client.get_server_details(self.serverid)
895
        self._insist_on_ssh_hostname(self._get_ipv4(server),
896
                                     self.username, self.passwd)
897

    
898
    def test_013_ssh_to_server_IPv6(self):
899
        """Test SSH to server public IPv6 works, verify hostname"""
900
        self._skipIf(self.is_windows, "only valid for Linux servers")
901
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
902

    
903
        server = self.client.get_server_details(self.serverid)
904
        self._insist_on_ssh_hostname(self._get_ipv6(server),
905
                                     self.username, self.passwd)
906

    
907
    def test_014_rdp_to_server_IPv4(self):
908
        "Test RDP connection to server public IPv4 works"""
909
        self._skipIf(not self.is_windows, "only valid for Windows servers")
910
        server = self.client.get_server_details(self.serverid)
911
        ipv4 = self._get_ipv4(server)
912
        sock = self._insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
913

    
914
        # No actual RDP processing done. We assume the RDP server is there
915
        # if the connection to the RDP port is successful.
916
        # FIXME: Use rdesktop, analyze exit code? see manpage [costasd]
917
        sock.close()
918

    
919
    def test_015_rdp_to_server_IPv6(self):
920
        "Test RDP connection to server public IPv6 works"""
921
        self._skipIf(not self.is_windows, "only valid for Windows servers")
922
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
923

    
924
        server = self.client.get_server_details(self.serverid)
925
        ipv6 = self._get_ipv6(server)
926
        sock = self._get_tcp_connection(socket.AF_INET6, ipv6, 3389)
927

    
928
        # No actual RDP processing done. We assume the RDP server is there
929
        # if the connection to the RDP port is successful.
930
        sock.close()
931

    
932
    def test_016_personality_is_enforced(self):
933
        """Test file injection for personality enforcement"""
934
        self._skipIf(self.is_windows, "only implemented for Linux servers")
935
        self._skipIf(self.personality is None, "No personality file selected")
936

    
937
        log.info("Trying to inject file for personality enforcement")
938

    
939
        server = self.client.get_server_details(self.serverid)
940

    
941
        for inj_file in self.personality:
942
            equal_files = self._check_file_through_ssh(self._get_ipv4(server),
943
                                                       inj_file['owner'],
944
                                                       self.passwd,
945
                                                       inj_file['path'],
946
                                                       inj_file['contents'])
947
            self.assertTrue(equal_files)
948

    
949
    def test_017_submit_delete_request(self):
950
        """Test submit request to delete server"""
951

    
952
        log.info("Deleting server")
953

    
954
        self.client.delete_server(self.serverid)
955

    
956
    def test_018_server_becomes_deleted(self):
957
        """Test server becomes DELETED"""
958

    
959
        log.info("Testing if server becomes DELETED")
960

    
961
        self._insist_on_status_transition(
962
            "ACTIVE", "DELETED", self.action_timeout, self.action_timeout)
963

    
964
    def test_019_server_no_longer_in_server_list(self):
965
        """Test server is no longer in server list"""
966

    
967
        log.info("Test if server is no longer listed")
968

    
969
        servers = self.client.list_servers()
970
        self.assertNotIn(self.serverid, [s["id"] for s in servers])
971

    
972

    
973
class NetworkTestCase(unittest.TestCase):
974
    """ Testing networking in cyclades """
975

    
976
    @classmethod
977
    def setUpClass(cls):
978
        "Initialize kamaki, get list of current networks"
979

    
980
        cls.client = CycladesClient(API, TOKEN)
981
        cls.compute = ComputeClient(API, TOKEN)
982

    
983
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
984
                                          TEST_RUN_ID,
985
                                          cls.imagename)
986

    
987
        #Dictionary initialization for the vms credentials
988
        cls.serverid = dict()
989
        cls.username = dict()
990
        cls.password = dict()
991
        cls.is_windows = cls.imagename.lower().find("windows") >= 0
992

    
993
        cls.result_dict = dict()
994

    
995
    def _skipIf(self, condition, msg):
996
        if condition:
997
            self.skipTest(msg)
998

    
999
    def _get_ipv4(self, server):
1000
        """Get the public IPv4 of a server from the detailed server info"""
1001

    
1002
        nics = server["attachments"]
1003

    
1004
        for nic in nics:
1005
            net_id = nic["network_id"]
1006
            if self.client.get_network_details(net_id)["public"]:
1007
                public_addrs = nic["ipv4"]
1008

    
1009
        self.assertTrue(public_addrs is not None)
1010

    
1011
        return public_addrs
1012

    
1013
    def _connect_loginname(self, os_value):
1014
        """Return the login name for connections based on the server OS"""
1015
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
1016
            return "user"
1017
        elif os_value in ("windows", "windows_alpha1"):
1018
            return "Administrator"
1019
        else:
1020
            return "root"
1021

    
1022
    def _ping_once(self, ip):
1023

    
1024
        """Test server responds to a single IPv4 or IPv6 ping"""
1025
        cmd = "ping -c 2 -w 3 %s" % (ip)
1026
        ping = subprocess.Popen(cmd, shell=True,
1027
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1028
        (stdout, stderr) = ping.communicate()
1029
        ret = ping.wait()
1030

    
1031
        return (ret == 0)
1032

    
1033
    def test_00001a_submit_create_server_A(self):
1034
        """Test submit create server request"""
1035

    
1036
        log.info("Creating test server A")
1037

    
1038
        serverA = self.client.create_server(self.servername, self.flavorid,
1039
                                            self.imageid, personality=None)
1040

    
1041
        self.assertEqual(serverA["name"], self.servername)
1042
        self.assertEqual(serverA["flavor"], self.flavorid)
1043
        self.assertEqual(serverA["image"], self.imageid)
1044
        self.assertEqual(serverA["status"], "BUILD")
1045

    
1046
        # Update class attributes to reflect data on building server
1047
        self.serverid['A'] = serverA["id"]
1048
        self.username['A'] = None
1049
        self.password['A'] = serverA["adminPass"]
1050

    
1051
        log.info("Server A id:" + str(serverA["id"]))
1052
        log.info("Server password " + (self.password['A']))
1053

    
1054
        self.result_dict["Server A ID"] = str(serverA["id"])
1055
        self.result_dict["Server A password"] = serverA["adminPass"]
1056

    
1057
    def test_00001b_serverA_becomes_active(self):
1058
        """Test server becomes ACTIVE"""
1059

    
1060
        log.info("Waiting until test server A becomes ACTIVE")
1061
        self.result_dict.clear()
1062

    
1063
        fail_tmout = time.time() + self.action_timeout
1064
        while True:
1065
            d = self.client.get_server_details(self.serverid['A'])
1066
            status = d['status']
1067
            if status == 'ACTIVE':
1068
                active = True
1069
                break
1070
            elif time.time() > fail_tmout:
1071
                self.assertLess(time.time(), fail_tmout)
1072
            else:
1073
                time.sleep(self.query_interval)
1074

    
1075
        self.assertTrue(active)
1076

    
1077
    def test_00002a_submit_create_server_B(self):
1078
        """Test submit create server request"""
1079

    
1080
        log.info("Creating test server B")
1081

    
1082
        serverB = self.client.create_server(self.servername, self.flavorid,
1083
                                            self.imageid, personality=None)
1084

    
1085
        self.assertEqual(serverB["name"], self.servername)
1086
        self.assertEqual(serverB["flavor"], self.flavorid)
1087
        self.assertEqual(serverB["image"], self.imageid)
1088
        self.assertEqual(serverB["status"], "BUILD")
1089

    
1090
        # Update class attributes to reflect data on building server
1091
        self.serverid['B'] = serverB["id"]
1092
        self.username['B'] = None
1093
        self.password['B'] = serverB["adminPass"]
1094

    
1095
        log.info("Server B id: " + str(serverB["id"]))
1096
        log.info("Password " + (self.password['B']))
1097

    
1098
        self.result_dict.clear()
1099
        self.result_dict["Server B ID"] = str(serverB["id"])
1100
        self.result_dict["Server B password"] = serverB["adminPass"]
1101

    
1102
    def test_00002b_serverB_becomes_active(self):
1103
        """Test server becomes ACTIVE"""
1104

    
1105
        log.info("Waiting until test server B becomes ACTIVE")
1106
        self.result_dict.clear()
1107

    
1108
        fail_tmout = time.time() + self.action_timeout
1109
        while True:
1110
            d = self.client.get_server_details(self.serverid['B'])
1111
            status = d['status']
1112
            if status == 'ACTIVE':
1113
                active = True
1114
                break
1115
            elif time.time() > fail_tmout:
1116
                self.assertLess(time.time(), fail_tmout)
1117
            else:
1118
                time.sleep(self.query_interval)
1119

    
1120
        self.assertTrue(active)
1121

    
1122
    def test_001_create_network(self):
1123
        """Test submit create network request"""
1124

    
1125
        log.info("Submit new network request")
1126
        self.result_dict.clear()
1127

    
1128
        name = SNF_TEST_PREFIX + TEST_RUN_ID
1129
        #previous_num = len(self.client.list_networks())
1130
        network = self.client.create_network(name, cidr='10.0.1.0/28',
1131
                                             dhcp=True)
1132

    
1133
        #Test if right name is assigned
1134
        self.assertEqual(network['name'], name)
1135

    
1136
        # Update class attributes
1137
        cls = type(self)
1138
        cls.networkid = network['id']
1139
        #networks = self.client.list_networks()
1140

    
1141
        fail_tmout = time.time() + self.action_timeout
1142

    
1143
        #Test if new network is created
1144
        while True:
1145
            d = self.client.get_network_details(network['id'])
1146
            if d['status'] == 'ACTIVE':
1147
                connected = True
1148
                break
1149
            elif time.time() > fail_tmout:
1150
                self.assertLess(time.time(), fail_tmout)
1151
            else:
1152
                log.info("Waiting for network to become ACTIVE")
1153
                time.sleep(self.query_interval)
1154

    
1155
        self.assertTrue(connected)
1156

    
1157
        self.result_dict["Private network ID"] = str(network['id'])
1158

    
1159
    def test_002_connect_to_network(self):
1160
        """Test connect VMs to network"""
1161

    
1162
        log.info("Connect VMs to private network")
1163
        self.result_dict.clear()
1164

    
1165
        self.client.connect_server(self.serverid['A'], self.networkid)
1166
        self.client.connect_server(self.serverid['B'], self.networkid)
1167

    
1168
        #Insist on connecting until action timeout
1169
        fail_tmout = time.time() + self.action_timeout
1170

    
1171
        while True:
1172

    
1173
            netsA = [x['network_id']
1174
                     for x in self.client.get_server_details(
1175
                         self.serverid['A'])['attachments']]
1176
            netsB = [x['network_id']
1177
                     for x in self.client.get_server_details(
1178
                         self.serverid['B'])['attachments']]
1179

    
1180
            if (self.networkid in netsA) and (self.networkid in netsB):
1181
                conn_exists = True
1182
                break
1183
            elif time.time() > fail_tmout:
1184
                self.assertLess(time.time(), fail_tmout)
1185
            else:
1186
                time.sleep(self.query_interval)
1187

    
1188
        #Adding private IPs to class attributes
1189
        cls = type(self)
1190
        cls.priv_ip = dict()
1191

    
1192
        nicsA = self.client.get_server_details(
1193
            self.serverid['A'])['attachments']
1194
        nicsB = self.client.get_server_details(
1195
            self.serverid['B'])['attachments']
1196

    
1197
        if conn_exists:
1198
            for nic in nicsA:
1199
                if nic["network_id"] == self.networkid:
1200
                    cls.priv_ip["A"] = nic["ipv4"]
1201
            self.result_dict["Server A private IP"] = str(cls.priv_ip["A"])
1202

    
1203
            for nic in nicsB:
1204
                if nic["network_id"] == self.networkid:
1205
                    cls.priv_ip["B"] = nic["ipv4"]
1206
            self.result_dict["Server B private IP"] = str(cls.priv_ip["B"])
1207

    
1208
        self.assertTrue(conn_exists)
1209
        self.assertIsNot(cls.priv_ip["A"], None)
1210
        self.assertIsNot(cls.priv_ip["B"], None)
1211

    
1212
    def test_002a_reboot(self):
1213
        """Rebooting server A"""
1214

    
1215
        log.info("Rebooting server A")
1216

    
1217
        self.client.shutdown_server(self.serverid['A'])
1218

    
1219
        fail_tmout = time.time() + self.action_timeout
1220
        while True:
1221
            d = self.client.get_server_details(self.serverid['A'])
1222
            status = d['status']
1223
            if status == 'STOPPED':
1224
                break
1225
            elif time.time() > fail_tmout:
1226
                self.assertLess(time.time(), fail_tmout)
1227
            else:
1228
                time.sleep(self.query_interval)
1229

    
1230
        self.client.start_server(self.serverid['A'])
1231

    
1232
        while True:
1233
            d = self.client.get_server_details(self.serverid['A'])
1234
            status = d['status']
1235
            if status == 'ACTIVE':
1236
                active = True
1237
                break
1238
            elif time.time() > fail_tmout:
1239
                self.assertLess(time.time(), fail_tmout)
1240
            else:
1241
                time.sleep(self.query_interval)
1242

    
1243
        self.assertTrue(active)
1244

    
1245
    def test_002b_ping_server_A(self):
1246
        "Test if server A responds to IPv4 pings"
1247

    
1248
        log.info("Testing if server A responds to IPv4 pings ")
1249
        self.result_dict.clear()
1250

    
1251
        server = self.client.get_server_details(self.serverid['A'])
1252
        ip = self._get_ipv4(server)
1253

    
1254
        fail_tmout = time.time() + self.action_timeout
1255

    
1256
        s = False
1257

    
1258
        self.result_dict["Server A public IP"] = str(ip)
1259

    
1260
        while True:
1261

    
1262
            if self._ping_once(ip):
1263
                s = True
1264
                break
1265

    
1266
            elif time.time() > fail_tmout:
1267
                self.assertLess(time.time(), fail_tmout)
1268

    
1269
            else:
1270
                time.sleep(self.query_interval)
1271

    
1272
        self.assertTrue(s)
1273

    
1274
    def test_002c_reboot(self):
1275
        """Reboot server B"""
1276

    
1277
        log.info("Rebooting server B")
1278
        self.result_dict.clear()
1279

    
1280
        self.client.shutdown_server(self.serverid['B'])
1281

    
1282
        fail_tmout = time.time() + self.action_timeout
1283
        while True:
1284
            d = self.client.get_server_details(self.serverid['B'])
1285
            status = d['status']
1286
            if status == 'STOPPED':
1287
                break
1288
            elif time.time() > fail_tmout:
1289
                self.assertLess(time.time(), fail_tmout)
1290
            else:
1291
                time.sleep(self.query_interval)
1292

    
1293
        self.client.start_server(self.serverid['B'])
1294

    
1295
        while True:
1296
            d = self.client.get_server_details(self.serverid['B'])
1297
            status = d['status']
1298
            if status == 'ACTIVE':
1299
                active = True
1300
                break
1301
            elif time.time() > fail_tmout:
1302
                self.assertLess(time.time(), fail_tmout)
1303
            else:
1304
                time.sleep(self.query_interval)
1305

    
1306
        self.assertTrue(active)
1307

    
1308
    def test_002d_ping_server_B(self):
1309
        """Test if server B responds to IPv4 pings"""
1310

    
1311
        log.info("Testing if server B responds to IPv4 pings")
1312
        self.result_dict.clear()
1313

    
1314
        server = self.client.get_server_details(self.serverid['B'])
1315
        ip = self._get_ipv4(server)
1316

    
1317
        fail_tmout = time.time() + self.action_timeout
1318

    
1319
        s = False
1320

    
1321
        self.result_dict["Server B public IP"] = str(ip)
1322

    
1323
        while True:
1324
            if self._ping_once(ip):
1325
                s = True
1326
                break
1327

    
1328
            elif time.time() > fail_tmout:
1329
                self.assertLess(time.time(), fail_tmout)
1330

    
1331
            else:
1332
                time.sleep(self.query_interval)
1333

    
1334
        self.assertTrue(s)
1335

    
1336
    def test_003a_setup_interface_A(self):
1337
        """Setup eth1 for server A"""
1338

    
1339
        self._skipIf(self.is_windows, "only valid for Linux servers")
1340

    
1341
        log.info("Setting up interface eth1 for server A")
1342
        self.result_dict.clear()
1343

    
1344
        server = self.client.get_server_details(self.serverid['A'])
1345
        image = self.client.get_image_details(self.imageid)
1346
        os_value = image['metadata']['os']
1347

    
1348
        users = image["metadata"].get("users", None)
1349
        userlist = users.split()
1350

    
1351
        if "root" in userlist:
1352
            loginname = "root"
1353
        elif users is None:
1354
            loginname = self._connect_loginname(os_value)
1355
        else:
1356
            loginname = choice(userlist)
1357

    
1358
        hostip = self._get_ipv4(server)
1359
        myPass = self.password['A']
1360

    
1361
        log.info("SSH in server A as %s/%s" % (loginname, myPass))
1362
        command = "ifconfig eth1 %s && ifconfig eth1 | " \
1363
                  "grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'" \
1364
                  % self.priv_ip["A"]
1365
        output, status = _ssh_execute(
1366
            hostip, loginname, myPass, command)
1367

    
1368
        self.assertEquals(status, 0)
1369
        self.assertEquals(output[0].strip(), self.priv_ip["A"])
1370

    
1371
    def test_003b_setup_interface_B(self):
1372
        """Setup eth1 for server B"""
1373

    
1374
        self._skipIf(self.is_windows, "only valid for Linux servers")
1375

    
1376
        log.info("Setting up interface eth1 for server B")
1377

    
1378
        server = self.client.get_server_details(self.serverid['B'])
1379
        image = self.client.get_image_details(self.imageid)
1380
        os_value = image['metadata']['os']
1381

    
1382
        users = image["metadata"].get("users", None)
1383
        userlist = users.split()
1384

    
1385
        if "root" in userlist:
1386
            loginname = "root"
1387
        elif users is None:
1388
            loginname = self._connect_loginname(os_value)
1389
        else:
1390
            loginname = choice(userlist)
1391

    
1392
        hostip = self._get_ipv4(server)
1393
        myPass = self.password['B']
1394

    
1395
        log.info("SSH in server B as %s/%s" % (loginname, myPass))
1396
        command = "ifconfig eth1 %s && ifconfig eth1 | " \
1397
                  "grep 'inet addr:' | cut -d: -f2 | awk '{ print $1}'" \
1398
                  % self.priv_ip["B"]
1399
        output, status = _ssh_execute(
1400
            hostip, loginname, myPass, command)
1401

    
1402
        self.assertEquals(status, 0)
1403
        self.assertEquals(output[0].strip(), self.priv_ip["B"])
1404

    
1405
    def test_003c_test_connection_exists(self):
1406
        """Ping server B from server A to test if connection exists"""
1407

    
1408
        self._skipIf(self.is_windows, "only valid for Linux servers")
1409

    
1410
        log.info("Testing if server A is actually connected to server B")
1411

    
1412
        server = self.client.get_server_details(self.serverid['A'])
1413
        image = self.client.get_image_details(self.imageid)
1414
        os_value = image['metadata']['os']
1415
        hostip = self._get_ipv4(server)
1416

    
1417
        users = image["metadata"].get("users", None)
1418
        userlist = users.split()
1419

    
1420
        if "root" in userlist:
1421
            loginname = "root"
1422
        elif users is None:
1423
            loginname = self._connect_loginname(os_value)
1424
        else:
1425
            loginname = choice(userlist)
1426

    
1427
        myPass = self.password['A']
1428

    
1429
        cmd = "if ping -c 2 -w 3 %s >/dev/null; \
1430
               then echo \'True\'; fi;" % self.priv_ip["B"]
1431
        lines, status = _ssh_execute(
1432
            hostip, loginname, myPass, cmd)
1433

    
1434
        exists = False
1435

    
1436
        if 'True\n' in lines:
1437
            exists = True
1438

    
1439
        self.assertTrue(exists)
1440

    
1441
    def test_004_disconnect_from_network(self):
1442
        "Disconnecting server A and B from network"
1443

    
1444
        log.info("Disconnecting servers from private network")
1445

    
1446
        prev_state = self.client.get_network_details(self.networkid)
1447
        prev_nics = prev_state['attachments']
1448
        #prev_conn = len(prev_nics)
1449

    
1450
        nicsA = [x['id']
1451
                 for x in self.client.get_server_details(
1452
                     self.serverid['A'])['attachments']]
1453
        nicsB = [x['id']
1454
                 for x in self.client.get_server_details(
1455
                     self.serverid['B'])['attachments']]
1456

    
1457
        for nic in prev_nics:
1458
            if nic in nicsA:
1459
                self.client.disconnect_server(self.serverid['A'], nic)
1460
            if nic in nicsB:
1461
                self.client.disconnect_server(self.serverid['B'], nic)
1462

    
1463
        #Insist on deleting until action timeout
1464
        fail_tmout = time.time() + self.action_timeout
1465

    
1466
        while True:
1467
            netsA = [x['network_id']
1468
                     for x in self.client.get_server_details(
1469
                         self.serverid['A'])['attachments']]
1470
            netsB = [x['network_id']
1471
                     for x in self.client.get_server_details(
1472
                         self.serverid['B'])['attachments']]
1473

    
1474
            #connected = (self.client.get_network_details(self.networkid))
1475
            #connections = connected['attachments']
1476
            if (self.networkid not in netsA) and (self.networkid not in netsB):
1477
                conn_exists = False
1478
                break
1479
            elif time.time() > fail_tmout:
1480
                self.assertLess(time.time(), fail_tmout)
1481
            else:
1482
                time.sleep(self.query_interval)
1483

    
1484
        self.assertFalse(conn_exists)
1485

    
1486
    def test_005_destroy_network(self):
1487
        """Test submit delete network request"""
1488

    
1489
        log.info("Submitting delete network request")
1490

    
1491
        self.client.delete_network(self.networkid)
1492

    
1493
        fail_tmout = time.time() + self.action_timeout
1494

    
1495
        while True:
1496

    
1497
            curr_net = []
1498
            networks = self.client.list_networks()
1499

    
1500
            for net in networks:
1501
                curr_net.append(net['id'])
1502

    
1503
            if self.networkid not in curr_net:
1504
                self.assertTrue(self.networkid not in curr_net)
1505
                break
1506

    
1507
            elif time.time() > fail_tmout:
1508
                self.assertLess(time.time(), fail_tmout)
1509

    
1510
            else:
1511
                time.sleep(self.query_interval)
1512

    
1513
    def test_006_cleanup_servers(self):
1514
        """Cleanup servers created for this test"""
1515

    
1516
        log.info("Delete servers created for this test")
1517

    
1518
        self.compute.delete_server(self.serverid['A'])
1519
        self.compute.delete_server(self.serverid['B'])
1520

    
1521
        fail_tmout = time.time() + self.action_timeout
1522

    
1523
        #Ensure server gets deleted
1524
        status = dict()
1525

    
1526
        while True:
1527
            details = self.compute.get_server_details(self.serverid['A'])
1528
            status['A'] = details['status']
1529
            details = self.compute.get_server_details(self.serverid['B'])
1530
            status['B'] = details['status']
1531
            if (status['A'] == 'DELETED') and (status['B'] == 'DELETED'):
1532
                deleted = True
1533
                break
1534
            elif time.time() > fail_tmout:
1535
                self.assertLess(time.time(), fail_tmout)
1536
            else:
1537
                time.sleep(self.query_interval)
1538

    
1539
        self.assertTrue(deleted)
1540

    
1541

    
1542
class TestRunnerProcess(Process):
1543
    """A distinct process used to execute part of the tests in parallel"""
1544
    def __init__(self, **kw):
1545
        Process.__init__(self, **kw)
1546
        kwargs = kw["kwargs"]
1547
        self.testq = kwargs["testq"]
1548
        self.worker_folder = kwargs["worker_folder"]
1549

    
1550
    def run(self):
1551
        # Make sure this test runner process dies with the parent
1552
        # and is not left behind.
1553
        #
1554
        # WARNING: This uses the prctl(2) call and is
1555
        # Linux-specific.
1556

    
1557
        prctl.set_pdeathsig(signal.SIGHUP)
1558

    
1559
        multi = logging.getLogger("multiprocess")
1560

    
1561
        while True:
1562
            multi.debug("I am process %d, GETting from queue is %s" %
1563
                        (os.getpid(), self.testq))
1564
            msg = self.testq.get()
1565

    
1566
            multi.debug("Dequeued msg: %s" % msg)
1567

    
1568
            if msg == "TEST_RUNNER_TERMINATE":
1569
                raise SystemExit
1570

    
1571
            elif issubclass(msg, unittest.TestCase):
1572
                # Assemble a TestSuite, and run it
1573

    
1574
                log_file = os.path.join(self.worker_folder, 'details_' +
1575
                                        (msg.__name__) + "_" +
1576
                                        TEST_RUN_ID + '.log')
1577

    
1578
                fail_file = os.path.join(self.worker_folder, 'failed_' +
1579
                                         (msg.__name__) + "_" +
1580
                                         TEST_RUN_ID + '.log')
1581
                error_file = os.path.join(self.worker_folder, 'error_' +
1582
                                          (msg.__name__) + "_" +
1583
                                          TEST_RUN_ID + '.log')
1584

    
1585
                f = open(log_file, 'w')
1586
                fail = open(fail_file, 'w')
1587
                error = open(error_file, 'w')
1588

    
1589
                log.info(yellow + '* Starting testcase: %s' % msg + normal)
1590

    
1591
                runner = unittest.TextTestRunner(
1592
                    f, verbosity=2, failfast=True,
1593
                    resultclass=BurninTestResult)
1594
                suite = unittest.TestLoader().loadTestsFromTestCase(msg)
1595
                result = runner.run(suite)
1596

    
1597
                for res in result.errors:
1598
                    log.error("snf-burnin encountered an error in "
1599
                              "testcase: %s" % msg)
1600
                    log.error("See log for details")
1601
                    error.write(str(res[0]) + '\n')
1602
                    error.write(str(res[0].shortDescription()) + '\n')
1603
                    error.write('\n')
1604

    
1605
                for res in result.failures:
1606
                    log.error("snf-burnin failed in testcase: %s" % msg)
1607
                    log.error("See log for details")
1608
                    fail.write(str(res[0]) + '\n')
1609
                    fail.write(str(res[0].shortDescription()) + '\n')
1610
                    fail.write('\n')
1611
                    if not NOFAILFAST:
1612
                        sys.exit()
1613

    
1614
                if (len(result.failures) == 0) and (len(result.errors) == 0):
1615
                    log.debug("Passed testcase: %s" % msg)
1616

    
1617
                f.close()
1618
                fail.close()
1619
                error.close()
1620

    
1621
            else:
1622
                raise Exception("Cannot handle msg: %s" % msg)
1623

    
1624

    
1625
def _run_cases_in_series(cases, image_folder):
1626
    """Run instances of TestCase in series"""
1627

    
1628
    for case in cases:
1629

    
1630
        test = case.__name__
1631

    
1632
        log.info(yellow + '* Starting testcase: %s' % test + normal)
1633
        log_file = os.path.join(image_folder, 'details_' +
1634
                                (case.__name__) + "_" +
1635
                                TEST_RUN_ID + '.log')
1636
        fail_file = os.path.join(image_folder, 'failed_' +
1637
                                 (case.__name__) + "_" +
1638
                                 TEST_RUN_ID + '.log')
1639
        error_file = os.path.join(image_folder, 'error_' +
1640
                                  (case.__name__) + "_" +
1641
                                  TEST_RUN_ID + '.log')
1642

    
1643
        f = open(log_file, "w")
1644
        fail = open(fail_file, "w")
1645
        error = open(error_file, "w")
1646

    
1647
        suite = unittest.TestLoader().loadTestsFromTestCase(case)
1648
        runner = unittest.TextTestRunner(
1649
            f, verbosity=2, failfast=True,
1650
            resultclass=BurninTestResult)
1651
        result = runner.run(suite)
1652

    
1653
        for res in result.errors:
1654
            log.error("snf-burnin encountered an error in "
1655
                      "testcase: %s" % test)
1656
            log.error("See log for details")
1657
            error.write(str(res[0]) + '\n')
1658
            error.write(str(res[0].shortDescription()) + '\n')
1659
            error.write('\n')
1660

    
1661
        for res in result.failures:
1662
            log.error("snf-burnin failed in testcase: %s" % test)
1663
            log.error("See log for details")
1664
            fail.write(str(res[0]) + '\n')
1665
            fail.write(str(res[0].shortDescription()) + '\n')
1666
            fail.write('\n')
1667
            if not NOFAILFAST:
1668
                sys.exit()
1669

    
1670
        if (len(result.failures) == 0) and (len(result.errors) == 0):
1671
            log.debug("Passed testcase: %s" % test)
1672

    
1673

    
1674
def _run_cases_in_parallel(cases, fanout, image_folder):
1675
    """Run instances of TestCase in parallel, in a number of distinct processes
1676

1677
    The cases iterable specifies the TestCases to be executed in parallel,
1678
    by test runners running in distinct processes.
1679
    The fanout parameter specifies the number of processes to spawn,
1680
    and defaults to 1.
1681
    The runner argument specifies the test runner class to use inside each
1682
    runner process.
1683

1684
    """
1685

    
1686
    multi = logging.getLogger("multiprocess")
1687
    handler = logging.StreamHandler()
1688
    multi.addHandler(handler)
1689

    
1690
    if VERBOSE:
1691
        multi.setLevel(logging.DEBUG)
1692
    else:
1693
        multi.setLevel(logging.INFO)
1694

    
1695
    testq = []
1696
    worker_folder = []
1697
    runners = []
1698

    
1699
    for i in xrange(0, fanout):
1700
        testq.append(Queue())
1701
        worker_folder.append(os.path.join(image_folder, 'process'+str(i)))
1702
        os.mkdir(worker_folder[i])
1703

    
1704
    for i in xrange(0, fanout):
1705
        kwargs = dict(testq=testq[i], worker_folder=worker_folder[i])
1706
        runners.append(TestRunnerProcess(kwargs=kwargs))
1707

    
1708
    multi.debug("Spawning %d test runner processes" % len(runners))
1709

    
1710
    for p in runners:
1711
        p.start()
1712

    
1713
    # Enqueue test cases
1714
    for i in xrange(0, fanout):
1715
        map(testq[i].put, cases)
1716
        testq[i].put("TEST_RUNNER_TERMINATE")
1717

    
1718
    multi.debug("Spawned %d test runners, PIDs are %s" %
1719
                (len(runners), [p.pid for p in runners]))
1720

    
1721
    multi.debug("Joining %d processes" % len(runners))
1722

    
1723
    for p in runners:
1724
        p.join()
1725

    
1726
    multi.debug("Done joining %d processes" % len(runners))
1727

    
1728

    
1729
def _images_test_case(**kwargs):
1730
    """Construct a new unit test case class from ImagesTestCase"""
1731
    name = "ImagesTestCase_%s" % kwargs["imageid"]
1732
    cls = type(name, (ImagesTestCase,), kwargs)
1733

    
1734
    #Patch extra parameters into test names by manipulating method docstrings
1735
    for (mname, m) in \
1736
            inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
1737
        if hasattr(m, __doc__):
1738
            m.__func__.__doc__ = "[%s] %s" % (cls.imagename, m.__doc__)
1739

    
1740
    # Make sure the class can be pickled, by listing it among
1741
    # the attributes of __main__. A PicklingError is raised otherwise.
1742
    thismodule = sys.modules[__name__]
1743
    setattr(thismodule, name, cls)
1744
    return cls
1745

    
1746

    
1747
def _spawn_server_test_case(**kwargs):
1748
    """Construct a new unit test case class from SpawnServerTestCase"""
1749

    
1750
    name = "SpawnServerTestCase_%s" % kwargs["imageid"]
1751
    cls = type(name, (SpawnServerTestCase,), kwargs)
1752

    
1753
    # Patch extra parameters into test names by manipulating method docstrings
1754
    for (mname, m) in \
1755
            inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
1756
        if hasattr(m, __doc__):
1757
            m.__func__.__doc__ = "[%s] %s" % (cls.imagename, m.__doc__)
1758

    
1759
    # Make sure the class can be pickled, by listing it among
1760
    # the attributes of __main__. A PicklingError is raised otherwise.
1761

    
1762
    thismodule = sys.modules[__name__]
1763
    setattr(thismodule, name, cls)
1764
    return cls
1765

    
1766

    
1767
def _spawn_network_test_case(**kwargs):
1768
    """Construct a new unit test case class from NetworkTestCase"""
1769

    
1770
    name = "NetworkTestCase" + TEST_RUN_ID
1771
    cls = type(name, (NetworkTestCase,), kwargs)
1772

    
1773
    # Make sure the class can be pickled, by listing it among
1774
    # the attributes of __main__. A PicklingError is raised otherwise.
1775

    
1776
    thismodule = sys.modules[__name__]
1777
    setattr(thismodule, name, cls)
1778
    return cls
1779

    
1780

    
1781
# --------------------------------------------------------------------
1782
# Clean up servers/networks functions
1783
def cleanup_servers(timeout, query_interval, delete_stale=False):
1784

    
1785
    c = ComputeClient(API, TOKEN)
1786

    
1787
    servers = c.list_servers()
1788
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
1789

    
1790
    if len(stale) == 0:
1791
        return
1792

    
1793
    # Show staled servers
1794
    print >>sys.stderr, yellow + \
1795
        "Found these stale servers from previous runs:" + \
1796
        normal
1797
    print >>sys.stderr, "    " + \
1798
        "\n    ".join(["%d: %s" % (s["id"], s["name"]) for s in stale])
1799

    
1800
    # Delete staled servers
1801
    if delete_stale:
1802
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
1803
        fail_tmout = time.time() + timeout
1804
        for s in stale:
1805
            c.delete_server(s["id"])
1806
        # Wait for all servers to be deleted
1807
        while True:
1808
            servers = c.list_servers()
1809
            stale = [s for s in servers
1810
                     if s["name"].startswith(SNF_TEST_PREFIX)]
1811
            if len(stale) == 0:
1812
                print >> sys.stderr, green + "    ...done" + normal
1813
                break
1814
            elif time.time() > fail_tmout:
1815
                print >> sys.stderr, red + \
1816
                    "Not all stale servers deleted. Action timed out." + \
1817
                    normal
1818
                sys.exit(1)
1819
            else:
1820
                time.sleep(query_interval)
1821
    else:
1822
        print >> sys.stderr, "Use --delete-stale to delete them."
1823

    
1824

    
1825
def cleanup_networks(action_timeout, query_interval, delete_stale=False):
1826

    
1827
    c = CycladesClient(API, TOKEN)
1828

    
1829
    networks = c.list_networks()
1830
    stale = [n for n in networks if n["name"].startswith(SNF_TEST_PREFIX)]
1831

    
1832
    if len(stale) == 0:
1833
        return
1834

    
1835
    # Show staled networks
1836
    print >> sys.stderr, yellow + \
1837
        "Found these stale networks from previous runs:" + \
1838
        normal
1839
    print "    " + \
1840
        "\n    ".join(["%s: %s" % (str(n["id"]), n["name"]) for n in stale])
1841

    
1842
    # Delete staled networks
1843
    if delete_stale:
1844
        print >> sys.stderr, "Deleting %d stale networks:" % len(stale)
1845
        fail_tmout = time.time() + action_timeout
1846
        for n in stale:
1847
            c.delete_network(n["id"])
1848
        # Wait for all networks to be deleted
1849
        while True:
1850
            networks = c.list_networks()
1851
            stale = [n for n in networks
1852
                     if n["name"].startswith(SNF_TEST_PREFIX)]
1853
            if len(stale) == 0:
1854
                print >> sys.stderr, green + "    ...done" + normal
1855
                break
1856
            elif time.time() > fail_tmout:
1857
                print >> sys.stderr, red + \
1858
                    "Not all stale networks deleted. Action timed out." + \
1859
                    normal
1860
                sys.exit(1)
1861
            else:
1862
                time.sleep(query_interval)
1863
    else:
1864
        print >> sys.stderr, "Use --delete-stale to delete them."
1865

    
1866

    
1867
# --------------------------------------------------------------------
1868
# Parse arguments functions
1869
def parse_comma(option, opt, value, parser):
1870
    tests = set(['all', 'auth', 'images', 'flavors',
1871
                 'pithos', 'servers', 'server_spawn',
1872
                 'network_spawn'])
1873
    parse_input = value.split(',')
1874

    
1875
    if not (set(parse_input)).issubset(tests):
1876
        raise OptionValueError("The selected set of tests is invalid")
1877

    
1878
    setattr(parser.values, option.dest, value.split(','))
1879

    
1880

    
1881
def parse_arguments(args):
1882

    
1883
    kw = {}
1884
    kw["usage"] = "%prog [options]"
1885
    kw["description"] = \
1886
        "%prog runs a number of test scenarios on a " \
1887
        "Synnefo deployment."
1888

    
1889
    parser = OptionParser(**kw)
1890
    parser.disable_interspersed_args()
1891

    
1892
    parser.add_option("--api",
1893
                      action="store", type="string", dest="api",
1894
                      help="The API URI to use to reach the Synnefo API",
1895
                      default=None)
1896
    parser.add_option("--plankton",
1897
                      action="store", type="string", dest="plankton",
1898
                      help="The API URI to use to reach the Plankton API",
1899
                      default=None)
1900
    parser.add_option("--plankton-user",
1901
                      action="store", type="string", dest="plankton_user",
1902
                      help="Owner of system images",
1903
                      default=DEFAULT_PLANKTON_USER)
1904
    parser.add_option("--pithos",
1905
                      action="store", type="string", dest="pithos",
1906
                      help="The API URI to use to reach the Pithos API",
1907
                      default=None)
1908
    parser.add_option("--astakos",
1909
                      action="store", type="string", dest="astakos",
1910
                      help="The API URI to use to reach the Astakos API",
1911
                      default=None)
1912
    parser.add_option("--token",
1913
                      action="store", type="string", dest="token",
1914
                      help="The token to use for authentication to the API")
1915
    parser.add_option("--nofailfast",
1916
                      action="store_true", dest="nofailfast",
1917
                      help="Do not fail immediately if one of the tests "
1918
                           "fails (EXPERIMENTAL)",
1919
                      default=False)
1920
    parser.add_option("--no-ipv6",
1921
                      action="store_true", dest="no_ipv6",
1922
                      help="Disables ipv6 related tests",
1923
                      default=False)
1924
    parser.add_option("--action-timeout",
1925
                      action="store", type="int", dest="action_timeout",
1926
                      metavar="TIMEOUT",
1927
                      help="Wait SECONDS seconds for a server action to "
1928
                           "complete, then the test is considered failed",
1929
                      default=100)
1930
    parser.add_option("--build-warning",
1931
                      action="store", type="int", dest="build_warning",
1932
                      metavar="TIMEOUT",
1933
                      help="Warn if TIMEOUT seconds have passed and a "
1934
                           "build operation is still pending",
1935
                      default=600)
1936
    parser.add_option("--build-fail",
1937
                      action="store", type="int", dest="build_fail",
1938
                      metavar="BUILD_TIMEOUT",
1939
                      help="Fail the test if TIMEOUT seconds have passed "
1940
                           "and a build operation is still incomplete",
1941
                      default=900)
1942
    parser.add_option("--query-interval",
1943
                      action="store", type="int", dest="query_interval",
1944
                      metavar="INTERVAL",
1945
                      help="Query server status when requests are pending "
1946
                           "every INTERVAL seconds",
1947
                      default=3)
1948
    parser.add_option("--fanout",
1949
                      action="store", type="int", dest="fanout",
1950
                      metavar="COUNT",
1951
                      help="Spawn up to COUNT child processes to execute "
1952
                           "in parallel, essentially have up to COUNT "
1953
                           "server build requests outstanding (EXPERIMENTAL)",
1954
                      default=1)
1955
    parser.add_option("--force-flavor",
1956
                      action="store", type="int", dest="force_flavorid",
1957
                      metavar="FLAVOR ID",
1958
                      help="Force all server creations to use the specified "
1959
                           "FLAVOR ID instead of a randomly chosen one, "
1960
                           "useful if disk space is scarce",
1961
                      default=None)
1962
    parser.add_option("--image-id",
1963
                      action="store", type="string", dest="force_imageid",
1964
                      metavar="IMAGE ID",
1965
                      help="Test the specified image id, use 'all' to test "
1966
                           "all available images (mandatory argument)",
1967
                      default=None)
1968
    parser.add_option("--show-stale",
1969
                      action="store_true", dest="show_stale",
1970
                      help="Show stale servers from previous runs, whose "
1971
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1972
                      default=False)
1973
    parser.add_option("--delete-stale",
1974
                      action="store_true", dest="delete_stale",
1975
                      help="Delete stale servers from previous runs, whose "
1976
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1977
                      default=False)
1978
    parser.add_option("--force-personality",
1979
                      action="store", type="string", dest="personality_path",
1980
                      help="Force a personality file injection.\
1981
                            File path required. ",
1982
                      default=None)
1983
    parser.add_option("--log-folder",
1984
                      action="store", type="string", dest="log_folder",
1985
                      help="Define the absolute path where the output \
1986
                            log is stored. ",
1987
                      default="/var/log/burnin/")
1988
    parser.add_option("--verbose", "-V",
1989
                      action="store_true", dest="verbose",
1990
                      help="Print detailed output about multiple "
1991
                           "processes spawning",
1992
                      default=False)
1993
    parser.add_option("--set-tests",
1994
                      action="callback",
1995
                      dest="tests",
1996
                      type="string",
1997
                      help='Set comma seperated tests for this run. \
1998
                            Available tests: auth, images, flavors, \
1999
                                             servers, server_spawn, \
2000
                                             network_spawn, pithos. \
2001
                            Default = all',
2002
                      default='all',
2003
                      callback=parse_comma)
2004

    
2005
    (opts, args) = parser.parse_args(args)
2006

    
2007
    # -----------------------
2008
    # Verify arguments
2009

    
2010
    # `delete_stale' implies `show_stale'
2011
    if opts.delete_stale:
2012
        opts.show_stale = True
2013

    
2014
    # `token' is mandatory
2015
    _mandatory_argument(opts.token, "--token")
2016
    # `api' is mandatory
2017
    _mandatory_argument(opts.api, "--api")
2018

    
2019
    if not opts.show_stale:
2020
        # `image-id' is mandatory
2021
        _mandatory_argument(opts.force_imageid, "--image_id")
2022
        if opts.force_imageid != 'all':
2023
            try:
2024
                opts.force_imageid = str(opts.force_imageid)
2025
            except ValueError:
2026
                print >>sys.stderr, red + \
2027
                    "Invalid value specified for" + \
2028
                    "--image-id. Use a valid id, or `all'." + \
2029
                    normal
2030
                sys.exit(1)
2031
        # `pithos' is mandatory
2032
        _mandatory_argument(opts.pithos, "--pithos")
2033
        # `astakos' is mandatory
2034
        _mandatory_argument(opts.astakos, "--astakos")
2035
        # `plankton' is mandatory
2036
        _mandatory_argument(opts.plankton, "--plankton")
2037

    
2038
    return (opts, args)
2039

    
2040

    
2041
def _mandatory_argument(Arg, Str):
2042
    if not Arg:
2043
        print >>sys.stderr, red + \
2044
            "The " + Str + " argument is mandatory.\n" + \
2045
            normal
2046
        sys.exit(1)
2047

    
2048

    
2049
# --------------------------------------------------------------------
2050
# Burnin main function
2051
def main():
2052
    """Assemble test cases into a test suite, and run it
2053

2054
    IMPORTANT: Tests have dependencies and have to be run in the specified
2055
    order inside a single test case. They communicate through attributes of the
2056
    corresponding TestCase class (shared fixtures). Distinct subclasses of
2057
    TestCase MAY SHARE NO DATA, since they are run in parallel, in distinct
2058
    test runner processes.
2059

2060
    """
2061

    
2062
    # Parse arguments using `optparse'
2063
    (opts, args) = parse_arguments(sys.argv[1:])
2064

    
2065
    # Some global variables
2066
    global API, TOKEN, PLANKTON, PLANKTON_USER
2067
    global PITHOS, ASTAKOS, NO_IPV6, VERBOSE, NOFAILFAST
2068
    API = opts.api
2069
    TOKEN = opts.token
2070
    PLANKTON = opts.plankton
2071
    PLANKTON_USER = opts.plankton_user
2072
    PITHOS = opts.pithos
2073
    ASTAKOS = opts.astakos
2074
    NO_IPV6 = opts.no_ipv6
2075
    VERBOSE = opts.verbose
2076
    NOFAILFAST = opts.nofailfast
2077

    
2078
    # If `show_stale', cleanup stale servers
2079
    # from previous runs and exit
2080
    if opts.show_stale:
2081
        # We must clean the servers first
2082
        cleanup_servers(opts.action_timeout, opts.query_interval,
2083
                        delete_stale=opts.delete_stale)
2084
        cleanup_networks(opts.action_timeout, opts.query_interval,
2085
                         delete_stale=opts.delete_stale)
2086
        return 0
2087

    
2088
    # Initialize a kamaki instance, get flavors, images
2089
    c = ComputeClient(API, TOKEN)
2090
    DIMAGES = c.list_images(detail=True)
2091
    DFLAVORS = c.list_flavors(detail=True)
2092

    
2093
    # FIXME: logging, log, LOG PID, TEST_RUN_ID, arguments
2094
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
2095
    #unittest.main(verbosity=2, catchbreak=True)
2096

    
2097
    # Get a list of images we are going to test
2098
    if opts.force_imageid == 'all':
2099
        test_images = DIMAGES
2100
    else:
2101
        test_images = filter(lambda x: x["id"] == opts.force_imageid, DIMAGES)
2102

    
2103
    # Create output (logging) folder
2104
    if not os.path.exists(opts.log_folder):
2105
        os.mkdir(opts.log_folder)
2106
    test_folder = os.path.join(opts.log_folder, TEST_RUN_ID)
2107
    os.mkdir(test_folder)
2108

    
2109
    for image in test_images:
2110
        imageid = str(image["id"])
2111
        imagename = image["name"]
2112
        # Choose a flavor (given from user or random)
2113
        if opts.force_flavorid:
2114
            flavorid = opts.force_flavorid
2115
        else:
2116
            flavorid = choice([f["id"] for f in DFLAVORS if f["disk"] >= 20])
2117
        # Personality dictionary for file injection test
2118
        if opts.personality_path is not None:
2119
            f = open(opts.personality_path)
2120
            content = b64encode(f.read())
2121
            personality = []
2122
            st = os.stat(opts.personality_path)
2123
            personality.append({
2124
                'path': '/root/test_inj_file',
2125
                'owner': 'root',
2126
                'group': 'root',
2127
                'mode': 0x7777 & st.st_mode,
2128
                'contents': content})
2129
        else:
2130
            personality = None
2131
        # Give a name to our test servers
2132
        servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, imagename)
2133
        is_windows = imagename.lower().find("windows") >= 0
2134

    
2135
        # Create Server TestCases
2136
        ServerTestCase = _spawn_server_test_case(
2137
            imageid=imageid,
2138
            flavorid=flavorid,
2139
            imagename=imagename,
2140
            personality=personality,
2141
            servername=servername,
2142
            is_windows=is_windows,
2143
            action_timeout=opts.action_timeout,
2144
            build_warning=opts.build_warning,
2145
            build_fail=opts.build_fail,
2146
            query_interval=opts.query_interval)
2147
        # Create Network TestCases
2148
        NetworkTestCase = _spawn_network_test_case(
2149
            action_timeout=opts.action_timeout,
2150
            imageid=imageid,
2151
            flavorid=flavorid,
2152
            imagename=imagename,
2153
            query_interval=opts.query_interval)
2154
        # Create Images TestCase
2155
        CImagesTestCase = _images_test_case(
2156
            action_timeout=opts.action_timeout,
2157
            imageid=imageid,
2158
            flavorid=flavorid,
2159
            imagename=imagename,
2160
            query_interval=opts.query_interval)
2161

    
2162
        # Choose the tests we are going to run
2163
        test_dict = {'auth': UnauthorizedTestCase,
2164
                     'images': CImagesTestCase,
2165
                     'flavors': FlavorsTestCase,
2166
                     'servers': ServersTestCase,
2167
                     'pithos': PithosTestCase,
2168
                     'server_spawn': ServerTestCase,
2169
                     'network_spawn': NetworkTestCase}
2170
        seq_cases = []
2171
        if 'all' in opts.tests:
2172
            seq_cases = [UnauthorizedTestCase, CImagesTestCase,
2173
                         FlavorsTestCase, ServersTestCase,
2174
                         PithosTestCase, ServerTestCase,
2175
                         NetworkTestCase]
2176
        else:
2177
            for test in opts.tests:
2178
                seq_cases.append(test_dict[test])
2179

    
2180
        # Folder for each image
2181
        image_folder = os.path.join(test_folder, imageid)
2182
        os.mkdir(image_folder)
2183

    
2184
        # Run each test
2185
        if opts.fanout > 1:
2186
            _run_cases_in_parallel(seq_cases, opts.fanout, image_folder)
2187
        else:
2188
            _run_cases_in_series(seq_cases, image_folder)
2189

    
2190

    
2191
# --------------------------------------------------------------------
2192
# Call main
2193
if __name__ == "__main__":
2194
    sys.exit(main())