Statistics
| Branch: | Tag: | Revision:

root / snf-tools / test_suite.py @ 5a140b23

History | View | Annotate | Download (24 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 datetime
39
import inspect
40
import logging
41
import os
42
import paramiko
43
import subprocess
44
import socket
45
import sys
46
import time
47

    
48
from random import choice
49
from IPy import IP
50
from kamaki.client import Client, ClientError
51

    
52
# Use backported unittest functionality if Python < 2.7
53
try:
54
    import unittest2 as unittest
55
except ImportError:
56
    import unittest
57

    
58

    
59
API = "http://dev67.dev.grnet.gr:8000/api/v1.1"
60
TOKEN = "46e427d657b20defe352804f0eb6f8a2"
61
# A unique id identifying this test run
62
UNIQUE_RUN_ID = datetime.datetime.strftime(datetime.datetime.now(),
63
                                           "%Y%m%d%H%M%S")
64
SNF_TEST_PREFIX = "snf-test"
65

    
66
# Setup logging (FIXME - verigak)
67
logging.basicConfig(format="%(message)s")
68
log = logging.getLogger("snf-test")
69
log.setLevel(logging.INFO)
70

    
71

    
72
class UnauthorizedTestCase(unittest.TestCase):
73
    def test_unauthorized_access(self):
74
        """Test access without a valid token fails"""
75
        c = Client(API, "123")
76
        with self.assertRaises(ClientError) as cm:
77
            c.list_servers()
78
        self.assertEqual(cm.exception.status, 401)
79

    
80

    
81
class ImagesTestCase(unittest.TestCase):
82
    """Test image lists for consistency"""
83
    @classmethod
84
    def setUpClass(cls):
85
        """Initialize kamaki, get (detailed) list of images"""
86
        log.info("Getting simple and detailed list of images")
87
        cls.client = Client(API, TOKEN)
88
        cls.images = cls.client.list_images()
89
        cls.dimages = cls.client.list_images(detail=True)
90

    
91
    def test_001_list_images(self):
92
        """Test image list actually returns images"""
93
        self.assertGreater(len(self.images), 0)
94

    
95
    def test_002_list_images_detailed(self):
96
        """Test detailed image list is the same length as list"""
97
        self.assertEqual(len(self.dimages), len(self.images))
98

    
99
    def test_003_same_image_names(self):
100
        """Test detailed and simple image list contain same names"""
101
        names = sorted(map(lambda x: x["name"], self.images))
102
        dnames = sorted(map(lambda x: x["name"], self.dimages))
103
        self.assertEqual(names, dnames)
104

    
105
    def test_004_unique_image_names(self):
106
        """Test images have unique names"""
107
        names = sorted(map(lambda x: x["name"], self.images))
108
        self.assertEqual(sorted(list(set(names))), names)
109

    
110
    def test_005_image_metadata(self):
111
        """Test every image has specific metadata defined"""
112
        keys = frozenset(["OS", "description", "size"])
113
        for i in self.dimages:
114
            self.assertTrue(keys.issubset(i["metadata"]["values"].keys()))
115

    
116

    
117
class FlavorsTestCase(unittest.TestCase):
118
    """Test flavor lists for consistency"""
119
    @classmethod
120
    def setUpClass(cls):
121
        """Initialize kamaki, get (detailed) list of flavors"""
122
        log.info("Getting simple and detailed list of flavors")
123
        cls.client = Client(API, TOKEN)
124
        cls.flavors = cls.client.list_flavors()
125
        cls.dflavors = cls.client.list_flavors(detail=True)
126

    
127
    def test_001_list_flavors(self):
128
        """Test flavor list actually returns flavors"""
129
        self.assertGreater(len(self.flavors), 0)
130

    
131
    def test_002_list_flavors_detailed(self):
132
        """Test detailed flavor list is the same length as list"""
133
        self.assertEquals(len(self.dflavors), len(self.flavors))
134

    
135
    def test_003_same_flavor_names(self):
136
        """Test detailed and simple flavor list contain same names"""
137
        names = sorted(map(lambda x: x["name"], self.flavors))
138
        dnames = sorted(map(lambda x: x["name"], self.dflavors))
139
        self.assertEqual(names, dnames)
140

    
141
    def test_004_unique_flavor_names(self):
142
        """Test flavors have unique names"""
143
        names = sorted(map(lambda x: x["name"], self.flavors))
144
        self.assertEqual(sorted(list(set(names))), names)
145

    
146
    def test_005_well_formed_flavor_names(self):
147
        """Test flavors have names of the form CxxRyyDzz
148

149
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
150

151
        """
152
        for f in self.dflavors:
153
            self.assertEqual("C%dR%dD%d" % (f["cpu"], f["ram"], f["disk"]),
154
                             f["name"],
155
                             "Flavor %s does not match its specs." % f["name"])
156

    
157

    
158
class ServersTestCase(unittest.TestCase):
159
    """Test server lists for consistency"""
160
    @classmethod
161
    def setUpClass(cls):
162
        """Initialize kamaki, get (detailed) list of servers"""
163
        log.info("Getting simple and detailed list of servers")
164
        cls.client = Client(API, TOKEN)
165
        cls.servers = cls.client.list_servers()
166
        cls.dservers = cls.client.list_servers(detail=True)
167

    
168
    def test_001_list_servers(self):
169
        """Test server list actually returns servers"""
170
        self.assertGreater(len(self.servers), 0)
171

    
172
    def test_002_list_servers_detailed(self):
173
        """Test detailed server list is the same length as list"""
174
        self.assertEqual(len(self.dservers), len(self.servers))
175

    
176
    def test_003_same_server_names(self):
177
        """Test detailed and simple flavor list contain same names"""
178
        names = sorted(map(lambda x: x["name"], self.servers))
179
        dnames = sorted(map(lambda x: x["name"], self.dservers))
180
        self.assertEqual(names, dnames)
181

    
182

    
183
# This class gets replicated into actual TestCases dynamically
184
class SpawnServerTestCase(unittest.TestCase):
185
    """Test scenario for server of the specified image"""
186

    
187
    @classmethod
188
    def setUpClass(cls):
189
        """Initialize a kamaki instance"""
190
        log.info("Spawning server for image `%s'", self.imagename)
191
        cls.client = Client(API, TOKEN)
192

    
193
    def _get_ipv4(self, server):
194
        public_addrs = filter(lambda x: x["id"] == "public",
195
                              server["addresses"]["values"])
196
        self.assertEqual(len(public_addrs), 1)
197
        ipv4_addrs = filter(lambda x: x["version"] == 4,
198
                            public_addrs[0]["values"])
199
        self.assertEqual(len(ipv4_addrs), 1)
200
        return ipv4_addrs[0]["addr"]
201

    
202
    def _get_ipv6(self, server):
203
        public_addrs = filter(lambda x: x["id"] == "public",
204
                              server["addresses"]["values"])
205
        self.assertEqual(len(public_addrs), 1)
206
        ipv6_addrs = filter(lambda x: x["version"] == 6,
207
                            public_addrs[0]["values"])
208
        self.assertEqual(len(ipv6_addrs), 1)
209
        return ipv6_addrs[0]["addr"]
210

    
211
    def _get_tcp_connection(family, host, port):
212
        tmout = time.time() + self.action_timeout
213
        while True:
214
            self.assertLess(time.time(), tmout,
215
                "Timed out trying to to %s:%s" % (host, port))
216
            sock = None
217
            for res in \
218
                socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
219
                                   socket.AI_PASSIVE):
