Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.2 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
Utility functions for Cyclades Tests
36
Cyclades require a lot helper functions and `common'
37
had grown too much.
38

39
"""
40

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

    
49
from synnefo_tools.burnin.common import BurninTests, MB, GB
50

    
51

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

    
59
        action_timeout = self.action_timeout
60
        action_warning = self.action_warning
61
        if action_warning > action_timeout:
62
            action_warning = action_timeout
63

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

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

    
90
    def _get_list_of_servers(self, detail=False):
91
        """Get (detailed) list of servers"""
92
        if detail:
93
            self.info("Getting detailed list of servers")
94
        else:
95
            self.info("Getting simple list of servers")
96
        return self.clients.cyclades.list_servers(detail=detail)
97

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

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

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

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

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

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

    
136
        return server
137

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

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

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

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

    
197
    def _insist_on_network_transition(self, network,
198
                                      curr_statuses, new_status):
199
        """Insist on network transiting from curr_statuses to new_status"""
200
        def check_fun():
201
            """Check network status"""
202
            ntw = self.clients.cyclades.get_network_details(network['id'])
203
            if ntw['status'] in curr_statuses:
204
                raise Retry()
205
            elif ntw['status'] == new_status:
206
                return
207
            else:
208
                msg = "Network %s with id %s went to unexpected status %s"
209
                self.error(msg, network['name'], network['id'], ntw['status'])
210
                self.fail(msg %
211
                          (network['name'], network['id'], ntw['status']))
212
        opmsg = "Waiting for network \"%s\" with id %s to become %s"
213
        self.info(opmsg, network['name'], network['id'], new_status)
214
        opmsg = opmsg % (network['name'], network['id'], new_status)
215
        self._try_until_timeout_expires(opmsg, check_fun)
216

    
217
    def _insist_on_network_connection(self, server, network, disconnect=False):
218
        """Insist that the server has connected to the network"""
219
        def check_fun():
220
            """Check network connection"""
221
            dsrv = self._get_server_details(server, quiet=True)
222
            nets = [s['network_id'] for s in dsrv['attachments']]
223
            if not disconnect and network['id'] not in nets:
224
                raise Retry()
225
            if disconnect and network['id'] in nets:
226
                raise Retry()
227
        if disconnect:
228
            opmsg = \
229
                "Waiting for server \"%s\" to disconnect from network \"%s\""
230
        else:
231
            opmsg = "Waiting for server \"%s\" to connect to network \"%s\""
232
        self.info(opmsg, server['name'], network['name'])
233
        opmsg = opmsg % (server['name'], network['name'])
234
        self._try_until_timeout_expires(opmsg, check_fun)
235

    
236
    def _insist_on_tcp_connection(self, family, host, port):
237
        """Insist on tcp connection"""
238
        def check_fun():
239
            """Get a connected socket from the specified family to host:port"""
240
            sock = None
241
            for res in socket.getaddrinfo(host, port, family,
242
                                          socket.SOCK_STREAM, 0,
243
                                          socket.AI_PASSIVE):
244
                fam, socktype, proto, _, saddr = res
245
                try:
246
                    sock = socket.socket(fam, socktype, proto)
247
                except socket.error:
248
                    sock = None
249
                    continue
250
                try:
251
                    sock.connect(saddr)
252
                except socket.error:
253
                    sock.close()
254
                    sock = None
255
                    continue
256
            if sock is None:
257
                raise Retry
258
            return sock
259
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
260
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
261
        opmsg = "Connecting over %s to %s:%s"
262
        self.info(opmsg, familystr.get(family, "Unknown"), host, port)
263
        opmsg = opmsg % (familystr.get(family, "Unknown"), host, port)
264
        return self._try_until_timeout_expires(opmsg, check_fun)
265

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

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

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

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

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

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

    
318
    def _image_is(self, image, osfamily):
319
        """Return true if the image is of `osfamily'"""
320
        d_image = self.clients.cyclades.get_image_details(image['id'])
321
        return d_image['metadata']['osfamily'].lower().find(osfamily) >= 0
322

    
323
    def _ssh_execute(self, hostip, username, password, command):
324
        """Execute a command via ssh"""
325
        ssh = paramiko.SSHClient()
326
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
327
        try:
328
            ssh.connect(hostip, username=username, password=password)
329
        except socket.error as err:
330
            self.fail(err)
331
        try:
332
            _, stdout, _ = ssh.exec_command(command)
333
        except paramiko.SSHException as err:
334
            self.fail(err)
335
        status = stdout.channel.recv_exit_status()
336
        output = stdout.readlines()
337
        ssh.close()
338
        return output, status
339

    
340
    def _insist_get_hostname_over_ssh(self, hostip, username, password):
341
        """Connect to server using ssh and get it's hostname"""
342
        def check_fun():
343
            """Get hostname"""
344
            try:
345
                lines, status = self._ssh_execute(
346
                    hostip, username, password, "hostname")
347
                self.assertEqual(status, 0)
348
                self.assertEqual(len(lines), 1)
349
                # Remove new line
350
                return lines[0].strip('\n')
351
            except AssertionError:
352
                raise Retry()
353
        opmsg = "Connecting to server using ssh and get it's hostname"
354
        self.info(opmsg)
355
        hostname = self._try_until_timeout_expires(opmsg, check_fun)
356
        self.info("Server's hostname is %s", hostname)
357
        return hostname
358

    
359
    # Too many arguments. pylint: disable-msg=R0913
360
    def _check_file_through_ssh(self, hostip, username, password,
361
                                remotepath, content):
362
        """Fetch file from server and compare contents"""
363
        self.info("Fetching file %s from remote server", remotepath)
364
        transport = paramiko.Transport((hostip, 22))
365
        transport.connect(username=username, password=password)
366
        with tempfile.NamedTemporaryFile() as ftmp:
367
            sftp = paramiko.SFTPClient.from_transport(transport)
368
            sftp.get(remotepath, ftmp.name)
369
            sftp.close()
370
            transport.close()
371
            self.info("Comparing file contents")
372
            remote_content = base64.b64encode(ftmp.read())
373
            self.assertEqual(content, remote_content)
374

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

    
385
    def _create_network(self, name, cidr="10.0.1.0/28", dhcp=True):
386
        """Create a new private network"""
387
        network = self.clients.cyclades.create_network(
388
            name, cidr=cidr, dhcp=dhcp)
389
        self.info("Network with id %s created", network['id'])
390

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

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

    
397
        return network
398

    
399

    
400
class Retry(Exception):
401
    """Retry the action
402

403
    This is used by _try_unit_timeout_expires method.
404

405
    """