Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / cyclades_common.py @ cee3ee9b

History | View | Annotate | Download (11.1 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 = time.time()
65
        while (start_time + action_warning) > time.time():
66
            try:
67
                return check_fun()
68
            except Retry:
69
                time.sleep(self.query_interval)
70
        self.warning("Operation `%s' is taking too long", opmsg)
71
        while (start_time + action_timeout) > time.time():
72
            try:
73
                return check_fun()
74
            except Retry:
75
                time.sleep(self.query_interval)
76
        self.error("Operation `%s' timed out", opmsg)
77
        self.fail("time out")
78

    
79
    def _get_list_of_servers(self, detail=False):
80
        """Get (detailed) list of servers"""
81
        if detail:
82
            self.info("Getting detailed list of servers")
83
        else:
84
            self.info("Getting simple list of servers")
85
        return self.clients.cyclades.list_servers(detail=detail)
86

    
87
    def _get_server_details(self, server):
88
        """Get details for a server"""
89
        self.info("Getting details for server %s with id %s",
90
                  server['name'], server['id'])
91
        return self.clients.cyclades.get_server_details(server['id'])
92

    
93
    def _create_server(self, name, image, flavor, personality):
94
        """Create a new server"""
95
        self.info("Creating a server with name %s", name)
96
        self.info("Using image %s with id %s", image['name'], image['id'])
97
        self.info("Using flavor %s with id %s", flavor['name'], flavor['id'])
98
        server = self.clients.cyclades.create_server(
99
            name, flavor['id'], image['id'], personality=personality)
100

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

    
104
        self.assertEqual(server['name'], name)
105
        self.assertEqual(server['flavor']['id'], flavor['id'])
106
        self.assertEqual(server['image']['id'], image['id'])
107
        self.assertEqual(server['status'], "BUILD")
108

    
109
        return server
110

    
111
    def _get_connection_username(self, server):
112
        """Determine the username to use to connect to the server"""
113
        users = server['metadata'].get("users", None)
114
        ret_user = None
115
        if users is not None:
116
            user_list = users.split()
117
            if "root" in user_list:
118
                ret_user = "root"
119
            else:
120
                ret_user = random.choice(user_list)
121
        else:
122
            # Return the login name for connections based on the server OS
123
            self.info("Could not find `users' metadata in server. Let's guess")
124
            os_value = server['metadata'].get("os")
125
            if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
126
                ret_user = "user"
127
            elif os_value in ("windows", "windows_alpha1"):
128
                ret_user = "Administrator"
129
            else:
130
                ret_user = "root"
131

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

    
136
    def _insist_on_server_transition(self, server, curr_status, new_status):
137
        """Insist on server transiting from curr_status to new_status"""
138
        def check_fun():
139
            """Check server status"""
140
            srv = self.clients.cyclades.get_server_details(server['id'])
141
            if srv['status'] == curr_status:
142
                raise Retry()
143
            elif srv['status'] == new_status:
144
                return
145
            else:
146
                msg = "Server %s went to unexpected status %s"
147
                self.error(msg, server['name'], srv['status'])
148
                self.fail(msg % (server['name'], srv['status']))
149
        opmsg = "Waiting for server %s to transit from %s to %s"
150
        self.info(opmsg, server['name'], curr_status, new_status)
151
        opmsg = opmsg % (server['name'], curr_status, new_status)
152
        self._try_until_timeout_expires(opmsg, check_fun)
153

    
154
    def _insist_on_tcp_connection(self, family, host, port):
155
        """Insist on tcp connection"""
156
        def check_fun():
157
            """Get a connected socket from the specified family to host:port"""
158
            sock = None
159
            for res in socket.getaddrinfo(host, port, family,
160
                                          socket.SOCK_STREAM, 0,
161
                                          socket.AI_PASSIVE):
162
                fam, socktype, proto, _, saddr = res
163
                try:
164
                    sock = socket.socket(fam, socktype, proto)
165
                except socket.error:
166
                    sock = None
167
                    continue
168
                try:
169
                    sock.connect(saddr)
170
                except socket.error:
171
                    sock.close()
172
                    sock = None
173
                    continue
174
            if sock is None:
175
                raise Retry
176
            return sock
177
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
178
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
179
        opmsg = "Connecting over %s to %s:%s"
180
        self.info(opmsg, familystr.get(family, "Unknown"), host, port)
181
        opmsg = opmsg % (familystr.get(family, "Unknown"), host, port)
182
        return self._try_until_timeout_expires(opmsg, check_fun)
183

    
184
    def _get_ip(self, server, version=4):
185
        """Get the public IP of a server from the detailed server info"""
186
        assert version in (4, 6)
187

    
188
        nics = server['attachments']
189
        public_addrs = None
190
        for nic in nics:
191
            net_id = nic['network_id']
192
            if self.clients.cyclades.get_network_details(net_id)['public']:
193
                public_addrs = nic['ipv' + str(version)]
194

    
195
        self.assertIsNotNone(public_addrs)
196
        msg = "Server's public IPv%s is %s"
197
        self.info(msg, version, public_addrs)
198
        return public_addrs
199

    
200
    def _insist_on_ping(self, ip_addr, version=4):
201
        """Test server responds to a single IPv4 of IPv6 ping"""
202
        def check_fun():
203
            """Ping to server"""
204
            cmd = ("ping%s -c 3 -w 20 %s" %
205
                   ("6" if version == 6 else "", ip_addr))
206
            ping = subprocess.Popen(
207
                cmd, shell=True, stdout=subprocess.PIPE,
208
                stderr=subprocess.PIPE)
209
            ping.communicate()
210
            ret = ping.wait()
211
            if ret != 0:
212
                raise Retry
213
        assert version in (4, 6)
214
        opmsg = "Sent IPv%s ping requests to %s"
215
        self.info(opmsg, version, ip_addr)
216
        opmsg = opmsg % (version, ip_addr)
217
        self._try_until_timeout_expires(opmsg, check_fun)
218

    
219
    def _image_is(self, image, osfamily):
220
        """Return true if the image is of `osfamily'"""
221
        d_image = self.clients.cyclades.get_image_details(image['id'])
222
        return d_image['metadata']['osfamily'].lower().find(osfamily) >= 0
223

    
224
    def _ssh_execute(self, hostip, username, password, command):
225
        """Execute a command via ssh"""
226
        ssh = paramiko.SSHClient()
227
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
228
        try:
229
            ssh.connect(hostip, username=username, password=password)
230
        except socket.error as err:
231
            self.fail(err)
232
        try:
233
            _, stdout, _ = ssh.exec_command(command)
234
        except paramiko.SSHException as err:
235
            self.fail(err)
236
        status = stdout.channel.recv_exit_status()
237
        output = stdout.readlines()
238
        ssh.close()
239
        return output, status
240

    
241
    def _insist_get_hostname_over_ssh(self, hostip, username, password):
242
        """Connect to server using ssh and get it's hostname"""
243
        def check_fun():
244
            """Get hostname"""
245
            try:
246
                lines, status = self._ssh_execute(
247
                    hostip, username, password, "hostname")
248
                self.assertEqual(status, 0)
249
                self.assertEqual(len(lines), 1)
250
                # Remove new line
251
                return lines[0].strip('\n')
252
            except AssertionError:
253
                raise Retry()
254
        opmsg = "Connecting to server using ssh and get it's hostname"
255
        self.info(opmsg)
256
        hostname = self._try_until_timeout_expires(opmsg, check_fun)
257
        self.info("Server's hostname is %s", hostname)
258
        return hostname
259

    
260
    # Too many arguments. pylint: disable-msg=R0913
261
    def _check_file_through_ssh(self, hostip, username, password,
262
                                remotepath, content):
263
        """Fetch file from server and compare contents"""
264
        self.info("Fetching file %s from remote server", remotepath)
265
        transport = paramiko.Transport((hostip, 22))
266
        transport.connect(username=username, password=password)
267
        with tempfile.NamedTemporaryFile() as ftmp:
268
            sftp = paramiko.SFTPClient.from_transport(transport)
269
            sftp.get(remotepath, ftmp.name)
270
            sftp.close()
271
            transport.close()
272
            self.info("Comparing file contents")
273
            remote_content = base64.b64encode(ftmp.read())
274
            self.assertEqual(content, remote_content)
275

    
276

    
277
class Retry(Exception):
278
    """Retry the action
279

280
    This is used by _try_unit_timeout_expires method.
281

282
    """