Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / tools / burnin.py @ f89d0238

History | View | Annotate | Download (53.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 paramiko
44
import prctl
45
import subprocess
46
import signal
47
import socket
48
import struct
49
import sys
50
import time
51
import hashlib
52
from base64 import b64encode
53
from pwd import getpwuid
54
from grp import getgrgid
55
from IPy import IP
56
from multiprocessing import Process, Queue
57
from random import choice
58

    
59
from kamaki.clients.compute import ComputeClient
60
from kamaki.clients.cyclades import CycladesClient
61

    
62
from fabric.api import *
63

    
64
from vncauthproxy.d3des import generate_response as d3des_generate_response
65

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

    
74

    
75
API = None
76
TOKEN = None
77
DEFAULT_API = "https://cyclades.okeanos.grnet.gr/api/v1.1"
78

    
79
# A unique id identifying this test run
80
TEST_RUN_ID = datetime.datetime.strftime(datetime.datetime.now(),
81
                                         "%Y%m%d%H%M%S")
82
SNF_TEST_PREFIX = "snf-test-"
83

    
84
# Setup logging (FIXME - verigak)
85
logging.basicConfig(format="%(message)s")
86
log = logging.getLogger("burnin")
87
log.setLevel(logging.INFO)
88

    
89
class UnauthorizedTestCase(unittest.TestCase):
90
    def test_unauthorized_access(self):
91
        """Test access without a valid token fails"""
92
        falseToken = '12345'
93
        c=ComputeClient(API, falseToken)
94

    
95
        with self.assertRaises(ClientError) as cm:
96
            c.list_servers()
97
        self.assertEqual(cm.exception.status, 401)
98

    
99

    
100
class ImagesTestCase(unittest.TestCase):
101
    """Test image lists for consistency"""
102
    @classmethod
103
    def setUpClass(cls):
104
        """Initialize kamaki, get (detailed) list of images"""
105
        log.info("Getting simple and detailed list of images")
106

    
107
        cls.client = ComputeClient(API, TOKEN)
108
        cls.images = cls.client.list_images()
109
        cls.dimages = cls.client.list_images(detail=True)
110

    
111
    def test_001_list_images(self):
112
        """Test image list actually returns images"""
113
        self.assertGreater(len(self.images), 0)
114
        self.assertEqual(len(self.images), -1)
115

    
116
    def test_002_list_images_detailed(self):
117
        """Test detailed image list is the same length as list"""
118
        self.assertEqual(len(self.dimages), len(self.images))
119
        self.assertEqual(len(self.dimages), 0)
120

    
121
    def test_003_same_image_names(self):
122
        """Test detailed and simple image list contain same names"""
123
        names = sorted(map(lambda x: x["name"], self.images))
124
        dnames = sorted(map(lambda x: x["name"], self.dimages))
125
        self.assertEqual(names, dnames)
126

    
127
    def test_004_unique_image_names(self):
128
        """Test images have unique names"""
129
        names = sorted(map(lambda x: x["name"], self.images))
130
        self.assertEqual(sorted(list(set(names))), names)
131

    
132
    def test_005_image_metadata(self):
133
        """Test every image has specific metadata defined"""
134
        keys = frozenset(["os", "description", "size"])
135
        for i in self.dimages:
136
            self.assertTrue(keys.issubset(i["metadata"]["values"].keys()))
137

    
138

    
139
class FlavorsTestCase(unittest.TestCase):
140
    """Test flavor lists for consistency"""
141
    @classmethod
142
    def setUpClass(cls):
143
        """Initialize kamaki, get (detailed) list of flavors"""
144
        log.info("Getting simple and detailed list of flavors")
145

    
146
        cls.client = ComputeClient(API, TOKEN)
147
        cls.flavors = cls.client.list_flavors()
148
        cls.dflavors = cls.client.list_flavors(detail=True)
149

    
150
    def test_001_list_flavors(self):
151
        """Test flavor list actually returns flavors"""
152
        self.assertGreater(len(self.flavors), 0)
153

    
154
    def test_002_list_flavors_detailed(self):
155
        """Test detailed flavor list is the same length as list"""
156
        self.assertEquals(len(self.dflavors), len(self.flavors))
157

    
158
    def test_003_same_flavor_names(self):
159
        """Test detailed and simple flavor list contain same names"""
160
        names = sorted(map(lambda x: x["name"], self.flavors))
161
        dnames = sorted(map(lambda x: x["name"], self.dflavors))
162
        self.assertEqual(names, dnames)
163

    
164
    def test_004_unique_flavor_names(self):
165
        """Test flavors have unique names"""
166
        names = sorted(map(lambda x: x["name"], self.flavors))
167
        self.assertEqual(sorted(list(set(names))), names)
168

    
169
    def test_005_well_formed_flavor_names(self):
170
        """Test flavors have names of the form CxxRyyDzz
171

172
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
173

174
        """
175
        for f in self.dflavors:
176
            self.assertEqual("C%dR%dD%d" % (f["cpu"], f["ram"], f["disk"]),
177
                             f["name"],
178
                             "Flavor %s does not match its specs." % f["name"])
179

    
180

    
181
class ServersTestCase(unittest.TestCase):
182
    """Test server lists for consistency"""
183
    @classmethod
184
    def setUpClass(cls):
185
        """Initialize kamaki, get (detailed) list of servers"""
186
        log.info("Getting simple and detailed list of servers")
187

    
188
        cls.client = ComputeClient(API, TOKEN)
189
        cls.servers = cls.client.list_servers()
190
        cls.dservers = cls.client.list_servers(detail=True)
191

    
192
    def test_001_list_servers(self):
193
        """Test server list actually returns servers"""
194
        self.assertGreater(len(self.servers), 0)
195

    
196
    def test_002_list_servers_detailed(self):
197
        """Test detailed server list is the same length as list"""
198
        self.assertEqual(len(self.dservers), len(self.servers))
199

    
200
    def test_003_same_server_names(self):
201
        """Test detailed and simple flavor list contain same names"""
202
        names = sorted(map(lambda x: x["name"], self.servers))
203
        dnames = sorted(map(lambda x: x["name"], self.dservers))
204
        self.assertEqual(names, dnames)
205

    
206

    
207
# This class gets replicated into actual TestCases dynamically
208
class SpawnServerTestCase(unittest.TestCase):
209
    """Test scenario for server of the specified image"""
210

    
211
    @classmethod
212
    def setUpClass(cls):
213
        """Initialize a kamaki instance"""
214
        log.info("Spawning server for image `%s'", cls.imagename)
215

    
216
        cls.client = ComputeClient(API, TOKEN)
217
        cls.cyclades = CycladesClient(API, TOKEN)
218

    
219
    def _get_ipv4(self, server):
220
        """Get the public IPv4 of a server from the detailed server info"""
221

    
222
        public_addrs = filter(lambda x: x["id"] == "public",
223
                              server["addresses"]["values"])
224
        self.assertEqual(len(public_addrs), 1)
225
        ipv4_addrs = filter(lambda x: x["version"] == 4,
226
                            public_addrs[0]["values"])
227
        self.assertEqual(len(ipv4_addrs), 1)
228
        return ipv4_addrs[0]["addr"]
229

    
230
    def _get_ipv6(self, server):
231
        """Get the public IPv6 of a server from the detailed server info"""
232
        public_addrs = filter(lambda x: x["id"] == "public",
233
                              server["addresses"]["values"])
234
        self.assertEqual(len(public_addrs), 1)
235
        ipv6_addrs = filter(lambda x: x["version"] == 6,
236
                            public_addrs[0]["values"])
237
        self.assertEqual(len(ipv6_addrs), 1)
238
        return ipv6_addrs[0]["addr"]
239

    
240
    def _connect_loginname(self, os):
241
        """Return the login name for connections based on the server OS"""
242
        if os in ("Ubuntu", "Kubuntu", "Fedora"):
243
            return "user"
244
        elif os in ("windows", "windows_alpha1"):
245
            return "Administrator"
246
        else:
247
            return "root"
248

    
249
    def _verify_server_status(self, current_status, new_status):
250
        """Verify a server has switched to a specified status"""
251
        server = self.client.get_server_details(self.serverid)
252
        if server["status"] not in (current_status, new_status):
253
            return None  # Do not raise exception, return so the test fails
254
        self.assertEquals(server["status"], new_status)
255

    
256
    def _get_connected_tcp_socket(self, family, host, port):
257
        """Get a connected socket from the specified family to host:port"""
258
        sock = None
259
        for res in \
260
            socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
261
                               socket.AI_PASSIVE):
