burnin.py 73.8 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
import datetime
import inspect
import logging
import os
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
43
import os.path
44
import paramiko
45
import prctl
46
import subprocess
47
import signal
48
49
50
import socket
import sys
import time
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
51
import tempfile
52
from base64 import b64encode
53
from IPy import IP
54
from multiprocessing import Process, Queue
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
55
from random import choice, randint
John Giannelos's avatar
John Giannelos committed
56
from optparse import OptionParser, OptionValueError
57

John Giannelos's avatar
John Giannelos committed
58
59
from kamaki.clients.compute import ComputeClient
from kamaki.clients.cyclades import CycladesClient
60
from kamaki.clients.image import ImageClient
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
61
from kamaki.clients.pithos import PithosClient
John Giannelos's avatar
John Giannelos committed
62
from kamaki.clients import ClientError
John Giannelos's avatar
John Giannelos committed
63

64
from vncauthproxy.d3des import generate_response as d3des_generate_response
65
66
67
68
69

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
74
75
76
77
78
79
# --------------------------------------------------------------------
# Global Variables
API = None
TOKEN = None
PLANKTON = None
PLANKTON_USER = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
80
81
PITHOS = None
PITHOS_USER = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
NO_IPV6 = None
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'

97

98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# --------------------------------------------------------------------
# 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
118
119
# --------------------------------------------------------------------
# BurninTestReulst class
120
121
122
123
class BurninTestResult(unittest.TextTestResult):
    def addSuccess(self, test):
        super(BurninTestResult, self).addSuccess(test)
        if self.showAll:
John Giannelos's avatar
John Giannelos committed
124
            if hasattr(test, 'result_dict'):
125
126
127
128
129
130
131
132
133
                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
134
135
            self.stream.flush()

136
137
138
139
    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
140
141
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
142

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

        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
156
157
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
158

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

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


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
169
170
# --------------------------------------------------------------------
# Format Results
John Giannelos's avatar
John Giannelos committed
171
172
class burninFormatter(logging.Formatter):
    err_fmt = red + "ERROR: %(msg)s" + normal
John Giannelos's avatar
John Giannelos committed
173
    dbg_fmt = green + "* %(msg)s" + normal
John Giannelos's avatar
John Giannelos committed
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
    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

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

198

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

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

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


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
218
219
# --------------------------------------------------------------------
# ImagesTestCase class
220
221
222
223
224
225
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
226
        cls.client = ComputeClient(API, TOKEN)
227
228
229
        cls.plankton = ImageClient(PLANKTON, TOKEN)
        cls.images = cls.plankton.list_public()
        cls.dimages = cls.plankton.list_public(detail=True)
230
        cls.result_dict = dict()
231
232
233

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

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

240
241
242
243
244
245
246
    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):
247
248
249
250
        """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))
251
252
253
254
        self.assertEqual(sorted(list(set(names))), names)

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


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
261
262
# --------------------------------------------------------------------
# FlavorsTestCase class
263
264
265
266
267
268
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
269
        cls.client = ComputeClient(API, TOKEN)
270
271
        cls.flavors = cls.client.list_flavors()
        cls.dflavors = cls.client.list_flavors(detail=True)
272
        cls.result_dict = dict()
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
300
301
302

    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
303
304
# --------------------------------------------------------------------
# ServersTestCase class
305
306
307
308
309
310
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")
311

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

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

    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)


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# --------------------------------------------------------------------
# Pithos Test Cases
class PithosTestCase(unittest.TestCase):
    """Test pithos functionality"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get list of containers"""
        log.info("Getting list of containers")

        cls.client = PithosClient(PITHOS, TOKEN, PITHOS_USER)
        cls.containers = cls.client.list_containers()
        cls.result_dict = dict()

    def test_001_list_containers(self):
        """Test container list actually returns containers"""
        self.assertGreater(len(self.containers), 0)

    def test_002_unique_containers(self):
        """Test if containers have unique names"""
        names = [n['name'] for n in self.containers]
        names = sorted(names)
        self.assertEqual(sorted(list(set(names))), names)

    def test_003_create_container(self):
        """Test create a container"""
        rand_num = randint(1000, 9999)
        rand_name = "%s%s" % (SNF_TEST_PREFIX, rand_num)
        names = [n['name'] for n in self.containers]
        while rand_name in names:
            rand_num = randint(1000, 9999)
            rand_name = "%s%s" % (SNF_TEST_PREFIX, rand_num)
        # Create container
        self.client.container = rand_name
        self.client.container_put()
        # Get list of containers
        new_containers = self.client.list_containers()
        new_container_names = [n['name'] for n in new_containers]
        self.assertIn(rand_name, new_container_names)

    def test_004_upload(self):
        """Test uploading something to pithos+"""
        # Create a tmp file
        with tempfile.TemporaryFile() as f:
            f.write("This is a temp file")
            f.seek(0, 0)
            # Where to save file
            self.client.upload_object("test.txt", f)

    def test_005_download(self):
        """Test download something from pithos+"""
        # Create tmp directory to save file
        tmp_dir = tempfile.mkdtemp()
        tmp_file = os.path.join(tmp_dir, "test.txt")
        with open(tmp_file, "wb+") as f:
            self.client.download_object("test.txt", f)
            # Read file
            f.seek(0, 0)
            content = f.read()
        # Remove files
        os.unlink(tmp_file)
        os.rmdir(tmp_dir)
        # Compare results
        self.assertEqual(content, "This is a temp file")

    def test_006_remove(self):
        """Test removing files and containers"""
        cont_name = self.client.container
        self.client.del_object("test.txt")
        self.client.purge_container()
        # List containers
        containers = self.client.list_containers()
        cont_names = [n['name'] for n in containers]
        self.assertNotIn(cont_name, cont_names)