220
                af, socktype, proto, canonname, sa = res
221
                try:
222
                    sock = socket.socket(af, socktype, proto)
223
                except socket.error as msg:
224
                    sock = None
225
                    continue
226
                try:
227
                    sock.connect(sa)
228
                except socket.error as msg:
229
                    sock.close()
230
                    sock = None
231
                    continue
232
                self.assertIsNotNone(sock)
233
                return sock
234
            time.sleep(self.query_interval)
235

    
236
    def _wait_for_status_transition(self, current_status, new_status,
237
                                    fail_timeout, warn_timeout=None):
238
        if warn_timeout is None:
239
            warn_timeout = fail_timeout + 1
240
        warn_tmout = time.time() + warn_timeout
241
        fail_tmout = time.time() + fail_timeout
242
        while True:
243
            if time.time() > warn_tmout:
244
                log.warning("Server %d: %s still not %s",
245
                            self.serverid, self.servername, new_status)
246
            self.assertLess(time.time(), fail_tmout)
247
            log.info("Getting status for server %d, Image %s",
248
                     self.serverid, self.imagename)
249
            server = self.client.get_server_details(self.serverid)
250
            self.assertIn(server["status"], (current_status, new_status))
251
            if server["status"] == new_status:
252
                break
253
            time.sleep(self.query_interval)