262
            af, socktype, proto, canonname, sa = res
263
            try:
264
                sock = socket.socket(af, socktype, proto)
265
            except socket.error as msg:
266
                sock = None
267
                continue
268
            try:
269
                sock.connect(sa)
270
            except socket.error as msg:
271
                sock.close()
272
                sock = None
273
                continue
274
        self.assertIsNotNone(sock)
275
        return sock
276

    
277
    def _ping_once(self, ipv6, ip):
278
        """Test server responds to a single IPv4 or IPv6 ping"""
279
        cmd = "ping%s -c 2 -w 3 %s" % ("6" if ipv6 else "", ip)
280
        ping = subprocess.Popen(cmd, shell=True,
281
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
282
        (stdout, stderr) = ping.communicate()
283
        ret = ping.wait()
284
        self.assertEquals(ret, 0)
285

    
286
    def _get_hostname_over_ssh(self, hostip, username, password):
287
        ssh = paramiko.SSHClient()
288
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
289
        try:
290
            ssh.connect(hostip, username=username, password=password)
291
        except socket.error:
292
            raise AssertionError
293
        stdin, stdout, stderr = ssh.exec_command("hostname")
294
        lines = stdout.readlines()
295
        self.assertEqual(len(lines), 1)
296
        return lines[0]
297

    
298
    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
299
                                   opmsg, callable, *args, **kwargs):
300
        if warn_timeout == fail_timeout:
301
            warn_timeout = fail_timeout + 1
302
        warn_tmout = time.time() + warn_timeout
303
        fail_tmout = time.time() + fail_timeout
304
        while True:
305
            self.assertLess(time.time(), fail_tmout,
306
                            "operation `%s' timed out" % opmsg)
307
            if time.time() > warn_tmout:
308
                log.warning("Server %d: `%s' operation `%s' not done yet",
309
                            self.serverid, self.servername, opmsg)
310
            try:
311
                log.info("%s... " % opmsg)
312
                return callable(*args, **kwargs)
313
            except AssertionError:
314
                pass
315
            time.sleep(self.query_interval)
316

    
317
    def _insist_on_tcp_connection(self, family, host, port):
318
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
319
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
320
        msg = "connect over %s to %s:%s" % \
321
              (familystr.get(family, "Unknown"), host, port)
322
        sock = self._try_until_timeout_expires(
323
                self.action_timeout, self.action_timeout,
324
                msg, self._get_connected_tcp_socket,
325
                family, host, port)
326
        return sock
327

    
328
    def _insist_on_status_transition(self, current_status, new_status,
329
                                    fail_timeout, warn_timeout=None):
330
        msg = "Server %d: `%s', waiting for %s -> %s" % \
331
              (self.serverid, self.servername, current_status, new_status)
332
        if warn_timeout is None:
333
            warn_timeout = fail_timeout
334
        self._try_until_timeout_expires(warn_timeout, fail_timeout,
335
                                        msg, self._verify_server_status,
336
                                        current_status, new_status)
337
        # Ensure the status is actually the expected one
338
        server = self.client.get_server_details(self.serverid)
339
        self.assertEquals(server["status"], new_status)
340

    
341
    def _insist_on_ssh_hostname(self, hostip, username, password):
342
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
343
        hostname = self._try_until_timeout_expires(
344
                self.action_timeout, self.action_timeout,
345
                msg, self._get_hostname_over_ssh,
346
                hostip, username, password)
347

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

    
351
    def _check_file_through_ssh(self, hostip, username, password, remotepath, content):
352
        msg = "Trying file injection through SSH to %s, as %s/%s" % (hostip, username, password)
353
        log.info(msg)
354
        try:
355
            ssh = paramiko.SSHClient()
356
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
357
            ssh.connect(hostip, username=username, password=password)
358
        except socket.error:
359
            raise AssertionError
360
        
361
        transport = paramiko.Transport((hostip,22))
362
        transport.connect(username = username, password = password)
363

    
364
        localpath = '/tmp/'+SNF_TEST_PREFIX+'injection'
365
        sftp = paramiko.SFTPClient.from_transport(transport)
366
        sftp.get(remotepath, localpath)
367
        
368
        sftp.close()
369
        transport.close()
370

    
371
        f = open(localpath)
372
        remote_content = b64encode(f.read())
373

    
374
        # Check if files are the same
375
        return (remote_content == content)
376

    
377
    def _skipIf(self, condition, msg):
378
        if condition:
379
            self.skipTest(msg)
380

    
381
    def test_001_submit_create_server(self):
382
        """Test submit create server request"""
383

    
384
        log.info("Submit new server request")
385
        server = self.client.create_server(self.servername, self.flavorid,
386
                                           self.imageid, self.personality)
387

    
388
        self.assertEqual(server["name"], self.servername)
389
        self.assertEqual(server["flavorRef"], self.flavorid)
390
        self.assertEqual(server["imageRef"], self.imageid)
391
        self.assertEqual(server["status"], "BUILD")
392

    
393
        # Update class attributes to reflect data on building server
394
        cls = type(self)
395
        cls.serverid = server["id"]
396
        cls.username = None
