Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / tools / burnin.py @ f97dce4d

History | View | Annotate | Download (41.7 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 struct
49
import sys
50
import time
51
import hashlib
52
from base64 import b64encode
53
from pwd import getpwuid
54
from grp import getgrgid
55
from IPy import IP
56
from multiprocessing import Process, Queue
57
from random import choice
58

    
59
from kamaki.clients import ClientError, ComputeClient, CycladesClient
60
from kamaki.config import Config
61

    
62
from vncauthproxy.d3des import generate_response as d3des_generate_response
63

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

    
72

    
73
API = None
74
TOKEN = None
75
DEFAULT_API = "http://127.0.0.1:8000/api/v1.1"
76

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

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

    
87
class UnauthorizedTestCase(unittest.TestCase):
88
    def test_unauthorized_access(self):
89
        """Test access without a valid token fails"""
90
        falseToken = '12345'
91
        conf = Config()
92
        conf.set('compute_token', falseToken)
93
        c=ComputeClient(conf)
94

    
95
        with self.assertRaises(ClientError) as cm:
96
            c.list_servers()
97
        self.assertEqual(cm.exception.status, 401)
98

    
99

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

    
107
        conf = Config()
108
        conf.set('compute_token', TOKEN)
109
        cls.client = ComputeClient(conf)
110
        cls.images = cls.client.list_images()
111
        cls.dimages = cls.client.list_images(detail=True)
112

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

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

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

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

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

    
138

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

    
146
        conf = Config()
147
        conf.set('compute_token', TOKEN)
148
        cls.client = ComputeClient(conf)
149
        cls.flavors = cls.client.list_flavors()
150
        cls.dflavors = cls.client.list_flavors(detail=True)
151

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

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

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

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

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

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

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

    
182

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

    
190
        conf = Config()
191
        conf.set('compute_token', TOKEN)
192
        cls.client = ComputeClient(conf)
193
        cls.servers = cls.client.list_servers()
194
        cls.dservers = cls.client.list_servers(detail=True)
195

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

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

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

    
210

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

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

    
220
        conf = Config()
221
        conf.set('compute_token', TOKEN)
222
        cls.client = ComputeClient(conf)
223
        cls.cyclades = CycladesClient(conf)
224

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

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

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

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

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

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

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

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

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

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

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

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

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

    
357

    
358
    def _file_md5(filename, block_size = 2**20):
359
        f = open(filename)
360
        md5 = hashlib.md5()
361
        while True:
362
            data = f.read(block_size)
363
            if not data:
364
                break
365
            md5.update(data)
366
            f.close()
367
    
368
        return md5.digest()
369

    
370
    
371
    def _check_file_through_ssh(self, hostip, username, password, remotepath, content):
372
        msg = "Trying file injection through SSH to %s, as %s/%s" % (hostip, username, password)
373
        log.info(msg)
374
        try:
375
            ssh = paramiko.SSHClient()
376
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
377
            ssh.connect(hostip, username=username, password=password)
378
        except socket.error:
379
            raise AssertionError
380
        
381
        transport = paramiko.Transport((hostip,22))
382
        transport.connect(username = username, password = password)
383

    
384
        localpath = '/tmp/'+SNF_TEST_PREFIX+'injection'
385
        sftp = paramiko.SFTPClient.from_transport(transport)
386
        sftp.get(remotepath, localpath)
387
        
388
        sftp.close()
389
        transport.close()
390

    
391
        f = open(localpath)
392
        remote_content = b64encode(f.read())
393

    
394
        # Check if files are the same
395
        return remote_content == content
396

    
397
    def _skipIf(self, condition, msg):
398
        if condition:
399
            self.skipTest(msg)
400

    
401
    def test_001_submit_create_server(self):
402
        """Test submit create server request"""
403
        server = self.client.create_server(self.servername, self.flavorid,
404
                                           self.imageid, self.personality)
405

    
406
        self.assertEqual(server["name"], self.servername)
407
        self.assertEqual(server["flavorRef"], self.flavorid)
408
        self.assertEqual(server["imageRef"], self.imageid)
409
        self.assertEqual(server["status"], "BUILD")
410

    
411
        # Update class attributes to reflect data on building server
412
        cls = type(self)
413
        cls.serverid = server["id"]
414
        cls.username = None
415
        cls.passwd = server["adminPass"]
416

    
417
    def test_002a_server_is_building_in_list(self):
418
        """Test server is in BUILD state, in server list"""
419
        servers = self.client.list_servers(detail=True)
420
        servers = filter(lambda x: x["name"] == self.servername, servers)
421
        self.assertEqual(len(servers), 1)
422
        server = servers[0]
423
        self.assertEqual(server["name"], self.servername)
424
        self.assertEqual(server["flavorRef"], self.flavorid)
425
        self.assertEqual(server["imageRef"], self.imageid)
426
        self.assertEqual(server["status"], "BUILD")
427

    
428
    def test_002b_server_is_building_in_details(self):
429
        """Test server is in BUILD state, in details"""
430
        server = self.client.get_server_details(self.serverid)
431
        self.assertEqual(server["name"], self.servername)
432
        self.assertEqual(server["flavorRef"], self.flavorid)
433
        self.assertEqual(server["imageRef"], self.imageid)
434
        self.assertEqual(server["status"], "BUILD")
435

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

    
442
        # Determine the username to use for future connections
443
        # to this host
444
        cls = type(self)
445
        cls.username = loginname
446
        if not cls.username:
447
            cls.username = self._connect_loginname(os)
448
        self.assertIsNotNone(cls.username)
449

    
450
    def test_002d_verify_server_metadata(self):
451
        """Test server metadata keys are set based on image metadata"""
452
        servermeta = self.client.get_server_metadata(self.serverid)
453
        imagemeta = self.client.get_image_metadata(self.imageid)
454
        self.assertEqual(servermeta["OS"], imagemeta["os"])
455

    
456
    def test_003_server_becomes_active(self):
457
        """Test server becomes ACTIVE"""
458
        self._insist_on_status_transition("BUILD", "ACTIVE",
459
                                         self.build_fail, self.build_warning)
460

    
461
    # def test_003a_get_server_oob_console(self):
462
    #     """Test getting OOB server console over VNC
463

    
464
    #     Implementation of RFB protocol follows
465
    #     http://www.realvnc.com/docs/rfbproto.pdf.
466

    
467
    #     """
468
        
469
    #     console = self.cyclades.get_server_console(self.serverid)
470
    #     self.assertEquals(console['type'], "vnc")
471
    #     sock = self._insist_on_tcp_connection(socket.AF_UNSPEC,
472
    #                                     console["host"], console["port"])
473

    
474
    #     # Step 1. ProtocolVersion message (par. 6.1.1)
475
    #     version = sock.recv(1024)
476
    #     self.assertEquals(version, 'RFB 003.008\n')
477
    #     sock.send(version)
478

    
479
    #     # Step 2. Security (par 6.1.2): Only VNC Authentication supported
480
    #     sec = sock.recv(1024)
481
    #     self.assertEquals(list(sec), ['\x01', '\x02'])
482

    
483
    #     # Step 3. Request VNC Authentication (par 6.1.2)
484
    #     sock.send('\x02')
485

    
486
    #     # Step 4. Receive Challenge (par 6.2.2)
487
    #     challenge = sock.recv(1024)
488
    #     self.assertEquals(len(challenge), 16)
489

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

    
495
    #     # Step 6. SecurityResult (par 6.1.3)
496
    #     result = sock.recv(4)
497
    #     self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
498
    #     sock.close()
499
        
500
    def test_004_server_has_ipv4(self):
501
        """Test active server has a valid IPv4 address"""
502
        server = self.client.get_server_details(self.serverid)
503
        ipv4 = self._get_ipv4(server)
504
        self.assertEquals(IP(ipv4).version(), 4)
505

    
506
    # def test_005_server_has_ipv6(self):
507
    #     """Test active server has a valid IPv6 address"""
508
    #     server = self.client.get_server_details(self.serverid)
509
    #     ipv6 = self._get_ipv6(server)
510
    #     self.assertEquals(IP(ipv6).version(), 6)
511

    
512
    def test_006_server_responds_to_ping_IPv4(self):
513
        """Test server responds to ping on IPv4 address"""
514
        server = self.client.get_server_details(self.serverid)
515
        ip = self._get_ipv4(server)
516
        self._try_until_timeout_expires(self.action_timeout,
517
                                        self.action_timeout,
518
                                        "PING IPv4 to %s" % ip,
519
                                        self._ping_once,
520
                                        False, ip)
521

    
522
    # def test_007_server_responds_to_ping_IPv6(self):
523
    #     """Test server responds to ping on IPv6 address"""
524
    #     server = self.client.get_server_details(self.serverid)
525
    #     ip = self._get_ipv6(server)
526
    #     self._try_until_timeout_expires(self.action_timeout,
527
    #                                     self.action_timeout,
528
    #                                     "PING IPv6 to %s" % ip,
529
    #                                     self._ping_once,
530
    #                                     True, ip)
531

    
532
    def test_008_submit_shutdown_request(self):
533
        """Test submit request to shutdown server"""
534
        self.cyclades.shutdown_server(self.serverid)
535

    
536
    def test_009_server_becomes_stopped(self):
537
        """Test server becomes STOPPED"""
538
        self._insist_on_status_transition("ACTIVE", "STOPPED",
539
                                         self.action_timeout,
540
                                         self.action_timeout)
541

    
542
    def test_010_submit_start_request(self):
543
        """Test submit start server request"""
544
        self.cyclades.start_server(self.serverid)
545

    
546
    def test_011_server_becomes_active(self):
547
        """Test server becomes ACTIVE again"""
548
        self._insist_on_status_transition("STOPPED", "ACTIVE",
549
                                         self.action_timeout,
550
                                         self.action_timeout)
551

    
552
    def test_011a_server_responds_to_ping_IPv4(self):
553
        """Test server OS is actually up and running again"""
554
        self.test_006_server_responds_to_ping_IPv4()
555

    
556
    def test_012_ssh_to_server_IPv4(self):
557
        """Test SSH to server public IPv4 works, verify hostname"""
558
        self._skipIf(self.is_windows, "only valid for Linux servers")
559
        server = self.client.get_server_details(self.serverid)
560
        self._insist_on_ssh_hostname(self._get_ipv4(server),
561
                                     self.username, self.passwd)
562

    
563
    # def test_013_ssh_to_server_IPv6(self):
564
    #     """Test SSH to server public IPv6 works, verify hostname"""
565
    #     self._skipIf(self.is_windows, "only valid for Linux servers")
566
    #     server = self.client.get_server_details(self.serverid)
567
    #     self._insist_on_ssh_hostname(self._get_ipv6(server),
568
    #                                  self.username, self.passwd)
569

    
570
    def test_014_rdp_to_server_IPv4(self):
571
        "Test RDP connection to server public IPv4 works"""
572
        self._skipIf(not self.is_windows, "only valid for Windows servers")
573
        server = self.client.get_server_details(self.serverid)
574
        ipv4 = self._get_ipv4(server)
575
        sock = _insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
576

    
577
        # No actual RDP processing done. We assume the RDP server is there
578
        # if the connection to the RDP port is successful.
579
        # FIXME: Use rdesktop, analyze exit code? see manpage [costasd]
580
        sock.close()
581

    
582
    # def test_015_rdp_to_server_IPv6(self):
583
    #     "Test RDP connection to server public IPv6 works"""
584
    #     self._skipIf(not self.is_windows, "only valid for Windows servers")
585
    #     server = self.client.get_server_details(self.serverid)
586
    #     ipv6 = self._get_ipv6(server)
587
    #     sock = _get_tcp_connection(socket.AF_INET6, ipv6, 3389)
588

    
589
    #     # No actual RDP processing done. We assume the RDP server is there
590
    #     # if the connection to the RDP port is successful.
591
    #     sock.close()
592

    
593
    def test_016_personality_is_enforced(self):
594
        """Test file injection for personality enforcement"""
595
        self._skipIf(self.is_windows, "only implemented for Linux servers")
596
        self._skipIf(self.personality == None, "No personality file selected")
597

    
598
        server = self.client.get_server_details(self.serverid)
599

    
600
        for inj_file in self.personality:
601
            equal_files = self._check_file_through_ssh(self._get_ipv4(server), inj_file['owner'], 
602
                                                       self.passwd, inj_file['path'], inj_file['contents'])
603
            self.assertTrue(equal_files)
604
        
605

    
606
    def test_017_submit_delete_request(self):
607
        """Test submit request to delete server"""
608
        self.client.delete_server(self.serverid)
609

    
610
    def test_018_server_becomes_deleted(self):
611
        """Test server becomes DELETED"""
612
        self._insist_on_status_transition("ACTIVE", "DELETED",
613
                                         self.action_timeout,
614
                                         self.action_timeout)
615

    
616
    def test_019_server_no_longer_in_server_list(self):
617
        """Test server is no longer in server list"""
618
        servers = self.client.list_servers()
619
        self.assertNotIn(self.serverid, [s["id"] for s in servers])
620

    
621

    
622
class NetworkTestCase(unittest.TestCase):
623
    """ Testing networking in cyclades """
624
    @classmethod
625
    def setUpClass(cls):
626
        "Initialize kamaki, get list of current networks"
627
        conf = Config()
628
        conf.set('compute_token', TOKEN)
629
        cls.client = CycladesClient(conf)
630
        cls.compute = ComputeClient(conf)
631

    
632
        images = cls.compute.list_images(detail = True)
633
        flavors = cls.compute.list_flavors(detail = True)
634
        imageid = choice([im['id'] for im in images])
635
        flavorid = choice([f["id"] for f in flavors if f["disk"] >= 20])
636

    
637
        for image in images:
638
            if image['id'] == imageid:
639
                imagename = image['name']
640

    
641
        servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, imagename)
