Revision 1b5d6e95

/dev/null
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 sys
49
import time
50
from base64 import b64encode
51
from IPy import IP
52
from multiprocessing import Process, Queue
53
from random import choice
54

  
55
from kamaki.clients.compute import ComputeClient
56
from kamaki.clients.cyclades import CycladesClient
57
from kamaki.clients import ClientError
58

  
59
from fabric.api import *
60

  
61
from vncauthproxy.d3des import generate_response as d3des_generate_response
62

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

  
71

  
72
API = None
73
TOKEN = None
74
DEFAULT_API = "https://cyclades.okeanos.grnet.gr/api/v1.1"
75

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

  
81
# Setup logging (FIXME - verigak)
82
logging.basicConfig(format="%(message)s")
83
log = logging.getLogger("burnin")
84
log.setLevel(logging.INFO)
85

  
86

  
87
class UnauthorizedTestCase(unittest.TestCase):
88
    def test_unauthorized_access(self):
89
        """Test access without a valid token fails"""
90
        log.info("Authentication test")
91

  
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

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

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

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

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

  
136

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

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

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

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

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

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

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

  
170
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
171

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

  
178

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

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

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

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

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

  
204

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

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

  
214
        cls.client = ComputeClient(API, TOKEN)
215
        cls.cyclades = CycladesClient(API, TOKEN)
216

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

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

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

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

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

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

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

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

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

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

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

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

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

  
349
    def _check_file_through_ssh(self, hostip, username, password,
350
                                remotepath, content):
351
        msg = "Trying file injection through SSH to %s, as %s/%s" % \
