Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin.py @ 746540cd

History | View | Annotate | Download (60.6 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
from optparse import OptionParser, OptionValueError
55

    
56
from kamaki.clients.compute import ComputeClient
57
from kamaki.clients.cyclades import CycladesClient
58
from kamaki.clients.image import ImageClient
59
from kamaki.clients import ClientError
60

    
61
from fabric.api import *
62

    
63
from vncauthproxy.d3des import generate_response as d3des_generate_response
64

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

    
73

    
74
API = None
75
TOKEN = None
76
DEFAULT_API = "https://cyclades.okeanos.grnet.gr/api/v1.1"
77
DEFAULT_PLANKTON = "https://cyclades.okeanos.grnet.gr/plankton"
78
DEFAULT_PLANKTON_USER = "images@okeanos.grnet.gr"
79

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

    
85
red = '\x1b[31m'
86
yellow = '\x1b[33m'
87
green = '\x1b[32m'
88
normal = '\x1b[0m'
89

    
90

    
91
class burninFormatter(logging.Formatter):
92

    
93
    err_fmt = red + "ERROR: %(msg)s" + normal
94
    dbg_fmt = green + "   * %(msg)s" + normal
95
    info_fmt = "%(msg)s"
96

    
97
    def __init__(self, fmt="%(levelno)s: %(msg)s"):
98
        logging.Formatter.__init__(self, fmt)
99

    
100
    def format(self, record):
101

    
102
        format_orig = self._fmt
103

    
104
        # Replace the original format with one customized by logging level
105
        if record.levelno == 10:    # DEBUG
106
            self._fmt = burninFormatter.dbg_fmt
107

    
108
        elif record.levelno == 20:  # INFO
109
            self._fmt = burninFormatter.info_fmt
110

    
111
        elif record.levelno == 40:  # ERROR
112
            self._fmt = burninFormatter.err_fmt
113

    
114
        result = logging.Formatter.format(self, record)
115
        self._fmt = format_orig
116

    
117
        return result
118

    
119

    
120
log = logging.getLogger("burnin")
121
log.setLevel(logging.DEBUG)
122
handler = logging.StreamHandler()
123
handler.setFormatter(burninFormatter())
124
log.addHandler(handler)
125

    
126

    
127
class UnauthorizedTestCase(unittest.TestCase):
128
    def test_unauthorized_access(self):
129
        """Test access without a valid token fails"""
130
        log.info("Authentication test")
131

    
132
        falseToken = '12345'
133
        c = ComputeClient(API, falseToken)
134

    
135
        with self.assertRaises(ClientError) as cm:
136
            c.list_servers()
137
            self.assertEqual(cm.exception.status, 401)
138

    
139

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

    
147
        cls.client = ComputeClient(API, TOKEN)
148
        cls.plankton = ImageClient(PLANKTON, TOKEN)
149
        cls.images = cls.plankton.list_public()
150
        cls.dimages = cls.plankton.list_public(detail=True)
151

    
152
    def test_001_list_images(self):
153
        """Test image list actually returns images"""
154
        self.assertGreater(len(self.images), 0)
155

    
156
    def test_002_list_images_detailed(self):
157
        """Test detailed image list is the same length as list"""
158
        self.assertEqual(len(self.dimages), len(self.images))
159

    
160
    def test_003_same_image_names(self):
161
        """Test detailed and simple image list contain same names"""
162
        names = sorted(map(lambda x: x["name"], self.images))
163
        dnames = sorted(map(lambda x: x["name"], self.dimages))
164
        self.assertEqual(names, dnames)
165

    
166
    def test_004_unique_image_names(self):
167
        """Test system images have unique names"""
168
        sys_images = filter(lambda x: x['owner'] == PLANKTON_USER,
169
                            self.dimages)
170
        names = sorted(map(lambda x: x["name"], sys_images))
171
        self.assertEqual(sorted(list(set(names))), names)
172

    
173
    def test_005_image_metadata(self):
174
        """Test every image has specific metadata defined"""
175
        keys = frozenset(["os", "description", "size"])
176
        details = self.client.list_images(detail=True)
177
        for i in details:
178
            self.assertTrue(keys.issubset(i["metadata"]["values"].keys()))
179

    
180

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

    
188
        cls.client = ComputeClient(API, TOKEN)
189
        cls.flavors = cls.client.list_flavors()
190
        cls.dflavors = cls.client.list_flavors(detail=True)
191

    
192
    def test_001_list_flavors(self):
193
        """Test flavor list actually returns flavors"""
194
        self.assertGreater(len(self.flavors), 0)
195

    
196
    def test_002_list_flavors_detailed(self):
197
        """Test detailed flavor list is the same length as list"""
198
        self.assertEquals(len(self.dflavors), len(self.flavors))
199

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

    
206
    def test_004_unique_flavor_names(self):
207
        """Test flavors have unique names"""
208
        names = sorted(map(lambda x: x["name"], self.flavors))
209
        self.assertEqual(sorted(list(set(names))), names)
210

    
211
    def test_005_well_formed_flavor_names(self):
212
        """Test flavors have names of the form CxxRyyDzz
213

214
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
215

216
        """
217
        for f in self.dflavors:
218
            self.assertEqual("C%dR%dD%d" % (f["cpu"], f["ram"], f["disk"]),
219
                             f["name"],
220
                             "Flavor %s does not match its specs." % f["name"])
221

    
222

    
223
class ServersTestCase(unittest.TestCase):
224
    """Test server lists for consistency"""
225
    @classmethod
226
    def setUpClass(cls):
227
        """Initialize kamaki, get (detailed) list of servers"""
228
        log.info("Getting simple and detailed list of servers")
229

    
230
        cls.client = ComputeClient(API, TOKEN)
231
        cls.servers = cls.client.list_servers()
232
        cls.dservers = cls.client.list_servers(detail=True)
233

    
234
    # def test_001_list_servers(self):
235
    #     """Test server list actually returns servers"""
236
    #     self.assertGreater(len(self.servers), 0)
237

    
238
    def test_002_list_servers_detailed(self):
239
        """Test detailed server list is the same length as list"""
240
        self.assertEqual(len(self.dservers), len(self.servers))
241

    
242
    def test_003_same_server_names(self):
243
        """Test detailed and simple flavor list contain same names"""
244
        names = sorted(map(lambda x: x["name"], self.servers))
245
        dnames = sorted(map(lambda x: x["name"], self.dservers))
246
        self.assertEqual(names, dnames)
247

    
248

    
249
# This class gets replicated into actual TestCases dynamically
250
class SpawnServerTestCase(unittest.TestCase):
251
    """Test scenario for server of the specified image"""
252

    
253
    @classmethod
254
    def setUpClass(cls):
255
        """Initialize a kamaki instance"""
256
        log.info("Spawning server for image `%s'", %cls.imagename)
257

    
258
        cls.client = ComputeClient(API, TOKEN)
259
        cls.cyclades = CycladesClient(API, TOKEN)
260

    
261
    def _get_ipv4(self, server):
262
        """Get the public IPv4 of a server from the detailed server info"""
263

    
264
        public_addrs = filter(lambda x: x["id"] == "public",
265
                              server["addresses"]["values"])
266
        self.assertEqual(len(public_addrs), 1)
267
        ipv4_addrs = filter(lambda x: x["version"] == 4,
268
                            public_addrs[0]["values"])
269
        self.assertEqual(len(ipv4_addrs), 1)
270
        return ipv4_addrs[0]["addr"]
271

    
272
    def _get_ipv6(self, server):
273
        """Get the public IPv6 of a server from the detailed server info"""
274
        public_addrs = filter(lambda x: x["id"] == "public",
275
                              server["addresses"]["values"])
276
        self.assertEqual(len(public_addrs), 1)
277
        ipv6_addrs = filter(lambda x: x["version"] == 6,
278
                            public_addrs[0]["values"])
279
        self.assertEqual(len(ipv6_addrs), 1)
280
        return ipv6_addrs[0]["addr"]
281

    
282
    def _connect_loginname(self, os):
283
        """Return the login name for connections based on the server OS"""
284
        if os in ("Ubuntu", "Kubuntu", "Fedora"):
285
            return "user"
286
        elif os in ("windows", "windows_alpha1"):
287
            return "Administrator"
288
        else:
289
            return "root"
290

    
291
    def _verify_server_status(self, current_status, new_status):
292
        """Verify a server has switched to a specified status"""
293
        server = self.client.get_server_details(self.serverid)
294
        if server["status"] not in (current_status, new_status):
295
            return None  # Do not raise exception, return so the test fails
296
        self.assertEquals(server["status"], new_status)
297

    
298
    def _get_connected_tcp_socket(self, family, host, port):
299
        """Get a connected socket from the specified family to host:port"""
300
        sock = None
301
        for res in \
302
            socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
303
                               socket.AI_PASSIVE):
