Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin.py @ 846980fe

History | View | Annotate | Download (57.4 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 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.image import ImageClient
58
from kamaki.clients import ClientError
59

    
60
from fabric.api import *
61

    
62
from vncauthproxy.d3des import generate_response as d3des_generate_response
63

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

    
72

    
73
API = None
74
TOKEN = None
75
DEFAULT_API = "https://cyclades.okeanos.grnet.gr/api/v1.1"
76
DEFAULT_PLANKTON = "https://cyclades.okeanos.grnet.gr/plankton"
77
DEFAULT_PLANKTON_USER = "images@okeanos.grnet.gr"
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

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

    
95
        falseToken = '12345'
96
        c = ComputeClient(API, falseToken)
97

    
98
        with self.assertRaises(ClientError) as cm:
99
            c.list_servers()
100
            self.assertEqual(cm.exception.status, 401)
101

    
102

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

    
110
        cls.client = ComputeClient(API, TOKEN)
111
        cls.plankton = ImageClient(PLANKTON, TOKEN)
112
        cls.images = cls.plankton.list_public()
113
        cls.dimages = cls.plankton.list_public(detail=True)
114

    
115
    def test_001_list_images(self):
116
        """Test image list actually returns images"""
117
        self.assertGreater(len(self.images), 0)
118

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

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

    
129
    def test_004_unique_image_names(self):
130
        """Test system images have unique names"""
131
        sys_images = filter(lambda x: x['owner'] == PLANKTON_USER,
132
                            self.dimages)
133
        names = sorted(map(lambda x: x["name"], sys_images))
134
        self.assertEqual(sorted(list(set(names))), names)
135

    
136
    def test_005_image_metadata(self):
137
        """Test every image has specific metadata defined"""
138
        keys = frozenset(["os", "description", "size"])
139
        details = self.client.list_images(detail=True)
140
        for i in details:
141
            self.assertTrue(keys.issubset(i["metadata"]["values"].keys()))
142

    
143

    
144
class FlavorsTestCase(unittest.TestCase):
145
    """Test flavor lists for consistency"""
146
    @classmethod
147
    def setUpClass(cls):
148
        """Initialize kamaki, get (detailed) list of flavors"""
149
        log.info("Getting simple and detailed list of flavors")
150

    
151
        cls.client = ComputeClient(API, TOKEN)
152
        cls.flavors = cls.client.list_flavors()
153
        cls.dflavors = cls.client.list_flavors(detail=True)
154

    
155
    def test_001_list_flavors(self):
156
        """Test flavor list actually returns flavors"""
157
        self.assertGreater(len(self.flavors), 0)
158

    
159
    def test_002_list_flavors_detailed(self):
160
        """Test detailed flavor list is the same length as list"""
161
        self.assertEquals(len(self.dflavors), len(self.flavors))
162

    
163
    def test_003_same_flavor_names(self):
164
        """Test detailed and simple flavor list contain same names"""
165
        names = sorted(map(lambda x: x["name"], self.flavors))
166
        dnames = sorted(map(lambda x: x["name"], self.dflavors))
167
        self.assertEqual(names, dnames)
168

    
169
    def test_004_unique_flavor_names(self):
170
        """Test flavors have unique names"""
171
        names = sorted(map(lambda x: x["name"], self.flavors))
172
        self.assertEqual(sorted(list(set(names))), names)
173

    
174
    def test_005_well_formed_flavor_names(self):
175
        """Test flavors have names of the form CxxRyyDzz
176

177
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
178

179
        """
180
        for f in self.dflavors:
181
            self.assertEqual("C%dR%dD%d" % (f["cpu"], f["ram"], f["disk"]),
182
                             f["name"],
183
                             "Flavor %s does not match its specs." % f["name"])
184

    
185

    
186
class ServersTestCase(unittest.TestCase):
187
    """Test server lists for consistency"""
188
    @classmethod
189
    def setUpClass(cls):
190
        """Initialize kamaki, get (detailed) list of servers"""
191
        log.info("Getting simple and detailed list of servers")
192

    
193
        cls.client = ComputeClient(API, TOKEN)
194
        cls.servers = cls.client.list_servers()
195
        cls.dservers = cls.client.list_servers(detail=True)
196

    
197
    # def test_001_list_servers(self):
198
    #     """Test server list actually returns servers"""
199
    #     self.assertGreater(len(self.servers), 0)
200

    
201
    def test_002_list_servers_detailed(self):
202
        """Test detailed server list is the same length as list"""
203
        self.assertEqual(len(self.dservers), len(self.servers))
204

    
205
    def test_003_same_server_names(self):
206
        """Test detailed and simple flavor list contain same names"""
207
        names = sorted(map(lambda x: x["name"], self.servers))
208
        dnames = sorted(map(lambda x: x["name"], self.dservers))
209
        self.assertEqual(names, dnames)
210

    
211

    
212
# This class gets replicated into actual TestCases dynamically
213
class SpawnServerTestCase(unittest.TestCase):
214
    """Test scenario for server of the specified image"""
215

    
216
    @classmethod
217
    def setUpClass(cls):
218
        """Initialize a kamaki instance"""
219
        log.info("Spawning server for image `%s'", cls.imagename)
220

    
221
        cls.client = ComputeClient(API, TOKEN)
222
        cls.cyclades = CycladesClient(API, TOKEN)
223

    
224
    def _get_ipv4(self, server):
225
        """Get the public IPv4 of a server from the detailed server info"""
226

    
227
        public_addrs = filter(lambda x: x["id"] == "public",
228
                              server["addresses"]["values"])
229
        self.assertEqual(len(public_addrs), 1)
230
        ipv4_addrs = filter(lambda x: x["version"] == 4,
231
                            public_addrs[0]["values"])
232
        self.assertEqual(len(ipv4_addrs), 1)
233
        return ipv4_addrs[0]["addr"]
234

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

    
245
    def _connect_loginname(self, os):
246
        """Return the login name for connections based on the server OS"""
247
        if os in ("Ubuntu", "Kubuntu", "Fedora"):
248
            return "user"
249
        elif os in ("windows", "windows_alpha1"):
250
            return "Administrator"
251
        else:
252
            return "root"
253

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

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

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

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

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

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

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

    
346
    def _insist_on_ssh_hostname(self, hostip, username, password):
347
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
348
        hostname = self._try_until_timeout_expires(
349
                self.action_timeout, self.action_timeout,
350
                msg, self._get_hostname_over_ssh,
351
                hostip, username, password)
352

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

    
356
    def _check_file_through_ssh(self, hostip, username, password,
357
                                remotepath, content):
358
        msg = "Trying file injection through SSH to %s, as %s/%s" % \
359
            (hostip, username, password)
360
        log.info(msg)
361
        try:
362
            ssh = paramiko.SSHClient()
363
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
364
            ssh.connect(hostip, username=username, password=password)
365
        except socket.error:
366
            raise AssertionError
367

    
368
        transport = paramiko.Transport((hostip, 22))
369
        transport.connect(username=username, password=password)
370

    
371
        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
372
        sftp = paramiko.SFTPClient.from_transport(transport)
373
        sftp.get(remotepath, localpath)
374
        sftp.close()
375
        transport.close()
376

    
377
        f = open(localpath)
378
        remote_content = b64encode(f.read())
379

    
380
        # Check if files are the same
381
        return (remote_content == content)
382

    
383
    def _skipIf(self, condition, msg):
384
        if condition:
385
            self.skipTest(msg)
386

    
387
    def test_001_submit_create_server(self):
388
        """Test submit create server request"""
389

    
390
        log.info("Submit new server request")
391
        server = self.client.create_server(self.servername, self.flavorid,
392
                                           self.imageid, self.personality)
393

    
394
        log.info("Server id: " + str(server["id"]))
395
        log.info("Server password: " + server["adminPass"])
396
        self.assertEqual(server["name"], self.servername)
397
        self.assertEqual(server["flavorRef"], self.flavorid)
398
        self.assertEqual(server["imageRef"], self.imageid)
399
        self.assertEqual(server["status"], "BUILD")
400

    
401
        # Update class attributes to reflect data on building server
402
        cls = type(self)
403
        cls.serverid = server["id"]
404
        cls.username = None
405
        cls.passwd = server["adminPass"]
406

    
407
    def test_002a_server_is_building_in_list(self):
408
        """Test server is in BUILD state, in server list"""
409
        log.info("Server in BUILD state in server list")
410

    
411
        servers = self.client.list_servers(detail=True)
412
        servers = filter(lambda x: x["name"] == self.servername, servers)
413
        self.assertEqual(len(servers), 1)
414
        server = servers[0]
415
        self.assertEqual(server["name"], self.servername)
416
        self.assertEqual(server["flavorRef"], self.flavorid)
417
        self.assertEqual(server["imageRef"], self.imageid)
418
        self.assertEqual(server["status"], "BUILD")
419

    
420
    def test_002b_server_is_building_in_details(self):
421
        """Test server is in BUILD state, in details"""
422

    
423
        log.info("Server in BUILD state in details")
424

    
425
        server = self.client.get_server_details(self.serverid)
426
        self.assertEqual(server["name"], self.servername)
427
        self.assertEqual(server["flavorRef"], self.flavorid)
428
        self.assertEqual(server["imageRef"], self.imageid)
429
        self.assertEqual(server["status"], "BUILD")
430

    
431
    def test_002c_set_server_metadata(self):
432

    
433
        log.info("Creating server metadata")
434

    
435
        image = self.client.get_image_details(self.imageid)
436
        os = image["metadata"]["values"]["os"]
437
        users = image["metadata"]["values"].get("users", None)
438
        self.client.update_server_metadata(self.serverid, OS=os)
439
        
440
        userlist = users.split()
441

    
442
        # Determine the username to use for future connections
443
        # to this host
444
        cls = type(self)
445

    
446
        if "root" in userlist:
447
            cls.username = "root"
448
        elif users == None:
449
            cls.username = self._connect_loginname(os)
450
        else:
451
            cls.username = choice(userlist)
452

    
453
        self.assertIsNotNone(cls.username)
454

    
455
    def test_002d_verify_server_metadata(self):
456
        """Test server metadata keys are set based on image metadata"""
457

    
458
        log.info("Verifying image metadata")
459

    
460
        servermeta = self.client.get_server_metadata(self.serverid)
461
        imagemeta = self.client.get_image_metadata(self.imageid)
462

    
463
        self.assertEqual(servermeta["OS"], imagemeta["os"])
464

    
465
    def test_003_server_becomes_active(self):
466
        """Test server becomes ACTIVE"""
467

    
468
        log.info("Waiting for server to become ACTIVE")
469

    
470
        self._insist_on_status_transition("BUILD", "ACTIVE",
471
                                         self.build_fail, self.build_warning)
472

    
473
    def test_003a_get_server_oob_console(self):
474
        """Test getting OOB server console over VNC
475

476
        Implementation of RFB protocol follows
477
        http://www.realvnc.com/docs/rfbproto.pdf.
478

479
        """
480
        console = self.cyclades.get_server_console(self.serverid)
481
        self.assertEquals(console['type'], "vnc")
482
        sock = self._insist_on_tcp_connection(socket.AF_INET,
483
                                        console["host"], console["port"])
484

    
485
        # Step 1. ProtocolVersion message (par. 6.1.1)
486
        version = sock.recv(1024)
487
        self.assertEquals(version, 'RFB 003.008\n')
488
        sock.send(version)
489

    
490
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
491
        sec = sock.recv(1024)
492
        self.assertEquals(list(sec), ['\x01', '\x02'])
493

    
494
        # Step 3. Request VNC Authentication (par 6.1.2)
495
        sock.send('\x02')
496

    
497
        # Step 4. Receive Challenge (par 6.2.2)
498
        challenge = sock.recv(1024)
499
        self.assertEquals(len(challenge), 16)
500

    
501
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
502
        response = d3des_generate_response(
503
            (console["password"] + '\0' * 8)[:8], challenge)
504
        sock.send(response)
505

    
506
        # Step 6. SecurityResult (par 6.1.3)
507
        result = sock.recv(4)
508
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
509
        sock.close()
510

    
511
    def test_004_server_has_ipv4(self):
512
        """Test active server has a valid IPv4 address"""
513

    
514
        log.info("Validate server's IPv4")
515

    
516
        server = self.client.get_server_details(self.serverid)
517
        ipv4 = self._get_ipv4(server)
518
        self.assertEquals(IP(ipv4).version(), 4)
519

    
520
    def test_005_server_has_ipv6(self):
521
        """Test active server has a valid IPv6 address"""
522
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
523

    
524
        log.info("Validate server's IPv6")
525

    
526
        server = self.client.get_server_details(self.serverid)
527
        ipv6 = self._get_ipv6(server)
528
        self.assertEquals(IP(ipv6).version(), 6)
529

    
530
    def test_006_server_responds_to_ping_IPv4(self):
531
        """Test server responds to ping on IPv4 address"""
532

    
533
        log.info("Testing if server responds to pings in IPv4")
534

    
535
        server = self.client.get_server_details(self.serverid)
536
        ip = self._get_ipv4(server)
537
        self._try_until_timeout_expires(self.action_timeout,
538
                                        self.action_timeout,
539
                                        "PING IPv4 to %s" % ip,
540
                                        self._ping_once,
541
                                        False, ip)
542

    
543
    def test_007_server_responds_to_ping_IPv6(self):
544
        """Test server responds to ping on IPv6 address"""
545
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
546
        log.info("Testing if server responds to pings in IPv6")
547

    
548
        server = self.client.get_server_details(self.serverid)
549
        ip = self._get_ipv6(server)
550
        self._try_until_timeout_expires(self.action_timeout,
551
                                        self.action_timeout,
552
                                        "PING IPv6 to %s" % ip,
553
                                        self._ping_once,
554
                                        True, ip)
555

    
556
    def test_008_submit_shutdown_request(self):
557
        """Test submit request to shutdown server"""
558

    
559
        log.info("Shutting down server")
560

    
561
        self.cyclades.shutdown_server(self.serverid)
562

    
563
    def test_009_server_becomes_stopped(self):
564
        """Test server becomes STOPPED"""
565

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

    
571
    def test_010_submit_start_request(self):
572
        """Test submit start server request"""
573

    
574
        log.info("Starting server")
575

    
576
        self.cyclades.start_server(self.serverid)
577

    
578
    def test_011_server_becomes_active(self):
579
        """Test server becomes ACTIVE again"""
580

    
581
        log.info("Waiting until server becomes ACTIVE")
582
        self._insist_on_status_transition("STOPPED", "ACTIVE",
583
                                         self.action_timeout,
584
                                         self.action_timeout)
585

    
586
    def test_011a_server_responds_to_ping_IPv4(self):
587
        """Test server OS is actually up and running again"""
588

    
589
        log.info("Testing if server is actually up and running")
590

    
591
        self.test_006_server_responds_to_ping_IPv4()
592

    
593
    def test_012_ssh_to_server_IPv4(self):
594
        """Test SSH to server public IPv4 works, verify hostname"""
595

    
596
        self._skipIf(self.is_windows, "only valid for Linux servers")
597
        server = self.client.get_server_details(self.serverid)
598
        self._insist_on_ssh_hostname(self._get_ipv4(server),
599
                                     self.username, self.passwd)
600

    
601
    def test_013_ssh_to_server_IPv6(self):
602
        """Test SSH to server public IPv6 works, verify hostname"""
603
        self._skipIf(self.is_windows, "only valid for Linux servers")
604
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
605

    
606
        server = self.client.get_server_details(self.serverid)
607
        self._insist_on_ssh_hostname(self._get_ipv6(server),
608
                                     self.username, self.passwd)
609

    
610
    def test_014_rdp_to_server_IPv4(self):
611
        "Test RDP connection to server public IPv4 works"""
612
        self._skipIf(not self.is_windows, "only valid for Windows servers")
613
        server = self.client.get_server_details(self.serverid)
614
        ipv4 = self._get_ipv4(server)
615
        sock = _insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
616

    
617
        # No actual RDP processing done. We assume the RDP server is there
618
        # if the connection to the RDP port is successful.
619
        # FIXME: Use rdesktop, analyze exit code? see manpage [costasd]
620
        sock.close()
621

    
622
    def test_015_rdp_to_server_IPv6(self):
623
        "Test RDP connection to server public IPv6 works"""
624
        self._skipIf(not self.is_windows, "only valid for Windows servers")
625
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
626

    
627
        server = self.client.get_server_details(self.serverid)
628
        ipv6 = self._get_ipv6(server)
629
        sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
630

    
631
        # No actual RDP processing done. We assume the RDP server is there
632
        # if the connection to the RDP port is successful.
633
        sock.close()
634

    
635
    def test_016_personality_is_enforced(self):
636
        """Test file injection for personality enforcement"""
637
        self._skipIf(self.is_windows, "only implemented for Linux servers")
638
        self._skipIf(self.personality == None, "No personality file selected")
639

    
640
        log.info("Trying to inject file for personality enforcement")
641

    
642
        server = self.client.get_server_details(self.serverid)
643

    
644
        for inj_file in self.personality:
645
            equal_files = self._check_file_through_ssh(self._get_ipv4(server),
646
                                                       inj_file['owner'],
647
                                                       self.passwd,
648
                                                       inj_file['path'],
649
                                                       inj_file['contents'])
650
            self.assertTrue(equal_files)
651

    
652
    def test_017_submit_delete_request(self):
653
        """Test submit request to delete server"""
654

    
655
        log.info("Deleting server")
656

    
657
        self.client.delete_server(self.serverid)
658

    
659
    def test_018_server_becomes_deleted(self):
660
        """Test server becomes DELETED"""
661

    
662
        log.info("Testing if server becomes DELETED")
663

    
664
        self._insist_on_status_transition("ACTIVE", "DELETED",
665
                                         self.action_timeout,
666
                                         self.action_timeout)
667

    
668
    def test_019_server_no_longer_in_server_list(self):
669
        """Test server is no longer in server list"""
670

    
671
        log.info("Test if server is no longer listed")
672

    
673
        servers = self.client.list_servers()
674
        self.assertNotIn(self.serverid, [s["id"] for s in servers])
675

    
676

    
677
class NetworkTestCase(unittest.TestCase):
678
    """ Testing networking in cyclades """
679

    
680
    @classmethod
681
    def setUpClass(cls):
682
        "Initialize kamaki, get list of current networks"
683

    
684
        cls.client = CycladesClient(API, TOKEN)
685
        cls.compute = ComputeClient(API, TOKEN)
686

    
687
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
688
                                          TEST_RUN_ID,
689
                                          cls.imagename)
