Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / cyclades_common.py @ 8c67f82e

History | View | Annotate | Download (11.6 kB)

1 d246be88 Ilias Tsitsimpis
# Copyright 2013 GRNET S.A. All rights reserved.
2 d246be88 Ilias Tsitsimpis
#
3 d246be88 Ilias Tsitsimpis
# Redistribution and use in source and binary forms, with or
4 d246be88 Ilias Tsitsimpis
# without modification, are permitted provided that the following
5 d246be88 Ilias Tsitsimpis
# conditions are met:
6 d246be88 Ilias Tsitsimpis
#
7 d246be88 Ilias Tsitsimpis
#   1. Redistributions of source code must retain the above
8 d246be88 Ilias Tsitsimpis
#      copyright notice, this list of conditions and the following
9 d246be88 Ilias Tsitsimpis
#      disclaimer.
10 d246be88 Ilias Tsitsimpis
#
11 d246be88 Ilias Tsitsimpis
#   2. Redistributions in binary form must reproduce the above
12 d246be88 Ilias Tsitsimpis
#      copyright notice, this list of conditions and the following
13 d246be88 Ilias Tsitsimpis
#      disclaimer in the documentation and/or other materials
14 d246be88 Ilias Tsitsimpis
#      provided with the distribution.
15 d246be88 Ilias Tsitsimpis
#
16 d246be88 Ilias Tsitsimpis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 d246be88 Ilias Tsitsimpis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 d246be88 Ilias Tsitsimpis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 d246be88 Ilias Tsitsimpis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 d246be88 Ilias Tsitsimpis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 d246be88 Ilias Tsitsimpis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 d246be88 Ilias Tsitsimpis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 d246be88 Ilias Tsitsimpis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 d246be88 Ilias Tsitsimpis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 d246be88 Ilias Tsitsimpis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 d246be88 Ilias Tsitsimpis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 d246be88 Ilias Tsitsimpis
# POSSIBILITY OF SUCH DAMAGE.
28 d246be88 Ilias Tsitsimpis
#
29 d246be88 Ilias Tsitsimpis
# The views and conclusions contained in the software and
30 d246be88 Ilias Tsitsimpis
# documentation are those of the authors and should not be
31 d246be88 Ilias Tsitsimpis
# interpreted as representing official policies, either expressed
32 d246be88 Ilias Tsitsimpis
# or implied, of GRNET S.A.
33 d246be88 Ilias Tsitsimpis
34 d246be88 Ilias Tsitsimpis
"""
35 d246be88 Ilias Tsitsimpis
Utility functions for Cyclades Tests
36 d246be88 Ilias Tsitsimpis
Cyclades require a lot helper functions and `common'
37 d246be88 Ilias Tsitsimpis
had grown too much.
38 d246be88 Ilias Tsitsimpis

39 d246be88 Ilias Tsitsimpis
"""
40 d246be88 Ilias Tsitsimpis
41 d246be88 Ilias Tsitsimpis
import time
42 cee3ee9b Ilias Tsitsimpis
import base64
43 d246be88 Ilias Tsitsimpis
import socket
44 d246be88 Ilias Tsitsimpis
import random
45 cee3ee9b Ilias Tsitsimpis
import paramiko
46 cee3ee9b Ilias Tsitsimpis
import tempfile
47 d246be88 Ilias Tsitsimpis
import subprocess
48 d246be88 Ilias Tsitsimpis
49 d246be88 Ilias Tsitsimpis
from synnefo_tools.burnin.common import BurninTests
50 d246be88 Ilias Tsitsimpis
51 d246be88 Ilias Tsitsimpis
52 d246be88 Ilias Tsitsimpis
# Too many public methods. pylint: disable-msg=R0904
53 d246be88 Ilias Tsitsimpis
class CycladesTests(BurninTests):
54 d246be88 Ilias Tsitsimpis
    """Extends the BurninTests class for Cyclades"""
55 8c67f82e Ilias Tsitsimpis
    def _ry_until_timeout_expires(self, opmsg, check_fun):
56 d246be88 Ilias Tsitsimpis
        """Try to perform an action until timeout expires"""
