Revision d14155e3
b/docs/admin-guide.rst | ||
---|---|---|
970 | 970 |
backend-modify Modify a backend |
971 | 971 |
backend-update-status Update backend statistics for instance allocation |
972 | 972 |
backend-remove Remove a Ganeti backend |
973 |
enforce-resources-cyclades Check and fix quota violations for Cyclades resources |
|
973 | 974 |
server-create Create a new server |
974 | 975 |
server-show Show server details |
975 | 976 |
server-list List servers |
b/snf-cyclades-app/synnefo/db/models.py | ||
---|---|---|
732 | 732 |
% (self.address, self.network_id, self.subnet_id, ip_type) |
733 | 733 |
|
734 | 734 |
def in_use(self): |
735 |
if self.machine is None: |
|
735 |
if self.nic is None or self.nic.machine is None:
|
|
736 | 736 |
return False |
737 | 737 |
else: |
738 |
return (not self.machine.deleted) |
|
738 |
return (not self.nic.machine.deleted)
|
|
739 | 739 |
|
740 | 740 |
class Meta: |
741 | 741 |
unique_together = ("network", "address") |
b/snf-cyclades-app/synnefo/quotas/enforce.py | ||
---|---|---|
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, 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, "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 sort_ips(vm_actions): |
|
125 |
def f(ip): |
|
126 |
if not ip.in_use(): |
|
127 |
level = 5 |
|
128 |
else: |
|
129 |
machine = ip.nic.machine |
|
130 |
_, _, action = vm_actions.get(machine.id, (None, None, None)) |
|
131 |
level = VM_SORT_LEVEL[_state_after_action(machine, action)] |
|
132 |
return (level, ip.id) |
|
133 |
return f |
|
134 |
|
|
135 |
|
|
136 |
def handle_floating_ip(viol_id, resource, ips, diff, actions): |
|
137 |
vm_actions = actions.get("vm", {}) |
|
138 |
ip_actions = actions["floating_ip"] |
|
139 |
ips = sorted(ips, key=sort_ips(vm_actions), reverse=True) |
|
140 |
for ip in ips: |
|
141 |
if diff < 1: |
|
142 |
break |
|
143 |
diff -= CHANGE[resource](ip) |
|
144 |
state = "USED" if ip.in_use() else "FREE" |
|
145 |
ip_actions[ip.id] = viol_id, state, "REMOVE" |
|
146 |
|
|
147 |
|
|
148 |
def get_vms(users=None): |
|
149 |
vms = VirtualMachine.objects.filter(deleted=False).\ |
|
150 |
select_related("flavor").order_by('-id') |
|
151 |
if users is not None: |
|
152 |
vms = vms.filter(userid__in=users) |
|
153 |
|
|
154 |
return _partition_by(lambda vm: vm.userid, vms) |
|
155 |
|
|
156 |
|
|
157 |
def get_floating_ips(users=None): |
|
158 |
ips = IPAddress.objects.filter(deleted=False, floating_ip=True).\ |
|
159 |
select_related("nic__machine") |
|
160 |
if users is not None: |
|
161 |
ips = ips.filter(userid__in=users) |
|
162 |
|
|
163 |
return _partition_by(lambda ip: ip.userid, ips) |
|
164 |
|
|
165 |
|
|
166 |
def get_actual_resources(resource_type, users=None): |
|
167 |
ACTUAL_RESOURCES = { |
|
168 |
"vm": get_vms, |
|
169 |
"floating_ip": get_floating_ips, |
|
170 |
} |
|
171 |
return ACTUAL_RESOURCES[resource_type](users=users) |
|
172 |
|
|
173 |
|
|
174 |
VM_ACTION = { |
|
175 |
"REMOVE": servers.destroy, |
|
176 |
"SHUTDOWN": servers.stop, |
|
177 |
} |
|
178 |
|
|
179 |
|
|
180 |
def apply_to_vm(action, vm_id): |
|
181 |
try: |
|
182 |
vm = VirtualMachine.objects.select_for_update().get(id=vm_id) |
|
183 |
VM_ACTION[action](vm) |
|
184 |
return True |
|
185 |
except BaseException: |
|
186 |
return False |
|
187 |
|
|
188 |
|
|
189 |
def perform_vm_actions(actions, fix=False): |
|
190 |
log = [] |
|
191 |
for vm_id, (viol_id, state, vm_action) in actions.iteritems(): |
|
192 |
data = ("vm", vm_id, state, vm_action, viol_id) |
|
193 |
if fix: |
|
194 |
r = apply_to_vm(vm_action, vm_id) |
|
195 |
data += ("DONE" if r else "FAILED",) |
|
196 |
log.append(data) |
|
197 |
return log |
|
198 |
|
|
199 |
|
|
200 |
def wait_for_ip(ip_id): |
|
201 |
for i in range(100): |
|
202 |
ip = IPAddress.objects.get(id=ip_id) |
|
203 |
if ip.nic_id is None: |
|
204 |
objs = IPAddress.objects.select_for_update() |
|
205 |
return objs.get(id=ip_id) |
|
206 |
time.sleep(1) |
|
207 |
raise ValueError( |
|
208 |
"Floating_ip %s: Waiting for port delete timed out." % ip_id) |
|
209 |
|
|
210 |
|
|
211 |
def remove_ip(ip_id): |
|
212 |
try: |
|
213 |
ip = IPAddress.objects.select_for_update().get(id=ip_id) |
|
214 |
port_id = ip.nic_id |
|
215 |
if port_id: |
|
216 |
objs = NetworkInterface.objects.select_for_update() |
|
217 |
port = objs.get(id=port_id) |
|
218 |
servers.delete_port(port) |
|
219 |
if port.machine: |
|
220 |
wait_server_job(port.machine) |
|
221 |
ip = wait_for_ip(ip_id) |
|
222 |
logic_ips.delete_floating_ip(ip) |
|
223 |
return True |
|
224 |
except BaseException: |
|
225 |
return False |
|
226 |
|
|
227 |
|
|
228 |
def perform_floating_ip_actions(actions, fix=False): |
|
229 |
log = [] |
|
230 |
for ip_id, (viol_id, state, ip_action) in actions.iteritems(): |
|
231 |
data = ("floating_ip", ip_id, state, ip_action, viol_id) |
|
232 |
if ip_action == "REMOVE": |
|
233 |
if fix: |
|
234 |
r = remove_ip(ip_id) |
|
235 |
data += ("DONE" if r else "FAILED",) |
|
236 |
log.append(data) |
|
237 |
return log |
|
238 |
|
|
239 |
|
|
240 |
def perform_actions(actions, fix=False): |
|
241 |
ACTION_HANDLING = [ |
|
242 |
("floating_ip", perform_floating_ip_actions), |
|
243 |
("vm", perform_vm_actions), |
|
244 |
] |
|
245 |
|
|
246 |
logs = [] |
|
247 |
for resource_type, handler in ACTION_HANDLING: |
|
248 |
t_actions = actions.get(resource_type, {}) |
|
249 |
log = handler(t_actions, fix=fix) |
|
250 |
logs += log |
|
251 |
return logs |
|
252 |
|
|
253 |
|
|
254 |
# It is important to check resources in this order, especially |
|
255 |
# floating_ip after vm resources. |
|
256 |
RESOURCE_HANDLING = [ |
|
257 |
("cyclades.cpu", handle_stop_active, "vm"), |
|
258 |
("cyclades.ram", handle_stop_active, "vm"), |
|
259 |
("cyclades.total_cpu", handle_destroy, "vm"), |
|
260 |
("cyclades.total_ram", handle_destroy, "vm"), |
|
261 |
("cyclades.disk", handle_destroy, "vm"), |
|
262 |
("cyclades.vm", handle_destroy, "vm"), |
|
263 |
("cyclades.floating_ip", handle_floating_ip, "floating_ip"), |
|
264 |
] |
b/snf-cyclades-app/synnefo/quotas/management/commands/enforce-resources-cyclades.py | ||
---|---|---|
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 string |
|
35 |
from optparse import make_option |
|
36 |
from django.db import transaction |
|
37 |
|
|
38 |
from synnefo.quotas import util |
|
39 |
from synnefo.quotas import enforce |
|
40 |
from synnefo.quotas import errors |
|
41 |
from snf_django.management.commands import SynnefoCommand, CommandError |
|
42 |
from snf_django.management.utils import pprint_table |
|
43 |
|
|
44 |
|
|
45 |
DEFAULT_RESOURCES = ["cyclades.cpu", |
|
46 |
"cyclades.ram", |
|
47 |
"cyclades.floating_ip", |
|
48 |
] |
|
49 |
|
|
50 |
|
|
51 |
class Command(SynnefoCommand): |
|
52 |
help = """Check and fix quota violations for Cyclades resources. |
|
53 |
""" |
|
54 |
option_list = SynnefoCommand.option_list + ( |
|
55 |
make_option("--users", dest="users", |
|
56 |
help=("Enforce resources only for the specified list " |
|
57 |
"of users, e.g uuid1,uuid2")), |
|
58 |
make_option("--resources", |
|
59 |
help="Specify resources to check, default: %s" % |
|
60 |
",".join(DEFAULT_RESOURCES)), |
|
61 |
make_option("--fix", |
|
62 |
default=False, |
|
63 |
action="store_true", |
|
64 |
help="Fix violations"), |
|
65 |
make_option("--force", |
|
66 |
default=False, |
|
67 |
action="store_true", |
|
68 |
help=("Confirm actions that may permanently " |
|
69 |
"remove a vm")), |
|
70 |
) |
|
71 |
|
|
72 |
def confirm(self): |
|
73 |
self.stderr.write("Confirm? [y/N] ") |
|
74 |
response = raw_input() |
|
75 |
if string.lower(response) not in ['y', 'yes']: |
|
76 |
self.stdout.write("Aborted.\n") |
|
77 |
exit() |
|
78 |
|
|
79 |
def get_handlers(self, resources): |
|
80 |
def rem(v): |
|
81 |
try: |
|
82 |
resources.remove(v) |
|
83 |
return True |
|
84 |
except ValueError: |
|
85 |
return False |
|
86 |
|
|
87 |
if resources is None: |
|
88 |
resources = list(DEFAULT_RESOURCES) |
|
89 |
else: |
|
90 |
resources = resources.split(",") |
|
91 |
|
|
92 |
handlers = [h for h in enforce.RESOURCE_HANDLING if rem(h[0])] |
|
93 |
if resources: |
|
94 |
m = "No such resource '%s'" % resources[0] |
|
95 |
raise CommandError(m) |
|
96 |
return handlers |
|
97 |
|
|
98 |
@transaction.commit_on_success |
|
99 |
def handle(self, *args, **options): |
|
100 |
write = self.stderr.write |
|
101 |
fix = options["fix"] |
|
102 |
force = options["force"] |
|
103 |
|
|
104 |
users = options['users'] |
|
105 |
if users is not None: |
|
106 |
users = users.split(',') |
|
107 |
|
|
108 |
handlers = self.get_handlers(options["resources"]) |
|
109 |
try: |
|
110 |
qh_holdings = util.get_qh_users_holdings(users) |
|
111 |
except errors.AstakosClientException as e: |
|
112 |
raise CommandError(e) |
|
113 |
|
|
114 |
resources = set(h[0] for h in handlers) |
|
115 |
dangerous = bool(resources.difference(DEFAULT_RESOURCES)) |
|
116 |
|
|
117 |
actions = {} |
|
118 |
overlimit = [] |
|
119 |
viol_id = 0 |
|
120 |
for resource, handle_resource, resource_type in handlers: |
|
121 |
if resource_type not in actions: |
|
122 |
actions[resource_type] = {} |
|
123 |
actual_resources = enforce.get_actual_resources(resource_type, |
|
124 |
users) |
|
125 |
for user, user_quota in qh_holdings.iteritems(): |
|
126 |
for source, source_quota in user_quota.iteritems(): |
|
127 |
try: |
|
128 |
qh = util.transform_quotas(source_quota) |
|
129 |
qh_value, qh_limit, qh_pending = qh[resource] |
|
130 |
except KeyError: |
|
131 |
write("Resource '%s' does not exist in Quotaholder" |
|
132 |
" for user '%s' and source '%s'!\n" % |
|
133 |
(resource, user, source)) |
|
134 |
continue |
|
135 |
if qh_pending: |
|
136 |
write("Pending commission for user '%s', source '%s', " |
|
137 |
"resource '%s'. Skipping\n" % |
|
138 |
(user, source, resource)) |
|
139 |
continue |
|
140 |
diff = qh_value - qh_limit |
|
141 |
if diff > 0: |
|
142 |
viol_id += 1 |
|
143 |
overlimit.append((viol_id, user, source, resource, |
|
144 |
qh_limit, qh_value)) |
|
145 |
relevant_resources = actual_resources[user] |
|
146 |
handle_resource(viol_id, resource, relevant_resources, |
|
147 |
diff, actions) |
|
148 |
|
|
149 |
if not overlimit: |
|
150 |
write("No violations.\n") |
|
151 |
return |
|
152 |
|
|
153 |
headers = ("#", "User", "Source", "Resource", "Limit", "Usage") |
|
154 |
pprint_table(self.stderr, overlimit, headers, |
|
155 |
options["output_format"], title="Violations") |
|
156 |
|
|
157 |
if any(actions.values()): |
|
158 |
write("\n") |
|
159 |
if fix: |
|
160 |
if dangerous and not force: |
|
161 |
write("You are enforcing resources that may permanently " |
|
162 |
"remove a vm.\n") |
|
163 |
self.confirm() |
|
164 |
write("Applying actions. Please wait...\n") |
|
165 |
title = "Applied Actions" if fix else "Suggested Actions" |
|
166 |
log = enforce.perform_actions(actions, fix=fix) |
|
167 |
headers = ("Type", "ID", "State", "Action", "Violation") |
|
168 |
if fix: |
|
169 |
headers += ("Result",) |
|
170 |
pprint_table(self.stderr, log, headers, |
|
171 |
options["output_format"], title=title) |
b/snf-cyclades-app/synnefo/quotas/util.py | ||
---|---|---|
103 | 103 |
return qh.service_get_quotas(user) |
104 | 104 |
|
105 | 105 |
|
106 |
def get_qh_users_holdings(users=None): |
|
107 |
qh = Quotaholder.get() |
|
108 |
if users is None or len(users) != 1: |
|
109 |
req = None |
|
110 |
else: |
|
111 |
req = users[0] |
|
112 |
quotas = qh.service_get_quotas(req) |
|
113 |
|
|
114 |
if users is None: |
|
115 |
return quotas |
|
116 |
|
|
117 |
qs = {} |
|
118 |
for user in users: |
|
119 |
try: |
|
120 |
qs[user] = quotas[user] |
|
121 |
except KeyError: |
|
122 |
pass |
|
123 |
return qs |
|
124 |
|
|
125 |
|
|
106 | 126 |
def transform_quotas(quotas): |
107 | 127 |
d = {} |
108 | 128 |
for resource, counters in quotas.iteritems(): |
Also available in: Unified diff