690

    
691
        #Dictionary initialization for the vms credentials
692
        cls.serverid = dict()
693
        cls.username = dict()
694
        cls.password = dict()
695
        cls.is_windows = cls.imagename.lower().find("windows") >= 0
696

    
697
    def _skipIf(self, condition, msg):
698
        if condition:
699
            self.skipTest(msg)
700

    
701
    def _get_ipv4(self, server):
702
        """Get the public IPv4 of a server from the detailed server info"""
703

    
704
        public_addrs = filter(lambda x: x["id"] == "public",
705
                              server["addresses"]["values"])
706
        self.assertEqual(len(public_addrs), 1)
707
        ipv4_addrs = filter(lambda x: x["version"] == 4,
708
                            public_addrs[0]["values"])
709
        self.assertEqual(len(ipv4_addrs), 1)
710
        return ipv4_addrs[0]["addr"]
711

    
712
    def _connect_loginname(self, os):
713
        """Return the login name for connections based on the server OS"""
714
        if os in ("Ubuntu", "Kubuntu", "Fedora"):
715
            return "user"
716
        elif os in ("windows", "windows_alpha1"):
717
            return "Administrator"
718
        else:
719
            return "root"
720

    
721
    def _ping_once(self, ip):
722

    
723
        """Test server responds to a single IPv4 or IPv6 ping"""
