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
        cls.plankton = ImageClient(PLANKTON, TOKEN)
239
240
241
242
243
244
        cls.images = \
            filter(lambda x: not x['name'].startswith(SNF_TEST_PREFIX),
                   cls.plankton.list_public())
        cls.dimages = \
            filter(lambda x: not x['name'].startswith(SNF_TEST_PREFIX),
                   cls.plankton.list_public(detail=True))
245
        cls.result_dict = dict()
246
247
248
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
249
250
251
252
253
254
        # 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"
255

256
257
258
259
260
261
262
263
264
265
266
267
268
    @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

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

273
274
    def test_002_list_images_detailed(self):
        """Test detailed image list is the same length as list"""
John Giannelos's avatar
John Giannelos committed
275
        self.assertEqual(len(self.dimages), len(self.images))
276

277
278
279
280
281
282
283
    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):
284
285
286
287
        """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))
288
289
290
291
        self.assertEqual(sorted(list(set(names))), names)

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

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

345

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

    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:
383
            flavor = (f["cpu"], f["ram"], f["disk"], f["SNF:disk_template"])
Christos Stavrakakis's avatar
Christos Stavrakakis committed
384
            self.assertEqual("C%dR%dD%d%s" % flavor,
385
386
387
388
                             f["name"],
                             "Flavor %s does not match its specs." % f["name"])


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
389
390
# --------------------------------------------------------------------
# ServersTestCase class
391
392
393
394
395
396
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")
397

John Giannelos's avatar
John Giannelos committed
398
        cls.client = ComputeClient(API, TOKEN)
399
400
        cls.servers = cls.client.list_servers()
        cls.dservers = cls.client.list_servers(detail=True)
401
        cls.result_dict = dict()
402

403
404
405
    # def test_001_list_servers(self):
    #     """Test server list actually returns servers"""
    #     self.assertGreater(len(self.servers), 0)
406
407
408
409
410
411
412
413
414
415
416
417

    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
418
419
420
421
422
423
424
# --------------------------------------------------------------------
# Pithos Test Cases
class PithosTestCase(unittest.TestCase):
    """Test pithos functionality"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get list of containers"""
425
426
427
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
428
        log.info("Getting list of containers")
429
        cls.client = PithosClient(PITHOS, TOKEN, cls.uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
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
491
492
493
494
495
        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)


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

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

510
        nics = server["attachments"]
John Giannelos's avatar
John Giannelos committed
511

512
513
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
514
            if self.cyclades.get_network_details(net_id)["public"]:
515
                public_addrs = nic["ipv4"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
516
517

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

519
        return public_addrs
520
521

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

524
        nics = server["attachments"]
John Giannelos's avatar
John Giannelos committed
525

526
527
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
528
            if self.cyclades.get_network_details(net_id)["public"]:
529
                public_addrs = nic["ipv6"]
John Giannelos's avatar
John Giannelos committed
530

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
533
        return public_addrs
534

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

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

581
    def _get_hostname_over_ssh(self, hostip, username, password):
582
583
        lines, status = _ssh_execute(
            hostip, username, password, "hostname")
584
        self.assertEqual(len(lines), 1)
585
        return lines[0]
586
587
588
589

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

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

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

    def _insist_on_ssh_hostname(self, hostip, username, password):
631
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
632
        hostname = self._try_until_timeout_expires(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
633
634
635
            self.action_timeout, self.action_timeout,
            msg, self._get_hostname_over_ssh,
            hostip, username, password)
636
637
638

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

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

653
654
655
656
        transport = paramiko.Transport((hostip, 22))
        transport.connect(username=username, password=password)

        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
657
        sftp = paramiko.SFTPClient.from_transport(transport)
658
        sftp.get(remotepath, localpath)
659
660
661
        sftp.close()
        transport.close()

662
663
664
        f = open(localpath)
        remote_content = b64encode(f.read())

665
        # Check if files are the same
666
        return (remote_content == content)
667

668
669
670
671
672
673
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)

    def test_001_submit_create_server(self):
        """Test submit create server request"""
674
675

        log.info("Submit new server request")
676
677
        server = self.client.create_server(self.servername, self.flavorid,
                                           self.imageid, self.personality)
678

679
680
        log.info("Server id: " + str(server["id"]))
        log.info("Server password: " + server["adminPass"])
681
        self.assertEqual(server["name"], self.servername)
682
683
        self.assertEqual(server["flavor"], self.flavorid)
        self.assertEqual(server["image"], self.imageid)
684
685
686
687
688
        self.assertEqual(server["status"], "BUILD")

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

692
693
694
        self.result_dict["Server ID"] = str(server["id"])
        self.result_dict["Password"] = str(server["adminPass"])

695
696
    def test_002a_server_is_building_in_list(self):
        """Test server is in BUILD state, in server list"""
