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

import json

from snf_django.utils.testing import BaseAPITest, mocked_quotaholder
37
from synnefo.db.models import IPAddress
38
39
40
from synnefo.db import models_factory as mf
from synnefo.db.models_factory import (NetworkFactory,
                                       VirtualMachineFactory)
41
from mock import patch, Mock
42
from functools import partial
43

Christos Stavrakakis's avatar
Christos Stavrakakis committed
44
45
46
from synnefo.cyclades_settings import cyclades_services
from synnefo.lib.services import get_service_path
from synnefo.lib import join_urls
47

Christos Stavrakakis's avatar
Christos Stavrakakis committed
48
49
50
51
52

compute_path = get_service_path(cyclades_services, "compute", version="v2.0")
URL = join_urls(compute_path, "os-floating-ips")
NETWORKS_URL = join_urls(compute_path, "networks")
SERVERS_URL = join_urls(compute_path, "servers")
53

54
55
56

floating_ips = IPAddress.objects.filter(floating_ip=True)
FloatingIPPoolFactory = partial(NetworkFactory, public=True, deleted=False,
57
58
                                floating_ip_pool=True)

59

60
class FloatingIPAPITest(BaseAPITest):
61
62
63
64
65
66
    def setUp(self):
        self.pool = mf.NetworkWithSubnetFactory(floating_ip_pool=True,
                                                public=True,
                                                subnet__cidr="192.168.2.0/24",
                                                subnet__gateway="192.168.2.1")

67
68
69
70
71
72
    def test_no_floating_ip(self):
        response = self.get(URL)
        self.assertSuccess(response)
        self.assertEqual(json.loads(response.content)["floating_ips"], [])

    def test_list_ips(self):
73
        ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True)
74
75
76
77
78
        with mocked_quotaholder():
            response = self.get(URL, "user1")
        self.assertSuccess(response)
        api_ip = json.loads(response.content)["floating_ips"][0]
        self.assertEqual(api_ip,
79
80
81
82
83
                         {"instance_id": str(ip.nic.machine_id),
                          "ip": ip.address,
                          "fixed_ip": None,
                          "id": str(ip.id),
                          "pool": str(ip.network_id)})
84
85

    def test_get_ip(self):
86
        ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True)
87
88
89
90
91
        with mocked_quotaholder():
            response = self.get(URL + "/%s" % ip.id, "user1")
        self.assertSuccess(response)
        api_ip = json.loads(response.content)["floating_ip"]
        self.assertEqual(api_ip,
92
93
94
95
96
                         {"instance_id": str(ip.nic.machine_id),
                          "ip": ip.address,
                          "fixed_ip": None,
                          "id": str(ip.id),
                          "pool": str(ip.network_id)})
97
98

    def test_wrong_user(self):
99
100
        ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True)
        response = self.delete(URL + "/%s" % ip.id, "user2")
101
102
103
        self.assertItemNotFound(response)

    def test_deleted_ip(self):
104
105
106
        ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True,
                                   deleted=True)
        response = self.delete(URL + "/%s" % ip.id, "user1")
107
108
109
        self.assertItemNotFound(response)

    def test_reserve(self):
110
        request = {'pool': self.pool.id}
111
112
113
        with mocked_quotaholder():
            response = self.post(URL, "test_user", json.dumps(request), "json")
        self.assertSuccess(response)
114
        ip = floating_ips.get()
115
116
117
        self.assertEqual(ip.address, "192.168.2.2")
        self.assertEqual(ip.nic, None)
        self.assertEqual(ip.network, self.pool)
118
        self.assertEqual(json.loads(response.content)["floating_ip"],
119
                         {"instance_id": None, "ip": "192.168.2.2",
120
                          "fixed_ip": None, "id": str(ip.id),
121
                          "pool": str(self.pool.id)})
122

123
    def test_reserve_no_pool(self):
124
125
126
127
128
        # No floating IP pools
        self.pool.delete()
        response = self.post(URL, "test_user", json.dumps({}), "json")
        self.assertFault(response, 503, 'serviceUnavailable')

129
        # Full network
130
131
132
133
134
135
        pool = mf.NetworkWithSubnetFactory(floating_ip_pool=True,
                                           public=True,
                                           subnet__cidr="192.168.2.0/31",
                                           subnet__gateway="192.168.2.1")
        response = self.post(URL, "test_user", json.dumps({}), "json")
        self.assertFault(response, 503, 'serviceUnavailable')