724
        cmd = "ping -c 2 -w 3 %s" % (ip)
725
        ping = subprocess.Popen(cmd, shell=True,
726
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
727
        (stdout, stderr) = ping.communicate()
728
        ret = ping.wait()
729

    
730
        return (ret == 0)
731

    
732
    def test_00001a_submit_create_server_A(self):
733
        """Test submit create server request"""
734

    
735
        log.info("Creating test server A")
736

    
737
        serverA = self.client.create_server(self.servername, self.flavorid,
738
                                            self.imageid, personality=None)
739

    
740
        self.assertEqual(serverA["name"], self.servername)
741
        self.assertEqual(serverA["flavorRef"], self.flavorid)
742
        self.assertEqual(serverA["imageRef"], self.imageid)
743
        self.assertEqual(serverA["status"], "BUILD")
744

    
745
        # Update class attributes to reflect data on building server
746
        self.serverid['A'] = serverA["id"]
747
        self.username['A'] = None
748
        self.password['A'] = serverA["adminPass"]
749

    
750
        log.info("Server A id:" + str(serverA["id"]))
751
        log.info("Server password " + (self.password['A']))
752

    
753
    def test_00001b_serverA_becomes_active(self):
754
        """Test server becomes ACTIVE"""
755

    
756
        log.info("Waiting until test server A becomes ACTIVE")
757

    
758
        fail_tmout = time.time() + self.action_timeout
759
        while True:
760
            d = self.client.get_server_details(self.serverid['A'])
761
            status = d['status']
762
            if status == 'ACTIVE':
763
                active = True
764
                break
765
            elif time.time() > fail_tmout:
766
                self.assertLess(time.time(), fail_tmout)
767
            else:
768
                time.sleep(self.query_interval)
769

    
770
        self.assertTrue(active)
771

    
772
    def test_00002a_submit_create_server_B(self):
773
        """Test submit create server request"""
774

    
775
        log.info("Creating test server B")
776

    
777
        serverB = self.client.create_server(self.servername, self.flavorid,
778
                                            self.imageid, personality=None)
779

    
780
        self.assertEqual(serverB["name"], self.servername)
781
        self.assertEqual(serverB["flavorRef"], self.flavorid)
782
        self.assertEqual(serverB["imageRef"], self.imageid)
783
        self.assertEqual(serverB["status"], "BUILD")
784

    
785
        # Update class attributes to reflect data on building server
786
        self.serverid['B'] = serverB["id"]
787
        self.username['B'] = None
788
        self.password['B'] = serverB["adminPass"]
789

    
790
        log.info("Server B id: " + str(serverB["id"]))
791
        log.info("Password " + (self.password['B']))
792

    
793
    def test_00002b_serverB_becomes_active(self):
794
        """Test server becomes ACTIVE"""
795

    
796
        log.info("Waiting until test server B becomes ACTIVE")
797

    
798
        fail_tmout = time.time() + self.action_timeout
799
        while True:
800
            d = self.client.get_server_details(self.serverid['B'])
801
            status = d['status']
802
            if status == 'ACTIVE':
803
                active = True
804
                break
805
            elif time.time() > fail_tmout:
806
                self.assertLess(time.time(), fail_tmout)
807
            else:
808
                time.sleep(self.query_interval)
809

    
810
        self.assertTrue(active)
811

    
812
    def test_001_create_network(self):
813
        """Test submit create network request"""
814

    
815
        log.info("Submit new network request")
816

    
817
        name = SNF_TEST_PREFIX + TEST_RUN_ID
818
        previous_num = len(self.client.list_networks())
819
        network = self.client.create_network(name)
820

    
821
        #Test if right name is assigned
822
        self.assertEqual(network['name'], name)
823

    
824
        # Update class attributes
825
        cls = type(self)
826
        cls.networkid = network['id']
827
        networks = self.client.list_networks()
828

    
829
        #Test if new network is created
830
        self.assertTrue(len(networks) > previous_num)
831

    
832
    def test_002_connect_to_network(self):
833
        """Test connect VMs to network"""
834

    
835
        log.info("Connect VMs to private network")
836

    
837
        self.client.connect_server(self.serverid['A'], self.networkid)
838
        self.client.connect_server(self.serverid['B'], self.networkid)
839

    
840
        #Insist on connecting until action timeout
841
        fail_tmout = time.time() + self.action_timeout
842

    
843
        while True:
844
            connected = (self.client.get_network_details(self.networkid))
845
            connections = connected['servers']['values']
846
            if (self.serverid['A'] in connections) \
847
                    and (self.serverid['B'] in connections):
848
                conn_exists = True
849
                break
850
            elif time.time() > fail_tmout:
851
                self.assertLess(time.time(), fail_tmout)
852
            else:
853
                time.sleep(self.query_interval)
854

    
855
        self.assertTrue(conn_exists)
856

    
857
    def test_002a_reboot(self):
858
        """Rebooting server A"""
859

    
860
        log.info("Rebooting server A")
861

    
862
        self.client.shutdown_server(self.serverid['A'])
863

    
864
        fail_tmout = time.time() + self.action_timeout
865
        while True:
866
            d = self.client.get_server_details(self.serverid['A'])
867
            status = d['status']
868
            if status == 'STOPPED':
869
                break
870
            elif time.time() > fail_tmout:
871
                self.assertLess(time.time(), fail_tmout)
872
            else:
873
                time.sleep(self.query_interval)
874

    
875
        self.client.start_server(self.serverid['A'])
876

    
877
        while True:
878
            d = self.client.get_server_details(self.serverid['A'])
879
            status = d['status']
880
            if status == 'ACTIVE':
881
                active = True
882
                break
883
            elif time.time() > fail_tmout:
884
                self.assertLess(time.time(), fail_tmout)
885
            else:
886
                time.sleep(self.query_interval)
887

    
888
        self.assertTrue(active)
889

    
890
    def test_002b_ping_server_A(self):
891
        "Test if server A is pingable"
892

    
893
        log.info("Testing if server A is pingable")
894

    
895
        server = self.client.get_server_details(self.serverid['A'])
896
        ip = self._get_ipv4(server)
897

    
898
        fail_tmout = time.time() + self.action_timeout
899

    
900
        s = False
901

    
902
        while True:
903

    
904
            if self._ping_once(ip):
905
                s = True
906
                break
907

    
908
            elif time.time() > fail_tmout:
909
                self.assertLess(time.time(), fail_tmout)
910

    
911
            else:
912
                time.sleep(self.query_interval)
913

    
914
        self.assertTrue(s)
915

    
916
    def test_002c_reboot(self):
917
        """Reboot server B"""
918

    
919
        log.info("Rebooting server B")
920

    
921
        self.client.shutdown_server(self.serverid['B'])
922

    
923
        fail_tmout = time.time() + self.action_timeout
924
        while True:
925
            d = self.client.get_server_details(self.serverid['B'])
926
            status = d['status']
927
            if status == 'STOPPED':
928
                break
929
            elif time.time() > fail_tmout:
930
                self.assertLess(time.time(), fail_tmout)
931
            else:
932
                time.sleep(self.query_interval)
933

    
934
        self.client.start_server(self.serverid['B'])
935

    
936
        while True:
937
            d = self.client.get_server_details(self.serverid['B'])
938
            status = d['status']
939
            if status == 'ACTIVE':
940
                active = True
941
                break
942
            elif time.time() > fail_tmout:
943
                self.assertLess(time.time(), fail_tmout)
944
            else:
945
                time.sleep(self.query_interval)
946

    
947
        self.assertTrue(active)
948

    
949
    def test_002d_ping_server_B(self):
950
        """Test if server B is pingable"""
951

    
952
        log.info("Testing if server B is pingable")
953
        server = self.client.get_server_details(self.serverid['B'])
954
        ip = self._get_ipv4(server)
955

    
956
        fail_tmout = time.time() + self.action_timeout
957

    
958
        s = False
959

    
960
        while True:
961
            if self._ping_once(ip):
962
                s = True
963
                break
964

    
965
            elif time.time() > fail_tmout:
966
                self.assertLess(time.time(), fail_tmout)
967

    
968
            else:
969
                time.sleep(self.query_interval)
970

    
971
        self.assertTrue(s)
972

    
973
    def test_003a_setup_interface_A(self):
974
        """Set up eth1 for server A"""
975

    
976
        self._skipIf(self.is_windows, "only valid for Linux servers")
977

    
978
        log.info("Setting up interface eth1 for server A")
979

    
980
        server = self.client.get_server_details(self.serverid['A'])
981
        image = self.client.get_image_details(self.imageid)
982
        os = image['metadata']['values']['os']
983

    
984
        users = image["metadata"]["values"].get("users", None)
985
        userlist = users.split()
986

    
987
        if "root" in userlist:
988
            loginname = "root"
989
        elif users == None:
990
            loginname = self._connect_loginname(os)
991
        else:
992
            loginname = choice(userlist)
993

    
994
        hostip = self._get_ipv4(server)
995
        myPass = self.password['A']
996

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

    
999
        res = False
1000

    
1001
        if loginname != "root":
1002
            with settings(
1003
                hide('warnings', 'running'),
1004
                warn_only=True,
1005
                host_string=hostip,
1006
                user=loginname, password=myPass
1007
                ):
1008

    
1009
                if len(sudo('ifconfig eth1 192.168.0.12')) == 0:
1010
                    res = True
1011

    
1012
        else:
1013
            with settings(
1014
                hide('warnings', 'running'),
1015
                warn_only=True,
1016
                host_string=hostip,
1017
                user=loginname, password=myPass
1018
                ):
1019

    
1020
                if len(run('ifconfig eth1 192.168.0.12')) == 0:
1021
                    res = True
1022

    
1023
        self.assertTrue(res)
1024

    
1025
    def test_003b_setup_interface_B(self):
1026
        """Setup eth1 for server B"""
1027

    
1028
        self._skipIf(self.is_windows, "only valid for Linux servers")
1029

    
1030
        log.info("Setting up interface eth1 for server B")
1031

    
1032
        server = self.client.get_server_details(self.serverid['B'])
1033
        image = self.client.get_image_details(self.imageid)
1034
        os = image['metadata']['values']['os']
1035

    
1036
        users = image["metadata"]["values"].get("users", None)
1037
        userlist = users.split()
1038

    
1039
        if "root" in userlist:
1040
            loginname = "root"
1041
        elif users == None:
1042
            loginname = self._connect_loginname(os)
1043
        else:
1044
            loginname = choice(userlist)
1045

    
1046
        hostip = self._get_ipv4(server)
1047
        myPass = self.password['B']
1048

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

    
1051
        res = False
1052

    
1053
        if loginname != "root":
1054
            with settings(
1055
                hide('warnings', 'running'),
1056
                warn_only=True,
1057
                host_string=hostip,
1058
                user=loginname, password=myPass
1059
                ):
1060

    
1061
                if len(sudo('ifconfig eth1 192.168.0.13')) == 0:
1062
                    res = True
1063

    
1064
        else:
1065
            with settings(
1066
                hide('warnings', 'running'),
1067
                warn_only=True,
1068
                host_string=hostip,
1069
                user=loginname, password=myPass
1070
                ):
1071

    
1072
                if len(run('ifconfig eth1 192.168.0.13')) == 0:
1073
                    res = True
1074

    
1075
        self.assertTrue(res)
1076

    
1077
    def test_003c_test_connection_exists(self):
1078
        """Ping server B from server A to test if connection exists"""
1079

    
1080
        self._skipIf(self.is_windows, "only valid for Linux servers")
1081

    
1082
        log.info("Testing if server A is actually connected to server B")
1083

    
1084
        server = self.client.get_server_details(self.serverid['A'])
1085
        image = self.client.get_image_details(self.imageid)
1086
        os = image['metadata']['values']['os']
1087
        hostip = self._get_ipv4(server)
1088

    
1089
        users = image["metadata"]["values"].get("users", None)
1090
        userlist = users.split()
1091

    
1092
        if "root" in userlist:
1093
            loginname = "root"
1094
        elif users == None:
1095
            loginname = self._connect_loginname(os)
1096
        else:
1097
            loginname = choice(userlist)
1098

    
1099
        myPass = self.password['A']
1100

    
1101
        try:
1102
            ssh = paramiko.SSHClient()
1103
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1104
            ssh.connect(hostip, username=loginname, password=myPass)
1105
        except socket.error:
1106
            raise AssertionError
1107

    
1108
        cmd = "if ping -c 2 -w 3 192.168.0.13 >/dev/null; \
1109
               then echo \'True\'; fi;"
1110
        stdin, stdout, stderr = ssh.exec_command(cmd)
1111
        lines = stdout.readlines()
1112

    
1113
        exists = False
1114

    
1115
        if 'True\n' in lines:
1116
            exists = True
1117

    
1118
        self.assertTrue(exists)
1119

    
1120

    
1121
    def test_004_disconnect_from_network(self):
1122
        "Disconnecting server A and B from network"
1123

    
1124
        log.info("Disconnecting servers from private network")
1125

    
1126
        prev_state = self.client.get_network_details(self.networkid)
1127
        prev_conn = len(prev_state['servers']['values'])
1128

    
1129
        self.client.disconnect_server(self.serverid['A'], self.networkid)
1130
        self.client.disconnect_server(self.serverid['B'], self.networkid)
1131

    
1132
        #Insist on deleting until action timeout
1133
        fail_tmout = time.time() + self.action_timeout
1134

    
1135
        while True:
1136
            connected = (self.client.get_network_details(self.networkid))
1137
            connections = connected['servers']['values']
1138
            if ((self.serverid['A'] not in connections) and
1139
                (self.serverid['B'] not in connections)):
1140
                conn_exists = False
1141
                break
1142
            elif time.time() > fail_tmout:
1143
                self.assertLess(time.time(), fail_tmout)
1144
            else:
1145
                time.sleep(self.query_interval)
1146

    
1147
        self.assertFalse(conn_exists)
1148

    
1149
    def test_005_destroy_network(self):
1150
        """Test submit delete network request"""
1151

    
1152
        log.info("Submitting delete network request")
1153

    
1154
        self.client.delete_network(self.networkid)
1155
        networks = self.client.list_networks()
1156

    
1157
        curr_net = []
1158
        for net in networks:
1159
            curr_net.append(net['id'])
1160

    
1161
        self.assertTrue(self.networkid not in curr_net)
1162

    
1163
    def test_006_cleanup_servers(self):
1164
        """Cleanup servers created for this test"""
1165

    
1166
        log.info("Delete servers created for this test")
1167

    
1168
        self.compute.delete_server(self.serverid['A'])
1169
        self.compute.delete_server(self.serverid['B'])
1170

    
1171
        fail_tmout = time.time() + self.action_timeout
1172

    
1173
        #Ensure server gets deleted
1174
        status = dict()
1175

    
1176
        while True:
1177
            details = self.compute.get_server_details(self.serverid['A'])
1178
            status['A'] = details['status']
1179
            details = self.compute.get_server_details(self.serverid['B'])
1180
            status['B'] = details['status']
1181
            if (status['A'] == 'DELETED') and (status['B'] == 'DELETED'):
1182
                deleted = True
1183
                break
1184
            elif time.time() > fail_tmout:
1185
                self.assertLess(time.time(), fail_tmout)
1186
            else:
1187
                time.sleep(self.query_interval)
1188

    
1189
        self.assertTrue(deleted)
1190

    
1191

    
1192
class TestRunnerProcess(Process):
1193
    """A distinct process used to execute part of the tests in parallel"""
1194
    def __init__(self, **kw):
1195
        Process.__init__(self, **kw)
1196
        kwargs = kw["kwargs"]
1197
        self.testq = kwargs["testq"]
1198
        self.runner = kwargs["runner"]
1199

    
1200
    def run(self):
1201
        # Make sure this test runner process dies with the parent
1202
        # and is not left behind.
1203
        #
1204
        # WARNING: This uses the prctl(2) call and is
1205
        # Linux-specific.
1206
        prctl.set_pdeathsig(signal.SIGHUP)
1207

    
1208
        while True:
1209
            log.debug("I am process %d, GETting from queue is %s",
1210
                     os.getpid(), self.testq)
1211
            msg = self.testq.get()
1212
            log.debug("Dequeued msg: %s", msg)
1213

    
1214
            if msg == "TEST_RUNNER_TERMINATE":
1215
                raise SystemExit
1216
            elif issubclass(msg, unittest.TestCase):
1217
                # Assemble a TestSuite, and run it
1218
                suite = unittest.TestLoader().loadTestsFromTestCase(msg)
1219
                self.runner.run(suite)
1220
            else:
1221
                raise Exception("Cannot handle msg: %s" % msg)
1222

    
1223

    
1224
def _run_cases_in_parallel(cases, fanout=1, runner=None):
1225
    """Run instances of TestCase in parallel, in a number of distinct processes
1226

1227
    The cases iterable specifies the TestCases to be executed in parallel,
1228
    by test runners running in distinct processes.
1229
    The fanout parameter specifies the number of processes to spawn,
1230
    and defaults to 1.
1231
    The runner argument specifies the test runner class to use inside each
1232
    runner process.
1233

1234
    """
1235
    if runner is None:
1236
        runner = unittest.TextTestRunner(verbosity=2, failfast=True)
1237

    
1238
    # testq: The master process enqueues TestCase objects into this queue,
1239
    #        test runner processes pick them up for execution, in parallel.
1240
    testq = Queue()
1241
    runners = []
1242
    for i in xrange(0, fanout):
1243
        kwargs = dict(testq=testq, runner=runner)
1244
        runners.append(TestRunnerProcess(kwargs=kwargs))
1245

    
1246
    log.info("Spawning %d test runner processes", len(runners))
1247
    for p in runners:
1248
        p.start()
1249
    log.debug("Spawned %d test runners, PIDs are %s",
1250
              len(runners), [p.pid for p in runners])
1251

    
1252
    # Enqueue test cases
1253
    map(testq.put, cases)
1254
    map(testq.put, ["TEST_RUNNER_TERMINATE"] * len(runners))
1255

    
1256
    log.debug("Joining %d processes", len(runners))
1257
    for p in runners:
1258
        p.join()
1259
    log.debug("Done joining %d processes", len(runners))
1260

    
1261

    
1262
def _spawn_server_test_case(**kwargs):
1263
    """Construct a new unit test case class from SpawnServerTestCase"""
1264

    
1265
    name = "SpawnServerTestCase_%s" % kwargs["imageid"]
1266
    cls = type(name, (SpawnServerTestCase,), kwargs)
1267

    
1268
    # Patch extra parameters into test names by manipulating method docstrings
1269
    for (mname, m) in \
1270
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
1271
        if hasattr(m, __doc__):
1272
            m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
1273

    
1274
    # Make sure the class can be pickled, by listing it among
1275
    # the attributes of __main__. A PicklingError is raised otherwise.
1276
    setattr(__main__, name, cls)
1277
    return cls
1278

    
1279

    
1280
def _spawn_network_test_case(**kwargs):
1281
    """Construct a new unit test case class from NetworkTestCase"""
1282

    
1283
    name = "NetworkTestCase" + TEST_RUN_ID
1284
    cls = type(name, (NetworkTestCase,), kwargs)
1285

    
1286
    # Make sure the class can be pickled, by listing it among
1287
    # the attributes of __main__. A PicklingError is raised otherwise.
1288
    setattr(__main__, name, cls)
1289
    return cls
1290

    
1291

    
1292
def cleanup_servers(delete_stale=False):
1293

    
1294
    c = ComputeClient(API, TOKEN)
1295

    
1296
    servers = c.list_servers()
1297
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
1298

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

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

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

    
1314

    
1315
def cleanup_networks(delete_stale=False):
1316

    
1317
    c = CycladesClient(API, TOKEN)
1318

    
1319
    networks = c.list_networks()
1320
    stale = [n for n in networks if n["name"].startswith(SNF_TEST_PREFIX)]
1321

    
1322
    if len(stale) == 0:
1323
        return
1324

    
1325
    print >> sys.stderr, "Found these stale networks from previous runs:"
1326
    print "    " + \
1327
          "\n    ".join(["%s: %s" % (str(n["id"]), n["name"]) for n in stale])
1328

    
1329
    if delete_stale:
1330
        print >> sys.stderr, "Deleting %d stale networks:" % len(stale)
1331
        for network in stale:
1332
            c.delete_network(network["id"])
1333
        print >> sys.stderr, "    ...done"
1334
    else:
1335
        print >> sys.stderr, "Use --delete-stale to delete them."
1336

    
1337

    
1338
def parse_arguments(args):
1339
    from optparse import OptionParser
1340

    
1341
    kw = {}
1342
    kw["usage"] = "%prog [options]"
1343
    kw["description"] = \
1344
        "%prog runs a number of test scenarios on a " \
1345
        "Synnefo deployment."
1346

    
1347
    parser = OptionParser(**kw)
1348
    parser.disable_interspersed_args()
1349
    parser.add_option("--api",
1350
                      action="store", type="string", dest="api",
1351
                      help="The API URI to use to reach the Synnefo API",
1352
                      default=DEFAULT_API)
1353
    parser.add_option("--plankton",
1354
                      action="store", type="string", dest="plankton",
1355
                      help="The API URI to use to reach the Plankton API",
1356
                      default=DEFAULT_PLANKTON)
1357
    parser.add_option("--plankton-user",
1358
                      action="store", type="string", dest="plankton_user",
1359
                      help="Owner of system images",
1360
                      default=DEFAULT_PLANKTON_USER)
1361
    parser.add_option("--token",
1362
                      action="store", type="string", dest="token",
1363
                      help="The token to use for authentication to the API")
1364
    parser.add_option("--nofailfast",
1365
                      action="store_true", dest="nofailfast",
1366
                      help="Do not fail immediately if one of the tests " \
1367
                           "fails (EXPERIMENTAL)",
1368
                      default=False)
1369
    parser.add_option("--no-ipv6",
1370
                      action="store_true", dest="no_ipv6",
1371
                      help="Disables ipv6 related tests",
1372
                      default=False)
1373
    parser.add_option("--action-timeout",
1374
                      action="store", type="int", dest="action_timeout",
1375
                      metavar="TIMEOUT",
1376
                      help="Wait SECONDS seconds for a server action to " \
1377
                           "complete, then the test is considered failed",
1378
                      default=100)
1379
    parser.add_option("--build-warning",
1380
                      action="store", type="int", dest="build_warning",
1381
                      metavar="TIMEOUT",
1382
                      help="Warn if TIMEOUT seconds have passed and a " \
1383
                           "build operation is still pending",
1384
                      default=600)
1385
    parser.add_option("--build-fail",
1386
                      action="store", type="int", dest="build_fail",
1387
                      metavar="BUILD_TIMEOUT",
1388
                      help="Fail the test if TIMEOUT seconds have passed " \
1389
                           "and a build operation is still incomplete",
1390
                      default=900)
1391
    parser.add_option("--query-interval",
1392
                      action="store", type="int", dest="query_interval",
1393
                      metavar="INTERVAL",
1394
                      help="Query server status when requests are pending " \
1395
                           "every INTERVAL seconds",
1396
                      default=3)
1397
    parser.add_option("--fanout",
1398
                      action="store", type="int", dest="fanout",
1399
                      metavar="COUNT",
1400
                      help="Spawn up to COUNT child processes to execute " \
1401
                           "in parallel, essentially have up to COUNT " \
1402
                           "server build requests outstanding (EXPERIMENTAL)",
1403
                      default=1)
1404
    parser.add_option("--force-flavor",
1405
                      action="store", type="int", dest="force_flavorid",
1406
                      metavar="FLAVOR ID",
1407
                      help="Force all server creations to use the specified "\
1408
                           "FLAVOR ID instead of a randomly chosen one, " \
1409
                           "useful if disk space is scarce",
1410
                      default=None)
1411
    parser.add_option("--image-id",
1412
                      action="store", type="string", dest="force_imageid",
1413
                      metavar="IMAGE ID",
1414
                      help="Test the specified image id, use 'all' to test " \
1415
                           "all available images (mandatory argument)",
1416
                      default=None)
1417
    parser.add_option("--show-stale",
1418
                      action="store_true", dest="show_stale",
1419
                      help="Show stale servers from previous runs, whose "\
1420
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1421
                      default=False)
1422
    parser.add_option("--delete-stale",
1423
                      action="store_true", dest="delete_stale",
1424
                      help="Delete stale servers from previous runs, whose "\
1425
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1426
                      default=False)
1427
    parser.add_option("--force-personality",
1428
                      action="store", type="string", dest="personality_path",
1429
                      help="Force a personality file injection.\
1430
                            File path required. ",
1431
                      default=None)
1432
    parser.add_option("--log-folder",
1433
                      action="store", type="string", dest="log_folder",
1434
                      help="Define the absolute path where the output \
1435
                            log is stored. ",
1436
                      default="/var/log/burnin/")
1437

    
1438
    # FIXME: Change the default for build-fanout to 10
1439
    # FIXME: Allow the user to specify a specific set of Images to test
1440

    
1441
    (opts, args) = parser.parse_args(args)
1442

    
1443
    # Verify arguments
1444
    if opts.delete_stale:
1445
        opts.show_stale = True
1446

    
1447
    if not opts.show_stale:
1448
        if not opts.force_imageid:
1449
            print >>sys.stderr, "The --image-id argument is mandatory."
1450
            parser.print_help()
1451
            sys.exit(1)
1452

    
1453
        if opts.force_imageid != 'all':
1454
            try:
1455
                opts.force_imageid = str(opts.force_imageid)
1456
            except ValueError:
1457
                print >>sys.stderr, "Invalid value specified for --image-id." \
1458
                                    "Use a valid id, or `all'."
1459
                sys.exit(1)
1460

    
1461
    return (opts, args)
1462

    
1463

    
1464
def main():
1465
    """Assemble test cases into a test suite, and run it
1466

1467
    IMPORTANT: Tests have dependencies and have to be run in the specified
1468
    order inside a single test case. They communicate through attributes of the
1469
    corresponding TestCase class (shared fixtures). Distinct subclasses of
1470
    TestCase MAY SHARE NO DATA, since they are run in parallel, in distinct
1471
    test runner processes.
1472

1473
    """
1474

    
1475
    (opts, args) = parse_arguments(sys.argv[1:])
1476

    
1477
    global API, TOKEN, PLANKTON, PLANKTON_USER, NO_IPV6
1478
    API = opts.api
1479
    TOKEN = opts.token
1480
    PLANKTON = opts.plankton
1481
    PLANKTON_USER = opts.plankton_user
1482
    NO_IPV6 = opts.no_ipv6
1483

    
1484
    # Cleanup stale servers from previous runs
1485
    if opts.show_stale:
1486
        cleanup_servers(delete_stale=opts.delete_stale)
1487
        cleanup_networks(delete_stale=opts.delete_stale)
1488
        return 0
1489

    
1490
    # Initialize a kamaki instance, get flavors, images
1491

    
1492
    c = ComputeClient(API, TOKEN)
1493

    
1494
    DIMAGES = c.list_images(detail=True)
1495
    DFLAVORS = c.list_flavors(detail=True)
1496

    
1497
    # FIXME: logging, log, LOG PID, TEST_RUN_ID, arguments
1498
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
1499
    #unittest.main(verbosity=2, catchbreak=True)
1500

    
1501
    if opts.force_imageid == 'all':
1502
        test_images = DIMAGES
1503
    else:
1504
        test_images = filter(lambda x: x["id"] == opts.force_imageid, DIMAGES)
1505

    
1506
    #New folder for log per image
1507

    
1508
    if not os.path.exists(opts.log_folder):
1509
        os.mkdir(opts.log_folder)
1510

    
1511
    test_folder = os.path.join(opts.log_folder, TEST_RUN_ID)
1512
    os.mkdir(test_folder)
1513

    
1514
    for image in test_images:
1515

    
1516
        imageid = str(image["id"])
1517

    
1518
        if opts.force_flavorid:
1519
            flavorid = opts.force_flavorid
1520
        else:
1521
            flavorid = choice([f["id"] for f in DFLAVORS if f["disk"] >= 20])
1522

    
1523
        imagename = image["name"]
1524

    
1525
        #Personality dictionary for file injection test
1526
        if opts.personality_path != None:
1527
            f = open(opts.personality_path)
1528
            content = b64encode(f.read())
1529
            personality = []
1530
            st = os.stat(opts.personality_path)
1531
            personality.append({
1532
                    'path': '/root/test_inj_file',
1533
                    'owner': 'root',
1534
                    'group': 'root',
1535
                    'mode': 0x7777 & st.st_mode,
1536
                    'contents': content
1537
                    })
1538
        else:
1539
            personality = None
1540

    
1541
        servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, imagename)