397
        cls.passwd = server["adminPass"]
398

    
399
    def test_002a_server_is_building_in_list(self):
400
        """Test server is in BUILD state, in server list"""
401
        
402
        log.info("Server in BUILD state in server list")
403

    
404
        servers = self.client.list_servers(detail=True)
405
        servers = filter(lambda x: x["name"] == self.servername, servers)
406
        self.assertEqual(len(servers), 1)
407
        server = servers[0]
408
        self.assertEqual(server["name"], self.servername)
409
        self.assertEqual(server["flavorRef"], self.flavorid)
410
        self.assertEqual(server["imageRef"], self.imageid)
411
        self.assertEqual(server["status"], "BUILD")
412

    
413
    def test_002b_server_is_building_in_details(self):
414
        """Test server is in BUILD state, in details"""
415

    
416
        log.info("Server in BUILD state in details")
417

    
418
        server = self.client.get_server_details(self.serverid)
419
        self.assertEqual(server["name"], self.servername)
420
        self.assertEqual(server["flavorRef"], self.flavorid)
421
        self.assertEqual(server["imageRef"], self.imageid)
422
        self.assertEqual(server["status"], "BUILD")
423

    
424
    def test_002c_set_server_metadata(self):
425

    
426
        log.info("Creating server metadata")
427

    
428
        image = self.client.get_image_details(self.imageid)
429
        os = image["metadata"]["values"]["os"]
430
        loginname = image["metadata"]["values"].get("users", None)
431
        self.client.update_server_metadata(self.serverid, OS=os)
432

    
433
        # Determine the username to use for future connections
434
        # to this host
435
        cls = type(self)
436
        cls.username = loginname
437
        if not cls.username:
438
            cls.username = self._connect_loginname(os)
439
        self.assertIsNotNone(cls.username)
440

    
441
    def test_002d_verify_server_metadata(self):
442
        """Test server metadata keys are set based on image metadata"""
443

    
444
        log.info("Verifying image metadata")
445

    
446
        servermeta = self.client.get_server_metadata(self.serverid)
447
        imagemeta = self.client.get_image_metadata(self.imageid)
448
        self.assertEqual(servermeta["OS"], imagemeta["os"])
449

    
450
    def test_003_server_becomes_active(self):
451
        """Test server becomes ACTIVE"""
452

    
453
        log.info("Waiting for server to become ACTIVE")
454

    
455
        self._insist_on_status_transition("BUILD", "ACTIVE",
456
                                         self.build_fail, self.build_warning)
457

    
458
    def test_003a_get_server_oob_console(self):
459
        """Test getting OOB server console over VNC
460

461
        Implementation of RFB protocol follows
462
        http://www.realvnc.com/docs/rfbproto.pdf.
463

464
        """
465
        
466
        console = self.cyclades.get_server_console(self.serverid)
467
        self.assertEquals(console['type'], "vnc")
468
        sock = self._insist_on_tcp_connection(socket.AF_UNSPEC,
469
                                        console["host"], console["port"])
470

    
471
        # Step 1. ProtocolVersion message (par. 6.1.1)
472
        version = sock.recv(1024)
473
        self.assertEquals(version, 'RFB 003.008\n')
474
        sock.send(version)
475

    
476
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
477
        sec = sock.recv(1024)
478
        self.assertEquals(list(sec), ['\x01', '\x02'])
479

    
480
        # Step 3. Request VNC Authentication (par 6.1.2)
481
        sock.send('\x02')
482

    
483
        # Step 4. Receive Challenge (par 6.2.2)
484
        challenge = sock.recv(1024)
485
        self.assertEquals(len(challenge), 16)
486

    
487
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
488
        response = d3des_generate_response(
489
            (console["password"] + '\0' * 8)[:8], challenge)
490
        sock.send(response)
491

    
492
        # Step 6. SecurityResult (par 6.1.3)
493
        result = sock.recv(4)
494
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
495
        sock.close()
496
        
497
    def test_004_server_has_ipv4(self):
498
        """Test active server has a valid IPv4 address"""
499

    
500
        log.info("Testing server's IPv4")
501

    
502
        server = self.client.get_server_details(self.serverid)
503
        ipv4 = self._get_ipv4(server)
504
        self.assertEquals(IP(ipv4).version(), 4)
505

    
506
    def test_005_server_has_ipv6(self):
507
        """Test active server has a valid IPv6 address"""
508

    
509
        log.info("Testing server's IPv6")
510

    
511
        server = self.client.get_server_details(self.serverid)
512
        ipv6 = self._get_ipv6(server)
513
        self.assertEquals(IP(ipv6).version(), 6)
514

    
515
    def test_006_server_responds_to_ping_IPv4(self):
516
        """Test server responds to ping on IPv4 address"""
517

    
518
        log.info("Testing if server responds to pings in IPv4")
519

    
520
        server = self.client.get_server_details(self.serverid)
521
        ip = self._get_ipv4(server)
522
        self._try_until_timeout_expires(self.action_timeout,
523
                                        self.action_timeout,
524
                                        "PING IPv4 to %s" % ip,
525
                                        self._ping_once,
526
                                        False, ip)
527

    
528
    def test_007_server_responds_to_ping_IPv6(self):
529
        """Test server responds to ping on IPv6 address"""
530

    
531
        log.info("Testing if server responds to pings in IPv6")
532

    
533
        server = self.client.get_server_details(self.serverid)
534
        ip = self._get_ipv6(server)
535
        self._try_until_timeout_expires(self.action_timeout,
536
                                        self.action_timeout,
537
                                        "PING IPv6 to %s" % ip,
538
                                        self._ping_once,
539
                                        True, ip)
540

    
541
    def test_008_submit_shutdown_request(self):
542
        """Test submit request to shutdown server"""
543

    
544
        log.info("Shutting down server")
545

    
546
        self.cyclades.shutdown_server(self.serverid)
547

    
548
    def test_009_server_becomes_stopped(self):
549
        """Test server becomes STOPPED"""
550

    
551
        log.info("Waiting until server becomes STOPPED")
552
        
553
        self._insist_on_status_transition("ACTIVE", "STOPPED",
554
                                         self.action_timeout,
555
                                         self.action_timeout)
556

    
557
    def test_010_submit_start_request(self):
558
        """Test submit start server request"""
559

    
560
        log.info("Starting server")
561

    
562
        self.cyclades.start_server(self.serverid)
563

    
564
    def test_011_server_becomes_active(self):
565
        """Test server becomes ACTIVE again"""
566

    
567
        log.info("Waiting until server becomes ACTIVE")
568
        
569
        self._insist_on_status_transition("STOPPED", "ACTIVE",
570
                                         self.action_timeout,
571
                                         self.action_timeout)
572

    
573
    def test_011a_server_responds_to_ping_IPv4(self):
574
        """Test server OS is actually up and running again"""
575

    
576
        log.info("Testing if server is actually up and running")