304
            af, socktype, proto, canonname, sa = res
305
            try:
306
                sock = socket.socket(af, socktype, proto)
307
            except socket.error as msg:
308
                sock = None
309
                continue
310
            try:
311
                sock.connect(sa)
312
            except socket.error as msg:
313
                sock.close()
314
                sock = None
315
                continue
316
        self.assertIsNotNone(sock)
317
        return sock
318

    
319
    def _ping_once(self, ipv6, ip):
320
        """Test server responds to a single IPv4 or IPv6 ping"""
321
        cmd = "ping%s -c 2 -w 3 %s" % ("6" if ipv6 else "", ip)
322
        ping = subprocess.Popen(cmd, shell=True,
323
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
324
        (stdout, stderr) = ping.communicate()
325
        ret = ping.wait()
326
        self.assertEquals(ret, 0)
327

    
328
    def _get_hostname_over_ssh(self, hostip, username, password):
329
        ssh = paramiko.SSHClient()
330
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
331
        try:
332
            ssh.connect(hostip, username=username, password=password)
333
        except socket.error:
334
            raise AssertionError
335
        stdin, stdout, stderr = ssh.exec_command("hostname")
336
        lines = stdout.readlines()
337
        self.assertEqual(len(lines), 1)
338
        return lines[0]
339

    
340
    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
341
                                   opmsg, callable, *args, **kwargs):
342
        if warn_timeout == fail_timeout:
343
            warn_timeout = fail_timeout + 1
344
        warn_tmout = time.time() + warn_timeout
345
        fail_tmout = time.time() + fail_timeout
346
        while True:
347
            self.assertLess(time.time(), fail_tmout,
348
                            "operation `%s' timed out" % opmsg)
349
            if time.time() > warn_tmout:
350
                log.warning("Server %d: `%s' operation `%s' not done yet",
351
                            self.serverid, self.servername, opmsg)
352
            try:
353
                log.info("%s... " % opmsg)
354
                return callable(*args, **kwargs)
355
            except AssertionError:
356
                pass
357
            time.sleep(self.query_interval)
358

    
359
    def _insist_on_tcp_connection(self, family, host, port):
360
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
361
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
362
        msg = "connect over %s to %s:%s" % \
363
              (familystr.get(family, "Unknown"), host, port)
364
        sock = self._try_until_timeout_expires(
365
                self.action_timeout, self.action_timeout,
366
                msg, self._get_connected_tcp_socket,
367
                family, host, port)
368
        return sock
369

    
370
    def _insist_on_status_transition(self, current_status, new_status,
371
                                    fail_timeout, warn_timeout=None):
372
        msg = "Server %d: `%s', waiting for %s -> %s" % \
373
              (self.serverid, self.servername, current_status, new_status)
374
        if warn_timeout is None:
375
            warn_timeout = fail_timeout
376
        self._try_until_timeout_expires(warn_timeout, fail_timeout,
377
                                        msg, self._verify_server_status,
378
                                        current_status, new_status)
379
        # Ensure the status is actually the expected one
380
        server = self.client.get_server_details(self.serverid)
381
        self.assertEquals(server["status"], new_status)
382

    
383
    def _insist_on_ssh_hostname(self, hostip, username, password):
384
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
385
        hostname = self._try_until_timeout_expires(
386
                self.action_timeout, self.action_timeout,
387
                msg, self._get_hostname_over_ssh,
388
                hostip, username, password)
389

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

    
393
    def _check_file_through_ssh(self, hostip, username, password,
394
                                remotepath, content):
395
        msg = "Trying file injection through SSH to %s, as %s/%s" % \
396
            (hostip, username, password)
397
        log.info(msg)
398
        try:
399
            ssh = paramiko.SSHClient()
400
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
401
            ssh.connect(hostip, username=username, password=password)
402
        except socket.error:
403
            raise AssertionError
404

    
405
        transport = paramiko.Transport((hostip, 22))
406
        transport.connect(username=username, password=password)
407

    
408
        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
409
        sftp = paramiko.SFTPClient.from_transport(transport)
410
        sftp.get(remotepath, localpath)
411
        sftp.close()
412
        transport.close()
413

    
414
        f = open(localpath)
415
        remote_content = b64encode(f.read())
416

    
417
        # Check if files are the same
418
        return (remote_content == content)
419

    
420
    def _skipIf(self, condition, msg):
421
        if condition:
422
            self.skipTest(msg)
423

    
424
    def test_001_submit_create_server(self):
425
        """Test submit create server request"""
426

    
427
        log.info("Submit new server request")
428
        server = self.client.create_server(self.servername, self.flavorid,
429
                                           self.imageid, self.personality)
430

    
431
        log.info("Server id: " + str(server["id"]))
432
        log.info("Server password: " + server["adminPass"])
433
        self.assertEqual(server["name"], self.servername)
434
        self.assertEqual(server["flavorRef"], self.flavorid)
435
        self.assertEqual(server["imageRef"], self.imageid)
436
        self.assertEqual(server["status"], "BUILD")
437

    
438
        # Update class attributes to reflect data on building server
439
        cls = type(self)
440
        cls.serverid = server["id"]
441
        cls.username = None
442
        cls.passwd = server["adminPass"]
443

    
444
    def test_002a_server_is_building_in_list(self):
445
        """Test server is in BUILD state, in server list"""
446
        log.info("Server in BUILD state in server list")
447

    
448
        servers = self.client.list_servers(detail=True)
449
        servers = filter(lambda x: x["name"] == self.servername, servers)
450
        self.assertEqual(len(servers), 1)
451
        server = servers[0]
452
        self.assertEqual(server["name"], self.servername)
453
        self.assertEqual(server["flavorRef"], self.flavorid)
454
        self.assertEqual(server["imageRef"], self.imageid)
455
        self.assertEqual(server["status"], "BUILD")
456

    
457
    def test_002b_server_is_building_in_details(self):
458
        """Test server is in BUILD state, in details"""
459

    
460
        log.info("Server in BUILD state in details")
461

    
462
        server = self.client.get_server_details(self.serverid)
463
        self.assertEqual(server["name"], self.servername)
464
        self.assertEqual(server["flavorRef"], self.flavorid)
