root / snf-cyclades-app / synnefo / quotas / __init__.py @ 62c86226
History | View | Annotate | Download (8.7 kB)
1 | 629acc65 | Giorgos Korfiatis | # Copyright 2012, 2013 GRNET S.A. All rights reserved.
|
---|---|---|---|
2 | bfe7ba3c | Christos Stavrakakis | #
|
3 | bfe7ba3c | Christos Stavrakakis | # Redistribution and use in source and binary forms, with or without
|
4 | bfe7ba3c | Christos Stavrakakis | # modification, are permitted provided that the following conditions
|
5 | bfe7ba3c | Christos Stavrakakis | # are met:
|
6 | bfe7ba3c | Christos Stavrakakis | #
|
7 | bfe7ba3c | Christos Stavrakakis | # 1. Redistributions of source code must retain the above copyright
|
8 | bfe7ba3c | Christos Stavrakakis | # notice, this list of conditions and the following disclaimer.
|
9 | bfe7ba3c | Christos Stavrakakis | #
|
10 | bfe7ba3c | Christos Stavrakakis | # 2. Redistributions in binary form must reproduce the above copyright
|
11 | bfe7ba3c | Christos Stavrakakis | # notice, this list of conditions and the following disclaimer in the
|
12 | bfe7ba3c | Christos Stavrakakis | # documentation and/or other materials provided with the distribution.
|
13 | bfe7ba3c | Christos Stavrakakis | #
|
14 | bfe7ba3c | Christos Stavrakakis | # THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
15 | bfe7ba3c | Christos Stavrakakis | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
16 | bfe7ba3c | Christos Stavrakakis | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
17 | bfe7ba3c | Christos Stavrakakis | # ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
18 | bfe7ba3c | Christos Stavrakakis | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
19 | bfe7ba3c | Christos Stavrakakis | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
20 | bfe7ba3c | Christos Stavrakakis | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
21 | bfe7ba3c | Christos Stavrakakis | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
22 | bfe7ba3c | Christos Stavrakakis | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
23 | bfe7ba3c | Christos Stavrakakis | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
24 | bfe7ba3c | Christos Stavrakakis | # SUCH DAMAGE.
|
25 | bfe7ba3c | Christos Stavrakakis | #
|
26 | bfe7ba3c | Christos Stavrakakis | # The views and conclusions contained in the software and documentation are
|
27 | bfe7ba3c | Christos Stavrakakis | # those of the authors and should not be interpreted as representing official
|
28 | bfe7ba3c | Christos Stavrakakis | # policies, either expressed or implied, of GRNET S.A.
|
29 | bfe7ba3c | Christos Stavrakakis | |
30 | bfe7ba3c | Christos Stavrakakis | from functools import wraps |
31 | 656cf771 | Christos Stavrakakis | from django.utils import simplejson as json |
32 | bfe7ba3c | Christos Stavrakakis | |
33 | bd40abfa | Christos Stavrakakis | from snf_django.lib.api import faults |
34 | 629acc65 | Giorgos Korfiatis | from synnefo.db.models import QuotaHolderSerial |
35 | 6dbd90c0 | Christos Stavrakakis | |
36 | 629acc65 | Giorgos Korfiatis | from synnefo.settings import (CYCLADES_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN, |
37 | 629acc65 | Giorgos Korfiatis | ASTAKOS_URL) |
38 | 629acc65 | Giorgos Korfiatis | from astakosclient import AstakosClient |
39 | 629acc65 | Giorgos Korfiatis | from astakosclient.errors import AstakosClientException, QuotaLimit |
40 | bfe7ba3c | Christos Stavrakakis | |
41 | bfe7ba3c | Christos Stavrakakis | import logging |
42 | bfe7ba3c | Christos Stavrakakis | log = logging.getLogger(__name__) |
43 | bfe7ba3c | Christos Stavrakakis | |
44 | 629acc65 | Giorgos Korfiatis | DEFAULT_SOURCE = 'system'
|
45 | 656cf771 | Christos Stavrakakis | RESOURCES = [ |
46 | 656cf771 | Christos Stavrakakis | "cyclades.vm",
|
47 | 656cf771 | Christos Stavrakakis | "cyclades.cpu",
|
48 | 656cf771 | Christos Stavrakakis | "cyclades.disk",
|
49 | 656cf771 | Christos Stavrakakis | "cyclades.ram",
|
50 | 656cf771 | Christos Stavrakakis | "cyclades.network.private"
|
51 | 656cf771 | Christos Stavrakakis | ] |
52 | bfe7ba3c | Christos Stavrakakis | |
53 | bfe7ba3c | Christos Stavrakakis | |
54 | 629acc65 | Giorgos Korfiatis | class Quotaholder(object): |
55 | 629acc65 | Giorgos Korfiatis | _object = None
|
56 | 629acc65 | Giorgos Korfiatis | |
57 | 629acc65 | Giorgos Korfiatis | @classmethod
|
58 | 629acc65 | Giorgos Korfiatis | def get(cls): |
59 | 629acc65 | Giorgos Korfiatis | if cls._object is None: |
60 | 629acc65 | Giorgos Korfiatis | cls._object = AstakosClient( |
61 | 629acc65 | Giorgos Korfiatis | ASTAKOS_URL, |
62 | 629acc65 | Giorgos Korfiatis | use_pool=True,
|
63 | 629acc65 | Giorgos Korfiatis | logger=log) |
64 | 629acc65 | Giorgos Korfiatis | return cls._object
|
65 | bfe7ba3c | Christos Stavrakakis | |
66 | bfe7ba3c | Christos Stavrakakis | |
67 | bfe7ba3c | Christos Stavrakakis | def uses_commission(func): |
68 | bfe7ba3c | Christos Stavrakakis | """Decorator for wrapping functions that needs commission.
|
69 | bfe7ba3c | Christos Stavrakakis |
|
70 | bfe7ba3c | Christos Stavrakakis | All decorated functions must take as first argument the `serials` list in
|
71 | bfe7ba3c | Christos Stavrakakis | order to extend them with the needed serial numbers, as return by the
|
72 | bfe7ba3c | Christos Stavrakakis | Quotaholder
|
73 | bfe7ba3c | Christos Stavrakakis |
|
74 | bfe7ba3c | Christos Stavrakakis | On successful competition of the decorated function, all serials are
|
75 | bfe7ba3c | Christos Stavrakakis | accepted to the quotaholder, otherwise they are rejected.
|
76 | bfe7ba3c | Christos Stavrakakis |
|
77 | bfe7ba3c | Christos Stavrakakis | """
|
78 | bfe7ba3c | Christos Stavrakakis | |
79 | bfe7ba3c | Christos Stavrakakis | @wraps(func)
|
80 | bfe7ba3c | Christos Stavrakakis | def wrapper(*args, **kwargs): |
81 | bfe7ba3c | Christos Stavrakakis | try:
|
82 | bfe7ba3c | Christos Stavrakakis | serials = [] |
83 | bfe7ba3c | Christos Stavrakakis | ret = func(serials, *args, **kwargs) |
84 | 642c426e | Christos Stavrakakis | except:
|
85 | 642c426e | Christos Stavrakakis | log.exception("Unexpected error")
|
86 | 642c426e | Christos Stavrakakis | try:
|
87 | 642c426e | Christos Stavrakakis | if serials:
|
88 | 642c426e | Christos Stavrakakis | reject_commission(serials=serials) |
89 | 642c426e | Christos Stavrakakis | except:
|
90 | 642c426e | Christos Stavrakakis | log.exception("Exception while rejecting serials %s", serials)
|
91 | 642c426e | Christos Stavrakakis | raise
|
92 | 642c426e | Christos Stavrakakis | raise
|
93 | 642c426e | Christos Stavrakakis | |
94 | 642c426e | Christos Stavrakakis | # func has completed successfully. accept serials
|
95 | 642c426e | Christos Stavrakakis | try:
|
96 | bfe7ba3c | Christos Stavrakakis | if serials:
|
97 | bfe7ba3c | Christos Stavrakakis | accept_commission(serials) |
98 | bfe7ba3c | Christos Stavrakakis | return ret
|
99 | bfe7ba3c | Christos Stavrakakis | except:
|
100 | 642c426e | Christos Stavrakakis | log.exception("Exception while accepting serials %s", serials)
|
101 | bfe7ba3c | Christos Stavrakakis | raise
|
102 | bfe7ba3c | Christos Stavrakakis | return wrapper
|
103 | bfe7ba3c | Christos Stavrakakis | |
104 | bfe7ba3c | Christos Stavrakakis | |
105 | bfe7ba3c | Christos Stavrakakis | ## FIXME: Wrap the following two functions inside transaction ?
|
106 | 11a54cb9 | Christos Stavrakakis | def accept_commission(serials, update_db=True): |
107 | bfe7ba3c | Christos Stavrakakis | """Accept a list of pending commissions.
|
108 | bfe7ba3c | Christos Stavrakakis |
|
109 | bfe7ba3c | Christos Stavrakakis | @param serials: List of QuotaHolderSerial objects
|
110 | bfe7ba3c | Christos Stavrakakis |
|
111 | bfe7ba3c | Christos Stavrakakis | """
|
112 | 11a54cb9 | Christos Stavrakakis | if update_db:
|
113 | 11a54cb9 | Christos Stavrakakis | for s in serials: |
114 | 11a54cb9 | Christos Stavrakakis | if s.pending:
|
115 | 11a54cb9 | Christos Stavrakakis | s.accepted = True
|
116 | 11a54cb9 | Christos Stavrakakis | s.save() |
117 | bfe7ba3c | Christos Stavrakakis | |
118 | 629acc65 | Giorgos Korfiatis | accept_serials = [s.serial for s in serials] |
119 | 629acc65 | Giorgos Korfiatis | qh_resolve_commissions(accept=accept_serials) |
120 | bfe7ba3c | Christos Stavrakakis | |
121 | bfe7ba3c | Christos Stavrakakis | |
122 | 11a54cb9 | Christos Stavrakakis | def reject_commission(serials, update_db=True): |
123 | bfe7ba3c | Christos Stavrakakis | """Reject a list of pending commissions.
|
124 | bfe7ba3c | Christos Stavrakakis |
|
125 | bfe7ba3c | Christos Stavrakakis | @param serials: List of QuotaHolderSerial objects
|
126 | bfe7ba3c | Christos Stavrakakis |
|
127 | bfe7ba3c | Christos Stavrakakis | """
|
128 | 11a54cb9 | Christos Stavrakakis | if update_db:
|
129 | 11a54cb9 | Christos Stavrakakis | for s in serials: |
130 | 11a54cb9 | Christos Stavrakakis | if s.pending:
|
131 | 11a54cb9 | Christos Stavrakakis | s.rejected = True
|
132 | 11a54cb9 | Christos Stavrakakis | s.save() |
133 | bfe7ba3c | Christos Stavrakakis | |
134 | 629acc65 | Giorgos Korfiatis | reject_serials = [s.serial for s in serials] |
135 | 629acc65 | Giorgos Korfiatis | qh_resolve_commissions(reject=reject_serials) |
136 | bfe7ba3c | Christos Stavrakakis | |
137 | bfe7ba3c | Christos Stavrakakis | |
138 | 629acc65 | Giorgos Korfiatis | def issue_commission(user, source, provisions, |
139 | 629acc65 | Giorgos Korfiatis | force=False, auto_accept=False): |
140 | bfe7ba3c | Christos Stavrakakis | """Issue a new commission to the quotaholder.
|
141 | bfe7ba3c | Christos Stavrakakis |
|
142 | bfe7ba3c | Christos Stavrakakis | Issue a new commission to the quotaholder, and create the
|
143 | bfe7ba3c | Christos Stavrakakis | corresponing QuotaHolderSerial object in DB.
|
144 | bfe7ba3c | Christos Stavrakakis |
|
145 | bfe7ba3c | Christos Stavrakakis | """
|
146 | bfe7ba3c | Christos Stavrakakis | |
147 | 629acc65 | Giorgos Korfiatis | qh = Quotaholder.get() |
148 | 629acc65 | Giorgos Korfiatis | try:
|
149 | 629acc65 | Giorgos Korfiatis | serial = qh.issue_one_commission(ASTAKOS_TOKEN, |
150 | 629acc65 | Giorgos Korfiatis | user, source, provisions, |
151 | ccfbf77b | Christos Stavrakakis | force=force, auto_accept=auto_accept) |
152 | 629acc65 | Giorgos Korfiatis | except QuotaLimit as e: |
153 | 629acc65 | Giorgos Korfiatis | msg, details = render_overlimit_exception(e) |
154 | 629acc65 | Giorgos Korfiatis | raise faults.OverLimit(msg, details=details)
|
155 | 629acc65 | Giorgos Korfiatis | except AstakosClientException as e: |
156 | 629acc65 | Giorgos Korfiatis | log.exception("Unexpected error")
|
157 | 629acc65 | Giorgos Korfiatis | raise
|
158 | bfe7ba3c | Christos Stavrakakis | |
159 | b14f55b5 | Christos Stavrakakis | if serial:
|
160 | b14f55b5 | Christos Stavrakakis | return QuotaHolderSerial.objects.create(serial=serial)
|
161 | b14f55b5 | Christos Stavrakakis | else:
|
162 | b14f55b5 | Christos Stavrakakis | raise Exception("No serial") |
163 | bfe7ba3c | Christos Stavrakakis | |
164 | bfe7ba3c | Christos Stavrakakis | |
165 | bfe7ba3c | Christos Stavrakakis | # Wrapper functions for issuing commissions for each resource type. Each
|
166 | bfe7ba3c | Christos Stavrakakis | # functions creates the `commission_info` dictionary as expected by the
|
167 | bfe7ba3c | Christos Stavrakakis | # `issue_commision` function. Commissions for deleting a resource, are the same
|
168 | bfe7ba3c | Christos Stavrakakis | # as for creating the same resource, but with negative resource sizes.
|
169 | bfe7ba3c | Christos Stavrakakis | |
170 | bfe7ba3c | Christos Stavrakakis | |
171 | bfe7ba3c | Christos Stavrakakis | def issue_vm_commission(user, flavor, delete=False): |
172 | 656cf771 | Christos Stavrakakis | resources = get_server_resources(flavor) |
173 | 656cf771 | Christos Stavrakakis | if delete:
|
174 | 656cf771 | Christos Stavrakakis | resources = reverse_quantities(resources) |
175 | 629acc65 | Giorgos Korfiatis | return issue_commission(user, DEFAULT_SOURCE, resources)
|
176 | bfe7ba3c | Christos Stavrakakis | |
177 | bfe7ba3c | Christos Stavrakakis | |
178 | bfe7ba3c | Christos Stavrakakis | def get_server_resources(flavor): |
179 | 656cf771 | Christos Stavrakakis | return {'cyclades.vm': 1, |
180 | 656cf771 | Christos Stavrakakis | 'cyclades.cpu': flavor.cpu,
|
181 | 656cf771 | Christos Stavrakakis | 'cyclades.disk': 1073741824 * flavor.disk, # flavor.disk is in GB |
182 | bfe7ba3c | Christos Stavrakakis | # 'public_ip': 1,
|
183 | bfe7ba3c | Christos Stavrakakis | #'disk_template': flavor.disk_template,
|
184 | 656cf771 | Christos Stavrakakis | 'cyclades.ram': 1048576 * flavor.ram} # flavor.ram is in MB |
185 | bfe7ba3c | Christos Stavrakakis | |
186 | bfe7ba3c | Christos Stavrakakis | |
187 | bfe7ba3c | Christos Stavrakakis | def issue_network_commission(user, delete=False): |
188 | 656cf771 | Christos Stavrakakis | resources = get_network_resources() |
189 | 656cf771 | Christos Stavrakakis | if delete:
|
190 | 656cf771 | Christos Stavrakakis | resources = reverse_quantities(resources) |
191 | 629acc65 | Giorgos Korfiatis | return issue_commission(user, DEFAULT_SOURCE, resources)
|
192 | bfe7ba3c | Christos Stavrakakis | |
193 | bfe7ba3c | Christos Stavrakakis | |
194 | bfe7ba3c | Christos Stavrakakis | def get_network_resources(): |
195 | 656cf771 | Christos Stavrakakis | return {"cyclades.network.private": 1} |
196 | bfe7ba3c | Christos Stavrakakis | |
197 | bfe7ba3c | Christos Stavrakakis | |
198 | 656cf771 | Christos Stavrakakis | def reverse_quantities(resources): |
199 | 656cf771 | Christos Stavrakakis | return dict((r, -s) for r, s in resources.items()) |
200 | 629acc65 | Giorgos Korfiatis | |
201 | 11a54cb9 | Christos Stavrakakis | |
202 | 11a54cb9 | Christos Stavrakakis | ##
|
203 | 11a54cb9 | Christos Stavrakakis | ## Reconcile pending commissions
|
204 | 11a54cb9 | Christos Stavrakakis | ##
|
205 | 11a54cb9 | Christos Stavrakakis | |
206 | 11a54cb9 | Christos Stavrakakis | |
207 | 11a54cb9 | Christos Stavrakakis | def accept_commissions(accepted): |
208 | 629acc65 | Giorgos Korfiatis | qh_resolve_commissions(accept=accepted) |
209 | 11a54cb9 | Christos Stavrakakis | |
210 | 11a54cb9 | Christos Stavrakakis | |
211 | 11a54cb9 | Christos Stavrakakis | def reject_commissions(rejected): |
212 | 629acc65 | Giorgos Korfiatis | qh_resolve_commissions(reject=rejected) |
213 | 11a54cb9 | Christos Stavrakakis | |
214 | 11a54cb9 | Christos Stavrakakis | |
215 | 11a54cb9 | Christos Stavrakakis | def fix_pending_commissions(): |
216 | 11a54cb9 | Christos Stavrakakis | (accepted, rejected) = resolve_pending_commissions() |
217 | 629acc65 | Giorgos Korfiatis | qh_resolve_commissions(accepted, rejected) |
218 | 629acc65 | Giorgos Korfiatis | |
219 | 629acc65 | Giorgos Korfiatis | |
220 | 629acc65 | Giorgos Korfiatis | def qh_resolve_commissions(accept=None, reject=None): |
221 | 629acc65 | Giorgos Korfiatis | if accept is None: |
222 | 629acc65 | Giorgos Korfiatis | accept = [] |
223 | 629acc65 | Giorgos Korfiatis | if reject is None: |
224 | 629acc65 | Giorgos Korfiatis | reject = [] |
225 | 11a54cb9 | Christos Stavrakakis | |
226 | 629acc65 | Giorgos Korfiatis | qh = Quotaholder.get() |
227 | 629acc65 | Giorgos Korfiatis | qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject) |
228 | 11a54cb9 | Christos Stavrakakis | |
229 | 11a54cb9 | Christos Stavrakakis | |
230 | 11a54cb9 | Christos Stavrakakis | def resolve_pending_commissions(): |
231 | 11a54cb9 | Christos Stavrakakis | """Resolve quotaholder pending commissions.
|
232 | 11a54cb9 | Christos Stavrakakis |
|
233 | 11a54cb9 | Christos Stavrakakis | Get pending commissions from the quotaholder and resolve them
|
234 | 11a54cb9 | Christos Stavrakakis | to accepted and rejected, according to the state of the
|
235 | 11a54cb9 | Christos Stavrakakis | QuotaHolderSerial DB table. A pending commission in the quotaholder
|
236 | 11a54cb9 | Christos Stavrakakis | can exist in the QuotaHolderSerial table and be either accepted or
|
237 | 11a54cb9 | Christos Stavrakakis | rejected, or can not exist in this table, so it is rejected.
|
238 | 11a54cb9 | Christos Stavrakakis |
|
239 | 11a54cb9 | Christos Stavrakakis | """
|
240 | 11a54cb9 | Christos Stavrakakis | |
241 | 11a54cb9 | Christos Stavrakakis | qh_pending = get_quotaholder_pending() |
242 | 11a54cb9 | Christos Stavrakakis | if not qh_pending: |
243 | 11a54cb9 | Christos Stavrakakis | return ([], [])
|
244 | 11a54cb9 | Christos Stavrakakis | |
245 | 11a54cb9 | Christos Stavrakakis | qh_pending.sort() |
246 | 11a54cb9 | Christos Stavrakakis | min_ = qh_pending[0]
|
247 | 11a54cb9 | Christos Stavrakakis | |
248 | 11a54cb9 | Christos Stavrakakis | serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
|
249 | 11a54cb9 | Christos Stavrakakis | accepted = serials.filter(accepted=True).values_list('serial', flat=True) |
250 | 11a54cb9 | Christos Stavrakakis | accepted = filter(lambda x: x in qh_pending, accepted) |
251 | 11a54cb9 | Christos Stavrakakis | |
252 | 11a54cb9 | Christos Stavrakakis | rejected = list(set(qh_pending) - set(accepted)) |
253 | 11a54cb9 | Christos Stavrakakis | |
254 | 11a54cb9 | Christos Stavrakakis | return (accepted, rejected)
|
255 | 11a54cb9 | Christos Stavrakakis | |
256 | 11a54cb9 | Christos Stavrakakis | |
257 | 11a54cb9 | Christos Stavrakakis | def get_quotaholder_pending(): |
258 | 629acc65 | Giorgos Korfiatis | qh = Quotaholder.get() |
259 | 629acc65 | Giorgos Korfiatis | pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN) |
260 | 11a54cb9 | Christos Stavrakakis | return pending_serials
|
261 | 5a70b1a3 | Christos Stavrakakis | |
262 | 5a70b1a3 | Christos Stavrakakis | |
263 | 629acc65 | Giorgos Korfiatis | def render_overlimit_exception(e): |
264 | 5a70b1a3 | Christos Stavrakakis | resource_name = {"vm": "Virtual Machine", |
265 | 5a70b1a3 | Christos Stavrakakis | "cpu": "CPU", |
266 | 5a70b1a3 | Christos Stavrakakis | "ram": "RAM", |
267 | 5a70b1a3 | Christos Stavrakakis | "network.private": "Private Network"} |
268 | 656cf771 | Christos Stavrakakis | details = json.loads(e.details) |
269 | 629acc65 | Giorgos Korfiatis | data = details['overLimit']['data'] |
270 | 656cf771 | Christos Stavrakakis | usage = data["usage"]
|
271 | 656cf771 | Christos Stavrakakis | limit = data["limit"]
|
272 | 656cf771 | Christos Stavrakakis | available = limit - usage |
273 | 629acc65 | Giorgos Korfiatis | provision = data['provision']
|
274 | 629acc65 | Giorgos Korfiatis | requested = provision['quantity']
|
275 | 629acc65 | Giorgos Korfiatis | resource = provision['resource']
|
276 | 629acc65 | Giorgos Korfiatis | res = resource.replace("cyclades.", "", 1) |
277 | 5a70b1a3 | Christos Stavrakakis | try:
|
278 | 5a70b1a3 | Christos Stavrakakis | resource = resource_name[res] |
279 | 5a70b1a3 | Christos Stavrakakis | except KeyError: |
280 | 5a70b1a3 | Christos Stavrakakis | resource = res |
281 | 5a70b1a3 | Christos Stavrakakis | |
282 | 5a70b1a3 | Christos Stavrakakis | msg = "Resource Limit Exceeded for your account."
|
283 | 5a70b1a3 | Christos Stavrakakis | details = "Limit for resource '%s' exceeded for your account."\
|
284 | 629acc65 | Giorgos Korfiatis | " Available: %s, Requested: %s"\
|
285 | 629acc65 | Giorgos Korfiatis | % (resource, available, requested) |
286 | 5a70b1a3 | Christos Stavrakakis | return msg, details |