Statistics
| Branch: | Tag: | Revision:

root / snf-tools / synnefo_tools / burnin / cyclades_common.py @ 17bb60a5

History | View | Annotate | Download (22.9 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 60a80953 Ilias Tsitsimpis
import IPy
43 cee3ee9b Ilias Tsitsimpis
import base64
44 d246be88 Ilias Tsitsimpis
import socket
45 d246be88 Ilias Tsitsimpis
import random
46 cee3ee9b Ilias Tsitsimpis
import paramiko
47 cee3ee9b Ilias Tsitsimpis
import tempfile
48 d246be88 Ilias Tsitsimpis
import subprocess
49 d246be88 Ilias Tsitsimpis
50 60a80953 Ilias Tsitsimpis
from kamaki.clients import ClientError
51 60a80953 Ilias Tsitsimpis
52 3e5bbd85 Ilias Tsitsimpis
from synnefo_tools.burnin.common import BurninTests, MB, GB
53 d246be88 Ilias Tsitsimpis
54 d246be88 Ilias Tsitsimpis
55 d246be88 Ilias Tsitsimpis
# Too many public methods. pylint: disable-msg=R0904
56 d246be88 Ilias Tsitsimpis
class CycladesTests(BurninTests):
57 d246be88 Ilias Tsitsimpis
    """Extends the BurninTests class for Cyclades"""
58 3eaf0ec5 Ilias Tsitsimpis
    def _try_until_timeout_expires(self, opmsg, check_fun):
59 d246be88 Ilias Tsitsimpis
        """Try to perform an action until timeout expires"""
60 d246be88 Ilias Tsitsimpis
        assert callable(check_fun), "Not a function"
61 d246be88 Ilias Tsitsimpis
62 d246be88 Ilias Tsitsimpis
        action_timeout = self.action_timeout
63 d246be88 Ilias Tsitsimpis
        action_warning = self.action_warning
64 d246be88 Ilias Tsitsimpis
        if action_warning > action_timeout:
65 d246be88 Ilias Tsitsimpis
            action_warning = action_timeout
66 d246be88 Ilias Tsitsimpis
67 8c67f82e Ilias Tsitsimpis
        start_time = int(time.time())
68 8c67f82e Ilias Tsitsimpis
        end_time = start_time + action_warning
69 8c67f82e Ilias Tsitsimpis
        while end_time > time.time():
70 d246be88 Ilias Tsitsimpis
            try:
71 8c67f82e Ilias Tsitsimpis
                ret_value = check_fun()
72 8c67f82e Ilias Tsitsimpis
                self.info("Operation `%s' finished in %s seconds",
73 8c67f82e Ilias Tsitsimpis
                          opmsg, int(time.time()) - start_time)
74 8c67f82e Ilias Tsitsimpis
                return ret_value
75 d246be88 Ilias Tsitsimpis
            except Retry:
76 d246be88 Ilias Tsitsimpis
                time.sleep(self.query_interval)
77 8c67f82e Ilias Tsitsimpis
        self.warning("Operation `%s' is taking too long after %s seconds",
78 8c67f82e Ilias Tsitsimpis
                     opmsg, int(time.time()) - start_time)
79 8c67f82e Ilias Tsitsimpis
80 8c67f82e Ilias Tsitsimpis
        end_time = start_time + action_timeout
81 8c67f82e Ilias Tsitsimpis
        while end_time > time.time():
82 d246be88 Ilias Tsitsimpis
            try:
83 8c67f82e Ilias Tsitsimpis
                ret_value = check_fun()
84 8c67f82e Ilias Tsitsimpis
                self.info("Operation `%s' finished in %s seconds",
85 8c67f82e Ilias Tsitsimpis
                          opmsg, int(time.time()) - start_time)
86 8c67f82e Ilias Tsitsimpis
                return ret_value
87 d246be88 Ilias Tsitsimpis
            except Retry:
88 d246be88 Ilias Tsitsimpis
                time.sleep(self.query_interval)
89 8c67f82e Ilias Tsitsimpis
        self.error("Operation `%s' timed out after %s seconds",
90 8c67f82e Ilias Tsitsimpis
                   opmsg, int(time.time()) - start_time)
91 d246be88 Ilias Tsitsimpis
        self.fail("time out")
92 d246be88 Ilias Tsitsimpis
93 d246be88 Ilias Tsitsimpis
    def _get_list_of_servers(self, detail=False):
94 d246be88 Ilias Tsitsimpis
        """Get (detailed) list of servers"""
95 d246be88 Ilias Tsitsimpis
        if detail:
96 d246be88 Ilias Tsitsimpis
            self.info("Getting detailed list of servers")
97 d246be88 Ilias Tsitsimpis
        else:
98 d246be88 Ilias Tsitsimpis
            self.info("Getting simple list of servers")
99 d246be88 Ilias Tsitsimpis
        return self.clients.cyclades.list_servers(detail=detail)
100 d246be88 Ilias Tsitsimpis
101 3eaf0ec5 Ilias Tsitsimpis
    def _get_list_of_networks(self, detail=False):
102 3eaf0ec5 Ilias Tsitsimpis
        """Get (detailed) list of networks"""
103 3eaf0ec5 Ilias Tsitsimpis
        if detail:
104 3eaf0ec5 Ilias Tsitsimpis
            self.info("Getting detailed list of networks")
105 3eaf0ec5 Ilias Tsitsimpis
        else:
106 3eaf0ec5 Ilias Tsitsimpis
            self.info("Getting simple list of networks")
107 60a80953 Ilias Tsitsimpis
        return self.clients.network.list_networks(detail=detail)
108 3eaf0ec5 Ilias Tsitsimpis
109 3eaf0ec5 Ilias Tsitsimpis
    def _get_server_details(self, server, quiet=False):
110 d246be88 Ilias Tsitsimpis
        """Get details for a server"""
111 3eaf0ec5 Ilias Tsitsimpis
        if not quiet:
112 3eaf0ec5 Ilias Tsitsimpis
            self.info("Getting details for server %s with id %s",
113 3eaf0ec5 Ilias Tsitsimpis
                      server['name'], server['id'])
114 d246be88 Ilias Tsitsimpis
        return self.clients.cyclades.get_server_details(server['id'])
115 d246be88 Ilias Tsitsimpis
116 60a80953 Ilias Tsitsimpis
    def _create_server(self, image, flavor, personality=None, network=False):
117 d246be88 Ilias Tsitsimpis
        """Create a new server"""
118 60a80953 Ilias Tsitsimpis
        if network:
119 60a80953 Ilias Tsitsimpis
            fip = self._create_floating_ip()
120 60a80953 Ilias Tsitsimpis
            port = self._create_port(fip['floating_network_id'],
121 60a80953 Ilias Tsitsimpis
                                     floating_ip=fip)
122 60a80953 Ilias Tsitsimpis
            networks = [{'port': port['id']}]
123 60a80953 Ilias Tsitsimpis
        else:
124 60a80953 Ilias Tsitsimpis
            networks = None
125 60a80953 Ilias Tsitsimpis
126 3eaf0ec5 Ilias Tsitsimpis
        servername = "%s for %s" % (self.run_id, image['name'])
127 3eaf0ec5 Ilias Tsitsimpis
        self.info("Creating a server with name %s", servername)
128 d246be88 Ilias Tsitsimpis
        self.info("Using image %s with id %s", image['name'], image['id'])
129 d246be88 Ilias Tsitsimpis
        self.info("Using flavor %s with id %s", flavor['name'], flavor['id'])
130 d246be88 Ilias Tsitsimpis
        server = self.clients.cyclades.create_server(
131 60a80953 Ilias Tsitsimpis
            servername, flavor['id'], image['id'],
132 60a80953 Ilias Tsitsimpis
            personality=personality, networks=networks)
133 d246be88 Ilias Tsitsimpis
134 d246be88 Ilias Tsitsimpis
        self.info("Server id: %s", server['id'])
135 d246be88 Ilias Tsitsimpis
        self.info("Server password: %s", server['adminPass'])
136 d246be88 Ilias Tsitsimpis
137 3eaf0ec5 Ilias Tsitsimpis
        self.assertEqual(server['name'], servername)
138 d246be88 Ilias Tsitsimpis
        self.assertEqual(server['flavor']['id'], flavor['id'])
139 d246be88 Ilias Tsitsimpis
        self.assertEqual(server['image']['id'], image['id'])
140 d246be88 Ilias Tsitsimpis
        self.assertEqual(server['status'], "BUILD")
141 d246be88 Ilias Tsitsimpis
142 3e5bbd85 Ilias Tsitsimpis
        # Verify quotas
143 3e5bbd85 Ilias Tsitsimpis
        self._check_quotas(disk=+int(flavor['disk'])*GB,
144 3e5bbd85 Ilias Tsitsimpis
                           vm=+1,
145 3e5bbd85 Ilias Tsitsimpis
                           ram=+int(flavor['ram'])*MB,
146 3e5bbd85 Ilias Tsitsimpis
                           cpu=+int(flavor['vcpus']))
147 3e5bbd85 Ilias Tsitsimpis
148 d246be88 Ilias Tsitsimpis
        return server
149 d246be88 Ilias Tsitsimpis
150 60a80953 Ilias Tsitsimpis
    def _delete_servers(self, servers, error=False):
151 60a80953 Ilias Tsitsimpis
        """Deleting a number of servers in parallel"""
152 60a80953 Ilias Tsitsimpis
        # Disconnect floating IPs
153 60a80953 Ilias Tsitsimpis
        for srv in servers:
154 60a80953 Ilias Tsitsimpis
            self.info("Disconnecting all floating IPs from server with id %s",
155 60a80953 Ilias Tsitsimpis
                      srv['id'])
156 60a80953 Ilias Tsitsimpis
            self._disconnect_from_network(srv)
157 60a80953 Ilias Tsitsimpis
158 60a80953 Ilias Tsitsimpis
        # Delete servers
159 60a80953 Ilias Tsitsimpis
        for srv in servers:
160 60a80953 Ilias Tsitsimpis
            self.info("Sending the delete request for server with id %s",
161 60a80953 Ilias Tsitsimpis
                      srv['id'])
162 60a80953 Ilias Tsitsimpis
            self.clients.cyclades.delete_server(srv['id'])
163 60a80953 Ilias Tsitsimpis
164 60a80953 Ilias Tsitsimpis
        if error:
165 60a80953 Ilias Tsitsimpis
            curr_states = ["ACTIVE", "ERROR", "STOPPED", "BUILD"]
166 60a80953 Ilias Tsitsimpis
        else:
167 60a80953 Ilias Tsitsimpis
            curr_states = ["ACTIVE"]
168 60a80953 Ilias Tsitsimpis
        for srv in servers:
169 60a80953 Ilias Tsitsimpis
            self._insist_on_server_transition(srv, curr_states, "DELETED")
170 60a80953 Ilias Tsitsimpis
171 60a80953 Ilias Tsitsimpis
        # Servers no longer in server list
172 60a80953 Ilias Tsitsimpis
        new_servers = [s['id'] for s in self._get_list_of_servers()]
173 60a80953 Ilias Tsitsimpis
        for srv in servers:
174 60a80953 Ilias Tsitsimpis
            self.info("Verifying that server with id %s is no longer in "
175 60a80953 Ilias Tsitsimpis
                      "server list", srv['id'])
176 60a80953 Ilias Tsitsimpis
            self.assertNotIn(srv['id'], new_servers)
177 60a80953 Ilias Tsitsimpis
178 60a80953 Ilias Tsitsimpis
        # Verify quotas
179 60a80953 Ilias Tsitsimpis
        flavors = \
180 60a80953 Ilias Tsitsimpis
            [self.clients.compute.get_flavor_details(srv['flavor']['id'])
181 60a80953 Ilias Tsitsimpis
             for srv in servers]
182 60a80953 Ilias Tsitsimpis
        self._verify_quotas_deleted(flavors)
183 60a80953 Ilias Tsitsimpis
184 3e5bbd85 Ilias Tsitsimpis
    def _verify_quotas_deleted(self, flavors):
185 3e5bbd85 Ilias Tsitsimpis
        """Verify quotas for a number of deleted servers"""
186 3e5bbd85 Ilias Tsitsimpis
        used_disk = 0
187 3e5bbd85 Ilias Tsitsimpis
        used_vm = 0
188 3e5bbd85 Ilias Tsitsimpis
        used_ram = 0
189 3e5bbd85 Ilias Tsitsimpis
        used_cpu = 0
190 3e5bbd85 Ilias Tsitsimpis
        for flavor in flavors:
191 3e5bbd85 Ilias Tsitsimpis
            used_disk += int(flavor['disk']) * GB
192 3e5bbd85 Ilias Tsitsimpis
            used_vm += 1
193 3e5bbd85 Ilias Tsitsimpis
            used_ram += int(flavor['ram']) * MB
194 3e5bbd85 Ilias Tsitsimpis
            used_cpu += int(flavor['vcpus'])
195 3e5bbd85 Ilias Tsitsimpis
        self._check_quotas(disk=-used_disk,
196 3e5bbd85 Ilias Tsitsimpis
                           vm=-used_vm,
197 3e5bbd85 Ilias Tsitsimpis
                           ram=-used_ram,
198 3e5bbd85 Ilias Tsitsimpis
                           cpu=-used_cpu)
199 3e5bbd85 Ilias Tsitsimpis
200 d246be88 Ilias Tsitsimpis
    def _get_connection_username(self, server):
201 d246be88 Ilias Tsitsimpis
        """Determine the username to use to connect to the server"""
202 d246be88 Ilias Tsitsimpis
        users = server['metadata'].get("users", None)
203 d246be88 Ilias Tsitsimpis
        ret_user = None
204 d246be88 Ilias Tsitsimpis
        if users is not None:
205 d246be88 Ilias Tsitsimpis
            user_list = users.split()
206 d246be88 Ilias Tsitsimpis
            if "root" in user_list:
207 d246be88 Ilias Tsitsimpis
                ret_user = "root"
208 d246be88 Ilias Tsitsimpis
            else:
209 d246be88 Ilias Tsitsimpis
                ret_user = random.choice(user_list)
210 d246be88 Ilias Tsitsimpis
        else:
211 d246be88 Ilias Tsitsimpis
            # Return the login name for connections based on the server OS
212 d246be88 Ilias Tsitsimpis
            self.info("Could not find `users' metadata in server. Let's guess")
213 d246be88 Ilias Tsitsimpis
            os_value = server['metadata'].get("os")
214 d246be88 Ilias Tsitsimpis
            if os_value in ("Ubuntu", "Kubuntu", "Fedora"):
215 d246be88 Ilias Tsitsimpis
                ret_user = "user"
216 d246be88 Ilias Tsitsimpis
            elif os_value in ("windows", "windows_alpha1"):
217 d246be88 Ilias Tsitsimpis
                ret_user = "Administrator"
218 d246be88 Ilias Tsitsimpis
            else:
219 d246be88 Ilias Tsitsimpis
                ret_user = "root"
220 d246be88 Ilias Tsitsimpis
221 d246be88 Ilias Tsitsimpis
        self.assertIsNotNone(ret_user)
222 d246be88 Ilias Tsitsimpis
        self.info("User's login name: %s", ret_user)
223 d246be88 Ilias Tsitsimpis
        return ret_user
224 d246be88 Ilias Tsitsimpis
225 3eaf0ec5 Ilias Tsitsimpis
    def _insist_on_server_transition(self, server, curr_statuses, new_status):
226 3eaf0ec5 Ilias Tsitsimpis
        """Insist on server transiting from curr_statuses to new_status"""
227 d246be88 Ilias Tsitsimpis
        def check_fun():
228 d246be88 Ilias Tsitsimpis
            """Check server status"""
229 3eaf0ec5 Ilias Tsitsimpis
            srv = self._get_server_details(server, quiet=True)
230 3eaf0ec5 Ilias Tsitsimpis
            if srv['status'] in curr_statuses:
231 d246be88 Ilias Tsitsimpis
                raise Retry()
232 d246be88 Ilias Tsitsimpis
            elif srv['status'] == new_status:
233 d246be88 Ilias Tsitsimpis
                return
234 d246be88 Ilias Tsitsimpis
            else:
235 3eaf0ec5 Ilias Tsitsimpis
                msg = "Server \"%s\" with id %s went to unexpected status %s"
236 3eaf0ec5 Ilias Tsitsimpis
                self.error(msg, server['name'], server['id'], srv['status'])
237 3eaf0ec5 Ilias Tsitsimpis
                self.fail(msg % (server['name'], server['id'], srv['status']))
238 5bef1f49 Ilias Tsitsimpis
        opmsg = "Waiting for server \"%s\" with id %s to become %s"
239 5bef1f49 Ilias Tsitsimpis
        self.info(opmsg, server['name'], server['id'], new_status)
240 5bef1f49 Ilias Tsitsimpis
        opmsg = opmsg % (server['name'], server['id'], new_status)
241 3eaf0ec5 Ilias Tsitsimpis
        self._try_until_timeout_expires(opmsg, check_fun)
242 3eaf0ec5 Ilias Tsitsimpis
243 3eaf0ec5 Ilias Tsitsimpis
    def _insist_on_network_transition(self, network,
244 3eaf0ec5 Ilias Tsitsimpis
                                      curr_statuses, new_status):
245 3eaf0ec5 Ilias Tsitsimpis
        """Insist on network transiting from curr_statuses to new_status"""
246 3eaf0ec5 Ilias Tsitsimpis
        def check_fun():
247 3eaf0ec5 Ilias Tsitsimpis
            """Check network status"""
248 60a80953 Ilias Tsitsimpis
            ntw = self.clients.network.get_network_details(network['id'])
249 3eaf0ec5 Ilias Tsitsimpis
            if ntw['status'] in curr_statuses:
250 3eaf0ec5 Ilias Tsitsimpis
                raise Retry()
251 3eaf0ec5 Ilias Tsitsimpis
            elif ntw['status'] == new_status:
252 3eaf0ec5 Ilias Tsitsimpis
                return
253 3eaf0ec5 Ilias Tsitsimpis
            else:
254 3eaf0ec5 Ilias Tsitsimpis
                msg = "Network %s with id %s went to unexpected status %s"
255 3eaf0ec5 Ilias Tsitsimpis
                self.error(msg, network['name'], network['id'], ntw['status'])
256 3eaf0ec5 Ilias Tsitsimpis
                self.fail(msg %
257 3eaf0ec5 Ilias Tsitsimpis
                          (network['name'], network['id'], ntw['status']))
258 5bef1f49 Ilias Tsitsimpis
        opmsg = "Waiting for network \"%s\" with id %s to become %s"
259 5bef1f49 Ilias Tsitsimpis
        self.info(opmsg, network['name'], network['id'], new_status)
260 5bef1f49 Ilias Tsitsimpis
        opmsg = opmsg % (network['name'], network['id'], new_status)
261 3eaf0ec5 Ilias Tsitsimpis
        self._try_until_timeout_expires(opmsg, check_fun)
262 3eaf0ec5 Ilias Tsitsimpis
263 d246be88 Ilias Tsitsimpis
    def _insist_on_tcp_connection(self, family, host, port):
264 d246be88 Ilias Tsitsimpis
        """Insist on tcp connection"""
265 d246be88 Ilias Tsitsimpis
        def check_fun():
266 d246be88 Ilias Tsitsimpis
            """Get a connected socket from the specified family to host:port"""
267 d246be88 Ilias Tsitsimpis
            sock = None
268 d246be88 Ilias Tsitsimpis
            for res in socket.getaddrinfo(host, port, family,
269 d246be88 Ilias Tsitsimpis
                                          socket.SOCK_STREAM, 0,
270 d246be88 Ilias Tsitsimpis
                                          socket.AI_PASSIVE):
271 d246be88 Ilias Tsitsimpis
                fam, socktype, proto, _, saddr = res
272 d246be88 Ilias Tsitsimpis
                try:
273 d246be88 Ilias Tsitsimpis
                    sock = socket.socket(fam, socktype, proto)
274 d246be88 Ilias Tsitsimpis
                except socket.error:
275 d246be88 Ilias Tsitsimpis
                    sock = None
276 d246be88 Ilias Tsitsimpis
                    continue
277 d246be88 Ilias Tsitsimpis
                try:
278 d246be88 Ilias Tsitsimpis
                    sock.connect(saddr)
279 d246be88 Ilias Tsitsimpis
                except socket.error:
280 d246be88 Ilias Tsitsimpis
                    sock.close()
281 d246be88 Ilias Tsitsimpis
                    sock = None
282 d246be88 Ilias Tsitsimpis
                    continue
283 d246be88 Ilias Tsitsimpis
            if sock is None:
284 d246be88 Ilias Tsitsimpis
                raise Retry
285 d246be88 Ilias Tsitsimpis
            return sock
286 d246be88 Ilias Tsitsimpis
        familystr = {socket.AF_INET: "IPv4", socket.AF_INET6: "IPv6",
287 d246be88 Ilias Tsitsimpis
                     socket.AF_UNSPEC: "Unspecified-IPv4/6"}
288 d246be88 Ilias Tsitsimpis
        opmsg = "Connecting over %s to %s:%s"
289 d246be88 Ilias Tsitsimpis
        self.info(opmsg, familystr.get(family, "Unknown"), host, port)
290 d246be88 Ilias Tsitsimpis
        opmsg = opmsg % (familystr.get(family, "Unknown"), host, port)
291 d246be88 Ilias Tsitsimpis
        return self._try_until_timeout_expires(opmsg, check_fun)
292 d246be88 Ilias Tsitsimpis
293 60a80953 Ilias Tsitsimpis
    def _get_ips(self, server, version=4, network=None):
294 60a80953 Ilias Tsitsimpis
        """Get the IPs of a server from the detailed server info
295 3eaf0ec5 Ilias Tsitsimpis

296 60a80953 Ilias Tsitsimpis
        If network not given then get the public IPs. Else the IPs
297 3eaf0ec5 Ilias Tsitsimpis
        attached to that network
298 3eaf0ec5 Ilias Tsitsimpis

299 3eaf0ec5 Ilias Tsitsimpis
        """
300 d246be88 Ilias Tsitsimpis
        assert version in (4, 6)
301 d246be88 Ilias Tsitsimpis
302 d246be88 Ilias Tsitsimpis
        nics = server['attachments']
303 60a80953 Ilias Tsitsimpis
        addrs = []
304 d246be88 Ilias Tsitsimpis
        for nic in nics:
305 d246be88 Ilias Tsitsimpis
            net_id = nic['network_id']
306 3eaf0ec5 Ilias Tsitsimpis
            if network is None:
307 60a80953 Ilias Tsitsimpis
                if self.clients.network.get_network_details(net_id)['public']:
308 ee89df69 Ilias Tsitsimpis
                    if nic['ipv' + str(version)]:
309 60a80953 Ilias Tsitsimpis
                        addrs.append(nic['ipv' + str(version)])
310 3eaf0ec5 Ilias Tsitsimpis
            else:
311 3eaf0ec5 Ilias Tsitsimpis
                if net_id == network['id']:
312 ee89df69 Ilias Tsitsimpis
                    if nic['ipv' + str(version)]:
313 60a80953 Ilias Tsitsimpis
                        addrs.append(nic['ipv' + str(version)])
314 60a80953 Ilias Tsitsimpis
315 60a80953 Ilias Tsitsimpis
        self.assertGreater(len(addrs), 0,
316 60a80953 Ilias Tsitsimpis
                           "Can not get IPs from server attachments")
317 60a80953 Ilias Tsitsimpis
318 60a80953 Ilias Tsitsimpis
        for addr in addrs:
319 60a80953 Ilias Tsitsimpis
            self.assertEquals(IPy.IP(addr).version(), version)
320 d246be88 Ilias Tsitsimpis
321 3eaf0ec5 Ilias Tsitsimpis
        if network is None:
322 3eaf0ec5 Ilias Tsitsimpis
            msg = "Server's public IPv%s is %s"
323 60a80953 Ilias Tsitsimpis
            for addr in addrs:
324 60a80953 Ilias Tsitsimpis
                self.info(msg, version, addr)
325 3eaf0ec5 Ilias Tsitsimpis
        else:
326 3eaf0ec5 Ilias Tsitsimpis
            msg = "Server's IPv%s attached to network \"%s\" is %s"
327 60a80953 Ilias Tsitsimpis
            for addr in addrs:
328 60a80953 Ilias Tsitsimpis
                self.info(msg, version, network['id'], addr)
329 3eaf0ec5 Ilias Tsitsimpis
        return addrs
330 d246be88 Ilias Tsitsimpis
331 cee3ee9b Ilias Tsitsimpis
    def _insist_on_ping(self, ip_addr, version=4):
332 d246be88 Ilias Tsitsimpis
        """Test server responds to a single IPv4 of IPv6 ping"""
333 d246be88 Ilias Tsitsimpis
        def check_fun():
334 d246be88 Ilias Tsitsimpis
            """Ping to server"""
335 d246be88 Ilias Tsitsimpis
            cmd = ("ping%s -c 3 -w 20 %s" %
336 d246be88 Ilias Tsitsimpis
                   ("6" if version == 6 else "", ip_addr))
337 d246be88 Ilias Tsitsimpis
            ping = subprocess.Popen(
338 d246be88 Ilias Tsitsimpis
                cmd, shell=True, stdout=subprocess.PIPE,
339 d246be88 Ilias Tsitsimpis
                stderr=subprocess.PIPE)
340 d246be88 Ilias Tsitsimpis
            ping.communicate()
341 d246be88 Ilias Tsitsimpis
            ret = ping.wait()
342 d246be88 Ilias Tsitsimpis
            if ret != 0:
343 d246be88 Ilias Tsitsimpis
                raise Retry
344 cee3ee9b Ilias Tsitsimpis
        assert version in (4, 6)
345 d246be88 Ilias Tsitsimpis
        opmsg = "Sent IPv%s ping requests to %s"
346 d246be88 Ilias Tsitsimpis
        self.info(opmsg, version, ip_addr)
347 d246be88 Ilias Tsitsimpis
        opmsg = opmsg % (version, ip_addr)
348 d246be88 Ilias Tsitsimpis
        self._try_until_timeout_expires(opmsg, check_fun)
349 d246be88 Ilias Tsitsimpis
350 cee3ee9b Ilias Tsitsimpis
    def _image_is(self, image, osfamily):
351 cee3ee9b Ilias Tsitsimpis
        """Return true if the image is of `osfamily'"""
352 cee3ee9b Ilias Tsitsimpis
        d_image = self.clients.cyclades.get_image_details(image['id'])
353 cee3ee9b Ilias Tsitsimpis
        return d_image['metadata']['osfamily'].lower().find(osfamily) >= 0
354 cee3ee9b Ilias Tsitsimpis
355 cee3ee9b Ilias Tsitsimpis
    def _ssh_execute(self, hostip, username, password, command):
356 cee3ee9b Ilias Tsitsimpis
        """Execute a command via ssh"""
357 cee3ee9b Ilias Tsitsimpis
        ssh = paramiko.SSHClient()
358 cee3ee9b Ilias Tsitsimpis
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
359 cee3ee9b Ilias Tsitsimpis
        try:
360 cee3ee9b Ilias Tsitsimpis
            ssh.connect(hostip, username=username, password=password)
361 cee3ee9b Ilias Tsitsimpis
        except socket.error as err:
362 cee3ee9b Ilias Tsitsimpis
            self.fail(err)
363 cee3ee9b Ilias Tsitsimpis
        try:
364 cee3ee9b Ilias Tsitsimpis
            _, stdout, _ = ssh.exec_command(command)
365 cee3ee9b Ilias Tsitsimpis
        except paramiko.SSHException as err:
366 cee3ee9b Ilias Tsitsimpis
            self.fail(err)
367 cee3ee9b Ilias Tsitsimpis
        status = stdout.channel.recv_exit_status()
368 cee3ee9b Ilias Tsitsimpis
        output = stdout.readlines()
369 cee3ee9b Ilias Tsitsimpis
        ssh.close()
370 cee3ee9b Ilias Tsitsimpis
        return output, status
371 cee3ee9b Ilias Tsitsimpis
372 cee3ee9b Ilias Tsitsimpis
    def _insist_get_hostname_over_ssh(self, hostip, username, password):
373 cee3ee9b Ilias Tsitsimpis
        """Connect to server using ssh and get it's hostname"""
374 cee3ee9b Ilias Tsitsimpis
        def check_fun():
375 cee3ee9b Ilias Tsitsimpis
            """Get hostname"""
376 cee3ee9b Ilias Tsitsimpis
            try:
377 cee3ee9b Ilias Tsitsimpis
                lines, status = self._ssh_execute(
378 cee3ee9b Ilias Tsitsimpis
                    hostip, username, password, "hostname")
379 cee3ee9b Ilias Tsitsimpis
                self.assertEqual(status, 0)
380 cee3ee9b Ilias Tsitsimpis
                self.assertEqual(len(lines), 1)
381 cee3ee9b Ilias Tsitsimpis
                # Remove new line
382 cee3ee9b Ilias Tsitsimpis
                return lines[0].strip('\n')
383 cee3ee9b Ilias Tsitsimpis
            except AssertionError:
384 cee3ee9b Ilias Tsitsimpis
                raise Retry()
385 cee3ee9b Ilias Tsitsimpis
        opmsg = "Connecting to server using ssh and get it's hostname"
386 cee3ee9b Ilias Tsitsimpis
        self.info(opmsg)
387 cee3ee9b Ilias Tsitsimpis
        hostname = self._try_until_timeout_expires(opmsg, check_fun)
388 cee3ee9b Ilias Tsitsimpis
        self.info("Server's hostname is %s", hostname)
389 cee3ee9b Ilias Tsitsimpis
        return hostname
390 cee3ee9b Ilias Tsitsimpis
391 cee3ee9b Ilias Tsitsimpis
    # Too many arguments. pylint: disable-msg=R0913
392 cee3ee9b Ilias Tsitsimpis
    def _check_file_through_ssh(self, hostip, username, password,
393 cee3ee9b Ilias Tsitsimpis
                                remotepath, content):
394 cee3ee9b Ilias Tsitsimpis
        """Fetch file from server and compare contents"""
395 cee3ee9b Ilias Tsitsimpis
        self.info("Fetching file %s from remote server", remotepath)
396 cee3ee9b Ilias Tsitsimpis
        transport = paramiko.Transport((hostip, 22))
397 cee3ee9b Ilias Tsitsimpis
        transport.connect(username=username, password=password)
398 cee3ee9b Ilias Tsitsimpis
        with tempfile.NamedTemporaryFile() as ftmp:
399 cee3ee9b Ilias Tsitsimpis
            sftp = paramiko.SFTPClient.from_transport(transport)
400 cee3ee9b Ilias Tsitsimpis
            sftp.get(remotepath, ftmp.name)
401 cee3ee9b Ilias Tsitsimpis
            sftp.close()
402 cee3ee9b Ilias Tsitsimpis
            transport.close()
403 cee3ee9b Ilias Tsitsimpis
            self.info("Comparing file contents")
404 cee3ee9b Ilias Tsitsimpis
            remote_content = base64.b64encode(ftmp.read())
405 cee3ee9b Ilias Tsitsimpis
            self.assertEqual(content, remote_content)
406 cee3ee9b Ilias Tsitsimpis
407 60a80953 Ilias Tsitsimpis
    # ----------------------------------
408 60a80953 Ilias Tsitsimpis
    # Networks
409 60a80953 Ilias Tsitsimpis
    def _create_network(self, cidr="10.0.1.0/28", dhcp=True):
410 3e5bbd85 Ilias Tsitsimpis
        """Create a new private network"""
411 60a80953 Ilias Tsitsimpis
        name = self.run_id
412 60a80953 Ilias Tsitsimpis
        network = self.clients.network.create_network(
413 60a80953 Ilias Tsitsimpis
            "MAC_FILTERED", name=name, shared=False)
414 3e5bbd85 Ilias Tsitsimpis
        self.info("Network with id %s created", network['id'])
415 60a80953 Ilias Tsitsimpis
        subnet = self.clients.network.create_subnet(
416 60a80953 Ilias Tsitsimpis
            network['id'], cidr=cidr, enable_dhcp=dhcp)
417 60a80953 Ilias Tsitsimpis
        self.info("Subnet with id %s created", subnet['id'])
418 3e5bbd85 Ilias Tsitsimpis
419 3e5bbd85 Ilias Tsitsimpis
        # Verify quotas
420 3e5bbd85 Ilias Tsitsimpis
        self._check_quotas(network=+1)
421 3e5bbd85 Ilias Tsitsimpis
422 3e5bbd85 Ilias Tsitsimpis
        #Test if the right name is assigned
423 3e5bbd85 Ilias Tsitsimpis
        self.assertEqual(network['name'], name)
424 3e5bbd85 Ilias Tsitsimpis
425 3e5bbd85 Ilias Tsitsimpis
        return network
426 3e5bbd85 Ilias Tsitsimpis
427 60a80953 Ilias Tsitsimpis
    def _delete_networks(self, networks, error=False):
428 60a80953 Ilias Tsitsimpis
        """Delete a network"""
429 60a80953 Ilias Tsitsimpis
        for net in networks:
430 60a80953 Ilias Tsitsimpis
            self.info("Deleting network with id %s", net['id'])
431 60a80953 Ilias Tsitsimpis
            self.clients.network.delete_network(net['id'])
432 60a80953 Ilias Tsitsimpis
433 60a80953 Ilias Tsitsimpis
        if error:
434 60a80953 Ilias Tsitsimpis
            curr_states = ["ACTIVE", "SNF:DRAINED", "ERROR"]
435 60a80953 Ilias Tsitsimpis
        else:
436 60a80953 Ilias Tsitsimpis
            curr_states = ["ACTIVE", "SNF:DRAINED"]
437 60a80953 Ilias Tsitsimpis
        for net in networks:
438 60a80953 Ilias Tsitsimpis
            self._insist_on_network_transition(net, curr_states, "DELETED")
439 60a80953 Ilias Tsitsimpis
440 60a80953 Ilias Tsitsimpis
        # Networks no longer in network list
441 60a80953 Ilias Tsitsimpis
        new_networks = [n['id'] for n in self._get_list_of_networks()]
442 60a80953 Ilias Tsitsimpis
        for net in networks:
443 60a80953 Ilias Tsitsimpis
            self.info("Verifying that network with id %s is no longer in "
444 60a80953 Ilias Tsitsimpis
                      "network list", net['id'])
445 60a80953 Ilias Tsitsimpis
            self.assertNotIn(net['id'], new_networks)
446 60a80953 Ilias Tsitsimpis
447 60a80953 Ilias Tsitsimpis
        # Verify quotas
448 60a80953 Ilias Tsitsimpis
        self._check_quotas(network=-len(networks))
449 60a80953 Ilias Tsitsimpis
450 60a80953 Ilias Tsitsimpis
    def _get_public_network(self, networks=None):
451 60a80953 Ilias Tsitsimpis
        """Get the public network"""
452 60a80953 Ilias Tsitsimpis
        if networks is None:
453 60a80953 Ilias Tsitsimpis
            networks = self._get_list_of_networks(detail=True)
454 60a80953 Ilias Tsitsimpis
        self.info("Getting the public network")
455 60a80953 Ilias Tsitsimpis
        for net in networks:
456 60a80953 Ilias Tsitsimpis
            if net['SNF:floating_ip_pool'] and net['public']:
457 60a80953 Ilias Tsitsimpis
                return net
458 60a80953 Ilias Tsitsimpis
        self.fail("Could not find a public network to use")
459 60a80953 Ilias Tsitsimpis
460 60a80953 Ilias Tsitsimpis
    def _create_floating_ip(self):
461 60a80953 Ilias Tsitsimpis
        """Create a new floating ip"""
462 60a80953 Ilias Tsitsimpis
        pub_net = self._get_public_network()
463 60a80953 Ilias Tsitsimpis
        self.info("Creating a new floating ip for network with id %s",
464 60a80953 Ilias Tsitsimpis
                  pub_net['id'])
465 60a80953 Ilias Tsitsimpis
        fip = self.clients.network.create_floatingip(pub_net['id'])
466 60a80953 Ilias Tsitsimpis
        # Verify that floating ip has been created
467 60a80953 Ilias Tsitsimpis
        fips = self.clients.network.list_floatingips()
468 60a80953 Ilias Tsitsimpis
        fips = [f['id'] for f in fips]
469 60a80953 Ilias Tsitsimpis
        self.assertIn(fip['id'], fips)
470 60a80953 Ilias Tsitsimpis
        # Verify quotas
471 60a80953 Ilias Tsitsimpis
        self._check_quotas(ip=+1)
472 60a80953 Ilias Tsitsimpis
        # Check that IP is IPv4
473 60a80953 Ilias Tsitsimpis
        self.assertEquals(IPy.IP(fip['floating_ip_address']).version(), 4)
474 60a80953 Ilias Tsitsimpis
475 60a80953 Ilias Tsitsimpis
        self.info("Floating IP %s with id %s created",
476 60a80953 Ilias Tsitsimpis
                  fip['floating_ip_address'], fip['id'])
477 60a80953 Ilias Tsitsimpis
        return fip
478 60a80953 Ilias Tsitsimpis
479 60a80953 Ilias Tsitsimpis
    def _create_port(self, network_id, device_id=None, floating_ip=None):
480 60a80953 Ilias Tsitsimpis
        """Create a new port attached to the a specific network"""
481 60a80953 Ilias Tsitsimpis
        self.info("Creating a new port to network with id %s", network_id)
482 60a80953 Ilias Tsitsimpis
        if floating_ip is not None:
483 60a80953 Ilias Tsitsimpis
            fixed_ips = [{'ip_address': floating_ip['floating_ip_address']}]
484 60a80953 Ilias Tsitsimpis
        else:
485 60a80953 Ilias Tsitsimpis
            fixed_ips = None
486 60a80953 Ilias Tsitsimpis
        port = self.clients.network.create_port(network_id,
487 60a80953 Ilias Tsitsimpis
                                                device_id=device_id,
488 60a80953 Ilias Tsitsimpis
                                                fixed_ips=fixed_ips)
489 60a80953 Ilias Tsitsimpis
        # Verify that port created
490 60a80953 Ilias Tsitsimpis
        ports = self.clients.network.list_ports()
491 60a80953 Ilias Tsitsimpis
        ports = [p['id'] for p in ports]
492 60a80953 Ilias Tsitsimpis
        self.assertIn(port['id'], ports)
493 60a80953 Ilias Tsitsimpis
        # Insist on creation
494 60a80953 Ilias Tsitsimpis
        if device_id is None:
495 60a80953 Ilias Tsitsimpis
            self._insist_on_port_transition(port, ["BUILD"], "DOWN")
496 60a80953 Ilias Tsitsimpis
        else:
497 60a80953 Ilias Tsitsimpis
            self._insist_on_port_transition(port, ["BUILD", "DOWN"], "ACTIVE")
498 60a80953 Ilias Tsitsimpis
499 60a80953 Ilias Tsitsimpis
        self.info("Port with id %s created", port['id'])
500 60a80953 Ilias Tsitsimpis
        return port
501 60a80953 Ilias Tsitsimpis
502 60a80953 Ilias Tsitsimpis
    def _insist_on_port_transition(self, port, curr_statuses, new_status):
503 60a80953 Ilias Tsitsimpis
        """Insist on port transiting from curr_statuses to new_status"""
504 60a80953 Ilias Tsitsimpis
        def check_fun():
505 60a80953 Ilias Tsitsimpis
            """Check port status"""
506 60a80953 Ilias Tsitsimpis
            portd = self.clients.network.get_port_details(port['id'])
507 60a80953 Ilias Tsitsimpis
            if portd['status'] in curr_statuses:
508 60a80953 Ilias Tsitsimpis
                raise Retry()
509 60a80953 Ilias Tsitsimpis
            elif portd['status'] == new_status:
510 60a80953 Ilias Tsitsimpis
                return
511 60a80953 Ilias Tsitsimpis
            else:
512 60a80953 Ilias Tsitsimpis
                msg = "Port %s went to unexpected status %s"
513 60a80953 Ilias Tsitsimpis
                self.fail(msg % (portd['id'], portd['status']))
514 60a80953 Ilias Tsitsimpis
        opmsg = "Waiting for port %s to become %s"
515 60a80953 Ilias Tsitsimpis
        self.info(opmsg, port['id'], new_status)
516 60a80953 Ilias Tsitsimpis
        opmsg = opmsg % (port['id'], new_status)
517 60a80953 Ilias Tsitsimpis
        self._try_until_timeout_expires(opmsg, check_fun)
518 60a80953 Ilias Tsitsimpis
519 60a80953 Ilias Tsitsimpis
    def _insist_on_port_deletion(self, portid):
520 60a80953 Ilias Tsitsimpis
        """Insist on port deletion"""
521 60a80953 Ilias Tsitsimpis
        def check_fun():
522 60a80953 Ilias Tsitsimpis
            """Check port details"""
523 60a80953 Ilias Tsitsimpis
            try:
524 60a80953 Ilias Tsitsimpis
                self.clients.network.get_port_details(portid)
525 60a80953 Ilias Tsitsimpis
            except ClientError as err:
526 60a80953 Ilias Tsitsimpis
                if err.status != 404:
527 60a80953 Ilias Tsitsimpis
                    raise
528 60a80953 Ilias Tsitsimpis
            else:
529 60a80953 Ilias Tsitsimpis
                raise Retry()
530 60a80953 Ilias Tsitsimpis
        opmsg = "Waiting for port %s to be deleted"
531 60a80953 Ilias Tsitsimpis
        self.info(opmsg, portid)
532 60a80953 Ilias Tsitsimpis
        opmsg = opmsg % portid
533 60a80953 Ilias Tsitsimpis
        self._try_until_timeout_expires(opmsg, check_fun)
534 60a80953 Ilias Tsitsimpis
535 60a80953 Ilias Tsitsimpis
    def _disconnect_from_network(self, server, network=None):
536 60a80953 Ilias Tsitsimpis
        """Disconnnect server from network"""
537 60a80953 Ilias Tsitsimpis
        if network is None:
538 60a80953 Ilias Tsitsimpis
            # Disconnect from public network
539 60a80953 Ilias Tsitsimpis
            network = self._get_public_network()
540 60a80953 Ilias Tsitsimpis
541 60a80953 Ilias Tsitsimpis
        lports = self.clients.network.list_ports()
542 60a80953 Ilias Tsitsimpis
        ports = []
543 60a80953 Ilias Tsitsimpis
        for port in lports:
544 60a80953 Ilias Tsitsimpis
            dport = self.clients.network.get_port_details(port['id'])
545 60a80953 Ilias Tsitsimpis
            if str(dport['network_id']) == str(network['id']) \
546 60a80953 Ilias Tsitsimpis
                    and str(dport['device_id']) == str(server['id']):
547 60a80953 Ilias Tsitsimpis
                ports.append(dport)
548 60a80953 Ilias Tsitsimpis
549 60a80953 Ilias Tsitsimpis
        # Find floating IPs attached to these ports
550 60a80953 Ilias Tsitsimpis
        ports_id = [p['id'] for p in ports]
551 60a80953 Ilias Tsitsimpis
        fips = [f for f in self.clients.network.list_floatingips()
552 60a80953 Ilias Tsitsimpis
                if str(f['port_id']) in ports_id]
553 60a80953 Ilias Tsitsimpis
554 60a80953 Ilias Tsitsimpis
        # First destroy the ports
555 60a80953 Ilias Tsitsimpis
        for port in ports:
556 60a80953 Ilias Tsitsimpis
            self.info("Destroying port with id %s", port['id'])
557 60a80953 Ilias Tsitsimpis
            self.clients.network.delete_port(port['id'])
558 60a80953 Ilias Tsitsimpis
            self._insist_on_port_deletion(port['id'])
559 60a80953 Ilias Tsitsimpis
560 60a80953 Ilias Tsitsimpis
        # Then delete the floating IPs
561 17bb60a5 Ilias Tsitsimpis
        self._delete_floating_ips(fips)
562 17bb60a5 Ilias Tsitsimpis
563 17bb60a5 Ilias Tsitsimpis
    def _delete_floating_ips(self, fips):
564 17bb60a5 Ilias Tsitsimpis
        """Delete floating ips"""
565 60a80953 Ilias Tsitsimpis
        for fip in fips:
566 60a80953 Ilias Tsitsimpis
            self.info("Destroying floating IP %s with id %s",
567 60a80953 Ilias Tsitsimpis
                      fip['floating_ip_address'], fip['id'])
568 60a80953 Ilias Tsitsimpis
            self.clients.network.delete_floatingip(fip['id'])
569 60a80953 Ilias Tsitsimpis
570 60a80953 Ilias Tsitsimpis
        # Check that floating IPs have been deleted
571 60a80953 Ilias Tsitsimpis
        list_ips = [f['id'] for f in self.clients.network.list_floatingips()]
572 60a80953 Ilias Tsitsimpis
        for fip in fips:
573 60a80953 Ilias Tsitsimpis
            self.assertNotIn(fip['id'], list_ips)
574 60a80953 Ilias Tsitsimpis
        # Verify quotas
575 60a80953 Ilias Tsitsimpis
        self._check_quotas(ip=-len(fips))
576 60a80953 Ilias Tsitsimpis
577 d246be88 Ilias Tsitsimpis
578 d246be88 Ilias Tsitsimpis
class Retry(Exception):
579 d246be88 Ilias Tsitsimpis
    """Retry the action
580 d246be88 Ilias Tsitsimpis

581 d246be88 Ilias Tsitsimpis
    This is used by _try_unit_timeout_expires method.
582 d246be88 Ilias Tsitsimpis

583 d246be88 Ilias Tsitsimpis
    """