Revision 764d99c4

b/docs/admin-guide.rst
160 160
you can set it for each resource like this::
161 161

  
162 162
    # use this to display quotas / uuid
163
    # snf-manage user-show 'uuid or email'
163
    # snf-manage user-show 'uuid or email' --quotas
164 164

  
165
    # snf-manage user-set-initial-quota --set-capacity 'user-uuid' 'cyclades.vm' 10
166

  
167
    # this applies the configuration
168
    # snf-manage astakos-quota --sync --user 'user-uuid'
165
    # snf-manage user-modify 'user-uuid' --set-quota 'cyclades.vm' 10
169 166

  
170 167

  
171 168
Enable the Projects feature
......
185 182

  
186 183
You can also set a user-specific limit with::
187 184

  
188
    # snf-manage user-set-initial-quota --set-capacity 'user-uuid' 'astakos.pending_app' 5
185
    # snf-manage user-modify 'user-uuid' --set-quota 'astakos.pending_app' 5
189 186

  
190 187
When users apply for projects they are not automatically granted
191 188
the resources. They must first be approved by the administrator.
b/snf-astakos-app/README
197 197
user-invite                   Invite somebody
198 198
user-list                     List users
199 199
user-modify                   Modify user
200
user-set-initial-quota        Import user quota limits from file or set quota for a single user from the command line
201 200
user-show                     Show user details
202 201
============================  ===========================
b/snf-astakos-app/astakos/im/management/commands/_common.py
237 237

  
238 238

  
239 239
def show_quotas(qh_quotas, astakos_initial, info=None):
240
    labels = ('source', 'resource', 'initial', 'total', 'usage')
240
    labels = ('source', 'resource', 'base quota', 'total quota', 'usage')
241 241
    if info is not None:
242 242
        labels = ('uuid', 'email') + labels
243 243

  
b/snf-astakos-app/astakos/im/management/commands/astakos-quota.py
35 35
from django.core.management.base import CommandError
36 36

  
37 37
from astakos.im.models import AstakosUser
38
from astakos.im.quotas import set_user_quota, list_user_quotas
38
from astakos.im.quotas import set_user_quota, list_user_quotas, add_base_quota
39 39
from astakos.im.functions import get_user_by_uuid
40 40
from astakos.im.management.commands._common import is_uuid, is_email
41 41
from snf_django.lib.db.transaction import commit_on_success_strict
......
70 70
                    metavar='<uuid or email>',
71 71
                    dest='user',
72 72
                    help="List quotas for a specified user"),
73
        make_option('--import-base-quota',
74
                    dest='import_base_quota',
75
                    metavar='<exported-quotas.txt>',
76
                    help=("Import base quotas from file. "
77
                          "The file must contain non-empty lines, and each "
78
                          "line must contain a single-space-separated list "
79
                          "of values: <user> <resource name> <capacity>")
80
                    ),
73 81
    )
74 82

  
75 83
    @commit_on_success_strict()
......
77 85
        sync = options['sync']
78 86
        verify = options['verify']
79 87
        user_ident = options['user']
88
        list_ = options['list']
89
        import_base_quota = options['import_base_quota']
90

  
91
        if import_base_quota:
92
            if any([sync, verify, list_]):
93
                m = "--from-file cannot be combined with other options."
94
                raise CommandError(m)
95
            self.import_from_file(import_base_quota)
96
        else:
97
            self.quotas(sync, verify, user_ident, options["output_format"])
98

  
99
    def quotas(self, sync, verify, user_ident, output_format):
80 100
        list_only = not sync and not verify
81 101

  
82 102
        if user_ident is not None:
......
97 117
        if list_only:
98 118
            print_data, labels = show_quotas(qh_quotas, astakos_i, info)
99 119
            utils.pprint_table(self.stdout, print_data, labels,
100
                               options["output_format"])
120
                               output_format)
101 121

  
102 122
        else:
103 123
            if verify:
......
163 183
            diffs = len(diff_quotas)
164 184
            if diffs:
165 185
                self.stdout.write("Quotas differ for %d users.\n" % (diffs))
186

  
187
    def import_from_file(self, location):
188
        users = set()
189
        with open(location) as f:
190
            for line in f.readlines():
