Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin.py @ e49bdb7c

History | View | Annotate | Download (57.3 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.plankton = ImageClient(PLANKTON, TOKEN)
111
        cls.images = cls.plankton.list_public()
112
        cls.dimages = cls.plankton.list_public(detail=True)
113

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

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

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

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

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

    
141

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

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

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

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

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

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

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

175
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
176

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

    
183

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

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

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

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

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

    
209

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

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

    
219
        cls.client = ComputeClient(API, TOKEN)
220
        cls.cyclades = CycladesClient(API, TOKEN)
221

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
366
        transport = paramiko.Transport((hostip, 22))
367
        transport.connect(username=username, password=password)
368

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

    
375
        f = open(localpath)
376
        remote_content = b64encode(f.read())
377

    
378
        # Check if files are the same
379
        return (remote_content == content)
380

    
381
    def _skipIf(self, condition, msg):
382
        if condition:
383
            self.skipTest(msg)
384

    
385
    def test_001_submit_create_server(self):
386
        """Test submit create server request"""
387

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

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

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

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

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

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

    
421
        log.info("Server in BUILD state in details")
422

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

    
429
    def test_002c_set_server_metadata(self):
430

    
431
        log.info("Creating server metadata")
432

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

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

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

    
451
        self.assertIsNotNone(cls.username)
452

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

    
456
        log.info("Verifying image metadata")
457

    
458
        servermeta = self.client.get_server_metadata(self.serverid)
459
        imagemeta = self.client.get_image_metadata(self.imageid)
460

    
461
        self.assertEqual(servermeta["OS"], imagemeta["os"])
462

    
463
    def test_003_server_becomes_active(self):
464
        """Test server becomes ACTIVE"""
465

    
466
        log.info("Waiting for server to become ACTIVE")
467

    
468
        self._insist_on_status_transition("BUILD", "ACTIVE",
469
                                         self.build_fail, self.build_warning)
470

    
471
    def test_003a_get_server_oob_console(self):
472
        """Test getting OOB server console over VNC
473

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

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

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

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

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

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

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

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

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

    
512
        log.info("Validate server's IPv4")
513

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

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

    
522
        log.info("Validate server's IPv6")
523

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

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

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

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

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

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

    
554
    def test_008_submit_shutdown_request(self):
555
        """Test submit request to shutdown server"""
556

    
557
        log.info("Shutting down server")
558

    
559
        self.cyclades.shutdown_server(self.serverid)
560

    
561
    def test_009_server_becomes_stopped(self):
562
        """Test server becomes STOPPED"""
563

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

    
569
    def test_010_submit_start_request(self):
570
        """Test submit start server request"""
571

    
572
        log.info("Starting server")
573

    
574
        self.cyclades.start_server(self.serverid)
575

    
576
    def test_011_server_becomes_active(self):
577
        """Test server becomes ACTIVE again"""
578

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

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

    
587
        log.info("Testing if server is actually up and running")
588

    
589
        self.test_006_server_responds_to_ping_IPv4()
590

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

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

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

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

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

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

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

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

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

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

    
638
        log.info("Trying to inject file for personality enforcement")
639

    
640
        server = self.client.get_server_details(self.serverid)
641

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

    
650
    def test_017_submit_delete_request(self):
651
        """Test submit request to delete server"""
652

    
653
        log.info("Deleting server")
654

    
655
        self.client.delete_server(self.serverid)
656

    
657
    def test_018_server_becomes_deleted(self):
658
        """Test server becomes DELETED"""
659

    
660
        log.info("Testing if server becomes DELETED")
661

    
662
        self._insist_on_status_transition("ACTIVE", "DELETED",
663
                                         self.action_timeout,
664
                                         self.action_timeout)
665

    
666
    def test_019_server_no_longer_in_server_list(self):
667
        """Test server is no longer in server list"""
668

    
669
        log.info("Test if server is no longer listed")
670

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

    
674

    
675
class NetworkTestCase(unittest.TestCase):
676
    """ Testing networking in cyclades """
677

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

    
682
        cls.client = CycladesClient(API, TOKEN)
683
        cls.compute = ComputeClient(API, TOKEN)
684

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

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

    
695
    def _skipIf(self, condition, msg):
696
        if condition:
697
            self.skipTest(msg)
698

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

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

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

    
719
    def _ping_once(self, ip):
720

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

    
728
        return (ret == 0)
729

    
730
    def test_00001a_submit_create_server_A(self):
731
        """Test submit create server request"""
732

    
733
        log.info("Creating test server A")
734

    
735
        serverA = self.client.create_server(self.servername, self.flavorid,
736
                                            self.imageid, personality=None)
737

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

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

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

    
751
    def test_00001b_serverA_becomes_active(self):
752
        """Test server becomes ACTIVE"""
753

    
754
        log.info("Waiting until test server A becomes ACTIVE")
755

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

    
768
        self.assertTrue(active)
769

    
770
    def test_00002a_submit_create_server_B(self):
771
        """Test submit create server request"""
772

    
773
        log.info("Creating test server B")
774

    
775
        serverB = self.client.create_server(self.servername, self.flavorid,
776
                                            self.imageid, personality=None)
777

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

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

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

    
791
    def test_00002b_serverB_becomes_active(self):
792
        """Test server becomes ACTIVE"""
793

    
794
        log.info("Waiting until test server B becomes ACTIVE")
795

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

    
808
        self.assertTrue(active)
809

    
810
    def test_001_create_network(self):
811
        """Test submit create network request"""
812

    
813
        log.info("Submit new network request")
814

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

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

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

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

    
830
    def test_002_connect_to_network(self):
831
        """Test connect VMs to network"""
832

    
833
        log.info("Connect VMs to private network")
834

    
835
        self.client.connect_server(self.serverid['A'], self.networkid)
836
        self.client.connect_server(self.serverid['B'], self.networkid)
837

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

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

    
853
        self.assertTrue(conn_exists)
854

    
855
    def test_002a_reboot(self):
856
        """Rebooting server A"""
857

    
858
        log.info("Rebooting server A")
859

    
860
        self.client.shutdown_server(self.serverid['A'])
861

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

    
873
        self.client.start_server(self.serverid['A'])
874

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

    
886
        self.assertTrue(active)
887

    
888
    def test_002b_ping_server_A(self):
889
        "Test if server A is pingable"
890

    
891
        log.info("Testing if server A is pingable")
892

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

    
896
        fail_tmout = time.time() + self.action_timeout
897

    
898
        s = False
899

    
900
        while True:
901

    
902
            if self._ping_once(ip):
903
                s = True
904
                break
905

    
906
            elif time.time() > fail_tmout:
907
                self.assertLess(time.time(), fail_tmout)
908

    
909
            else:
910
                time.sleep(self.query_interval)
911

    
912
        self.assertTrue(s)
913

    
914
    def test_002c_reboot(self):
915
        """Reboot server B"""
916

    
917
        log.info("Rebooting server B")
918

    
919
        self.client.shutdown_server(self.serverid['B'])
920

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

    
932
        self.client.start_server(self.serverid['B'])
933

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

    
945
        self.assertTrue(active)
946

    
947
    def test_002d_ping_server_B(self):
948
        """Test if server B is pingable"""
949

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

    
954
        fail_tmout = time.time() + self.action_timeout
955

    
956
        s = False
957

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

    
963
            elif time.time() > fail_tmout:
964
                self.assertLess(time.time(), fail_tmout)
965

    
966
            else:
967
                time.sleep(self.query_interval)
968

    
969
        self.assertTrue(s)
970

    
971
    def test_003a_setup_interface_A(self):
972
        """Set up eth1 for server A"""
973

    
974
        self._skipIf(self.is_windows, "only valid for Linux servers")
975

    
976
        log.info("Setting up interface eth1 for server A")
977

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

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

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

    
992
        hostip = self._get_ipv4(server)
993
        myPass = self.password['A']
994

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

    
997
        res = False
998

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

    
1007
                if len(sudo('ifconfig eth1 192.168.0.12')) == 0:
1008
                    res = True
1009

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

    
1018
                if len(run('ifconfig eth1 192.168.0.12')) == 0:
1019
                    res = True
1020

    
1021
        self.assertTrue(res)
1022

    
1023
    def test_003b_setup_interface_B(self):
1024
        """Setup eth1 for server B"""
1025

    
1026
        self._skipIf(self.is_windows, "only valid for Linux servers")
1027

    
1028
        log.info("Setting up interface eth1 for server B")
1029

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

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

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

    
1044
        hostip = self._get_ipv4(server)
1045
        myPass = self.password['B']
1046

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

    
1049
        res = False
1050

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

    
1059
                if len(sudo('ifconfig eth1 192.168.0.13')) == 0:
1060
                    res = True
1061

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

    
1070
                if len(run('ifconfig eth1 192.168.0.13')) == 0:
1071
                    res = True
1072

    
1073
        self.assertTrue(res)
1074

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

    
1078
        self._skipIf(self.is_windows, "only valid for Linux servers")
1079

    
1080
        log.info("Testing if server A is actually connected to server B")
1081

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

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

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

    
1097
        myPass = self.password['A']
1098

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

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

    
1111
        exists = False
1112

    
1113
        if 'True\n' in lines:
1114
            exists = True
1115

    
1116
        self.assertTrue(exists)
1117

    
1118

    
1119
    def test_004_disconnect_from_network(self):
1120
        "Disconnecting server A and B from network"
1121

    
1122
        log.info("Disconnecting servers from private network")
1123

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

    
1127
        self.client.disconnect_server(self.serverid['A'], self.networkid)
1128
        self.client.disconnect_server(self.serverid['B'], self.networkid)
1129

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

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

    
1145
        self.assertFalse(conn_exists)
1146

    
1147
    def test_005_destroy_network(self):
1148
        """Test submit delete network request"""
1149

    
1150
        log.info("Submitting delete network request")
1151

    
1152
        self.client.delete_network(self.networkid)
1153
        networks = self.client.list_networks()
1154

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

    
1159
        self.assertTrue(self.networkid not in curr_net)
1160

    
1161
    def test_006_cleanup_servers(self):
1162
        """Cleanup servers created for this test"""
1163

    
1164
        log.info("Delete servers created for this test")
1165

    
1166
        self.compute.delete_server(self.serverid['A'])
1167
        self.compute.delete_server(self.serverid['B'])
1168

    
1169
        fail_tmout = time.time() + self.action_timeout
1170

    
1171
        #Ensure server gets deleted
1172
        status = dict()
1173

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

    
1187
        self.assertTrue(deleted)
1188

    
1189

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

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

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

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

    
1221

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

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

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

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

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

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

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

    
1259

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

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

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

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

    
1277

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

    
1281
    name = "NetworkTestCase" + TEST_RUN_ID
1282
    cls = type(name, (NetworkTestCase,), kwargs)
1283

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

    
1289

    
1290
def cleanup_servers(delete_stale=False):
1291

    
1292
    c = ComputeClient(API, TOKEN)
1293

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

    
1297
    if len(stale) == 0:
1298
        return
1299

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

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

    
1312

    
1313
def cleanup_networks(delete_stale=False):
1314

    
1315
    c = CycladesClient(API, TOKEN)
1316

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

    
1320
    if len(stale) == 0:
1321
        return
1322

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

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

    
1335

    
1336
def parse_arguments(args):
1337
    from optparse import OptionParser
1338

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

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

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

    
1439
    (opts, args) = parser.parse_args(args)
1440

    
1441
    # Verify arguments
1442
    if opts.delete_stale:
1443
        opts.show_stale = True
1444

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

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

    
1459
    return (opts, args)
1460

    
1461

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

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

1471
    """
1472

    
1473
    (opts, args) = parse_arguments(sys.argv[1:])
1474

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

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

    
1488
    # Initialize a kamaki instance, get flavors, images
1489

    
1490
    c = ComputeClient(API, TOKEN)
1491

    
1492
    DIMAGES = c.list_images(detail=True)
1493
    DFLAVORS = c.list_flavors(detail=True)
1494

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

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

    
1504
    #New folder for log per image
1505

    
1506
    if not os.path.exists(opts.log_folder):
1507
        os.mkdir(opts.log_folder)
1508

    
1509
    test_folder = os.path.join(opts.log_folder, TEST_RUN_ID)
1510
    os.mkdir(test_folder)
1511

    
1512
    for image in test_images:
1513

    
1514
        imageid = str(image["id"])
1515

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

    
1521
        imagename = image["name"]
1522

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

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

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

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

    
1563
        seq_cases = [UnauthorizedTestCase, ImagesTestCase, FlavorsTestCase,
1564
                     ServersTestCase, ServerTestCase, NetworkTestCase]
1565

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

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

    
1581
            f = open(log_file, "w")
1582
            fail = open(fail_file, "w")
1583
            error = open(error_file, "w")
1584

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

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

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

    
1601
if __name__ == "__main__":
1602
    sys.exit(main())