Revision 629acc65 snf-cyclades-app/synnefo/quotas/__init__.py
b/snf-cyclades-app/synnefo/quotas/__init__.py | ||
---|---|---|
1 |
# Copyright 2012 GRNET S.A. All rights reserved. |
|
1 |
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
|
|
2 | 2 |
# |
3 | 3 |
# Redistribution and use in source and binary forms, with or without |
4 | 4 |
# modification, are permitted provided that the following conditions |
... | ... | |
31 | 31 |
from contextlib import contextmanager |
32 | 32 |
|
33 | 33 |
from snf_django.lib.api import faults |
34 |
from synnefo.db.models import QuotaHolderSerial, VirtualMachine, Network
|
|
34 |
from synnefo.db.models import QuotaHolderSerial |
|
35 | 35 |
from synnefo.settings import CYCLADES_USE_QUOTAHOLDER |
36 | 36 |
|
37 |
if CYCLADES_USE_QUOTAHOLDER: |
|
38 |
from synnefo.settings import (CYCLADES_QUOTAHOLDER_URL, |
|
39 |
CYCLADES_QUOTAHOLDER_TOKEN, |
|
40 |
CYCLADES_QUOTAHOLDER_POOLSIZE) |
|
41 |
from synnefo.lib.quotaholder import QuotaholderClient |
|
42 |
else: |
|
43 |
from synnefo.settings import (VMS_USER_QUOTA, MAX_VMS_PER_USER, |
|
44 |
NETWORKS_USER_QUOTA, MAX_NETWORKS_PER_USER) |
|
45 |
|
|
46 |
from synnefo.lib.quotaholder.api import (NoCapacityError, NoQuantityError, |
|
47 |
NoEntityError, CallError) |
|
37 |
from synnefo.settings import (CYCLADES_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN, |
|
38 |
ASTAKOS_URL) |
|
39 |
from astakosclient import AstakosClient |
|
40 |
from astakosclient.errors import AstakosClientException, QuotaLimit |
|
48 | 41 |
|
49 | 42 |
import logging |
50 | 43 |
log = logging.getLogger(__name__) |
51 | 44 |
|
45 |
DEFAULT_SOURCE = 'system' |
|
52 | 46 |
|
53 |
class DummySerial(QuotaHolderSerial): |
|
54 |
accepted = True |
|
55 |
rejected = True |
|
56 |
pending = True |
|
57 |
id = None |
|
58 |
|
|
59 |
def save(*args, **kwargs): |
|
60 |
pass |
|
61 |
|
|
62 |
|
|
63 |
class DummyQuotaholderClient(object): |
|
64 |
def issue_commission(self, **commission_info): |
|
65 |
provisions = commission_info["provisions"] |
|
66 |
userid = commission_info["target"] |
|
67 |
for provision in provisions: |
|
68 |
entity, resource, size = provision |
|
69 |
if resource == "cyclades.vm" and size > 0: |
|
70 |
user_vms = VirtualMachine.objects.filter(userid=userid, |
|
71 |
deleted=False).count() |
|
72 |
user_vm_limit = VMS_USER_QUOTA.get(userid, MAX_VMS_PER_USER) |
|
73 |
log.debug("Users VMs %s User Limits %s", user_vms, |
|
74 |
user_vm_limit) |
|
75 |
if user_vms + size > user_vm_limit: |
|
76 |
raise NoQuantityError(source='cyclades', |
|
77 |
target=userid, |
|
78 |
resource=resource, |
|
79 |
requested=size, |
|
80 |
current=user_vms, |
|
81 |
limit=user_vm_limit) |
|
82 |
if resource == "cyclades.network.private" and size > 0: |
|
83 |
user_networks = Network.objects.filter(userid=userid, |
|
84 |
deleted=False).count() |
|
85 |
user_network_limit =\ |
|
86 |
NETWORKS_USER_QUOTA.get(userid, MAX_NETWORKS_PER_USER) |
|
87 |
if user_networks + size > user_network_limit: |
|
88 |
raise NoQuantityError(source='cyclades', |
|
89 |
target=userid, |
|
90 |
resource=resource, |
|
91 |
requested=size, |
|
92 |
current=user_networks, |
|
93 |
limit=user_network_limit) |
|
94 |
|
|
95 |
return None |
|
96 |
|
|
97 |
def accept_commission(self, *args, **kwargs): |
|
98 |
pass |
|
99 |
|
|
100 |
def reject_commission(self, *args, **kwargs): |
|
101 |
pass |
|
102 |
|
|
103 |
def get_pending_commissions(self, *args, **kwargs): |
|
104 |
return [] |
|
105 |
|
|
106 |
|
|
107 |
@contextmanager |
|
108 |
def get_quota_holder(): |
|
109 |
"""Context manager for using a QuotaHolder.""" |
|
110 |
if CYCLADES_USE_QUOTAHOLDER: |
|
111 |
quotaholder = QuotaholderClient(CYCLADES_QUOTAHOLDER_URL, |
|
112 |
token=CYCLADES_QUOTAHOLDER_TOKEN, |
|
113 |
poolsize=CYCLADES_QUOTAHOLDER_POOLSIZE) |
|
114 |
else: |
|
115 |
quotaholder = DummyQuotaholderClient() |
|
116 | 47 |
|
117 |
try: |
|
118 |
yield quotaholder |
|
119 |
finally: |
|
120 |
pass |
|
48 |
class Quotaholder(object): |
|
49 |
_object = None |
|
50 |
|
|
51 |
@classmethod |
|
52 |
def get(cls): |
|
53 |
if cls._object is None: |
|
54 |
cls._object = AstakosClient( |
|
55 |
ASTAKOS_URL, |
|
56 |
use_pool=True, |
|
57 |
logger=log) |
|
58 |
return cls._object |
|
121 | 59 |
|
122 | 60 |
|
123 | 61 |
def uses_commission(func): |
... | ... | |
171 | 109 |
s.accepted = True |
172 | 110 |
s.save() |
173 | 111 |
|
174 |
with get_quota_holder() as qh: |
|
175 |
qh.accept_commission(context={}, |
|
176 |
clientkey='cyclades', |
|
177 |
serials=[s.serial for s in serials]) |
|
112 |
accept_serials = [s.serial for s in serials] |
|
113 |
qh_resolve_commissions(accept=accept_serials) |
|
178 | 114 |
|
179 | 115 |
|
180 | 116 |
def reject_commission(serials, update_db=True): |
... | ... | |
189 | 125 |
s.rejected = True |
190 | 126 |
s.save() |
191 | 127 |
|
192 |
with get_quota_holder() as qh: |
|
193 |
qh.reject_commission(context={}, |
|
194 |
clientkey='cyclades', |
|
195 |
serials=[s.serial for s in serials]) |
|
128 |
reject_serials = [s.serial for s in serials] |
|
129 |
qh_resolve_commissions(reject=reject_serials) |
|
196 | 130 |
|
197 | 131 |
|
198 |
def issue_commission(**commission_info): |
|
132 |
def issue_commission(user, source, provisions, |
|
133 |
force=False, auto_accept=False): |
|
199 | 134 |
"""Issue a new commission to the quotaholder. |
200 | 135 |
|
201 | 136 |
Issue a new commission to the quotaholder, and create the |
... | ... | |
203 | 138 |
|
204 | 139 |
""" |
205 | 140 |
|
206 |
with get_quota_holder() as qh: |
|
207 |
try: |
|
208 |
serial = qh.issue_commission(**commission_info) |
|
209 |
except (NoCapacityError, NoQuantityError) as e: |
|
210 |
msg, details = render_quotaholder_exception(e) |
|
211 |
raise faults.OverLimit(msg, details=details) |
|
212 |
except CallError as e: |
|
213 |
log.exception("Unexpected error") |
|
214 |
raise |
|
141 |
qh = Quotaholder.get() |
|
142 |
try: |
|
143 |
serial = qh.issue_one_commission(ASTAKOS_TOKEN, |
|
144 |
user, source, provisions, |
|
145 |
force, auto_accept) |
|
146 |
except QuotaLimit as e: |
|
147 |
msg, details = render_overlimit_exception(e) |
|
148 |
raise faults.OverLimit(msg, details=details) |
|
149 |
except AstakosClientException as e: |
|
150 |
log.exception("Unexpected error") |
|
151 |
raise |
|
215 | 152 |
|
216 | 153 |
if serial: |
217 | 154 |
return QuotaHolderSerial.objects.create(serial=serial) |
218 |
elif not CYCLADES_USE_QUOTAHOLDER: |
|
219 |
return DummySerial() |
|
220 | 155 |
else: |
221 | 156 |
raise Exception("No serial") |
222 | 157 |
|
... | ... | |
228 | 163 |
|
229 | 164 |
|
230 | 165 |
def issue_vm_commission(user, flavor, delete=False): |
231 |
resources = get_server_resources(flavor) |
|
232 |
commission_info = create_commission(user, resources, delete) |
|
233 |
|
|
234 |
return issue_commission(**commission_info) |
|
166 |
resources = prepare(get_server_resources(flavor), delete) |
|
167 |
return issue_commission(user, DEFAULT_SOURCE, resources) |
|
235 | 168 |
|
236 | 169 |
|
237 | 170 |
def get_server_resources(flavor): |
... | ... | |
244 | 177 |
|
245 | 178 |
|
246 | 179 |
def issue_network_commission(user, delete=False): |
247 |
resources = get_network_resources() |
|
248 |
commission_info = create_commission(user, resources, delete) |
|
249 |
|
|
250 |
return issue_commission(**commission_info) |
|
180 |
resources = prepare(get_network_resources(), delete) |
|
181 |
return issue_commission(user, DEFAULT_SOURCE, resources) |
|
251 | 182 |
|
252 | 183 |
|
253 | 184 |
def get_network_resources(): |
254 | 185 |
return {"network.private": 1} |
255 | 186 |
|
256 | 187 |
|
257 |
def invert_resources(resources_dict): |
|
258 |
return dict((r, -s) for r, s in resources_dict.items()) |
|
259 |
|
|
260 |
|
|
261 |
def create_commission(user, resources, delete=False): |
|
188 |
def prepare(resources_dict, delete): |
|
262 | 189 |
if delete: |
263 |
resources = invert_resources(resources) |
|
264 |
provisions = [('cyclades', 'cyclades.' + r, s) |
|
265 |
for r, s in resources.items()] |
|
266 |
return {"context": {}, |
|
267 |
"target": user, |
|
268 |
"key": "1", |
|
269 |
"clientkey": "cyclades", |
|
270 |
#"owner": "", |
|
271 |
#"ownerkey": "1", |
|
272 |
"name": "", |
|
273 |
"provisions": provisions} |
|
190 |
return dict((r, -s) for r, s in resources_dict.items()) |
|
191 |
return resources_dict |
|
192 |
|
|
274 | 193 |
|
275 | 194 |
## |
276 | 195 |
## Reconcile pending commissions |
... | ... | |
278 | 197 |
|
279 | 198 |
|
280 | 199 |
def accept_commissions(accepted): |
281 |
with get_quota_holder() as qh: |
|
282 |
qh.accept_commission(context={}, |
|
283 |
clientkey='cyclades', |
|
284 |
serials=accepted) |
|
200 |
qh_resolve_commissions(accept=accepted) |
|
285 | 201 |
|
286 | 202 |
|
287 | 203 |
def reject_commissions(rejected): |
288 |
with get_quota_holder() as qh: |
|
289 |
qh.reject_commission(context={}, |
|
290 |
clientkey='cyclades', |
|
291 |
serials=rejected) |
|
204 |
qh_resolve_commissions(reject=rejected) |
|
292 | 205 |
|
293 | 206 |
|
294 | 207 |
def fix_pending_commissions(): |
295 | 208 |
(accepted, rejected) = resolve_pending_commissions() |
209 |
qh_resolve_commissions(accepted, rejected) |
|
210 |
|
|
211 |
|
|
212 |
def qh_resolve_commissions(accept=None, reject=None): |
|
213 |
if accept is None: |
|
214 |
accept = [] |
|
215 |
if reject is None: |
|
216 |
reject = [] |
|
296 | 217 |
|
297 |
with get_quota_holder() as qh: |
|
298 |
if accepted: |
|
299 |
qh.accept_commission(context={}, |
|
300 |
clientkey='cyclades', |
|
301 |
serials=accepted) |
|
302 |
if rejected: |
|
303 |
qh.reject_commission(context={}, |
|
304 |
clientkey='cyclades', |
|
305 |
serials=rejected) |
|
218 |
qh = Quotaholder.get() |
|
219 |
qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject) |
|
306 | 220 |
|
307 | 221 |
|
308 | 222 |
def resolve_pending_commissions(): |
... | ... | |
333 | 247 |
|
334 | 248 |
|
335 | 249 |
def get_quotaholder_pending(): |
336 |
with get_quota_holder() as qh: |
|
337 |
pending_serials = qh.get_pending_commissions(context={}, |
|
338 |
clientkey='cyclades') |
|
250 |
qh = Quotaholder.get() |
|
251 |
pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN) |
|
339 | 252 |
return pending_serials |
340 | 253 |
|
341 | 254 |
|
342 |
def render_quotaholder_exception(e):
|
|
255 |
def render_overlimit_exception(e):
|
|
343 | 256 |
resource_name = {"vm": "Virtual Machine", |
344 | 257 |
"cpu": "CPU", |
345 | 258 |
"ram": "RAM", |
346 | 259 |
"network.private": "Private Network"} |
347 |
res = e.resource.replace("cyclades.", "", 1) |
|
260 |
details = e.details |
|
261 |
data = details['overLimit']['data'] |
|
262 |
available = data['available'] |
|
263 |
provision = data['provision'] |
|
264 |
requested = provision['quantity'] |
|
265 |
resource = provision['resource'] |
|
266 |
res = resource.replace("cyclades.", "", 1) |
|
348 | 267 |
try: |
349 | 268 |
resource = resource_name[res] |
350 | 269 |
except KeyError: |
351 | 270 |
resource = res |
352 | 271 |
|
353 |
requested = e.requested |
|
354 |
current = e.current |
|
355 |
limit = e.limit |
|
356 | 272 |
msg = "Resource Limit Exceeded for your account." |
357 | 273 |
details = "Limit for resource '%s' exceeded for your account."\ |
358 |
" Current value: %s, Limit: %s, Requested: %s"\
|
|
359 |
% (resource, current, limit, requested)
|
|
274 |
" Available: %s, Requested: %s"\
|
|
275 |
% (resource, available, requested)
|
|
360 | 276 |
return msg, details |
Also available in: Unified diff