1542
        is_windows = imagename.lower().find("windows") >= 0
1543

    
1544
        ServerTestCase = _spawn_server_test_case(
1545
            imageid=imageid,
1546
            flavorid=flavorid,
1547
            imagename=imagename,
1548
            personality=personality,
1549
            servername=servername,
1550
            is_windows=is_windows,
1551
            action_timeout=opts.action_timeout,
1552
            build_warning=opts.build_warning,
1553
            build_fail=opts.build_fail,
1554
            query_interval=opts.query_interval,
1555
            )
1556

    
1557
        NetworkTestCase = _spawn_network_test_case(
1558
            action_timeout=opts.action_timeout,
1559
            imageid=imageid,
1560
            flavorid=flavorid,
1561
            imagename=imagename,
1562
            query_interval=opts.query_interval,
1563
            )
1564

    
1565
        seq_cases = [UnauthorizedTestCase, ImagesTestCase, FlavorsTestCase,
1566
                     ServersTestCase, ServerTestCase, NetworkTestCase]
1567

    
1568
        #folder for each image
1569
        image_folder = os.path.join(test_folder, imageid)
1570
        os.mkdir(image_folder)
1571

    
1572
        for case in seq_cases:
1573
            log_file = os.path.join(image_folder, 'details_' +
1574
                                    (case.__name__) + "_" +
1575
                                    TEST_RUN_ID + '.log')
1576
            fail_file = os.path.join(image_folder, 'failed_' +
1577
                                     (case.__name__) + "_" +
1578
                                     TEST_RUN_ID + '.log')
1579
            error_file = os.path.join(image_folder, 'error_' +
1580
                                      (case.__name__) + "_" +
1581
                                      TEST_RUN_ID + '.log')
1582

    
1583
            f = open(log_file, "w")
1584
            fail = open(fail_file, "w")
1585
            error = open(error_file, "w")
1586

    
1587
            suite = unittest.TestLoader().loadTestsFromTestCase(case)
1588
            runner = unittest.TextTestRunner(f, verbosity=2, failfast=True)
1589
            result = runner.run(suite)
1590

    
1591
            for res in result.errors:
1592
                error.write(str(res[0]) + '\n')
1593
                error.write(str(res[0].shortDescription()) + '\n')
1594
                error.write('\n')
1595

    
1596
            for res in result.failures:
1597
                fail.write(str(res[0]) + '\n')
1598
                fail.write(str(res[0].shortDescription()) + '\n')
1599
                fail.write('\n')
1600
                if opts.nofailfast == False:
1601
                    sys.exit()
1602

    
1603
if __name__ == "__main__":
1604
    sys.exit(main())