Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / cyclades_common.py @ 5bef1f49

History | View | Annotate | Download (14.9 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
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
        return server
131

    
132
    def _get_connection_username(self, server):
133
        """Determine the username to use to connect to the server"""
134
        users = server['metadata'].get("users", None)
135
        ret_user = None
136
        if users is not None:
137
            user_list = users.split()
138
            if "root" in user_list:
139
                ret_user = "root"
140
            else:
141
                ret_user = random.choice(user_list)
142
        else:
143
            # Return the login name for connections based on the server OS
144
            self.info("Could not find `users' metadata in server. Let's guess")
145
            os_value = server['metadata'].get("os")
146
            if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
147
                ret_user = "user"
148
            elif os_value in ("windows", "windows_alpha1"):
149
                ret_user = "Administrator"
150
            else:
151
                ret_user = "root"
152

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

    
157
    def _insist_on_server_transition(self, server, curr_statuses, new_status):
158
        """Insist on server transiting from curr_statuses to new_status"""
159
        def check_fun():
160
            """Check server status"""
161
            srv = self._get_server_details(server, quiet=True)
162
            if srv['status'] in curr_statuses:
163
                raise Retry()
164
            elif srv['status'] == new_status:
165
                return
166
            else:
167
                msg = "Server \"%s\" with id %s went to unexpected status %s"
168
                self.error(msg, server['name'], server['id'], srv['status'])
169
                self.fail(msg % (server['name'], server['id'], srv['status']))
170
        opmsg = "Waiting for server \"%s\" with id %s to become %s"
171
        self.info(opmsg, server['name'], server['id'], new_status)
172
        opmsg = opmsg % (server['name'], server['id'], new_status)
173
        self._try_until_timeout_expires(opmsg, check_fun)
174

    
175
    def _insist_on_network_transition(self, network,
176
                                      curr_statuses, new_status):
177
        """Insist on network transiting from curr_statuses to new_status"""
178
        def check_fun():
179
            """Check network status"""
180
            ntw = self.clients.cyclades.get_network_details(network['id'])
181
            if ntw['status'] in curr_statuses:
182
                raise Retry()
183
            elif ntw['status'] == new_status:
184
                return
185
            else:
186
                msg = "Network %s with id %s went to unexpected status %s"
187
                self.error(msg, network['name'], network['id'], ntw['status'])
188
                self.fail(msg %
189
                          (network['name'], network['id'], ntw['status']))
190
        opmsg = "Waiting for network \"%s\" with id %s to become %s"
191
        self.info(opmsg, network['name'], network['id'], new_status)
192
        opmsg = opmsg % (network['name'], network['id'], new_status)
193
        self._try_until_timeout_expires(opmsg, check_fun)
194

    
195
    def _insist_on_network_connection(self, server, network, disconnect=False):
196
        """Insist that the server has connected to the network"""
197
        def check_fun():
198
            """Check network connection"""
199
            dsrv = self._get_server_details(server, quiet=True)
200
            nets = [s['network_id'] for s in dsrv['attachments']]
201
            if not disconnect and network['id'] not in nets:
202
                raise Retry()
203
            if disconnect and network['id'] in nets:
204
                raise Retry()
205
        if disconnect:
206
            opmsg = \
207
                "Waiting for server \"%s\" to disconnect from network \"%s\""
208
        else:
209
            opmsg = "Waiting for server \"%s\" to connect to network \"%s\""
210
        self.info(opmsg, server['name'], network['name'])
211
        opmsg = opmsg % (server['name'], network['name'])
212
        self._try_until_timeout_expires(opmsg, check_fun)
213

    
214
    def _insist_on_tcp_connection(self, family, host, port):
215
        """Insist on tcp connection"""
216
        def check_fun():
217
            """Get a connected socket from the specified family to host:port"""
218
            sock = None
219
            for res in socket.getaddrinfo(host, port, family,
220
                                          socket.SOCK_STREAM, 0,
221
                                          socket.AI_PASSIVE):
222
                fam, socktype, proto, _, saddr = res
223
                try:
224
                    sock = socket.socket(fam, socktype, proto)
225
                except socket.error:
226
                    sock = None
227
                    continue
228
                try:
229
                    sock.connect(saddr)
230
                except socket.error:
231
                    sock.close()
232
                    sock = None
233
                    continue
234
            if sock is None:
235
                raise Retry
236
            return sock
237
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
238
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
239
        opmsg = "Connecting over %s to %s:%s"
240
        self.info(opmsg, familystr.get(family, "Unknown"), host, port)
241
        opmsg = opmsg % (familystr.get(family, "Unknown"), host, port)
242
        return self._try_until_timeout_expires(opmsg, check_fun)
243

    
244
    def _get_ip(self, server, version=4, network=None):
245
        """Get the IP of a server from the detailed server info
246

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

250
        """
251
        assert version in (4, 6)
252

    
253
        nics = server['attachments']
254
        addrs = None
255
        for nic in nics:
256
            net_id = nic['network_id']
257
            if network is None:
258
                if self.clients.cyclades.get_network_details(net_id)['public']:
259
                    if nic['ipv' + str(version)]:
260
                        addrs = nic['ipv' + str(version)]
261
                        break
262
            else:
263
                if net_id == network['id']:
264
                    if nic['ipv' + str(version)]:
265
                        addrs = nic['ipv' + str(version)]
266
                        break
267

    
268
        self.assertIsNotNone(addrs, "Can not get IP from server attachments")
269
        if network is None:
270
            msg = "Server's public IPv%s is %s"
271
            self.info(msg, version, addrs)
272
        else:
273
            msg = "Server's IPv%s attached to network \"%s\" is %s"
274
            self.info(msg, version, network['id'], addrs)
275
        return addrs
276

    
277
    def _insist_on_ping(self, ip_addr, version=4):
278
        """Test server responds to a single IPv4 of IPv6 ping"""
279
        def check_fun():
280
            """Ping to server"""
281
            cmd = ("ping%s -c 3 -w 20 %s" %
282
                   ("6" if version == 6 else "", ip_addr))
283
            ping = subprocess.Popen(
284
                cmd, shell=True, stdout=subprocess.PIPE,
285
                stderr=subprocess.PIPE)
286
            ping.communicate()
287
            ret = ping.wait()
288
            if ret != 0:
289
                raise Retry
290
        assert version in (4, 6)
291
        opmsg = "Sent IPv%s ping requests to %s"
292
        self.info(opmsg, version, ip_addr)
293
        opmsg = opmsg % (version, ip_addr)
294
        self._try_until_timeout_expires(opmsg, check_fun)
295

    
296
    def _image_is(self, image, osfamily):
297
        """Return true if the image is of `osfamily'"""
298
        d_image = self.clients.cyclades.get_image_details(image['id'])
299
        return d_image['metadata']['osfamily'].lower().find(osfamily) >= 0
300

    
301
    def _ssh_execute(self, hostip, username, password, command):
302
        """Execute a command via ssh"""
303
        ssh = paramiko.SSHClient()
304
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
305
        try:
306
            ssh.connect(hostip, username=username, password=password)
307
        except socket.error as err:
308
            self.fail(err)
309
        try:
310
            _, stdout, _ = ssh.exec_command(command)
311
        except paramiko.SSHException as err:
312
            self.fail(err)
313
        status = stdout.channel.recv_exit_status()
314
        output = stdout.readlines()
315
        ssh.close()
316
        return output, status
317

    
318
    def _insist_get_hostname_over_ssh(self, hostip, username, password):
319
        """Connect to server using ssh and get it's hostname"""
320
        def check_fun():
321
            """Get hostname"""
322
            try:
323
                lines, status = self._ssh_execute(
324
                    hostip, username, password, "hostname")
325
                self.assertEqual(status, 0)
326
                self.assertEqual(len(lines), 1)
327
                # Remove new line
328
                return lines[0].strip('\n')
329
            except AssertionError:
330
                raise Retry()
331
        opmsg = "Connecting to server using ssh and get it's hostname"
332
        self.info(opmsg)
333
        hostname = self._try_until_timeout_expires(opmsg, check_fun)
334
        self.info("Server's hostname is %s", hostname)
335
        return hostname
336

    
337
    # Too many arguments. pylint: disable-msg=R0913
338
    def _check_file_through_ssh(self, hostip, username, password,
339
                                remotepath, content):
340
        """Fetch file from server and compare contents"""
341
        self.info("Fetching file %s from remote server", remotepath)
342
        transport = paramiko.Transport((hostip, 22))
343
        transport.connect(username=username, password=password)
344
        with tempfile.NamedTemporaryFile() as ftmp:
345
            sftp = paramiko.SFTPClient.from_transport(transport)
346
            sftp.get(remotepath, ftmp.name)
347
            sftp.close()
348
            transport.close()
349
            self.info("Comparing file contents")
350
            remote_content = base64.b64encode(ftmp.read())
351
            self.assertEqual(content, remote_content)
352

    
353
    def _disconnect_from_network(self, server, network):
354
        """Disconnect server from network"""
355
        nid = None
356
        for nic in server['attachments']:
357
            if nic['network_id'] == network['id']:
358
                nid = nic['id']
359
                break
360
        self.assertIsNotNone(nid, "Could not find network card")
361
        self.clients.cyclades.disconnect_server(server['id'], nid)
362

    
363

    
364
class Retry(Exception):
365
    """Retry the action
366

367
    This is used by _try_unit_timeout_expires method.
368

369
    """