burnin.py 70.1 KB
Newer Older
1
#!/usr/bin/env python
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

"""Perform integration testing on a running Synnefo deployment"""

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
38
#import __main__
39
40
41
42
43
import datetime
import inspect
import logging
import os
import paramiko
44
import prctl
45
import subprocess
46
import signal
47
48
49
import socket
import sys
import time
50
from base64 import b64encode
51
from IPy import IP
52
from multiprocessing import Process, Queue
53
from random import choice
John Giannelos's avatar
John Giannelos committed
54
from optparse import OptionParser, OptionValueError
55

John Giannelos's avatar
John Giannelos committed
56
57
from kamaki.clients.compute import ComputeClient
from kamaki.clients.cyclades import CycladesClient
58
from kamaki.clients.image import ImageClient
John Giannelos's avatar
John Giannelos committed
59
from kamaki.clients import ClientError
John Giannelos's avatar
John Giannelos committed
60

61
from vncauthproxy.d3des import generate_response as d3des_generate_response
62
63
64
65
66

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# --------------------------------------------------------------------
# Global Variables
API = None
TOKEN = None
PLANKTON = None
PLANKTON_USER = None
NO_IPV6 = None
DEFAULT_API = "https://cyclades.okeanos.grnet.gr/api/v1.1"
DEFAULT_PLANKTON = "https://cyclades.okeanos.grnet.gr/plankton"
DEFAULT_PLANKTON_USER = "images@okeanos.grnet.gr"
NOFAILFAST = None
VERBOSE = None

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

red = '\x1b[31m'
yellow = '\x1b[33m'
green = '\x1b[32m'
normal = '\x1b[0m'

94

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# --------------------------------------------------------------------
# Global functions
def _ssh_execute(hostip, username, password, command):
    """Execute a command via ssh"""
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    try:
        ssh.connect(hostip, username=username, password=password)
    except socket.error:
        raise AssertionError
    try:
        stdin, stdout, stderr = ssh.exec_command(command)
    except paramiko.SSHException:
        raise AssertionError
    status = stdout.channel.recv_exit_status()
    output = stdout.readlines()
    ssh.close()
    return output, status


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
115
116
# --------------------------------------------------------------------
# BurninTestReulst class
117
118
119
120
class BurninTestResult(unittest.TextTestResult):
    def addSuccess(self, test):
        super(BurninTestResult, self).addSuccess(test)
        if self.showAll:
John Giannelos's avatar
John Giannelos committed
121
            if hasattr(test, 'result_dict'):
122
123
124
125
126
127
128
129
130
                run_details = test.result_dict

                self.stream.write("\n")
                for i in run_details:
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
                self.stream.write("\n")

        elif self.dots:
            self.stream.write('.')
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
131
132
            self.stream.flush()

133
134
135
136
    def addError(self, test, err):
        super(BurninTestResult, self).addError(test, err)
        if self.showAll:
            self.stream.writeln("ERROR")
John Giannelos's avatar
John Giannelos committed
137
138
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
139

John Giannelos's avatar
John Giannelos committed
140
141
142
143
                self.stream.write("\n")
                for i in run_details:
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
                self.stream.write("\n")
144
145
146
147
148
149
150
151
152

        elif self.dots:
            self.stream.write('E')
            self.stream.flush()

    def addFailure(self, test, err):
        super(BurninTestResult, self).addFailure(test, err)
        if self.showAll:
            self.stream.writeln("FAIL")
John Giannelos's avatar
John Giannelos committed
153
154
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
155

John Giannelos's avatar
John Giannelos committed
156
157
158
159
                self.stream.write("\n")
                for i in run_details:
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
                self.stream.write("\n")
160
161
162
163
164
165

        elif self.dots:
            self.stream.write('F')
            self.stream.flush()


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
166
167
# --------------------------------------------------------------------
# Format Results
John Giannelos's avatar
John Giannelos committed
168
169
class burninFormatter(logging.Formatter):
    err_fmt = red + "ERROR: %(msg)s" + normal
John Giannelos's avatar
John Giannelos committed
170
    dbg_fmt = green + "* %(msg)s" + normal