254

    
255
    def _get_hostname_over_ssh(self, hostip, username, password):
256
        ssh = paramiko.SSHClient()
257
        ssh.set_missing_host_key_policy(paramiko.AutoAdPolicy())
258
        ssh.connect(hostip, username=username, password=password)
259
        stdin, stdout, stderr = ssh.exec_command("hostname")
260
        lines = stdout.readlines()
261
        self.assertEqual(len(lines), 1)
262
        self.assertEqual(lines[0], "notsnf-%s" % self.serverid)
263

    
264
    def _skipIf(self, condition, msg):
265
        if condition:
266
            self.skipTest(msg)
267

    
268
    def test_001_submit_create_server(self):
269
        """Test submit create server request"""
270
        server = self.client.create_server(self.servername, self.flavorid,
271
                                           self.imageid, self.personality)
272
        self.assertEqual(server["name"], self.servername)
273
        self.assertEqual(server["flavorRef"], self.flavorid)
274
        self.assertEqual(server["imageRef"], self.imageid)
275
        self.assertEqual(server["status"], "BUILD")
276

    
277
        # Update class attributes to reflect data on building server
278
        cls = type(self)
279
        cls.serverid = server["id"]
280
        cls.passwd = server["adminPass"]
281

    
282
    def test_002a_server_is_building_in_list(self):
283
        """Test server is in BUILD state, in server list"""
284
        servers = self.client.list_servers(detail=True)
285
        servers = filter(lambda x: x["name"] == self.servername, servers)
286
        self.assertEqual(len(servers), 1)
287
        server = servers[0]
288
        self.assertEqual(server["name"], self.servername)
289
        self.assertEqual(server["flavorRef"], self.flavorid)
290
        self.assertEqual(server["imageRef"], self.imageid)
291
        self.assertEqual(server["status"], "BUILD")
292

    
293
    def test_002b_server_is_building_in_details(self):
294
        """Test server is in BUILD state, in details"""
295
        server = self.client.get_server_details(self.serverid)
296
        self.assertEqual(server["name"], self.servername)
297
        self.assertEqual(server["flavorRef"], self.flavorid)
298
        self.assertEqual(server["imageRef"], self.imageid)
299
        self.assertEqual(server["status"], "BUILD")
300

    
301
    def test_002c_set_server_metadata(self):
302
        image = self.client.get_image_details(self.imageid)
303
        self.client.update_server_metadata(
304
            self.serverid, OS=image["metadata"]["values"]["OS"])
305

    
306
    def test_002d_verify_server_metadata(self):
307
        """Test server metadata keys are set based on image metadata"""
308
        servermeta = self.client.get_server_metadata(self.serverid)
309
        imagemeta = self.client.get_image_metadata(self.imageid)
310
        self.assertEqual(servermeta["OS"], imagemeta["OS"])
311

    
312
    def test_003_server_becomes_active(self):
313
        """Test server becomes ACTIVE"""
314
        self._wait_for_status_transition("BUILD", "ACTIVE",
315
                                         self.build_fail, self.build_warning)
316

    
317
    def test_003a_get_server_oob_console(self):