465
        self.assertEqual(server["imageRef"], self.imageid)
466
        self.assertEqual(server["status"], "BUILD")
467

    
468
    def test_002c_set_server_metadata(self):
469

    
470
        log.info("Creating server metadata")
471

    
472
        image = self.client.get_image_details(self.imageid)
473
        os = image["metadata"]["values"]["os"]
474
        users = image["metadata"]["values"].get("users", None)
475
        self.client.update_server_metadata(self.serverid, OS=os)
476

    
477
        userlist = users.split()
478

    
479
        # Determine the username to use for future connections
480
        # to this host
481
        cls = type(self)
482

    
483
        if "root" in userlist:
484
            cls.username = "root"
485
        elif users == None:
486
            cls.username = self._connect_loginname(os)
487
        else:
488
            cls.username = choice(userlist)
489

    
490
        self.assertIsNotNone(cls.username)
491

    
492
    def test_002d_verify_server_metadata(self):
493
        """Test server metadata keys are set based on image metadata"""
494

    
495
        log.info("Verifying image metadata")
496

    
497
        servermeta = self.client.get_server_metadata(self.serverid)
498
        imagemeta = self.client.get_image_metadata(self.imageid)
499

    
500
        self.assertEqual(servermeta["OS"], imagemeta["os"])
501

    
502
    def test_003_server_becomes_active(self):
503
        """Test server becomes ACTIVE"""
504

    
505
        log.info("Waiting for server to become ACTIVE")
506

    
507
        self._insist_on_status_transition("BUILD", "ACTIVE",
508
                                         self.build_fail, self.build_warning)
509

    
510
    def test_003a_get_server_oob_console(self):
511
        """Test getting OOB server console over VNC
512

513
        Implementation of RFB protocol follows
514
        http://www.realvnc.com/docs/rfbproto.pdf.
515

516
        """
517
        console = self.cyclades.get_server_console(self.serverid)
518
        self.assertEquals(console['type'], "vnc")
519
        sock = self._insist_on_tcp_connection(socket.AF_INET,
520
                                        console["host"], console["port"])
521

    
522
        # Step 1. ProtocolVersion message (par. 6.1.1)
523
        version = sock.recv(1024)
524
        self.assertEquals(version, 'RFB 003.008\n')
525
        sock.send(version)
526

    
527
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
528
        sec = sock.recv(1024)
529
        self.assertEquals(list(sec), ['\x01', '\x02'])
530

    
531
        # Step 3. Request VNC Authentication (par 6.1.2)
532
        sock.send('\x02')
533

    
534
        # Step 4. Receive Challenge (par 6.2.2)
535
        challenge = sock.recv(1024)
536
        self.assertEquals(len(challenge), 16)
537

    
538
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
539
        response = d3des_generate_response(
540
            (console["password"] + '\0' * 8)[:8], challenge)
541
        sock.send(response)
542

    
543
        # Step 6. SecurityResult (par 6.1.3)
544
        result = sock.recv(4)
545
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
546
        sock.close()
547

    
548
    def test_004_server_has_ipv4(self):
549
        """Test active server has a valid IPv4 address"""
550

    
551
        log.info("Validate server's IPv4")
552

    
553
        server = self.client.get_server_details(self.serverid)
554
        ipv4 = self._get_ipv4(server)
555
        self.assertEquals(IP(ipv4).version(), 4)
556

    
557
    def test_005_server_has_ipv6(self):
558
        """Test active server has a valid IPv6 address"""
559
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
560

    
561
        log.info("Validate server's IPv6")
562

    
563
        server = self.client.get_server_details(self.serverid)
564
        ipv6 = self._get_ipv6(server)
565
        self.assertEquals(IP(ipv6).version(), 6)
566

    
567
    def test_006_server_responds_to_ping_IPv4(self):
568
        """Test server responds to ping on IPv4 address"""
569

    
570
        log.info("Testing if server responds to pings in IPv4")
571

    
572
        server = self.client.get_server_details(self.serverid)
573
        ip = self._get_ipv4(server)
574
        self._try_until_timeout_expires(self.action_timeout,
575
                                        self.action_timeout,
576
                                        "PING IPv4 to %s" % ip,
577
                                        self._ping_once,
578
                                        False, ip)
579

    
580
    def test_007_server_responds_to_ping_IPv6(self):
581
        """Test server responds to ping on IPv6 address"""
582
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
583
        log.info("Testing if server responds to pings in IPv6")
584

    
585
        server = self.client.get_server_details(self.serverid)
586
        ip = self._get_ipv6(server)
587
        self._try_until_timeout_expires(self.action_timeout,
588
                                        self.action_timeout,
589
                                        "PING IPv6 to %s" % ip,
590
                                        self._ping_once,
591
                                        True, ip)
592

    
593
    def test_008_submit_shutdown_request(self):
594
        """Test submit request to shutdown server"""
595

    
596
        log.info("Shutting down server")
597

    
598
        self.cyclades.shutdown_server(self.serverid)
599

    
600
    def test_009_server_becomes_stopped(self):
601
        """Test server becomes STOPPED"""
602

    
603
        log.info("Waiting until server becomes STOPPED")
604
        self._insist_on_status_transition("ACTIVE", "STOPPED",
605
                                         self.action_timeout,
606
                                         self.action_timeout)
607

    
608
    def test_010_submit_start_request(self):
609
        """Test submit start server request"""
610

    
611
        log.info("Starting server")
612

    
613
        self.cyclades.start_server(self.serverid)
614

    
615
    def test_011_server_becomes_active(self):
616
        """Test server becomes ACTIVE again"""
617

    
618
        log.info("Waiting until server becomes ACTIVE")
619
        self._insist_on_status_transition("STOPPED", "ACTIVE",
620
                                         self.action_timeout,
621
                                         self.action_timeout)
622

    
623
    def test_011a_server_responds_to_ping_IPv4(self):
624
        """Test server OS is actually up and running again"""
625

    
626
        log.info("Testing if server is actually up and running")
627

    
628
        self.test_006_server_responds_to_ping_IPv4()
629

    
630
    def test_012_ssh_to_server_IPv4(self):
631
        """Test SSH to server public IPv4 works, verify hostname"""
632

    
633
        self._skipIf(self.is_windows, "only valid for Linux servers")
634
        server = self.client.get_server_details(self.serverid)
635
        self._insist_on_ssh_hostname(self._get_ipv4(server),
636
                                     self.username, self.passwd)
637

    
638
    def test_013_ssh_to_server_IPv6(self):
639
        """Test SSH to server public IPv6 works, verify hostname"""
640
        self._skipIf(self.is_windows, "only valid for Linux servers")
641
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
642

    
643
        server = self.client.get_server_details(self.serverid)
644
        self._insist_on_ssh_hostname(self._get_ipv6(server),
645
                                     self.username, self.passwd)
646

    
647
    def test_014_rdp_to_server_IPv4(self):
648
        "Test RDP connection to server public IPv4 works"""
649
        self._skipIf(not self.is_windows, "only valid for Windows servers")
650
        server = self.client.get_server_details(self.serverid)
651
        ipv4 = self._get_ipv4(server)
652
        sock = _insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
653

    
654
        # No actual RDP processing done. We assume the RDP server is there
655
        # if the connection to the RDP port is successful.
656
        # FIXME: Use rdesktop, analyze exit code? see manpage [costasd]
657
        sock.close()
658

    
659
    def test_015_rdp_to_server_IPv6(self):
660
        "Test RDP connection to server public IPv6 works"""