191
                try:
192
                    t = line.rstrip('\n').split(' ')
193
                    user = t[0]
194
                    resource = t[1]
195
                    capacity = t[2]
196
                except(IndexError, TypeError):
197
                    self.stdout.write('Invalid line format: %s:\n' % t)
198
                    continue
199
                else:
200
                    try:
201
                        user = self.get_user(user)
202
                        users.add(user.id)
203
                    except CommandError:
204
                        self.stdout.write('Not found user: %s\n' % user)
205
                        continue
206
                    else:
207
                        try:
208
                            add_base_quota(user, resource, capacity)
209
                        except Exception, e:
210
                            self.stdout.write('Failed to add quota: %s\n' % e)
211
                            continue
b/snf-astakos-app/astakos/im/management/commands/user-modify.py
39 39

  
40 40
from astakos.im.models import AstakosUser
41 41
from astakos.im.functions import (activate, deactivate)
42
from astakos.im import quotas
42 43
from ._common import remove_user_permission, add_user_permission, is_uuid
43 44
from snf_django.lib.db.transaction import commit_on_success_strict
45
import string
44 46

  
45 47

  
46 48
class Command(BaseCommand):
......
102 104
        make_option('--delete-permission',
103 105
                    dest='delete-permission',
104 106
                    help="Delete user permission"),
107
        make_option('--set-base-quota',
108
                    dest='set_base_quota',
109
                    metavar='<resource> <capacity>',
110
                    nargs=2,
111
                    help=("Set base quota for a specified resource. "
112
                          "The special value 'default' sets the user base "
113
                          "quota to the default value.")
114
                    ),
115

  
105 116
    )
106 117

  
107 118
    @commit_on_success_strict()
......
210 221

  
211 222
        if password:
212 223
            self.stdout.write('User\'s new password: %s\n' % password)
224

  
225
        set_base_quota = options.get('set_base_quota')
226
        if set_base_quota is not None:
227
            resource, capacity = set_base_quota
228
            self.set_limit(user, resource, capacity, False)
229

  
230
    def set_limit(self, user, resource, capacity, force):
231
        if capacity != 'default':
232
            try:
233
                capacity = int(capacity)
234
            except ValueError:
235
                m = "Please specify capacity as a decimal integer or 'default'"
236
                raise CommandError(m)
237

  
238
        try:
239
            quota, default_capacity = user.get_resource_policy(resource)
240
        except Resource.DoesNotExist:
241
            raise CommandError("No such resource: %s" % resource)
242

  
243
        current = quota.capacity if quota is not None else 'default'
244

  
245
        if not force:
246
            self.stdout.write("user: %s (%s)\n" % (user.uuid, user.username))
247
            self.stdout.write("default capacity: %s\n" % default_capacity)
248
            self.stdout.write("current capacity: %s\n" % current)
249
            self.stdout.write("new capacity: %s\n" % capacity)
250
            self.stdout.write("Confirm? (y/n) ")
251
            response = raw_input()
252
            if string.lower(response) not in ['y', 'yes']:
253
                self.stdout.write("Aborted.\n")
254
                return
255

  
256
        if capacity == 'default':
257
            try:
258
                quotas.remove_base_quota(user, resource)
259
            except Exception as e:
260
                import traceback
261
                traceback.print_exc()
262
                raise CommandError("Failed to remove policy: %s" % e)
263
        else:
264
            try:
265
                quotas.add_base_quota(user, resource, capacity)
266
            except Exception as e:
267
                raise CommandError("Failed to add policy: %s" % e)
/dev/null
1
# Copyright 2012, 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

  
36
from optparse import make_option
37
from collections import namedtuple
38

  
39
from django.core.management.base import BaseCommand, CommandError
40

  
41
from snf_django.lib.db.transaction import commit_on_success_strict
42
from astakos.im.models import AstakosUser, AstakosUserQuota, Resource
43
from astakos.im.quotas import qh_sync_user, qh_sync_users
44

  
45
from ._common import is_uuid, is_email
46

  
47
AddResourceArgs = namedtuple('AddQuotaArgs', ('resource',
48
                                              'capacity',
49
                                              ))