577

    
578
        self.test_006_server_responds_to_ping_IPv4()
579

    
580
    def test_012_ssh_to_server_IPv4(self):
581
        """Test SSH to server public IPv4 works, verify hostname"""
582

    
583
        self._skipIf(self.is_windows, "only valid for Linux servers")
584
        server = self.client.get_server_details(self.serverid)
585
        self._insist_on_ssh_hostname(self._get_ipv4(server),
586
                                     self.username, self.passwd)
587

    
588
    def test_013_ssh_to_server_IPv6(self):
589
        """Test SSH to server public IPv6 works, verify hostname"""
590
        self._skipIf(self.is_windows, "only valid for Linux servers")
591
        server = self.client.get_server_details(self.serverid)
592
        self._insist_on_ssh_hostname(self._get_ipv6(server),
593
                                     self.username, self.passwd)
594

    
595
    def test_014_rdp_to_server_IPv4(self):
596
        "Test RDP connection to server public IPv4 works"""
597
        self._skipIf(not self.is_windows, "only valid for Windows servers")
598
        server = self.client.get_server_details(self.serverid)
599
        ipv4 = self._get_ipv4(server)
600
        sock = _insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
601

    
602
        # No actual RDP processing done. We assume the RDP server is there
603
        # if the connection to the RDP port is successful.
604
        # FIXME: Use rdesktop, analyze exit code? see manpage [costasd]
605
        sock.close()
606

    
607
    def test_015_rdp_to_server_IPv6(self):
608
        "Test RDP connection to server public IPv6 works"""
609
        self._skipIf(not self.is_windows, "only valid for Windows servers")
610
        server = self.client.get_server_details(self.serverid)
611
        ipv6 = self._get_ipv6(server)
612
        sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
613

    
614
        # No actual RDP processing done. We assume the RDP server is there
615
        # if the connection to the RDP port is successful.
616
        sock.close()
617

    
618
    def test_016_personality_is_enforced(self):
619
        """Test file injection for personality enforcement"""
620

    
621
        log.info("Trying to inject file for personality enforcement")
622

    
623
        self._skipIf(self.is_windows, "only implemented for Linux servers")
624
        self._skipIf(self.personality == None, "No personality file selected")
625

    
626
        server = self.client.get_server_details(self.serverid)
627

    
628
        for inj_file in self.personality:
629
            equal_files = self._check_file_through_ssh(self._get_ipv4(server), inj_file['owner'], 
630
                                                       self.passwd, inj_file['path'], inj_file['contents'])
631
            self.assertTrue(equal_files)
632
        
633

    
634
    def test_017_submit_delete_request(self):
635
        """Test submit request to delete server"""
636

    
637
        log.info("Deleting server")
638

    
639
        self.client.delete_server(self.serverid)
640

    
641
    def test_018_server_becomes_deleted(self):
642
        """Test server becomes DELETED"""
643

    
644
        log.info("Testing if server becomes DELETED")
645

    
646
        self._insist_on_status_transition("ACTIVE", "DELETED",
647
                                         self.action_timeout,
648
                                         self.action_timeout)
649

    
650
    def test_019_server_no_longer_in_server_list(self):
651
        """Test server is no longer in server list"""
652

    
653
        log.info("Test if server is no longer listed")
654

    
655
        servers = self.client.list_servers()
656
        self.assertNotIn(self.serverid, [s["id"] for s in servers])
657

    
658

    
659
class NetworkTestCase(unittest.TestCase):
660
    """ Testing networking in cyclades """
661
  
662
    @classmethod
663
    def setUpClass(cls):
664
        "Initialize kamaki, get list of current networks"
665

    
666
        cls.client = CycladesClient(API, TOKEN)
667
        cls.compute = ComputeClient(API, TOKEN)
668

    
669
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, cls.imagename)
670

    
671
        #Dictionary initialization for the vms credentials
672
        cls.serverid = dict()
673
        cls.username = dict()
674
        cls.password = dict()
675

    
676
    def _get_ipv4(self, server):
677
        """Get the public IPv4 of a server from the detailed server info"""
678

    
679
        public_addrs = filter(lambda x: x["id"] == "public",
680
                              server["addresses"]["values"])
681
        self.assertEqual(len(public_addrs), 1)
682
        ipv4_addrs = filter(lambda x: x["version"] == 4,
683
                            public_addrs[0]["values"])
684
        self.assertEqual(len(ipv4_addrs), 1)
685
        return ipv4_addrs[0]["addr"]
686
    
687

    
688
    def _connect_loginname(self, os):
689
        """Return the login name for connections based on the server OS"""
690
        if os in ("Ubuntu", "Kubuntu", "Fedora"):
691
            return "user"
692
        elif os in ("windows", "windows_alpha1"):
693
            return "Administrator"
694
        else:
695
            return "root"
696

    
697
    def _ping_once(self, ip):
698
    
699
        """Test server responds to a single IPv4 or IPv6 ping"""
700
        cmd = "ping -c 2 -w 3 %s" % (ip)