John Giannelos's avatar
John Giannelos committed
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
    info_fmt = "%(msg)s"

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

    def format(self, record):
        format_orig = self._fmt
        # Replace the original format with one customized by logging level
        if record.levelno == 10:    # DEBUG
            self._fmt = burninFormatter.dbg_fmt
        elif record.levelno == 20:  # INFO
            self._fmt = burninFormatter.info_fmt
        elif record.levelno == 40:  # ERROR
            self._fmt = burninFormatter.err_fmt
        result = logging.Formatter.format(self, record)
        self._fmt = format_orig
        return result

189
log = logging.getLogger("burnin")
John Giannelos's avatar
John Giannelos committed
190
191
192
193
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(burninFormatter())
log.addHandler(handler)
194

195

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
196
197
# --------------------------------------------------------------------
# UnauthorizedTestCase class
198
class UnauthorizedTestCase(unittest.TestCase):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
199
    """Test unauthorized access"""
200
201
202
203
    @classmethod
    def setUpClass(cls):
        cls.result_dict = dict()

204
205
    def test_unauthorized_access(self):
        """Test access without a valid token fails"""
206
        log.info("Authentication test")
207
        falseToken = '12345'
208
        c = ComputeClient(API, falseToken)
209

210
211
        with self.assertRaises(ClientError) as cm:
            c.list_servers()
John Giannelos's avatar
John Giannelos committed
212
            self.assertEqual(cm.exception.status, 401)
213
214


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
215
216
# --------------------------------------------------------------------
# ImagesTestCase class
217
218
219
220
221
222
class ImagesTestCase(unittest.TestCase):
    """Test image lists for consistency"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get (detailed) list of images"""
        log.info("Getting simple and detailed list of images")
John Giannelos's avatar
John Giannelos committed
223
        cls.client = ComputeClient(API, TOKEN)
224
225
226
        cls.plankton = ImageClient(PLANKTON, TOKEN)
        cls.images = cls.plankton.list_public()
        cls.dimages = cls.plankton.list_public(detail=True)
227
        cls.result_dict = dict()
228
229
230

    def test_001_list_images(self):
        """Test image list actually returns images"""
John Giannelos's avatar
John Giannelos committed
231
        self.assertGreater(len(self.images), 0)
232

233
234
    def test_002_list_images_detailed(self):
        """Test detailed image list is the same length as list"""
John Giannelos's avatar
John Giannelos committed
235
        self.assertEqual(len(self.dimages), len(self.images))
236

237
238
239
240
241
242
243
    def test_003_same_image_names(self):
        """Test detailed and simple image list contain same names"""
        names = sorted(map(lambda x: x["name"], self.images))
        dnames = sorted(map(lambda x: x["name"], self.dimages))
        self.assertEqual(names, dnames)

    def test_004_unique_image_names(self):
244
245
246
247
        """Test system images have unique names"""
        sys_images = filter(lambda x: x['owner'] == PLANKTON_USER,
                            self.dimages)
        names = sorted(map(lambda x: x["name"], sys_images))
248
249
250
251
        self.assertEqual(sorted(list(set(names))), names)

    def test_005_image_metadata(self):
        """Test every image has specific metadata defined"""
252
        keys = frozenset(["osfamily", "root_partition"])
John Giannelos's avatar
John Giannelos committed
253
254
        details = self.client.list_images(detail=True)
        for i in details:
255
256
257
            self.assertTrue(keys.issubset(i["metadata"]["values"].keys()))


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
258
259
# --------------------------------------------------------------------
# FlavorsTestCase class
260
261
262
263
264
265
class FlavorsTestCase(unittest.TestCase):
    """Test flavor lists for consistency"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get (detailed) list of flavors"""
        log.info("Getting simple and detailed list of flavors")
John Giannelos's avatar
John Giannelos committed
266
        cls.client = ComputeClient(API, TOKEN)
267
268
        cls.flavors = cls.client.list_flavors()
        cls.dflavors = cls.client.list_flavors(detail=True)
269
        cls.result_dict = dict()
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299

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

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

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

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

    def test_005_well_formed_flavor_names(self):
        """Test flavors have names of the form CxxRyyDzz
        Where xx is vCPU count, yy is RAM in MiB, zz is Disk in GiB
        """
        for f in self.dflavors:
            self.assertEqual("C%dR%dD%d" % (f["cpu"], f["ram"], f["disk"]),
                             f["name"],
                             "Flavor %s does not match its specs." % f["name"])


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
300
301
# --------------------------------------------------------------------
# ServersTestCase class
302
303
304
305
306
307
class ServersTestCase(unittest.TestCase):
    """Test server lists for consistency"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get (detailed) list of servers"""
        log.info("Getting simple and detailed list of servers")