50

  
51

  
52

  
53
class Command(BaseCommand):
54
    help = """Import user quota limits from file or set quota
55
for a single user from the command line
56

  
57
    The file must contain non-empty lines, and each line must
58
    contain a single-space-separated list of values:
59

  
60
    <user> <resource name> <capacity>
61

  
62
    For example to grant the following user with 10 private networks
63
    (independent of any he receives from projects):
64

  
65
    6119a50b-cbc7-42c0-bafc-4b6570e3f6ac cyclades.network.private 10
66

  
67
    Similar syntax is used when setting quota from the command line:
68

  
69
    --set-capacity 6119a50b-cbc7-42c0-bafc-4b6570e3f6ac cyclades.vm 10
70

  
71
    The special value of 'default' sets the user setting to the default.
72
    """
73

  
74
    option_list = BaseCommand.option_list + (
75
        make_option('--from-file',
76
                    dest='from_file',
77
                    metavar='<exported-quotas.txt>',
78
                    help="Import quotas from file"),
79
        make_option('--set-capacity',
80
                    dest='set_capacity',
81
                    metavar='<uuid or email> <resource> <capacity>',
82
                    nargs=3,
83
                    help="Set capacity for a specified user/resource pair"),
84

  
85
        make_option('-f', '--no-confirm',
86
                    action='store_true',
87
                    default=False,
88
                    dest='force',
89
                    help="Do not ask for confirmation"),
90
    )
91

  
92
    @commit_on_success_strict()
93
    def handle(self, *args, **options):
94
        from_file = options['from_file']
95
        set_capacity = options['set_capacity']
96
        force = options['force']
97

  
98
        if from_file is not None:
99
            if set_capacity is not None:
100
                raise CommandError("Cannot combine option `--from-file' with "
101
                                   "`--set-capacity'.")
102
            self.import_from_file(from_file)
103
            return
104

  
105
        if set_capacity is not None:
106
            user, resource, capacity = set_capacity
107
            self.set_limit(user, resource, capacity, force)
108
            return
109

  
110
        m = "Please use either `--from-file' or `--set-capacity' options"
111
        raise CommandError(m)
112

  
113
    def set_limit(self, user_ident, resource, capacity, force):
114
        if is_uuid(user_ident):
115
            try:
116
                user = AstakosUser.objects.get(uuid=user_ident)
117
            except AstakosUser.DoesNotExist:
118
                raise CommandError('Not found user having uuid: %s' %
119
                                   user_ident)
120
        elif is_email(user_ident):
121
            try:
122
                user = AstakosUser.objects.get(username=user_ident)
123
            except AstakosUser.DoesNotExist:
124
                raise CommandError('Not found user having email: %s' %
125
                                   user_ident)
126
        else:
127
            raise CommandError('Please specify user by uuid or email')
128

  
129
        if capacity != 'default':
130
            try:
131
                capacity = int(capacity)
132
            except ValueError:
133
                m = "Please specify capacity as a decimal integer or 'default'"
134
                raise CommandError(m)
135

  
136
        args = AddResourceArgs(resource=resource,
137
                               capacity=capacity,
138
                               )
139

  
140
        try:
141
            quota, default_capacity = user.get_resource_policy(resource)
142
        except Resource.DoesNotExist:
143
            raise CommandError("No such resource: %s" % resource)
144

  
145
        current = quota.capacity if quota is not None else 'default'
146

  
147
        if not force:
148
            self.stdout.write("user: %s (%s)\n" % (user.uuid, user.username))
149
            self.stdout.write("default capacity: %s\n" % default_capacity)
150
            self.stdout.write("current capacity: %s\n" % current)
151
            self.stdout.write("new capacity: %s\n" % capacity)
152
            self.stdout.write("Confirm? (y/n) ")
153
            response = raw_input()
154
            if string.lower(response) not in ['y', 'yes']:
155
                self.stdout.write("Aborted.\n")
156
                return
157

  
158
        if capacity == 'default':
159
            try:
160
                q = AstakosUserQuota.objects.get(user=user,
161
                                                 resource__name=resource)
162
                q.delete()
163
            except Exception as e:
164
                import traceback
165
                traceback.print_exc()
166
                raise CommandError("Failed to remove policy: %s" % e)
167
        else:
168
            try:
