Statistics
| Branch: | Tag: | Revision:

root / snf-tools / test_suite.py @ 4fdd25ab

History | View | Annotate | Download (28.5 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
        server = self.client.get_server_details(self.serverid)
229
        self.assertIn(server["status"], (current_status, new_status))
230
        self.assertEquals(server["status"], new_status)
231

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

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

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

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

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

    
303
    def _insist_on_status_transition(self, current_status, new_status,
304
                                    fail_timeout, warn_timeout=None):
305
        msg = "Server %d: `%s', waiting for %s -> %s" % \
306
              (self.serverid, self.servername, current_status, new_status)
307
        if warn_timeout is None:
308
            warn_timeout = fail_timeout
309
        self._try_until_timeout_expires(warn_timeout, fail_timeout,
310
                                        msg, self._verify_server_status,
311
                                        current_status, new_status)
312

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

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

    
323
    def _skipIf(self, condition, msg):
324
        if condition:
325
            self.skipTest(msg)
326

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

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

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

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

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

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

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

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

    
386
    def test_003a_get_server_oob_console(self):
387
        """Test getting OOB server console over VNC
388

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

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

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

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

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

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

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

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

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

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

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

    
446
    def test_007_server_responds_to_ping_IPv6(self):
447
        """Test server responds to ping on IPv6 address"""
448
        server = self.client.get_server_details(self.serverid)
449
        ip = self._get_ipv6(server)
450
        self._try_until_timeout_expires(self.action_timeout,
451
                                        self.action_timeout,
452
                                        "PING IPv6 to %s" % ip,
453
                                        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
    def test_017_submit_delete_request(self):
522
        """Test submit request to delete server"""
523
        self.client.delete_server(self.serverid)
524

    
525
    def test_018_server_becomes_deleted(self):
526
        """Test server becomes DELETED"""
527
        self._insist_on_status_transition("ACTIVE", "DELETED",
528
                                         self.action_timeout,
529
                                         self.action_timeout)
530

    
531
    def test_019_server_no_longer_in_server_list(self):
532
        """Test server is no longer in server list"""
533
        servers = self.client.list_servers()
534
        self.assertNotIn(self.serverid, [s['id'] for s in servers])
535

    
536

    
537
def _spawn_server_test_case(**kwargs):
538
    """Construct a new unit test case class from SpawnServerTestCase"""
539

    
540
    name = "SpawnServerTestCase_%s" % kwargs["imagename"].replace(" ", "_")
541
    cls = type(name, (SpawnServerTestCase,), kwargs)
542

    
543
    # Patch extra parameters into test names by manipulating method docstrings
544
    for (mname, m) in \
545
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
546
            if hasattr(m, __doc__):
547
                m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
548

    
549
    return cls
550

    
551

    
552
def cleanup_servers(delete_stale=False):
553
    c = Client(API, TOKEN)
554
    servers = c.list_servers()
555
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
556

    
557
    if len(stale) == 0:
558
        return
559

    
560
    print >> sys.stderr, "Found these stale servers from previous runs:"
561
    print "    " + \
562
          "\n    ".join(["%d: %s" % (s['id'], s['name']) for s in stale])
563

    
564
    if delete_stale:
565
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
566
        for server in stale:
567
            c.delete_server(server['id'])
568
        print >> sys.stderr, "    ...done"
569
    else:
570
        print >> sys.stderr, "Use --delete-stale to delete them."
571

    
572

    
573
def parse_arguments(args):
574
    from optparse import OptionParser
575

    
576
    kw = {}
577
    kw["usage"] = "%prog [options]"
578
    kw["description"] = \
579
        "%prog runs a number of test scenarios on a " \
580
        "Synnefo deployment."
581

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

    
636
    # FIXME: Change the default for build-fanout to 10
637
    # FIXME: Allow the user to specify a specific set of Images to test
638

    
639
    (opts, args) = parser.parse_args(args)
640

    
641
    # Verify arguments
642
    if opts.delete_stale:
643
        opts.show_stale = True
644

    
645
    return (opts, args)
646

    
647

    
648
def main():
649
    """Assemble test cases into a test suite, and run it
650

651
    IMPORTANT: Tests have dependencies and have to be run in the specified
652
    order inside a single test case. They communicate through attributes of the
653
    corresponding TestCase class (shared fixtures). TestCase classes for
654
    distinct Images may be run in parallel.
655

656
    """
657

    
658
    (opts, args) = parse_arguments(sys.argv[1:])
659

    
660
    # Cleanup stale servers from previous runs
661
    if opts.show_stale:
662
        cleanup_servers(delete_stale=opts.delete_stale)
663
        return 0
664

    
665
    # Initialize a kamaki instance, get flavors, images
666
    c = Client(API, TOKEN)
667
    DIMAGES = c.list_images(detail=True)
668
    DFLAVORS = c.list_flavors(detail=True)
669

    
670
    #
671
    # Assemble all test cases
672
    #
673

    
674
    # Initial test cases
675
    cases = [UnauthorizedTestCase, FlavorsTestCase, ImagesTestCase]
676

    
677
    # Image test cases
678
    imageid = 1
679
    flavorid = opts.force_flavorid if opts.force_flavorid \
680
               else choice(filter(lambda x: x["disk"] >= 20, DFLAVORS))["id"]
681
    imagename = "Debian Base"
682
    personality = None
683
    servername = "%s-%s for %s" % (SNF_TEST_PREFIX, UNIQUE_RUN_ID, imagename)
684
    is_windows = imagename.lower().find("windows") >= 0
685
    case = _spawn_server_test_case(imageid=imageid, flavorid=flavorid,
686
                                   imagename=imagename,
687
                                   personality=personality,
688
                                   servername=servername,
689
                                   is_windows=is_windows,
690
                                   action_timeout=opts.action_timeout,
691
                                   build_warning=opts.build_warning,
692
                                   build_fail=opts.build_fail,
693
                                   query_interval=opts.query_interval)
694
    cases.append(case)
695

    
696
    # FIXME: logging, log, UNIQUE_RUN_ID arguments
697
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
698
    #unittest.main(verbosity=2, catchbreak=True)
699

    
700
    #
701
    # Run the resulting test suite
702
    #
703
    suites = map(unittest.TestLoader().loadTestsFromTestCase, cases)
704
    alltests = unittest.TestSuite(suites)
705
    unittest.TextTestRunner(verbosity=2, failfast=opts.failfast).run(alltests)
706

    
707

    
708
if __name__ == "__main__":
709
    sys.exit(main())