318
        """Test getting OOB server console over VNC"""
319
        console = self.client.get_server_console(self.serverid)
320
        self.assertEquals(console['type'], "vnc")
321
        sock = self._get_tcp_connection(socket.AF_UNSPEC,
322
                                        console["host"], console["port"])
323
        version = sock.recv(1024)
324
        self.assertTrue(version.startswith("RFB "))
325
        sock.close()
326

    
327
    def test_004_server_has_ipv4(self):
328
        """Test active server has a valid IPv4 address"""
329
        server = self.client.get_server_details(self.serverid)
330
        ipv4 = self._get_ipv4(server)
331
        self.assertEquals(IP(ipv4).version(), 4)
332

    
333
    def test_005_server_has_ipv6(self):
334
        """Test active server has a valid IPv6 address"""
335
        server = self.client.get_server_details(self.serverid)
336
        ipv6 = self._get_ipv6(server)
337
        self.assertEquals(IP(ipv6).version(), 6)
338

    
339
    def test_006_server_responds_to_ping_IPv4(self):
340
        """Test server responds to ping on IPv4 address"""
341
        server = self.client.get_server_details(self.serverid)
342
        ping = subprocess.Popen("ping -c 4 -w 4 %s" % self._get_ipv4(server),
343
                                shell=True, stdout=subprocess.PIPE,
344
                                stderr=subprocess.PIPE)
345
        (stdout, stderr) = ping.communicate()
346
        ret = ping.wait()
347
        self.assertEquals(ret, 0,
348
                          "ping IPv4 %s failed.\nStdout:\n%s\nStderr:\n%s" %
349
                          (self._get_ipv4(server), stdout, stderr))
350

    
351
    def test_007_server_responds_to_ping_IPv6(self):
352
        """Test server responds to ping on IPv6 address"""
353
        server = self.client.get_server_details(self.serverid)
354
        ping6 = subprocess.Popen("ping6 -c 4 -w 4 %s" % self._get_ipv6(server),
355
                                 shell=True, stdout=subprocess.PIPE,
356
                                 stderr=subprocess.PIPE)
357
        (stdout, stderr) = ping6.communicate()
358
        ret = ping6.wait()
359
        self.assertEquals(ret, 0,
360
                          "ping IPv6 %s failed.\nStdout:\n%s\nStderr:\n%s" %
361
                          (self._get_ipv6(server), stdout, stderr))
362

    
363
    def test_008_submit_shutdown_request(self):
364
        """Test submit request to shutdown server"""
365
        self.client.shutdown_server(self.serverid)
366

    
367
    def test_009_server_becomes_stopped(self):
368
        """Test server becomes STOPPED"""
369
        self._wait_for_status_transition("ACTIVE", "STOPPED",
370
                                         self.action_timeout)
371

    
372
    def test_010_submit_start_request(self):
373
        """Test submit start server request"""
374
        self.client.start_server(self.serverid)
375

    
376
    def test_011_server_becomes_active(self):
377
        """Test server becomes ACTIVE again"""
378
        self._wait_for_status_transition("STOPPED", "ACTIVE",
379
                                         self.action_timeout)
380

    
381
    def test_011a_server_responds_to_ping_IPv4(self):
382
        """Test server OS is actually up and running again"""
383
        self.test_006_server_responds_to_ping_IPv4()
384

    
385
    def test_012_ssh_to_server_IPv4(self):
386
        """Test SSH to server public IPv4 works, verify hostname"""
387
        self._skipIf(self.is_windows, "only for Linux servers")
388
        server = self.client.get_server_details(self.serverid)
389
        hostname = self._get_hostname_over_ssh(self._get_ipv4(server),
390
                                               "root", self.passwd)
391
        self.assertEqual(hostname, "notsnf-%s" % self.serverid)
392

    
393
    def test_013_ssh_to_server_IPv6(self):
394
        """Test SSH to server public IPv6 works, verify hostname"""
395
        self._skipIf(self.is_windows, "only for Linux servers")
396
        server = self.client.get_server_details(self.serverid)