169
                user.add_resource_policy(*args)
170
            except Exception as e:
171
                raise CommandError("Failed to add policy: %s" % e)
172
        qh_sync_user(user.id)
173

  
174
    def import_from_file(self, location):
175
        users = set()
176
        with open(location) as f:
177
            for line in f.readlines():
178
                try:
179
                    t = line.rstrip('\n').split(' ')
180
                    user = t[0]
181
                    args = AddResourceArgs(*t[1:])
182
                except(IndexError, TypeError):
183
                    self.stdout.write('Invalid line format: %s:\n' % t)
184
                    continue
185
                else:
186
                    try:
187
                        user = AstakosUser.objects.get(uuid=user)
188
                        users.add(user.id)
189
                    except AstakosUser.DoesNotExist:
190
                        self.stdout.write('Not found user having uuid: %s\n'
191
                                          % user)
192
                        continue
193
                    else:
194
                        try:
195
                            user.add_resource_policy(*args)
196
                        except Exception, e:
197
                            self.stdout.write('Failed to policy: %s\n' % e)
198
                            continue
199
        qh_sync_users(users)
b/snf-astakos-app/astakos/im/models.py
446 446
    def policies(self):
447 447
        return self.astakosuserquota_set.select_related().all()
448 448

  
449
    @policies.setter
450
    def policies(self, policies):
451
        for p in policies:
452
            p.setdefault('resource', '')
453
            p.setdefault('capacity', 0)
454
            p.setdefault('update', True)
455
            self.add_resource_policy(**p)
456

  
457
    def add_resource_policy(
458
            self, resource, capacity,
459
            update=True):
460
        """Raises ObjectDoesNotExist, IntegrityError"""
461
        resource = Resource.objects.get(name=resource)
462
        if update:
463
            AstakosUserQuota.objects.update_or_create(
464
                user=self, resource=resource, defaults={
465
                    'capacity':capacity,
466
                    })
467
        else:
468
            q = self.astakosuserquota_set
469
            q.create(
470
                resource=resource, capacity=capacity,
471
                )
472

  
473 449
    def get_resource_policy(self, resource):
474 450
        resource = Resource.objects.get(name=resource)
475 451
        default_capacity = resource.uplimit
......
479 455
        except AstakosUserQuota.DoesNotExist:
480 456
            return None, default_capacity
481 457

  
482
    def remove_resource_policy(self, service, resource):
483
        """Raises ObjectDoesNotExist, IntegrityError"""
484
        resource = Resource.objects.get(name=resource)
485
        q = self.policies.get(resource=resource).delete()
486

  
487 458
    def update_uuid(self):
488 459
        while not self.uuid:
489 460
            uuid_val =  str(uuid.uuid4())
b/snf-astakos-app/astakos/im/quotas.py
152 152
    return True, None
153 153

  
154 154

  
155
def add_base_quota(user, resource, capacity):
156
    resource = Resource.objects.get(name=resource)
157
    obj, created = AstakosUserQuota.objects.get_or_create(
158
        user=user, resource=resource, defaults={
159
            'capacity': capacity,
160
        })
161

  
162
    if not created:
163
        obj.capacity = capacity
164
        obj.save()
165
    qh_sync_user(user.id)
166

  
167

  
168
def remove_base_quota(user, resource):
169
    AstakosUserQuota.objects.filter(
170
        user=user, resource__name=resource).delete()
171
    qh_sync_user(user.id)
172

  
173

  
155 174
def initial_quotas(users):
156 175
    initial = {}
157 176
    default_quotas = get_default_quota()
b/snf-astakos-app/astakos/im/tests.py
1274 1274
    @im_settings(PROJECT_ADMINS=['uuid1'])
1275 1275
    def test_applications(self):
1276 1276
        # let user have 2 pending applications
1277
        self.user.add_resource_policy('astakos.pending_app', 2)
1278
        quotas.qh_sync_users(AstakosUser.objects.all())
1277
        quotas.add_base_quota(self.user, 'astakos.pending_app', 2)
1279 1278

  
1280 1279
        r = self.user_client.get(reverse('project_add'), follow=True)
1281 1280
        self.assertRedirects(r, reverse('project_add'))

Also available in: Unified diff