burnin.py 78.5 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
62
from kamaki.clients.astakos import AstakosClient
John Giannelos's avatar
John Giannelos committed
63
from kamaki.clients import ClientError
John Giannelos's avatar
John Giannelos committed
64

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

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

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

98

99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# --------------------------------------------------------------------
# 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


119
120
121
122
123
124
def _get_user_id():
    """Authenticate to astakos and get unique users id"""
    astakos = AstakosClient(ASTAKOS, TOKEN)
    return astakos.authenticate()['uuid']


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
125
126
# --------------------------------------------------------------------
# BurninTestReulst class
127
128
129
130
class BurninTestResult(unittest.TextTestResult):
    def addSuccess(self, test):
        super(BurninTestResult, self).addSuccess(test)
        if self.showAll:
John Giannelos's avatar
John Giannelos committed
131
            if hasattr(test, 'result_dict'):
132
133
134
135
136
137
138
139
140
                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
141
142
            self.stream.flush()

143
144
145
146
    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
147
148
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
149

John Giannelos's avatar
John Giannelos committed
150
151
152
153
                self.stream.write("\n")
                for i in run_details:
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
                self.stream.write("\n")
154
155
156
157
158
159
160
161
162

        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
163
164
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
165

John Giannelos's avatar
John Giannelos committed
166
167
168
169
                self.stream.write("\n")
                for i in run_details:
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
                self.stream.write("\n")
170
171
172
173
174
175

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


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
176
177
# --------------------------------------------------------------------
# Format Results
John Giannelos's avatar
John Giannelos committed
178
179
class burninFormatter(logging.Formatter):
    err_fmt = red + "ERROR: %(msg)s" + normal
John Giannelos's avatar
John Giannelos committed
180
    dbg_fmt = green + "* %(msg)s" + normal
John Giannelos's avatar
John Giannelos committed
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
    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

199
log = logging.getLogger("burnin")
John Giannelos's avatar
John Giannelos committed
200
201
202
203
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(burninFormatter())
log.addHandler(handler)
204

205

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
206
207
# --------------------------------------------------------------------
# UnauthorizedTestCase class
208
class UnauthorizedTestCase(unittest.TestCase):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
209
    """Test unauthorized access"""
210
211
212
213
    @classmethod
    def setUpClass(cls):
        cls.result_dict = dict()

214
215
    def test_unauthorized_access(self):
        """Test access without a valid token fails"""
216
        log.info("Authentication test")
217
        falseToken = '12345'
218
        c = ComputeClient(API, falseToken)
219

220
221
        with self.assertRaises(ClientError) as cm:
            c.list_servers()
John Giannelos's avatar
John Giannelos committed
222
            self.assertEqual(cm.exception.status, 401)
223
224


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
225
# --------------------------------------------------------------------
226
# This class gest replicated into Images TestCases dynamically
227
228
229
230
231
232
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
233
        cls.client = ComputeClient(API, TOKEN)
234
235
236
        cls.plankton = ImageClient(PLANKTON, TOKEN)
        cls.images = cls.plankton.list_public()
        cls.dimages = cls.plankton.list_public(detail=True)
237
        cls.result_dict = dict()
238
239
240
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
241
242
243
244
245
246
        # Create temp directory and store it inside our class
        # XXX: In my machine /tmp has not enough space
        #      so use current directory to be sure.
        cls.temp_dir = tempfile.mkdtemp(dir=os.getcwd())
        cls.temp_image_name = \
            SNF_TEST_PREFIX + cls.imageid + ".diskdump"
247

248
249
250
251
252
253
254
255
256
257
258
259
260
    @classmethod
    def tearDownClass(cls):
        """Remove local files"""
        try:
            temp_file = os.path.join(cls.temp_dir, cls.temp_image_name)
            os.unlink(temp_file)
        except:
            pass
        try:
            os.rmdir(cls.temp_dir)
        except:
            pass

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

