burnin.py 78.7 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
def _get_user_id():
    """Authenticate to astakos and get unique users id"""
    astakos = AstakosClient(ASTAKOS, TOKEN)
122
123
124
125
126
    authenticate = astakos.authenticate()
    if 'uuid' in authenticate:
        return authenticate['uuid']
    else:
        return authenticate['uniq']
127
128


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

147
148
149
150
    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
151
152
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
153

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

        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
167
168
            if hasattr(test, 'result_dict'):
                run_details = test.result_dict
169

John Giannelos's avatar
John Giannelos committed
170
171
172
173
                self.stream.write("\n")
                for i in run_details:
                    self.stream.write("%s : %s \n" % (i, run_details[i]))
                self.stream.write("\n")
174
175
176
177
178
179

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


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

203
log = logging.getLogger("burnin")
John Giannelos's avatar
John Giannelos committed
204
205
206
207
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setFormatter(burninFormatter())
log.addHandler(handler)
208

209

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

218
219
    def test_unauthorized_access(self):
        """Test access without a valid token fails"""
220
        log.info("Authentication test")
221
        falseToken = '12345'
222
        c = ComputeClient(API, falseToken)
223

224
225
        with self.assertRaises(ClientError) as cm:
            c.list_servers()
John Giannelos's avatar
John Giannelos committed
226
            self.assertEqual(cm.exception.status, 401)
227
228


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
229
# --------------------------------------------------------------------
230
# This class gest replicated into Images TestCases dynamically
231
232
233
234
235
236
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
237
        cls.client = ComputeClient(API, TOKEN)
238
239
240
        cls.plankton = ImageClient(PLANKTON, TOKEN)
        cls.images = cls.plankton.list_public()
        cls.dimages = cls.plankton.list_public(detail=True)
241
        cls.result_dict = dict()
242
243
244
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
245
246
247
248
249
250
        # 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"
251

252
253
254
255
256
257
258
259
260
261
262
263
264
    @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

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

269
270
    def test_002_list_images_detailed(self):
        """Test detailed image list is the same length as list"""
John Giannelos's avatar
John Giannelos committed
271
        self.assertEqual(len(self.dimages), len(self.images))
272

273
274
275
276
277
278
279
    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):
280
281
282
283
        """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))
284
285
286
287
        self.assertEqual(sorted(list(set(names))), names)

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

293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
    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'
313
        pithos_client = PithosClient(PITHOS, TOKEN, self.uuid)
314
315
316
317
318
        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")
319
        location = "pithos://" + self.uuid + \
320
321
322
323
324
            "/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)
325
326
        # Get image id
        details = self.plankton.list_public(detail=True)
327
        detail = filter(lambda x: x['location'] == location, details)
328
329
330
331
332
333
334
335
336
        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+
337
        pithos_client = PithosClient(PITHOS, TOKEN, self.uuid)
338
339
        pithos_client.container = "images"
        pithos_client.del_object(self.temp_image_name)
340

341

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

    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
384
385
# --------------------------------------------------------------------
# ServersTestCase class
386
387
388
389
390
391
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")
392

John Giannelos's avatar
John Giannelos committed
393
        cls.client = ComputeClient(API, TOKEN)
394
395
        cls.servers = cls.client.list_servers()
        cls.dservers = cls.client.list_servers(detail=True)
396
        cls.result_dict = dict()
397

398
399
400
    # def test_001_list_servers(self):
    #     """Test server list actually returns servers"""
    #     self.assertGreater(len(self.servers), 0)
401
402
403
404
405
406
407
408
409
410
411
412

    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
413
414
415
416
417
418
419
# --------------------------------------------------------------------
# Pithos Test Cases
class PithosTestCase(unittest.TestCase):
    """Test pithos functionality"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get list of containers"""
420
421
422
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
423
        log.info("Getting list of containers")
424
        cls.client = PithosClient(PITHOS, TOKEN, cls.uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
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
487
488
489
490
        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)


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

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

505
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
506

507
508
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
509
            if self.cyclades.get_network_details(net_id)["public"]:
510
                public_addrs = nic["ipv4"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
511
512

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

514
        return public_addrs
515
516

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

519
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
520

521
522
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
523
            if self.cyclades.get_network_details(net_id)["public"]:
524
                public_addrs = nic["ipv6"]
John Giannelos's avatar
John Giannelos committed
525

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
528
        return public_addrs
529

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

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

576
    def _get_hostname_over_ssh(self, hostip, username, password):
577
578
        lines, status = _ssh_execute(
            hostip, username, password, "hostname")
579
        self.assertEqual(len(lines), 1)
580
        return lines[0]
581
582
583
584

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

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

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

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

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

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

648
649
650
651
        transport = paramiko.Transport((hostip, 22))
        transport.connect(username=username, password=password)

        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
652
        sftp = paramiko.SFTPClient.from_transport(transport)
653
        sftp.get(remotepath, localpath)
654
655
656
        sftp.close()
        transport.close()

657
658
659
        f = open(localpath)
        remote_content = b64encode(f.read())

660
        # Check if files are the same
661
        return (remote_content == content)
662

663
664
665
666
667
668
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)

    def test_001_submit_create_server(self):
        """Test submit create server request"""