701
        ping = subprocess.Popen(cmd, shell=True,
702
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
703
        (stdout, stderr) = ping.communicate()
704
        ret = ping.wait()
705
        
706
        return (ret == 0)
707

    
708

    
709
    def test_00001a_submit_create_server_A(self):
710
        """Test submit create server request"""
711

    
712
        log.info("Creating test server A")
713

    
714
        serverA = self.client.create_server(self.servername, self.flavorid,
715
                                            self.imageid, personality=None)
716

    
717
        self.assertEqual(serverA["name"], self.servername)
718
        self.assertEqual(serverA["flavorRef"], self.flavorid)
719
        self.assertEqual(serverA["imageRef"], self.imageid)
720
        self.assertEqual(serverA["status"], "BUILD")
721

    
722
        # Update class attributes to reflect data on building server
723
        self.serverid['A'] = serverA["id"]
724
        self.username['A'] = None
725
        self.password['A'] = serverA["adminPass"]
726

    
727
        log.info("Created new server A:")
728
        log.info("Password " + (self.password['A']) + '\n')
729
        
730

    
731

    
732
    def test_00001b_serverA_becomes_active(self):
733
        """Test server becomes ACTIVE"""
734
        
735
        log.info("Waiting until test server A becomes ACTIVE")
736

    
737
        fail_tmout = time.time()+self.action_timeout
738
        while True:
739
            d = self.client.get_server_details(self.serverid['A'])
740
            status = d['status']
741
            if status == 'ACTIVE':
742
                active = True
743
                break
744
            elif time.time() > fail_tmout:
745
                self.assertLess(time.time(), fail_tmout)
746
            else:
747
                time.sleep(self.query_interval)
748

    
749
        self.assertTrue(active)
750

    
751
    
752
    def test_00002a_submit_create_server_B(self):
753
        """Test submit create server request"""
754
        
755
        log.info("Creating test server B")
756

    
757
        serverB = self.client.create_server(self.servername, self.flavorid,
758
                                            self.imageid, personality=None)
759

    
760

    
761
        self.assertEqual(serverB["name"], self.servername)
762
        self.assertEqual(serverB["flavorRef"], self.flavorid)
763
        self.assertEqual(serverB["imageRef"], self.imageid)
764
        self.assertEqual(serverB["status"], "BUILD")
765

    
766
        # Update class attributes to reflect data on building server
767
        self.serverid['B'] = serverB["id"]
768
        self.username['B'] = None
769
        self.password['B'] = serverB["adminPass"]
770

    
771
        log.info("Created new server B:")
772
        log.info("Password " + (self.password['B']) + '\n')
773

    
774

    
775

    
776
    def test_00002b_serverB_becomes_active(self):
777
        """Test server becomes ACTIVE"""
778

    
779
        log.info("Waiting until test server B becomes ACTIVE")
780

    
781
        fail_tmout = time.time()+self.action_timeout
782
        while True:
783
            d = self.client.get_server_details(self.serverid['B'])
784
            status = d['status']
785
            if status == 'ACTIVE':
786
                active = True
787
                break
788
            elif time.time() > fail_tmout:
789
                self.assertLess(time.time(), fail_tmout)
790
            else:
791
                time.sleep(self.query_interval)
792

    
793
        self.assertTrue(active)
794

    
795

    
796
    def test_001_create_network(self):
797
        """Test submit create network request"""
798

    
799
        log.info("Submit new network request")
800

    
801
        name = SNF_TEST_PREFIX+TEST_RUN_ID
802
        previous_num = len(self.client.list_networks())
803
        network =  self.client.create_network(name)        
804
       
805
        #Test if right name is assigned
806
        self.assertEqual(network['name'], name)
807
        
808
        # Update class attributes
809
        cls = type(self)
810
        cls.networkid = network['id']
811
        networks = self.client.list_networks()
812

    
813
        #Test if new network is created
814
        self.assertTrue(len(networks) > previous_num)
815
        
816
    
817
    def test_002_connect_to_network(self):
818
        """Test connect VMs to network"""
819

    
820
        log.info("Connect VMs to private network")
821

    
822
        self.client.connect_server(self.serverid['A'], self.networkid)
823
        self.client.connect_server(self.serverid['B'], self.networkid)
824
                
825
        #Insist on connecting until action timeout
826
        fail_tmout = time.time()+self.action_timeout
827

    
828
        while True:
829
            connected = (self.client.get_network_details(self.networkid))
830
            connections = connected['servers']['values']
831
            if (self.serverid['A'] in connections) and (self.serverid['B'] in connections):
832
                conn_exists = True
833
                break
834
            elif time.time() > fail_tmout:
835
                self.assertLess(time.time(), fail_tmout)
836
            else:
837
                time.sleep(self.query_interval)
838

    
839
        self.assertTrue(conn_exists)
840

    
841
    
842
    def test_002a_reboot(self):
843
        """Rebooting server A"""
844

    
845
        log.info("Rebooting server A")
846

    
847
        self.client.reboot_server(self.serverid['A']) 
848
       
849
        fail_tmout = time.time()+self.action_timeout
850
        while True:
851
            d = self.client.get_server_details(self.serverid['A'])
852
            status = d['status']
853
            if status == 'ACTIVE':
854
                active = True
855
                break
856
            elif time.time() > fail_tmout:
857
                self.assertLess(time.time(), fail_tmout)
858
            else:
859
                time.sleep(self.query_interval)
860
               
861
        self.assertTrue(active)
862

    
863

    
864
    def test_002b_ping_server_A(self):
865
        "Test if server A is pingable"
866

    
867
        log.info("Testing if server A is pingable")
868

    
869
        server = self.client.get_server_details(self.serverid['A'])
870
        ip = self._get_ipv4(server)
871
        
872
        fail_tmout = time.time()+self.action_timeout
873
        
874
        s = False
875

    
876
        while True:
877

    
878
            if self._ping_once(ip):
879
                s = True
880
                break
881

    
882
            elif time.time() > fail_tmout:
883
                self.assertLess(time.time(), fail_tmout)
884

    
885
            else:
886
                time.sleep(self.query_interval)
887

    
888
        self.assertTrue(s)
889

    
890

    
891
    def test_002c_reboot(self):
892
        """Reboot server B"""
893

    
894
        log.info("Rebooting server B")
895

    
896
        self.client.reboot_server(self.serverid['B']) 
897
        
898
        fail_tmout = time.time()+self.action_timeout
899
        while True:
900
            d = self.client.get_server_details(self.serverid['B'])
901
            status = d['status']
902
            if status == 'ACTIVE':
903
                active = True
904
                break
905
            elif time.time() > fail_tmout:
906
                self.assertLess(time.time(), fail_tmout)
907
            else:
908
                time.sleep(self.query_interval)
909
                
910
        self.assertTrue(active)
911
        
912

    
913
    def test_002d_ping_server_B(self):
914
        """Test if server B is pingable"""
915

    
916

    
917
        log.info("Testing if server B is pingable")
918
        server = self.client.get_server_details(self.serverid['B'])
919
        ip = self._get_ipv4(server)
920
        
921
        fail_tmout = time.time()+self.action_timeout
922
        
923
        s = False
924

    
925
        while True:
926
            if self._ping_once(ip):
927
                s = True
928
                break
929

    
930
            elif time.time() > fail_tmout:
931
                self.assertLess(time.time(), fail_tmout)
932

    
933
            else:
934
                time.sleep(self.query_interval)
935

    
936
        self.assertTrue(s)
937

    
938

    
939
    def test_003a_setup_interface_A(self):
940
        """Set up eth1 for server A"""
941

    
942
        log.info("Setting up interface eth1 for server A")
943

    
944
        server = self.client.get_server_details(self.serverid['A'])
945
        image = self.client.get_image_details(self.imageid)
946
        os = image['metadata']['values']['os']
947
        
948
        users = image["metadata"]["values"].get("users", None)
949
        userlist = users.split()
950

    
951
        if "root" in userlist:
952
            loginname = "root"
953
        elif users == None:
954
            loginname = self._connect_loginname(os)
955
        else:
956
            loginname = choice(userlist)
957

    
958
        hostip = self._get_ipv4(server)
959
        myPass = self.password['A']
960
        
961
        log.info("SSH in server A: \n")
962
        log.info("Username: " + loginname + '\n') 
963
        log.info("Password " + myPass + '\n')
964
        
965
        res = False
966

    
967
        if loginname != "root":
968
            with settings(
969
                hide('warnings', 'running'),
970
                warn_only=True, 
971
                host_string = hostip, 
972
                user = loginname, password = myPass
973
                ):
974

    
975
                if len(sudo('ifconfig eth1 192.168.0.12')) == 0: 
976
                    res = True
977
            
978
        else:
979
            with settings(
980
                hide('warnings', 'running'),
981
                warn_only=True, 
982
                host_string = hostip, 
983
                user = loginname, password = myPass
984
                ):
985

    
986
                if len(run('ifconfig eth1 192.168.0.12')) == 0:
987
                    res = True
988

    
989
        self.assertTrue(res)
990

    
991

    
992
    def test_003b_setup_interface_B(self):
993
        """Setup eth1 for server B"""
994

    
995
        log.info("Setting up interface eth1 for server B")
996

    
997
        server = self.client.get_server_details(self.serverid['B'])
998
        image = self.client.get_image_details(self.imageid)
999
        os = image['metadata']['values']['os']
1000
        
1001
        users = image["metadata"]["values"].get("users", None)
1002
        userlist = users.split()
1003

    
1004
        if "root" in userlist:
1005
            loginname = "root"
1006
        elif users == None:
1007
            loginname = self._connect_loginname(os)
1008
        else:
1009
            loginname = choice(userlist)
1010

    
1011
        hostip = self._get_ipv4(server)
1012
        myPass = self.password['B']
1013

    
1014
        log.info("SSH in server A: \n")
1015
        log.info("Username: " + loginname + '\n') 
1016
        log.info("Password " + myPass + '\n')
1017

    
1018
        res = False
1019

    
1020
        if loginname != "root":
1021
            with settings(
1022
                hide('warnings', 'running'),
1023
                warn_only=True, 
1024
                host_string = hostip, 
1025
                user = loginname, password = myPass
1026
                ):
1027
                
1028
                if len(sudo('ifconfig eth1 192.168.0.13'))== 0:
1029
                    res = True
1030
            
1031
        else :
1032
            with settings(
1033
                hide('warnings', 'running'),
1034
                warn_only=True, 
1035
                host_string = hostip, 
1036
                user = loginname, password = myPass
1037
                ):
1038

    
1039
                if len(run('ifconfig eth1 192.168.0.13')) == 0:
1040
                    res = True
1041

    
1042

    
1043
        self.assertTrue(res)
1044
            
1045

    
1046

    
1047
    def test_003c_test_connection_exists(self):
1048
        """Ping server B from server A to test if connection exists"""
1049

    
1050
        log.info("Testing if server A is actually connected to server B")
1051

    
1052
        server = self.client.get_server_details(self.serverid['A'])
1053
        image = self.client.get_image_details(self.imageid)
1054
        os = image['metadata']['values']['os']
1055
        hostip = self._get_ipv4(server)
1056

    
1057
        users = image["metadata"]["values"].get("users", None)
1058
        userlist = users.split()
1059

    
1060
        if "root" in userlist:
1061
            loginname = "root"
1062
        elif users == None:
1063
            loginname = self._connect_loginname(os)
1064
        else:
1065
            loginname = choice(userlist)
1066

    
1067
        myPass = self.password['A']
1068

    
1069
        try:
1070
            ssh = paramiko.SSHClient()
1071
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1072
            ssh.connect(hostip, username = loginname, password = myPass)
1073
        except socket.error:
1074
            raise AssertionError
1075

    
1076
        cmd = "if ping -c 2 -w 3 192.168.0.13 >/dev/null; then echo \'True\'; fi;"
1077
        stdin, stdout, stderr = ssh.exec_command(cmd)
1078
        lines = stdout.readlines()
1079

    
1080
        exists = False
1081

    
1082
        if 'True\n' in lines:
1083
            exists = True
1084

    
1085
        self.assertTrue(exists)
1086

    
1087

    
1088
#TODO: Test IPv6 private connectity
1089

    
1090

    
1091
    def test_004_disconnect_from_network(self):
1092
        "Disconnecting server A and B from network"
1093

    
1094
        log.info("Disconnecting servers from private network")
1095

    
1096
        prev_state = self.client.get_network_details(self.networkid)
1097
        prev_conn = len(prev_state['servers']['values'])
1098

    
1099
        self.client.disconnect_server(self.serverid['A'], self.networkid)
1100
        self.client.disconnect_server(self.serverid['B'], self.networkid)
1101

    
1102
        #Insist on deleting until action timeout
1103
        fail_tmout = time.time()+self.action_timeout
1104

    
1105
        while True:
1106
            connected = (self.client.get_network_details(self.networkid))
1107
            connections = connected['servers']['values']
1108
            if (self.serverid['A'] not in connections) and (self.serverid['B'] not in connections):
1109
                conn_exists = False
1110
                break
1111
            elif time.time() > fail_tmout:
1112
                self.assertLess(time.time(), fail_tmout)
1113
            else:
1114
                time.sleep(self.query_interval)
1115

    
1116
        self.assertFalse(conn_exists)
1117

    
1118
    def test_005_destroy_network(self):
1119
        """Test submit delete network request"""
1120

    
1121
        log.info("Submitting delete network request")
1122

    
1123
        self.client.delete_network(self.networkid)        
1124
        networks = self.client.list_networks()
1125

    
1126
        curr_net = []
1127
        for net in networks:
1128
            curr_net.append(net['id'])
1129

    
1130
        self.assertTrue(self.networkid not in curr_net)
1131
        
1132
    def test_006_cleanup_servers(self):
1133
        """Cleanup servers created for this test"""
1134

    
1135
        log.info("Delete servers created for this test")
1136

    
1137
        self.compute.delete_server(self.serverid['A'])
1138
        self.compute.delete_server(self.serverid['B'])
1139

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

    
1142
        #Ensure server gets deleted
1143
        status = dict() 
1144

    
1145
        while True:
1146
            details = self.compute.get_server_details(self.serverid['A'])
1147
            status['A'] = details['status']
1148
            details = self.compute.get_server_details(self.serverid['B'])
1149
            status['B'] = details['status']
1150
            if (status['A'] == 'DELETED') and (status['B'] == 'DELETED'):
1151
                deleted = True
1152
                break
1153
            elif time.time() > fail_tmout: 
1154
                self.assertLess(time.time(), fail_tmout)
1155
            else:
1156
                time.sleep(self.query_interval)
1157
                
1158
        self.assertTrue(deleted)
1159

    
1160

    
1161
class TestRunnerProcess(Process):
1162
    """A distinct process used to execute part of the tests in parallel"""
1163
    def __init__(self, **kw):
1164
        Process.__init__(self, **kw)
1165
        kwargs = kw["kwargs"]
1166
        self.testq = kwargs["testq"]
1167
        self.runner = kwargs["runner"]
1168

    
1169
    def run(self):
1170
        # Make sure this test runner process dies with the parent
1171
        # and is not left behind.
1172
        #
1173
        # WARNING: This uses the prctl(2) call and is
1174
        # Linux-specific.
1175
        prctl.set_pdeathsig(signal.SIGHUP)
1176

    
1177
        while True:
1178
            log.debug("I am process %d, GETting from queue is %s",
1179
                     os.getpid(), self.testq)
1180
            msg = self.testq.get()
1181
            log.debug("Dequeued msg: %s", msg)
1182

    
1183
            if msg == "TEST_RUNNER_TERMINATE":
1184
                raise SystemExit
1185
            elif issubclass(msg, unittest.TestCase):
1186
                # Assemble a TestSuite, and run it
1187
                suite = unittest.TestLoader().loadTestsFromTestCase(msg)
1188
                self.runner.run(suite)
1189
            else:
1190
                raise Exception("Cannot handle msg: %s" % msg)
1191

    
1192

    
1193

    
1194
def _run_cases_in_parallel(cases, fanout=1, runner=None):
1195
    """Run instances of TestCase in parallel, in a number of distinct processes
1196

1197
    The cases iterable specifies the TestCases to be executed in parallel,
1198
    by test runners running in distinct processes.
1199
    The fanout parameter specifies the number of processes to spawn,
1200
    and defaults to 1.
1201
    The runner argument specifies the test runner class to use inside each
1202
    runner process.
1203

1204
    """
1205
    if runner is None:
1206
        runner = unittest.TextTestRunner(verbosity=2, failfast=True)
1207

    
1208
    # testq: The master process enqueues TestCase objects into this queue,
1209
    #        test runner processes pick them up for execution, in parallel.
1210
    testq = Queue()
1211
    runners = []
1212
    for i in xrange(0, fanout):
1213
        kwargs = dict(testq=testq, runner=runner)
1214
        runners.append(TestRunnerProcess(kwargs=kwargs))
1215

    
1216
    log.info("Spawning %d test runner processes", len(runners))
1217
    for p in runners:
1218
        p.start()
1219
    log.debug("Spawned %d test runners, PIDs are %s",
1220
              len(runners), [p.pid for p in runners])
1221

    
1222
    # Enqueue test cases
1223
    map(testq.put, cases)
1224
    map(testq.put, ["TEST_RUNNER_TERMINATE"] * len(runners))
1225

    
1226
    log.debug("Joining %d processes", len(runners))
1227
    for p in runners:
1228
        p.join()
1229
    log.debug("Done joining %d processes", len(runners))
1230

    
1231

    
1232
def _spawn_server_test_case(**kwargs):
1233
    """Construct a new unit test case class from SpawnServerTestCase"""
1234

    
1235
    name = "SpawnServerTestCase_%s" % kwargs["imageid"]
1236
    cls = type(name, (SpawnServerTestCase,), kwargs)
1237

    
1238
    # Patch extra parameters into test names by manipulating method docstrings
1239
    for (mname, m) in \
1240
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
1241
            if hasattr(m, __doc__):
1242
                m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
1243

    
1244
    # Make sure the class can be pickled, by listing it among
1245
    # the attributes of __main__. A PicklingError is raised otherwise.
1246
    setattr(__main__, name, cls)
1247
    return cls 
1248

    
1249
def _spawn_network_test_case(**kwargs):
1250
    """Construct a new unit test case class from NetworkTestCase"""
1251

    
1252
    name = "NetworkTestCase"+TEST_RUN_ID
1253
    cls = type(name, (NetworkTestCase,), kwargs)
1254

    
1255
    # Make sure the class can be pickled, by listing it among
1256
    # the attributes of __main__. A PicklingError is raised otherwise.
1257
    setattr(__main__, name, cls)
1258
    return cls 
1259

    
1260

    
1261
def cleanup_servers(delete_stale=False):
1262

    
1263
    c = ComputeClient(API, TOKEN)
1264

    
1265
    servers = c.list_servers()
1266
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
1267

    
1268
    if len(stale) == 0:
1269
        return
1270

    
1271
    print >> sys.stderr, "Found these stale servers from previous runs:"
1272
    print "    " + \
1273
          "\n    ".join(["%d: %s" % (s["id"], s["name"]) for s in stale])
1274

    
1275
    if delete_stale:
1276
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
1277
        for server in stale:
1278
            c.delete_server(server["id"])
1279
        print >> sys.stderr, "    ...done"
1280
    else:
1281
        print >> sys.stderr, "Use --delete-stale to delete them."
1282

    
1283

    
1284
def parse_arguments(args):
1285
    from optparse import OptionParser
1286

    
1287
    kw = {}
1288
    kw["usage"] = "%prog [options]"
1289
    kw["description"] = \
1290
        "%prog runs a number of test scenarios on a " \
1291
        "Synnefo deployment."
1292

    
1293
    parser = OptionParser(**kw)
1294
    parser.disable_interspersed_args()
1295
    parser.add_option("--api",
1296
                      action="store", type="string", dest="api",
1297
                      help="The API URI to use to reach the Synnefo API",
1298
                      default=DEFAULT_API)
1299
    parser.add_option("--token",
1300
                      action="store", type="string", dest="token",
1301
                      help="The token to use for authentication to the API")
1302
    parser.add_option("--nofailfast",
1303
                      action="store_true", dest="nofailfast",
1304
                      help="Do not fail immediately if one of the tests " \
1305
                           "fails (EXPERIMENTAL)",
1306
                      default=False)
1307
    parser.add_option("--action-timeout",
1308
                      action="store", type="int", dest="action_timeout",
1309
                      metavar="TIMEOUT",
1310
                      help="Wait SECONDS seconds for a server action to " \
1311
                           "complete, then the test is considered failed",
1312
                      default=100)
1313
    parser.add_option("--build-warning",
1314
                      action="store", type="int", dest="build_warning",
1315
                      metavar="TIMEOUT",
1316
                      help="Warn if TIMEOUT seconds have passed and a " \
1317
                           "build operation is still pending",
1318
                      default=600)
1319
    parser.add_option("--build-fail",
1320
                      action="store", type="int", dest="build_fail",
1321
                      metavar="BUILD_TIMEOUT",
1322
                      help="Fail the test if TIMEOUT seconds have passed " \
1323
                           "and a build operation is still incomplete",
1324
                      default=900)
1325
    parser.add_option("--query-interval",
1326
                      action="store", type="int", dest="query_interval",
1327
                      metavar="INTERVAL",
1328
                      help="Query server status when requests are pending " \
1329
                           "every INTERVAL seconds",
1330
                      default=3)
1331
    parser.add_option("--fanout",
1332
                      action="store", type="int", dest="fanout",
1333
                      metavar="COUNT",
1334
                      help="Spawn up to COUNT child processes to execute " \
1335
                           "in parallel, essentially have up to COUNT " \
1336
                           "server build requests outstanding (EXPERIMENTAL)",
1337
                      default=1)
1338
    parser.add_option("--force-flavor",
1339
                      action="store", type="int", dest="force_flavorid",
1340
                      metavar="FLAVOR ID",
1341
                      help="Force all server creations to use the specified "\
1342
                           "FLAVOR ID instead of a randomly chosen one, " \
1343
                           "useful if disk space is scarce",
1344
                      default=None)
1345
    parser.add_option("--image-id",
1346
                      action="store", type="string", dest="force_imageid",
1347
                      metavar="IMAGE ID",
1348
                      help="Test the specified image id, use 'all' to test " \
1349
                           "all available images (mandatory argument)",
1350
                      default=None)
1351
    parser.add_option("--show-stale",
1352
                      action="store_true", dest="show_stale",
1353
                      help="Show stale servers from previous runs, whose "\
1354
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1355
                      default=False)
1356
    parser.add_option("--delete-stale",
1357
                      action="store_true", dest="delete_stale",
1358
                      help="Delete stale servers from previous runs, whose "\
1359
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1360
                      default=False)
1361
    parser.add_option("--force-personality",
1362
                      action="store", type="string", dest="personality_path",
1363
                      help="Force a personality file injection. File path required. ",
1364
                      default=None)
1365
    
1366

    
1367
    # FIXME: Change the default for build-fanout to 10
1368
    # FIXME: Allow the user to specify a specific set of Images to test
1369

    
1370
    (opts, args) = parser.parse_args(args)
1371

    
1372
    # Verify arguments
1373
    if opts.delete_stale:
1374
        opts.show_stale = True
1375

    
1376
    if not opts.show_stale:
1377
        if not opts.force_imageid:
1378
            print >>sys.stderr, "The --image-id argument is mandatory."
1379
            parser.print_help()
1380
            sys.exit(1)
1381

    
1382
        if opts.force_imageid != 'all':
1383
            try:
1384
                opts.force_imageid = str(opts.force_imageid)
1385
            except ValueError:
1386
                print >>sys.stderr, "Invalid value specified for --image-id." \
1387
                                    "Use a valid id, or `all'."
1388
                sys.exit(1)
1389

    
1390
    return (opts, args)
1391

    
1392

    
1393
def main():
1394
    """Assemble test cases into a test suite, and run it
1395

1396
    IMPORTANT: Tests have dependencies and have to be run in the specified
1397
    order inside a single test case. They communicate through attributes of the
1398
    corresponding TestCase class (shared fixtures). Distinct subclasses of
1399
    TestCase MAY SHARE NO DATA, since they are run in parallel, in distinct
1400
    test runner processes.
1401

1402
    """
1403

    
1404
    (opts, args) = parse_arguments(sys.argv[1:])
1405

    
1406
    global API, TOKEN
1407
    API = opts.api
1408
    TOKEN = opts.token
1409

    
1410
    # Cleanup stale servers from previous runs
1411
    if opts.show_stale:
1412
        cleanup_servers(delete_stale=opts.delete_stale)
1413
        return 0
1414

    
1415
    # Initialize a kamaki instance, get flavors, images
1416

    
1417
    c = ComputeClient(API, TOKEN)
1418

    
1419
    DIMAGES = c.list_images(detail=True)
1420
    DFLAVORS = c.list_flavors(detail=True)
1421

    
1422
    # FIXME: logging, log, LOG PID, TEST_RUN_ID, arguments
1423
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
1424
    #unittest.main(verbosity=2, catchbreak=True)
1425

    
1426
    if opts.force_imageid == 'all':
1427
        test_images = DIMAGES
1428
    else:
1429
        test_images = filter(lambda x: x["id"] == opts.force_imageid, DIMAGES)
1430

    
1431
    for image in test_images:
1432
        imageid = str(image["id"])
1433
        flavorid = choice([f["id"] for f in DFLAVORS if f["disk"] >= 20])
1434
        imagename = image["name"]
1435
        
1436
        #Personality dictionary for file injection test
1437
        if opts.personality_path != None:
1438
            f = open(opts.personality_path)
1439
            content = b64encode(f.read())
1440
            personality = []
1441
            st = os.stat(opts.personality_path)
1442
            personality.append({
1443
                    'path': '/root/test_inj_file',
1444
                    'owner': 'root',
1445
                    'group': 'root',
1446
                    'mode': 0x7777 & st.st_mode,
1447
                    'contents': content
1448
                    })
1449
        else:
1450
            personality = None
1451

    
1452
        servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, imagename)