265
266
    def test_002_list_images_detailed(self):
        """Test detailed image list is the same length as list"""
John Giannelos's avatar
John Giannelos committed
267
        self.assertEqual(len(self.dimages), len(self.images))
268

269
270
271
272
273
274
275
    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):
276
277
278
279
        """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))
280
281
282
283
        self.assertEqual(sorted(list(set(names))), names)

    def test_005_image_metadata(self):
        """Test every image has specific metadata defined"""
284
        keys = frozenset(["osfamily", "root_partition"])
John Giannelos's avatar
John Giannelos committed
285
286
        details = self.client.list_images(detail=True)
        for i in details:
287
288
            self.assertTrue(keys.issubset(i["metadata"]["values"].keys()))

289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
    def test_006_download_image(self):
        """Download image from pithos+"""
        # Get image location
        image = filter(
            lambda x: x['id'] == self.imageid, self.dimages)[0]
        image_location = \
            image['location'].replace("://", " ").replace("/", " ").split()
        log.info("Download image, with owner %s\n\tcontainer %s, and name %s"
                 % (image_location[1], image_location[2], image_location[3]))
        pithos_client = PithosClient(PITHOS, TOKEN, image_location[1])
        pithos_client.container = image_location[2]
        temp_file = os.path.join(self.temp_dir, self.temp_image_name)
        with open(temp_file, "wb+") as f:
            pithos_client.download_object(image_location[3], f)

    def test_007_upload_image(self):
        """Upload and register image"""
        temp_file = os.path.join(self.temp_dir, self.temp_image_name)
        log.info("Upload image to pithos+")
        # Create container `images'
309
        pithos_client = PithosClient(PITHOS, TOKEN, self.uuid)
310
311
312
313
314
        pithos_client.container = "images"
        pithos_client.container_put()
        with open(temp_file, "rb+") as f:
            pithos_client.upload_object(self.temp_image_name, f)
        log.info("Register image to plankton")
315
        location = "pithos://" + self.uuid + \
316
317
318
319
320
            "/images/" + self.temp_image_name
        params = {'is_public': True}
        properties = {'OSFAMILY': "linux", 'ROOT_PARTITION': 1}
        self.plankton.register(self.temp_image_name, location,
                               params, properties)
321
322
        # Get image id
        details = self.plankton.list_public(detail=True)
323
        detail = filter(lambda x: x['location'] == location, details)
324
325
326
327
328
329
330
331
332
        self.assertEqual(len(detail), 1)
        cls = type(self)
        cls.temp_image_id = detail[0]['id']
        log.info("Image registered with id %s" % detail[0]['id'])

    def test_008_cleanup_image(self):
        """Cleanup image test"""
        log.info("Cleanup image test")
        # Remove image from pithos+
333
        pithos_client = PithosClient(PITHOS, TOKEN, self.uuid)
334
335
        pithos_client.container = "images"
        pithos_client.del_object(self.temp_image_name)
336

337

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
338
339
# --------------------------------------------------------------------
# FlavorsTestCase class
340
341
342
343
344
345
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
346
        cls.client = ComputeClient(API, TOKEN)
347
348
        cls.flavors = cls.client.list_flavors()
        cls.dflavors = cls.client.list_flavors(detail=True)
349
        cls.result_dict = dict()
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

    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
380
381
# --------------------------------------------------------------------
# ServersTestCase class
382
383
384
385
386
387
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")
388

John Giannelos's avatar
John Giannelos committed
389
        cls.client = ComputeClient(API, TOKEN)
390
391
        cls.servers = cls.client.list_servers()
        cls.dservers = cls.client.list_servers(detail=True)
392
        cls.result_dict = dict()
393

394
395
396
    # def test_001_list_servers(self):
    #     """Test server list actually returns servers"""
    #     self.assertGreater(len(self.servers), 0)
397
398
399
400
401
402
403
404
405
406
407
408

    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
409
410
411
412
413
414
415
# --------------------------------------------------------------------
# Pithos Test Cases
class PithosTestCase(unittest.TestCase):
    """Test pithos functionality"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get list of containers"""
