server_tests.py 14 KB
Newer Older
1
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
38
39
40
# Copyright 2013 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.

"""
This is the burnin class that tests the Servers' functionality

"""

import sys
import IPy
41
42
import stat
import base64
43
44
45
46
47
48
import random
import socket

from vncauthproxy.d3des import generate_response as d3des_generate_response

from synnefo_tools.burnin.common import BurninTests, Proper, run_test
49
from synnefo_tools.burnin.cyclades_common import CycladesTests
50
51
52
53


# Too many public methods. pylint: disable-msg=R0904
# This class gets replicated into actual TestCases dynamically
54
class GeneratedServerTestSuite(CycladesTests):
55
56
    """Test Spawning Serverfunctionality"""
    use_image = Proper(value=None)
57
    personality = Proper(value=None)
58
59
60
61
62
    avail_flavors = Proper(value=None)
    use_flavor = Proper(value=None)
    server = Proper(value=None)
    ipv4 = Proper(value=None)
    ipv6 = Proper(value=None)
63
64
    username = Proper(value=None)
    password = Proper(value=None)
65
66
67

    def test_001_submit_create_server(self):
        """Submit a create server request"""
68
69
70
71
72
73
74
75
76
77
        if self._image_is(self.use_image, "linux"):
            # Enforce personality test
            self.info("Creating personality content to be used")
            self.personality = [{
                'path': "/root/test_inj_file",
                'owner': "root",
                'group': "root",
                'mode': stat.S_IRUSR | stat.S_IWUSR,
                'contents': base64.b64encode("This is a personality file")
            }]
78
79
80
        self.use_flavor = random.choice(self.avail_flavors)

        self.server = self._create_server(
81
            self.use_image, self.use_flavor, self.personality)
82
83
        self.username = self._get_connection_username(self.server)
        self.password = self.server['adminPass']
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

    def test_002_server_build_list(self):
        """Test server is in BUILD state, in server list"""
        servers = self._get_list_of_servers(detail=True)
        servers = [s for s in servers if s['id'] == self.server['id']]

        self.assertEqual(len(servers), 1)
        server = servers[0]
        self.assertEqual(server['name'], self.server['name'])
        self.assertEqual(server['flavor']['id'], self.use_flavor['id'])
        self.assertEqual(server['image']['id'], self.use_image['id'])
        self.assertEqual(server['status'], "BUILD")

    def test_003_server_build_details(self):
        """Test server is in BUILD state, in details"""
        server = self._get_server_details(self.server)
        self.assertEqual(server['name'], self.server['name'])
        self.assertEqual(server['flavor']['id'], self.use_flavor['id'])
        self.assertEqual(server['image']['id'], self.use_image['id'])
        self.assertEqual(server['status'], "BUILD")

    def test_004_set_server_metadata(self):
        """Test setting some of the server's metadata"""
        image = self.clients.cyclades.get_image_details(self.use_image['id'])
        os_value = image['metadata']['os']
        self.clients.cyclades.update_server_metadata(
            self.server['id'], OS=os_value)

        servermeta = \
            self.clients.cyclades.get_server_metadata(self.server['id'])
        imagemeta = \
            self.clients.cyclades.get_image_metadata(self.use_image['id'])
        self.assertEqual(servermeta['OS'], imagemeta['os'])

    def test_005_server_becomes_active(self):
        """Test server becomes ACTIVE"""
120
        self._insist_on_server_transition(self.server, ["BUILD"], "ACTIVE")
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

    def test_006_get_server_oob_console(self):
        """Test getting OOB server console over VNC

        Implementation of RFB protocol follows
        http://www.realvnc.com/docs/rfbproto.pdf.

        """
        console = self.clients.cyclades.get_server_console(self.server['id'])
        self.assertEquals(console['type'], "vnc")
        sock = self._insist_on_tcp_connection(
            socket.AF_INET, console['host'], console['port'])

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

    def test_007_server_has_ipv4(self):
        """Test active server has a valid IPv4 address"""
        server = self.clients.cyclades.get_server_details(self.server['id'])
        # Update the server attribute
        self.server = server

166
        self.ipv4 = self._get_ip(server, version=4)
167
168
169
170
171
172
        self.assertEquals(IPy.IP(self.ipv4).version(), 4)

    def test_008_server_has_ipv6(self):
        """Test active server has a valid IPv6 address"""
        self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled")

173
        self.ipv6 = self._get_ip(self.server, version=6)
174
175
176
177
        self.assertEquals(IPy.IP(self.ipv6).version(), 6)

    def test_009_server_ping_ipv4(self):
        """Test server responds to ping on IPv4 address"""
178
        self._insist_on_ping(self.ipv4, version=4)
179
180
181
182

    def test_010_server_ping_ipv6(self):
        """Test server responds to ping on IPv6 address"""
        self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled")
183
        self._insist_on_ping(self.ipv6, version=6)
184
185
186
187
188
189
190

    def test_011_submit_shutdown(self):
        """Test submit request to shutdown server"""
        self.clients.cyclades.shutdown_server(self.server['id'])

    def test_012_server_becomes_stopped(self):
        """Test server becomes STOPPED"""
191
        self._insist_on_server_transition(self.server, ["ACTIVE"], "STOPPED")
192
193
194
195
196
197
198

    def test_013_submit_start(self):
        """Test submit start server request"""
        self.clients.cyclades.start_server(self.server['id'])

    def test_014_server_becomes_active(self):
        """Test server becomes ACTIVE again"""
199
        self._insist_on_server_transition(self.server, ["STOPPED"], "ACTIVE")
