burnin.py 82 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
# --------------------------------------------------------------------
# Global Variables
77
AUTH_URL = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
78
TOKEN = None
79
SYSTEM_IMAGES_USER = None
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
80
81
82
83
84
85
86
87
88
89
90
91
92
93
NO_IPV6 = None
NOFAILFAST = None
VERBOSE = None

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

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

94

95
96
97
98
99
100
101
102
# --------------------------------------------------------------------
# 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)
103
104
    except socket.error, err:
        raise AssertionError(err)
105
106
    try:
        stdin, stdout, stderr = ssh.exec_command(command)
107
108
    except paramiko.SSHException, err:
        raise AssertionError(err)
109
110
111
112
113
114
    status = stdout.channel.recv_exit_status()
    output = stdout.readlines()
    ssh.close()
    return output, status


115
116
def _get_user_id():
    """Authenticate to astakos and get unique users id"""
117
    astakos = AstakosClient(AUTH_URL, TOKEN)
118
    astakos.CONNECTION_RETRY_LIMIT = 2
119
    authenticate = astakos.authenticate()
120
    return authenticate['access']['user']['id']
121
122


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

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

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

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

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

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


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

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

203

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
204
205
# --------------------------------------------------------------------
# UnauthorizedTestCase class
206
class UnauthorizedTestCase(unittest.TestCase):
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
207
    """Test unauthorized access"""
208
209
    @classmethod
    def setUpClass(cls):
210
        cls.astakos = AstakosClient(AUTH_URL, TOKEN)
211
        cls.astakos.CONNECTION_RETRY_LIMIT = 2
212
213
        cls.compute_url = \
            cls.astakos.get_service_endpoints('compute')['publicURL']
214
215
        cls.result_dict = dict()

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

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


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
228
# --------------------------------------------------------------------
229
# This class gest replicated into Images TestCases dynamically
230
231
232
233
234
235
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")
236
        cls.astakos_client = AstakosClient(AUTH_URL, TOKEN)
237
        cls.astakos_client.CONNECTION_RETRY_LIMIT = 2
238
239
240
241
        # Compute Client
        compute_url = \
            cls.astakos_client.get_service_endpoints('compute')['publicURL']
        cls.compute_client = ComputeClient(compute_url, TOKEN)
242
        cls.compute_client.CONNECTION_RETRY_LIMIT = 2
243
244
245
246
        # Image Client
        image_url = \
            cls.astakos_client.get_service_endpoints('image')['publicURL']
        cls.image_client = ImageClient(image_url, TOKEN)
247
        cls.image_client.CONNECTION_RETRY_LIMIT = 2
248
249
250
251
        # Pithos Client
        pithos_url = cls.astakos_client.\
            get_service_endpoints('object-store')['publicURL']
        cls.pithos_client = PithosClient(pithos_url, TOKEN)
252
        cls.pithos_client.CONNECTION_RETRY_LIMIT = 2
253
254

        # Get images
255
256
        cls.images = \
            filter(lambda x: not x['name'].startswith(SNF_TEST_PREFIX),
257
                   cls.image_client.list_public())
258
259
        cls.dimages = \
            filter(lambda x: not x['name'].startswith(SNF_TEST_PREFIX),
260
                   cls.image_client.list_public(detail=True))
261
        cls.result_dict = dict()
262
263
264
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
265
266
267
268
269
270
        # 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"
271

272
273
274
275
276
277
278
279
280
281
282
283
284
    @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

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

289
290
    def test_002_list_images_detailed(self):
        """Test detailed image list is the same length as list"""
John Giannelos's avatar
John Giannelos committed
291
        self.assertEqual(len(self.dimages), len(self.images))
292

293
294
295
296
297
298
    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)

299
300
301
302
303
304
    def test_004_unique_image_names(self):
        """Test system images have unique names"""
        sys_images = filter(lambda x: x['owner'] == SYSTEM_IMAGES_USER,
                            self.dimages)
        names = sorted(map(lambda x: x["name"], sys_images))
        self.assertEqual(sorted(list(set(names))), names)
305
306
307

    def test_005_image_metadata(self):
        """Test every image has specific metadata defined"""
