Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / server_tests.py @ 9355a604

History | View | Annotate | Download (14.3 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
"""
35
This is the burnin class that tests the Servers' functionality
36

37
"""
38

    
39
import sys
40
import stat
41
import base64
42
import random
43
import socket
44

    
45
from vncauthproxy.d3des import generate_response as d3des_generate_response
46

    
47
from synnefo_tools.burnin.common import BurninTests, Proper
48
from synnefo_tools.burnin.cyclades_common import CycladesTests
49

    
50

    
51
# pylint: disable=too-many-public-methods,too-many-instance-attributes
52
# This class gets replicated into actual TestCases dynamically
53
class GeneratedServerTestSuite(CycladesTests):
54
    """Test Spawning Serverfunctionality"""
55
    use_image = Proper(value=None)
56
    personality = Proper(value=None)
57
    avail_flavors = Proper(value=None)
58
    use_flavor = Proper(value=None)
59
    server = Proper(value=None)
60
    ipv4 = Proper(value=None)
61
    ipv6 = Proper(value=None)
62
    username = Proper(value=None)
63
    password = Proper(value=None)
64

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

    
79
        self.server = self._create_server(
80
            self.use_image, self.use_flavor,
81
            personality=self.personality, network=True)
82
        self.username = self._get_connection_username(self.server)
83
        self.password = self.server['adminPass']
84

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

    
90
        self.assertEqual(len(servers), 1)
91
        server = servers[0]
92
        self.assertEqual(server['name'], self.server['name'])
93
        self.assertEqual(server['flavor']['id'], self.use_flavor['id'])
94
        self.assertEqual(server['image']['id'], self.use_image['id'])
95
        self.assertEqual(server['status'], "BUILD")
96

    
97
    def test_003_server_build_details(self):
98
        """Test server is in BUILD state, in details"""
99
        server = self._get_server_details(self.server)
100
        self.assertEqual(server['name'], self.server['name'])
101
        self.assertEqual(server['flavor']['id'], self.use_flavor['id'])
102
        self.assertEqual(server['image']['id'], self.use_image['id'])
103
        self.assertEqual(server['status'], "BUILD")
104

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

    
112
        servermeta = \
113
            self.clients.cyclades.get_server_metadata(self.server['id'])
114
        imagemeta = \
115
            self.clients.cyclades.get_image_metadata(self.use_image['id'])
116
        self.assertEqual(servermeta['OS'], imagemeta['os'])
117

    
118
    def test_005_server_becomes_active(self):
119
        """Test server becomes ACTIVE"""
120
        self._insist_on_server_transition(self.server, ["BUILD"], "ACTIVE")
121

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

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

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

    
134
        # Step 1. ProtocolVersion message (par. 6.1.1)
135
        version = sock.recv(1024)
136
        self.assertEquals(version, 'RFB 003.008\n')
137
        sock.send(version)
138

    
139
        # Step 2. Security (par 6.1.2): Only VNC Authentication supported
140
        sec = sock.recv(1024)
141
        self.assertEquals(list(sec), ['\x01', '\x02'])
142

    
143
        # Step 3. Request VNC Authentication (par 6.1.2)
144
        sock.send('\x02')
145

    
146
        # Step 4. Receive Challenge (par 6.2.2)
147
        challenge = sock.recv(1024)
148
        self.assertEquals(len(challenge), 16)
149

    
150
        # Step 5. DES-Encrypt challenge, use password as key (par 6.2.2)
151
        response = d3des_generate_response(
152
            (console["password"] + '\0' * 8)[:8], challenge)
153
        sock.send(response)
154

    
155
        # Step 6. SecurityResult (par 6.1.3)
156
        result = sock.recv(4)
157
        self.assertEquals(list(result), ['\x00', '\x00', '\x00', '\x00'])
158
        sock.close()
159

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

    
166
        self.ipv4 = self._get_ips(server, version=4)
167

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

    
172
        self.ipv6 = self._get_ips(self.server, version=6)
173

    
174
    def test_009_server_ping_ipv4(self):
175
        """Test server responds to ping on IPv4 address"""
176
        for ipv4 in self.ipv4:
177
            self._insist_on_ping(ipv4, version=4)
178

    
179
    def test_010_server_ping_ipv6(self):
180
        """Test server responds to ping on IPv6 address"""
181
        self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled")
182
        self._insist_on_ping(self.ipv6[0], version=6)
183

    
184
    def test_011_attach_second_network(self):
185
        """Attach a second public IP to our server"""
186
        floating_ip = self._create_floating_ip()
187
        self._create_port(floating_ip['floating_network_id'],
188
                          device_id=self.server['id'],
189
                          floating_ip=floating_ip)
190

    
191
        # Update server attributes
192
        server = self.clients.cyclades.get_server_details(self.server['id'])
193
        self.server = server
194
        self.ipv4 = self._get_ips(server, version=4)
195
        self.assertEqual(len(self.ipv4), 2)
196

    
197
        # Test new IPv4
198
        self.test_009_server_ping_ipv4()
199

    
200
    def test_012_submit_shutdown(self):
201
        """Test submit request to shutdown server"""
202
        self.clients.cyclades.shutdown_server(self.server['id'])
203

    
204
    def test_013_server_becomes_stopped(self):
205
        """Test server becomes STOPPED"""
206
        self._insist_on_server_transition(self.server, ["ACTIVE"], "STOPPED")
207

    
208
    def test_014_submit_start(self):
209
        """Test submit start server request"""
210
        self.clients.cyclades.start_server(self.server['id'])
211

    
212
    def test_015_server_becomes_active(self):
213
        """Test server becomes ACTIVE again"""
214
        self._insist_on_server_transition(self.server, ["STOPPED"], "ACTIVE")
215

    
216
    def test_016_server_ping_ipv4(self):
217
        """Test server OS is actually up and running again"""
218
        self.test_009_server_ping_ipv4()
219

    
220
    def test_017_ssh_to_server_ipv4(self):
221
        """Test SSH to server public IPv4 works, verify hostname"""
222
        self._skip_if(not self._image_is(self.use_image, "linux"),
223
                      "only valid for Linux servers")
224
        hostname1 = self._insist_get_hostname_over_ssh(
225
            self.ipv4[0], self.username, self.password)
226
        hostname2 = self._insist_get_hostname_over_ssh(
227
            self.ipv4[1], self.username, self.password)
228
        # The hostname must be of the form 'prefix-id'
229
        self.assertTrue(hostname1.endswith("-%d" % self.server['id']))
230
        self.assertEqual(hostname1, hostname2)
231

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

    
242
    def test_019_rdp_to_server_ipv4(self):
243
        """Test RDP connection to server public IPv4 works"""
244
        self._skip_if(not self._image_is(self.use_image, "windows"),
245
                      "only valid for Windows servers")
246
        sock = self._insist_on_tcp_connection(
247
            socket.AF_INET, self.ipv4[0], 3389)
248
        # No actual RDP processing done. We assume the RDP server is there
249
        # if the connection to the RDP port is successful.
250
        # pylint: disable=fixme
251
        # FIXME: Use rdesktop, analyze exit code? see manpage
252
        sock.close()
253

    
254
    def test_020_rdp_to_server_ipv6(self):
255
        """Test RDP connection to server public IPv6 works"""
256
        self._skip_if(not self._image_is(self.use_image, "windows"),
257
                      "only valid for Windows servers")
258
        self._skip_if(not self.use_ipv6, "--no-ipv6 flag enabled")
259
        sock = self._insist_on_tcp_connection(
260
            socket.AF_INET, self.ipv6[0], 3389)
261
        # No actual RDP processing done. We assume the RDP server is there
262
        # if the connection to the RDP port is successful.
263
        # pylint: disable=fixme
264
        # FIXME: Use rdesktop, analyze exit code? see manpage
265
        sock.close()
266

    
267
    def test_021_personality(self):
268
        """Test file injection for personality enforcement"""
269
        self._skip_if(not self._image_is(self.use_image, "linux"),
270
                      "only implemented for linux servers")
271
        assert self.personality is not None, "No personality used"
272

    
273
        for inj_file in self.personality:
274
            self._check_file_through_ssh(
275
                self.ipv4[0], inj_file['owner'], self.password,
276
                inj_file['path'], inj_file['contents'])
277

    
278
    def test_022_destroy_floating_ips(self):
279
        """Destroy the floating IPs"""
280
        self._disconnect_from_network(self.server)
281

    
282
    def test_023_submit_delete_request(self):
283
        """Test submit request to delete server"""
284
        self._delete_servers([self.server])
285

    
286

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

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

299
    """
300
    avail_images = Proper(value=None)
301
    avail_flavors = Proper(value=None)
302
    gen_classes = Proper(value=None)
303

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

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

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

    
321
        if self.flavors is None:
322
            self.info("No --flavors given. Will use all of them")
323
            self.avail_flavors = flavors
324
        else:
325
            self.avail_flavors = self._find_flavors(
326
                self.flavors, flavors=flavors)
327
        self.info("Found %s flavors to choose from", len(self.avail_flavors))
328

    
329
    def test_003_create_testsuites(self):
330
        """Generate the GeneratedServerTestSuite tests"""
331
        gen_classes = []
332
        for img in self.avail_images:
333
            name = (str("GeneratedServerTestSuite_(%s)" %
334
                    img['name']).replace(" ", "_"))
335
            self.info("Constructing class %s", name)
336
            class_dict = {
337
                'use_image': Proper(value=img),
338
                'avail_flavors': Proper(value=self.avail_flavors)
339
            }
340
            cls = type(name, (GeneratedServerTestSuite,), class_dict)
341
            # Make sure the class can be pickled, by listing it among
342
            # the attributes of __main__. A PicklingError is raised otherwise.
343
            thismodule = sys.modules[__name__]
344
            setattr(thismodule, name, cls)
345
            # Append the generated class
346
            gen_classes.append(cls)
347

    
348
        self.gen_classes = gen_classes
349

    
350
    def test_004_run_testsuites(self):
351
        """Run the generated tests"""
352
        self._run_tests(self.gen_classes)