# --------------------------------------------------------------------
408
409
410
411
412
413
414
# 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
415
        log.info("Spawning server for image `%s'" % cls.imagename)
John Giannelos's avatar
John Giannelos committed
416
417
        cls.client = ComputeClient(API, TOKEN)
        cls.cyclades = CycladesClient(API, TOKEN)
418
        cls.result_dict = dict()
419
420

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

423
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
424

425
426
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
427
            if self.cyclades.get_network_details(net_id)["public"]:
428
                public_addrs = nic["ipv4"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
429
430

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

432
        return public_addrs
433
434

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

437
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
438

439
440
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
441
            if self.cyclades.get_network_details(net_id)["public"]:
442
                public_addrs = nic["ipv6"]
John Giannelos's avatar
John Giannelos committed
443

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
446
        return public_addrs
447

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
448
    def _connect_loginname(self, os_value):
449
        """Return the login name for connections based on the server OS"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
450
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
451
            return "user"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
452
        elif os_value in ("windows", "windows_alpha1"):
453
            return "Administrator"
454
        else:
455
            return "root"
456
457
458
459

    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)
460
461
        if server["status"] not in (current_status, new_status):
            return None  # Do not raise exception, return so the test fails
462
463
464
465
466
467
468
469
470
471
472
        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
473
            except socket.error:
474
475
476
477
                sock = None
                continue
            try:
                sock.connect(sa)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
478
            except socket.error:
479
480
481
482
483
484
485
486
487
488
489
490
491
492
                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)
493

494
    def _get_hostname_over_ssh(self, hostip, username, password):
495
496
        lines, status = _ssh_execute(
            hostip, username, password, "hostname")
497
        self.assertEqual(len(lines), 1)
498
        return lines[0]
499
500
501
502

    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
                                   opmsg, callable, *args, **kwargs):
        if warn_timeout == fail_timeout:
503
504
505
506
            warn_timeout = fail_timeout + 1
        warn_tmout = time.time() + warn_timeout
        fail_tmout = time.time() + fail_timeout
        while True:
507
            self.assertLess(time.time(), fail_tmout,
508
                            "operation `%s' timed out" % opmsg)
509
            if time.time() > warn_tmout:
510
511
                log.warning("Server %d: `%s' operation `%s' not done yet",
                            self.serverid, self.servername, opmsg)
512
            try:
513
                log.info("%s... " % opmsg)
514
515
516
                return callable(*args, **kwargs)
            except AssertionError:
                pass
517
518
            time.sleep(self.query_interval)

519
    def _insist_on_tcp_connection(self, family, host, port):
520
521
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
522
523
524
        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
525
526
527
            self.action_timeout, self.action_timeout,
            msg, self._get_connected_tcp_socket,
            family, host, port)
528
529
530
        return sock

    def _insist_on_status_transition(self, current_status, new_status,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
531
                                     fail_timeout, warn_timeout=None):
532
533
        msg = "Server %d: `%s', waiting for %s -> %s" % \
              (self.serverid, self.servername, current_status, new_status)
534
535
536
537
538
        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)
539
540
541
        # Ensure the status is actually the expected one
        server = self.client.get_server_details(self.serverid)
        self.assertEquals(server["status"], new_status)
542
543

    def _insist_on_ssh_hostname(self, hostip, username, password):
544
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
545
        hostname = self._try_until_timeout_expires(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
546
547
548
            self.action_timeout, self.action_timeout,
            msg, self._get_hostname_over_ssh,
            hostip, username, password)
549
550
551

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

553
554
555
556
    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)
557
        log.info(msg)
558
559
560
561
        try:
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect(hostip, username=username, password=password)
562
            ssh.close()
563
564
        except socket.error:
            raise AssertionError
565

566
567
568
569
        transport = paramiko.Transport((hostip, 22))
        transport.connect(username=username, password=password)

        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
570
        sftp = paramiko.SFTPClient.from_transport(transport)
571
        sftp.get(remotepath, localpath)
572
573
574
        sftp.close()
        transport.close()

575
576
577
        f = open(localpath)
        remote_content = b64encode(f.read())

578
        # Check if files are the same
579
        return (remote_content == content)
580

581
582
583
584
585
586
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)

    def test_001_submit_create_server(self):
        """Test submit create server request"""
587
588

        log.info("Submit new server request")
589
590
        server = self.client.create_server(self.servername, self.flavorid,
                                           self.imageid, self.personality)
591

592
593
        log.info("Server id: " + str(server["id"]))
        log.info("Server password: " + server["adminPass"])
594
595
596
597
598
599
600
601
        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"]
602
        cls.username = None
603
604
        cls.passwd = server["adminPass"]

605
606
607
        self.result_dict["Server ID"] = str(server["id"])
        self.result_dict["Password"] = str(server["adminPass"])

608
609
    def test_002a_server_is_building_in_list(self):
        """Test server is in BUILD state, in server list"""
610
611
        log.info("Server in BUILD state in server list")

612
613
        self.result_dict.clear()

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

617
618
619
620
621
622
623
624
        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"""
625
626
627

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

628
629
630
631
632
633
634
        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):
