cyclades_common.py 16.2 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
41
# 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.

"""
Utility functions for Cyclades Tests
Cyclades require a lot helper functions and `common'
had grown too much.

"""

import time
42
import base64
43
44
import socket
import random
45
46
import paramiko
import tempfile
47
48
import subprocess

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
49
from synnefo_tools.burnin.common import BurninTests, MB, GB
50
51
52
53
54


# Too many public methods. pylint: disable-msg=R0904
class CycladesTests(BurninTests):
    """Extends the BurninTests class for Cyclades"""
55
    def _try_until_timeout_expires(self, opmsg, check_fun):
56
57
58
59
60
61
62
63
        """Try to perform an action until timeout expires"""
        assert callable(check_fun), "Not a function"

        action_timeout = self.action_timeout
        action_warning = self.action_warning
        if action_warning > action_timeout:
            action_warning = action_timeout

64
65
66
        start_time = int(time.time())
        end_time = start_time + action_warning
        while end_time > time.time():
67
            try:
68
69
70
71
                ret_value = check_fun()
                self.info("Operation `%s' finished in %s seconds",
                          opmsg, int(time.time()) - start_time)
                return ret_value
72
73
            except Retry:
                time.sleep(self.query_interval)
74
75
76
77
78
        self.warning("Operation `%s' is taking too long after %s seconds",
                     opmsg, int(time.time()) - start_time)

        end_time = start_time + action_timeout
        while end_time > time.time():
79
            try:
80
81
82
83
                ret_value = check_fun()
                self.info("Operation `%s' finished in %s seconds",
                          opmsg, int(time.time()) - start_time)
                return ret_value
84
85
            except Retry:
                time.sleep(self.query_interval)
86
87
        self.error("Operation `%s' timed out after %s seconds",
                   opmsg, int(time.time()) - start_time)
88
89
90
91
92
93
94
95
96
97
        self.fail("time out")

    def _get_list_of_servers(self, detail=False):
        """Get (detailed) list of servers"""
        if detail:
            self.info("Getting detailed list of servers")
        else:
            self.info("Getting simple list of servers")
        return self.clients.cyclades.list_servers(detail=detail)

98
99
100
101
102
103
104
105
106
    def _get_list_of_networks(self, detail=False):
        """Get (detailed) list of networks"""
        if detail:
            self.info("Getting detailed list of networks")
        else:
            self.info("Getting simple list of networks")
        return self.clients.cyclades.list_networks(detail=detail)

    def _get_server_details(self, server, quiet=False):
107
        """Get details for a server"""
108
109
110
        if not quiet:
            self.info("Getting details for server %s with id %s",
                      server['name'], server['id'])
111
112
        return self.clients.cyclades.get_server_details(server['id'])

113
    def _create_server(self, image, flavor, personality=None):
114
        """Create a new server"""
115
116
        servername = "%s for %s" % (self.run_id, image['name'])
        self.info("Creating a server with name %s", servername)
117
118
119
        self.info("Using image %s with id %s", image['name'], image['id'])
        self.info("Using flavor %s with id %s", flavor['name'], flavor['id'])
        server = self.clients.cyclades.create_server(
120
            servername, flavor['id'], image['id'], personality=personality)
121
122
123
124

        self.info("Server id: %s", server['id'])
        self.info("Server password: %s", server['adminPass'])

125
        self.assertEqual(server['name'], servername)
126
127
128
129
        self.assertEqual(server['flavor']['id'], flavor['id'])
        self.assertEqual(server['image']['id'], image['id'])
        self.assertEqual(server['status'], "BUILD")

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
130
131
132
133
134
135
        # Verify quotas
        self._check_quotas(disk=+int(flavor['disk'])*GB,
                           vm=+1,
                           ram=+int(flavor['ram'])*MB,
                           cpu=+int(flavor['vcpus']))

136
137
        return server

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
    def _verify_quotas_deleted(self, flavors):
        """Verify quotas for a number of deleted servers"""
        used_disk = 0
        used_vm = 0
        used_ram = 0
        used_cpu = 0
        for flavor in flavors:
            used_disk += int(flavor['disk']) * GB
            used_vm += 1
            used_ram += int(flavor['ram']) * MB
            used_cpu += int(flavor['vcpus'])
        self._check_quotas(disk=-used_disk,
                           vm=-used_vm,
                           ram=-used_ram,
                           cpu=-used_cpu)