200
201
202
203
204

    def test_015_server_ping_ipv4(self):
        """Test server OS is actually up and running again"""
        self.test_009_server_ping_ipv4()

205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
    def test_016_ssh_to_server_ipv4(self):
        """Test SSH to server public IPv4 works, verify hostname"""
        self._skip_if(not self._image_is(self.use_image, "linux"),
                      "only valid for Linux servers")
        hostname = self._insist_get_hostname_over_ssh(
            self.ipv4, self.username, self.password)
        # The hostname must be of the form 'prefix-id'
        self.assertTrue(hostname.endswith("-%d" % self.server['id']))

    def test_017_ssh_to_server_ipv6(self):
        """Test SSH to server public IPv6 works, verify hostname"""
        self._skip_if(not self._image_is(self.use_image, "linux"),
                      "only valid for Linux servers")
        self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled")
        hostname = self._insist_get_hostname_over_ssh(
            self.ipv6, self.username, self.password)
        # The hostname must be of the form 'prefix-id'
        self.assertTrue(hostname.endswith("-%d" % self.server['id']))

    def test_018_rdp_to_server_ipv4(self):
        """Test RDP connection to server public IPv4 works"""
        self._skip_if(not self._image_is(self.use_image, "windows"),
                      "only valid for Windows servers")
        sock = self._insist_on_tcp_connection(socket.AF_INET, self.ipv4, 3389)
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        # pylint: disable-msg=W0511
        # FIXME: Use rdesktop, analyze exit code? see manpage
        sock.close()

    def test_019_rdp_to_server_ipv6(self):
        """Test RDP connection to server public IPv6 works"""
        self._skip_if(not self._image_is(self.use_image, "windows"),
                      "only valid for Windows servers")
        self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled")
        sock = self._insist_on_tcp_connection(socket.AF_INET, self.ipv6, 3389)
        # No actual RDP processing done. We assume the RDP server is there
        # if the connection to the RDP port is successful.
        # pylint: disable-msg=W0511
        # FIXME: Use rdesktop, analyze exit code? see manpage
        sock.close()

    def test_020_personality(self):
        """Test file injection for personality enforcement"""
        self._skip_if(not self._image_is(self.use_image, "linux"),
                      "only implemented for linux servers")
        assert self.personality is not None, "No personality used"

        for inj_file in self.personality:
            self._check_file_through_ssh(
                self.ipv4, inj_file['owner'], self.password,
                inj_file['path'], inj_file['contents'])

    def test_021_submit_delete_request(self):
        """Test submit request to delete server"""
        self.clients.cyclades.delete_server(self.server['id'])

    def test_022_server_becomes_deleted(self):
        """Test server becomes DELETED"""
264
        self._insist_on_server_transition(self.server, ["ACTIVE"], "DELETED")
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
265
266
        # Verify quotas
        self._verify_quotas_deleted([self.use_flavor])
267
268
269
270
271
272

    def test_023_server_no_longer(self):
        """Test server is no longer in server list"""
        servers = self._get_list_of_servers()
        self.assertNotIn(self.server['id'], [s['id'] for s in servers])

273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312

# --------------------------------------------------------------------
# The actuall test class. We use this class to dynamically create
# tests from the GeneratedServerTestSuite class. Each of these classes
# will run the same tests using different images and or flavors.
# The creation and running of our GeneratedServerTestSuite class will
# happen as a testsuite itself (everything here is a test!).
class ServerTestSuite(BurninTests):
    """Generate and run the GeneratedServerTestSuite

    We will generate as many testsuites as the number of images given.
    Each of these testsuites will use the given flavors at will (random).

    """
    avail_images = Proper(value=None)
    avail_flavors = Proper(value=None)
    gen_classes = Proper(value=None)

    def test_001_images_to_use(self):
        """Find images to be used by GeneratedServerTestSuite"""
        if self.images is None:
            self.info("No --images given. Will use the default %s",
                      "^Debian Base$")
            filters = ["name:^Debian Base$"]
        else:
            filters = self.images

        self.avail_images = self._find_images(filters)
        self.info("Found %s images. Let's create an equal number of tests",
                  len(self.avail_images))

    def test_002_flavors_to_use(self):
        """Find flavors to be used by GeneratedServerTestSuite"""
        flavors = self._get_list_of_flavors(detail=True)

        if self.flavors is None:
            self.info("No --flavors given. Will use all of them")
            self.avail_flavors = flavors
        else:
            self.avail_flavors = self._find_flavors(
313
                self.flavors, flavors=flavors)
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
        self.info("Found %s flavors to choose from", len(self.avail_flavors))

    def test_003_create_testsuites(self):
        """Generate the GeneratedServerTestSuite tests"""
        gen_classes = []
        for img in self.avail_images:
            name = (str("GeneratedServerTestSuite_(%s)" %
                    img['name']).replace(" ", "_"))
            self.info("Constructing class %s", name)
            class_dict = {
                'use_image': Proper(value=img),
                'avail_flavors': Proper(value=self.avail_flavors)
            }
            cls = type(name, (GeneratedServerTestSuite,), class_dict)
            # Make sure the class can be pickled, by listing it among
            # the attributes of __main__. A PicklingError is raised otherwise.
            thismodule = sys.modules[__name__]
            setattr(thismodule, name, cls)
            # Append the generated class
            gen_classes.append(cls)

        self.gen_classes = gen_classes

    def test_004_run_testsuites(self):
        """Run the generated tests"""
        for gen_cls in self.gen_classes:
            self.info("Running testsuite %s", gen_cls.__name__)
            run_test(gen_cls)