308

John Giannelos's avatar
John Giannelos committed
309
        cls.client = ComputeClient(API, TOKEN)
310
311
        cls.servers = cls.client.list_servers()
        cls.dservers = cls.client.list_servers(detail=True)
312
        cls.result_dict = dict()
313

314
315
316
    # def test_001_list_servers(self):
    #     """Test server list actually returns servers"""
    #     self.assertGreater(len(self.servers), 0)
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335

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

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


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

    @classmethod
    def setUpClass(cls):
        """Initialize a kamaki instance"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
336
        log.info("Spawning server for image `%s'" % cls.imagename)
John Giannelos's avatar
John Giannelos committed
337
338
        cls.client = ComputeClient(API, TOKEN)
        cls.cyclades = CycladesClient(API, TOKEN)
339
        cls.result_dict = dict()
340
341

    def _get_ipv4(self, server):
342
        """Get the public IPv4 of a server from the detailed server info"""
343

344
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
345

346
347
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
348
            if self.cyclades.get_network_details(net_id)["public"]:
349
                public_addrs = nic["ipv4"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
350
351

        self.assertTrue(public_addrs is not None)
John Giannelos's avatar
John Giannelos committed
352

353
        return public_addrs
354
355

    def _get_ipv6(self, server):
356
        """Get the public IPv6 of a server from the detailed server info"""
John Giannelos's avatar
John Giannelos committed
357

358
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
359

360
361
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
362
            if self.cyclades.get_network_details(net_id)["public"]:
363
                public_addrs = nic["ipv6"]
John Giannelos's avatar
John Giannelos committed
364

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
365
        self.assertTrue(public_addrs is not None)
John Giannelos's avatar
John Giannelos committed
366

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
367
        return public_addrs
368

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
369
    def _connect_loginname(self, os_value):
370
        """Return the login name for connections based on the server OS"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
371
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
372
            return "user"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
373
        elif os_value in ("windows", "windows_alpha1"):
374
            return "Administrator"
375
        else:
376
            return "root"
377
378
379
380

    def _verify_server_status(self, current_status, new_status):
        """Verify a server has switched to a specified status"""
        server = self.client.get_server_details(self.serverid)
381
382
        if server["status"] not in (current_status, new_status):
            return None  # Do not raise exception, return so the test fails
383
384
385
386
387
388
389
390
391
392
393
        self.assertEquals(server["status"], new_status)

    def _get_connected_tcp_socket(self, family, host, port):
        """Get a connected socket from the specified family to host:port"""
        sock = None
        for res in \
            socket.getaddrinfo(host, port, family, socket.SOCK_STREAM, 0,
                               socket.AI_PASSIVE):
            af, socktype, proto, canonname, sa = res
            try:
                sock = socket.socket(af, socktype, proto)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
394
            except socket.error:
395
396
397
398
                sock = None
                continue
            try:
                sock.connect(sa)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
399
            except socket.error:
400
401
402
403
404
405
406
407
408
409
410
411
412
413
                sock.close()
                sock = None
                continue
        self.assertIsNotNone(sock)
        return sock

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

415
    def _get_hostname_over_ssh(self, hostip, username, password):
416
417
        lines, status = _ssh_execute(
            hostip, username, password, "hostname")
418
        self.assertEqual(len(lines), 1)
419
        return lines[0]
420
421
422
423

    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
                                   opmsg, callable, *args, **kwargs):
        if warn_timeout == fail_timeout:
424
425
426
427
            warn_timeout = fail_timeout + 1
        warn_tmout = time.time() + warn_timeout
        fail_tmout = time.time() + fail_timeout
        while True:
428
            self.assertLess(time.time(), fail_tmout,
429
                            "operation `%s' timed out" % opmsg)
430
            if time.time() > warn_tmout:
431
432
                log.warning("Server %d: `%s' operation `%s' not done yet",
                            self.serverid, self.servername, opmsg)
433
            try:
434
                log.info("%s... " % opmsg)
435
436
437
                return callable(*args, **kwargs)
            except AssertionError:
                pass
438
439
            time.sleep(self.query_interval)

440
    def _insist_on_tcp_connection(self, family, host, port):