57 d246be88 Ilias Tsitsimpis
        assert callable(check_fun), "Not a function"
58 d246be88 Ilias Tsitsimpis
59 d246be88 Ilias Tsitsimpis
        action_timeout = self.action_timeout
60 d246be88 Ilias Tsitsimpis
        action_warning = self.action_warning
61 d246be88 Ilias Tsitsimpis
        if action_warning > action_timeout:
62 d246be88 Ilias Tsitsimpis
            action_warning = action_timeout
63 d246be88 Ilias Tsitsimpis
64 8c67f82e Ilias Tsitsimpis
        start_time = int(time.time())
65 8c67f82e Ilias Tsitsimpis
        end_time = start_time + action_warning
66 8c67f82e Ilias Tsitsimpis
        while end_time > time.time():
67 d246be88 Ilias Tsitsimpis
            try:
68 8c67f82e Ilias Tsitsimpis
                ret_value = check_fun()
69 8c67f82e Ilias Tsitsimpis
                self.info("Operation `%s' finished in %s seconds",
70 8c67f82e Ilias Tsitsimpis
                          opmsg, int(time.time()) - start_time)
71 8c67f82e Ilias Tsitsimpis
                return ret_value
72 d246be88 Ilias Tsitsimpis
            except Retry:
73 d246be88 Ilias Tsitsimpis
                time.sleep(self.query_interval)
74 8c67f82e Ilias Tsitsimpis
        self.warning("Operation `%s' is taking too long after %s seconds",
75 8c67f82e Ilias Tsitsimpis
                     opmsg, int(time.time()) - start_time)
76 8c67f82e Ilias Tsitsimpis
77 8c67f82e Ilias Tsitsimpis
        end_time = start_time + action_timeout
78 8c67f82e Ilias Tsitsimpis
        while end_time > time.time():
79 d246be88 Ilias Tsitsimpis
            try:
80 8c67f82e Ilias Tsitsimpis
                ret_value = check_fun()
81 8c67f82e Ilias Tsitsimpis
                self.info("Operation `%s' finished in %s seconds",
82 8c67f82e Ilias Tsitsimpis
                          opmsg, int(time.time()) - start_time)
83 8c67f82e Ilias Tsitsimpis
                return ret_value
84 d246be88 Ilias Tsitsimpis
            except Retry:
85 d246be88 Ilias Tsitsimpis
                time.sleep(self.query_interval)
86 8c67f82e Ilias Tsitsimpis
        self.error("Operation `%s' timed out after %s seconds",
87 8c67f82e Ilias Tsitsimpis
                   opmsg, int(time.time()) - start_time)
88 d246be88 Ilias Tsitsimpis
        self.fail("time out")
89 d246be88 Ilias Tsitsimpis
90 d246be88 Ilias Tsitsimpis
    def _get_list_of_servers(self, detail=False):
91 d246be88 Ilias Tsitsimpis
        """Get (detailed) list of servers"""
92 d246be88 Ilias Tsitsimpis
        if detail:
93 d246be88 Ilias Tsitsimpis
            self.info("Getting detailed list of servers")
94 d246be88 Ilias Tsitsimpis
        else:
95 d246be88 Ilias Tsitsimpis
            self.info("Getting simple list of servers")
96 d246be88 Ilias Tsitsimpis
        return self.clients.cyclades.list_servers(detail=detail)
97 d246be88 Ilias Tsitsimpis
98 d246be88 Ilias Tsitsimpis
    def _get_server_details(self, server):
99 d246be88 Ilias Tsitsimpis
        """Get details for a server"""
100 d246be88 Ilias Tsitsimpis
        self.info("Getting details for server %s with id %s",
101 d246be88 Ilias Tsitsimpis
                  server['name'], server['id'])
102 d246be88 Ilias Tsitsimpis
        return self.clients.cyclades.get_server_details(server['id'])
103 d246be88 Ilias Tsitsimpis
104 cee3ee9b Ilias Tsitsimpis
    def _create_server(self, name, image, flavor, personality):
105 d246be88 Ilias Tsitsimpis
        """Create a new server"""
