]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/rest/module.py
bump version to 12.0.3-pve3
[ceph.git] / ceph / src / pybind / mgr / rest / module.py
1
2 """
3 A RESTful API for Ceph
4 """
5
6 # We must share a global reference to this instance, because it is the
7 # gatekeeper to all accesses to data from the C++ side (e.g. the REST API
8 # request handlers need to see it)
9 _global_instance = {'plugin': None}
10 def global_instance():
11 assert _global_instance['plugin'] is not None
12 return _global_instance['plugin']
13
14
15 import os
16 import logging
17 import logging.config
18 import json
19 import uuid
20 import errno
21 import sys
22
23 import cherrypy
24 from django.core.servers.basehttp import get_internal_wsgi_application
25
26 from mgr_module import MgrModule
27
28 from rest.app.manager.request_collection import RequestCollection
29 from rest.app.types import OsdMap, NotFound, Config, FsMap, MonMap, \
30 PgSummary, Health, MonStatus
31
32 from logger import logger
33
34 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rest.app.settings")
35
36 django_log = logging.getLogger("django.request")
37 django_log.addHandler(logging.StreamHandler())
38 django_log.setLevel(logging.DEBUG)
39
40
41 def recurse_refs(root, path):
42 if isinstance(root, dict):
43 for k, v in root.items():
44 recurse_refs(v, path + "->%s" % k)
45 elif isinstance(root, list):
46 for n, i in enumerate(root):
47 recurse_refs(i, path + "[%d]" % n)
48
49 logger().info("%s %d (%s)" % (path, sys.getrefcount(root), root.__class__))
50
51
52 class Module(MgrModule):
53 COMMANDS = [
54 {
55 "cmd": "enable_auth "
56 "name=val,type=CephChoices,strings=true|false",
57 "desc": "Set whether to authenticate API access by key",
58 "perm": "rw"
59 },
60 {
61 "cmd": "auth_key_create "
62 "name=key_name,type=CephString",
63 "desc": "Create an API key with this name",
64 "perm": "rw"
65 },
66 {
67 "cmd": "auth_key_delete "
68 "name=key_name,type=CephString",
69 "desc": "Delete an API key with this name",
70 "perm": "rw"
71 },
72 {
73 "cmd": "auth_key_list",
74 "desc": "List all API keys",
75 "perm": "rw"
76 },
77 ]
78
79 def __init__(self, *args, **kwargs):
80 super(Module, self).__init__(*args, **kwargs)
81 _global_instance['plugin'] = self
82 self.log.info("Constructing module {0}: instance {1}".format(
83 __name__, _global_instance))
84 self.requests = RequestCollection()
85
86 self.keys = {}
87 self.enable_auth = True
88
89 def notify(self, notify_type, notify_id):
90 # FIXME: don't bother going and get_sync_object'ing the map
91 # unless there is actually someone waiting for it (find out inside
92 # requests.on_map)
93 self.log.info("Notify {0}".format(notify_type))
94 if notify_type == "command":
95 self.requests.on_completion(notify_id)
96 elif notify_type == "osd_map":
97 self.requests.on_map(OsdMap, self.get_sync_object(OsdMap))
98 elif notify_type == "mon_map":
99 self.requests.on_map(MonMap, self.get_sync_object(MonMap))
100 elif notify_type == "pg_summary":
101 self.requests.on_map(PgSummary, self.get_sync_object(PgSummary))
102 else:
103 self.log.warning("Unhandled notification type '{0}'".format(notify_type))
104
105 def get_sync_object(self, object_type, path=None):
106 if object_type == OsdMap:
107 data = self.get("osd_map")
108
109 assert data is not None
110
111 data['tree'] = self.get("osd_map_tree")
112 data['crush'] = self.get("osd_map_crush")
113 data['crush_map_text'] = self.get("osd_map_crush_map_text")
114 data['osd_metadata'] = self.get("osd_metadata")
115 obj = OsdMap(data['epoch'], data)
116 elif object_type == Config:
117 data = self.get("config")
118 obj = Config(0, data)
119 elif object_type == MonMap:
120 data = self.get("mon_map")
121 obj = MonMap(data['epoch'], data)
122 elif object_type == FsMap:
123 data = self.get("fs_map")
124 obj = FsMap(data['epoch'], data)
125 elif object_type == PgSummary:
126 data = self.get("pg_summary")
127 self.log.debug("JSON: {0}".format(data))
128 obj = PgSummary(0, data)
129 elif object_type == Health:
130 data = self.get("health")
131 obj = Health(0, json.loads(data['json']))
132 elif object_type == MonStatus:
133 data = self.get("mon_status")
134 obj = MonStatus(0, json.loads(data['json']))
135 else:
136 raise NotImplementedError(object_type)
137
138 # TODO: move 'path' handling up into C++ land so that we only
139 # Pythonize the part we're interested in
140 if path:
141 try:
142 for part in path:
143 if isinstance(obj, dict):
144 obj = obj[part]
145 else:
146 obj = getattr(obj, part)
147 except (AttributeError, KeyError):
148 raise NotFound(object_type, path)
149
150 return obj
151
152 def get_authenticators(self):
153 """
154 For the benefit of django rest_framework APIView classes
155 """
156 return [self._auth_cls()]
157
158 def shutdown(self):
159 cherrypy.engine.exit()
160
161 def serve(self):
162 self.keys = self._load_keys()
163 self.enable_auth = self.get_config_json("enable_auth")
164 if self.enable_auth is None:
165 self.enable_auth = True
166
167 app = get_internal_wsgi_application()
168
169 from rest_framework import authentication
170
171 class KeyUser(object):
172 def __init__(self, username):
173 self.username = username
174
175 def is_authenticated(self):
176 return True
177
178 # Take a local reference to use inside the APIKeyAuthentication
179 # class definition
180 log = self.log
181
182 # Configure django.request logger
183 logging.getLogger("django.request").handlers = self.log.handlers
184 logging.getLogger("django.request").setLevel(logging.DEBUG)
185
186 class APIKeyAuthentication(authentication.BaseAuthentication):
187 def authenticate(self, request):
188 if not global_instance().enable_auth:
189 return KeyUser("anonymous"), None
190
191 username = request.META.get('HTTP_X_USERNAME')
192 if not username:
193 log.warning("Rejecting: no X_USERNAME")
194 return None
195
196 if username not in global_instance().keys:
197 log.warning("Rejecting: username does not exist")
198 return None
199
200 api_key = request.META.get('HTTP_X_APIKEY')
201 expect_key = global_instance().keys[username]
202 if api_key != expect_key:
203 log.warning("Rejecting: wrong API key")
204 return None
205
206 log.debug("Accepted for user {0}".format(username))
207 return KeyUser(username), None
208
209 self._auth_cls = APIKeyAuthentication
210
211 cherrypy.config.update({
212 'server.socket_port': 8002,
213 'engine.autoreload.on': False
214 })
215 cherrypy.tree.graft(app, '/')
216
217 cherrypy.engine.start()
218 cherrypy.engine.block()
219
220 def _generate_key(self):
221 return uuid.uuid4().__str__()
222
223 def _load_keys(self):
224 loaded_keys = self.get_config_json("keys")
225 self.log.debug("loaded_keys: {0}".format(loaded_keys))
226 if loaded_keys is None:
227 return {}
228 else:
229 return loaded_keys
230
231 def _save_keys(self):
232 self.set_config_json("keys", self.keys)
233
234 def handle_command(self, cmd):
235 self.log.info("handle_command: {0}".format(json.dumps(cmd, indent=2)))
236 prefix = cmd['prefix']
237 if prefix == "enable_auth":
238 enable = cmd['val'] == "true"
239 self.set_config_json("enable_auth", enable)
240 self.enable_auth = enable
241 return 0, "", ""
242 elif prefix == "auth_key_create":
243 if cmd['key_name'] in self.keys:
244 return 0, self.keys[cmd['key_name']], ""
245 else:
246 self.keys[cmd['key_name']] = self._generate_key()
247 self._save_keys()
248
249 return 0, self.keys[cmd['key_name']], ""
250 elif prefix == "auth_key_delete":
251 if cmd['key_name'] in self.keys:
252 del self.keys[cmd['key_name']]
253 self._save_keys()
254
255 return 0, "", ""
256 elif prefix == "auth_key_list":
257 return 0, json.dumps(self._load_keys(), indent=2), ""
258 else:
259 return -errno.EINVAL, "", "Command not found '{0}'".format(prefix)