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