Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / quotas / enforce.py @ ff5edb80

History | View | Annotate | Download (9.7 kB)

1
# Copyright 2013-2014 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
import time
35
from synnefo.db.models import VirtualMachine, IPAddress, NetworkInterface
36
from synnefo.logic import servers
37
from synnefo.logic import ips as logic_ips
38
from synnefo.logic import backend
39

    
40

    
41
MiB = 2 ** 20
42
GiB = 2 ** 30
43

    
44

    
45
def _partition_by(f, l, convert=None):
46
    if convert is None:
47
        convert = lambda x: x
48
    d = {}
49
    for x in l:
50
        group = f(x)
51
        group_l = d.get(group, [])
52
        group_l.append(convert(x))
53
        d[group] = group_l
54
    return d
55

    
56

    
57
CHANGE = {
58
    "cyclades.ram": lambda vm: vm.flavor.ram * MiB,
59
    "cyclades.cpu": lambda vm: vm.flavor.cpu,
60
    "cyclades.vm": lambda vm: 1,
61
    "cyclades.total_ram": lambda vm: vm.flavor.ram * MiB,
62
    "cyclades.total_cpu": lambda vm: vm.flavor.cpu,
63
    "cyclades.disk": lambda vm: vm.flavor.disk * GiB,
64
    "cyclades.floating_ip": lambda vm: 1,
65
    }
66

    
67

    
68
def wait_server_job(server):
69
    jobID = server.task_job_id
70
    client = server.get_client()
71
    status, error = backend.wait_for_job(client, jobID)
72
    if status != "success":
73
        raise ValueError(error)
74

    
75

    
76
VM_SORT_LEVEL = {
77
    "ERROR": 4,
78
    "BUILD": 3,
79
    "STOPPED": 2,
80
    "STARTED": 1,
81
    "RESIZE": 1,
82
    "DESTROYED": 0,
83
    }
84

    
85

    
86
def sort_vms():
87
    def f(vm):
88
        level = VM_SORT_LEVEL[vm.operstate]
89
        return (level, vm.id)
90
    return f
91

    
92

    
93
def handle_stop_active(viol_id, resource, vms, diff, actions):
94
    vm_actions = actions["vm"]
95
    vms = [vm for vm in vms if vm.operstate in ["STARTED", "BUILD", "ERROR"]]
96
    vms = sorted(vms, key=sort_vms(), reverse=True)
97
    for vm in vms:
98
        if diff < 1:
99
            break
100
        diff -= CHANGE[resource](vm)
101
        if vm_actions.get(vm.id) is None:
102
            action = "REMOVE" if vm.operstate == "ERROR" else "SHUTDOWN"
103
            vm_actions[vm.id] = viol_id, vm.operstate, vm.backend_id, action
104

    
105

    
106
def handle_destroy(viol_id, resource, vms, diff, actions):
107
    vm_actions = actions["vm"]
108
    vms = sorted(vms, key=sort_vms(), reverse=True)
109
    for vm in vms:
110
        if diff < 1:
111
            break
112
        diff -= CHANGE[resource](vm)
113
        vm_actions[vm.id] = viol_id, vm.operstate, vm.backend_id, "REMOVE"
114

    
115

    
116
def _state_after_action(vm, action):
117
    if action == "REMOVE":
118
        return "ERROR"  # highest
119
    if action == "SHUTDOWN":
120
        return "STOPPED"
121
    return vm.operstate  # no action
122

    
123

    
124
def _maybe_action(tpl):
125
    if tpl is None:
126
        return None
127
    return tpl[-1]
128

    
129

    
130
def sort_ips(vm_actions):
131
    def f(ip):
132
        if not ip.in_use():
133
            level = 5
134
        else:
135
            machine = ip.nic.machine
136
            action = _maybe_action(vm_actions.get(machine.id))
137
            level = VM_SORT_LEVEL[_state_after_action(machine, action)]
138
        return (level, ip.id)
139
    return f
140

    
141

    
142
def handle_floating_ip(viol_id, resource, ips, diff, actions):
143
    vm_actions = actions.get("vm", {})
144
    ip_actions = actions["floating_ip"]
145
    ips = sorted(ips, key=sort_ips(vm_actions), reverse=True)
146
    for ip in ips:
147
        if diff < 1:
148
            break
149
        diff -= CHANGE[resource](ip)
150
        state = "USED" if ip.in_use() else "FREE"
151
        if ip.nic and ip.nic.machine:
152
            backend_id = ip.nic.machine.backend_id
153
        else:
154
            backend_id = None
155
        ip_actions[ip.id] = viol_id, state, backend_id, "REMOVE"
156

    
157

    
158
def get_vms(users=None, projects=None):
159
    vms = VirtualMachine.objects.filter(deleted=False).\
160
        select_related("flavor").order_by('-id')
161
    if users is not None:
162
        vms = vms.filter(userid__in=users)
163
    if projects is not None:
164
        vms = vms.filter(project__in=projects)
165

    
166
    vmsdict = _partition_by(lambda vm: vm.project, vms)
167
    for project, projectdict in vmsdict.iteritems():
168
        vmsdict[project] = _partition_by(lambda vm: vm.userid, projectdict)
169
    return vmsdict
170

    
171

    
172
def get_floating_ips(users=None, projects=None):
173
    ips = IPAddress.objects.filter(deleted=False, floating_ip=True).\
174
        select_related("nic__machine")
