diff options
Diffstat (limited to '')
-rw-r--r-- | src/pybind/mgr/restful/api/__init__.py | 39 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/config.py | 86 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/crush.py | 26 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/doc.py | 15 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/mon.py | 40 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/osd.py | 135 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/perf.py | 27 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/pool.py | 140 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/request.py | 93 | ||||
-rw-r--r-- | src/pybind/mgr/restful/api/server.py | 35 |
10 files changed, 636 insertions, 0 deletions
diff --git a/src/pybind/mgr/restful/api/__init__.py b/src/pybind/mgr/restful/api/__init__.py new file mode 100644 index 000000000..a105dfe87 --- /dev/null +++ b/src/pybind/mgr/restful/api/__init__.py @@ -0,0 +1,39 @@ +from pecan import expose +from pecan.rest import RestController + +from .config import Config +from .crush import Crush +from .doc import Doc +from .mon import Mon +from .osd import Osd +from .pool import Pool +from .perf import Perf +from .request import Request +from .server import Server + + +class Root(RestController): + config = Config() + crush = Crush() + doc = Doc() + mon = Mon() + osd = Osd() + perf = Perf() + pool = Pool() + request = Request() + server = Server() + + @expose(template='json') + def get(self, **kwargs): + """ + Show the basic information for the REST API + This includes values like api version or auth method + """ + return { + 'api_version': 1, + 'auth': + 'Use "ceph restful create-key <key>" to create a key pair, ' + 'pass it as HTTP Basic auth to authenticate', + 'doc': 'See /doc endpoint', + 'info': "Ceph Manager RESTful API server", + } diff --git a/src/pybind/mgr/restful/api/config.py b/src/pybind/mgr/restful/api/config.py new file mode 100644 index 000000000..5b0e0af96 --- /dev/null +++ b/src/pybind/mgr/restful/api/config.py @@ -0,0 +1,86 @@ +from pecan import expose, request +from pecan.rest import RestController + +from restful import common, context +from restful.decorators import auth + + +class ConfigOsd(RestController): + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show OSD configuration options + """ + flags = context.instance.get("osd_map")['flags'] + + # pause is a valid osd config command that sets pauserd,pausewr + flags = flags.replace('pauserd,pausewr', 'pause') + + return flags.split(',') + + + @expose(template='json') + @auth + def patch(self, **kwargs): + """ + Modify OSD configuration options + """ + args = request.json + + commands = [] + + valid_flags = set(args.keys()) & set(common.OSD_FLAGS) + invalid_flags = list(set(args.keys()) - valid_flags) + if invalid_flags: + context.instance.log.warning("%s not valid to set/unset", invalid_flags) + + for flag in list(valid_flags): + if args[flag]: + mode = 'set' + else: + mode = 'unset' + + commands.append({ + 'prefix': 'osd ' + mode, + 'key': flag, + }) + + return context.instance.submit_request([commands], **kwargs) + + + +class ConfigClusterKey(RestController): + def __init__(self, key): + self.key = key + + + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show specific configuration option + """ + return context.instance.get("config").get(self.key, None) + + + +class ConfigCluster(RestController): + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show all cluster configuration options + """ + return context.instance.get("config") + + + @expose() + def _lookup(self, key, *remainder): + return ConfigClusterKey(key), remainder + + + +class Config(RestController): + cluster = ConfigCluster() + osd = ConfigOsd() diff --git a/src/pybind/mgr/restful/api/crush.py b/src/pybind/mgr/restful/api/crush.py new file mode 100644 index 000000000..015c49496 --- /dev/null +++ b/src/pybind/mgr/restful/api/crush.py @@ -0,0 +1,26 @@ +from pecan import expose +from pecan.rest import RestController + +from restful import common, context +from collections import defaultdict + +from restful.decorators import auth + + +class CrushRule(RestController): + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show crush rules + """ + crush = context.instance.get('osd_map_crush') + rules = crush['rules'] + + for rule in rules: + rule['osd_count'] = len(common.crush_rule_osds(crush['buckets'], rule)) + + return rules + +class Crush(RestController): + rule = CrushRule() diff --git a/src/pybind/mgr/restful/api/doc.py b/src/pybind/mgr/restful/api/doc.py new file mode 100644 index 000000000..f1038c21b --- /dev/null +++ b/src/pybind/mgr/restful/api/doc.py @@ -0,0 +1,15 @@ +from pecan import expose +from pecan.rest import RestController + +from restful import context + +import restful + + +class Doc(RestController): + @expose(template='json') + def get(self, **kwargs): + """ + Show documentation information + """ + return context.instance.get_doc_api(restful.api.Root) diff --git a/src/pybind/mgr/restful/api/mon.py b/src/pybind/mgr/restful/api/mon.py new file mode 100644 index 000000000..20d033605 --- /dev/null +++ b/src/pybind/mgr/restful/api/mon.py @@ -0,0 +1,40 @@ +from pecan import expose, response +from pecan.rest import RestController + +from restful import context +from restful.decorators import auth + + +class MonName(RestController): + def __init__(self, name): + self.name = name + + + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for the monitor name + """ + mon = [x for x in context.instance.get_mons() + if x['name'] == self.name] + if len(mon) != 1: + response.status = 500 + return {'message': 'Failed to identify the monitor node "{}"'.format(self.name)} + return mon[0] + + + +class Mon(RestController): + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for all the monitors + """ + return context.instance.get_mons() + + + @expose() + def _lookup(self, name, *remainder): + return MonName(name), remainder diff --git a/src/pybind/mgr/restful/api/osd.py b/src/pybind/mgr/restful/api/osd.py new file mode 100644 index 000000000..8577fae98 --- /dev/null +++ b/src/pybind/mgr/restful/api/osd.py @@ -0,0 +1,135 @@ +from pecan import expose, request, response +from pecan.rest import RestController + +from restful import common, context +from restful.decorators import auth + + +class OsdIdCommand(RestController): + def __init__(self, osd_id): + self.osd_id = osd_id + + + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show implemented commands for the OSD id + """ + osd = context.instance.get_osd_by_id(self.osd_id) + + if not osd: + response.status = 500 + return {'message': 'Failed to identify the OSD id "{}"'.format(self.osd_id)} + + if osd['up']: + return common.OSD_IMPLEMENTED_COMMANDS + else: + return [] + + + @expose(template='json') + @auth + def post(self, **kwargs): + """ + Run the implemented command for the OSD id + """ + command = request.json.get('command', None) + + osd = context.instance.get_osd_by_id(self.osd_id) + + if not osd: + response.status = 500 + return {'message': 'Failed to identify the OSD id "{}"'.format(self.osd_id)} + + if not osd['up'] or command not in common.OSD_IMPLEMENTED_COMMANDS: + response.status = 500 + return {'message': 'Command "{}" not available'.format(command)} + + return context.instance.submit_request([[{ + 'prefix': 'osd ' + command, + 'who': str(self.osd_id) + }]], **kwargs) + + + +class OsdId(RestController): + def __init__(self, osd_id): + self.osd_id = osd_id + self.command = OsdIdCommand(osd_id) + + + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for the OSD id + """ + osd = context.instance.get_osds(ids=[str(self.osd_id)]) + if len(osd) != 1: + response.status = 500 + return {'message': 'Failed to identify the OSD id "{}"'.format(self.osd_id)} + + return osd[0] + + + @expose(template='json') + @auth + def patch(self, **kwargs): + """ + Modify the state (up, in) of the OSD id or reweight it + """ + args = request.json + + commands = [] + + if 'in' in args: + if args['in']: + commands.append({ + 'prefix': 'osd in', + 'ids': [str(self.osd_id)] + }) + else: + commands.append({ + 'prefix': 'osd out', + 'ids': [str(self.osd_id)] + }) + + if 'up' in args: + if args['up']: + response.status = 500 + return {'message': "It is not valid to set a down OSD to be up"} + else: + commands.append({ + 'prefix': 'osd down', + 'ids': [str(self.osd_id)] + }) + + if 'reweight' in args: + commands.append({ + 'prefix': 'osd reweight', + 'id': self.osd_id, + 'weight': args['reweight'] + }) + + return context.instance.submit_request([commands], **kwargs) + + + +class Osd(RestController): + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for all the OSDs + """ + # Parse request args + # TODO Filter by ids + pool_id = kwargs.get('pool', None) + + return context.instance.get_osds(pool_id) + + + @expose() + def _lookup(self, osd_id, *remainder): + return OsdId(int(osd_id)), remainder diff --git a/src/pybind/mgr/restful/api/perf.py b/src/pybind/mgr/restful/api/perf.py new file mode 100644 index 000000000..4224599f6 --- /dev/null +++ b/src/pybind/mgr/restful/api/perf.py @@ -0,0 +1,27 @@ +from pecan import expose, request, response +from pecan.rest import RestController + +from restful import context +from restful.decorators import auth, lock, paginate + +import re + +class Perf(RestController): + @expose(template='json') + @paginate + @auth + def get(self, **kwargs): + """ + List all the available performance counters + + Options: + - 'daemon' -- filter by daemon, accepts Python regexp + """ + + counters = context.instance.get_all_perf_counters() + + if 'daemon' in kwargs: + _re = re.compile(kwargs['daemon']) + counters = {k: v for k, v in counters.items() if _re.match(k)} + + return counters diff --git a/src/pybind/mgr/restful/api/pool.py b/src/pybind/mgr/restful/api/pool.py new file mode 100644 index 000000000..40de54eb9 --- /dev/null +++ b/src/pybind/mgr/restful/api/pool.py @@ -0,0 +1,140 @@ +from pecan import expose, request, response +from pecan.rest import RestController + +from restful import common, context +from restful.decorators import auth + + +class PoolId(RestController): + def __init__(self, pool_id): + self.pool_id = pool_id + + + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for the pool id + """ + pool = context.instance.get_pool_by_id(self.pool_id) + + if not pool: + response.status = 500 + return {'message': 'Failed to identify the pool id "{}"'.format(self.pool_id)} + + # pgp_num is called pg_placement_num, deal with that + if 'pg_placement_num' in pool: + pool['pgp_num'] = pool.pop('pg_placement_num') + return pool + + + @expose(template='json') + @auth + def patch(self, **kwargs): + """ + Modify the information for the pool id + """ + try: + args = request.json + except ValueError: + response.status = 400 + return {'message': 'Bad request: malformed JSON or wrong Content-Type'} + + # Get the pool info for its name + pool = context.instance.get_pool_by_id(self.pool_id) + if not pool: + response.status = 500 + return {'message': 'Failed to identify the pool id "{}"'.format(self.pool_id)} + + # Check for invalid pool args + invalid = common.invalid_pool_args(args) + if invalid: + response.status = 500 + return {'message': 'Invalid arguments found: "{}"'.format(invalid)} + + # Schedule the update request + return context.instance.submit_request(common.pool_update_commands(pool['pool_name'], args), **kwargs) + + + @expose(template='json') + @auth + def delete(self, **kwargs): + """ + Remove the pool data for the pool id + """ + pool = context.instance.get_pool_by_id(self.pool_id) + + if not pool: + response.status = 500 + return {'message': 'Failed to identify the pool id "{}"'.format(self.pool_id)} + + return context.instance.submit_request([[{ + 'prefix': 'osd pool delete', + 'pool': pool['pool_name'], + 'pool2': pool['pool_name'], + 'yes_i_really_really_mean_it': True + }]], **kwargs) + + + +class Pool(RestController): + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for all the pools + """ + pools = context.instance.get('osd_map')['pools'] + + # pgp_num is called pg_placement_num, deal with that + for pool in pools: + if 'pg_placement_num' in pool: + pool['pgp_num'] = pool.pop('pg_placement_num') + + return pools + + + @expose(template='json') + @auth + def post(self, **kwargs): + """ + Create a new pool + Requires name and pg_num dict arguments + """ + args = request.json + + # Check for the required arguments + pool_name = args.pop('name', None) + if pool_name is None: + response.status = 500 + return {'message': 'You need to specify the pool "name" argument'} + + pg_num = args.pop('pg_num', None) + if pg_num is None: + response.status = 500 + return {'message': 'You need to specify the "pg_num" argument'} + + # Run the pool create command first + create_command = { + 'prefix': 'osd pool create', + 'pool': pool_name, + 'pg_num': pg_num + } + + # Check for invalid pool args + invalid = common.invalid_pool_args(args) + if invalid: + response.status = 500 + return {'message': 'Invalid arguments found: "{}"'.format(invalid)} + + # Schedule the creation and update requests + return context.instance.submit_request( + [[create_command]] + + common.pool_update_commands(pool_name, args), + **kwargs + ) + + + @expose() + def _lookup(self, pool_id, *remainder): + return PoolId(int(pool_id)), remainder diff --git a/src/pybind/mgr/restful/api/request.py b/src/pybind/mgr/restful/api/request.py new file mode 100644 index 000000000..67143ef50 --- /dev/null +++ b/src/pybind/mgr/restful/api/request.py @@ -0,0 +1,93 @@ +from pecan import expose, request, response +from pecan.rest import RestController + +from restful import context +from restful.decorators import auth, lock, paginate + + +class RequestId(RestController): + def __init__(self, request_id): + self.request_id = request_id + + + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for the request id + """ + request = [x for x in context.instance.requests + if x.id == self.request_id] + if len(request) != 1: + response.status = 500 + return {'message': 'Unknown request id "{}"'.format(self.request_id)} + return request[0] + + + @expose(template='json') + @auth + @lock + def delete(self, **kwargs): + """ + Remove the request id from the database + """ + for index in range(len(context.instance.requests)): + if context.instance.requests[index].id == self.request_id: + return context.instance.requests.pop(index) + + # Failed to find the job to cancel + response.status = 500 + return {'message': 'No such request id'} + + + +class Request(RestController): + @expose(template='json') + @paginate + @auth + def get(self, **kwargs): + """ + List all the available requests + """ + return context.instance.requests + + + @expose(template='json') + @auth + @lock + def delete(self, **kwargs): + """ + Remove all the finished requests + """ + num_requests = len(context.instance.requests) + + context.instance.requests = [x for x in context.instance.requests + if not x.is_finished()] + remaining = len(context.instance.requests) + # Return the job statistics + return { + 'cleaned': num_requests - remaining, + 'remaining': remaining, + } + + + @expose(template='json') + @auth + def post(self, **kwargs): + """ + Pass through method to create any request + """ + if isinstance(request.json, list): + if all(isinstance(element, list) for element in request.json): + return context.instance.submit_request(request.json, **kwargs) + + # The request.json has wrong format + response.status = 500 + return {'message': 'The request format should be [[{c1},{c2}]]'} + + return context.instance.submit_request([[request.json]], **kwargs) + + + @expose() + def _lookup(self, request_id, *remainder): + return RequestId(request_id), remainder diff --git a/src/pybind/mgr/restful/api/server.py b/src/pybind/mgr/restful/api/server.py new file mode 100644 index 000000000..8ce634937 --- /dev/null +++ b/src/pybind/mgr/restful/api/server.py @@ -0,0 +1,35 @@ +from pecan import expose +from pecan.rest import RestController + +from restful import context +from restful.decorators import auth + + +class ServerFqdn(RestController): + def __init__(self, fqdn): + self.fqdn = fqdn + + + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for the server fqdn + """ + return context.instance.get_server(self.fqdn) + + + +class Server(RestController): + @expose(template='json') + @auth + def get(self, **kwargs): + """ + Show the information for all the servers + """ + return context.instance.list_servers() + + + @expose() + def _lookup(self, fqdn, *remainder): + return ServerFqdn(fqdn), remainder |