diff --git a/snf-astakos-app/astakos/im/functions.py b/snf-astakos-app/astakos/im/functions.py index ae78731bb73b2813795a6f4b3303adf5aa727f63..669e72e2105cf5d365156e3f9494aabe3d183f60 100644 --- a/snf-astakos-app/astakos/im/functions.py +++ b/snf-astakos-app/astakos/im/functions.py @@ -721,6 +721,76 @@ def enable_base_project(user): quotas.qh_sync_project(project) +MODIFY_KEYS_MAIN = ["owner", "realname", "homepage", "description"] +MODIFY_KEYS_EXTRA = ["end_date", "member_join_policy", "member_leave_policy", + "limit_on_members_number", "private"] +MODIFY_KEYS = MODIFY_KEYS_MAIN + MODIFY_KEYS_EXTRA + + +def modifies_main_fields(request): + return set(request.keys()).intersection(MODIFY_KEYS_MAIN) + + +def modify_project(project_id, request): + project = get_project_for_update(project_id) + if project.state not in Project.INITIALIZED_STATES: + m = _(astakos_messages.UNINITIALIZED_NO_MODIFY) % project.uuid + raise ProjectConflict(m) + + if project.is_base: + main_fields = modifies_main_fields(request) + if main_fields: + m = (_(astakos_messages.BASE_NO_MODIFY_FIELDS) + % ", ".join(map(str, main_fields))) + raise ProjectBadRequest(m) + + new_name = request.get("realname") + if new_name is not None and project.is_alive: + check_conflicting_projects(project, new_name) + project.realname = new_name + project.name = new_name + project.save() + + _modify_projects(Project.objects.filter(id=project.id), request) + + +def modify_projects_in_bulk(flt, request): + main_fields = modifies_main_fields(request) + if main_fields: + raise ProjectBadRequest("Cannot modify field(s) '%s' in bulk" % + ", ".join(map(str, main_fields))) + + projects = Project.objects.initialized(flt).select_for_update() + _modify_projects(projects, request) + + +def _modify_projects(projects, request): + upds = {} + for key in MODIFY_KEYS: + value = request.get(key) + if value is not None: + upds[key] = value + projects.update(**upds) + + changed_resources = set() + pquotas = [] + req_policies = request.get("resources", {}) + req_policies = validate_resource_policies(req_policies, admin=True) + for project in projects: + for resource, m_capacity, p_capacity in req_policies: + changed_resources.add(resource) + pquotas.append( + ProjectResourceQuota( + project=project, + resource=resource, + member_capacity=m_capacity, + project_capacity=p_capacity)) + ProjectResourceQuota.objects.\ + filter(project__in=projects, resource__in=changed_resources).delete() + ProjectResourceQuota.objects.bulk_create(pquotas) + quotas.qh_sync_projects(projects) + + def submit_application(owner=None, name=None, project_id=None, @@ -798,13 +868,15 @@ def submit_application(owner=None, return application -def validate_resource_policies(policies): +def validate_resource_policies(policies, admin=False): if not isinstance(policies, dict): raise ProjectBadRequest("Malformed resource policies") resource_names = policies.keys() - resources = Resource.objects.filter(name__in=resource_names, - api_visible=True) + resources = Resource.objects.filter(name__in=resource_names) + if not admin: + resources = resources.filter(api_visible=True) + resource_d = {} for resource in resources: resource_d[resource.name] = resource diff --git a/snf-astakos-app/astakos/im/management/commands/project-modify.py b/snf-astakos-app/astakos/im/management/commands/project-modify.py new file mode 100644 index 0000000000000000000000000000000000000000..e636aff390527838f84350941ee443799f33fcaf --- /dev/null +++ b/snf-astakos-app/astakos/im/management/commands/project-modify.py @@ -0,0 +1,169 @@ +# 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 optparse import make_option + +from django.db.models import Q +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from synnefo.util import units +from astakos.im import functions +from astakos.im import models +import astakos.api.projects as api +import synnefo.util.date as date_util +from snf_django.management import utils +from astakos.im.management.commands import _common + + +def make_policies(limits): + policies = {} + for (name, member_capacity, project_capacity) in limits: + try: + member_capacity = units.parse(member_capacity) + project_capacity = units.parse(project_capacity) + except units.ParseError: + m = "Please specify capacity as a decimal integer" + raise CommandError(m) + policies[name] = {"member_capacity": member_capacity, + "project_capacity": project_capacity} + return policies + +Simple = type('Simple', (), {}) + + +class Param(object): + def __init__(self, key=Simple, mod=Simple, action=Simple, nargs=Simple, + is_main=False, help=""): + self.key = key + self.mod = mod + self.action = action + self.nargs = nargs + self.is_main = is_main + self.help = help + + +PARAMS = { + "name": Param(key="realname", help="Set project name"), + "owner": Param(mod=_common.get_accepted_user, help="Set project owner"), + "homepage": Param(help="Set project homepage"), + "description": Param(help="Set project description"), + "end_date": Param(mod=date_util.isoparse, is_main=True, + help=("Set project end date in ISO format " + "(e.g. 2014-01-01T00:00Z)")), + "join_policy": Param(key="member_join_policy", is_main=True, + mod=(lambda x: api.MEMBERSHIP_POLICY[x]), + help="Set join policy (auto, moderated, or closed)"), + "leave_policy": Param(key="member_leave_policy", is_main=True, + mod=(lambda x: api.MEMBERSHIP_POLICY[x]), + help=("Set leave policy " + "(auto, moderated, or closed)")), + "max_members": Param(key="limit_on_members_number", mod=int, is_main=True, + help="Set maximum members limit"), + "private": Param(mod=utils.parse_bool, is_main=True, + help="Set project private"), + "limit": Param(key="resources", mod=make_policies, is_main=True, + nargs=3, action="append", + help=("Set resource limits: " + "resource_name member_capacity project_capacity")), +} + + +def make_options(): + options = [] + for key, param in PARAMS.iteritems(): + opt = "--" + key.replace('_', '-') + kwargs = {} + if param.action is not Simple: + kwargs["action"] = param.action + if param.nargs is not Simple: + kwargs["nargs"] = param.nargs + kwargs["help"] = param.help + options.append(make_option(opt, **kwargs)) + return tuple(options) + + +class Command(BaseCommand): + args = "<project id> (or --all-base-projects)" + help = "Modify an already initialized project" + option_list = BaseCommand.option_list + make_options() + ( + make_option('--all-base-projects', + action='store_true', + default=False, + help="Modify in bulk all initialized base projects"), + make_option('--exclude', + help=("If `--all-base-projects' is given, exclude projects" + " given as a list of uuids: uuid1,uuid2,uuid3")), + ) + + def check_args(self, args, all_base, exclude): + if all_base and args or not all_base and len(args) != 1: + m = "Please provide a project ID or --all-base-projects" + raise CommandError(m) + if not all_base and exclude: + m = ("Option --exclude is meaningful only combined with " + " --all-base-projects.") + raise CommandError(m) + + def mk_all_base_filter(self, all_base, exclude): + flt = Q(state__in=models.Project.INITIALIZED_STATES, is_base=True) + if exclude: + exclude = exclude.split(',') + flt &= ~Q(uuid__in=exclude) + return flt + + @transaction.commit_on_success + def handle(self, *args, **options): + all_base = options["all_base_projects"] + exclude = options["exclude"] + self.check_args(args, all_base, exclude) + + try: + changes = {} + for key, value in options.iteritems(): + param = PARAMS.get(key) + if param is None or value is None: + continue + if all_base and not param.is_main: + m = "Cannot modify field '%s' in bulk" % key + raise CommandError(m) + k = key if param.key is Simple else param.key + v = value if param.mod is Simple else param.mod(value) + changes[k] = v + + if all_base: + flt = self.mk_all_base_filter(all_base, exclude) + functions.modify_projects_in_bulk(flt, changes) + else: + functions.modify_project(args[0], changes) + except BaseException as e: + raise CommandError(e) diff --git a/snf-astakos-app/astakos/im/messages.py b/snf-astakos-app/astakos/im/messages.py index a165cf060568fc4523b156586a227404e084f1c8..6df798bb1a42bd25e8b80aed91c449877e875f98 100644 --- a/snf-astakos-app/astakos/im/messages.py +++ b/snf-astakos-app/astakos/im/messages.py @@ -273,6 +273,8 @@ APPLICATION_CANNOT_CANCEL = "Cannot cancel application %s in state '%s'" APPLICATION_CANCELLED = "Your project application has been cancelled." REACHED_PENDING_APPLICATION_LIMIT = ("You have reached the maximum number " "of pending project applications: %s.") +UNINITIALIZED_NO_MODIFY = "Cannot modify: project %s is not initialized." +BASE_NO_MODIFY_FIELDS = "Cannot modify field(s) '%s' of base projects." PENDING_APPLICATION_LIMIT_ADD = \ ("You are not allowed to create a new project " diff --git a/snf-deploy/snfdeploy/components.py b/snf-deploy/snfdeploy/components.py index d448b809274ced01cfaa7858d2f2eca25022f8c2..381394e5ac1a853c0757794b1d01316bc0837e3c 100644 --- a/snf-deploy/snfdeploy/components.py +++ b/snf-deploy/snfdeploy/components.py @@ -658,18 +658,18 @@ class Astakos(SynnefoComponent): ] def modify_all_quota(self): - cmd = "snf-manage user-modify -f --all --base-quota" - return [ - "%s pithos.diskspace 40G" % cmd, - "%s astakos.pending_app 2" % cmd, - "%s cyclades.vm 4" % cmd, - "%s cyclades.disk 40G" % cmd, - "%s cyclades.total_ram 16G" % cmd, - "%s cyclades.ram 8G" % cmd, - "%s cyclades.total_cpu 32" % cmd, - "%s cyclades.cpu 16" % cmd, - "%s cyclades.network.private 4" % cmd, - "%s cyclades.floating_ip 4" % cmd, + cmd = "snf-manage project-modify --all-base-projects --limit" + return [ + "%s pithos.diskspace 40G 40G" % cmd, + "%s astakos.pending_app 2 2" % cmd, + "%s cyclades.vm 4 4" % cmd, + "%s cyclades.disk 40G 40G" % cmd, + "%s cyclades.total_ram 16G 16G" % cmd, + "%s cyclades.ram 8G 8G" % cmd, + "%s cyclades.total_cpu 32 32" % cmd, + "%s cyclades.cpu 16 16" % cmd, + "%s cyclades.network.private 4 4" % cmd, + "%s cyclades.floating_ip 4 4" % cmd, ] def get_services(self):