642
        is_windows = imagename.lower().find("windows") >= 0
643
        setupCase =  _spawn_server_test_case(imageid=str(imageid), flavorid=flavorid,
644
                                             imagename=imagename,
645
                                             personality=None,
646
                                             servername=servername,
647
                                             is_windows=is_windows,
648
                                             action_timeout=200,
649
                                             build_warning=1200,
650
                                             build_fail=500,
651
                                             query_interval=3)
652

    
653
        #Using already implemented tests for serverlist population
654
        suite = unittest.TestSuite()
655
        suite.addTest(setupCase('test_001_submit_create_server'))
656
        suite.addTest(setupCase('test_002a_server_is_building_in_list'))
657
        suite.addTest(setupCase('test_002b_server_is_building_in_details'))        
658
        suite.addTest(setupCase('test_003_server_becomes_active'))
659
        unittest.TextTestRunner(verbosity=2).run(suite)
660

    
661
    def test_001_create_network(self):
662
        """Test submit create network request"""
663
        name = SNF_TEST_PREFIX+TEST_RUN_ID
664
        previous_num = len(self.client.list_networks())
665
        network =  self.client.create_network(name)        
666
       
667
        #Test if right name is assigned
668
        self.assertEqual(network['name'], name)
669
        
670
        # Update class attributes