106 d246be88 Ilias Tsitsimpis
        self.info("Creating a server with name %s", name)
107 d246be88 Ilias Tsitsimpis
        self.info("Using image %s with id %s", image['name'], image['id'])
108 d246be88 Ilias Tsitsimpis
        self.info("Using flavor %s with id %s", flavor['name'], flavor['id'])
109 d246be88 Ilias Tsitsimpis
        server = self.clients.cyclades.create_server(
110 cee3ee9b Ilias Tsitsimpis
            name, flavor['id'], image['id'], personality=personality)
111 d246be88 Ilias Tsitsimpis
112 d246be88 Ilias Tsitsimpis
        self.info("Server id: %s", server['id'])
113 d246be88 Ilias Tsitsimpis
        self.info("Server password: %s", server['adminPass'])
114 d246be88 Ilias Tsitsimpis
115 d246be88 Ilias Tsitsimpis
        self.assertEqual(server['name'], name)
116 d246be88 Ilias Tsitsimpis
        self.assertEqual(server['flavor']['id'], flavor['id'])
117 d246be88 Ilias Tsitsimpis
        self.assertEqual(server['image']['id'], image['id'])
118 d246be88 Ilias Tsitsimpis
        self.assertEqual(server['status'], "BUILD")
119 d246be88 Ilias Tsitsimpis
120 d246be88 Ilias Tsitsimpis
        return server
121 d246be88 Ilias Tsitsimpis
122 d246be88 Ilias Tsitsimpis
    def _get_connection_username(self, server):
123 d246be88 Ilias Tsitsimpis
        """Determine the username to use to connect to the server"""
124 d246be88 Ilias Tsitsimpis
        users = server['metadata'].get("users", None)
125 d246be88 Ilias Tsitsimpis
        ret_user = None
126 d246be88 Ilias Tsitsimpis
        if users is not None:
127 d246be88 Ilias Tsitsimpis
            user_list = users.split()
128 d246be88 Ilias Tsitsimpis
            if "root" in user_list:
129 d246be88 Ilias Tsitsimpis
                ret_user = "root"
130 d246be88 Ilias Tsitsimpis
            else:
131 d246be88 Ilias Tsitsimpis
                ret_user = random.choice(user_list)
132 d246be88 Ilias Tsitsimpis
        else:
133 d246be88 Ilias Tsitsimpis
            # Return the login name for connections based on the server OS
134 d246be88 Ilias Tsitsimpis
            self.info("Could not find `users' metadata in server. Let's guess")
135 d246be88 Ilias Tsitsimpis
            os_value = server['metadata'].get("os")
136 d246be88 Ilias Tsitsimpis
            if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
137 d246be88 Ilias Tsitsimpis
                ret_user = "user"
138 d246be88 Ilias Tsitsimpis
            elif os_value in ("windows", "windows_alpha1"):
139 d246be88 Ilias Tsitsimpis
                ret_user = "Administrator"
140 d246be88 Ilias Tsitsimpis
            else:
141 d246be88 Ilias Tsitsimpis
                ret_user = "root"
142 d246be88 Ilias Tsitsimpis
143 d246be88 Ilias Tsitsimpis
        self.assertIsNotNone(ret_user)
144 d246be88 Ilias Tsitsimpis
        self.info("User's login name: %s", ret_user)
145 d246be88 Ilias Tsitsimpis
        return ret_user
146 d246be88 Ilias Tsitsimpis
147 d246be88 Ilias Tsitsimpis
    def _insist_on_server_transition(self, server, curr_status, new_status):
148 d246be88 Ilias Tsitsimpis
        """Insist on server transiting from curr_status to new_status"""
149 d246be88 Ilias Tsitsimpis
        def check_fun():
150 d246be88 Ilias Tsitsimpis
            """Check server status"""
151 d246be88 Ilias Tsitsimpis
            srv = self.clients.cyclades.get_server_details(server['id'])
152 d246be88 Ilias Tsitsimpis
            if srv['status'] == curr_status:
153 d246be88 Ilias Tsitsimpis
                raise Retry()