441
442
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
443
444
445
        msg = "connect over %s to %s:%s" % \
              (familystr.get(family, "Unknown"), host, port)
        sock = self._try_until_timeout_expires(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
446
447
448
            self.action_timeout, self.action_timeout,
            msg, self._get_connected_tcp_socket,
            family, host, port)
449
450
451
        return sock

    def _insist_on_status_transition(self, current_status, new_status,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
452
                                     fail_timeout, warn_timeout=None):
453
454
        msg = "Server %d: `%s', waiting for %s -> %s" % \
              (self.serverid, self.servername, current_status, new_status)
455
456
457
458
459
        if warn_timeout is None:
            warn_timeout = fail_timeout
        self._try_until_timeout_expires(warn_timeout, fail_timeout,
                                        msg, self._verify_server_status,
                                        current_status, new_status)
460
461
462
        # Ensure the status is actually the expected one
        server = self.client.get_server_details(self.serverid)
        self.assertEquals(server["status"], new_status)
463
464

    def _insist_on_ssh_hostname(self, hostip, username, password):
465
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
466
        hostname = self._try_until_timeout_expires(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
467
468
469
            self.action_timeout, self.action_timeout,
            msg, self._get_hostname_over_ssh,
            hostip, username, password)
470
471
472

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

474
475
476
477
    def _check_file_through_ssh(self, hostip, username, password,
                                remotepath, content):
        msg = "Trying file injection through SSH to %s, as %s/%s" % \
            (hostip, username, password)
478
        log.info(msg)
479
480
481
482
        try:
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect(hostip, username=username, password=password)
483
            ssh.close()
484
485
        except socket.error:
            raise AssertionError
486

487
488
489
490
        transport = paramiko.Transport((hostip, 22))
        transport.connect(username=username, password=password)

        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
491
        sftp = paramiko.SFTPClient.from_transport(transport)
492
        sftp.get(remotepath, localpath)
493
494
495
        sftp.close()
        transport.close()

496
497
498
        f = open(localpath)
        remote_content = b64encode(f.read())

499
        # Check if files are the same
500
        return (remote_content == content)
501

502
503
504
505
506
507
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)

    def test_001_submit_create_server(self):
        """Test submit create server request"""
508
509

        log.info("Submit new server request")
510
511
        server = self.client.create_server(self.servername, self.flavorid,
                                           self.imageid, self.personality)
512

513
514
        log.info("Server id: " + str(server["id"]))
        log.info("Server password: " + server["adminPass"])
515
516
517
518
519
520
521
522
        self.assertEqual(server["name"], self.servername)
        self.assertEqual(server["flavorRef"], self.flavorid)
        self.assertEqual(server["imageRef"], self.imageid)
        self.assertEqual(server["status"], "BUILD")

        # Update class attributes to reflect data on building server
        cls = type(self)
        cls.serverid = server["id"]
523
        cls.username = None
524
525
        cls.passwd = server["adminPass"]

526
527
528
        self.result_dict["Server ID"] = str(server["id"])
        self.result_dict["Password"] = str(server["adminPass"])

529
530
    def test_002a_server_is_building_in_list(self):
        """Test server is in BUILD state, in server list"""
531
532
        log.info("Server in BUILD state in server list")

533
534
        self.result_dict.clear()

535
536
        servers = self.client.list_servers(detail=True)
        servers = filter(lambda x: x["name"] == self.servername, servers)
John Giannelos's avatar
John Giannelos committed
537

538
539
540
541
542
543
544
545
        server = servers[0]
        self.assertEqual(server["name"], self.servername)
        self.assertEqual(server["flavorRef"], self.flavorid)
        self.assertEqual(server["imageRef"], self.imageid)
        self.assertEqual(server["status"], "BUILD")

    def test_002b_server_is_building_in_details(self):
        """Test server is in BUILD state, in details"""
546
547
548

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

549
550
551
552
553
554
555
        server = self.client.get_server_details(self.serverid)
        self.assertEqual(server["name"], self.servername)
        self.assertEqual(server["flavorRef"], self.flavorid)
        self.assertEqual(server["imageRef"], self.imageid)
        self.assertEqual(server["status"], "BUILD")

    def test_002c_set_server_metadata(self):
556
557
558

        log.info("Creating server metadata")