671
        cls = type(self)
672
        cls.networkid = network['id']
673
        networks = self.client.list_networks()
674

    
675
        #Test if new network is created
676
        self.assertTrue(len(networks) > previous_num)
677
        
678
    
679
    def test_002_connect_to_network(self):
680
        """Test connect VM to network"""
681
        servers = self.compute.list_servers()
682
        server = choice(servers)
683
        self.client.connect_server(server['id'], self.networkid)
684
        
685
        #Update class attributes
686
        cls = type(self)
687
        cls.serverid = server['id']
688

    
689
        #FIXME: Insist on new connection instead of this
690
        time.sleep(15)
691

    
692
        connected = (self.client.get_network_details(self.networkid))
693
        connections = len(connected['servers']['values'])
694

    
695
        self.assertTrue(connections>=1)
696
        
697

    
698
    def test_003_disconnect_from_network(self):
699
        prev_state = self.client.get_network_details(self.networkid)
700
        prev_conn = len(prev_state['servers']['values'])
701

    
702
        self.client.disconnect_server(self.serverid, self.networkid)
703

    
704
        #FIXME: Insist on deleting instead of this
705
        time.sleep(15)
706

    
707
        connected = (self.client.get_network_details(self.networkid))
708
        curr_conn = len(connected['servers']['values'])