416
417
418
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
419
        log.info("Getting list of containers")
420
        cls.client = PithosClient(PITHOS, TOKEN, cls.uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
        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)


# --------------------------------------------------------------------
487
488
489
490
491
492
# 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
493
        log.info("Spawning server for image `%s'" % cls.imagename)
John Giannelos's avatar
John Giannelos committed
494
495
        cls.client = ComputeClient(API, TOKEN)
        cls.cyclades = CycladesClient(API, TOKEN)
496
        cls.result_dict = dict()
497
498

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

501
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
502

503
504
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
505
            if self.cyclades.get_network_details(net_id)["public"]:
506
                public_addrs = nic["ipv4"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
507
508

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

510
        return public_addrs
511
512

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

515
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
516

517
518
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
519
            if self.cyclades.get_network_details(net_id)["public"]:
520
                public_addrs = nic["ipv6"]
John Giannelos's avatar
John Giannelos committed
521

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
524
        return public_addrs
525

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
526
    def _connect_loginname(self, os_value):
527
        """Return the login name for connections based on the server OS"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
528
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
529
            return "user"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
530
        elif os_value in ("windows", "windows_alpha1"):
531
            return "Administrator"
532
        else:
533
            return "root"
534
535
536
537

    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)
538
539
        if server["status"] not in (current_status, new_status):
            return None  # Do not raise exception, return so the test fails
540
541
542
543
544
545
546
547
548
549
550
        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
551
            except socket.error:
552
553
554
555
                sock = None
                continue
            try:
                sock.connect(sa)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
556
            except socket.error:
557
558
559
560
561
562
563
564
565
566
567
568
569
570
                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)
571

572
    def _get_hostname_over_ssh(self, hostip, username, password):
573
574
        lines, status = _ssh_execute(
            hostip, username, password, "hostname")
575
        self.assertEqual(len(lines), 1)
576
        return lines[0]
577
578
579
580

    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
                                   opmsg, callable, *args, **kwargs):
        if warn_timeout == fail_timeout:
581
582
583
584
            warn_timeout = fail_timeout + 1
        warn_tmout = time.time() + warn_timeout
        fail_tmout = time.time() + fail_timeout
        while True:
585
            self.assertLess(time.time(), fail_tmout,
586
                            "operation `%s' timed out" % opmsg)
587
            if time.time() > warn_tmout:
588
589
                log.warning("Server %d: `%s' operation `%s' not done yet",
                            self.serverid, self.servername, opmsg)
590
            try:
591
                log.info("%s... " % opmsg)
592
593
594
                return callable(*args, **kwargs)
            except AssertionError:
                pass
595
596
            time.sleep(self.query_interval)

597
    def _insist_on_tcp_connection(self, family, host, port):
598
599
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
600
601
602
        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
603
604
605
            self.action_timeout, self.action_timeout,
            msg, self._get_connected_tcp_socket,
            family, host, port)
606
607
608
        return sock

    def _insist_on_status_transition(self, current_status, new_status,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
609
                                     fail_timeout, warn_timeout=None):
610
611
        msg = "Server %d: `%s', waiting for %s -> %s" % \
              (self.serverid, self.servername, current_status, new_status)
612
613
614
615
616
        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)
617
618
619
        # Ensure the status is actually the expected one
        server = self.client.get_server_details(self.serverid)
        self.assertEquals(server["status"], new_status)
620
621

    def _insist_on_ssh_hostname(self, hostip, username, password):
622
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
623
        hostname = self._try_until_timeout_expires(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
624
625
626
            self.action_timeout, self.action_timeout,
            msg, self._get_hostname_over_ssh,
            hostip, username, password)
627
628
629

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

631
632
633
634
    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)
635
        log.info(msg)
636
637
638
639
        try:
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect(hostip, username=username, password=password)
640
            ssh.close()
641
642
        except socket.error:
            raise AssertionError
643

644
645
646
647
        transport = paramiko.Transport((hostip, 22))
        transport.connect(username=username, password=password)

        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
648
        sftp = paramiko.SFTPClient.from_transport(transport)
649
        sftp.get(remotepath, localpath)
650
651
652
        sftp.close()
        transport.close()

653
654
655
        f = open(localpath)
        remote_content = b64encode(f.read())

656
        # Check if files are the same
657
        return (remote_content == content)
658

659
660
661
662
663
664
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)

    def test_001_submit_create_server(self):
        """Test submit create server request"""
665
666

        log.info("Submit new server request")
667
668
        server = self.client.create_server(self.servername, self.flavorid,
                                           self.imageid, self.personality)
669

670
671
        log.info("Server id: " + str(server["id"]))
        log.info("Server password: " + server["adminPass"])
672
673
674
675
676
677
678
679
        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"]
680
        cls.username = None
681
682
        cls.passwd = server["adminPass"]

683
684
685
        self.result_dict["Server ID"] = str(server["id"])
        self.result_dict["Password"] = str(server["adminPass"])

686
687
    def test_002a_server_is_building_in_list(self):
        """Test server is in BUILD state, in server list"""
688
689
        log.info("Server in BUILD state in server list")

690
691
        self.result_dict.clear()

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

695
696
697
698
699
700
701
702
        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"""
703
704
705

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

706
707
708
709
710
711
712
        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):
