Commit 579cff8b authored by Giorgos Korfiatis's avatar Giorgos Korfiatis
Browse files

astakos: Support units in resource-related commands

Add library synnefo.util.units for parsing numerical values
with an optional unit suffix and printing integer values based
on a given style.

Styles `b', `kb', `mb', etc allow printing in the respective multiples
of byte, style `auto' in a human-readable manner, while `none' omits
units altogether.

Add option `--unit-style' in all astakos management commands that print
resource limits and quota usage. Consult the resource definitions to find
out which unit, if any, to show. For resources with unit `bytes', all
commands default to MB.
parent 0e7b5d54
......@@ -41,8 +41,10 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError
from django.core.management import CommandError
from synnefo.util import units
from synnefo.lib.ordereddict import OrderedDict
from astakos.im.models import AstakosUser
from astakos.im.resources import get_resources
DEFAULT_CONTENT_TYPE = None
......@@ -236,7 +238,32 @@ def is_email(s):
return True
def show_quotas(qh_quotas, astakos_initial, info=None):
style_options = ', '.join(units.STYLES)
def check_style(style):
if style not in units.STYLES:
m = "Invalid unit style. Valid ones are %s." % style_options
raise CommandError(m)
class ResourceDict(object):
_object = None
@classmethod
def get(cls):
if cls._object is None:
cls._object = get_resources()
return cls._object
def show_resource_value(number, resource, style):
resource_dict = ResourceDict.get()
unit = resource_dict[resource]['unit']
return units.show(number, unit, style)
def show_quotas(qh_quotas, astakos_initial, info=None, style=None):
labels = ('source', 'resource', 'base quota', 'total quota', 'usage')
if info is not None:
labels = ('uuid', 'email') + labels
......@@ -251,8 +278,10 @@ def show_quotas(qh_quotas, astakos_initial, info=None):
s_initial = h_initial.get(source) if h_initial else None
for resource, values in source_quotas.iteritems():
initial = s_initial.get(resource) if s_initial else None
fields = (source, resource, initial,
values['limit'], values['usage'])
initial = show_resource_value(initial, resource, style)
limit = show_resource_value(values['limit'], resource, style)
usage = show_resource_value(values['usage'], resource, style)
fields = (source, resource, initial, limit, usage)
if info is not None:
fields = (holder, email) + fields
......
......@@ -38,6 +38,7 @@ from synnefo.lib.ordereddict import OrderedDict
from synnefo.webproject.management.commands import SynnefoCommand
from synnefo.webproject.management import utils
from astakos.im.models import Chain, ProjectApplication
from ._common import show_resource_value, style_options, check_style
class Command(SynnefoCommand):
......@@ -64,12 +65,19 @@ class Command(SynnefoCommand):
default=False,
help=("Show a list of project memberships")
),
make_option('--unit-style',
default='mb',
help=("Specify display unit for resource values "
"(one of %s); defaults to mb") % style_options),
)
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError("Please provide project ID or name")
self.unit_style = options['unit_style']
check_style(self.unit_style)
show_pending = bool(options['pending'])
show_members = bool(options['members'])
search_apps = options['app']
......@@ -82,31 +90,49 @@ class Command(SynnefoCommand):
raise CommandError("id should be an integer value.")
if search_apps:
self.pprint_dict(app_info(id_))
app = get_app(id_)
self.print_app(app)
else:
state, project, app = get_chain_state(id_)
self.pprint_dict(chain_fields(state, project, app))
self.print_project(state, project, app)
if show_members and project is not None:
self.stdout.write("\n")
fields, labels = members_fields(project)
self.pprint_table(fields, labels)
self.pprint_table(fields, labels, title="Members")
if show_pending and state in Chain.PENDING_STATES:
self.stdout.write("\n")
self.pprint_dict(app_fields(app))
self.print_app(app)
def pprint_dict(self, d, vertical=True):
utils.pprint_table(self.stdout, [d.values()], d.keys(),
self.output_format, vertical=vertical)
def pprint_table(self, tbl, labels):
def pprint_table(self, tbl, labels, title=None):
utils.pprint_table(self.stdout, tbl, labels,
self.output_format)
self.output_format, title=title)
def print_app(self, app):
app_info = app_fields(app)
self.pprint_dict(app_info)
self.print_resources(app)
def print_project(self, state, project, app):
if project is None:
self.print_app(app)
else:
self.pprint_dict(project_fields(state, project, app))
self.print_resources(project.application)
def print_resources(self, app):
fields, labels = resource_fields(app, self.unit_style)
if fields:
self.stdout.write("\n")
self.pprint_table(fields, labels, title="Resource limits")
def app_info(app_id):
def get_app(app_id):
try:
app = ProjectApplication.objects.get(id=app_id)
return app_fields(app)
return ProjectApplication.objects.get(id=app_id)
except ProjectApplication.DoesNotExist:
raise CommandError("Application with id %s not found." % app_id)
......@@ -126,6 +152,19 @@ def chain_fields(state, project, app):
return app_fields(app)
def resource_fields(app, style):
labels = ('name', 'description', 'max per member')
policies = app.projectresourcegrant_set.all()
collect = []
for policy in policies:
name = policy.resource.name
desc = policy.resource.desc
capacity = policy.member_capacity
collect.append((name, desc,
show_resource_value(capacity, name, style)))
return collect, labels
def app_fields(app):
mem_limit = app.limit_on_members_number
mem_limit_show = mem_limit if mem_limit is not None else "unlimited"
......@@ -143,7 +182,6 @@ def app_fields(app):
('request issue date', app.issue_date),
('request start date', app.start_date),
('request end date', app.end_date),
('resources', app.resource_policies),
('join policy', app.member_join_policy_display),
('leave policy', app.member_leave_policy_display),
('max members', mem_limit_show),
......@@ -183,7 +221,6 @@ def project_fields(s, project, last_app):
mem_limit_show = mem_limit if mem_limit is not None else "unlimited"
d.update([
('resources', app.resource_policies),
('join policy', app.member_join_policy_display),
('leave policy', app.member_leave_policy_display),
('max members', mem_limit_show),
......
......@@ -42,7 +42,7 @@ from astakos.im.management.commands._common import is_uuid, is_email
from snf_django.lib.db.transaction import commit_on_success_strict
from synnefo.webproject.management.commands import SynnefoCommand
from synnefo.webproject.management import utils
from ._common import show_quotas
from ._common import show_quotas, style_options, check_style, units
import logging
logger = logging.getLogger(__name__)
......@@ -57,6 +57,10 @@ class Command(SynnefoCommand):
dest='list',
default=False,
help="List all quota (default)"),
make_option('--unit-style',
default='mb',
help=("Specify display unit for resource values "
"(one of %s); defaults to mb") % style_options),
make_option('--verify',
action='store_true',
dest='verify',
......@@ -77,7 +81,9 @@ class Command(SynnefoCommand):
help=("Import base quota from file. "
"The file must contain non-empty lines, and each "
"line must contain a single-space-separated list "
"of values: <user> <resource name> <capacity>")
"of values: <user> <resource name> <capacity>. "
"Capacity can be followed by a unit with no "
"separating space (e.g 10GB).")
),
)
......@@ -95,9 +101,13 @@ class Command(SynnefoCommand):
raise CommandError(m)
self.import_from_file(import_base_quota)
else:
self.quotas(sync, verify, user_ident, options["output_format"])
unit_style = options["unit_style"]
check_style(unit_style)
def quotas(self, sync, verify, user_ident, output_format):
self.quotas(sync, verify, user_ident, options["output_format"],
unit_style)
def quotas(self, sync, verify, user_ident, output_format, style):
list_only = not sync and not verify
if user_ident is not None:
......@@ -112,7 +122,8 @@ class Command(SynnefoCommand):
for user in users:
info[user.uuid] = user.email
print_data, labels = show_quotas(qh_quotas, astakos_i, info)
print_data, labels = show_quotas(qh_quotas, astakos_i, info,
style=style)
utils.pprint_table(self.stdout, print_data, labels,
output_format)
......@@ -186,6 +197,12 @@ class Command(SynnefoCommand):
user = t[0]
resource = t[1]
capacity = t[2]
try:
capacity = units.parse(capacity)
except units.ParseError:
m = ("Capacity should be an integer, optionally "
"followed by a unit.")
raise CommandError(m)
except(IndexError, TypeError):
self.stdout.write('Invalid line format: %s:\n' % t)
continue
......
......@@ -31,24 +31,44 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from optparse import make_option
from astakos.im.models import Resource
from synnefo.webproject.management.commands import ListCommand
from ._common import show_resource_value, style_options, check_style
class Command(ListCommand):
help = "List resources"
object_class = Resource
option_list = ListCommand.option_list + (
make_option('--unit-style',
default='mb',
help=("Specify display unit for resource values "
"(one of %s); defaults to mb") % style_options),
)
FIELDS = {
"id": ("id", "ID"),
"name": ("name", "Resource Name"),
"service": ("service", "Service"),
"unit": ("unit", "Unit"),
"limit": ("uplimit", "Base Quota"),
"limit": ("limit_with_unit", "Base Quota"),
"description": ("desc", "Description"),
"allow_in_projects": ("allow_in_projects",
"Make resource available in projects"),
}
fields = ["id", "name", "service", "unit", "limit", "allow_in_projects",
fields = ["id", "name", "service", "limit", "allow_in_projects",
"description"]
def show_limit(self, resource):
limit = resource.uplimit
return show_resource_value(limit, resource.name, self.unit_style)
def handle_args(self, *args, **options):
self.unit_style = options['unit_style']
check_style(self.unit_style)
def handle_db_objects(self, rows, *args, **kwargs):
for resource in rows:
resource.limit_with_unit = self.show_limit(resource)
......@@ -37,6 +37,7 @@ from django.utils import simplejson as json
from astakos.im.models import Resource
from astakos.im.resources import update_resource
from ._common import show_resource_value, style_options, check_style, units
class Command(BaseCommand):
......@@ -57,6 +58,10 @@ class Command(BaseCommand):
dest='from_file',
metavar='<limits_file.json>',
help="Read default base quotas from a json file"),
make_option('--unit-style',
default='mb',
help=("Specify display unit for resource values "
"(one of %s); defaults to mb") % style_options),
)
def handle(self, *args, **options):
......@@ -73,7 +78,11 @@ class Command(BaseCommand):
if key in actions and value is not None]
if len(opts) != 1:
raise CommandError("Please provide exactly one option.")
raise CommandError("Please provide exactly one of the options: %s."
% ", ".join(actions.keys()))
self.unit_style = options['unit_style']
check_style(self.unit_style)
key, value = opts[0]
action = actions[key]
......@@ -123,26 +132,28 @@ class Command(BaseCommand):
for resource in resources:
self.stdout.write("Resource '%s' (%s)\n" %
(resource.name, resource.desc))
unit = (" in %s" % resource.unit) if resource.unit else ""
self.stdout.write("Current limit%s: %s\n"
% (unit, resource.uplimit))
value = show_resource_value(resource.uplimit, resource.name,
self.unit_style)
self.stdout.write("Current limit: %s\n" % value)
while True:
self.stdout.write("New limit%s (leave blank to keep current): "
% (unit))
self.stdout.write("New limit (leave blank to keep current): ")
response = raw_input()
if response == "":
break
else:
try:
value = int(response)
except ValueError:
value = units.parse(response)
except units.ParseError:
continue
update_resource(resource, value)
break
def change_resource_limit(self, resource, limit):
try:
limit = int(limit)
except:
raise CommandError("Limit should be an integer.")
update_resource(resource, limit)
if not isinstance(limit, (int, long)):
try:
limit = units.parse(limit)
except units.ParseError:
m = ("Limit should be an integer, optionally followed "
"by a unit.")
raise CommandError(m)
update_resource(resource, limit)
......@@ -39,10 +39,12 @@ from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group
from django.core.exceptions import ValidationError
from synnefo.util import units
from astakos.im.models import AstakosUser, Resource
from astakos.im import quotas
from astakos.im import activation_backends
from ._common import remove_user_permission, add_user_permission, is_uuid
from ._common import (remove_user_permission, add_user_permission, is_uuid,
show_resource_value)
from snf_django.lib.db.transaction import commit_on_success_strict
activation_backend = activation_backends.get_backend()
......@@ -290,10 +292,11 @@ class Command(BaseCommand):
self.set_limit(user, resource, capacity, False)
def set_limit(self, user, resource, capacity, force):
style = None
if capacity != 'default':
try:
capacity = int(capacity)
except ValueError:
capacity, style = units.parse_with_style(capacity)
except:
m = "Please specify capacity as a decimal integer or 'default'"
raise CommandError(m)
......@@ -302,13 +305,16 @@ class Command(BaseCommand):
except Resource.DoesNotExist:
raise CommandError("No such resource: %s" % resource)
current = quota.capacity if quota is not None else 'default'
if not force:
s_default = show_resource_value(default_capacity, resource, style)
s_current = (show_resource_value(quota.capacity, resource, style)
if quota is not None else 'default')
s_capacity = (show_resource_value(capacity, resource, style)
if capacity != 'default' else capacity)
self.stdout.write("user: %s (%s)\n" % (user.uuid, user.username))
self.stdout.write("default capacity: %s\n" % default_capacity)
self.stdout.write("current capacity: %s\n" % current)
self.stdout.write("new capacity: %s\n" % capacity)
self.stdout.write("default capacity: %s\n" % s_default)
self.stdout.write("current capacity: %s\n" % s_current)
self.stdout.write("new capacity: %s\n" % s_capacity)
self.stdout.write("Confirm? (y/n) ")
response = raw_input()
if string.lower(response) not in ['y', 'yes']:
......
......@@ -41,7 +41,7 @@ from synnefo.lib.ordereddict import OrderedDict
from synnefo.webproject.management.commands import SynnefoCommand
from synnefo.webproject.management import utils
from ._common import format, show_quotas
from ._common import format, show_quotas, style_options, check_style
import uuid
......@@ -56,6 +56,10 @@ class Command(SynnefoCommand):
dest='list_quotas',
default=False,
help="Also list user quota"),
make_option('--unit-style',
default='mb',
help=("Specify display unit for resource values "
"(one of %s); defaults to mb") % style_options),
make_option('--projects',
action='store_true',
dest='list_projects',
......@@ -122,10 +126,14 @@ class Command(SynnefoCommand):
options["output_format"], vertical=True)
if options["list_quotas"]:
unit_style = options["unit_style"]
check_style(unit_style)
quotas, initial = list_user_quotas([user])
if quotas:
self.stdout.write("\n")
print_data, labels = show_quotas(quotas, initial)
print_data, labels = show_quotas(quotas, initial,
style=unit_style)
utils.pprint_table(self.stdout, print_data, labels,
options["output_format"],
title="User Quota")
......
# Copyright 2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from synnefo.lib.ordereddict import OrderedDict
import re
DEFAULT_PARSE_BASE = 1024
PARSE_EXPONENTS = {
'': 0,
'bytes': 0,
'K': 1,
'KB': 1,
'KIB': 1,
'M': 2,
'MB': 2,
'MIB': 2,
'G': 3,
'GB': 3,
'GIB': 3,
'T': 4,
'TB': 4,
'TIB': 4,
'P': 5,
'PB': 5,
'PIB': 5,
}
_MATCHER = re.compile('^(\d+\.?\d*)(.*)$')
class ParseError(Exception):
pass
def _parse_number_with_unit(s):
match = _MATCHER.match(s)
if not match:
raise ParseError()
number, unit = match.groups()
try:
number = long(number)
except ValueError:
number = float(number)
return number, unit.strip().upper()
def parse_with_style(s):
n, unit = _parse_number_with_unit(s)
try:
exponent = PARSE_EXPONENTS[unit]
except KeyError:
raise ParseError()
multiplier = DEFAULT_PARSE_BASE ** exponent
return long(n * multiplier), exponent
def parse(s):
n, _ = parse_with_style(s)
return n
UNITS = {
'bytes': {
'DISPLAY': ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
'BASE': 1024,
}
}
STYLE_TO_EXP = OrderedDict(
[('b', 0),
('kb', 1),
('mb', 2),
('gb', 3),
('tb', 4),
('pb', 5),
]
)
STYLES = STYLE_TO_EXP.keys() + ['auto', 'none']
class StyleError(Exception):
pass
def show_float(n):
if n < 1:
return "%.3f" % n
if n < 10:
return "%.2f" % n
return "%.1f" % n
def get_exponent(style):
if isinstance(style, (int, long)):
if style in STYLE_TO_EXP.values():
return style
else:
raise StyleError()
else:
try:
return STYLE_TO_EXP[style]
except KeyError:
raise StyleError()
def show(n, unit, style=None):