154 d246be88 Ilias Tsitsimpis
            elif srv['status'] == new_status:
155 d246be88 Ilias Tsitsimpis
                return
156 d246be88 Ilias Tsitsimpis
            else:
157 d246be88 Ilias Tsitsimpis
                msg = "Server %s went to unexpected status %s"
158 d246be88 Ilias Tsitsimpis
                self.error(msg, server['name'], srv['status'])
159 d246be88 Ilias Tsitsimpis
                self.fail(msg % (server['name'], srv['status']))
160 d246be88 Ilias Tsitsimpis
        opmsg = "Waiting for server %s to transit from %s to %s"
161 d246be88 Ilias Tsitsimpis
        self.info(opmsg, server['name'], curr_status, new_status)
162 d246be88 Ilias Tsitsimpis
        opmsg = opmsg % (server['name'], curr_status, new_status)
163 d246be88 Ilias Tsitsimpis
        self._try_until_timeout_expires(opmsg, check_fun)
164 d246be88 Ilias Tsitsimpis
165 d246be88 Ilias Tsitsimpis
    def _insist_on_tcp_connection(self, family, host, port):
166 d246be88 Ilias Tsitsimpis
        """Insist on tcp connection"""
167 d246be88 Ilias Tsitsimpis
        def check_fun():
168 d246be88 Ilias Tsitsimpis
            """Get a connected socket from the specified family to host:port"""
169 d246be88 Ilias Tsitsimpis
            sock = None
170 d246be88 Ilias Tsitsimpis
            for res in socket.getaddrinfo(host, port, family,
171 d246be88 Ilias Tsitsimpis
                                          socket.SOCK_STREAM, 0,
172 d246be88 Ilias Tsitsimpis
                                          socket.AI_PASSIVE):
173 d246be88 Ilias Tsitsimpis
                fam, socktype, proto, _, saddr = res
174 d246be88 Ilias Tsitsimpis
                try:
175 d246be88 Ilias Tsitsimpis
                    sock = socket.socket(fam, socktype, proto)
176 d246be88 Ilias Tsitsimpis
                except socket.error:
177 d246be88 Ilias Tsitsimpis
                    sock = None
178 d246be88 Ilias Tsitsimpis
                    continue
179 d246be88 Ilias Tsitsimpis
                try:
180 d246be88 Ilias Tsitsimpis
                    sock.connect(saddr)
181 d246be88 Ilias Tsitsimpis
                except socket.error:
182 d246be88 Ilias Tsitsimpis
                    sock.close()
183 d246be88 Ilias Tsitsimpis
                    sock = None
184 d246be88 Ilias Tsitsimpis
                    continue
185 d246be88 Ilias Tsitsimpis
            if sock is None:
186 d246be88 Ilias Tsitsimpis
                raise Retry
187 d246be88 Ilias Tsitsimpis
            return sock
188 d246be88 Ilias Tsitsimpis
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
189 d246be88 Ilias Tsitsimpis
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
190 d246be88 Ilias Tsitsimpis
        opmsg = "Connecting over %s to %s:%s"
191 d246be88 Ilias Tsitsimpis
        self.info(opmsg, familystr.get(family, "Unknown"), host, port)
192 d246be88 Ilias Tsitsimpis
        opmsg = opmsg % (familystr.get(family, "Unknown"), host, port)
193 d246be88 Ilias Tsitsimpis
        return self._try_until_timeout_expires(opmsg, check_fun)
194 d246be88 Ilias Tsitsimpis
195 cee3ee9b Ilias Tsitsimpis
    def _get_ip(self, server, version=4):
196 d246be88 Ilias Tsitsimpis
        """Get the public IP of a server from the detailed server info"""
197 d246be88 Ilias Tsitsimpis
        assert version in (4, 6)
198 d246be88 Ilias Tsitsimpis
199 d246be88 Ilias Tsitsimpis
        nics = server['attachments']
200 d246be88 Ilias Tsitsimpis
        public_addrs = None
201 d246be88 Ilias Tsitsimpis
        for nic in nics:
202 d246be88 Ilias Tsitsimpis
            net_id = nic['network_id']