661
        self._skipIf(not self.is_windows, "only valid for Windows servers")
662
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
663

    
664
        server = self.client.get_server_details(self.serverid)
665
        ipv6 = self._get_ipv6(server)
666
        sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
667

    
668
        # No actual RDP processing done. We assume the RDP server is there
669
        # if the connection to the RDP port is successful.
670
        sock.close()
671

    
672
    def test_016_personality_is_enforced(self):
673
        """Test file injection for personality enforcement"""
674
        self._skipIf(self.is_windows, "only implemented for Linux servers")
675
        self._skipIf(self.personality == None, "No personality file selected")
676

    
677
        log.info("Trying to inject file for personality enforcement")
678

    
679
        server = self.client.get_server_details(self.serverid)
680

    
681
        for inj_file in self.personality:
682
            equal_files = self._check_file_through_ssh(self._get_ipv4(server),
683
                                                       inj_file['owner'],
684
                                                       self.passwd,
685
                                                       inj_file['path'],
686
                                                       inj_file['contents'])
687
            self.assertTrue(equal_files)
688

    
689
    def test_017_submit_delete_request(self):
690
        """Test submit request to delete server"""
691

    
692
        log.info("Deleting server")
693

    
694
        self.client.delete_server(self.serverid)
695

    
696
    def test_018_server_becomes_deleted(self):
697
        """Test server becomes DELETED"""
698

    
699
        log.info("Testing if server becomes DELETED")
700

    
701
        self._insist_on_status_transition("ACTIVE", "DELETED",
702
                                         self.action_timeout,
703
                                         self.action_timeout)
704

    
705
    def test_019_server_no_longer_in_server_list(self):
706
        """Test server is no longer in server list"""
707

    
708
        log.info("Test if server is no longer listed")
709

    
710
        servers = self.client.list_servers()
711
        self.assertNotIn(self.serverid, [s["id"] for s in servers])
712

    
713

    
714
class NetworkTestCase(unittest.TestCase):
715
    """ Testing networking in cyclades """
716

    
717
    @classmethod
718
    def setUpClass(cls):
719
        "Initialize kamaki, get list of current networks"
720

    
721
        cls.client = CycladesClient(API, TOKEN)
722
        cls.compute = ComputeClient(API, TOKEN)
723

    
724
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
725
                                          TEST_RUN_ID,
726
                                          cls.imagename)
727

    
728
        #Dictionary initialization for the vms credentials
729
        cls.serverid = dict()
730
        cls.username = dict()
731
        cls.password = dict()
732
        cls.is_windows = cls.imagename.lower().find("windows") >= 0
733

    
734
    def _skipIf(self, condition, msg):
735
        if condition:
736
            self.skipTest(msg)
737

    
738
    def _get_ipv4(self, server):
739
        """Get the public IPv4 of a server from the detailed server info"""
740

    
741
        public_addrs = filter(lambda x: x["id"] == "public",
742
                              server["addresses"]["values"])
743
        self.assertEqual(len(public_addrs), 1)
744
        ipv4_addrs = filter(lambda x: x["version"] == 4,
745
                            public_addrs[0]["values"])
746
        self.assertEqual(len(ipv4_addrs), 1)
747
        return ipv4_addrs[0]["addr"]
748

    
749
    def _connect_loginname(self, os):
750
        """Return the login name for connections based on the server OS"""
751
        if os in ("Ubuntu", "Kubuntu", "Fedora"):
752
            return "user"
753
        elif os in ("windows", "windows_alpha1"):
754
            return "Administrator"
755
        else:
756
            return "root"
757

    
758
    def _ping_once(self, ip):
759

    
760
        """Test server responds to a single IPv4 or IPv6 ping"""
761
        cmd = "ping -c 2 -w 3 %s" % (ip)