709

    
710
        self.assertTrue(curr_conn < prev_conn)
711

    
712
    def test_004_destroy_network(self):
713
        """Test submit delete network request"""
714
        self.client.delete_network(self.networkid)
715
        
716
        networks = self.client.list_networks()
717
        self.assertEqual(len(networks),1)
718

    
719

    
720
class TestRunnerProcess(Process):
721
    """A distinct process used to execute part of the tests in parallel"""
722
    def __init__(self, **kw):
723
        Process.__init__(self, **kw)
724
        kwargs = kw["kwargs"]
725
        self.testq = kwargs["testq"]
726
        self.runner = kwargs["runner"]
727

    
728
    def run(self):
729
        # Make sure this test runner process dies with the parent
730
        # and is not left behind.
731
        #
732
        # WARNING: This uses the prctl(2) call and is
733
        # Linux-specific.
734
        prctl.set_pdeathsig(signal.SIGHUP)
735

    
736
        while True:
737
            log.debug("I am process %d, GETting from queue is %s",
738
                     os.getpid(), self.testq)
739
            msg = self.testq.get()
740
            log.debug("Dequeued msg: %s", msg)
741

    
742
            if msg == "TEST_RUNNER_TERMINATE":
743
                raise SystemExit
744
            elif issubclass(msg, unittest.TestCase):