669
670

        log.info("Submit new server request")
671
672
        server = self.client.create_server(self.servername, self.flavorid,
                                           self.imageid, self.personality)
673

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

687
688
689
        self.result_dict["Server ID"] = str(server["id"])
        self.result_dict["Password"] = str(server["adminPass"])

690
691
    def test_002a_server_is_building_in_list(self):
        """Test server is in BUILD state, in server list"""
692
693
        log.info("Server in BUILD state in server list")

694
695
        self.result_dict.clear()

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

699
700
701
702
703
704
705
706
        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"""
707
708
709

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

710
711
712
713
714
715
716
        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):
717
718
719

        log.info("Creating server metadata")

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

725
        userlist = users.split()
726

727
728
729
        # Determine the username to use for future connections
        # to this host
        cls = type(self)
730
731
732

        if "root" in userlist:
            cls.username = "root"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
733
734
        elif users is None:
            cls.username = self._connect_loginname(os_value)
735
736
737
        else:
            cls.username = choice(userlist)

738
        self.assertIsNotNone(cls.username)
739
740
741

    def test_002d_verify_server_metadata(self):
        """Test server metadata keys are set based on image metadata"""
742
743
744

        log.info("Verifying image metadata")

745
746
        servermeta = self.client.get_server_metadata(self.serverid)
        imagemeta = self.client.get_image_metadata(self.imageid)
747

John Giannelos's avatar
John Giannelos committed
748
        self.assertEqual(servermeta["OS"], imagemeta["os"])
749
750
751

    def test_003_server_becomes_active(self):
        """Test server becomes ACTIVE"""
752
753
754

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
755
756
        self._insist_on_status_transition(
            "BUILD", "ACTIVE", self.build_fail, self.build_warning)
757

John Giannelos's avatar
John Giannelos committed
758
759
    def test_003a_get_server_oob_console(self):
        """Test getting OOB server console over VNC
760

John Giannelos's avatar
John Giannelos committed
761
762
        Implementation of RFB protocol follows
        http://www.realvnc.com/docs/rfbproto.pdf.
763

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

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

796
797
    def test_004_server_has_ipv4(self):
        """Test active server has a valid IPv4 address"""
798

799
        log.info("Validate server's IPv4")
800

801
802
        server = self.client.get_server_details(self.serverid)
        ipv4 = self._get_ipv4(server)
803
804
805
806

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

807
808
        self.assertEquals(IP(ipv4).version(), 4)

809
810
    def test_005_server_has_ipv6(self):
        """Test active server has a valid IPv6 address"""
811
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
812

813
        log.info("Validate server's IPv6")
814

815
816
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
817
818
819
820

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

821
        self.assertEquals(IP(ipv6).version(), 6)
822

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

        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)
849
850
851

    def test_008_submit_shutdown_request(self):
        """Test submit request to shutdown server"""
852
853
854

        log.info("Shutting down server")

855
        self.cyclades.shutdown_server(self.serverid)
856
857
858

    def test_009_server_becomes_stopped(self):
        """Test server becomes STOPPED"""
859
860

        log.info("Waiting until server becomes STOPPED")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
861
862
        self._insist_on_status_transition(
            "ACTIVE", "STOPPED", self.action_timeout, self.action_timeout)
863
864
865

    def test_010_submit_start_request(self):
        """Test submit start server request"""
866
867
868

        log.info("Starting server")

869
        self.cyclades.start_server(self.serverid)
870
871
872

    def test_011_server_becomes_active(self):
        """Test server becomes ACTIVE again"""
873
874

        log.info("Waiting until server becomes ACTIVE")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
875
876
        self._insist_on_status_transition(
            "STOPPED", "ACTIVE", self.action_timeout, self.action_timeout)
877

John Giannelos's avatar
John Giannelos committed
878
879
    def test_011a_server_responds_to_ping_IPv4(self):
        """Test server OS is actually up and running again"""
880

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

John Giannelos's avatar
John Giannelos committed
883
        self.test_006_server_responds_to_ping_IPv4()
884

John Giannelos's avatar
John Giannelos committed
885
886
    def test_012_ssh_to_server_IPv4(self):
        """Test SSH to server public IPv4 works, verify hostname"""
887

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

John Giannelos's avatar
John Giannelos committed
893
894
895
896
    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")
897