136

137
138
139
        request = {'pool': pool.id}
        response = self.post(URL, "test_user", json.dumps(request), "json")
        self.assertConflict(response)
140

141
    def test_reserve_with_address(self):
142
        request = {'pool': self.pool.id, "address": "192.168.2.10"}
143
144
145
        with mocked_quotaholder():
            response = self.post(URL, "test_user", json.dumps(request), "json")
        self.assertSuccess(response)
146
        ip = floating_ips.get()
147
148
        self.assertEqual(json.loads(response.content)["floating_ip"],
                         {"instance_id": None, "ip": "192.168.2.10",
149
150
                          "fixed_ip": None, "id": str(ip.id),
                          "pool": str(self.pool.id)})
151
152
153
154
155
156

        # Already reserved
        with mocked_quotaholder():
            response = self.post(URL, "test_user", json.dumps(request), "json")
        self.assertFault(response, 409, "conflict")

157
158
159
        # Used by instance
        self.pool.reserve_address("192.168.2.20")
        request = {'pool': self.pool.id, "address": "192.168.2.20"}
160
161
162
163
164
        with mocked_quotaholder():
            response = self.post(URL, "test_user", json.dumps(request), "json")
        self.assertFault(response, 409, "conflict")

        # Address out of pool
165
        request = {'pool': self.pool.id, "address": "192.168.3.5"}
166
167
168
169
        with mocked_quotaholder():
            response = self.post(URL, "test_user", json.dumps(request), "json")
        self.assertBadRequest(response)

170
    def test_release_in_use(self):
171
172
        ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True)
        vm = ip.nic.machine
173
174
175
176
177
        with mocked_quotaholder():
            response = self.delete(URL + "/%s" % ip.id, ip.userid)
        self.assertFault(response, 409, "conflict")
        # Also send a notification to remove the NIC and assert that FIP is in
        # use until notification from ganeti arrives
178
        request = {"removeFloatingIp": {"address": ip.address}}
Christos Stavrakakis's avatar
Christos Stavrakakis committed
179
        url = SERVERS_URL + "/%s/action" % vm.id
180
181
182
183
184
        with patch('synnefo.logic.rapi_pool.GanetiRapiClient') as c:
            c().ModifyInstance.return_value = 10
            response = self.post(url, vm.userid, json.dumps(request),
                                 "json")
        self.assertEqual(response.status_code, 202)
185
186
187
188
189
        with mocked_quotaholder():
            response = self.delete(URL + "/%s" % ip.id, ip.userid)
        self.assertFault(response, 409, "conflict")

    def test_release(self):
190
        ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True, nic=None)
191
192
193
        with mocked_quotaholder():
            response = self.delete(URL + "/%s" % ip.id, ip.userid)
        self.assertSuccess(response)
194
        ips_after = floating_ips.filter(id=ip.id)
195
        self.assertEqual(len(ips_after), 0)
196

197
198
    @patch("synnefo.logic.backend", Mock())
    def test_delete_network_with_floating_ips(self):
199
200
201
202
203
204
        ip = mf.IPv4AddressFactory(userid="user1", floating_ip=True,
                                   network=self.pool, nic=None)
        # Mark the network as non-pubic to not get 403
        network = ip.network
        network.public = False
        network.save()
205
206
        # Can not remove network with floating IPs
        with mocked_quotaholder():
207
208
209
            response = self.delete(NETWORKS_URL + "/%s" % self.pool.id,
                                   self.pool.userid)
        self.assertConflict(response)
210
211
212
213
        # But we can with only deleted Floating Ips
        ip.deleted = True
        ip.save()
        with mocked_quotaholder():
214
215
            response = self.delete(NETWORKS_URL + "/%s" % self.pool.id,
                                   self.pool.userid)
216
217
        self.assertSuccess(response)

218

Christos Stavrakakis's avatar
Christos Stavrakakis committed
219
POOLS_URL = join_urls(compute_path, "os-floating-ip-pools")
220
221


222
class FloatingIPPoolsAPITest(BaseAPITest):
223
224
225
226
227
228
    def test_no_pool(self):
        response = self.get(POOLS_URL)
        self.assertSuccess(response)
        self.assertEqual(json.loads(response.content)["floating_ip_pools"], [])

    def test_list_pools(self):