697
698
        log.info("Server in BUILD state in server list")

699
700
        self.result_dict.clear()

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

704
705
        server = servers[0]
        self.assertEqual(server["name"], self.servername)
706
707
        self.assertEqual(server["flavor"], self.flavorid)
        self.assertEqual(server["image"], self.imageid)
708
709
710
711
        self.assertEqual(server["status"], "BUILD")

    def test_002b_server_is_building_in_details(self):
        """Test server is in BUILD state, in details"""
712
713
714

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

715
716
        server = self.client.get_server_details(self.serverid)
        self.assertEqual(server["name"], self.servername)
717
718
        self.assertEqual(server["flavor"], self.flavorid)
        self.assertEqual(server["image"], self.imageid)
719
720
721
        self.assertEqual(server["status"], "BUILD")

    def test_002c_set_server_metadata(self):
722
723
724

        log.info("Creating server metadata")

725
        image = self.client.get_image_details(self.imageid)
726
727
        os_value = image["metadata"]["os"]
        users = image["metadata"].get("users", None)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
728
        self.client.update_server_metadata(self.serverid, OS=os_value)
John Giannelos's avatar
John Giannelos committed
729

730
        userlist = users.split()
731

732
733
734
        # Determine the username to use for future connections
        # to this host
        cls = type(self)
735
736
737

        if "root" in userlist:
            cls.username = "root"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
738
739
        elif users is None:
            cls.username = self._connect_loginname(os_value)
740
741
742
        else:
            cls.username = choice(userlist)

743
        self.assertIsNotNone(cls.username)
744
745
746

    def test_002d_verify_server_metadata(self):
        """Test server metadata keys are set based on image metadata"""
747
748
749

        log.info("Verifying image metadata")

750
751
        servermeta = self.client.get_server_metadata(self.serverid)
        imagemeta = self.client.get_image_metadata(self.imageid)
752

John Giannelos's avatar
John Giannelos committed
753
        self.assertEqual(servermeta["OS"], imagemeta["os"])
754
755
756

    def test_003_server_becomes_active(self):
        """Test server becomes ACTIVE"""
757
758
759

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
760
761
        self._insist_on_status_transition(
            "BUILD", "ACTIVE", self.build_fail, self.build_warning)
762