352
            (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
        sftp.close()
368
        transport.close()
369

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

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

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

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

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

  
387
        log.info("Server id: " + str(server["id"]))
388
        log.info("Server password: " + server["adminPass"])
389
        self.assertEqual(server["name"], self.servername)
390
        self.assertEqual(server["flavorRef"], self.flavorid)
391
        self.assertEqual(server["imageRef"], self.imageid)
392
        self.assertEqual(server["status"], "BUILD")
393

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

  
400
    def test_002a_server_is_building_in_list(self):
401
        """Test server is in BUILD state, in server list"""
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
        userlist = loginname.split()
434

  
435
        # Determine the username to use for future connections
436
        # to this host
437
        cls = type(self)
438

  
439
        if "root" in userlist:
440
            cls.username = "root"
441
        elif users == None:
442
            cls.username = self._connect_loginname(os)
443
        else:
444
            cls.username = choice(userlist)
445

  
446
        self.assertIsNotNone(cls.username)
447

  
448
    def test_002d_verify_server_metadata(self):
449
        """Test server metadata keys are set based on image metadata"""
450

  
451
        log.info("Verifying image metadata")
452

  
453
        servermeta = self.client.get_server_metadata(self.serverid)
454
        imagemeta = self.client.get_image_metadata(self.imageid)
455

  
456
        self.assertEqual(servermeta["OS"], imagemeta["os"])
457

  
458
    def test_003_server_becomes_active(self):
459
        """Test server becomes ACTIVE"""
460

  
461
        log.info("Waiting for server to become ACTIVE")
462

  
463
        self._insist_on_status_transition("BUILD", "ACTIVE",
464
                                         self.build_fail, self.build_warning)
465

  
466
    def test_003a_get_server_oob_console(self):
467
        """Test getting OOB server console over VNC
468

  
469
        Implementation of RFB protocol follows
470
        http://www.realvnc.com/docs/rfbproto.pdf.
471

  
472
        """
473
        console = self.cyclades.get_server_console(self.serverid)
474
        self.assertEquals(console['type'], "vnc")
475
        sock = self._insist_on_tcp_connection(socket.AF_INET,
476
                                        console["host"], console["port"])
477

  
478
        # Step 1. ProtocolVersion message (par. 6.1.1)
479
        version = sock.recv(1024)
480
        self.assertEquals(version, 'RFB 003.008\n')
481
        sock.send(version)
482

  
483
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
484
        sec = sock.recv(1024)
485
        self.assertEquals(list(sec), ['\x01', '\x02'])
486

  
487
        # Step 3. Request VNC Authentication (par 6.1.2)
488
        sock.send('\x02')
489

  
490
        # Step 4. Receive Challenge (par 6.2.2)
491
        challenge = sock.recv(1024)
492
        self.assertEquals(len(challenge), 16)
493

  
494
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
495
        response = d3des_generate_response(
496
            (console["password"] + '\0' * 8)[:8], challenge)
497
        sock.send(response)
498

  
499
        # Step 6. SecurityResult (par 6.1.3)
500
        result = sock.recv(4)
501
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
502
        sock.close()
503

  
504
    def test_004_server_has_ipv4(self):
505
        """Test active server has a valid IPv4 address"""
506

  
507
        log.info("Validate server's IPv4")
508

  
509
        server = self.client.get_server_details(self.serverid)
510
        ipv4 = self._get_ipv4(server)
511
        self.assertEquals(IP(ipv4).version(), 4)
512

  
513
    def test_005_server_has_ipv6(self):
514
        """Test active server has a valid IPv6 address"""
515

  
516
        log.info("Validate server's IPv6")
517

  
518
        server = self.client.get_server_details(self.serverid)
519
        ipv6 = self._get_ipv6(server)
520
        self.assertEquals(IP(ipv6).version(), 6)
521

  
522
    def test_006_server_responds_to_ping_IPv4(self):
523
        """Test server responds to ping on IPv4 address"""
524

  
525
        log.info("Testing if server responds to pings in IPv4")
526

  
527
        server = self.client.get_server_details(self.serverid)
528
        ip = self._get_ipv4(server)
529
        self._try_until_timeout_expires(self.action_timeout,
530
                                        self.action_timeout,
531
                                        "PING IPv4 to %s" % ip,
532
                                        self._ping_once,
533
                                        False, ip)
534

  
535
    def test_007_server_responds_to_ping_IPv6(self):
536
        """Test server responds to ping on IPv6 address"""
537

  
538
        log.info("Testing if server responds to pings in IPv6")
539

  
540
        server = self.client.get_server_details(self.serverid)
541
        ip = self._get_ipv6(server)
542
        self._try_until_timeout_expires(self.action_timeout,
543
                                        self.action_timeout,
544
                                        "PING IPv6 to %s" % ip,
545
                                        self._ping_once,
546
                                        True, ip)
547

  
548
    def test_008_submit_shutdown_request(self):
549
        """Test submit request to shutdown server"""
550

  
551
        log.info("Shutting down server")
552

  
553
        self.cyclades.shutdown_server(self.serverid)
554

  
555
    def test_009_server_becomes_stopped(self):
556
        """Test server becomes STOPPED"""
557

  
558
        log.info("Waiting until server becomes STOPPED")
559
        self._insist_on_status_transition("ACTIVE", "STOPPED",
560
                                         self.action_timeout,
561
                                         self.action_timeout)
562

  
563
    def test_010_submit_start_request(self):
564
        """Test submit start server request"""
565

  
566
        log.info("Starting server")
567

  
568
        self.cyclades.start_server(self.serverid)
569

  
570
    def test_011_server_becomes_active(self):
571
        """Test server becomes ACTIVE again"""
572

  
573
        log.info("Waiting until server becomes ACTIVE")
574
        self._insist_on_status_transition("STOPPED", "ACTIVE",
575
                                         self.action_timeout,
576
                                         self.action_timeout)
577

  
578
    def test_011a_server_responds_to_ping_IPv4(self):
579
        """Test server OS is actually up and running again"""
580

  
581
        log.info("Testing if server is actually up and running")
582

  
583
        self.test_006_server_responds_to_ping_IPv4()
584

  
585
    def test_012_ssh_to_server_IPv4(self):
586
        """Test SSH to server public IPv4 works, verify hostname"""
587

  
588
        self._skipIf(self.is_windows, "only valid for Linux servers")
589
        server = self.client.get_server_details(self.serverid)
590
        self._insist_on_ssh_hostname(self._get_ipv4(server),
591
                                     self.username, self.passwd)
592

  
593
    def test_013_ssh_to_server_IPv6(self):
594
        """Test SSH to server public IPv6 works, verify hostname"""
595
        self._skipIf(self.is_windows, "only valid for Linux servers")
596
        server = self.client.get_server_details(self.serverid)
597
        self._insist_on_ssh_hostname(self._get_ipv6(server),
598
                                     self.username, self.passwd)
599

  
600
    def test_014_rdp_to_server_IPv4(self):
601
        "Test RDP connection to server public IPv4 works"""
602
        self._skipIf(not self.is_windows, "only valid for Windows servers")
603
        server = self.client.get_server_details(self.serverid)
604
        ipv4 = self._get_ipv4(server)
605
        sock = _insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
606

  
607
        # No actual RDP processing done. We assume the RDP server is there
608
        # if the connection to the RDP port is successful.
609
        # FIXME: Use rdesktop, analyze exit code? see manpage [costasd]
610
        sock.close()
611

  
612
    def test_015_rdp_to_server_IPv6(self):
613
        "Test RDP connection to server public IPv6 works"""
614
        self._skipIf(not self.is_windows, "only valid for Windows servers")
615
        server = self.client.get_server_details(self.serverid)
616
        ipv6 = self._get_ipv6(server)
617
        sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
618

  
619
        # No actual RDP processing done. We assume the RDP server is there
620
        # if the connection to the RDP port is successful.
621
        sock.close()
622

  
623
    def test_016_personality_is_enforced(self):
624
        """Test file injection for personality enforcement"""
625
        self._skipIf(self.is_windows, "only implemented for Linux servers")
626
        self._skipIf(self.personality == None, "No personality file selected")
627

  
628
        log.info("Trying to inject file for personality enforcement")
629

  
630
        server = self.client.get_server_details(self.serverid)
631

  
632
        for inj_file in self.personality:
633
            equal_files = self._check_file_through_ssh(self._get_ipv4(server),
634
                                                       inj_file['owner'],
635
                                                       self.passwd,
636
                                                       inj_file['path'],
637
                                                       inj_file['contents'])
638
            self.assertTrue(equal_files)
639

  
640
    def test_017_submit_delete_request(self):
641
        """Test submit request to delete server"""
642

  
643
        log.info("Deleting server")
644

  
645
        self.client.delete_server(self.serverid)
646

  
647
    def test_018_server_becomes_deleted(self):
648
        """Test server becomes DELETED"""
649

  
650
        log.info("Testing if server becomes DELETED")
651

  
652
        self._insist_on_status_transition("ACTIVE", "DELETED",
653
                                         self.action_timeout,
654
                                         self.action_timeout)
655

  
656
    def test_019_server_no_longer_in_server_list(self):
657
        """Test server is no longer in server list"""
658

  
659
        log.info("Test if server is no longer listed")
660

  
661
        servers = self.client.list_servers()
662
        self.assertNotIn(self.serverid, [s["id"] for s in servers])
663

  
664

  
665
class NetworkTestCase(unittest.TestCase):
666
    """ Testing networking in cyclades """
667

  
668
    @classmethod
669
    def setUpClass(cls):
670
        "Initialize kamaki, get list of current networks"
671

  
672
        cls.client = CycladesClient(API, TOKEN)
673
        cls.compute = ComputeClient(API, TOKEN)
674

  
675
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
676
                                          TEST_RUN_ID,
677
                                          cls.imagename)