308
        keys = frozenset(["osfamily", "root_partition"])
309
310
311
        details = self.compute_client.list_images(detail=True)
        sys_images = filter(lambda x: x['user_id'] == SYSTEM_IMAGES_USER,
                            details)
312
        for i in sys_images:
313
            self.assertTrue(keys.issubset(i["metadata"].keys()))
314

315
316
317
318
319
320
321
322
323
    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]))
324
325
        self.pithos_client.account = image_location[1]
        self.pithos_client.container = image_location[2]
326
327
        temp_file = os.path.join(self.temp_dir, self.temp_image_name)
        with open(temp_file, "wb+") as f:
328
            self.pithos_client.download_object(image_location[3], f)
329
330
331
332
333
334

    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'
335
336
337
        self.pithos_client.account = self.uuid
        self.pithos_client.container = "images"
        self.pithos_client.container_put()
338
        with open(temp_file, "rb+") as f:
339
            self.pithos_client.upload_object(self.temp_image_name, f)
340
        log.info("Register image to plankton")
341
        location = "pithos://" + self.uuid + \
342
            "/images/" + self.temp_image_name
343
        params = {'is_public': False}
344
        properties = {'OSFAMILY': "linux", 'ROOT_PARTITION': 1}
345
346
        self.image_client.register(
            self.temp_image_name, location, params, properties)
347
        # Get image id
348
        details = self.image_client.list_public(detail=True)
349
        detail = filter(lambda x: x['location'] == location, details)
350
351
352
353
354
355
356
357
358
        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+
359
360
361
        self.pithos_client.account = self.uuid
        self.pithos_client.container = "images"
        self.pithos_client.del_object(self.temp_image_name)
362

363

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
364
365
# --------------------------------------------------------------------
# FlavorsTestCase class
366
367
368
369
370
371
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")
372
        cls.astakos_client = AstakosClient(AUTH_URL, TOKEN)
373
        cls.astakos_client.CONNECTION_RETRY_LIMIT = 2
374
375
376
377
        # Compute Client
        compute_url = \
            cls.astakos_client.get_service_endpoints('compute')['publicURL']
        cls.compute_client = ComputeClient(compute_url, TOKEN)
378
        cls.compute_client.CONNECTION_RETRY_LIMIT = 2
379
380
        cls.flavors = cls.compute_client.list_flavors()
        cls.dflavors = cls.compute_client.list_flavors(detail=True)
381
        cls.result_dict = dict()
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406

    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:
407
            flavor = (f["vcpus"], f["ram"], f["disk"], f["SNF:disk_template"])
Christos Stavrakakis's avatar
Christos Stavrakakis committed
408
            self.assertEqual("C%dR%dD%d%s" % flavor,
409
410
411
412
                             f["name"],
                             "Flavor %s does not match its specs." % f["name"])


Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
413
414
# --------------------------------------------------------------------
# ServersTestCase class
415
416
417
418
419
420
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")
421

422
        cls.astakos_client = AstakosClient(AUTH_URL, TOKEN)
423
        cls.astakos_client.CONNECTION_RETRY_LIMIT = 2
424
425
426
427
        # Compute Client
        compute_url = \
            cls.astakos_client.get_service_endpoints('compute')['publicURL']
        cls.compute_client = ComputeClient(compute_url, TOKEN)
428
        cls.compute_client.CONNECTION_RETRY_LIMIT = 2
429
430
        cls.servers = cls.compute_client.list_servers()
        cls.dservers = cls.compute_client.list_servers(detail=True)
431
        cls.result_dict = dict()
432

433
434
435
    # def test_001_list_servers(self):
    #     """Test server list actually returns servers"""
    #     self.assertGreater(len(self.servers), 0)
436
437
438
439
440
441
442
443
444
445
446
447

    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
448
449
450
451
452
453
454
# --------------------------------------------------------------------
# Pithos Test Cases
class PithosTestCase(unittest.TestCase):
    """Test pithos functionality"""
    @classmethod
    def setUpClass(cls):
        """Initialize kamaki, get list of containers"""
455
456
457
        # Get uniq user id
        cls.uuid = _get_user_id()
        log.info("Uniq user id = %s" % cls.uuid)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