154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
    def _get_connection_username(self, server):
        """Determine the username to use to connect to the server"""
        users = server['metadata'].get("users", None)
        ret_user = None
        if users is not None:
            user_list = users.split()
            if "root" in user_list:
                ret_user = "root"
            else:
                ret_user = random.choice(user_list)
        else:
            # Return the login name for connections based on the server OS
            self.info("Could not find `users' metadata in server. Let's guess")
            os_value = server['metadata'].get("os")
            if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
                ret_user = "user"
            elif os_value in ("windows", "windows_alpha1"):
                ret_user = "Administrator"
            else:
                ret_user = "root"

        self.assertIsNotNone(ret_user)
        self.info("User's login name: %s", ret_user)
        return ret_user

179
180
    def _insist_on_server_transition(self, server, curr_statuses, new_status):
        """Insist on server transiting from curr_statuses to new_status"""
181
182
        def check_fun():
            """Check server status"""
183
184
            srv = self._get_server_details(server, quiet=True)
            if srv['status'] in curr_statuses:
185
186
187
188
                raise Retry()
            elif srv['status'] == new_status:
                return
            else:
189
190
191
                msg = "Server \"%s\" with id %s went to unexpected status %s"
                self.error(msg, server['name'], server['id'], srv['status'])
                self.fail(msg % (server['name'], server['id'], srv['status']))
192
193
194
        opmsg = "Waiting for server \"%s\" with id %s to become %s"
        self.info(opmsg, server['name'], server['id'], new_status)
        opmsg = opmsg % (server['name'], server['id'], new_status)
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
        self._try_until_timeout_expires(opmsg, check_fun)

    def _insist_on_network_transition(self, network,
                                      curr_statuses, new_status):
        """Insist on network transiting from curr_statuses to new_status"""
        def check_fun():
            """Check network status"""
            ntw = self.clients.cyclades.get_network_details(network['id'])
            if ntw['status'] in curr_statuses:
                raise Retry()
            elif ntw['status'] == new_status:
                return
            else:
                msg = "Network %s with id %s went to unexpected status %s"
                self.error(msg, network['name'], network['id'], ntw['status'])
                self.fail(msg %
                          (network['name'], network['id'], ntw['status']))