229
230
231
232
233
234
235
        net = mf.NetworkWithSubnetFactory(floating_ip_pool=True,
                                          public=True,
                                          subnet__cidr="192.168.2.0/30",
                                          subnet__gateway="192.168.2.1")
        mf.NetworkWithSubnetFactory(public=True, deleted=True)
        mf.NetworkWithSubnetFactory(public=False, deleted=False)
        mf.NetworkWithSubnetFactory(public=True, floating_ip_pool=False)
236
237
238
        response = self.get(POOLS_URL)
        self.assertSuccess(response)
        self.assertEqual(json.loads(response.content)["floating_ip_pools"],
239
                         [{"name": str(net.id), "size": 4, "free": 1}])
240
241


242
class FloatingIPActionsTest(BaseAPITest):
243
    def setUp(self):
244
245
246
        self.vm = VirtualMachineFactory()
        self.vm.operstate = "ACTIVE"
        self.vm.save()
247
248

    def test_bad_request(self):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
249
        url = SERVERS_URL + "/%s/action" % self.vm.id
250
251
252
253
254
255
256
257
258
259
        response = self.post(url, self.vm.userid, json.dumps({}), "json")
        self.assertBadRequest(response)
        response = self.post(url, self.vm.userid,
                             json.dumps({"addFloatingIp": {}}),
                             "json")
        self.assertBadRequest(response)

    @patch('synnefo.logic.rapi_pool.GanetiRapiClient')
    def test_add_floating_ip(self, mock):
        # Not exists
Christos Stavrakakis's avatar
Christos Stavrakakis committed
260
        url = SERVERS_URL + "/%s/action" % self.vm.id
261
262
263
264
        request = {"addFloatingIp": {"address": "10.0.0.1"}}
        response = self.post(url, self.vm.userid, json.dumps(request), "json")
        self.assertItemNotFound(response)
        # In use
265
266
        ip = mf.IPv4AddressFactory(floating_ip=True, userid=self.vm.userid)
        request = {"addFloatingIp": {"address": ip.address}}
267
        response = self.post(url, self.vm.userid, json.dumps(request), "json")
268
        self.assertConflict(response)
269
        # Success
270
271
272
        ip = mf.IPv4AddressFactory(floating_ip=True, nic=None,
                                   userid=self.vm.userid)
        request = {"addFloatingIp": {"address": ip.address}}
273
        mock().ModifyInstance.return_value = 1
274
275
        response = self.post(url, self.vm.userid, json.dumps(request), "json")
        self.assertEqual(response.status_code, 202)
276
277
278
        ip_after = floating_ips.get(id=ip.id)
        self.assertEqual(ip_after.nic.machine, self.vm)
        nic = self.vm.nics.get()
279
280
        nic.state = "ACTIVE"
        nic.save()
281
282
283
284
285
        response = self.get(SERVERS_URL + "/%s" % self.vm.id,
                            self.vm.userid)
        self.assertSuccess(response)
        nic = json.loads(response.content)["server"]["attachments"][0]
        self.assertEqual(nic["OS-EXT-IPS:type"], "floating")
286
287
288
289

    @patch('synnefo.logic.rapi_pool.GanetiRapiClient')
    def test_remove_floating_ip(self, mock):
        # Not exists
Christos Stavrakakis's avatar
Christos Stavrakakis committed
290
        url = SERVERS_URL + "/%s/action" % self.vm.id
291
292
        request = {"removeFloatingIp": {"address": "10.0.0.1"}}
        response = self.post(url, self.vm.userid, json.dumps(request), "json")
293
        self.assertBadRequest(response)
294
        # Not In Use
295
296
297
        ip = mf.IPv4AddressFactory(floating_ip=True, nic=None,
                                   userid=self.vm.userid)
        request = {"removeFloatingIp": {"address": ip.address}}
298
        response = self.post(url, self.vm.userid, json.dumps(request), "json")
299
        self.assertBadRequest(response)
300
        # Success
301
302
303
        ip = mf.IPv4AddressFactory(floating_ip=True,
                                   userid=self.vm.userid, nic__machine=self.vm)
        request = {"removeFloatingIp": {"address": ip.address}}
304
        mock().ModifyInstance.return_value = 2
305
306
307
        response = self.post(url, self.vm.userid, json.dumps(request), "json")
        self.assertEqual(response.status_code, 202)
        # Yet used. Wait for the callbacks
308
309
        ip_after = floating_ips.get(id=ip.id)
        self.assertEqual(ip_after.nic.machine, self.vm)