745
                # Assemble a TestSuite, and run it
746
                suite = unittest.TestLoader().loadTestsFromTestCase(msg)
747
                self.runner.run(suite)
748
            else:
749
                raise Exception("Cannot handle msg: %s" % msg)
750

    
751

    
752

    
753
def _run_cases_in_parallel(cases, fanout=1, runner=None):
754
    """Run instances of TestCase in parallel, in a number of distinct processes
755

756
    The cases iterable specifies the TestCases to be executed in parallel,
757
    by test runners running in distinct processes.
758
    The fanout parameter specifies the number of processes to spawn,
759
    and defaults to 1.
760
    The runner argument specifies the test runner class to use inside each
761
    runner process.
762

763
    """
764
    if runner is None:
765
        runner = unittest.TextTestRunner(verbosity=2, failfast=True)
766

    
767
    # testq: The master process enqueues TestCase objects into this queue,
768
    #        test runner processes pick them up for execution, in parallel.
769
    testq = Queue()
770
    runners = []
771
    for i in xrange(0, fanout):
772
        kwargs = dict(testq=testq, runner=runner)
773
        runners.append(TestRunnerProcess(kwargs=kwargs))
774

    
775
    log.info("Spawning %d test runner processes", len(runners))
776
    for p in runners:
777
        p.start()
778
    log.debug("Spawned %d test runners, PIDs are %s",
779
              len(runners), [p.pid for p in runners])
780

    
781
    # Enqueue test cases
782
    map(testq.put, cases)