397
        hostname = self._get_hostname_over_ssh(self._get_ipv6(server),
398
                                               "root", self.passwd)
399
        self.assertEqual(hostname, "notsnf-%s" % self.serverid)
400

    
401
    def test_014_rdp_to_server_IPv4(self):
402
        "Test RDP connection to server public IPv4 works"""
403
        self._skipIf(not self.is_windows, "only for Windows servers")
404
        server = self.client.get_server_details(self.serverid)
405
        ipv4 = self._get_ipv4(server)
406
        sock = _get_tcp_connection(socket.AF_INET, ipv4, 3389)
407

    
408
        # No actual RDP processing done. We assume the RDP server is there
409
        # if the connection to the RDP port is successful.
410
        sock.close()
411

    
412
    def test_015_rdp_to_server_IPv6(self):
413
        "Test RDP connection to server public IPv6 works"""
414
        self._skipIf(not self.is_windows, "only for Windows servers")
415
        server = self.client.get_server_details(self.serverid)
416
        ipv6 = self._get_ipv6(server)
417
        sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
418

    
419
        # No actual RDP processing done. We assume the RDP server is there
420
        # if the connection to the RDP port is successful.
421
        sock.close()
422

    
423
    def test_016_personality_is_enforced(self):
424
        """Test file injection for personality enforcement"""
425
        self._skipIf(self.is_windows, "only for Linux servers")
426
        self.assertTrue(False, "test not implemented, will fail")
427

    
428

    
429
def _spawn_server_test_case(**kwargs):
430
    """Construct a new unit test case class from SpawnServerTestCase"""
431

    
432
    name = "SpawnServerTestCase_%s" % kwargs["imagename"].replace(" ", "_")
433
    cls = type(name, (SpawnServerTestCase,), kwargs)
434

    
435
    # Patch extra parameters into test names by manipulating method docstrings
436
    for (mname, m) in \
437
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
438
            if hasattr(m, __doc__):
439
                m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
440

    
441
    return cls
442

    
443

    
444
def cleanup_servers(delete_stale=False):
445
    c = Client(API, TOKEN)
446
    servers = c.list_servers()
447
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
448

    
449
    print >> sys.stderr, "Found these stale servers from previous runs:"
450
    print "    " + \
451
          "\n    ".join(["%d: %s" % (s['id'], s['name']) for s in stale])
452

    
453
    if delete_stale:
454
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
455
        for server in stale:
456
            c.delete_server(server['id'])
457
        print >> sys.stderr, "    ...done"
458
    else:
459
        print >> sys.stderr, "Use --delete-stale to delete them."
460

    
461

    
462
def parse_arguments(args):
463
    from optparse import OptionParser
464

    
465
    kw = {}
466
    kw["usage"] = "%prog [options]"
467
    kw["description"] = \
468
        "%prog runs a number of test scenarios on a " \
469
        "Synnefo deployment."
470

    
471
    parser = OptionParser(**kw)
472
    parser.disable_interspersed_args()
473
    parser.add_option("--action-timeout",
474
                      action="store", type="int", dest="action_timeout",
475
                      metavar="TIMEOUT",
476
                      help="Wait SECONDS seconds for a server action to " \
477
                           "complete, then the test is considered failed",
478
                      default=20)
479
    parser.add_option("--build-warning",
480
                      action="store", type="int", dest="build_warning",
481
                      metavar="TIMEOUT",
482
                      help="Warn if TIMEOUT seconds have passed and a " \
483
                           "build operation is still pending",
484
                      default=600)
485
    parser.add_option("--build-fail",
486
                      action="store", type="int", dest="build_fail",
487
                      metavar="BUILD_TIMEOUT",
488
                      help="Fail the test if TIMEOUT seconds have passed " \
489
                           "and a build operation is still incomplete",
490
                      default=900)
491
    parser.add_option("--query-interval",
492
                      action="store", type="int", dest="query_interval",
493
                      metavar="INTERVAL",
494
                      help="Query server status when requests are pending " \
495
                           "every INTERVAL seconds",
496
                      default=3)