635
636
637

        log.info("Creating server metadata")

638
        image = self.client.get_image_details(self.imageid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
639
        os_value = image["metadata"]["values"]["os"]
640
        users = image["metadata"]["values"].get("users", None)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
641
        self.client.update_server_metadata(self.serverid, OS=os_value)
John Giannelos's avatar
John Giannelos committed
642

643
        userlist = users.split()
644

645
646
647
        # Determine the username to use for future connections
        # to this host
        cls = type(self)
648
649
650

        if "root" in userlist:
            cls.username = "root"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
651
652
        elif users is None:
            cls.username = self._connect_loginname(os_value)
653
654
655
        else:
            cls.username = choice(userlist)

656
        self.assertIsNotNone(cls.username)
657
658
659

    def test_002d_verify_server_metadata(self):
        """Test server metadata keys are set based on image metadata"""
660
661
662

        log.info("Verifying image metadata")

663
664
        servermeta = self.client.get_server_metadata(self.serverid)
        imagemeta = self.client.get_image_metadata(self.imageid)
665

John Giannelos's avatar
John Giannelos committed
666
        self.assertEqual(servermeta["OS"], imagemeta["os"])
667
668
669

    def test_003_server_becomes_active(self):
        """Test server becomes ACTIVE"""
670
671
672

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
673
674
        self._insist_on_status_transition(
            "BUILD", "ACTIVE", self.build_fail, self.build_warning)
675

John Giannelos's avatar
John Giannelos committed
676
677
    def test_003a_get_server_oob_console(self):
        """Test getting OOB server console over VNC
678

John Giannelos's avatar
John Giannelos committed
679
680
        Implementation of RFB protocol follows
        http://www.realvnc.com/docs/rfbproto.pdf.
681

John Giannelos's avatar
John Giannelos committed
682
683
684
        """
        console = self.cyclades.get_server_console(self.serverid)
        self.assertEquals(console['type'], "vnc")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
685
686
        sock = self._insist_on_tcp_connection(
            socket.AF_INET, console["host"], console["port"])
John Giannelos's avatar
John Giannelos committed
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712

        # 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()
713

714
715
    def test_004_server_has_ipv4(self):
        """Test active server has a valid IPv4 address"""
716

717
        log.info("Validate server's IPv4")
718

719
720
        server = self.client.get_server_details(self.serverid)
        ipv4 = self._get_ipv4(server)
721
722
723
724

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

725
726
        self.assertEquals(IP(ipv4).version(), 4)

727
728
    def test_005_server_has_ipv6(self):
        """Test active server has a valid IPv6 address"""
729
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
730

731
        log.info("Validate server's IPv6")
732

733
734
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
735
736
737
738

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

739
        self.assertEquals(IP(ipv6).version(), 6)
740

John Giannelos's avatar
John Giannelos committed
741
742
743
744
    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")
745
        self.result_dict.clear()
John Giannelos's avatar
John Giannelos committed
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766

        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)