203 d246be88 Ilias Tsitsimpis
            if self.clients.cyclades.get_network_details(net_id)['public']:
204 d246be88 Ilias Tsitsimpis
                public_addrs = nic['ipv' + str(version)]
205 d246be88 Ilias Tsitsimpis
206 d246be88 Ilias Tsitsimpis
        self.assertIsNotNone(public_addrs)
207 cee3ee9b Ilias Tsitsimpis
        msg = "Server's public IPv%s is %s"
208 cee3ee9b Ilias Tsitsimpis
        self.info(msg, version, public_addrs)
209 d246be88 Ilias Tsitsimpis
        return public_addrs
210 d246be88 Ilias Tsitsimpis
211 cee3ee9b Ilias Tsitsimpis
    def _insist_on_ping(self, ip_addr, version=4):
212 d246be88 Ilias Tsitsimpis
        """Test server responds to a single IPv4 of IPv6 ping"""
213 d246be88 Ilias Tsitsimpis
        def check_fun():
214 d246be88 Ilias Tsitsimpis
            """Ping to server"""
215 d246be88 Ilias Tsitsimpis
            cmd = ("ping%s -c 3 -w 20 %s" %
216 d246be88 Ilias Tsitsimpis
                   ("6" if version == 6 else "", ip_addr))
217 d246be88 Ilias Tsitsimpis
            ping = subprocess.Popen(
218 d246be88 Ilias Tsitsimpis
                cmd, shell=True, stdout=subprocess.PIPE,
219 d246be88 Ilias Tsitsimpis
                stderr=subprocess.PIPE)
220 d246be88 Ilias Tsitsimpis
            ping.communicate()
221 d246be88 Ilias Tsitsimpis
            ret = ping.wait()
222 d246be88 Ilias Tsitsimpis
            if ret != 0:
223 d246be88 Ilias Tsitsimpis
                raise Retry
224 cee3ee9b Ilias Tsitsimpis
        assert version in (4, 6)
225 d246be88 Ilias Tsitsimpis
        opmsg = "Sent IPv%s ping requests to %s"
226 d246be88 Ilias Tsitsimpis
        self.info(opmsg, version, ip_addr)
227 d246be88 Ilias Tsitsimpis
        opmsg = opmsg % (version, ip_addr)
228 d246be88 Ilias Tsitsimpis
        self._try_until_timeout_expires(opmsg, check_fun)
229 d246be88 Ilias Tsitsimpis
230 cee3ee9b Ilias Tsitsimpis
    def _image_is(self, image, osfamily):
231 cee3ee9b Ilias Tsitsimpis
        """Return true if the image is of `osfamily'"""
232 cee3ee9b Ilias Tsitsimpis
        d_image = self.clients.cyclades.get_image_details(image['id'])
233 cee3ee9b Ilias Tsitsimpis
        return d_image['metadata']['osfamily'].lower().find(osfamily) >= 0
234 cee3ee9b Ilias Tsitsimpis
235 cee3ee9b Ilias Tsitsimpis
    def _ssh_execute(self, hostip, username, password, command):
236 cee3ee9b Ilias Tsitsimpis
        """Execute a command via ssh"""
237 cee3ee9b Ilias Tsitsimpis
        ssh = paramiko.SSHClient()
238 cee3ee9b Ilias Tsitsimpis
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
239 cee3ee9b Ilias Tsitsimpis
        try:
240 cee3ee9b Ilias Tsitsimpis
            ssh.connect(hostip, username=username, password=password)
241 cee3ee9b Ilias Tsitsimpis
        except socket.error as err:
242 cee3ee9b Ilias Tsitsimpis
            self.fail(err)
243 cee3ee9b Ilias Tsitsimpis
        try:
244 cee3ee9b Ilias Tsitsimpis
            _, stdout, _ = ssh.exec_command(command)
245 cee3ee9b Ilias Tsitsimpis
        except paramiko.SSHException as err:
246 cee3ee9b Ilias Tsitsimpis
            self.fail(err)
247 cee3ee9b Ilias Tsitsimpis
        status = stdout.channel.recv_exit_status()
