root / snf-cyclades-app / synnefo / quotas / enforce.py @ 13f1e2ff
History | View | Annotate | Download (8.8 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 |
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): |
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 |
|
164 |
return _partition_by(lambda vm: vm.userid, vms) |
165 |
|
166 |
|
167 |
def get_floating_ips(users=None): |
168 |
ips = IPAddress.objects.filter(deleted=False, floating_ip=True).\ |
169 |
select_related("nic__machine")
|
170 |
if users is not None: |
171 |
ips = ips.filter(userid__in=users) |
172 |
|
173 |
return _partition_by(lambda ip: ip.userid, ips) |
174 |
|
175 |
|
176 |
def get_actual_resources(resource_type, users=None): |
177 |
ACTUAL_RESOURCES = { |
178 |
"vm": get_vms,
|
179 |
"floating_ip": get_floating_ips,
|
180 |
} |
181 |
return ACTUAL_RESOURCES[resource_type](users=users)
|
182 |
|
183 |
|
184 |
VM_ACTION = { |
185 |
"REMOVE": servers.destroy,
|
186 |
"SHUTDOWN": servers.stop,
|
187 |
} |
188 |
|
189 |
|
190 |
def apply_to_vm(action, vm_id, shutdown_timeout): |
191 |
try:
|
192 |
vm = VirtualMachine.objects.select_for_update().get(id=vm_id) |
193 |
VM_ACTION[action](vm, shutdown_timeout=shutdown_timeout) |
194 |
return True |
195 |
except BaseException: |
196 |
return False |
197 |
|
198 |
|
199 |
def allow_operation(backend_id, opcount, maxops): |
200 |
if backend_id is None or maxops is None: |
201 |
return True |
202 |
backend_ops = opcount.get(backend_id, 0)
|
203 |
if backend_ops >= maxops:
|
204 |
return False |
205 |
opcount[backend_id] = backend_ops + 1
|
206 |
return True |
207 |
|
208 |
|
209 |
def perform_vm_actions(actions, opcount, maxops=None, fix=False, options={}): |
210 |
log = [] |
211 |
for vm_id, (viol_id, state, backend_id, vm_action) in actions.iteritems(): |
212 |
if not allow_operation(backend_id, opcount, maxops): |
213 |
continue
|
214 |
data = ("vm", vm_id, state, backend_id, vm_action, viol_id)
|
215 |
if fix:
|
216 |
r = apply_to_vm(vm_action, vm_id, options.get("shutdown_timeout"))
|
217 |
data += ("DONE" if r else "FAILED",) |
218 |
log.append(data) |
219 |
return log
|
220 |
|
221 |
|
222 |
def wait_for_ip(ip_id): |
223 |
for i in range(100): |
224 |
ip = IPAddress.objects.get(id=ip_id) |
225 |
if ip.nic_id is None: |
226 |
objs = IPAddress.objects.select_for_update() |
227 |
return objs.get(id=ip_id)
|
228 |
time.sleep(1)
|
229 |
raise ValueError( |
230 |
"Floating_ip %s: Waiting for port delete timed out." % ip_id)
|
231 |
|
232 |
|
233 |
def remove_ip(ip_id): |
234 |
try:
|
235 |
ip = IPAddress.objects.select_for_update().get(id=ip_id) |
236 |
port_id = ip.nic_id |
237 |
if port_id:
|
238 |
objs = NetworkInterface.objects.select_for_update() |
239 |
port = objs.get(id=port_id) |
240 |
servers.delete_port(port) |
241 |
if port.machine:
|
242 |
wait_server_job(port.machine) |
243 |
ip = wait_for_ip(ip_id) |
244 |
logic_ips.delete_floating_ip(ip) |
245 |
return True |
246 |
except BaseException: |
247 |
return False |
248 |
|
249 |
|
250 |
def perform_floating_ip_actions(actions, opcount, maxops=None, fix=False, |
251 |
options={}): |
252 |
log = [] |
253 |
for ip_id, (viol_id, state, backend_id, ip_action) in actions.iteritems(): |
254 |
if not allow_operation(backend_id, opcount, maxops): |
255 |
continue
|
256 |
data = ("floating_ip", ip_id, state, backend_id, ip_action, viol_id)
|
257 |
if ip_action == "REMOVE": |
258 |
if fix:
|
259 |
r = remove_ip(ip_id) |
260 |
data += ("DONE" if r else "FAILED",) |
261 |
log.append(data) |
262 |
return log
|
263 |
|
264 |
|
265 |
def perform_actions(actions, maxops=None, fix=False, options={}): |
266 |
ACTION_HANDLING = [ |
267 |
("floating_ip", perform_floating_ip_actions),
|
268 |
("vm", perform_vm_actions),
|
269 |
] |
270 |
|
271 |
opcount = {} |
272 |
logs = [] |
273 |
for resource_type, handler in ACTION_HANDLING: |
274 |
t_actions = actions.get(resource_type, {}) |
275 |
log = handler(t_actions, opcount, maxops=maxops, fix=fix, |
276 |
options=options) |
277 |
logs += log |
278 |
return logs
|
279 |
|
280 |
|
281 |
# It is important to check resources in this order, especially
|
282 |
# floating_ip after vm resources.
|
283 |
RESOURCE_HANDLING = [ |
284 |
("cyclades.cpu", handle_stop_active, "vm"), |
285 |
("cyclades.ram", handle_stop_active, "vm"), |
286 |
("cyclades.total_cpu", handle_destroy, "vm"), |
287 |
("cyclades.total_ram", handle_destroy, "vm"), |
288 |
("cyclades.disk", handle_destroy, "vm"), |
289 |
("cyclades.vm", handle_destroy, "vm"), |
290 |
("cyclades.floating_ip", handle_floating_ip, "floating_ip"), |
291 |
] |