713
714
715

        log.info("Creating server metadata")

716
        image = self.client.get_image_details(self.imageid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
717
        os_value = image["metadata"]["values"]["os"]
718
        users = image["metadata"]["values"].get("users", None)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
719
        self.client.update_server_metadata(self.serverid, OS=os_value)
John Giannelos's avatar
John Giannelos committed
720

721
        userlist = users.split()
722

723
724
725
        # Determine the username to use for future connections
        # to this host
        cls = type(self)
726
727
728

        if "root" in userlist:
            cls.username = "root"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
729
730
        elif users is None:
            cls.username = self._connect_loginname(os_value)
731
732
733
        else:
            cls.username = choice(userlist)

734
        self.assertIsNotNone(cls.username)
735
736
737

    def test_002d_verify_server_metadata(self):
        """Test server metadata keys are set based on image metadata"""
738
739
740

        log.info("Verifying image metadata")

741
742
        servermeta = self.client.get_server_metadata(self.serverid)
        imagemeta = self.client.get_image_metadata(self.imageid)
743

John Giannelos's avatar
John Giannelos committed
744
        self.assertEqual(servermeta["OS"], imagemeta["os"])
745
746
747

    def test_003_server_becomes_active(self):
        """Test server becomes ACTIVE"""
748
749
750

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
751
752
        self._insist_on_status_transition(
            "BUILD", "ACTIVE", self.build_fail, self.build_warning)
753

John Giannelos's avatar
John Giannelos committed
754
755
    def test_003a_get_server_oob_console(self):
        """Test getting OOB server console over VNC
756

John Giannelos's avatar
John Giannelos committed
757
758
        Implementation of RFB protocol follows
        http://www.realvnc.com/docs/rfbproto.pdf.
759

John Giannelos's avatar
John Giannelos committed
760
761
762
        """
        console = self.cyclades.get_server_console(self.serverid)
        self.assertEquals(console['type'], "vnc")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
763
764
        sock = self._insist_on_tcp_connection(
            socket.AF_INET, console["host"], console["port"])
John Giannelos's avatar
John Giannelos committed
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790

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

792
793
    def test_004_server_has_ipv4(self):
        """Test active server has a valid IPv4 address"""
794

795
        log.info("Validate server's IPv4")
796

797
798
        server = self.client.get_server_details(self.serverid)
        ipv4 = self._get_ipv4(server)
799
800
801
802

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

803
804
        self.assertEquals(IP(ipv4).version(), 4)

805
806
    def test_005_server_has_ipv6(self):
        """Test active server has a valid IPv6 address"""
807
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
808

809
        log.info("Validate server's IPv6")
810

811
812
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
813
814
815
816

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

817
        self.assertEquals(IP(ipv6).version(), 6)
818

John Giannelos's avatar
John Giannelos committed
819
820
821
822
    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")
823
        self.result_dict.clear()
John Giannelos's avatar
John Giannelos committed
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844

        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)