John Giannelos's avatar
John Giannelos committed
763
764
    def test_003a_get_server_oob_console(self):
        """Test getting OOB server console over VNC
765

John Giannelos's avatar
John Giannelos committed
766
767
        Implementation of RFB protocol follows
        http://www.realvnc.com/docs/rfbproto.pdf.
768

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

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

801
802
    def test_004_server_has_ipv4(self):
        """Test active server has a valid IPv4 address"""
803

804
        log.info("Validate server's IPv4")
805

806
807
        server = self.client.get_server_details(self.serverid)
        ipv4 = self._get_ipv4(server)
808
809
810
811

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

812
813
        self.assertEquals(IP(ipv4).version(), 4)

814
815
    def test_005_server_has_ipv6(self):
        """Test active server has a valid IPv6 address"""
816
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
817

818
        log.info("Validate server's IPv6")
819

820
821
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
822
823
824
825

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

826
        self.assertEquals(IP(ipv6).version(), 6)
827

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

        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)
854
855
856

    def test_008_submit_shutdown_request(self):
        """Test submit request to shutdown server"""
857
858
859

        log.info("Shutting down server")

860
        self.cyclades.shutdown_server(self.serverid)
861
862
863

    def test_009_server_becomes_stopped(self):
        """Test server becomes STOPPED"""
864
865

        log.info("Waiting until server becomes STOPPED")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
866
867
        self._insist_on_status_transition(
            "ACTIVE", "STOPPED", self.action_timeout, self.action_timeout)
868
869
870

    def test_010_submit_start_request(self):
        """Test submit start server request"""
871
872
873

        log.info("Starting server")

874
        self.cyclades.start_server(self.serverid)
875
876
877

    def test_011_server_becomes_active(self):
        """Test server becomes ACTIVE again"""
878
879

        log.info("Waiting until server becomes ACTIVE")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
880
881
        self._insist_on_status_transition(
            "STOPPED", "ACTIVE", self.action_timeout, self.action_timeout)
882

John Giannelos's avatar
John Giannelos committed
883
884
    def test_011a_server_responds_to_ping_IPv4(self):
        """Test server OS is actually up and running again"""
885

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

John Giannelos's avatar
John Giannelos committed
888
        self.test_006_server_responds_to_ping_IPv4()
889

John Giannelos's avatar
John Giannelos committed
890
891
    def test_012_ssh_to_server_IPv4(self):
        """Test SSH to server public IPv4 works, verify hostname"""
892

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

John Giannelos's avatar
John Giannelos committed
898
899
900
901
    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")
902

John Giannelos's avatar
John Giannelos committed
903
904
905
        server = self.client.get_server_details(self.serverid)
        self._insist_on_ssh_hostname(self._get_ipv6(server),
                                     self.username, self.passwd)
906

John Giannelos's avatar
John Giannelos committed
907
908
909
910
911
    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
912
        sock = self._insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
913

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

John Giannelos's avatar
John Giannelos committed
919
920
921
922
    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")
923

John Giannelos's avatar
John Giannelos committed
924
925
        server = self.client.get_server_details(self.serverid)
        ipv6 = self._get_ipv6(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
926
        sock = self._get_tcp_connection(socket.AF_INET6, ipv6, 3389)
927

John Giannelos's avatar
John Giannelos committed
928
929
930
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        sock.close()
931

John Giannelos's avatar
John Giannelos committed
932
933
934
    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
935
        self._skipIf(self.personality is None, "No personality file selected")
936

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

John Giannelos's avatar
John Giannelos committed
939
        server = self.client.get_server_details(self.serverid)
940

John Giannelos's avatar
John Giannelos committed
941
942
943
944
945
946
947
        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)
948

949
950
    def test_017_submit_delete_request(self):
        """Test submit request to delete server"""
951
952
953

        log.info("Deleting server")

954
955
956
957
        self.client.delete_server(self.serverid)

    def test_018_server_becomes_deleted(self):
        """Test server becomes DELETED"""
958
959
960

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
961
962
        self._insist_on_status_transition(
            "ACTIVE", "DELETED", self.action_timeout, self.action_timeout)
963
964
965

    def test_019_server_no_longer_in_server_list(self):
        """Test server is no longer in server list"""
966
967
968

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

969
        servers = self.client.list_servers()
970
971
972
        self.assertNotIn(self.serverid, [s["id"] for s in servers])


973
class NetworkTestCase(unittest.TestCase):
John Giannelos's avatar
John Giannelos committed
974
    """ Testing networking in cyclades """
975

976
    @classmethod
John Giannelos's avatar
John Giannelos committed
977
978
    def setUpClass(cls):
        "Initialize kamaki, get list of current networks"
John Giannelos's avatar
John Giannelos committed
979
980
981

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

983
984
985
        cls.servername = "%s%s for %s" % (SNF_TEST_PREFIX,
                                          TEST_RUN_ID,
                                          cls.imagename)
986
987
988
989
990

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

993
994
        cls.result_dict = dict()

995
996
997
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)
998

999
1000
1001
    def _get_ipv4(self, server):
        """Get the public IPv4 of a server from the detailed server info"""

1002
        nics = server["attachments"]
John Giannelos's avatar
John Giannelos committed
1003

1004
1005
        for nic in nics:
            net_id = nic["network_id"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1006
            if self.client.get_network_details(net_id)["public"]:
1007
                public_addrs = nic["ipv4"]
John Giannelos's avatar
John Giannelos committed
1008

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1011
        return public_addrs
1012

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

John Giannelos's avatar
John Giannelos committed
1022
    def _ping_once(self, ip):
1023

John Giannelos's avatar
John Giannelos committed
1024
1025
1026
1027
1028
1029
        """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()
1030

John Giannelos's avatar
John Giannelos committed
1031
1032
        return (ret == 0)

John Giannelos's avatar
John Giannelos committed
1033
    def test_00001a_submit_create_server_A(self):
1034
        """Test submit create server request"""
1035
1036
1037

        log.info("Creating test server A")

1038
        serverA = self.client.create_server(self.servername, self.flavorid,
1039
                                            self.imageid, personality=None)
1040

John Giannelos's avatar
John Giannelos committed
1041
        self.assertEqual(serverA["name"], self.servername)
1042
1043
        self.assertEqual(serverA["flavor"], self.flavorid)
        self.assertEqual(serverA["image"], self.imageid)
John Giannelos's avatar
John Giannelos committed
1044
        self.assertEqual(serverA["status"], "BUILD")
1045
1046

        # Update class attributes to reflect data on building server
John Giannelos's avatar
John Giannelos committed
1047
1048
1049
        self.serverid['A'] = serverA["id"]
        self.username['A'] = None
        self.password['A'] = serverA["adminPass"]
1050

1051
1052
        log.info("Server A id:" + str(serverA["id"]))
        log.info("Server password " + (self.password['A']))
1053

1054
1055
        self.result_dict["Server A ID"] = str(serverA["id"])
        self.result_dict["Server A password"] = serverA["adminPass"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1056

John Giannelos's avatar
John Giannelos committed
1057
    def test_00001b_serverA_becomes_active(self):
1058
        """Test server becomes ACTIVE"""
1059

1060
        log.info("Waiting until test server A becomes ACTIVE")
1061
        self.result_dict.clear()
1062

1063
        fail_tmout = time.time() + self.action_timeout
1064