783
    map(testq.put, ["TEST_RUNNER_TERMINATE"] * len(runners))
784

    
785
    log.debug("Joining %d processes", len(runners))
786
    for p in runners:
787
        p.join()
788
    log.debug("Done joining %d processes", len(runners))
789

    
790

    
791
def _spawn_server_test_case(**kwargs):
792
    """Construct a new unit test case class from SpawnServerTestCase"""
793

    
794
    name = "SpawnServerTestCase_%s" % kwargs["imageid"]
795
    cls = type(name, (SpawnServerTestCase,), kwargs)
796

    
797
    # Patch extra parameters into test names by manipulating method docstrings
798
    for (mname, m) in \
799
        inspect.getmembers(cls, lambda x: inspect.ismethod(x)):
800
            if hasattr(m, __doc__):
801
                m.__func__.__doc__ = "[%s] %s" % (imagename, m.__doc__)
802

    
803
    # Make sure the class can be pickled, by listing it among
804
    # the attributes of __main__. A PicklingError is raised otherwise.
805
    setattr(__main__, name, cls)
806
    return cls
807

    
808

    
809
def cleanup_servers(delete_stale=False):
810

    
811
    conf = Config()
812
    conf.set('compute_token', TOKEN)
813
    c = ComputeClient(conf)
814

    
815
    servers = c.list_servers()
816
    stale = [s for s in servers if s["name"].startswith(SNF_TEST_PREFIX)]
817

    
818
    if len(stale) == 0:
819
        return
820

    
821
    print >> sys.stderr, "Found these stale servers from previous runs:"
822
    print "    " + \
823
          "\n    ".join(["%d: %s" % (s["id"], s["name"]) for s in stale])
824

    
825
    if delete_stale:
826
        print >> sys.stderr, "Deleting %d stale servers:" % len(stale)
827
        for server in stale:
828
            c.delete_server(server["id"])
829
        print >> sys.stderr, "    ...done"
830
    else:
831
        print >> sys.stderr, "Use --delete-stale to delete them."
832

    
833

    
834
def parse_arguments(args):
835
    from optparse import OptionParser
836

    
837
    kw = {}
838
    kw["usage"] = "%prog [options]"
839
    kw["description"] = \
840
        "%prog runs a number of test scenarios on a " \
841
        "Synnefo deployment."
842

    
843
    parser = OptionParser(**kw)
844
    parser.disable_interspersed_args()
845
    parser.add_option("--api",
846
                      action="store", type="string", dest="api",
847
                      help="The API URI to use to reach the Synnefo API",
848
                      default=DEFAULT_API)
849
    parser.add_option("--token",
850
                      action="store", type="string", dest="token",
851
                      help="The token to use for authentication to the API")
852
    parser.add_option("--nofailfast",
853
                      action="store_true", dest="nofailfast",
854
                      help="Do not fail immediately if one of the tests " \
855
                           "fails (EXPERIMENTAL)",
856
                      default=False)
857
    parser.add_option("--action-timeout",
858
                      action="store", type="int", dest="action_timeout",
859
                      metavar="TIMEOUT",
860
                      help="Wait SECONDS seconds for a server action to " \
861
                           "complete, then the test is considered failed",
862
                      default=100)
863
    parser.add_option("--build-warning",
864
                      action="store", type="int", dest="build_warning",
865
                      metavar="TIMEOUT",
866
                      help="Warn if TIMEOUT seconds have passed and a " \
867
                           "build operation is still pending",
868
                      default=600)
869
    parser.add_option("--build-fail",
870
                      action="store", type="int", dest="build_fail",
871
                      metavar="BUILD_TIMEOUT",
872
                      help="Fail the test if TIMEOUT seconds have passed " \
873
                           "and a build operation is still incomplete",
874
                      default=900)
875
    parser.add_option("--query-interval",
876
                      action="store", type="int", dest="query_interval",
877
                      metavar="INTERVAL",
878
                      help="Query server status when requests are pending " \
879
                           "every INTERVAL seconds",
880
                      default=3)