175
    if users is not None:
176
        ips = ips.filter(userid__in=users)
177
    if projects is not None:
178
        ips = ips.filter(project__in=projects)
179

    
180
    ipsdict = _partition_by(lambda ip: ip.project, ips)
181
    for project, projectdict in ipsdict.iteritems():
182
        ipsdict[project] = _partition_by(lambda ip: ip.userid, projectdict)
183
    return ipsdict
184

    
185

    
186
def get_actual_resources(resource_type, users=None, projects=None):
187
    ACTUAL_RESOURCES = {
188
        "vm": get_vms,
189
        "floating_ip": get_floating_ips,
190
        }
191
    return ACTUAL_RESOURCES[resource_type](users=users, projects=projects)
192

    
193

    
194
def skip_check(obj, to_check=None, excluded=None):
195
    return (to_check is not None and obj not in to_check or
196
            excluded is not None and obj in excluded)
197

    
198

    
199
def pick_project_resources(project_dict, users=None, excluded_users=None):
200
    resources = []
201
    for user, user_resources in project_dict.iteritems():
202
        if skip_check(user, users, excluded_users):
203
            continue
204
        resources += user_resources
205
    return resources
206

    
207

    
208
VM_ACTION = {
209
    "REMOVE": servers.destroy,
210
    "SHUTDOWN": servers.stop,
211
}
212

    
213

    
214
def apply_to_vm(action, vm_id, shutdown_timeout):
215
    try:
216
        vm = VirtualMachine.objects.select_for_update().get(id=vm_id)
217
        VM_ACTION[action](vm, shutdown_timeout=shutdown_timeout)
218
        return True
219
    except BaseException:
220
        return False
221

    
222

    
223
def allow_operation(backend_id, opcount, maxops):
224
    if backend_id is None or maxops is None:
225
        return True
226
    backend_ops = opcount.get(backend_id, 0)
227
    if backend_ops >= maxops:
228
        return False
229
    opcount[backend_id] = backend_ops + 1
230
    return True
231

    
232

    
233
def perform_vm_actions(actions, opcount, maxops=None, fix=False, options={}):
234
    log = []
235
    for vm_id, (viol_id, state, backend_id, vm_action) in actions.iteritems():
236
        if not allow_operation(backend_id, opcount, maxops):
237
            continue
238
        data = ("vm", vm_id, state, backend_id, vm_action, viol_id)
239
        if fix:
240
            r = apply_to_vm(vm_action, vm_id, options.get("shutdown_timeout"))
241
            data += ("DONE" if r else "FAILED",)
242
        log.append(data)
243
    return log
244

    
245

    
246
def wait_for_ip(ip_id):
247
    for i in range(100):
248
        ip = IPAddress.objects.get(id=ip_id)
249
        if ip.nic_id is None:
250
            objs = IPAddress.objects.select_for_update()
251
            return objs.get(id=ip_id)
252
        time.sleep(1)
253
    raise ValueError(
254
        "Floating_ip %s: Waiting for port delete timed out." % ip_id)
255

    
256

    
257
def remove_ip(ip_id):
258
    try:
259
        ip = IPAddress.objects.select_for_update().get(id=ip_id)
260
        port_id = ip.nic_id
261
        if port_id:
262
            objs = NetworkInterface.objects.select_for_update()
263
            port = objs.get(id=port_id)
264
            servers.delete_port(port)
265
            if port.machine:
266
                wait_server_job(port.machine)
267
            ip = wait_for_ip(ip_id)
268
        logic_ips.delete_floating_ip(ip)
269
        return True
270
    except BaseException:
271
        return False
272

    
273

    
274
def perform_floating_ip_actions(actions, opcount, maxops=None, fix=False,
275
                                options={}):
276
    log = []
277
    for ip_id, (viol_id, state, backend_id, ip_action) in actions.iteritems():
278
        if not allow_operation(backend_id, opcount, maxops):
279
            continue
280
        data = ("floating_ip", ip_id, state, backend_id, ip_action, viol_id)
281
        if ip_action == "REMOVE":
282
            if fix:
283
                r = remove_ip(ip_id)
284
                data += ("DONE" if r else "FAILED",)
285
        log.append(data)
286
    return log
287

    
288

    
289
def perform_actions(actions, maxops=None, fix=False, options={}):
290
    ACTION_HANDLING = [
291
        ("floating_ip", perform_floating_ip_actions),
292
        ("vm", perform_vm_actions),
293
        ]
294

    
295
    opcount = {}
296
    logs = []
297
    for resource_type, handler in ACTION_HANDLING:
298
        t_actions = actions.get(resource_type, {})
299
        log = handler(t_actions, opcount, maxops=maxops, fix=fix,
300
                      options=options)
301
        logs += log
302
    return logs
303

    
304

    
305
# It is important to check resources in this order, especially
306
# floating_ip after vm resources.
307
RESOURCE_HANDLING = [
308
    ("cyclades.cpu", handle_stop_active, "vm"),
309
    ("cyclades.ram", handle_stop_active, "vm"),
310
    ("cyclades.total_cpu", handle_destroy, "vm"),
311
    ("cyclades.total_ram", handle_destroy, "vm"),
312
    ("cyclades.disk", handle_destroy, "vm"),
313
    ("cyclades.vm", handle_destroy, "vm"),
314
    ("cyclades.floating_ip", handle_floating_ip, "floating_ip"),
315
    ]