559
        image = self.client.get_image_details(self.imageid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
560
        os_value = image["metadata"]["values"]["os"]
561
        users = image["metadata"]["values"].get("users", None)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
562
        self.client.update_server_metadata(self.serverid, OS=os_value)
John Giannelos's avatar
John Giannelos committed
563

564
        userlist = users.split()
565

566
567
568
        # Determine the username to use for future connections
        # to this host
        cls = type(self)
569
570
571

        if "root" in userlist:
            cls.username = "root"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
572
573
        elif users is None:
            cls.username = self._connect_loginname(os_value)
574
575
576
        else:
            cls.username = choice(userlist)

577
        self.assertIsNotNone(cls.username)
578
579
580

    def test_002d_verify_server_metadata(self):
        """Test server metadata keys are set based on image metadata"""
581
582
583

        log.info("Verifying image metadata")

584
585
        servermeta = self.client.get_server_metadata(self.serverid)
        imagemeta = self.client.get_image_metadata(self.imageid)
586

John Giannelos's avatar
John Giannelos committed
587
        self.assertEqual(servermeta["OS"], imagemeta["os"])
588
589
590

    def test_003_server_becomes_active(self):
        """Test server becomes ACTIVE"""
591
592
593

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
594
595
        self._insist_on_status_transition(
            "BUILD", "ACTIVE", self.build_fail, self.build_warning)
596

John Giannelos's avatar
John Giannelos committed
597
598
    def test_003a_get_server_oob_console(self):
        """Test getting OOB server console over VNC
599

John Giannelos's avatar
John Giannelos committed
600
601
        Implementation of RFB protocol follows
        http://www.realvnc.com/docs/rfbproto.pdf.
602

John Giannelos's avatar
John Giannelos committed
603
604
605
        """
        console = self.cyclades.get_server_console(self.serverid)
        self.assertEquals(console['type'], "vnc")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
606
607
        sock = self._insist_on_tcp_connection(
            socket.AF_INET, console["host"], console["port"])
John Giannelos's avatar
John Giannelos committed
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633

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

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

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

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

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

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

635
636
    def test_004_server_has_ipv4(self):
        """Test active server has a valid IPv4 address"""
637

638
        log.info("Validate server's IPv4")
639

640
641
        server = self.client.get_server_details(self.serverid)
        ipv4 = self._get_ipv4(server)
642
643
644
645

        self.result_dict.clear()
        self.result_dict["IPv4"] = str(ipv4)

646
647
        self.assertEquals(IP(ipv4).version(), 4)

648
649
    def test_005_server_has_ipv6(self):
        """Test active server has a valid IPv6 address"""
650
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
651

652
        log.info("Validate server's IPv6")
653

654
655
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
656
657
658
659

        self.result_dict.clear()
        self.result_dict["IPv6"] = str(ipv6)

660
        self.assertEquals(IP(ipv6).version(), 6)
661

John Giannelos's avatar
John Giannelos committed
662
663
664
665
    def test_006_server_responds_to_ping_IPv4(self):
        """Test server responds to ping on IPv4 address"""

        log.info("Testing if server responds to pings in IPv4")
666
        self.result_dict.clear()
John Giannelos's avatar
John Giannelos committed
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687

        server = self.client.get_server_details(self.serverid)
        ip = self._get_ipv4(server)
        self._try_until_timeout_expires(self.action_timeout,
                                        self.action_timeout,
                                        "PING IPv4 to %s" % ip,
                                        self._ping_once,
                                        False, ip)

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

        server = self.client.get_server_details(self.serverid)
        ip = self._get_ipv6(server)
        self._try_until_timeout_expires(self.action_timeout,
                                        self.action_timeout,
                                        "PING IPv6 to %s" % ip,
                                        self._ping_once,
                                        True, ip)
688
689
690

    def test_008_submit_shutdown_request(self):
        """Test submit request to shutdown server"""
691
692
693

        log.info("Shutting down server")

694
        self.cyclades.shutdown_server(self.serverid)
695
696
697

    def test_009_server_becomes_stopped(self):
        """Test server becomes STOPPED"""
698
699

        log.info("Waiting until server becomes STOPPED")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
700
701
        self._insist_on_status_transition(
            "ACTIVE", "STOPPED", self.action_timeout, self.action_timeout)
702
703
704

    def test_010_submit_start_request(self):
        """Test submit start server request"""
705
706
707

        log.info("Starting server")

708
        self.cyclades.start_server(self.serverid)
709
710
711

    def test_011_server_becomes_active(self):
        """Test server becomes ACTIVE again"""
712
713

        log.info("Waiting until server becomes ACTIVE")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
714
715
        self._insist_on_status_transition(
            "STOPPED", "ACTIVE", self.action_timeout, self.action_timeout)
716

John Giannelos's avatar
John Giannelos committed
717
718
    def test_011a_server_responds_to_ping_IPv4(self):
        """Test server OS is actually up and running again"""
719

John Giannelos's avatar
John Giannelos committed
720
        log.info("Testing if server is actually up and running")
721

John Giannelos's avatar
John Giannelos committed
722
        self.test_006_server_responds_to_ping_IPv4()
723

John Giannelos's avatar
John Giannelos committed
724
725
    def test_012_ssh_to_server_IPv4(self):
        """Test SSH to server public IPv4 works, verify hostname"""
726

John Giannelos's avatar
John Giannelos committed
727
728
729
730
        self._skipIf(self.is_windows, "only valid for Linux servers")
        server = self.client.get_server_details(self.serverid)
        self._insist_on_ssh_hostname(self._get_ipv4(server),
                                     self.username, self.passwd)
731

John Giannelos's avatar
John Giannelos committed
732
733
734
735
    def test_013_ssh_to_server_IPv6(self):
        """Test SSH to server public IPv6 works, verify hostname"""
        self._skipIf(self.is_windows, "only valid for Linux servers")
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
736

John Giannelos's avatar
John Giannelos committed
737
738
739
        server = self.client.get_server_details(self.serverid)
        self._insist_on_ssh_hostname(self._get_ipv6(server),
                                     self.username, self.passwd)
740

John Giannelos's avatar
John Giannelos committed
741
742
743
744
745
    def test_014_rdp_to_server_IPv4(self):
        "Test RDP connection to server public IPv4 works"""
        self._skipIf(not self.is_windows, "only valid for Windows servers")
        server = self.client.get_server_details(self.serverid)
        ipv4 = self._get_ipv4(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
746
        sock = self._insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
747

John Giannelos's avatar
John Giannelos committed
748
749
750
751
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        # FIXME: Use rdesktop, analyze exit code? see manpage [costasd]
        sock.close()
752

John Giannelos's avatar
John Giannelos committed
753
754
755
756
    def test_015_rdp_to_server_IPv6(self):
        "Test RDP connection to server public IPv6 works"""
        self._skipIf(not self.is_windows, "only valid for Windows servers")
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
757

John Giannelos's avatar
John Giannelos committed
758
759
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
760
        sock = self._get_tcp_connection(socket.AF_INET6, ipv6, 3389)
761

John Giannelos's avatar
John Giannelos committed
762
763
764
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        sock.close()
765

John Giannelos's avatar
John Giannelos committed
766
767
768
    def test_016_personality_is_enforced(self):
        """Test file injection for personality enforcement"""
        self._skipIf(self.is_windows, "only implemented for Linux servers")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
769
        self._skipIf(self.personality is None, "No personality file selected")
770

John Giannelos's avatar
John Giannelos committed
771
        log.info("Trying to inject file for personality enforcement")
772

John Giannelos's avatar
John Giannelos committed
773
        server = self.client.get_server_details(self.serverid)
774

John Giannelos's avatar
John Giannelos committed
775
776
777
778
779
780
781
        for inj_file in self.personality:
            equal_files = self._check_file_through_ssh(self._get_ipv4(server),
                                                       inj_file['owner'],
                                                       self.passwd,
                                                       inj_file['path'],
                                                       inj_file['contents'])
            self.assertTrue(equal_files)
782

783
784
    def test_017_submit_delete_request(self):
        """Test submit request to delete server"""
785
786
787

        log.info("Deleting server")

788
789
790
791
        self.client.delete_server(self.serverid)

    def test_018_server_becomes_deleted(self):
        """Test server becomes DELETED"""
792
793
794

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
795
796
        self._insist_on_status_transition(
            "ACTIVE", "DELETED", self.action_timeout, self.action_timeout)
797
798
799

    def test_019_server_no_longer_in_server_list(self):
        """Test server is no longer in server list"""
800
801
802

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

803
        servers = self.client.list_servers()
804
805
806
        self.assertNotIn(self.serverid, [s["id"] for s in servers])


807
class NetworkTestCase(unittest.TestCase):
John Giannelos's avatar
John Giannelos committed
808
    """ Testing networking in cyclades """
809

810
    @classmethod
John Giannelos's avatar
John Giannelos committed
811
812
    def setUpClass(cls):
        "Initialize kamaki, get list of current networks"
John Giannelos's avatar
John Giannelos committed
813
814
815

        cls.client = CycladesClient(API, TOKEN)
        cls.compute = ComputeClient(API, TOKEN)
816

817
818
819
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
                                          TEST_RUN_ID,
                                          cls.imagename)
820
821
822
823
824

        #Dictionary initialization for the vms credentials
        cls.serverid = dict()
        cls.username = dict()
        cls.password = dict()
John Giannelos's avatar
John Giannelos committed
825
        cls.is_windows = cls.imagename.lower().find("windows") >= 0
826

827
828
        cls.result_dict = dict()

829
830
831
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)
832