248 cee3ee9b Ilias Tsitsimpis
        output = stdout.readlines()
249 cee3ee9b Ilias Tsitsimpis
        ssh.close()
250 cee3ee9b Ilias Tsitsimpis
        return output, status
251 cee3ee9b Ilias Tsitsimpis
252 cee3ee9b Ilias Tsitsimpis
    def _insist_get_hostname_over_ssh(self, hostip, username, password):
253 cee3ee9b Ilias Tsitsimpis
        """Connect to server using ssh and get it's hostname"""
254 cee3ee9b Ilias Tsitsimpis
        def check_fun():
255 cee3ee9b Ilias Tsitsimpis
            """Get hostname"""
256 cee3ee9b Ilias Tsitsimpis
            try:
257 cee3ee9b Ilias Tsitsimpis
                lines, status = self._ssh_execute(
258 cee3ee9b Ilias Tsitsimpis
                    hostip, username, password, "hostname")
259 cee3ee9b Ilias Tsitsimpis
                self.assertEqual(status, 0)
260 cee3ee9b Ilias Tsitsimpis
                self.assertEqual(len(lines), 1)
261 cee3ee9b Ilias Tsitsimpis
                # Remove new line
262 cee3ee9b Ilias Tsitsimpis
                return lines[0].strip('\n')
263 cee3ee9b Ilias Tsitsimpis
            except AssertionError:
264 cee3ee9b Ilias Tsitsimpis
                raise Retry()
265 cee3ee9b Ilias Tsitsimpis
        opmsg = "Connecting to server using ssh and get it's hostname"
266 cee3ee9b Ilias Tsitsimpis
        self.info(opmsg)
267 cee3ee9b Ilias Tsitsimpis
        hostname = self._try_until_timeout_expires(opmsg, check_fun)
268 cee3ee9b Ilias Tsitsimpis
        self.info("Server's hostname is %s", hostname)
269 cee3ee9b Ilias Tsitsimpis
        return hostname
270 cee3ee9b Ilias Tsitsimpis
271 cee3ee9b Ilias Tsitsimpis
    # Too many arguments. pylint: disable-msg=R0913
272 cee3ee9b Ilias Tsitsimpis
    def _check_file_through_ssh(self, hostip, username, password,
273 cee3ee9b Ilias Tsitsimpis
                                remotepath, content):
274 cee3ee9b Ilias Tsitsimpis
        """Fetch file from server and compare contents"""
275 cee3ee9b Ilias Tsitsimpis
        self.info("Fetching file %s from remote server", remotepath)
276 cee3ee9b Ilias Tsitsimpis
        transport = paramiko.Transport((hostip, 22))
277 cee3ee9b Ilias Tsitsimpis
        transport.connect(username=username, password=password)
278 cee3ee9b Ilias Tsitsimpis
        with tempfile.NamedTemporaryFile() as ftmp:
279 cee3ee9b Ilias Tsitsimpis
            sftp = paramiko.SFTPClient.from_transport(transport)
280 cee3ee9b Ilias Tsitsimpis
            sftp.get(remotepath, ftmp.name)
281 cee3ee9b Ilias Tsitsimpis
            sftp.close()
282 cee3ee9b Ilias Tsitsimpis
            transport.close()
283 cee3ee9b Ilias Tsitsimpis
            self.info("Comparing file contents")
284 cee3ee9b Ilias Tsitsimpis
            remote_content = base64.b64encode(ftmp.read())
285 cee3ee9b Ilias Tsitsimpis
            self.assertEqual(content, remote_content)
286 cee3ee9b Ilias Tsitsimpis
287 d246be88 Ilias Tsitsimpis
288 d246be88 Ilias Tsitsimpis
class Retry(Exception):
289 d246be88 Ilias Tsitsimpis
    """Retry the action
290 d246be88 Ilias Tsitsimpis

291 d246be88 Ilias Tsitsimpis
    This is used by _try_unit_timeout_expires method.
292 d246be88 Ilias Tsitsimpis

293 d246be88 Ilias Tsitsimpis
    """