845
846
847

    def test_008_submit_shutdown_request(self):
        """Test submit request to shutdown server"""
848
849
850

        log.info("Shutting down server")

851
        self.cyclades.shutdown_server(self.serverid)
852
853
854

    def test_009_server_becomes_stopped(self):
        """Test server becomes STOPPED"""
855
856

        log.info("Waiting until server becomes STOPPED")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
857
858
        self._insist_on_status_transition(
            "ACTIVE", "STOPPED", self.action_timeout, self.action_timeout)
859
860
861

    def test_010_submit_start_request(self):
        """Test submit start server request"""
862
863
864

        log.info("Starting server")

865
        self.cyclades.start_server(self.serverid)
866
867
868

    def test_011_server_becomes_active(self):
        """Test server becomes ACTIVE again"""
869
870

        log.info("Waiting until server becomes ACTIVE")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
871
872
        self._insist_on_status_transition(
            "STOPPED", "ACTIVE", self.action_timeout, self.action_timeout)
873

John Giannelos's avatar
John Giannelos committed
874
875
    def test_011a_server_responds_to_ping_IPv4(self):
        """Test server OS is actually up and running again"""
876

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

John Giannelos's avatar
John Giannelos committed
879
        self.test_006_server_responds_to_ping_IPv4()
880

John Giannelos's avatar
John Giannelos committed
881
882
    def test_012_ssh_to_server_IPv4(self):
        """Test SSH to server public IPv4 works, verify hostname"""
883

John Giannelos's avatar
John Giannelos committed
884
885
886
887
        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)
888

John Giannelos's avatar
John Giannelos committed
889
890
891
892
    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")
893

John Giannelos's avatar
John Giannelos committed
894
895
896
        server = self.client.get_server_details(self.serverid)
        self._insist_on_ssh_hostname(self._get_ipv6(server),
                                     self.username, self.passwd)
897

John Giannelos's avatar
John Giannelos committed
898
899
900
901
902
    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
903
        sock = self._insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
904

John Giannelos's avatar
John Giannelos committed
905
906
907
908
        # 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()
909

John Giannelos's avatar
John Giannelos committed
910
911
912
913
    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")
914

John Giannelos's avatar
John Giannelos committed
915
916
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
917
        sock = self._get_tcp_connection(socket.AF_INET6, ipv6, 3389)
918

John Giannelos's avatar
John Giannelos committed
919
920
921
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        sock.close()
922

John Giannelos's avatar
John Giannelos committed
923
924
925
    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
926
        self._skipIf(self.personality is None, "No personality file selected")
927

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

John Giannelos's avatar
John Giannelos committed
930
        server = self.client.get_server_details(self.serverid)
931

John Giannelos's avatar
John Giannelos committed
932
933
934
935
936
937
938
        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)
939

940
941
    def test_017_submit_delete_request(self):
        """Test submit request to delete server"""
942
943
944

        log.info("Deleting server")

945
946
947
948
        self.client.delete_server(self.serverid)

    def test_018_server_becomes_deleted(self):
        """Test server becomes DELETED"""
949
950
951

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
952
953
        self._insist_on_status_transition(
            "ACTIVE", "DELETED", self.action_timeout, self.action_timeout)