767
768
769

    def test_008_submit_shutdown_request(self):
        """Test submit request to shutdown server"""
770
771
772

        log.info("Shutting down server")

773
        self.cyclades.shutdown_server(self.serverid)
774
775
776

    def test_009_server_becomes_stopped(self):
        """Test server becomes STOPPED"""
777
778

        log.info("Waiting until server becomes STOPPED")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
779
780
        self._insist_on_status_transition(
            "ACTIVE", "STOPPED", self.action_timeout, self.action_timeout)
781
782
783

    def test_010_submit_start_request(self):
        """Test submit start server request"""
784
785
786

        log.info("Starting server")

787
        self.cyclades.start_server(self.serverid)
788
789
790

    def test_011_server_becomes_active(self):
        """Test server becomes ACTIVE again"""
791
792

        log.info("Waiting until server becomes ACTIVE")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
793
794
        self._insist_on_status_transition(
            "STOPPED", "ACTIVE", self.action_timeout, self.action_timeout)
795

John Giannelos's avatar
John Giannelos committed
796
797
    def test_011a_server_responds_to_ping_IPv4(self):
        """Test server OS is actually up and running again"""
798

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

John Giannelos's avatar
John Giannelos committed
801
        self.test_006_server_responds_to_ping_IPv4()
802

John Giannelos's avatar
John Giannelos committed
803
804
    def test_012_ssh_to_server_IPv4(self):
        """Test SSH to server public IPv4 works, verify hostname"""
805

John Giannelos's avatar
John Giannelos committed
806
807
808
809
        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)
810

John Giannelos's avatar
John Giannelos committed
811
812
813
814
    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")
815

John Giannelos's avatar
John Giannelos committed
816
817
818
        server = self.client.get_server_details(self.serverid)
        self._insist_on_ssh_hostname(self._get_ipv6(server),
                                     self.username, self.passwd)
819

John Giannelos's avatar
John Giannelos committed
820
821
822
823
824
    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
825
        sock = self._insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
826

John Giannelos's avatar
John Giannelos committed
827
828
829
830
        # 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()
831

John Giannelos's avatar
John Giannelos committed
832
833
834
835
    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")
836

John Giannelos's avatar
John Giannelos committed
837
838
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
839
        sock = self._get_tcp_connection(socket.AF_INET6, ipv6, 3389)
840

John Giannelos's avatar
John Giannelos committed
841
842
843
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        sock.close()
844

John Giannelos's avatar
John Giannelos committed
845
846
847
    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
848
        self._skipIf(self.personality is None, "No personality file selected")
849

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

John Giannelos's avatar
John Giannelos committed
852
        server = self.client.get_server_details(self.serverid)
853

John Giannelos's avatar
John Giannelos committed
854
855
856
857
858
859
860
        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)
861

862
863
    def test_017_submit_delete_request(self):
        """Test submit request to delete server"""
864
865
866

        log.info("Deleting server")

867
868
869
870
        self.client.delete_server(self.serverid)

    def test_018_server_becomes_deleted(self):
        """Test server becomes DELETED"""
871
872
873

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
874
875
        self._insist_on_status_transition(
            "ACTIVE", "DELETED", self.action_timeout, self.action_timeout)
876
877
878

    def test_019_server_no_longer_in_server_list(self):
        """Test server is no longer in server list"""
879
880
881

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

882
        servers = self.client.list_servers()
883
884
885
        self.assertNotIn(self.serverid, [s["id"] for s in servers])


886
class NetworkTestCase(unittest.TestCase):
John Giannelos's avatar
John Giannelos committed
887
    """ Testing networking in cyclades """
888

889
    @classmethod
John Giannelos's avatar
John Giannelos committed
890
891
    def setUpClass(cls):
        "Initialize kamaki, get list of current networks"
John Giannelos's avatar
John Giannelos committed
892
893
894

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

896
897
898
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
                                          TEST_RUN_ID,
                                          cls.imagename)
899
900
901
902
903

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

906
907
        cls.result_dict = dict()

908
909
910
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)
911

912
913
914
    def _get_ipv4(self, server):
        """Get the public IPv4 of a server from the detailed server info"""