1453
        is_windows = imagename.lower().find("windows") >= 0
1454
        
1455

    
1456
    ServerTestCase = _spawn_server_test_case(imageid=imageid, flavorid=flavorid,
1457
                                             imagename=imagename,
1458
                                             personality=personality,
1459
                                             servername=servername,
1460
                                             is_windows=is_windows,
1461
                                             action_timeout=opts.action_timeout,
1462
                                             build_warning=opts.build_warning,
1463
                                             build_fail=opts.build_fail,
1464
                                             query_interval=opts.query_interval,
1465
                                             )
1466

    
1467
    
1468
    NetworkTestCase = _spawn_network_test_case(action_timeout = opts.action_timeout,
1469
                                               imageid = imageid,
1470
                                               flavorid = flavorid,
1471
                                               imagename=imagename,
1472
                                               query_interval = opts.query_interval,
1473
                                               )
1474
    
1475

    
1476
    seq_cases = [UnauthorizedTestCase, ImagesTestCase, FlavorsTestCase, ServersTestCase, ServerTestCase]
1477

    
1478
#    seq_cases = [NetworkTestCase]
1479

    
1480

    
1481
    os.mkdir(TEST_RUN_ID)
1482
    
1483
    for case in seq_cases:
1484
        log_file = TEST_RUN_ID+'/'+'details_'+(case.__name__)+"_"+TEST_RUN_ID+'.log'
1485
        fail_file = TEST_RUN_ID+'/'+'failed_'+(case.__name__)+"_"+TEST_RUN_ID+'.log'
1486
        error_file = TEST_RUN_ID+'/'+'error_'+(case.__name__)+"_"+TEST_RUN_ID+'.log'
1487

    
1488
        f = open(log_file, "w")
1489
        fail = open(fail_file, "w")
1490
        error = open(error_file, "w")
1491

    
1492
        suite = unittest.TestLoader().loadTestsFromTestCase(case)
1493
        runner = unittest.TextTestRunner(f, verbosity=2)
1494
        result = runner.run(suite)
1495
        
1496
        error.write("Testcases errors: \n\n")
1497
        for res in result.errors:
1498
            error.write(str(res[0])+'\n')
1499
            error.write(str(res[0].__doc__) + '\n')
1500
            error.write('\n')
1501

    
1502
            
1503
        fail.write("Testcases failures: \n\n")
1504
        for res in result.failures:
1505
            fail.write(str(res[0])+'\n')
1506
            fail.write(str(res[0].__doc__) + '\n')
1507
            fail.write('\n')
1508
        
1509

    
1510

    
1511
if __name__ == "__main__":
1512
    sys.exit(main())