762
        ping = subprocess.Popen(cmd, shell=True,
763
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
764
        (stdout, stderr) = ping.communicate()
765
        ret = ping.wait()
766

    
767
        return (ret == 0)
768

    
769
    def test_00001a_submit_create_server_A(self):
770
        """Test submit create server request"""
771

    
772
        log.info("Creating test server A")
773

    
774
        serverA = self.client.create_server(self.servername, self.flavorid,
775
                                            self.imageid, personality=None)
776

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

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

    
787
        log.info("Server A id:" + str(serverA["id"]))
788
        log.info("Server password " + (self.password['A']))
789

    
790
    def test_00001b_serverA_becomes_active(self):
791
        """Test server becomes ACTIVE"""
792

    
793
        log.info("Waiting until test server A becomes ACTIVE")
794

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

    
807
        self.assertTrue(active)
808

    
809
    def test_00002a_submit_create_server_B(self):
810
        """Test submit create server request"""
811

    
812
        log.info("Creating test server B")
813

    
814
        serverB = self.client.create_server(self.servername, self.flavorid,
815
                                            self.imageid, personality=None)
816

    
817
        self.assertEqual(serverB["name"], self.servername)
818
        self.assertEqual(serverB["flavorRef"], self.flavorid)
819
        self.assertEqual(serverB["imageRef"], self.imageid)
820
        self.assertEqual(serverB["status"], "BUILD")
821

    
822
        # Update class attributes to reflect data on building server
823
        self.serverid['B'] = serverB["id"]
824
        self.username['B'] = None
825
        self.password['B'] = serverB["adminPass"]
826

    
827
        log.info("Server B id: " + str(serverB["id"]))
828
        log.info("Password " + (self.password['B']))
829

    
830
    def test_00002b_serverB_becomes_active(self):
831
        """Test server becomes ACTIVE"""
832

    
833
        log.info("Waiting until test server B becomes ACTIVE")
834

    
835
        fail_tmout = time.time() + self.action_timeout
836
        while True:
837
            d = self.client.get_server_details(self.serverid['B'])
838
            status = d['status']
839
            if status == 'ACTIVE':
840
                active = True
841
                break
842
            elif time.time() > fail_tmout:
843
                self.assertLess(time.time(), fail_tmout)
844
            else:
845
                time.sleep(self.query_interval)
846

    
847
        self.assertTrue(active)
848

    
849
    def test_001_create_network(self):
850
        """Test submit create network request"""
851

    
852
        log.info("Submit new network request")
853

    
854
        name = SNF_TEST_PREFIX + TEST_RUN_ID
855
        previous_num = len(self.client.list_networks())
856
        network = self.client.create_network(name)
857

    
858
        #Test if right name is assigned
859
        self.assertEqual(network['name'], name)
860

    
861
        # Update class attributes
862
        cls = type(self)
863
        cls.networkid = network['id']
864
        networks = self.client.list_networks()
865

    
866
        #Test if new network is created
867
        self.assertTrue(len(networks) > previous_num)
868

    
869
    def test_002_connect_to_network(self):
870
        """Test connect VMs to network"""
871

    
872
        log.info("Connect VMs to private network")
873

    
874
        self.client.connect_server(self.serverid['A'], self.networkid)
875
        self.client.connect_server(self.serverid['B'], self.networkid)
876

    
877
        #Insist on connecting until action timeout
878
        fail_tmout = time.time() + self.action_timeout
879

    
880
        while True:
881
            connected = (self.client.get_network_details(self.networkid))
882
            connections = connected['servers']['values']
883
            if (self.serverid['A'] in connections) \
884
                    and (self.serverid['B'] in connections):
885
                conn_exists = True
886
                break
887
            elif time.time() > fail_tmout:
888
                self.assertLess(time.time(), fail_tmout)
889
            else:
890
                time.sleep(self.query_interval)
891

    
892
        self.assertTrue(conn_exists)
893

    
894
    def test_002a_reboot(self):
895
        """Rebooting server A"""
896

    
897
        log.info("Rebooting server A")
898

    
899
        self.client.shutdown_server(self.serverid['A'])
900

    
901
        fail_tmout = time.time() + self.action_timeout
902
        while True:
903
            d = self.client.get_server_details(self.serverid['A'])
904
            status = d['status']
905
            if status == 'STOPPED':
906
                break
907
            elif time.time() > fail_tmout:
908
                self.assertLess(time.time(), fail_tmout)
909
            else:
910
                time.sleep(self.query_interval)
911

    
912
        self.client.start_server(self.serverid['A'])
913

    
914
        while True:
915
            d = self.client.get_server_details(self.serverid['A'])
916
            status = d['status']
917
            if status == 'ACTIVE':
918
                active = True
919
                break
920
            elif time.time() > fail_tmout:
921
                self.assertLess(time.time(), fail_tmout)
922
            else:
923
                time.sleep(self.query_interval)
924

    
925
        self.assertTrue(active)
926

    
927
    def test_002b_ping_server_A(self):
928
        "Test if server A is pingable"
929

    
930
        log.info("Testing if server A is pingable")
931

    
932
        server = self.client.get_server_details(self.serverid['A'])
933
        ip = self._get_ipv4(server)
934

    
935
        fail_tmout = time.time() + self.action_timeout
936

    
937
        s = False
938

    
939
        while True:
940

    
941
            if self._ping_once(ip):
942
                s = True
943
                break
944

    
945
            elif time.time() > fail_tmout:
946
                self.assertLess(time.time(), fail_tmout)
947

    
948
            else:
949
                time.sleep(self.query_interval)
950

    
951
        self.assertTrue(s)
952

    
953
    def test_002c_reboot(self):
954
        """Reboot server B"""
955

    
956
        log.info("Rebooting server B")
957

    
958
        self.client.shutdown_server(self.serverid['B'])
959

    
960
        fail_tmout = time.time() + self.action_timeout
961
        while True:
962
            d = self.client.get_server_details(self.serverid['B'])
963
            status = d['status']
964
            if status == 'STOPPED':
965
                break
966
            elif time.time() > fail_tmout:
967
                self.assertLess(time.time(), fail_tmout)
968
            else:
969
                time.sleep(self.query_interval)
970

    
971
        self.client.start_server(self.serverid['B'])
972

    
973
        while True:
974
            d = self.client.get_server_details(self.serverid['B'])
975
            status = d['status']
976
            if status == 'ACTIVE':
977
                active = True
978
                break
979
            elif time.time() > fail_tmout:
980
                self.assertLess(time.time(), fail_tmout)
981
            else:
982
                time.sleep(self.query_interval)
983

    
984
        self.assertTrue(active)
985

    
986
    def test_002d_ping_server_B(self):
987
        """Test if server B is pingable"""
988

    
989
        log.info("Testing if server B is pingable")
990
        server = self.client.get_server_details(self.serverid['B'])
991
        ip = self._get_ipv4(server)
992

    
993
        fail_tmout = time.time() + self.action_timeout
994

    
995
        s = False
996

    
997
        while True:
998
            if self._ping_once(ip):
999
                s = True
1000
                break
1001

    
1002
            elif time.time() > fail_tmout:
1003
                self.assertLess(time.time(), fail_tmout)
1004

    
1005
            else:
1006
                time.sleep(self.query_interval)
1007

    
1008
        self.assertTrue(s)
1009

    
1010
    def test_003a_setup_interface_A(self):
1011
        """Set up eth1 for server A"""
1012

    
1013
        self._skipIf(self.is_windows, "only valid for Linux servers")
1014

    
1015
        log.info("Setting up interface eth1 for server A")
1016

    
1017
        server = self.client.get_server_details(self.serverid['A'])
1018
        image = self.client.get_image_details(self.imageid)
1019
        os = image['metadata']['values']['os']
1020

    
1021
        users = image["metadata"]["values"].get("users", None)
1022
        userlist = users.split()
1023

    
1024
        if "root" in userlist:
1025
            loginname = "root"
1026
        elif users == None:
1027
            loginname = self._connect_loginname(os)
1028
        else:
1029
            loginname = choice(userlist)
1030

    
1031
        hostip = self._get_ipv4(server)
1032
        myPass = self.password['A']
1033

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

    
1036
        res = False
1037

    
1038
        if loginname != "root":
1039
            with settings(
1040
                hide('warnings', 'running'),
1041
                warn_only=True,
1042
                host_string=hostip,
1043
                user=loginname, password=myPass
1044
                ):
1045

    
1046
                if len(sudo('ifconfig eth1 192.168.0.12')) == 0:
1047
                    res = True
1048

    
1049
        else:
1050
            with settings(
1051
                hide('warnings', 'running'),
1052
                warn_only=True,
1053
                host_string=hostip,
1054
                user=loginname, password=myPass
1055
                ):
1056

    
1057
                if len(run('ifconfig eth1 192.168.0.12')) == 0:
1058
                    res = True
1059

    
1060
        self.assertTrue(res)
1061

    
1062
    def test_003b_setup_interface_B(self):
1063
        """Setup eth1 for server B"""
1064

    
1065
        self._skipIf(self.is_windows, "only valid for Linux servers")
1066

    
1067
        log.info("Setting up interface eth1 for server B")
1068

    
1069
        server = self.client.get_server_details(self.serverid['B'])
1070
        image = self.client.get_image_details(self.imageid)
1071
        os = image['metadata']['values']['os']
1072

    
1073
        users = image["metadata"]["values"].get("users", None)
1074
        userlist = users.split()
1075

    
1076
        if "root" in userlist:
1077
            loginname = "root"
1078
        elif users == None:
1079
            loginname = self._connect_loginname(os)
1080
        else:
1081
            loginname = choice(userlist)
1082

    
1083
        hostip = self._get_ipv4(server)
1084
        myPass = self.password['B']
1085

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

    
1088
        res = False
1089

    
1090
        if loginname != "root":
1091
            with settings(
1092
                hide('warnings', 'running'),
1093
                warn_only=True,
1094
                host_string=hostip,
1095
                user=loginname, password=myPass
1096
                ):
1097

    
1098
                if len(sudo('ifconfig eth1 192.168.0.13')) == 0:
1099
                    res = True
1100

    
1101
        else:
1102
            with settings(
1103
                hide('warnings', 'running'),
1104
                warn_only=True,
1105
                host_string=hostip,
1106
                user=loginname, password=myPass
1107
                ):
1108

    
1109
                if len(run('ifconfig eth1 192.168.0.13')) == 0:
1110
                    res = True
1111

    
1112
        self.assertTrue(res)
1113

    
1114
    def test_003c_test_connection_exists(self):
1115
        """Ping server B from server A to test if connection exists"""
1116

    
1117
        self._skipIf(self.is_windows, "only valid for Linux servers")
1118

    
1119
        log.info("Testing if server A is actually connected to server B")
1120

    
1121
        server = self.client.get_server_details(self.serverid['A'])
1122
        image = self.client.get_image_details(self.imageid)
1123
        os = image['metadata']['values']['os']
1124
        hostip = self._get_ipv4(server)
1125

    
1126
        users = image["metadata"]["values"].get("users", None)
1127
        userlist = users.split()
1128

    
1129
        if "root" in userlist:
1130
            loginname = "root"
1131
        elif users == None:
1132
            loginname = self._connect_loginname(os)
1133
        else:
1134
            loginname = choice(userlist)
1135

    
1136
        myPass = self.password['A']
1137

    
1138
        try:
1139
            ssh = paramiko.SSHClient()
1140
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
1141
            ssh.connect(hostip, username=loginname, password=myPass)
1142
        except socket.error:
1143
            raise AssertionError
1144

    
1145
        cmd = "if ping -c 2 -w 3 192.168.0.13 >/dev/null; \
1146
               then echo \'True\'; fi;"
1147
        stdin, stdout, stderr = ssh.exec_command(cmd)
1148
        lines = stdout.readlines()
1149

    
1150
        exists = False
1151

    
1152
        if 'True\n' in lines:
1153
            exists = True
1154

    
1155
        self.assertTrue(exists)
1156

    
1157
    def test_004_disconnect_from_network(self):
1158
        "Disconnecting server A and B from network"
1159

    
1160
        log.info("Disconnecting servers from private network")
1161

    
1162
        prev_state = self.client.get_network_details(self.networkid)
1163
        prev_conn = len(prev_state['servers']['values'])
1164

    
1165
        self.client.disconnect_server(self.serverid['A'], self.networkid)
1166
        self.client.disconnect_server(self.serverid['B'], self.networkid)
1167

    
1168
        #Insist on deleting until action timeout
1169
        fail_tmout = time.time() + self.action_timeout
1170

    
1171
        while True:
1172
            connected = (self.client.get_network_details(self.networkid))
1173
            connections = connected['servers']['values']
1174
            if ((self.serverid['A'] not in connections) and
1175
                (self.serverid['B'] not in connections)):
1176
                conn_exists = False
1177
                break
1178
            elif time.time() > fail_tmout:
1179
                self.assertLess(time.time(), fail_tmout)
1180
            else:
1181
                time.sleep(self.query_interval)
1182

    
1183
        self.assertFalse(conn_exists)
1184

    
1185
    def test_005_destroy_network(self):
1186
        """Test submit delete network request"""
1187

    
1188
        log.info("Submitting delete network request")
1189

    
1190
        self.client.delete_network(self.networkid)
1191
        networks = self.client.list_networks()
1192

    
1193
        curr_net = []
1194
        for net in networks:
1195
            curr_net.append(net['id'])
1196

    
1197
        self.assertTrue(self.networkid not in curr_net)
1198

    
1199
    def test_006_cleanup_servers(self):
1200
        """Cleanup servers created for this test"""
1201

    
1202
        log.info("Delete servers created for this test")
1203

    
1204
        self.compute.delete_server(self.serverid['A'])
1205
        self.compute.delete_server(self.serverid['B'])
1206

    
1207
        fail_tmout = time.time() + self.action_timeout
1208

    
1209
        #Ensure server gets deleted
1210
        status = dict()
1211

    
1212
        while True:
1213
            details = self.compute.get_server_details(self.serverid['A'])
1214
            status['A'] = details['status']
1215
            details = self.compute.get_server_details(self.serverid['B'])
1216
            status['B'] = details['status']
1217
            if (status['A'] == 'DELETED') and (status['B'] == 'DELETED'):
1218
                deleted = True
1219
                break
1220
            elif time.time() > fail_tmout:
1221
                self.assertLess(time.time(), fail_tmout)
1222
            else:
1223
                time.sleep(self.query_interval)
1224

    
1225
        self.assertTrue(deleted)
1226

    
1227

    
1228
class TestRunnerProcess(Process):
1229
    """A distinct process used to execute part of the tests in parallel"""
1230
    def __init__(self, **kw):
1231
        Process.__init__(self, **kw)
1232
        kwargs = kw["kwargs"]
1233
        self.testq = kwargs["testq"]
1234
        self.runner = kwargs["runner"]
1235

    
1236
    def run(self):
1237
        # Make sure this test runner process dies with the parent
1238
        # and is not left behind.
1239
        #
1240
        # WARNING: This uses the prctl(2) call and is
1241
        # Linux-specific.
1242
        prctl.set_pdeathsig(signal.SIGHUP)
1243

    
1244
        while True:
1245
            log.debug("I am process %d, GETting from queue is %s",
1246
                     os.getpid(), self.testq)
1247
            msg = self.testq.get()
1248
            log.debug("Dequeued msg: %s", msg)
1249

    
1250
            if msg == "TEST_RUNNER_TERMINATE":
1251
                raise SystemExit
1252
            elif issubclass(msg, unittest.TestCase):
1253
                # Assemble a TestSuite, and run it
1254
                suite = unittest.TestLoader().loadTestsFromTestCase(msg)
1255
                self.runner.run(suite)
1256
            else:
1257
                raise Exception("Cannot handle msg: %s" % msg)
1258

    
1259

    
1260
def _run_cases_in_parallel(cases, fanout=1, runner=None):
1261
    """Run instances of TestCase in parallel, in a number of distinct processes
1262

1263
    The cases iterable specifies the TestCases to be executed in parallel,
1264
    by test runners running in distinct processes.
1265
    The fanout parameter specifies the number of processes to spawn,
1266
    and defaults to 1.
1267
    The runner argument specifies the test runner class to use inside each
1268
    runner process.
1269

1270
    """
1271
    if runner is None:
1272
        runner = unittest.TextTestRunner(verbosity=2, failfast=True)
1273

    
1274
    # testq: The master process enqueues TestCase objects into this queue,
1275
    #        test runner processes pick them up for execution, in parallel.
1276
    testq = Queue()
1277
    runners = []
1278
    for i in xrange(0, fanout):
1279
        kwargs = dict(testq=testq, runner=runner)
1280
        runners.append(TestRunnerProcess(kwargs=kwargs))
1281

    
1282
    log.info("Spawning %d test runner processes", len(runners))
1283
    for p in runners:
1284
        p.start()
1285
    log.debug("Spawned %d test runners, PIDs are %s",
1286
              len(runners), [p.pid for p in runners])
1287

    
1288
    # Enqueue test cases
1289
    map(testq.put, cases)
1290
    map(testq.put, ["TEST_RUNNER_TERMINATE"] * len(runners))
1291

    
1292
    log.debug("Joining %d processes", len(runners))
1293
    for p in runners:
1294
        p.join()
1295
    log.debug("Done joining %d processes", len(runners))
1296

    
1297

    
1298
def _spawn_server_test_case(**kwargs):
1299
    """Construct a new unit test case class from SpawnServerTestCase"""
1300

    
1301
    name = "SpawnServerTestCase_%s" % kwargs["imageid"]
1302
    cls = type(name, (SpawnServerTestCase,), kwargs)
1303

    
1304
    # Patch extra parameters into test names by manipulating method docstrings
1305
    for (mname, m) in \
1306
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
1307
        if hasattr(m, __doc__):
1308
            m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
1309

    
1310
    # Make sure the class can be pickled, by listing it among
1311
    # the attributes of __main__. A PicklingError is raised otherwise.
1312
    setattr(__main__, name, cls)
1313
    return cls
1314

    
1315

    
1316
def _spawn_network_test_case(**kwargs):
1317
    """Construct a new unit test case class from NetworkTestCase"""
1318

    
1319
    name = "NetworkTestCase" + TEST_RUN_ID
1320
    cls = type(name, (NetworkTestCase,), kwargs)
1321

    
1322
    # Make sure the class can be pickled, by listing it among
1323
    # the attributes of __main__. A PicklingError is raised otherwise.
1324
    setattr(__main__, name, cls)
1325
    return cls
1326

    
1327

    
1328
def cleanup_servers(delete_stale=False):
1329

    
1330
    c = ComputeClient(API, TOKEN)
1331

    
1332
    servers = c.list_servers()
1333
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
1334

    
1335
    if len(stale) == 0:
1336
        return
1337

    
1338
    print >> sys.stderr, yellow + "Found these stale servers from previous runs:" + normal
1339
    print "    " + \
1340
          "\n    ".join(["%d: %s" % (s["id"], s["name"]) for s in stale])
1341

    
1342
    if delete_stale:
1343
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
1344
        for server in stale:
1345
            c.delete_server(server["id"])
1346
        print >> sys.stderr, green + "    ...done" + normal
1347
    else:
1348
        print >> sys.stderr, "Use --delete-stale to delete them."
1349

    
1350

    
1351
def cleanup_networks(delete_stale=False):
1352

    
1353
    c = CycladesClient(API, TOKEN)
1354

    
1355
    networks = c.list_networks()
1356
    stale = [n for n in networks if n["name"].startswith(SNF_TEST_PREFIX)]
1357

    
1358
    if len(stale) == 0:
1359
        return
1360

    
1361
    print >> sys.stderr, yellow + "Found these stale networks from previous runs:" + normal
1362
    print "    " + \
1363
          "\n    ".join(["%s: %s" % (str(n["id"]), n["name"]) for n in stale])
1364

    
1365
    if delete_stale:
1366
        print >> sys.stderr, "Deleting %d stale networks:" % len(stale)
1367
        for network in stale:
1368
            c.delete_network(network["id"])
1369
        print >> sys.stderr, green + "    ...done" + normal
1370
    else:
1371
        print >> sys.stderr, "Use --delete-stale to delete them."
1372

    
1373

    
1374
def parse_comma(option, opt, value, parser):
1375
    tests = set(['all', 'auth', 'images', 'flavors',
1376
               'servers', 'server_spawn', 'network_spawn'])
1377
    parse_input = value.split(',')
1378

    
1379
    if not (set(parse_input)).issubset(tests):
1380
        raise OptionValueError("The selected set of tests is invalid")
1381

    
1382
    setattr(parser.values, option.dest, value.split(','))
1383

    
1384

    
1385
def parse_arguments(args):
1386

    
1387
    kw = {}
1388
    kw["usage"] = "%prog [options]"
1389
    kw["description"] = \
1390
        "%prog runs a number of test scenarios on a " \
1391
        "Synnefo deployment."
1392

    
1393
    parser = OptionParser(**kw)
1394
    parser.disable_interspersed_args()
1395

    
1396
    parser.add_option("--api",
1397
                      action="store", type="string", dest="api",
1398
                      help="The API URI to use to reach the Synnefo API",
1399
                      default=DEFAULT_API)
1400
    parser.add_option("--plankton",
1401
                      action="store", type="string", dest="plankton",
1402
                      help="The API URI to use to reach the Plankton API",
1403
                      default=DEFAULT_PLANKTON)
1404
    parser.add_option("--plankton-user",
1405
                      action="store", type="string", dest="plankton_user",
1406
                      help="Owner of system images",
1407
                      default=DEFAULT_PLANKTON_USER)
1408
    parser.add_option("--token",
1409
                      action="store", type="string", dest="token",
1410
                      help="The token to use for authentication to the API")
1411
    parser.add_option("--nofailfast",
1412
                      action="store_true", dest="nofailfast",
1413
                      help="Do not fail immediately if one of the tests " \
1414
                           "fails (EXPERIMENTAL)",
1415
                      default=False)
1416
    parser.add_option("--no-ipv6",
1417
                      action="store_true", dest="no_ipv6",
1418
                      help="Disables ipv6 related tests",
1419
                      default=False)
1420
    parser.add_option("--action-timeout",
1421
                      action="store", type="int", dest="action_timeout",
1422
                      metavar="TIMEOUT",
1423
                      help="Wait SECONDS seconds for a server action to " \
1424
                           "complete, then the test is considered failed",
1425
                      default=100)
1426
    parser.add_option("--build-warning",
1427
                      action="store", type="int", dest="build_warning",
1428
                      metavar="TIMEOUT",
1429
                      help="Warn if TIMEOUT seconds have passed and a " \
1430
                           "build operation is still pending",
1431
                      default=600)
1432
    parser.add_option("--build-fail",
1433
                      action="store", type="int", dest="build_fail",
1434
                      metavar="BUILD_TIMEOUT",
1435
                      help="Fail the test if TIMEOUT seconds have passed " \
1436
                           "and a build operation is still incomplete",
1437
                      default=900)
1438
    parser.add_option("--query-interval",
1439
                      action="store", type="int", dest="query_interval",
1440
                      metavar="INTERVAL",
1441
                      help="Query server status when requests are pending " \
1442
                           "every INTERVAL seconds",
1443
                      default=3)
1444
    parser.add_option("--fanout",
1445
                      action="store", type="int", dest="fanout",
1446
                      metavar="COUNT",
1447
                      help="Spawn up to COUNT child processes to execute " \
1448
                           "in parallel, essentially have up to COUNT " \
1449
                           "server build requests outstanding (EXPERIMENTAL)",
1450
                      default=1)
1451
    parser.add_option("--force-flavor",
1452
                      action="store", type="int", dest="force_flavorid",
1453
                      metavar="FLAVOR ID",
1454
                      help="Force all server creations to use the specified "\
1455
                           "FLAVOR ID instead of a randomly chosen one, " \
1456
                           "useful if disk space is scarce",
1457
                      default=None)
1458
    parser.add_option("--image-id",
1459
                      action="store", type="string", dest="force_imageid",
1460
                      metavar="IMAGE ID",
1461
                      help="Test the specified image id, use 'all' to test " \
1462
                           "all available images (mandatory argument)",
1463
                      default=None)
1464
    parser.add_option("--show-stale",
1465
                      action="store_true", dest="show_stale",
1466
                      help="Show stale servers from previous runs, whose "\
1467
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1468
                      default=False)
1469
    parser.add_option("--delete-stale",
1470
                      action="store_true", dest="delete_stale",
1471
                      help="Delete stale servers from previous runs, whose "\
1472
                           "name starts with `%s'" % SNF_TEST_PREFIX,
1473
                      default=False)
1474
    parser.add_option("--force-personality",
1475
                      action="store", type="string", dest="personality_path",
1476
                      help="Force a personality file injection.\
1477
                            File path required. ",
1478
                      default=None)
1479
    parser.add_option("--log-folder",
1480
                      action="store", type="string", dest="log_folder",
1481
                      help="Define the absolute path where the output \
1482
                            log is stored. ",
1483
                      default="/var/log/burnin/")
1484
    parser.add_option("--set-tests",
1485
                      action="callback",
1486
                      dest="tests",
1487
                      type="string",
1488
                      help='Set comma seperated tests for this run. \
1489
                            Available tests: auth, images, flavors, \
1490
                                             servers, server_spawn, \
1491
                                             network_spawn. \
1492
                            Default = all',
1493
                      default='all',
1494
                      callback=parse_comma)
1495

    
1496
    # FIXME: Change the default for build-fanout to 10
1497
    # FIXME: Allow the user to specify a specific set of Images to test
1498

    
1499
    (opts, args) = parser.parse_args(args)
1500

    
1501
    # Verify arguments
1502
    if opts.delete_stale:
1503
        opts.show_stale = True
1504

    
1505
    if not opts.show_stale:
1506
        if not opts.force_imageid:
1507
            print >>sys.stderr, red + "The --image-id argument " \
1508
                                       "is mandatory.\n" + normal
1509
            parser.print_help()
1510
            sys.exit(1)
1511

    
1512
        if not opts.token:
1513
            print >>sys.stderr, red + "The --token argument is " \
1514
                                      "mandatory.\n" + normal
1515
            parser.print_help()
1516
            sys.exit(1)
1517

    
1518
        if opts.force_imageid != 'all':
1519
            try:
1520
                opts.force_imageid = str(opts.force_imageid)
1521
            except ValueError:
1522
                print >>sys.stderr, red + "Invalid value specified for" \
1523
                    "--image-id. Use a valid id, or `all'." + normal
1524
                sys.exit(1)
1525

    
1526
    return (opts, args)
1527

    
1528

    
1529
def main():
1530
    """Assemble test cases into a test suite, and run it
1531

1532
    IMPORTANT: Tests have dependencies and have to be run in the specified
1533
    order inside a single test case. They communicate through attributes of the
1534
    corresponding TestCase class (shared fixtures). Distinct subclasses of
1535
    TestCase MAY SHARE NO DATA, since they are run in parallel, in distinct
1536
    test runner processes.
1537

1538
    """
1539

    
1540
    (opts, args) = parse_arguments(sys.argv[1:])
1541

    
1542
    global API, TOKEN, PLANKTON, PLANKTON_USER, NO_IPV6
1543
    API = opts.api
1544
    TOKEN = opts.token
1545
    PLANKTON = opts.plankton
1546
    PLANKTON_USER = opts.plankton_user
1547
    NO_IPV6 = opts.no_ipv6
1548

    
1549
    # Cleanup stale servers from previous runs
1550
    if opts.show_stale:
1551
        cleanup_servers(delete_stale=opts.delete_stale)
1552
        cleanup_networks(delete_stale=opts.delete_stale)
1553
        return 0
1554

    
1555
    # Initialize a kamaki instance, get flavors, images
1556
    c = ComputeClient(API, TOKEN)
1557

    
1558
    DIMAGES = c.list_images(detail=True)
1559
    DFLAVORS = c.list_flavors(detail=True)
1560

    
1561
    # FIXME: logging, log, LOG PID, TEST_RUN_ID, arguments
1562
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
1563
    #unittest.main(verbosity=2, catchbreak=True)
1564

    
1565
    if opts.force_imageid == 'all':
1566
        test_images = DIMAGES
1567
    else:
1568
        test_images = filter(lambda x: x["id"] == opts.force_imageid, DIMAGES)
1569

    
1570
    #New folder for log per image
1571
    if not os.path.exists(opts.log_folder):
1572
        os.mkdir(opts.log_folder)
1573

    
1574
    test_folder = os.path.join(opts.log_folder, TEST_RUN_ID)
1575
    os.mkdir(test_folder)
1576

    
1577
    for image in test_images:
1578

    
1579
        imageid = str(image["id"])
1580

    
1581
        if opts.force_flavorid:
1582
            flavorid = opts.force_flavorid
1583
        else:
1584
            flavorid = choice([f["id"] for f in DFLAVORS if f["disk"] >= 20])
1585

    
1586
        imagename = image["name"]
1587

    
1588
        #Personality dictionary for file injection test
1589
        if opts.personality_path != None:
1590
            f = open(opts.personality_path)
1591
            content = b64encode(f.read())
1592
            personality = []
1593
            st = os.stat(opts.personality_path)
1594
            personality.append({
1595
                    'path': '/root/test_inj_file',
1596
                    'owner': 'root',
1597
                    'group': 'root',
1598
                    'mode': 0x7777 & st.st_mode,
1599
                    'contents': content
1600
                    })
1601
        else:
1602
            personality = None
1603

    
1604
        servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, imagename)