458
        log.info("Getting list of containers")
459
460

        cls.astakos_client = AstakosClient(AUTH_URL, TOKEN)
461
        cls.astakos_client.CONNECTION_RETRY_LIMIT = 2
462
463
464
465
        # Pithos Client
        pithos_url = cls.astakos_client.\
            get_service_endpoints('object-store')['publicURL']
        cls.pithos_client = PithosClient(pithos_url, TOKEN, cls.uuid)
466
        cls.pithos_client.CONNECTION_RETRY_LIMIT = 2
467
468

        cls.containers = cls.pithos_client.list_containers()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
        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
490
491
        self.pithos_client.container = rand_name
        self.pithos_client.container_put()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
492
        # Get list of containers
493
        new_containers = self.pithos_client.list_containers()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
494
495
496
497
498
499
500
501
502
503
        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
504
            self.pithos_client.upload_object("test.txt", f)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
505
506
507
508
509
510
511

    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:
512
            self.pithos_client.download_object("test.txt", f)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
513
514
515
516
517
518
519
520
521
522
523
            # 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"""
524
525
526
        cont_name = self.pithos_client.container
        self.pithos_client.del_object("test.txt")
        self.pithos_client.purge_container()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
527
        # List containers
528
        containers = self.pithos_client.list_containers()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
529
530
531
532
533
        cont_names = [n['name'] for n in containers]
        self.assertNotIn(cont_name, cont_names)


# --------------------------------------------------------------------
534
535
536
537
538
539
# 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
540
        log.info("Spawning server for image `%s'" % cls.imagename)
541
542

        cls.astakos_client = AstakosClient(AUTH_URL, TOKEN)
543
        cls.astakos_client.CONNECTION_RETRY_LIMIT = 2
544
545
546
547
        # Cyclades Client
        compute_url = \
            cls.astakos_client.get_service_endpoints('compute')['publicURL']
        cls.cyclades_client = CycladesClient(compute_url, TOKEN)
548
        cls.cyclades_client.CONNECTION_RETRY_LIMIT = 2
549

550
        cls.result_dict = dict()
551
552

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

555
        nics = server["attachments"]
John Giannelos's avatar
John Giannelos committed
556

557
558
        for nic in nics:
            net_id = nic["network_id"]
559
            if self.cyclades_client.get_network_details(net_id)["public"]:
560
                public_addrs = nic["ipv4"]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
561
562

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

564
        return public_addrs
565
566

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

569
        nics = server["attachments"]
John Giannelos's avatar
John Giannelos committed
570

571
572
        for nic in nics:
            net_id = nic["network_id"]
573
            if self.cyclades_client.get_network_details(net_id)["public"]:
574
                public_addrs = nic["ipv6"]
John Giannelos's avatar
John Giannelos committed
575

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
578
        return public_addrs
579

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
580
    def _connect_loginname(self, os_value):
581
        """Return the login name for connections based on the server OS"""
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
582
        if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
583
            return "user"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
584
        elif os_value in ("windows", "windows_alpha1"):
585
            return "Administrator"
586
        else:
587
            return "root"
588
589
590

    def _verify_server_status(self, current_status, new_status):
        """Verify a server has switched to a specified status"""
591
        server = self.cyclades_client.get_server_details(self.serverid)
592
593
        if server["status"] not in (current_status, new_status):
            return None  # Do not raise exception, return so the test fails
594
595
596
597
598
599
600
601
602
603
604
        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
605
            except socket.error:
606
607
608
609
                sock = None
                continue
            try:
                sock.connect(sa)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
610
            except socket.error:
611
612
613
614
615
616
617
618
                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"""
619
        cmd = "ping%s -c 7 -w 20 %s" % ("6" if ipv6 else "", ip)
620
621
622
623
624
        ping = subprocess.Popen(cmd, shell=True,
                                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (stdout, stderr) = ping.communicate()
        ret = ping.wait()
        self.assertEquals(ret, 0)
625

626
    def _get_hostname_over_ssh(self, hostip, username, password):
627
628
        lines, status = _ssh_execute(
            hostip, username, password, "hostname")
629
        self.assertEqual(len(lines), 1)
630
        return lines[0]
631
632
633
634

    def _try_until_timeout_expires(self, warn_timeout, fail_timeout,
                                   opmsg, callable, *args, **kwargs):
        if warn_timeout == fail_timeout:
635
636
637
638
            warn_timeout = fail_timeout + 1
        warn_tmout = time.time() + warn_timeout
        fail_tmout = time.time() + fail_timeout
        while True:
639
            self.assertLess(time.time(), fail_tmout,
640
                            "operation `%s' timed out" % opmsg)
641
            if time.time() > warn_tmout:
642
643
                log.warning("Server %d: `%s' operation `%s' not done yet",
                            self.serverid, self.servername, opmsg)
644
            try:
645
                log.info("%s... " % opmsg)
646
647
648
                return callable(*args, **kwargs)
            except AssertionError:
                pass
649
650
            time.sleep(self.query_interval)

651
    def _insist_on_tcp_connection(self, family, host, port):
652
653
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
654
655
656
        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
657
658
659
            self.action_timeout, self.action_timeout,
            msg, self._get_connected_tcp_socket,
            family, host, port)
660
661
662
        return sock

    def _insist_on_status_transition(self, current_status, new_status,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
663
                                     fail_timeout, warn_timeout=None):
664
665
        msg = "Server %d: `%s', waiting for %s -> %s" % \
              (self.serverid, self.servername, current_status, new_status)
666
667
668
669
670
        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)
671
        # Ensure the status is actually the expected one
672
        server = self.cyclades_client.get_server_details(self.serverid)
673
        self.assertEquals(server["status"], new_status)
674
675

    def _insist_on_ssh_hostname(self, hostip, username, password):
676
        msg = "SSH to %s, as %s/%s" % (hostip, username, password)
677
        hostname = self._try_until_timeout_expires(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
678
679
680
            self.action_timeout, self.action_timeout,
            msg, self._get_hostname_over_ssh,
            hostip, username, password)
681
682
683

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

685
686
687
688
    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)
689
        log.info(msg)
690
691
692
693
        try:
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect(hostip, username=username, password=password)
694
            ssh.close()
695
696
        except socket.error, err:
            raise AssertionError(err)
697

698
699
700
701
        transport = paramiko.Transport((hostip, 22))
        transport.connect(username=username, password=password)

        localpath = '/tmp/' + SNF_TEST_PREFIX + 'injection'
702
        sftp = paramiko.SFTPClient.from_transport(transport)
703
        sftp.get(remotepath, localpath)
704
705
706
        sftp.close()
        transport.close()

707
708
709
        f = open(localpath)
        remote_content = b64encode(f.read())

710
        # Check if files are the same
711
        return (remote_content == content)
712

713
714
715
716
717
718
    def _skipIf(self, condition, msg):
        if condition:
            self.skipTest(msg)

    def test_001_submit_create_server(self):
        """Test submit create server request"""
719
720

        log.info("Submit new server request")
721
722
        server = self.cyclades_client.create_server(
            self.servername, self.flavorid, self.imageid, self.personality)
723

724
725
        log.info("Server id: " + str(server["id"]))
        log.info("Server password: " + server["adminPass"])
726
        self.assertEqual(server["name"], self.servername)
727
728
        self.assertEqual(server["flavor"]["id"], self.flavorid)
        self.assertEqual(server["image"]["id"], self.imageid)
729
730
731
732
733
        self.assertEqual(server["status"], "BUILD")

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

737
738
739
        self.result_dict["Server ID"] = str(server["id"])
        self.result_dict["Password"] = str(server["adminPass"])

740
741
    def test_002a_server_is_building_in_list(self):
        """Test server is in BUILD state, in server list"""
742
743
        log.info("Server in BUILD state in server list")

744
745
        self.result_dict.clear()

746
        servers = self.cyclades_client.list_servers(detail=True)
747
        servers = filter(lambda x: x["name"] == self.servername, servers)
John Giannelos's avatar
John Giannelos committed
748

749
750
        server = servers[0]
        self.assertEqual(server["name"], self.servername)
751
752
        self.assertEqual(server["flavor"]["id"], self.flavorid)
        self.assertEqual(server["image"]["id"], self.imageid)
753
754
755
756
        self.assertEqual(server["status"], "BUILD")

    def test_002b_server_is_building_in_details(self):
        """Test server is in BUILD state, in details"""
757
758
759

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

760
        server = self.cyclades_client.get_server_details(self.serverid)
761
        self.assertEqual(server["name"], self.servername)
762
763
        self.assertEqual(server["flavor"]["id"], self.flavorid)
        self.assertEqual(server["image"]["id"], self.imageid)
764
765
766
        self.assertEqual(server["status"], "BUILD")

    def test_002c_set_server_metadata(self):
767
768
769

        log.info("Creating server metadata")

770
        image = self.cyclades_client.get_image_details(self.imageid)
771
772
        os_value = image["metadata"]["os"]
        users = image["metadata"].get("users", None)
773
        self.cyclades_client.update_server_metadata(self.serverid, OS=os_value)
John Giannelos's avatar
John Giannelos committed
774

775
        userlist = users.split()
776

777
778
779
        # Determine the username to use for future connections
        # to this host
        cls = type(self)
780
781
782

        if "root" in userlist:
            cls.username = "root"
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
783
784
        elif users is None:
            cls.username = self._connect_loginname(os_value)
785
786
787
        else:
            cls.username = choice(userlist)

788
        self.assertIsNotNone(cls.username)
789
790
791

    def test_002d_verify_server_metadata(self):
        """Test server metadata keys are set based on image metadata"""
792
793
794

        log.info("Verifying image metadata")

795
796
        servermeta = self.cyclades_client.get_server_metadata(self.serverid)
        imagemeta = self.cyclades_client.get_image_metadata(self.imageid)
797

John Giannelos's avatar
John Giannelos committed
798
        self.assertEqual(servermeta["OS"], imagemeta["os"])
799
800
801

    def test_003_server_becomes_active(self):
        """Test server becomes ACTIVE"""
802
803
804

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
805
806
        self._insist_on_status_transition(
            "BUILD", "ACTIVE", self.build_fail, self.build_warning)
807

John Giannelos's avatar
John Giannelos committed
808
809
    def test_003a_get_server_oob_console(self):
        """Test getting OOB server console over VNC
