diff --git a/src/actinia_core/common/response_models.py b/src/actinia_core/common/response_models.py index 80c727bf9..1f955fc17 100644 --- a/src/actinia_core/common/response_models.py +++ b/src/actinia_core/common/response_models.py @@ -33,10 +33,9 @@ from .process_chain import GrassModule __license__ = "GPLv3" -__author__ = "Sören Gebbert" -__copyright__ = "Copyright 2016-2018, Sören Gebbert and mundialis GmbH & Co. KG" -__maintainer__ = "Sören Gebbert" -__email__ = "soerengebbert@googlemail.com" +__author__ = "Sören Gebbert, Julia Haas, Guido Riembauer" +__copyright__ = "Copyright 2016-2021, Sören Gebbert and mundialis GmbH & Co. KG" +__maintainer__ = "mundialis" class ProgressInfoModel(Schema): @@ -185,6 +184,31 @@ class SimpleResponseModel(Schema): required = ["status", "message"] +class LockedMapsetListResponseModel(Schema): + """Response schema that is used to list all locked mapsets. + + """ + type = 'object' + properties = { + 'status': { + 'type': 'string', + 'description': 'The status of the request' + }, + 'locked_mapsets_list': { + 'type': 'array', + 'items': {'type': 'string'}, + 'description': 'The names of all locked mapsets' + }, + 'message': { + 'type': 'string', + 'description': 'A simple message to describes the status of the resource' + } + } + required = ["status", "locked_mapsets_list", "message"] + example = {"status": "success", "locked_mapsets_list": ["utm32n/test_mapset"], + "message": "number of locked mapsets: 1"} + + class ApiInfoModel(Schema): """Response schema that contains API information of the called endpoint. diff --git a/src/actinia_core/endpoints.py b/src/actinia_core/endpoints.py index 1a55a855a..dcffb7754 100644 --- a/src/actinia_core/endpoints.py +++ b/src/actinia_core/endpoints.py @@ -34,6 +34,7 @@ from actinia_core.rest.location_management import \ ListLocationsResource, LocationManagementResourceUser from actinia_core.rest.location_management import LocationManagementResourceAdmin +from actinia_core.rest.mapsets import AllMapsetsListingResourceAdmin from actinia_core.rest.mapset_management import \ ListMapsetsResource, MapsetManagementResourceUser from actinia_core.rest.mapset_management import \ @@ -106,6 +107,7 @@ def create_core_endpoints(): flask_api.add_resource( MapsetLockManagementResource, '/locations//mapsets//lock') + # Raster management flask_api.add_resource( RasterLayersResource, '/locations//mapsets/' @@ -200,6 +202,11 @@ def create_core_endpoints(): AsyncEphemeralRasterLayerRegionExporterResource, '/locations//mapsets/' '/raster_layers//geotiff_async_orig') + + # all mapsets across all locations listing + flask_api.add_resource( + AllMapsetsListingResourceAdmin, '/mapsets') + # User management flask_api.add_resource(UserListResource, '/users') flask_api.add_resource(UserManagementResource, '/users/') diff --git a/src/actinia_core/rest/mapsets.py b/src/actinia_core/rest/mapsets.py new file mode 100644 index 000000000..44929ead6 --- /dev/null +++ b/src/actinia_core/rest/mapsets.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +####### +# actinia-core - an open source REST API for scalable, distributed, high +# performance processing of geographical data that uses GRASS GIS for +# computational tasks. For details, see https://actinia.mundialis.de/ +# +# Copyright (c) 2021 mundialis GmbH & Co. KG +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +####### + +""" +Mapset resources for information across all locations + +* List all mapset locks +* TODO: List all mapsets in all locations +""" + +from flask import jsonify, make_response +from flask_restful_swagger_2 import swagger +from flask import request +from .resource_base import ResourceBase +from actinia_core.common.app import auth +from actinia_core.common.api_logger import log_api_call +from actinia_core.common.config import global_config +from actinia_core.common.redis_lock import RedisLockingInterface +from actinia_core.common.response_models import SimpleResponseModel, \ + LockedMapsetListResponseModel +from .user_auth import check_user_permissions +from .user_auth import very_admin_role +# from .common.response_models import MapsetInfoModel + + +__license__ = "GPLv3" +__author__ = "Julia Haas, Guido Riembauer" +__copyright__ = "Copyright 2021 mundialis GmbH & Co. KG" +__maintainer__ = "mundialis" + + +class AllMapsetsListingResourceAdmin(ResourceBase): + """ Get all locked mapsets + """ + decorators = [log_api_call, check_user_permissions, + very_admin_role, auth.login_required] + + @swagger.doc({ + 'tags': ['Mapsets'], + 'description': 'Get all locked mapsets. ' + 'Minimum required user role: admin.', + 'parameters': [ + { + 'in': 'path', + 'name': 'status', + 'type': 'string', + 'description': ("If set to 'locked', list all locked mapsets across" + " all locations.") + }], + 'responses': { + '200': { + 'description': 'Get a list of (locked) mapsets ', + 'schema': LockedMapsetListResponseModel + }, + '500': { + 'description': 'The error message and a detailed error log', + 'schema': SimpleResponseModel + } + } + }) + def get(self): + if 'status' in request.args: + if request.args['status'] == "locked": + redis_interface = RedisLockingInterface() + kwargs = dict() + kwargs["host"] = global_config.REDIS_SERVER_URL + kwargs["port"] = global_config.REDIS_SERVER_PORT + if (global_config.REDIS_SERVER_PW + and global_config.REDIS_SERVER_PW is not None): + kwargs["password"] = global_config.REDIS_SERVER_PW + + redis_interface.connect(**kwargs) + redis_connection = redis_interface.redis_server + keys_locked = redis_connection.keys("RESOURCE-LOCK*") + redis_interface.disconnect() + keys_locked_dec = [key.decode() for key in keys_locked] + mapsets_locked = ["/".join(key.split("/")[-2:]) + for key in keys_locked_dec] + try: + return make_response(jsonify(LockedMapsetListResponseModel( + status="success", + message="number of locked mapsets: %s" % len(mapsets_locked), + locked_mapsets_list=mapsets_locked)), 200) + + except Exception as e: + return make_response(jsonify(SimpleResponseModel( + status="error", + message="Unable to list locked mapsets: Exception %s" + % (str(e)))), 500) + else: + # TODO: https://github.com/mundialis/actinia_core/issues/162 + pass diff --git a/tests/test_mapsets.py b/tests/test_mapsets.py new file mode 100644 index 000000000..f2a90c4cb --- /dev/null +++ b/tests/test_mapsets.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +####### +# actinia-core - an open source REST API for scalable, distributed, high +# performance processing of geographical data that uses GRASS GIS for +# computational tasks. For details, see https://actinia.mundialis.de/ +# +# Copyright (c) 2021 mundialis GmbH & Co. KG +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +####### + +""" +Tests: Mapset test case +""" +from flask.json import loads as json_load +import uuid +import unittest +try: + from .test_resource_base import ActiniaResourceTestCaseBase, URL_PREFIX +except: + from test_resource_base import ActiniaResourceTestCaseBase, URL_PREFIX + +__license__ = "GPLv3" +__author__ = "Julia Haas, Guido Riembauer" +__copyright__ = "Copyright 2021 mundialis GmbH & Co. KG" +__maintainer__ = "mundialis" + + +class MapsetsTestCase(ActiniaResourceTestCaseBase): + + test_mapsets = [str(uuid.uuid4()), str(uuid.uuid4())] + + def tearDown(self): + # unlock and delete the test mapsets + rv = self.server.get(URL_PREFIX + '/locations/nc_spm_08/mapsets', + headers=self.user_auth_header) + existing_mapsets = json_load(rv.data)["process_results"] + for mapset in self.test_mapsets: + if mapset in existing_mapsets: + rvdellock = self.server.delete( + URL_PREFIX + '/locations/nc_spm_08/mapsets/%s/lock' % mapset, + headers=self.admin_auth_header) + print(rvdellock.data.decode()) + + rvdel = self.server.delete( + URL_PREFIX + '/locations/nc_spm_08/mapsets/%s' % mapset, + headers=self.admin_auth_header) + print(rvdel.data.decode()) + + def test_two_locked_mapsets(self): + # Test correct behaviour if two mapsets are locked + for mapset in self.test_mapsets: + self.create_new_mapset(mapset) + rvpost = self.server.post(URL_PREFIX + '/locations/nc_spm_08/mapsets/%s/lock' % mapset, + headers=self.admin_auth_header) + rv = self.server.get(URL_PREFIX + '/mapsets?status=locked', + headers=self.admin_auth_header) + self.assertEqual(rv.status_code, 200, "HTML status code is wrong %i" % rv.status_code) + self.assertEqual(rv.mimetype, "application/json", "Wrong mimetype %s" % rv.mimetype) + rvdata = json_load(rv.data) + mapset_list = rvdata["locked_mapsets_list"] + ref_list = ["nc_spm_08/{}".format(ms) for ms in self.test_mapsets] + for ref_mapset in ref_list: + self.assertIn(ref_mapset, mapset_list, "%s is not in the list of locked mapsets" % ref_mapset) + + message = rvdata["message"] + mapsets_no = int(message.split(":")[-1]) + self.assertGreaterEqual(mapsets_no, 2, "Number of locked mapsets is smaller than 2") + + print("Currently there are %s locked mapsets:\n %s" % (str(mapsets_no), "\n".join(mapset_list))) + + def test_user_error(self): + # Test correct behaviour if user role is not admin + rv = self.server.get(URL_PREFIX + '/mapsets?status=locked', + headers=self.user_auth_header) + self.assertEqual(rv.status_code, 401, "Status code is not 401: %s" % rv.status_code) + + +if __name__ == '__main__': + unittest.main()