1605
        is_windows = imagename.lower().find("windows") >= 0
1606

    
1607
        ServerTestCase = _spawn_server_test_case(
1608
            imageid=imageid,
1609
            flavorid=flavorid,
1610
            imagename=imagename,
1611
            personality=personality,
1612
            servername=servername,
1613
            is_windows=is_windows,
1614
            action_timeout=opts.action_timeout,
1615
            build_warning=opts.build_warning,
1616
            build_fail=opts.build_fail,
1617
            query_interval=opts.query_interval,
1618
            )
1619

    
1620
        NetworkTestCase = _spawn_network_test_case(
1621
            action_timeout=opts.action_timeout,
1622
            imageid=imageid,
1623
            flavorid=flavorid,
1624
            imagename=imagename,
1625
            query_interval=opts.query_interval,
1626
            )
1627

    
1628
        test_dict = {'auth': UnauthorizedTestCase,
1629
                     'images': ImagesTestCase,
1630
                     'flavors': FlavorsTestCase,
1631
                     'servers': ServersTestCase,
1632
                     'server_spawn': ServerTestCase,
1633
                     'network_spawn': NetworkTestCase}
1634

    
1635
        seq_cases = []
1636
        if 'all' in opts.tests:
1637
            seq_cases = [UnauthorizedTestCase, ImagesTestCase, FlavorsTestCase,
1638
                         ServersTestCase, ServerTestCase, NetworkTestCase]