881
    parser.add_option("--fanout",
882
                      action="store", type="int", dest="fanout",
883
                      metavar="COUNT",
884
                      help="Spawn up to COUNT child processes to execute " \
885
                           "in parallel, essentially have up to COUNT " \
886
                           "server build requests outstanding (EXPERIMENTAL)",
887
                      default=1)
888
    parser.add_option("--force-flavor",
889
                      action="store", type="int", dest="force_flavorid",
890
                      metavar="FLAVOR ID",
891
                      help="Force all server creations to use the specified "\
892
                           "FLAVOR ID instead of a randomly chosen one, " \
893
                           "useful if disk space is scarce",
894
                      default=None)
895
    parser.add_option("--image-id",
896
                      action="store", type="string", dest="force_imageid",
897
                      metavar="IMAGE ID",
898
                      help="Test the specified image id, use 'all' to test " \
899
                           "all available images (mandatory argument)",
900
                      default=None)
901
    parser.add_option("--show-stale",
902
                      action="store_true", dest="show_stale",
903
                      help="Show stale servers from previous runs, whose "\
904
                           "name starts with `%s'" % SNF_TEST_PREFIX,
905
                      default=False)
906
    parser.add_option("--delete-stale",
907
                      action="store_true", dest="delete_stale",
908
                      help="Delete stale servers from previous runs, whose "\
909
                           "name starts with `%s'" % SNF_TEST_PREFIX,
910
                      default=False)
911
    parser.add_option("--force-personality",
912
                      action="store", dest="personality_path",
913
                      help="Force a personality file injection. File path required. ",
914
                      default=None)
915
    
916

    
917
    # FIXME: Change the default for build-fanout to 10
918
    # FIXME: Allow the user to specify a specific set of Images to test
919

    
920
    (opts, args) = parser.parse_args(args)
921

    
922
    # Verify arguments
923
    if opts.delete_stale:
924
        opts.show_stale = True
925

    
926
    if not opts.show_stale:
927
        if not opts.force_imageid:
928
            print >>sys.stderr, "The --image-id argument is mandatory."
929
            parser.print_help()
930
            sys.exit(1)
931

    
932
        if opts.force_imageid != 'all':
933
            try:
934
                opts.force_imageid = str(opts.force_imageid)
935
            except ValueError:
936
                print >>sys.stderr, "Invalid value specified for --image-id." \
937
                                    "Use a valid id, or `all'."
938
                sys.exit(1)
939

    
940
    return (opts, args)
941

    
942

    
943
def main():
944
    """Assemble test cases into a test suite, and run it
945

946
    IMPORTANT: Tests have dependencies and have to be run in the specified
947
    order inside a single test case. They communicate through attributes of the
948
    corresponding TestCase class (shared fixtures). Distinct subclasses of
949
    TestCase MAY SHARE NO DATA, since they are run in parallel, in distinct
950
    test runner processes.
951

952
    """
953
    (opts, args) = parse_arguments(sys.argv[1:])
954

    
955
    global API, TOKEN
956
    API = opts.api
957
    TOKEN = opts.token
958

    
959
    # Cleanup stale servers from previous runs
960
    if opts.show_stale:
961
        cleanup_servers(delete_stale=opts.delete_stale)
962
        return 0
963

    
964
    # Initialize a kamaki instance, get flavors, images
965

    
966
    conf = Config()
967
    conf.set('compute_token', TOKEN)
968
    c = ComputeClient(conf)
969

    
970
    DIMAGES = c.list_images(detail=True)
971
    DFLAVORS = c.list_flavors(detail=True)
972

    
973
    # FIXME: logging, log, LOG PID, TEST_RUN_ID, arguments
974
    # Run them: FIXME: In parallel, FAILEARLY, catchbreak?
975
    #unittest.main(verbosity=2, catchbreak=True)