810

John Giannelos's avatar
John Giannelos committed
811
812
        Implementation of RFB protocol follows
        http://www.realvnc.com/docs/rfbproto.pdf.
813

John Giannelos's avatar
John Giannelos committed
814
        """
815
        console = self.cyclades_client.get_server_console(self.serverid)
John Giannelos's avatar
John Giannelos committed
816
        self.assertEquals(console['type'], "vnc")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
817
818
        sock = self._insist_on_tcp_connection(
            socket.AF_INET, console["host"], console["port"])
John Giannelos's avatar
John Giannelos committed
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844

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

846
847
    def test_004_server_has_ipv4(self):
        """Test active server has a valid IPv4 address"""
848

849
        log.info("Validate server's IPv4")
850

851
        server = self.cyclades_client.get_server_details(self.serverid)
852
        ipv4 = self._get_ipv4(server)
853
854
855
856

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

857
858
        self.assertEquals(IP(ipv4).version(), 4)

859
860
    def test_005_server_has_ipv6(self):
        """Test active server has a valid IPv6 address"""
861
        self._skipIf(NO_IPV6, "--no-ipv6 flag enabled")
862

863
        log.info("Validate server's IPv6")
864

865
        server = self.cyclades_client.get_server_details(self.serverid)
866
        ipv6 = self._get_ipv6(server)
867
868
869
870

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

871
        self.assertEquals(IP(ipv6).version(), 6)
872

John Giannelos's avatar
John Giannelos committed
873
874
875
876
    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")
877
        self.result_dict.clear()
John Giannelos's avatar
John Giannelos committed
878

879
        server = self.cyclades_client.get_server_details(self.serverid)
John Giannelos's avatar
John Giannelos committed
880
881
882
883
884
885
886
887
888
889
890
891
        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")

892
        server = self.cyclades_client.get_server_details(self.serverid)
John Giannelos's avatar
John Giannelos committed
893
894
895
896
897
898
        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)
899
900
901

    def test_008_submit_shutdown_request(self):
        """Test submit request to shutdown server"""
902
903
904

        log.info("Shutting down server")

905
        self.cyclades_client.shutdown_server(self.serverid)
906
907
908

    def test_009_server_becomes_stopped(self):
        """Test server becomes STOPPED"""
909
910

        log.info("Waiting until server becomes STOPPED")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
911
912
        self._insist_on_status_transition(
            "ACTIVE", "STOPPED", self.action_timeout, self.action_timeout)
913
914
915

    def test_010_submit_start_request(self):
        """Test submit start server request"""
916
917
918

        log.info("Starting server")

919
        self.cyclades_client.start_server(self.serverid)
920
921
922

    def test_011_server_becomes_active(self):
        """Test server becomes ACTIVE again"""
923
924

        log.info("Waiting until server becomes ACTIVE")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
925
926
        self._insist_on_status_transition(
            "STOPPED", "ACTIVE", self.action_timeout, self.action_timeout)
927

John Giannelos's avatar
John Giannelos committed
928
929
    def test_011a_server_responds_to_ping_IPv4(self):
        """Test server OS is actually up and running again"""
930

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

John Giannelos's avatar
John Giannelos committed
933
        self.test_006_server_responds_to_ping_IPv4()
934

John Giannelos's avatar
John Giannelos committed
935
936
    def test_012_ssh_to_server_IPv4(self):
        """Test SSH to server public IPv4 works, verify hostname"""
937

John Giannelos's avatar
John Giannelos committed
938
        self._skipIf(self.is_windows, "only valid for Linux servers")
939
        server = self.cyclades_client.get_server_details(self.serverid)
John Giannelos's avatar
John Giannelos committed
940
941
        self._insist_on_ssh_hostname(self._get_ipv4(server),
                                     self.username, self.passwd)
942

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

948
        server = self.cyclades_client.get_server_details(self.serverid)
John Giannelos's avatar
John Giannelos committed
949
950
        self._insist_on_ssh_hostname(self._get_ipv6(server),
                                     self.username, self.passwd)
951

John Giannelos's avatar
John Giannelos committed
952
953
954
    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")
955
        server = self.cyclades_client.get_server_details(self.serverid)
John Giannelos's avatar
John Giannelos committed
956
        ipv4 = self._get_ipv4(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
957
        sock = self._insist_on_tcp_connection(socket.AF_INET, ipv4, 3389)
958

John Giannelos's avatar
John Giannelos committed
959
960
961
962
        # 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()
963

John Giannelos's avatar
John Giannelos committed
964
965
966
967
    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")
968

969
        server = self.cyclades_client.get_server_details(self.serverid)
John Giannelos's avatar
John Giannelos committed
970
        ipv6 = self._get_ipv6(server)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
971
        sock = self._get_tcp_connection(socket.AF_INET6, ipv6, 3389)
972

John Giannelos's avatar
John Giannelos committed
973
974
975
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        sock.close()
976

John Giannelos's avatar
John Giannelos committed
977
978
979
    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
980
        self._skipIf(self.personality is None, "No personality file selected")
981

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

984
        server = self.cyclades_client.get_server_details(self.serverid)
985

John Giannelos's avatar
John Giannelos committed
986
987
988
989
990
991
992
        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)
993

994
995
    def test_017_submit_delete_request(self):
        """Test submit request to delete server"""
996
997
998

        log.info("Deleting server")

999
        self.cyclades_client.delete_server(self.serverid)
1000
1001
1002

    def test_018_server_becomes_deleted(self):
        """Test server becomes DELETED"""
1003
1004
1005

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
1006
1007
        self._insist_on_status_transition(
            "ACTIVE", "DELETED", self.action_timeout, self.action_timeout)
1008
1009
1010

    def test_019_server_no_longer_in_server_list(self):
        """Test server is no longer in server list"""
1011
1012
1013

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

1014
        servers = self.cyclades_client.list_servers()
1015
1016
1017
        self.assertNotIn(self.serverid, [s["id"] for s in servers])


1018
class NetworkTestCase(unittest.TestCase):