833
834
835
    def _get_ipv4(self, server):
        """Get the public IPv4 of a server from the detailed server info"""

836
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
837

838
839
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
840
            if self.client.get_network_details(net_id)["public"]:
841
                public_addrs = nic["ipv4"]
John Giannelos's avatar
John Giannelos committed
842

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
843
        self.assertTrue(public_addrs is not None)
John Giannelos's avatar
John Giannelos committed
844

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
845
        return public_addrs
846

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
847
    def _connect_loginname(self, os_value):
848
        """Return the login name for connections based on the server OS"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
849
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
850
            return "user"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
851
        elif os_value in ("windows", "windows_alpha1"):
852
853
854
855
            return "Administrator"
        else:
            return "root"

John Giannelos's avatar
John Giannelos committed
856
    def _ping_once(self, ip):
857

John Giannelos's avatar
John Giannelos committed
858
859
860
861
862
863
        """Test server responds to a single IPv4 or IPv6 ping"""
        cmd = "ping -c 2 -w 3 %s" % (ip)
        ping = subprocess.Popen(cmd, shell=True,
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (stdout, stderr) = ping.communicate()
        ret = ping.wait()
864

John Giannelos's avatar
John Giannelos committed
865
866
        return (ret == 0)

John Giannelos's avatar
John Giannelos committed
867
    def test_00001a_submit_create_server_A(self):
868
        """Test submit create server request"""
869
870
871

        log.info("Creating test server A")

872
        serverA = self.client.create_server(self.servername, self.flavorid,
873
                                            self.imageid, personality=None)
874

John Giannelos's avatar
John Giannelos committed
875
876
877
878
        self.assertEqual(serverA["name"], self.servername)
        self.assertEqual(serverA["flavorRef"], self.flavorid)
        self.assertEqual(serverA["imageRef"], self.imageid)
        self.assertEqual(serverA["status"], "BUILD")
879
880

        # Update class attributes to reflect data on building server
John Giannelos's avatar
John Giannelos committed
881
882
883
        self.serverid['A'] = serverA["id"]
        self.username['A'] = None
        self.password['A'] = serverA["adminPass"]
884

885
886
        log.info("Server A id:" + str(serverA["id"]))
        log.info("Server password " + (self.password['A']))
887

888
889
        self.result_dict["Server A ID"] = str(serverA["id"])
        self.result_dict["Server A password"] = serverA["adminPass"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
890

John Giannelos's avatar
John Giannelos committed
891
    def test_00001b_serverA_becomes_active(self):
892
        """Test server becomes ACTIVE"""
893

894
        log.info("Waiting until test server A becomes ACTIVE")
895
        self.result_dict.clear()
896

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

        self.assertTrue(active)

John Giannelos's avatar
John Giannelos committed
911
    def test_00002a_submit_create_server_B(self):
John Giannelos's avatar
John Giannelos committed
912
        """Test submit create server request"""
913

914
        log.info("Creating test server B")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
915

John Giannelos's avatar
John Giannelos committed
916
        serverB = self.client.create_server(self.servername, self.flavorid,
917
918
                                            self.imageid, personality=None)

John Giannelos's avatar
John Giannelos committed
919
920
921
922
923
924
925
926
927
928
        self.assertEqual(serverB["name"], self.servername)
        self.assertEqual(serverB["flavorRef"], self.flavorid)
        self.assertEqual(serverB["imageRef"], self.imageid)
        self.assertEqual(serverB["status"], "BUILD")

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

929
930
        log.info("Server B id: " + str(serverB["id"]))
        log.info("Password " + (self.password['B']))
931

932
933
934
935
        self.result_dict.clear()
        self.result_dict["Server B ID"] = str(serverB["id"])
        self.result_dict["Server B password"] = serverB["adminPass"]

John Giannelos's avatar
John Giannelos committed
936
    def test_00002b_serverB_becomes_active(self):
937
938
        """Test server becomes ACTIVE"""

939
        log.info("Waiting until test server B becomes ACTIVE")
940
        self.result_dict.clear()
941

942
        fail_tmout = time.time() + self.action_timeout
943
944
945
946
947
948
949
950
951
952
953
954
        while True:
            d = self.client.get_server_details(self.serverid['B'])
            status = d['status']
            if status == 'ACTIVE':
                active = True
                break
            elif time.time() > fail_tmout:
                self.assertLess(time.time(), fail_tmout)
            else:
                time.sleep(self.query_interval)

        self.assertTrue(active)
John Giannelos's avatar
John Giannelos committed
955

956
    def test_001_create_network(self):
John Giannelos's avatar
John Giannelos committed
957
        """Test submit create network request"""
958
959

        log.info("Submit new network request")
960
        self.result_dict.clear()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
961

962
        name = SNF_TEST_PREFIX + TEST_RUN_ID
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
963
964
        #previous_num = len(self.client.list_networks())
        network = self.client.create_network(name, cidr='10.0.0.1/28')
965

966
        #Test if right name is assigned
John Giannelos's avatar
John Giannelos committed
967
        self.assertEqual(network['name'], name)
968

969
        # Update class attributes
John Giannelos's avatar
John Giannelos committed
970
971
        cls = type(self)
        cls.networkid = network['id']
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
972
        #networks = self.client.list_networks()
973

John Giannelos's avatar
John Giannelos committed
974
975
        fail_tmout = time.time() + self.action_timeout

976
        #Test if new network is created
John Giannelos's avatar
John Giannelos committed
977
978
979
980
981
982
983
984
985
986
987
988
        while True:
            d = self.client.get_network_details(network['id'])
            if d['status'] == 'ACTIVE':
                connected = True
                break
            elif time.time() > fail_tmout:
                self.assertLess(time.time(), fail_tmout)
            else:
                log.info("Waiting for network to become ACTIVE")
                time.sleep(self.query_interval)

        self.assertTrue(connected)
989

990
991
        self.result_dict["Private network ID"] = str(network['id'])

992
    def test_002_connect_to_network(self):
993
        """Test connect VMs to network"""
994

995
        log.info("Connect VMs to private network")
996
        self.result_dict.clear()
997

998
999
        self.client.connect_server(self.serverid['A'], self.networkid)
        self.client.connect_server(self.serverid['B'], self.networkid)
1000

1001
        #Insist on connecting until action timeout
1002
        fail_tmout = time.time() + self.action_timeout
1003

1004
        while True:
John Giannelos's avatar
John Giannelos committed
1005

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1006
1007
1008
1009
1010
1011
            netsA = [x['network_id']
                     for x in self.client.get_server_details(
                         self.serverid['A'])['attachments']['values']]
            netsB = [x['network_id']
                     for x in self.client.get_server_details(
                         self.serverid['B'])['attachments']['values']]
John Giannelos's avatar
John Giannelos committed
1012
1013

            if (self.networkid in netsA) and (self.networkid in netsB):
1014
                conn_exists = True
John Giannelos's avatar
John Giannelos committed
1015
1016
                break
            elif time.time() > fail_tmout:
1017
1018
1019
                self.assertLess(time.time(), fail_tmout)
            else:
                time.sleep(self.query_interval)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1020

John Giannelos's avatar
John Giannelos committed
1021
1022
1023
1024
        #Adding private IPs to class attributes
        cls = type(self)
        cls.priv_ip = dict()

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1025
1026
1027
1028
        nicsA = self.client.get_server_details(
            self.serverid['A'])['attachments']['values']
        nicsB = self.client.get_server_details(
            self.serverid['B'])['attachments']['values']
John Giannelos's avatar
John Giannelos committed
1029
1030
1031
1032
1033
1034
1035
1036
1037

        if conn_exists:
            for nic in nicsA:
                if nic["network_id"] == self.networkid:
                    cls.priv_ip["A"] = nic["ipv4"]

            for nic in nicsB:
                if nic["network_id"] == self.networkid:
                    cls.priv_ip["B"] = nic["ipv4"]
1038
1039

        self.assertTrue(conn_exists)