212
213
214
        opmsg = "Waiting for network \"%s\" with id %s to become %s"
        self.info(opmsg, network['name'], network['id'], new_status)
        opmsg = opmsg % (network['name'], network['id'], new_status)
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
        self._try_until_timeout_expires(opmsg, check_fun)

    def _insist_on_network_connection(self, server, network, disconnect=False):
        """Insist that the server has connected to the network"""
        def check_fun():
            """Check network connection"""
            dsrv = self._get_server_details(server, quiet=True)
            nets = [s['network_id'] for s in dsrv['attachments']]
            if not disconnect and network['id'] not in nets:
                raise Retry()
            if disconnect and network['id'] in nets:
                raise Retry()
        if disconnect:
            opmsg = \
                "Waiting for server \"%s\" to disconnect from network \"%s\""
        else:
            opmsg = "Waiting for server \"%s\" to connect to network \"%s\""
        self.info(opmsg, server['name'], network['name'])
        opmsg = opmsg % (server['name'], network['name'])
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
264
265
        self._try_until_timeout_expires(opmsg, check_fun)

    def _insist_on_tcp_connection(self, family, host, port):
        """Insist on tcp connection"""
        def check_fun():
            """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):
                fam, socktype, proto, _, saddr = res
                try:
                    sock = socket.socket(fam, socktype, proto)
                except socket.error:
                    sock = None
                    continue
                try:
                    sock.connect(saddr)
                except socket.error:
                    sock.close()
                    sock = None
                    continue
            if sock is None:
                raise Retry
            return sock
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
        opmsg = "Connecting over %s to %s:%s"
        self.info(opmsg, familystr.get(family, "Unknown"), host, port)
        opmsg = opmsg % (familystr.get(family, "Unknown"), host, port)
        return self._try_until_timeout_expires(opmsg, check_fun)

266
267
268
269
270
271
272
    def _get_ip(self, server, version=4, network=None):
        """Get the IP of a server from the detailed server info

        If network not given then get the public IP. Else the ip
        attached to that network

        """
273
274
275
        assert version in (4, 6)

        nics = server['attachments']
276
        addrs = None
277
278
        for nic in nics:
            net_id = nic['network_id']
279
280
            if network is None:
                if self.clients.cyclades.get_network_details(net_id)['public']:
281
282
283
                    if nic['ipv' + str(version)]:
                        addrs = nic['ipv' + str(version)]
                        break
284
285
            else:
                if net_id == network['id']:
286
287
288
                    if nic['ipv' + str(version)]:
                        addrs = nic['ipv' + str(version)]
                        break
289

290
291
292
293
294
295
296
297
        self.assertIsNotNone(addrs, "Can not get IP from server attachments")
        if network is None:
            msg = "Server's public IPv%s is %s"
            self.info(msg, version, addrs)
        else:
            msg = "Server's IPv%s attached to network \"%s\" is %s"
            self.info(msg, version, network['id'], addrs)
        return addrs
298

299
    def _insist_on_ping(self, ip_addr, version=4):
300
301
302
303
304
305
306
307
308
309
310
311
        """Test server responds to a single IPv4 of IPv6 ping"""
        def check_fun():
            """Ping to server"""
            cmd = ("ping%s -c 3 -w 20 %s" %
                   ("6" if version == 6 else "", ip_addr))
            ping = subprocess.Popen(
                cmd, shell=True, stdout=subprocess.PIPE,
                stderr=subprocess.PIPE)
            ping.communicate()
            ret = ping.wait()
            if ret != 0:
                raise Retry
312
        assert version in (4, 6)
313
314
315
316
317
        opmsg = "Sent IPv%s ping requests to %s"
        self.info(opmsg, version, ip_addr)
        opmsg = opmsg % (version, ip_addr)
        self._try_until_timeout_expires(opmsg, check_fun)

318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
    def _image_is(self, image, osfamily):
        """Return true if the image is of `osfamily'"""
        d_image = self.clients.cyclades.get_image_details(image['id'])
        return d_image['metadata']['osfamily'].lower().find(osfamily) >= 0

    def _ssh_execute(self, hostip, username, password, command):
        """Execute a command via ssh"""
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        try:
            ssh.connect(hostip, username=username, password=password)
        except socket.error as err:
            self.fail(err)
        try:
            _, stdout, _ = ssh.exec_command(command)
        except paramiko.SSHException as err:
            self.fail(err)
        status = stdout.channel.recv_exit_status()
        output = stdout.readlines()
        ssh.close()
        return output, status

    def _insist_get_hostname_over_ssh(self, hostip, username, password):
        """Connect to server using ssh and get it's hostname"""
        def check_fun():
            """Get hostname"""
            try:
                lines, status = self._ssh_execute(
                    hostip, username, password, "hostname")
                self.assertEqual(status, 0)
                self.assertEqual(len(lines), 1)
                # Remove new line
                return lines[0].strip('\n')
            except AssertionError:
                raise Retry()
        opmsg = "Connecting to server using ssh and get it's hostname"
        self.info(opmsg)
        hostname = self._try_until_timeout_expires(opmsg, check_fun)
        self.info("Server's hostname is %s", hostname)
        return hostname

    # Too many arguments. pylint: disable-msg=R0913
    def _check_file_through_ssh(self, hostip, username, password,
                                remotepath, content):
        """Fetch file from server and compare contents"""
        self.info("Fetching file %s from remote server", remotepath)
        transport = paramiko.Transport((hostip, 22))
        transport.connect(username=username, password=password)
        with tempfile.NamedTemporaryFile() as ftmp:
            sftp = paramiko.SFTPClient.from_transport(transport)
            sftp.get(remotepath, ftmp.name)
            sftp.close()
            transport.close()
            self.info("Comparing file contents")
            remote_content = base64.b64encode(ftmp.read())
            self.assertEqual(content, remote_content)

375
376
377
378
379
380
381
382
383
384
    def _disconnect_from_network(self, server, network):
        """Disconnect server from network"""
        nid = None
        for nic in server['attachments']:
            if nic['network_id'] == network['id']:
                nid = nic['id']
                break
        self.assertIsNotNone(nid, "Could not find network card")
        self.clients.cyclades.disconnect_server(server['id'], nid)

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
385
386
387
388
389
390
391
392
393
394
395
396
397
398
    def _create_network(self, name, cidr="10.0.1.0/28", dhcp=True):
        """Create a new private network"""
        network = self.clients.cyclades.create_network(
            name, cidr=cidr, dhcp=dhcp)
        self.info("Network with id %s created", network['id'])

        # Verify quotas
        self._check_quotas(network=+1)

        #Test if the right name is assigned
        self.assertEqual(network['name'], name)

        return network

399
400
401
402
403
404
405

class Retry(Exception):
    """Retry the action

    This is used by _try_unit_timeout_expires method.

    """