954
955
956

    def test_019_server_no_longer_in_server_list(self):
        """Test server is no longer in server list"""
957
958
959

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

960
        servers = self.client.list_servers()
961
962
963
        self.assertNotIn(self.serverid, [s["id"] for s in servers])


964
class NetworkTestCase(unittest.TestCase):
John Giannelos's avatar
John Giannelos committed
965
    """ Testing networking in cyclades """
966

967
    @classmethod
John Giannelos's avatar
John Giannelos committed
968
969
    def setUpClass(cls):
        "Initialize kamaki, get list of current networks"
John Giannelos's avatar
John Giannelos committed
970
971
972

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

974
975
976
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
                                          TEST_RUN_ID,
                                          cls.imagename)
977
978
979
980
981

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

984
985
        cls.result_dict = dict()

986
987
988
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)
989

990
991
992
    def _get_ipv4(self, server):
        """Get the public IPv4 of a server from the detailed server info"""

993
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
994

995
996
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
997
            if self.client.get_network_details(net_id)["public"]:
998
                public_addrs = nic["ipv4"]
John Giannelos's avatar
John Giannelos committed
999

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1002
        return public_addrs
1003

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1004
    def _connect_loginname(self, os_value):
1005
        """Return the login name for connections based on the server OS"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1006
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
1007
            return "user"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1008
        elif os_value in ("windows", "windows_alpha1"):
1009
1010
1011
1012
            return "Administrator"
        else:
            return "root"

John Giannelos's avatar
John Giannelos committed
1013
    def _ping_once(self, ip):
1014

John Giannelos's avatar
John Giannelos committed
1015
1016
1017
1018
1019
1020
        """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()
1021

John Giannelos's avatar
John Giannelos committed
1022
1023
        return (ret == 0)

John Giannelos's avatar
John Giannelos committed
1024
    def test_00001a_submit_create_server_A(self):
1025
        """Test submit create server request"""
1026
1027
1028

        log.info("Creating test server A")

1029
        serverA = self.client.create_server(self.servername, self.flavorid,
1030
                                            self.imageid, personality=None)
1031

John Giannelos's avatar
John Giannelos committed
1032
1033
1034
1035
        self.assertEqual(serverA["name"], self.servername)
        self.assertEqual(serverA["flavorRef"], self.flavorid)
        self.assertEqual(serverA["imageRef"], self.imageid)
        self.assertEqual(serverA["status"], "BUILD")
1036
1037

        # Update class attributes to reflect data on building server
John Giannelos's avatar
John Giannelos committed
1038
1039
1040
        self.serverid['A'] = serverA["id"]
        self.username['A'] = None
        self.password['A'] = serverA["adminPass"]
1041

1042
1043
        log.info("Server A id:" + str(serverA["id"]))
        log.info("Server password " + (self.password['A']))
1044

1045
1046
        self.result_dict["Server A ID"] = str(serverA["id"])
        self.result_dict["Server A password"] = serverA["adminPass"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1047

John Giannelos's avatar
John Giannelos committed
1048
    def test_00001b_serverA_becomes_active(self):
1049
        """Test server becomes ACTIVE"""
1050

1051
        log.info("Waiting until test server A becomes ACTIVE")
1052
        self.result_dict.clear()
1053

1054
        fail_tmout = time.time() + self.action_timeout
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
        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
1068
    def test_00002a_submit_create_server_B(self):
John Giannelos's avatar
John Giannelos committed
1069
        """Test submit create server request"""
1070

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

John Giannelos's avatar
John Giannelos committed
1073
        serverB = self.client.create_server(self.servername, self.flavorid,
1074
1075
                                            self.imageid, personality=None)

John Giannelos's avatar
John Giannelos committed
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
        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"]

1086
1087
        log.info("Server B id: " + str(serverB["id"]))
        log.info("Password " + (self.password['B']))
1088