497
    parser.add_option("--build-fanout",
498
                      action="store", type="int", dest="build_fanout",
499
                      metavar="COUNT",
500
                      help="Test COUNT images in parallel, by submitting " \
501
                           "COUNT server build requests simultaneously",
502
                      default=1)
503
    parser.add_option("--force-flavor",
504
                      action="store", type="int", dest="force_flavorid",
505
                      metavar="FLAVOR ID",
506
                      help="Force all server creations to use the specified "\
507
                           "FLAVOR ID instead of a randomly chosen one, " \
508
                           "useful if disk space is scarce",
509
                      default=None)    # FIXME
510
    parser.add_option("--show-stale",
511
                      action="store_true", dest="show_stale",
512
                      help="Show stale servers from previous runs, whose "\
513
                           "name starts with '%s'" % SNF_TEST_PREFIX,
514
                      default=False)
515
    parser.add_option("--delete-stale",
516
                      action="store_true", dest="delete_stale",
517
                      help="Delete stale servers from previous runs, whose "\
518
                           "name starts with '%s'" % SNF_TEST_PREFIX,
519
                      default=False)
520

    
521
    # FIXME: Change the default for build-fanout to 10
522
    # FIXME: Allow the user to specify a specific set of Images to test
523

    
524
    (opts, args) = parser.parse_args(args)
525

    
526
    # Verify arguments
527
    if opts.delete_stale:
528
        opts.show_stale = True
529

    
530
    return (opts, args)
531

    
532

    
533
def main():
534
    """Assemble test cases into a test suite, and run it
535

536
    IMPORTANT: Tests have dependencies and have to be run in the specified
537
    order inside a single test case. They communicate through attributes of the
538
    corresponding TestCase class (shared fixtures). TestCase classes for
539
    distinct Images may be run in parallel.
540

541
    """
542

    
543
    (opts, args) = parse_arguments(sys.argv[1:])
544

    
545
    # Cleanup stale servers from previous runs
546
    if opts.show_stale:
547
        cleanup_servers(delete_stale=opts.delete_stale)
548
        return 0
549

    
550
    # Initialize a kamaki instance, get flavors, images
551
    c = Client(API, TOKEN)
552
    DIMAGES = c.list_images(detail=True)
553
    DFLAVORS = c.list_flavors(detail=True)
554

    
555
    #
556
    # Assemble all test cases
557
    #
558

    
559
    # Initial test cases
560
    cases = [UnauthorizedTestCase, FlavorsTestCase, ImagesTestCase]
561

    
562
    # Image test cases
563
    imageid = 1
564
    flavorid = opts.force_flavorid if opts.force_flavorid \
565
               else choice(filter(lambda x: x["disk"] >= 20, DFLAVORS))["id"]
566
    imagename = "Debian Base"
567
    personality = None
568
    servername = "%s-%s for %s" % (SNF_TEST_PREFIX, UNIQUE_RUN_ID, imagename)
569
    is_windows = imagename.lower().find("windows") >= 0
570
    case = _spawn_server_test_case(imageid=imageid, flavorid=flavorid,
571
                                   imagename=imagename,
572
                                   personality=personality,
573
                                   servername=servername,
574
                                   is_windows=is_windows,
575
                                   action_timeout=opts.action_timeout,
576
                                   build_warning=opts.build_warning,
577
                                   build_fail=opts.build_fail,
578
                                   query_interval=opts.query_interval)
579
    cases.append(case)
580

    
581
    # FIXME: logging, log, UNIQUE_RUN_ID arguments
582
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
583
    #unittest.main(verbosity=2, catchbreak=True)
584

    
585
    #
586
    # Run the resulting test suite
587
    #
588
    suites = map(unittest.TestLoader().loadTestsFromTestCase, cases)
589
    alltests = unittest.TestSuite(suites)
590
    unittest.TextTestRunner(verbosity=2).run(alltests)
591

    
592

    
593
if __name__ == "__main__":
594
    sys.exit(main())