Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / server_tests.py @ 3e5bbd85

History | View | Annotate | Download (14 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 IPy
41
import stat
42
import base64
43
import random
44
import socket
45

    
46
from vncauthproxy.d3des import generate_response as d3des_generate_response
47

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

    
51

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

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

    
80
        self.server = self._create_server(
81
            self.use_image, self.use_flavor, self.personality)
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_ip(server, version=4)
167
        self.assertEquals(IPy.IP(self.ipv4).version(), 4)
168

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

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

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

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

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

    
189
    def test_012_server_becomes_stopped(self):
190
        """Test server becomes STOPPED"""
191
        self._insist_on_server_transition(self.server, ["ACTIVE"], "STOPPED")
192

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

    
197
    def test_014_server_becomes_active(self):
198
        """Test server becomes ACTIVE again"""
199
        self._insist_on_server_transition(self.server, ["STOPPED"], "ACTIVE")
200

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

    
205
    def test_016_ssh_to_server_ipv4(self):
206
        """Test SSH to server public IPv4 works, verify hostname"""
207
        self._skip_if(not self._image_is(self.use_image, "linux"),
208
                      "only valid for Linux servers")
209
        hostname = self._insist_get_hostname_over_ssh(
210
            self.ipv4, self.username, self.password)
211
        # The hostname must be of the form 'prefix-id'
212
        self.assertTrue(hostname.endswith("-%d" % self.server['id']))
213

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

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

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

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

    
253
        for inj_file in self.personality:
254
            self._check_file_through_ssh(
255
                self.ipv4, inj_file['owner'], self.password,
256
                inj_file['path'], inj_file['contents'])
257

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

    
262
    def test_022_server_becomes_deleted(self):
263
        """Test server becomes DELETED"""
264
        self._insist_on_server_transition(self.server, ["ACTIVE"], "DELETED")
265
        # Verify quotas
266
        self._verify_quotas_deleted([self.use_flavor])
267

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

    
273

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

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

286
    """
287
    avail_images = Proper(value=None)
288
    avail_flavors = Proper(value=None)
289
    gen_classes = Proper(value=None)
290

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

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

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

    
308
        if self.flavors is None:
309
            self.info("No --flavors given. Will use all of them")
310
            self.avail_flavors = flavors
311
        else:
312
            self.avail_flavors = self._find_flavors(
313
                self.flavors, flavors=flavors)
314
        self.info("Found %s flavors to choose from", len(self.avail_flavors))
315

    
316
    def test_003_create_testsuites(self):
317
        """Generate the GeneratedServerTestSuite tests"""
318
        gen_classes = []
319
        for img in self.avail_images:
320
            name = (str("GeneratedServerTestSuite_(%s)" %
321
                    img['name']).replace(" ", "_"))
322
            self.info("Constructing class %s", name)
323
            class_dict = {
324
                'use_image': Proper(value=img),
325
                'avail_flavors': Proper(value=self.avail_flavors)
326
            }
327
            cls = type(name, (GeneratedServerTestSuite,), class_dict)
328
            # Make sure the class can be pickled, by listing it among
329
            # the attributes of __main__. A PicklingError is raised otherwise.
330
            thismodule = sys.modules[__name__]
331
            setattr(thismodule, name, cls)
332
            # Append the generated class
333
            gen_classes.append(cls)
334

    
335
        self.gen_classes = gen_classes
336

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