678

  
679
        #Dictionary initialization for the vms credentials
680
        cls.serverid = dict()
681
        cls.username = dict()
682
        cls.password = dict()
683

  
684
    def _get_ipv4(self, server):
685
        """Get the public IPv4 of a server from the detailed server info"""
686

  
687
        public_addrs = filter(lambda x: x["id"] == "public",
688
                              server["addresses"]["values"])
689
        self.assertEqual(len(public_addrs), 1)
690
        ipv4_addrs = filter(lambda x: x["version"] == 4,
691
                            public_addrs[0]["values"])
692
        self.assertEqual(len(ipv4_addrs), 1)
693
        return ipv4_addrs[0]["addr"]
694

  
695
    def _connect_loginname(self, os):
696
        """Return the login name for connections based on the server OS"""
697
        if os in ("Ubuntu", "Kubuntu", "Fedora"):
698
            return "user"
699
        elif os in ("windows", "windows_alpha1"):
700
            return "Administrator"
701
        else:
702
            return "root"
703

  
704
    def _ping_once(self, ip):
705

  
706
        """Test server responds to a single IPv4 or IPv6 ping"""
707
        cmd = "ping -c 2 -w 3 %s" % (ip)
708
        ping = subprocess.Popen(cmd, shell=True,
709
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
710
        (stdout, stderr) = ping.communicate()
711
        ret = ping.wait()
712

  
713
        return (ret == 0)
714

  
715
    def test_00001a_submit_create_server_A(self):
716
        """Test submit create server request"""
717

  
718
        log.info("Creating test server A")
719

  
720
        serverA = self.client.create_server(self.servername, self.flavorid,
721
                                            self.imageid, personality=None)
722

  
723
        self.assertEqual(serverA["name"], self.servername)
724
        self.assertEqual(serverA["flavorRef"], self.flavorid)
725
        self.assertEqual(serverA["imageRef"], self.imageid)
726
        self.assertEqual(serverA["status"], "BUILD")
727

  
728
        # Update class attributes to reflect data on building server
729
        self.serverid['A'] = serverA["id"]
730
        self.username['A'] = None
731
        self.password['A'] = serverA["adminPass"]
732

  
733
        log.info("Server A id:" + str(serverA["id"]))
734
        log.info("Server password " + (self.password['A']))
735

  
736
    def test_00001b_serverA_becomes_active(self):
737
        """Test server becomes ACTIVE"""
738

  
739
        log.info("Waiting until test server A becomes ACTIVE")
740

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

  
753
        self.assertTrue(active)
754

  
755
    def test_00002a_submit_create_server_B(self):
756
        """Test submit create server request"""
757

  
758
        log.info("Creating test server B")
759

  
760
        serverB = self.client.create_server(self.servername, self.flavorid,
761
                                            self.imageid, personality=None)
762

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

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

  
773
        log.info("Server B id: " + str(serverB["id"]))
774
        log.info("Password " + (self.password['B']))
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
    def test_001_create_network(self):
796
        """Test submit create network request"""
797

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

  
800
        name = SNF_TEST_PREFIX + TEST_RUN_ID
801
        previous_num = len(self.client.list_networks())
802
        network = self.client.create_network(name)
803

  
804
        #Test if right name is assigned
805
        self.assertEqual(network['name'], name)
806

  
807
        # Update class attributes
808
        cls = type(self)
809
        cls.networkid = network['id']
810
        networks = self.client.list_networks()
811

  
812
        #Test if new network is created
813
        self.assertTrue(len(networks) > previous_num)
814

  
815
    def test_002_connect_to_network(self):
816
        """Test connect VMs to network"""
817

  
818
        log.info("Connect VMs to private network")
819

  
820
        self.client.connect_server(self.serverid['A'], self.networkid)
821
        self.client.connect_server(self.serverid['B'], self.networkid)
822

  
823
        #Insist on connecting until action timeout
824
        fail_tmout = time.time() + self.action_timeout
825

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

  
838
        self.assertTrue(conn_exists)
839

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

  
843
        log.info("Rebooting server A")
844

  
845
        self.client.shutdown_server(self.serverid['A'])
846

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

  
858
        self.client.start_server(self.serverid['A'])
859

  
860
        while True:
861
            d = self.client.get_server_details(self.serverid['A'])
862
            status = d['status']
863
            if status == 'ACTIVE':
864
                active = True
865
                break
866
            elif time.time() > fail_tmout:
867
                self.assertLess(time.time(), fail_tmout)
868
            else:
869
                time.sleep(self.query_interval)
870

  
871
        self.assertTrue(active)
872

  
873
    def test_002b_ping_server_A(self):
874
        "Test if server A is pingable"
875

  
876
        log.info("Testing if server A is pingable")
877

  
878
        server = self.client.get_server_details(self.serverid['A'])
879
        ip = self._get_ipv4(server)
880

  
881
        fail_tmout = time.time() + self.action_timeout
882

  
883
        s = False
884

  
885
        while True:
886

  
887
            if self._ping_once(ip):
888
                s = True
889
                break
890

  
891
            elif time.time() > fail_tmout:
892
                self.assertLess(time.time(), fail_tmout)
893

  
894
            else:
895
                time.sleep(self.query_interval)
896

  
897
        self.assertTrue(s)
898

  
899
    def test_002c_reboot(self):
900
        """Reboot server B"""
901

  
902
        log.info("Rebooting server B")
903

  
904
        self.client.shutdown_server(self.serverid['B'])
905

  
906
        fail_tmout = time.time() + self.action_timeout
907
        while True:
908
            d = self.client.get_server_details(self.serverid['B'])
909
            status = d['status']
910
            if status == 'STOPPED':
911
                break
912
            elif time.time() > fail_tmout:
913
                self.assertLess(time.time(), fail_tmout)
914
            else:
915
                time.sleep(self.query_interval)
916

  
917
        self.client.start_server(self.serverid['B'])
918

  
919
        while True:
920
            d = self.client.get_server_details(self.serverid['B'])
921
            status = d['status']
922
            if status == 'ACTIVE':
923
                active = True
924
                break
925
            elif time.time() > fail_tmout:
926
                self.assertLess(time.time(), fail_tmout)
927
            else:
928
                time.sleep(self.query_interval)
929

  
930
        self.assertTrue(active)
931

  
932
    def test_002d_ping_server_B(self):
933
        """Test if server B is pingable"""
934

  
935
        log.info("Testing if server B is pingable")
936
        server = self.client.get_server_details(self.serverid['B'])
937
        ip = self._get_ipv4(server)
938

  
939
        fail_tmout = time.time() + self.action_timeout
940

  
941
        s = False
942

  
943
        while True:
944
            if self._ping_once(ip):
945
                s = True
946
                break
947

  
948
            elif time.time() > fail_tmout:
949
                self.assertLess(time.time(), fail_tmout)
950

  
951
            else:
952
                time.sleep(self.query_interval)
953

  
954
        self.assertTrue(s)
955

  
956
    def test_003a_setup_interface_A(self):
957
        """Set up eth1 for server A"""
958

  
959
        log.info("Setting up interface eth1 for server A")
960

  
961
        server = self.client.get_server_details(self.serverid['A'])
962
        image = self.client.get_image_details(self.imageid)
963
        os = image['metadata']['values']['os']
964

  
965
        users = image["metadata"]["values"].get("users", None)
966
        userlist = users.split()
967

  
968
        if "root" in userlist:
969
            loginname = "root"
970
        elif users == None:
971
            loginname = self._connect_loginname(os)
972
        else:
973
            loginname = choice(userlist)
974

  
975
        hostip = self._get_ipv4(server)
976
        myPass = self.password['A']
977

  
978
        log.info("SSH in server A as %s/%s" % (loginname, myPass))
979

  
980
        res = False
981

  
982
        if loginname != "root":
983
            with settings(
984
                hide('warnings', 'running'),
985
                warn_only=True,
986
                host_string=hostip,
987
                user=loginname, password=myPass
988
                ):
989

  
990
                if len(sudo('ifconfig eth1 192.168.0.12')) == 0:
991
                    res = True
992

  
993
        else:
994
            with settings(
995
                hide('warnings', 'running'),
996
                warn_only=True,
997
                host_string=hostip,
998
                user=loginname, password=myPass
999
                ):
1000

  
1001
                if len(run('ifconfig eth1 192.168.0.12')) == 0:
1002
                    res = True
1003

  
1004
        self.assertTrue(res)
1005

  
1006
    def test_003b_setup_interface_B(self):
1007
        """Setup eth1 for server B"""
1008

  
1009
        log.info("Setting up interface eth1 for server B")
1010

  
1011
        server = self.client.get_server_details(self.serverid['B'])
1012
        image = self.client.get_image_details(self.imageid)
1013
        os = image['metadata']['values']['os']
1014

  
1015
        users = image["metadata"]["values"].get("users", None)
1016
        userlist = users.split()
1017

  
1018
        if "root" in userlist:
1019
            loginname = "root"
1020
        elif users == None:
1021
            loginname = self._connect_loginname(os)
1022
        else:
1023
            loginname = choice(userlist)
1024

  
1025
        hostip = self._get_ipv4(server)
1026
        myPass = self.password['B']
1027

  
1028
        log.info("SSH in server B as %s/%s" % (loginname, myPass))
1029

  
1030
        res = False
1031

  
1032
        if loginname != "root":
1033
            with settings(
1034
                hide('warnings', 'running'),
1035
                warn_only=True,
1036
                host_string=hostip,
1037
                user=loginname, password=myPass
1038
                ):
1039

  
1040
                if len(sudo('ifconfig eth1 192.168.0.13')) == 0:
1041
                    res = True
1042

  
1043
        else:
1044
            with settings(
1045
                hide('warnings', 'running'),
1046
                warn_only=True,
1047
                host_string=hostip,
1048
                user=loginname, password=myPass
1049
                ):
1050

  
1051
                if len(run('ifconfig eth1 192.168.0.13')) == 0:
1052
                    res = True
1053

  
1054
        self.assertTrue(res)
1055

  
1056
    def test_003c_test_connection_exists(self):
1057
        """Ping server B from server A to test if connection exists"""
1058

  
1059
        log.info("Testing if server A is actually connected to server B")
1060

  
1061
        server = self.client.get_server_details(self.serverid['A'])
1062
        image = self.client.get_image_details(self.imageid)
1063
        os = image['metadata']['values']['os']
1064
        hostip = self._get_ipv4(server)
1065

  
1066
        users = image["metadata"]["values"].get("users", None)
1067
        userlist = users.split()
1068

  
1069
        if "root" in userlist:
1070
            loginname = "root"
1071
        elif users == None:
1072
            loginname = self._connect_loginname(os)
1073
        else:
1074
            loginname = choice(userlist)
1075

  
1076
        myPass = self.password['A']
1077

  
1078
        try:
1079
            ssh = paramiko.SSHClient()
1080
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1081
            ssh.connect(hostip, username=loginname, password=myPass)
1082
        except socket.error:
1083
            raise AssertionError
1084

  
1085
        cmd = "if ping -c 2 -w 3 192.168.0.13 >/dev/null; \
1086
               then echo \'True\'; fi;"
1087
        stdin, stdout, stderr = ssh.exec_command(cmd)
1088
        lines = stdout.readlines()
1089

  
1090
        exists = False
1091

  
1092
        if 'True\n' in lines:
1093
            exists = True
1094

  
1095
        self.assertTrue(exists)
1096

  
1097
#TODO: Test IPv6 private connectity
1098

  
1099
    def test_004_disconnect_from_network(self):
1100
        "Disconnecting server A and B from network"
1101

  
1102
        log.info("Disconnecting servers from private network")
1103

  
1104
        prev_state = self.client.get_network_details(self.networkid)
1105
        prev_conn = len(prev_state['servers']['values'])
1106

  
1107
        self.client.disconnect_server(self.serverid['A'], self.networkid)
1108
        self.client.disconnect_server(self.serverid['B'], self.networkid)
1109

  
1110
        #Insist on deleting until action timeout
1111
        fail_tmout = time.time() + self.action_timeout
1112

  
1113
        while True:
1114
            connected = (self.client.get_network_details(self.networkid))
1115
            connections = connected['servers']['values']
1116
            if ((self.serverid['A'] not in connections) and
1117
                (self.serverid['B'] not in connections)):
1118
                conn_exists = False
1119
                break
1120
            elif time.time() > fail_tmout:
1121
                self.assertLess(time.time(), fail_tmout)
1122
            else:
1123
                time.sleep(self.query_interval)
1124

  
1125
        self.assertFalse(conn_exists)
1126

  
1127
    def test_005_destroy_network(self):
1128
        """Test submit delete network request"""
1129

  
1130
        log.info("Submitting delete network request")
1131

  
1132
        self.client.delete_network(self.networkid)
1133
        networks = self.client.list_networks()
1134

  
1135
        curr_net = []
1136
        for net in networks:
1137
            curr_net.append(net['id'])
1138

  
1139
        self.assertTrue(self.networkid not in curr_net)
1140

  
1141
    def test_006_cleanup_servers(self):
1142
        """Cleanup servers created for this test"""
1143

  
1144
        log.info("Delete servers created for this test")
1145

  
1146
        self.compute.delete_server(self.serverid['A'])
1147
        self.compute.delete_server(self.serverid['B'])
1148

  
1149
        fail_tmout = time.time() + self.action_timeout
1150

  
1151
        #Ensure server gets deleted
1152
        status = dict()
1153

  
1154
        while True:
1155
            details = self.compute.get_server_details(self.serverid['A'])
1156
            status['A'] = details['status']
1157
            details = self.compute.get_server_details(self.serverid['B'])
1158
            status['B'] = details['status']
1159
            if (status['A'] == 'DELETED') and (status['B'] == 'DELETED'):
1160
                deleted = True
1161
                break
1162
            elif time.time() > fail_tmout:
1163
                self.assertLess(time.time(), fail_tmout)
1164
            else:
1165
                time.sleep(self.query_interval)
1166

  
1167
        self.assertTrue(deleted)
1168

  
1169

  
1170
class TestRunnerProcess(Process):
1171
    """A distinct process used to execute part of the tests in parallel"""
1172
    def __init__(self, **kw):
1173
        Process.__init__(self, **kw)
1174
        kwargs = kw["kwargs"]
1175
        self.testq = kwargs["testq"]
1176
        self.runner = kwargs["runner"]
1177

  
1178
    def run(self):
1179
        # Make sure this test runner process dies with the parent
1180
        # and is not left behind.
1181
        #
1182
        # WARNING: This uses the prctl(2) call and is
1183
        # Linux-specific.
1184
        prctl.set_pdeathsig(signal.SIGHUP)
1185

  
1186
        while True:
1187
            log.debug("I am process %d, GETting from queue is %s",
1188
                     os.getpid(), self.testq)
1189
            msg = self.testq.get()
1190
            log.debug("Dequeued msg: %s", msg)
1191

  
1192
            if msg == "TEST_RUNNER_TERMINATE":
1193
                raise SystemExit
1194
            elif issubclass(msg, unittest.TestCase):
1195
                # Assemble a TestSuite, and run it
1196
                suite = unittest.TestLoader().loadTestsFromTestCase(msg)
1197
                self.runner.run(suite)
1198
            else:
1199
                raise Exception("Cannot handle msg: %s" % msg)
1200

  
1201

  
1202
def _run_cases_in_parallel(cases, fanout=1, runner=None):
1203
    """Run instances of TestCase in parallel, in a number of distinct processes
1204

  
1205
    The cases iterable specifies the TestCases to be executed in parallel,
1206
    by test runners running in distinct processes.
1207
    The fanout parameter specifies the number of processes to spawn,
1208
    and defaults to 1.
1209
    The runner argument specifies the test runner class to use inside each
1210
    runner process.
1211

  
1212
    """
1213
    if runner is None:
1214
        runner = unittest.TextTestRunner(verbosity=2, failfast=True)
1215

  
1216
    # testq: The master process enqueues TestCase objects into this queue,
1217
    #        test runner processes pick them up for execution, in parallel.
1218
    testq = Queue()
1219
    runners = []
1220
    for i in xrange(0, fanout):
1221
        kwargs = dict(testq=testq, runner=runner)
1222
        runners.append(TestRunnerProcess(kwargs=kwargs))
1223

  
1224
    log.info("Spawning %d test runner processes", len(runners))
1225
    for p in runners:
1226
        p.start()
1227
    log.debug("Spawned %d test runners, PIDs are %s",
1228
              len(runners), [p.pid for p in runners])
1229

  
1230
    # Enqueue test cases
1231
    map(testq.put, cases)
1232
    map(testq.put, ["TEST_RUNNER_TERMINATE"] * len(runners))
1233

  
1234
    log.debug("Joining %d processes", len(runners))
1235
    for p in runners:
1236
        p.join()
1237
    log.debug("Done joining %d processes", len(runners))
1238

  
1239

  
1240
def _spawn_server_test_case(**kwargs):
1241
    """Construct a new unit test case class from SpawnServerTestCase"""
1242

  
1243
    name = "SpawnServerTestCase_%s" % kwargs["imageid"]
1244
    cls = type(name, (SpawnServerTestCase,), kwargs)
1245

  
1246
    # Patch extra parameters into test names by manipulating method docstrings
1247
    for (mname, m) in \
1248
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
1249
        if hasattr(m, __doc__):
1250
            m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
1251

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

  
1257

  
1258
def _spawn_network_test_case(**kwargs):
1259
    """Construct a new unit test case class from NetworkTestCase"""
1260

  
1261
    name = "NetworkTestCase" + TEST_RUN_ID
1262
    cls = type(name, (NetworkTestCase,), kwargs)
1263

  
1264
    # Make sure the class can be pickled, by listing it among
1265
    # the attributes of __main__. A PicklingError is raised otherwise.
1266
    setattr(__main__, name, cls)
1267
    return cls
1268

  
1269

  
1270
def cleanup_servers(delete_stale=False):
1271

  
1272
    c = ComputeClient(API, TOKEN)
1273

  
1274
    servers = c.list_servers()
1275
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
1276

  
1277
    if len(stale) == 0:
1278
        return
1279

  
1280
    print >> sys.stderr, "Found these stale servers from previous runs:"
1281
    print "    " + \
1282
          "\n    ".join(["%d: %s" % (s["id"], s["name"]) for s in stale])
1283

  
1284
    if delete_stale:
1285
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
1286
        for server in stale:
1287
            c.delete_server(server["id"])
1288
        print >> sys.stderr, "    ...done"
1289
    else:
1290
        print >> sys.stderr, "Use --delete-stale to delete them."
1291

  
1292

  
1293
def cleanup_networks(delete_stale=False):
1294

  
1295
    c = CycladesClient(API, TOKEN)
1296

  
1297
    networks = c.list_networks()
1298
    stale = [n for n in networks if n["name"].startswith(SNF_TEST_PREFIX)]
1299

  
1300
    if len(stale) == 0:
1301
        return
1302

  
1303
    print >> sys.stderr, "Found these stale networks from previous runs:"
1304
    print "    " + \
1305
          "\n    ".join(["%s: %s" % (str(n["id"]), n["name"]) for n in stale])
1306

  
1307
    if delete_stale:
1308
        print >> sys.stderr, "Deleting %d stale networks:" % len(stale)
1309
        for network in stale:
1310
            c.delete_network(network["id"])
1311
        print >> sys.stderr, "    ...done"
1312
    else:
1313
        print >> sys.stderr, "Use --delete-stale to delete them."
1314

  
1315

  
1316
def parse_arguments(args):
1317
    from optparse import OptionParser
1318

  
1319
    kw = {}
1320
    kw["usage"] = "%prog [options]"
1321
    kw["description"] = \
1322
        "%prog runs a number of test scenarios on a " \
1323
        "Synnefo deployment."
1324

  
1325
    parser = OptionParser(**kw)
1326
    parser.disable_interspersed_args()
1327
    parser.add_option("--api",
1328
                      action="store", type="string", dest="api",
1329
                      help="The API URI to use to reach the Synnefo API",
1330
                      default=DEFAULT_API)
1331
    parser.add_option("--token",
1332
                      action="store", type="string", dest="token",
1333
                      help="The token to use for authentication to the API")
1334
    parser.add_option("--nofailfast",
1335
                      action="store_true", dest="nofailfast",
1336
                      help="Do not fail immediately if one of the tests " \
1337
                           "fails (EXPERIMENTAL)",
1338
                      default=False)
1339
    parser.add_option("--action-timeout",
1340
                      action="store", type="int", dest="action_timeout",
1341
                      metavar="TIMEOUT",
1342
                      help="Wait SECONDS seconds for a server action to " \
1343
                           "complete, then the test is considered failed",
1344
                      default=100)
1345
    parser.add_option("--build-warning",
1346
                      action="store", type="int", dest="build_warning",
1347
                      metavar="TIMEOUT",
1348
                      help="Warn if TIMEOUT seconds have passed and a " \
1349
                           "build operation is still pending",
1350
                      default=600)
1351
    parser.add_option("--build-fail",
1352
                      action="store", type="int", dest="build_fail",
1353
                      metavar="BUILD_TIMEOUT",
1354
                      help="Fail the test if TIMEOUT seconds have passed " \
1355
                           "and a build operation is still incomplete",
1356
                      default=900)
1357
    parser.add_option("--query-interval",
1358
                      action="store", type="int", dest="query_interval",
1359
                      metavar="INTERVAL",
1360
                      help="Query server status when requests are pending " \
1361
                           "every INTERVAL seconds",
1362
                      default=3)
1363
    parser.add_option("--fanout",
1364
                      action="store", type="int", dest="fanout",
1365
                      metavar="COUNT",
1366
                      help="Spawn up to COUNT child processes to execute " \
1367
                           "in parallel, essentially have up to COUNT " \
1368
                           "server build requests outstanding (EXPERIMENTAL)",
1369
                      default=1)
1370
    parser.add_option("--force-flavor",
1371
                      action="store", type="int", dest="force_flavorid",
1372
                      metavar="FLAVOR ID",
1373
                      help="Force all server creations to use the specified "\
1374
                           "FLAVOR ID instead of a randomly chosen one, " \
1375
                           "useful if disk space is scarce",
1376
                      default=None)
1377
    parser.add_option("--image-id",
1378
                      action="store", type="string", dest="force_imageid",
1379
                      metavar="IMAGE ID",
1380
                      help="Test the specified image id, use 'all' to test " \
1381
                           "all available images (mandatory argument)",
1382
                      default=None)
1383
    parser.add_option("--show-stale",
1384
                      action="store_true", dest="show_stale",
1385
                      help="Show stale servers from previous runs, whose "\
1386
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1387
                      default=False)
1388
    parser.add_option("--delete-stale",
1389
                      action="store_true", dest="delete_stale",
1390
                      help="Delete stale servers from previous runs, whose "\
1391
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1392
                      default=False)
1393
    parser.add_option("--force-personality",
1394
                      action="store", type="string", dest="personality_path",
1395
                      help="Force a personality file injection.\
1396
                            File path required. ",
1397
                      default=None)
1398
    parser.add_option("--log-folder",
1399
                      action="store", type="string", dest="log_folder",
1400
                      help="Define the absolute path where the output \
1401
                            log is stored. ",
1402
                      default="/var/log/burnin/")
1403

  
1404
    # FIXME: Change the default for build-fanout to 10
1405
    # FIXME: Allow the user to specify a specific set of Images to test
1406

  
1407
    (opts, args) = parser.parse_args(args)
1408

  
1409
    # Verify arguments
1410
    if opts.delete_stale:
1411
        opts.show_stale = True
1412

  
1413
    if not opts.show_stale:
1414
        if not opts.force_imageid:
1415
            print >>sys.stderr, "The --image-id argument is mandatory."
1416
            parser.print_help()
1417
            sys.exit(1)
1418

  
1419
        if opts.force_imageid != 'all':
1420
            try:
1421
                opts.force_imageid = str(opts.force_imageid)
1422
            except ValueError:
1423
                print >>sys.stderr, "Invalid value specified for --image-id." \
1424
                                    "Use a valid id, or `all'."
1425
                sys.exit(1)
1426

  
1427
    return (opts, args)
1428

  
1429

  
1430
def main():
1431
    """Assemble test cases into a test suite, and run it
1432

  
1433
    IMPORTANT: Tests have dependencies and have to be run in the specified
1434
    order inside a single test case. They communicate through attributes of the
1435
    corresponding TestCase class (shared fixtures). Distinct subclasses of
1436
    TestCase MAY SHARE NO DATA, since they are run in parallel, in distinct
1437
    test runner processes.
1438

  
1439
    """
1440

  
1441
    (opts, args) = parse_arguments(sys.argv[1:])
1442

  
1443
    global API, TOKEN
1444
    API = opts.api
1445
    TOKEN = opts.token
1446

  
1447
    # Cleanup stale servers from previous runs
1448
    if opts.show_stale:
1449
        cleanup_servers(delete_stale=opts.delete_stale)
1450
        cleanup_networks(delete_stale=opts.delete_stale)
1451
        return 0
1452

  
1453
    # Initialize a kamaki instance, get flavors, images
1454

  
1455
    c = ComputeClient(API, TOKEN)
1456

  
1457
    DIMAGES = c.list_images(detail=True)
1458
    DFLAVORS = c.list_flavors(detail=True)
1459

  
1460
    # FIXME: logging, log, LOG PID, TEST_RUN_ID, arguments
1461
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
1462
    #unittest.main(verbosity=2, catchbreak=True)
1463

  
1464
    if opts.force_imageid == 'all':
1465
        test_images = DIMAGES
1466
    else:
1467
        test_images = filter(lambda x: x["id"] == opts.force_imageid, DIMAGES)
1468

  
1469
    #New folder for log per image
1470

  
1471
    if not os.path.exists(opts.log_folder):
1472
        os.mkdir(opts.log_folder)
1473

  
1474
    test_folder = os.path.join(opts.log_folder, TEST_RUN_ID)
1475
    os.mkdir(test_folder)
1476

  
1477
    for image in test_images:
1478

  
1479
        imageid = str(image["id"])
1480

  
1481
        if opts.force_flavorid:
1482
            flavorid = opts.force_flavorid
1483
        else:
1484
            flavorid = choice([f["id"] for f in DFLAVORS if f["disk"] >= 20])
1485

  
1486
        imagename = image["name"]
1487

  
1488
        #Personality dictionary for file injection test
1489
        if opts.personality_path != None:
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff