Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin.py @ 2e3b7dc8

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(["osfamily", "root_partition"])
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
        cls.client = ComputeClient(API, TOKEN)
258
        cls.cyclades = CycladesClient(API, TOKEN)
259

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
467
    def test_002c_set_server_metadata(self):
468

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

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

    
476
        userlist = users.split()
477

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

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

    
489
        self.assertIsNotNone(cls.username)
490

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
610
        log.info("Starting server")
611

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

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

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

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

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

    
627
        self.test_006_server_responds_to_ping_IPv4()
628

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
691
        log.info("Deleting server")
692

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

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

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

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

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

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

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

    
712

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

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

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

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

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

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

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

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

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

    
757
    def _ping_once(self, ip):
758

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

    
766
        return (ret == 0)
767

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

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

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

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

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

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

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

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

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

    
806
        self.assertTrue(active)
807

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

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

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

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

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

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

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

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

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

    
846
        self.assertTrue(active)
847

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

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

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

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

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

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

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

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

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

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

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

    
891
        self.assertTrue(conn_exists)
892

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

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

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

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

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

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

    
924
        self.assertTrue(active)
925

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

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

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

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

    
936
        s = False
937

    
938
        while True:
939

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

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

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

    
950
        self.assertTrue(s)
951

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

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

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

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

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

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

    
983
        self.assertTrue(active)
984

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

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

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

    
994
        s = False
995

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

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

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

    
1007
        self.assertTrue(s)
1008

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

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

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

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

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

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

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

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

    
1035
        res = False
1036

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

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

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

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

    
1059
        self.assertTrue(res)
1060

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

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

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

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

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

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

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

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

    
1087
        res = False
1088

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

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

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

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

    
1111
        self.assertTrue(res)
1112

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

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

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

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

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

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

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

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

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

    
1149
        exists = False
1150

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

    
1154
        self.assertTrue(exists)
1155

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

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

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

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

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

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

    
1182
        self.assertFalse(conn_exists)
1183

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

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

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

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

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

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

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

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

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

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

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

    
1224
        self.assertTrue(deleted)
1225

    
1226

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

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

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

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

    
1258

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

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

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

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

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

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

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

    
1296

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

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

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

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

    
1314

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

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

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

    
1326

    
1327
def cleanup_servers(delete_stale=False):
1328

    
1329
    c = ComputeClient(API, TOKEN)
1330

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

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

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

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

    
1349

    
1350
def cleanup_networks(delete_stale=False):
1351

    
1352
    c = CycladesClient(API, TOKEN)
1353

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

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

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

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

    
1372

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

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

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

    
1383

    
1384
def parse_arguments(args):
1385

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

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

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

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

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

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

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

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

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

    
1525
    return (opts, args)
1526

    
1527

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

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

1537
    """
1538

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

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

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

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

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

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

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

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

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

    
1576
    for image in test_images:
1577

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

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

    
1585
        imagename = image["name"]
1586

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

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

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

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

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

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

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

    
1646
        for case in seq_cases:
1647

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

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

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

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

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

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

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

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