Statistics
| Branch: | Tag: | Revision:

root / snf-tools / test_suite.py @ bc14ba88

History | View | Annotate | Download (27.8 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 struct
46
import sys
47
import time
48

    
49
from IPy import IP
50
from random import choice
51
from kamaki.client import Client, ClientError
52
from vncauthproxy.d3des import generate_response as d3des_generate_response
53

    
54
# Use backported unittest functionality if Python < 2.7
55
try:
56
    import unittest2 as unittest
57
except ImportError:
58
    if sys.version_info < (2, 7):
59
        raise Exception("The unittest2 package is required for Python < 2.7")
60
    import unittest
61

    
62

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

    
70
# Setup logging (FIXME - verigak)
71
logging.basicConfig(format="%(message)s")
72
log = logging.getLogger("snf-test")
73
log.setLevel(logging.INFO)
74

    
75

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

    
84

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

    
95
    def test_001_list_images(self):
96
        """Test image list actually returns images"""
97
        self.assertGreater(len(self.images), 0)
98

    
99
    def test_002_list_images_detailed(self):
100
        """Test detailed image list is the same length as list"""
101
        self.assertEqual(len(self.dimages), len(self.images))
102

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

    
109
    def test_004_unique_image_names(self):
110
        """Test images have unique names"""
111
        names = sorted(map(lambda x: x["name"], self.images))
112
        self.assertEqual(sorted(list(set(names))), names)
113

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

    
120

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

    
131
    def test_001_list_flavors(self):
132
        """Test flavor list actually returns flavors"""
133
        self.assertGreater(len(self.flavors), 0)
134

    
135
    def test_002_list_flavors_detailed(self):
136
        """Test detailed flavor list is the same length as list"""
137
        self.assertEquals(len(self.dflavors), len(self.flavors))
138

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

    
145
    def test_004_unique_flavor_names(self):
146
        """Test flavors have unique names"""
147
        names = sorted(map(lambda x: x["name"], self.flavors))
148
        self.assertEqual(sorted(list(set(names))), names)
149

    
150
    def test_005_well_formed_flavor_names(self):
151
        """Test flavors have names of the form CxxRyyDzz
152

153
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
154

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

    
161

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

    
172
    def test_001_list_servers(self):
173
        """Test server list actually returns servers"""
174
        self.assertGreater(len(self.servers), 0)
175

    
176
    def test_002_list_servers_detailed(self):
177
        """Test detailed server list is the same length as list"""
178
        self.assertEqual(len(self.dservers), len(self.servers))
179

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

    
186

    
187
# This class gets replicated into actual TestCases dynamically
188
class SpawnServerTestCase(unittest.TestCase):
189
    """Test scenario for server of the specified image"""
190

    
191
    @classmethod
192
    def setUpClass(cls):
193
        """Initialize a kamaki instance"""
194
        log.info("Spawning server for image `%s'", cls.imagename)
195
        cls.client = Client(API, TOKEN)
196

    
197
    def _get_ipv4(self, server):
198
        """Get the public IPv4 of a server from the detailed server info"""
199
        public_addrs = filter(lambda x: x["id"] == "public",
200
                              server["addresses"]["values"])
201
        self.assertEqual(len(public_addrs), 1)
202
        ipv4_addrs = filter(lambda x: x["version"] == 4,
203
                            public_addrs[0]["values"])
204
        self.assertEqual(len(ipv4_addrs), 1)
205
        return ipv4_addrs[0]["addr"]
206

    
207
    def _get_ipv6(self, server):
208
        """Get the public IPv6 of a server from the detailed server info"""
209
        public_addrs = filter(lambda x: x["id"] == "public",
210
                              server["addresses"]["values"])
211
        self.assertEqual(len(public_addrs), 1)
212
        ipv6_addrs = filter(lambda x: x["version"] == 6,
213
                            public_addrs[0]["values"])
214
        self.assertEqual(len(ipv6_addrs), 1)
215
        return ipv6_addrs[0]["addr"]
216

    
217
    def _connect_loginname(self, os):
218
        """Return the login name for connections based on the server OS"""
219
        if os in ('ubuntu', 'kubuntu', 'fedora'):
220
            return 'user'
221
        elif os == 'windows':
222
            return 'Administrator'
223
        else:
224
            return 'root'
225

    
226
    def _verify_server_status(self, current_status, new_status):
227
        """Verify a server has switched to a specified status"""
228
        log.info("Getting status for server %d, Image %s",
229
                 self.serverid, self.imagename)
230
        server = self.client.get_server_details(self.serverid)
231
        self.assertIn(server["status"], (current_status, new_status))
232
        self.assertEquals(server["status"], new_status)
233

    
234
    def _get_connected_tcp_socket(self, family, host, port):
235
        """Get a connected socket from the specified family to host:port"""
236
        sock = None
237
        for res in \
238
            socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
239
                               socket.AI_PASSIVE):
240
            af, socktype, proto, canonname, sa = res
241
            try:
242
                sock = socket.socket(af, socktype, proto)
243
            except socket.error as msg:
244
                sock = None
245
                continue
246
            try:
247
                sock.connect(sa)
248
            except socket.error as msg:
249
                sock.close()
250
                sock = None
251
                continue
252
        self.assertIsNotNone(sock)
253
        return sock
254

    
255
    def _ping_once(self, ipv6, ip):
256
        """Test server responds to a single IPv4 or IPv6 ping"""
257
        log.info("PING IPv%s to %s", "6" if ipv6 else "4", ip)
258
        cmd = "ping%s -c 2 -w 3 %s" % ("6" if ipv6 else "", ip)
259
        ping = subprocess.Popen(cmd, shell=True,
260
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
261
        (stdout, stderr) = ping.communicate()
262
        ret = ping.wait()
263
        self.assertEquals(ret, 0)
264

    
265
    def _get_hostname_over_ssh(self, hostip, username, password):
266
        ssh = paramiko.SSHClient()
267
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
268
        try:
269
            ssh.connect(hostip, username=username, password=password)
270
        except socket.error:
271
            raise AssertionError
272
        stdin, stdout, stderr = ssh.exec_command("hostname")
273
        lines = stdout.readlines()
274
        self.assertEqual(len(lines), 1)
275
        return lines
276

    
277
    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
278
                                   opmsg, callable, *args, **kwargs):
279
        if warn_timeout == fail_timeout:
280
            warn_timeout = fail_timeout + 1
281
        warn_tmout = time.time() + warn_timeout
282
        fail_tmout = time.time() + fail_timeout
283
        while True:
284
            if time.time() > warn_tmout:
285
                log.warning("Server %d: %s operation '%s' not done yet",
286
                            opmsg, self.serverid, self.servername)
287
            try:
288
                return callable(*args, **kwargs)
289
            except AssertionError:
290
                pass
291

    
292
            self.assertLess(time.time(), fail_tmout,
293
                            "operation '%s' timed out" % opmsg)
294
            time.sleep(self.query_interval)
295

    
296
    def _insist_on_tcp_connection(self, family, host, port):
297
        familystr = {socket.AF_INET: 'IPv4', socket.AF_INET6: 'IPv6'}
298
        msg = "connect over %s to %s:%s" % \
299
              (familystr.get(family, "Unknown"), host, port)
300
        sock = self._try_until_timeout_expires(
301
                self.action_timeout, self.action_timeout,
302
                msg, self._get_connected_tcp_socket,
303
                family, host, port)
304
        return sock
305

    
306
    def _insist_on_status_transition(self, current_status, new_status,
307
                                    fail_timeout, warn_timeout=None):
308
        msg = "status transition %s -> %s" % (current_status, new_status)
309
        if warn_timeout is None:
310
            warn_timeout = fail_timeout
311
        self._try_until_timeout_expires(warn_timeout, fail_timeout,
312
                                        msg, self._verify_server_status,
313
                                        current_status, new_status)
314

    
315
    def _insist_on_ssh_hostname(self, hostip, username, password):
316
        msg = "ssh to %s, as %s/%s" % (hostip, username, password)
317
        hostname = self._try_until_timeout_expires(
318
                self.action_timeout, self.action_timeout,
319
                msg, self._get_hostname_over_ssh,
320
                hostip, username, password)
321

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

    
325
    def _skipIf(self, condition, msg):
326
        if condition:
327
            self.skipTest(msg)
328

    
329
    def test_001_submit_create_server(self):
330
        """Test submit create server request"""
331
        server = self.client.create_server(self.servername, self.flavorid,
332
                                           self.imageid, self.personality)
333
        self.assertEqual(server["name"], self.servername)
334
        self.assertEqual(server["flavorRef"], self.flavorid)
335
        self.assertEqual(server["imageRef"], self.imageid)
336
        self.assertEqual(server["status"], "BUILD")
337

    
338
        # Update class attributes to reflect data on building server
339
        cls = type(self)
340
        cls.serverid = server["id"]
341
        cls.username = None
342
        cls.passwd = server["adminPass"]
343

    
344
    def test_002a_server_is_building_in_list(self):
345
        """Test server is in BUILD state, in server list"""
346
        servers = self.client.list_servers(detail=True)
347
        servers = filter(lambda x: x["name"] == self.servername, servers)
348
        self.assertEqual(len(servers), 1)
349
        server = servers[0]
350
        self.assertEqual(server["name"], self.servername)
351
        self.assertEqual(server["flavorRef"], self.flavorid)
352
        self.assertEqual(server["imageRef"], self.imageid)
353
        self.assertEqual(server["status"], "BUILD")
354

    
355
    def test_002b_server_is_building_in_details(self):
356
        """Test server is in BUILD state, in details"""
357
        server = self.client.get_server_details(self.serverid)
358
        self.assertEqual(server["name"], self.servername)
359
        self.assertEqual(server["flavorRef"], self.flavorid)
360
        self.assertEqual(server["imageRef"], self.imageid)
361
        self.assertEqual(server["status"], "BUILD")
362

    
363
    def test_002c_set_server_metadata(self):
364
        image = self.client.get_image_details(self.imageid)
365
        os = image["metadata"]["values"]["OS"]
366
        loginname = image["metadata"]["values"].get("loginname", None)
367
        self.client.update_server_metadata(self.serverid, OS=os)
368

    
369
        # Determine the username to use for future connections
370
        # to this host
371
        cls = type(self)
372
        cls.username = loginname
373
        if not cls.username:
374
            cls.username = self._connect_loginname(os)
375
        self.assertIsNotNone(cls.username)
376

    
377
    def test_002d_verify_server_metadata(self):
378
        """Test server metadata keys are set based on image metadata"""
379
        servermeta = self.client.get_server_metadata(self.serverid)
380
        imagemeta = self.client.get_image_metadata(self.imageid)
381
        self.assertEqual(servermeta["OS"], imagemeta["OS"])
382

    
383
    def test_003_server_becomes_active(self):
384
        """Test server becomes ACTIVE"""
385
        self._insist_on_status_transition("BUILD", "ACTIVE",
386
                                         self.build_fail, self.build_warning)
387

    
388
    def test_003a_get_server_oob_console(self):
389
        """Test getting OOB server console over VNC
390

391
        Implementation of RFB protocol follows
392
        http://www.realvnc.com/docs/rfbproto.pdf.
393

394
        """
395
        console = self.client.get_server_console(self.serverid)
396
        self.assertEquals(console['type'], "vnc")
397
        sock = self._insist_on_tcp_connection(socket.AF_UNSPEC,
398
                                        console["host"], console["port"])
399

    
400
        # Step 1. ProtocolVersion message (par. 6.1.1)
401
        version = sock.recv(1024)
402
        self.assertEquals(version, 'RFB 003.008\n')
403
        sock.send(version)
404

    
405
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
406
        sec = sock.recv(1024)
407
        self.assertEquals(list(sec), ['\x01', '\x02'])
408

    
409
        # Step 3. Request VNC Authentication (par 6.1.2)
410
        sock.send('\x02')
411

    
412
        # Step 4. Receive Challenge (par 6.2.2)
413
        challenge = sock.recv(1024)
414
        self.assertEquals(len(challenge), 16)
415

    
416
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
417
        response = d3des_generate_response(
418
            (console["password"] + '\0' * 8)[:8], challenge)
419
        sock.send(response)
420

    
421
        # Step 6. SecurityResult (par 6.1.3)
422
        result = sock.recv(4)
423
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
424
        sock.close()
425

    
426
    def test_004_server_has_ipv4(self):
427
        """Test active server has a valid IPv4 address"""
428
        server = self.client.get_server_details(self.serverid)
429
        ipv4 = self._get_ipv4(server)
430
        self.assertEquals(IP(ipv4).version(), 4)
431

    
432
    def test_005_server_has_ipv6(self):
433
        """Test active server has a valid IPv6 address"""
434
        server = self.client.get_server_details(self.serverid)
435
        ipv6 = self._get_ipv6(server)
436
        self.assertEquals(IP(ipv6).version(), 6)
437

    
438
    def test_006_server_responds_to_ping_IPv4(self):
439
        """Test server responds to ping on IPv4 address"""
440
        server = self.client.get_server_details(self.serverid)
441
        ip = self._get_ipv4(server)
442
        self._try_until_timeout_expires(self.action_timeout,
443
                                        self.action_timeout,
444
                                        "PING IPv4", self._ping_once,
445
                                        False, ip)
446

    
447
    def test_007_server_responds_to_ping_IPv6(self):
448
        """Test server responds to ping on IPv6 address"""
449
        server = self.client.get_server_details(self.serverid)
450
        ip = self._get_ipv6(server)
451
        self._try_until_timeout_expires(self.action_timeout,
452
                                        self.action_timeout,
453
                                        "PING IPv6", self._ping_once,
454
                                        True, ip)
455

    
456
    def test_008_submit_shutdown_request(self):
457
        """Test submit request to shutdown server"""
458
        self.client.shutdown_server(self.serverid)
459

    
460
    def test_009_server_becomes_stopped(self):
461
        """Test server becomes STOPPED"""
462
        self._insist_on_status_transition("ACTIVE", "STOPPED",
463
                                         self.action_timeout,
464
                                         self.action_timeout)
465

    
466
    def test_010_submit_start_request(self):
467
        """Test submit start server request"""
468
        self.client.start_server(self.serverid)
469

    
470
    def test_011_server_becomes_active(self):
471
        """Test server becomes ACTIVE again"""
472
        self._insist_on_status_transition("STOPPED", "ACTIVE",
473
                                         self.action_timeout,
474
                                         self.action_timeout)
475

    
476
    def test_011a_server_responds_to_ping_IPv4(self):
477
        """Test server OS is actually up and running again"""
478
        self.test_006_server_responds_to_ping_IPv4()
479

    
480
    def test_012_ssh_to_server_IPv4(self):
481
        """Test SSH to server public IPv4 works, verify hostname"""
482
        self._skipIf(self.is_windows, "only valid for Linux servers")
483
        server = self.client.get_server_details(self.serverid)
484
        self._insist_on_ssh_hostname(self._get_ipv4(server),
485
                                     self.username, self.passwd)
486

    
487
    def test_013_ssh_to_server_IPv6(self):
488
        """Test SSH to server public IPv6 works, verify hostname"""
489
        self._skipIf(self.is_windows, "only valid for Linux servers")
490
        server = self.client.get_server_details(self.serverid)
491
        self._insist_on_ssh_hostname(self._get_ipv6(server),
492
                                     self.username, self.passwd)
493

    
494
    def test_014_rdp_to_server_IPv4(self):
495
        "Test RDP connection to server public IPv4 works"""
496
        self._skipIf(not self.is_windows, "only valid for Windows servers")
497
        server = self.client.get_server_details(self.serverid)
498
        ipv4 = self._get_ipv4(server)
499
        sock = _insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
500

    
501
        # No actual RDP processing done. We assume the RDP server is there
502
        # if the connection to the RDP port is successful.
503
        sock.close()
504

    
505
    def test_015_rdp_to_server_IPv6(self):
506
        "Test RDP connection to server public IPv6 works"""
507
        self._skipIf(not self.is_windows, "only valid for Windows servers")
508
        server = self.client.get_server_details(self.serverid)
509
        ipv6 = self._get_ipv6(server)
510
        sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
511

    
512
        # No actual RDP processing done. We assume the RDP server is there
513
        # if the connection to the RDP port is successful.
514
        sock.close()
515

    
516
    def test_016_personality_is_enforced(self):
517
        """Test file injection for personality enforcement"""
518
        self._skipIf(self.is_windows, "only implemented for Linux servers")
519
        self.assertTrue(False, "test not implemented, will fail")
520

    
521

    
522
def _spawn_server_test_case(**kwargs):
523
    """Construct a new unit test case class from SpawnServerTestCase"""
524

    
525
    name = "SpawnServerTestCase_%s" % kwargs["imagename"].replace(" ", "_")
526
    cls = type(name, (SpawnServerTestCase,), kwargs)
527

    
528
    # Patch extra parameters into test names by manipulating method docstrings
529
    for (mname, m) in \
530
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
531
            if hasattr(m, __doc__):
532
                m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
533

    
534
    return cls
535

    
536

    
537
def cleanup_servers(delete_stale=False):
538
    c = Client(API, TOKEN)
539
    servers = c.list_servers()
540
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
541

    
542
    print >> sys.stderr, "Found these stale servers from previous runs:"
543
    print "    " + \
544
          "\n    ".join(["%d: %s" % (s['id'], s['name']) for s in stale])
545

    
546
    if delete_stale:
547
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
548
        for server in stale:
549
            c.delete_server(server['id'])
550
        print >> sys.stderr, "    ...done"
551
    else:
552
        print >> sys.stderr, "Use --delete-stale to delete them."
553

    
554

    
555
def parse_arguments(args):
556
    from optparse import OptionParser
557

    
558
    kw = {}
559
    kw["usage"] = "%prog [options]"
560
    kw["description"] = \
561
        "%prog runs a number of test scenarios on a " \
562
        "Synnefo deployment."
563

    
564
    parser = OptionParser(**kw)
565
    parser.disable_interspersed_args()
566
    parser.add_option("--failfast",
567
                      action="store_true", dest="failfast",
568
                      help="Fail immediately if one of the tests fails",
569
                      default=False)
570
    parser.add_option("--action-timeout",
571
                      action="store", type="int", dest="action_timeout",
572
                      metavar="TIMEOUT",
573
                      help="Wait SECONDS seconds for a server action to " \
574
                           "complete, then the test is considered failed",
575
                      default=20)
576
    parser.add_option("--build-warning",
577
                      action="store", type="int", dest="build_warning",
578
                      metavar="TIMEOUT",
579
                      help="Warn if TIMEOUT seconds have passed and a " \
580
                           "build operation is still pending",
581
                      default=600)
582
    parser.add_option("--build-fail",
583
                      action="store", type="int", dest="build_fail",
584
                      metavar="BUILD_TIMEOUT",
585
                      help="Fail the test if TIMEOUT seconds have passed " \
586
                           "and a build operation is still incomplete",
587
                      default=900)
588
    parser.add_option("--query-interval",
589
                      action="store", type="int", dest="query_interval",
590
                      metavar="INTERVAL",
591
                      help="Query server status when requests are pending " \
592
                           "every INTERVAL seconds",
593
                      default=3)
594
    parser.add_option("--build-fanout",
595
                      action="store", type="int", dest="build_fanout",
596
                      metavar="COUNT",
597
                      help="Test COUNT images in parallel, by submitting " \
598
                           "COUNT server build requests simultaneously",
599
                      default=1)
600
    parser.add_option("--force-flavor",
601
                      action="store", type="int", dest="force_flavorid",
602
                      metavar="FLAVOR ID",
603
                      help="Force all server creations to use the specified "\
604
                           "FLAVOR ID instead of a randomly chosen one, " \
605
                           "useful if disk space is scarce",
606
                      default=None)    # FIXME
607
    parser.add_option("--show-stale",
608
                      action="store_true", dest="show_stale",
609
                      help="Show stale servers from previous runs, whose "\
610
                           "name starts with '%s'" % SNF_TEST_PREFIX,
611
                      default=False)
612
    parser.add_option("--delete-stale",
613
                      action="store_true", dest="delete_stale",
614
                      help="Delete stale servers from previous runs, whose "\
615
                           "name starts with '%s'" % SNF_TEST_PREFIX,
616
                      default=False)
617

    
618
    # FIXME: Change the default for build-fanout to 10
619
    # FIXME: Allow the user to specify a specific set of Images to test
620

    
621
    (opts, args) = parser.parse_args(args)
622

    
623
    # Verify arguments
624
    if opts.delete_stale:
625
        opts.show_stale = True
626

    
627
    return (opts, args)
628

    
629

    
630
def main():
631
    """Assemble test cases into a test suite, and run it
632

633
    IMPORTANT: Tests have dependencies and have to be run in the specified
634
    order inside a single test case. They communicate through attributes of the
635
    corresponding TestCase class (shared fixtures). TestCase classes for
636
    distinct Images may be run in parallel.
637

638
    """
639

    
640
    (opts, args) = parse_arguments(sys.argv[1:])
641

    
642
    # Cleanup stale servers from previous runs
643
    if opts.show_stale:
644
        cleanup_servers(delete_stale=opts.delete_stale)
645
        return 0
646

    
647
    # Initialize a kamaki instance, get flavors, images
648
    c = Client(API, TOKEN)
649
    DIMAGES = c.list_images(detail=True)
650
    DFLAVORS = c.list_flavors(detail=True)
651

    
652
    #
653
    # Assemble all test cases
654
    #
655

    
656
    # Initial test cases
657
    cases = [UnauthorizedTestCase, FlavorsTestCase, ImagesTestCase]
658

    
659
    # Image test cases
660
    imageid = 1
661
    flavorid = opts.force_flavorid if opts.force_flavorid \
662
               else choice(filter(lambda x: x["disk"] >= 20, DFLAVORS))["id"]
663
    imagename = "Debian Base"
664
    personality = None
665
    servername = "%s-%s for %s" % (SNF_TEST_PREFIX, UNIQUE_RUN_ID, imagename)
666
    is_windows = imagename.lower().find("windows") >= 0
667
    case = _spawn_server_test_case(imageid=imageid, flavorid=flavorid,
668
                                   imagename=imagename,
669
                                   personality=personality,
670
                                   servername=servername,
671
                                   is_windows=is_windows,
672
                                   action_timeout=opts.action_timeout,
673
                                   build_warning=opts.build_warning,
674
                                   build_fail=opts.build_fail,
675
                                   query_interval=opts.query_interval)
676
    cases.append(case)
677

    
678
    # FIXME: logging, log, UNIQUE_RUN_ID arguments
679
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
680
    #unittest.main(verbosity=2, catchbreak=True)
681

    
682
    #
683
    # Run the resulting test suite
684
    #
685
    suites = map(unittest.TestLoader().loadTestsFromTestCase, cases)
686
    alltests = unittest.TestSuite(suites)
687
    unittest.TextTestRunner(verbosity=2, failfast=opts.failfast).run(alltests)
688

    
689

    
690
if __name__ == "__main__":
691
    sys.exit(main())