John Giannelos's avatar
John Giannelos committed
898
899
900
        server = self.client.get_server_details(self.serverid)
        self._insist_on_ssh_hostname(self._get_ipv6(server),
                                     self.username, self.passwd)
901

John Giannelos's avatar
John Giannelos committed
902
903
904
905
906
    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
907
        sock = self._insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
908

John Giannelos's avatar
John Giannelos committed
909
910
911
912
        # 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()
913

John Giannelos's avatar
John Giannelos committed
914
915
916
917
    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")
918

John Giannelos's avatar
John Giannelos committed
919
920
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
921
        sock = self._get_tcp_connection(socket.AF_INET6, ipv6, 3389)
922

John Giannelos's avatar
John Giannelos committed
923
924
925
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        sock.close()
926

John Giannelos's avatar
John Giannelos committed
927
928
929
    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
930
        self._skipIf(self.personality is None, "No personality file selected")
931

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

John Giannelos's avatar
John Giannelos committed
934
        server = self.client.get_server_details(self.serverid)
935

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

944
945
    def test_017_submit_delete_request(self):
        """Test submit request to delete server"""
946
947
948

        log.info("Deleting server")

949
950
951
952
        self.client.delete_server(self.serverid)

    def test_018_server_becomes_deleted(self):
        """Test server becomes DELETED"""
953
954
955

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
956
957
        self._insist_on_status_transition(
            "ACTIVE", "DELETED", self.action_timeout, self.action_timeout)
958
959
960

    def test_019_server_no_longer_in_server_list(self):
        """Test server is no longer in server list"""
961
962
963

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

964
        servers = self.client.list_servers()
965
966
967
        self.assertNotIn(self.serverid, [s["id"] for s in servers])


968
class NetworkTestCase(unittest.TestCase):
John Giannelos's avatar
John Giannelos committed
969
    """ Testing networking in cyclades """
970

971
    @classmethod
John Giannelos's avatar
John Giannelos committed
972
973
    def setUpClass(cls):
        "Initialize kamaki, get list of current networks"
John Giannelos's avatar
John Giannelos committed
974
975
976

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

978
979
980
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
                                          TEST_RUN_ID,
                                          cls.imagename)
981
982
983
984
985

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

988
989
        cls.result_dict = dict()

990
991
992
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)
993

994
995
996
    def _get_ipv4(self, server):
        """Get the public IPv4 of a server from the detailed server info"""

997
        nics = server["attachments"]["values"]
John Giannelos's avatar
John Giannelos committed
998

999
1000
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1001
            if self.client.get_network_details(net_id)["public"]:
1002
                public_addrs = nic["ipv4"]
John Giannelos's avatar
John Giannelos committed
1003

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1006
        return public_addrs
1007

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

John Giannelos's avatar
John Giannelos committed
1017
    def _ping_once(self, ip):
1018

John Giannelos's avatar
John Giannelos committed
1019
1020
1021
1022
1023
1024
        """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()
1025

John Giannelos's avatar
John Giannelos committed
1026
1027
        return (ret == 0)

John Giannelos's avatar
John Giannelos committed
1028
    def test_00001a_submit_create_server_A(self):
1029
        """Test submit create server request"""
1030
1031
1032

        log.info("Creating test server A")

1033
        serverA = self.client.create_server(self.servername, self.flavorid,
1034
                                            self.imageid, personality=None)
1035

John Giannelos's avatar
John Giannelos committed
1036
1037
1038
1039
        self.assertEqual(serverA["name"], self.servername)
        self.assertEqual(serverA["flavorRef"], self.flavorid)
        self.assertEqual(serverA["imageRef"], self.imageid)
        self.assertEqual(serverA["status"], "BUILD")
1040
1041

        # Update class attributes to reflect data on building server
John Giannelos's avatar
John Giannelos committed
1042
1043
1044
        self.serverid['A'] = serverA["id"]
        self.username['A'] = None
        self.password['A'] = serverA["adminPass"]
1045

1046
1047
        log.info("Server A id:" + str(serverA["id"]))
        log.info("Server password " + (self.password['A']))
1048

1049
1050
        self.result_dict["Server A ID"] = str(serverA["id"])
        self.result_dict["Server A password"] = serverA["adminPass"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1051

John Giannelos's avatar
John Giannelos committed
1052
    def test_00001b_serverA_becomes_active(self):
1053
        """Test server becomes ACTIVE"""
1054

1055
        log.info("Waiting until test server A becomes ACTIVE")
1056
        self.result_dict.clear()
1057

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

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

John Giannelos's avatar
John Giannelos committed
1077
        serverB = self.client.create_server(self.servername, self.flavorid,
1078
1079
                                            self.imageid, personality=None)

John Giannelos's avatar
John Giannelos committed
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
        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"]