976

    
977
    if opts.force_imageid == 'all':
978
        test_images = DIMAGES
979
    else:
980
        test_images = filter(lambda x: x["id"] == opts.force_imageid, DIMAGES)
981

    
982
    for image in test_images:
983
        imageid = str(image["id"])
984
        flavorid = choice([f["id"] for f in DFLAVORS if f["disk"] >= 20])
985
        imagename = image["name"]
986
        
987
        
988
        if opts.personality_path != None:
989
            f = open(opts.personality_path)
990
            content = b64encode(f.read())
991
            personality = []
992
            st = os.stat(opts.personality_path)
993
            personality.append({
994
                    'path': '/root/test_inj_file',
995
                    'owner': 'root',
996
                    'group': 'root',
997
                    'mode': 0x7777 & st.st_mode,
998
                    'contents': content
999
                    })
1000
        else:
1001
            personality = None
1002

    
1003
        servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, imagename)
1004
        is_windows = imagename.lower().find("windows") >= 0
1005
        
1006
        ServerTestCase = _spawn_server_test_case(imageid=imageid, flavorid=flavorid,
1007
                                                 imagename=imagename,
1008
                                                 personality=personality,
1009
                                                 servername=servername,
1010
                                                 is_windows=is_windows,
1011
                                                 action_timeout=opts.action_timeout,
1012
                                                 build_warning=opts.build_warning,
1013
                                                 build_fail=opts.build_fail,
1014
                                                 query_interval=opts.query_interval,
1015
                                                 )
1016

    
1017

    
1018
    #Running all the testcases sequentially
1019
    #seq_cases = [UnauthorizedTestCase, FlavorsTestCase, ImagesTestCase, ServerTestCase, NetworkTestCase]
1020

    
1021
    seq_cases = [NetworkTestCase]
1022
    for case in seq_cases:
1023
        suite = unittest.TestLoader().loadTestsFromTestCase(case)
1024
        unittest.TextTestRunner(verbosity=2).run(suite)
1025
        
1026
    
1027

    
1028
    # # The Following cases run sequentially
1029
    # seq_cases = [UnauthorizedTestCase, FlavorsTestCase, ImagesTestCase]
1030
    # _run_cases_in_parallel(seq_cases, fanout=3, runner=runner)
1031

    
1032
    # # The following cases run in parallel
1033
    # par_cases = []
1034

    
1035
    # if opts.force_imageid == 'all':
1036
    #     test_images = DIMAGES
1037
    # else:
1038
    #     test_images = filter(lambda x: x["id"] == opts.force_imageid, DIMAGES)
1039

    
1040
    # for image in test_images:
1041
    #     imageid = image["id"]
1042
    #     imagename = image["name"]
1043
    #     if opts.force_flavorid:
1044
    #         flavorid = opts.force_flavorid
1045
    #     else:
1046
    #         flavorid = choice([f["id"] for f in DFLAVORS if f["disk"] >= 20])
1047
    #     personality = None   # FIXME
1048
    #     servername = "%s%s for %s" % (SNF_TEST_PREFIX, TEST_RUN_ID, imagename)
1049
    #     is_windows = imagename.lower().find("windows") >= 0
1050
    #     case = _spawn_server_test_case(imageid=str(imageid), flavorid=flavorid,
1051
    #                                    imagename=imagename,
1052
    #                                    personality=personality,
1053
    #                                    servername=servername,
1054
    #                                    is_windows=is_windows,
1055
    #                                    action_timeout=opts.action_timeout,
1056
    #                                    build_warning=opts.build_warning,
1057
    #                                    build_fail=opts.build_fail,
1058
    #                                    query_interval=opts.query_interval)
1059
    #     par_cases.append(case)
1060

    
1061
    # _run_cases_in_parallel(par_cases, fanout=opts.fanout, runner=runner)
1062

    
1063
if __name__ == "__main__":
1064
    sys.exit(main())