1639
        else:
1640
            for test in opts.tests:
1641
                seq_cases.append(test_dict[test])
1642

    
1643
        #folder for each image
1644
        image_folder = os.path.join(test_folder, imageid)
1645
        os.mkdir(image_folder)
1646

    
1647
        for case in seq_cases:
1648

    
1649
            test = (key for key, value in test_dict.items()
1650
                    if value == case).next()
1651

    
1652
            log.info(yellow + '* Starting testcase: %s' %test + normal)
1653
            log_file = os.path.join(image_folder, 'details_' +
1654
                                    (case.__name__) + "_" +
1655
                                    TEST_RUN_ID + '.log')
1656
            fail_file = os.path.join(image_folder, 'failed_' +
1657
                                     (case.__name__) + "_" +
1658
                                     TEST_RUN_ID + '.log')
1659
            error_file = os.path.join(image_folder, 'error_' +
1660
                                      (case.__name__) + "_" +
1661
                                      TEST_RUN_ID + '.log')
1662

    
1663
            f = open(log_file, "w")
1664
            fail = open(fail_file, "w")
1665
            error = open(error_file, "w")
1666

    
1667
            suite = unittest.TestLoader().loadTestsFromTestCase(case)
1668
            runner = unittest.TextTestRunner(f, verbosity=2, failfast=True)
1669
            result = runner.run(suite)
1670

    
1671
            for res in result.errors:
1672
                log.error("snf-burnin encountered an error in " \
1673
                              "testcase: %s" %test)
1674
                log.error("See log for details")
1675
                error.write(str(res[0]) + '\n')
1676
                error.write(str(res[0].shortDescription()) + '\n')
1677
                error.write('\n')
1678

    
1679
            for res in result.failures:
1680
                log.error("snf-burnin failed in testcase: %s" %test)
1681
                log.error("See log for details")
1682
                fail.write(str(res[0]) + '\n')
1683
                fail.write(str(res[0].shortDescription()) + '\n')
1684
                fail.write('\n')
1685
                if opts.nofailfast == False:
1686
                    sys.exit()
1687

    
1688
            if (len(result.failures) == 0) and (len(result.errors) == 0):
1689
                log.debug("Passed testcase: %s" %test)
1690

    
1691
if __name__ == "__main__":
1692
    sys.exit(main())