915
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
916

917
918
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
919
            if self.client.get_network_details(net_id)["public"]:
920
                public_addrs = nic["ipv4"]
John Giannelos's avatar
John Giannelos committed
921

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
924
        return public_addrs
925

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
926
    def _connect_loginname(self, os_value):
927
        """Return the login name for connections based on the server OS"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
928
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
929
            return "user"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
930
        elif os_value in ("windows", "windows_alpha1"):
931
932
933
934
            return "Administrator"
        else:
            return "root"

John Giannelos's avatar
John Giannelos committed
935
    def _ping_once(self, ip):
936

John Giannelos's avatar
John Giannelos committed
937
938
939
940
941
942
        """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()
943

John Giannelos's avatar
John Giannelos committed
944
945
        return (ret == 0)

John Giannelos's avatar
John Giannelos committed
946
    def test_00001a_submit_create_server_A(self):
947
        """Test submit create server request"""
948
949
950

        log.info("Creating test server A")

951
        serverA = self.client.create_server(self.servername, self.flavorid,
952
                                            self.imageid, personality=None)
953

John Giannelos's avatar
John Giannelos committed
954
955
956
957
        self.assertEqual(serverA["name"], self.servername)
        self.assertEqual(serverA["flavorRef"], self.flavorid)
        self.assertEqual(serverA["imageRef"], self.imageid)
        self.assertEqual(serverA["status"], "BUILD")
958
959

        # Update class attributes to reflect data on building server
John Giannelos's avatar
John Giannelos committed
960
961
962
        self.serverid['A'] = serverA["id"]
        self.username['A'] = None
        self.password['A'] = serverA["adminPass"]
963

964
965
        log.info("Server A id:" + str(serverA["id"]))
        log.info("Server password " + (self.password['A']))
966

967
968
        self.result_dict["Server A ID"] = str(serverA["id"])
        self.result_dict["Server A password"] = serverA["adminPass"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
969

John Giannelos's avatar
John Giannelos committed
970
    def test_00001b_serverA_becomes_active(self):
971
        """Test server becomes ACTIVE"""
972

973
        log.info("Waiting until test server A becomes ACTIVE")
974
        self.result_dict.clear()
975

976
        fail_tmout = time.time() + self.action_timeout
977
978
979
980
981
982
983
984
985
986
987
988
989
        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
990
    def test_00002a_submit_create_server_B(self):
John Giannelos's avatar
John Giannelos committed
991
        """Test submit create server request"""
992

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

John Giannelos's avatar
John Giannelos committed
995
        serverB = self.client.create_server(self.servername, self.flavorid,
996
997
                                            self.imageid, personality=None)

John Giannelos's avatar
John Giannelos committed
998
999
1000
1001
1002
1003
1004
1005
1006
1007
        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"]

1008
1009
        log.info("Server B id: " + str(serverB["id"]))
        log.info("Password " + (self.password['B']))
1010

1011
1012
1013
1014
        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
1015
    def test_00002b_serverB_becomes_active(self):
1016
1017
        """Test server becomes ACTIVE"""

1018
        log.info("Waiting until test server B becomes ACTIVE")
1019
        self.result_dict.clear()
1020

1021
        fail_tmout = time.time() + self.action_timeout
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
        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
1034

1035
    def test_001_create_network(self):
John Giannelos's avatar
John Giannelos committed
1036
        """Test submit create network request"""
1037
1038

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

1041
        name = SNF_TEST_PREFIX + TEST_RUN_ID
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1042
1043
        #previous_num = len(self.client.list_networks())
        network = self.client.create_network(name, cidr='10.0.0.1/28')
1044

1045
        #Test if right name is assigned
John Giannelos's avatar
John Giannelos committed
1046
        self.assertEqual(network['name'], name)
1047

1048
        # Update class attributes
John Giannelos's avatar
John Giannelos committed
1049
1050
        cls = type(self)
        cls.networkid = network['id']
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1051
        #networks = self.client.list_networks()
1052

John Giannelos's avatar
John Giannelos committed
1053
1054
        fail_tmout = time.time() + self.action_timeout

1055
        #Test if new network is created
John Giannelos's avatar
John Giannelos committed
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
        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)
1068

1069
1070
        self.result_dict["Private network ID"] = str(network['id'])

1071
    def test_002_connect_to_network(self):
1072
        """Test connect VMs to network"""
1073

1074
        log.info("Connect VMs to private network")
John Giannelos's avatar