{"action":"created","comment":{"url":"https://api.github.com/repos/ceph/ceph/pulls/comments/115726050","pull_request_review_id":37287739,"id":115726050,"diff_hunk":"@@ -0,0 +1,501 @@\n+\"\"\"\n+A RESTful API for Ceph\n+\"\"\"\n+\n+import os\n+import json\n+import time\n+import errno\n+import inspect\n+import threading\n+import traceback\n+\n+import common\n+\n+from uuid import uuid4\n+from pecan import jsonify, make_app\n+from pecan.rest import RestController\n+from werkzeug.serving import make_server, make_ssl_devcert\n+\n+from mgr_module import MgrModule, CommandResult\n+\n+# Global instance to share\n+instance = None\n+\n+\n+\n+class CommandsRequest(object):\n+    \"\"\"\n+    This class handles parallel as well as sequential execution of\n+    commands. The class accept a list of iterables that should be\n+    executed sequentially. Each iterable can contain several commands\n+    that can be executed in parallel.\n+\n+    Example:\n+    [[c1,c2],[c3,c4]]\n+     - run c1 and c2 in parallel\n+     - wait for them to finish\n+     - run c3 and c4 in parallel\n+     - wait for them to finish\n+    \"\"\"\n+\n+\n+    def __init__(self, commands_arrays):\n+        self.id = str(id(self))\n+\n+        # Filter out empty sub-requests\n+        commands_arrays = filter(\n+            lambda x: len(x) != 0,\n+            commands_arrays,\n+        )\n+\n+        self.running = []\n+        self.waiting = commands_arrays[1:]\n+        self.finished = []\n+        self.failed = []\n+\n+        self.lock = threading.RLock()\n+        if not len(commands_arrays):\n+            # Nothing to run\n+            return\n+\n+        # Process first iteration of commands_arrays in parallel\n+        results = self.run(commands_arrays[0])\n+\n+        self.running.extend(results)\n+\n+\n+    def run(self, commands):\n+        \"\"\"\n+        A static method that will execute the given list of commands in\n+        parallel and will return the list of command results.\n+        \"\"\"\n+\n+        # Gather the results (in parallel)\n+        results = []\n+        for index in range(len(commands)):\n+            tag = '%s:%d' % (str(self.id), index)\n+\n+            # Store the result\n+            result = CommandResult(tag)\n+            result.command = common.humanify_command(commands[index])\n+            results.append(result)\n+\n+            # Run the command\n+            instance.send_command(result, 'mon', '', json.dumps(commands[index]), tag)\n+\n+        return results\n+\n+\n+    def next(self):\n+        with self.lock:\n+            if not self.waiting:\n+                # Nothing to run\n+                return\n+\n+            # Run a next iteration of commands\n+            commands = self.waiting[0]\n+            self.waiting = self.waiting[1:]\n+\n+            self.running.extend(self.run(commands))\n+\n+\n+    def finish(self, tag):\n+        with self.lock:\n+            for index in range(len(self.running)):\n+                if self.running[index].tag == tag:\n+                    if self.running[index].r == 0:\n+                        self.finished.append(self.running.pop(index))\n+                    else:\n+                        self.failed.append(self.running.pop(index))\n+                    return True\n+\n+            # No such tag found\n+            return False\n+\n+\n+    def is_running(self, tag):\n+        for result in self.running:\n+            if result.tag == tag:\n+                return True\n+        return False\n+\n+\n+    def is_ready(self):\n+        with self.lock:\n+            return not self.running and self.waiting\n+\n+\n+    def is_waiting(self):\n+        return bool(self.waiting)\n+\n+\n+    def is_finished(self):\n+        with self.lock:\n+            return not self.running and not self.waiting\n+\n+\n+    def has_failed(self):\n+        return bool(self.failed)\n+\n+\n+    def get_state(self):\n+        with self.lock:\n+            if not self.is_finished():\n+                return \"pending\"\n+\n+            if self.has_failed():\n+                return \"failed\"\n+\n+            return \"success\"\n+\n+\n+    def __json__(self):\n+        return {\n+            'id': self.id,\n+            'running': map(\n+                lambda x: (x.command, x.outs, x.outb),\n+                self.running\n+            ),\n+            'finished': map(\n+                lambda x: (x.command, x.outs, x.outb),\n+                self.finished\n+            ),\n+            'waiting': map(\n+                lambda x: (x.command, x.outs, x.outb),\n+                self.waiting\n+            ),\n+            'failed': map(\n+                lambda x: (x.command, x.outs, x.outb),\n+                self.failed\n+            ),\n+            'is_waiting': self.is_waiting(),\n+            'is_finished': self.is_finished(),\n+            'has_failed': self.has_failed(),\n+            'state': self.get_state(),\n+        }\n+\n+\n+\n+class Module(MgrModule):\n+    COMMANDS = [\n+        {\n+            \"cmd\": \"create_key name=key_name,type=CephString\",\n+            \"desc\": \"Create an API key with this name\",\n+            \"perm\": \"rw\"\n+        },\n+        {\n+            \"cmd\": \"delete_key name=key_name,type=CephString\",\n+            \"desc\": \"Delete an API key with this name\",\n+            \"perm\": \"rw\"\n+        },\n+        {\n+            \"cmd\": \"list_keys\",\n+            \"desc\": \"List all API keys\",\n+            \"perm\": \"rw\"\n+        },\n+    ]\n+\n+    def __init__(self, *args, **kwargs):\n+        super(Module, self).__init__(*args, **kwargs)\n+        global instance\n+        instance = self\n+\n+        self.requests = []\n+        self.requests_lock = threading.RLock()\n+\n+        self.keys = {}\n+        self.disable_auth = False\n+\n+        self.server = None\n+\n+\n+    def serve(self):\n+        try:\n+            self._serve()\n+        except:\n+            self.log.error(str(traceback.format_exc()))\n+\n+\n+    def _serve(self):\n+        # Load stored authentication keys\n+        self.keys = self.get_config_json(\"keys\") or {}\n+\n+        jsonify._instance = jsonify.GenericJSON(\n+            sort_keys=True,\n+            indent=4,\n+            separators=(',', ': '),\n+        )\n+\n+        # Create the HTTPS werkzeug server serving pecan app\n+        self.server = make_server(\n+            host='0.0.0.0',\n+            port=8002,\n+            app=make_app('restful.api.Root'),\n+            ssl_context=self.load_cert(),\n+        )\n+\n+        self.server.serve_forever()\n+\n+\n+    def shutdown(self):\n+        try:\n+            if self.server:\n+                self.server.shutdown()\n+        except:\n+            self.log.error(str(traceback.format_exc()))\n+\n+\n+    def notify(self, notify_type, tag):\n+        try:\n+            self._notify(notify_type, tag)\n+        except:\n+            self.log.error(str(traceback.format_exc()))\n+\n+\n+    def _notify(self, notify_type, tag):\n+        if notify_type == \"command\":\n+            # we can safely skip all the sequential commands\n+            if tag == 'seq':\n+                return\n+\n+            request = filter(\n+                lambda x: x.is_running(tag),\n+                self.requests)\n+\n+            if len(request) != 1:\n+                self.log.warn(\"Unknown request '%s'\" % str(tag))\n+                return\n+\n+            request = request[0]\n+            request.finish(tag)\n+            if request.is_ready():\n+                request.next()\n+        else:\n+            self.log.debug(\"Unhandled notification type '%s'\" % notify_type)\n+\n+\n+    def handle_command(self, command):\n+        self.log.warn(\"Handling command: '%s'\" % str(command))\n+        if command['prefix'] == \"create_key\":\n+            if command['key_name'] in self.keys:\n+                return 0, self.keys[command['key_name']], \"\"\n+\n+            else:\n+                self.keys[command['key_name']] = str(uuid4())\n+                self.set_config_json('keys', self.keys)\n+\n+            return (\n+                0,\n+                self.keys[command['key_name']],\n+                \"\",\n+            )\n+\n+        elif command['prefix'] == \"delete_key\":\n+            if command['key_name'] in self.keys:\n+                del self.keys[command['key_name']]\n+                self.set_config_json('keys', self.keys)\n+\n+            return (\n+                0,\n+                \"\",\n+                \"\",\n+            )\n+\n+        elif command['prefix'] == \"list_keys\":\n+            return (\n+                0,\n+                json.dumps(self.get_config_json('keys'), indent=2),\n+                \"\",\n+            )\n+\n+        else:\n+            return (\n+                -errno.EINVAL,\n+                \"\",\n+                \"Command not found '{0}'\".format(command['prefix'])\n+            )\n+\n+\n+    def load_cert(self):\n+        cert_base = '/etc/ceph/ceph-mgr-restful'\n+        cert_file = cert_base + '.crt'\n+        pkey_file = cert_base + '.key'\n+\n+        # If the files are already there, we are good\n+        if os.access(cert_file, os.R_OK) and os.access(pkey_file, os.R_OK):\n+            return (cert_file, pkey_file)\n+\n+        # If the certificate is in the ceph config db, write it to the files\n+        cert = self.get_config_json('cert')\n+        pkey = self.get_config_json('pkey')\n+\n+        if cert and pkey:\n+            f = file(cert_file, 'w')\n+            f.write(cert)\n+            f.close()\n+\n+            f = file(pkey_file, 'w')\n+            f.write(pkey)\n+            f.close()\n+            return (cert_file, pkey_file)\n+\n+        # Otherwise, generate the certificate and save it in the config db\n+        make_ssl_devcert(cert_base, host='localhost')\n+\n+        f = file(cert_file, 'r')\n+        self.set_config_json('cert', f.read())\n+        f.close()\n+\n+        f = file(pkey_file, 'r')\n+        self.set_config_json('pkey', f.read())\n+        f.close()\n+\n+        return (cert_file, pkey_file)\n+\n+\n+    def get_doc_api(self, root, prefix=''):\n+        doc = {}\n+        for _obj in dir(root):\n+            obj = getattr(root, _obj)\n+\n+            if isinstance(obj, RestController):\n+                doc.update(self.get_doc_api(obj, prefix + '/' + _obj))\n+\n+        if getattr(root, '_lookup', None) and isinstance(root._lookup('0')[0], RestController):\n+            doc.update(self.get_doc_api(root._lookup('0')[0], prefix + '/<arg>'))\n+\n+        prefix = prefix or '/'\n+\n+        doc[prefix] = {}\n+        for method in 'get', 'post', 'patch', 'delete':\n+            if getattr(root, method, None):\n+                doc[prefix][method.upper()] = inspect.getdoc(getattr(root, method)).split('\\n')\n+\n+        if len(doc[prefix]) == 0:\n+            del doc[prefix]\n+\n+        return doc\n+\n+\n+    def get_mons(self):\n+        mon_map_mons = self.get('mon_map')['mons']\n+        mon_status = json.loads(self.get('mon_status')['json'])\n+\n+        # Add more information\n+        for mon in mon_map_mons:\n+            mon['in_quorum'] = mon['rank'] in mon_status['quorum']\n+            mon['server'] = self.get_metadata(\"mon\", mon['name'])['hostname']\n+            mon['leader'] = mon['rank'] == mon_status['quorum'][0]\n+\n+        return mon_map_mons\n+\n+\n+    def get_osd_pools(self):\n+        osds = dict(map(lambda x: (x['osd'], []), self.get('osd_map')['osds']))\n+        pools = dict(map(lambda x: (x['pool'], x), self.get('osd_map')['pools']))\n+        crush_rules = self.get('osd_map_crush')['rules']\n+\n+        osds_by_pool = {}\n+        for pool_id, pool in pools.items():\n+            pool_osds = None\n+            for rule in [r for r in crush_rules if r['ruleset'] == pool['crush_ruleset']]:\n+                if rule['min_size'] <= pool['size'] <= rule['max_size']:\n+                    pool_osds = common.crush_rule_osds(self.get('osd_map_tree')['nodes'], rule)\n+\n+            osds_by_pool[pool_id] = pool_osds\n+\n+        for pool_id in pools.keys():\n+            for in_pool_id in osds_by_pool[pool_id]:\n+                osds[in_pool_id].append(pool_id)\n+\n+        return osds\n+\n+\n+    def get_osds(self, pool_id=None, ids=None):\n+        # Get data\n+        osd_map = self.get('osd_map')\n+        osd_metadata = self.get('osd_metadata')\n+\n+        # Update the data with the additional info from the osd map\n+        osds = osd_map['osds']\n+\n+        # Filter by osd ids\n+        if ids is not None:\n+            osds = filter(\n+                lambda x: str(x['osd']) in ids,\n+                osds\n+            )\n+\n+        # Get list of pools per osd node\n+        pools_map = self.get_osd_pools()\n+\n+        # map osd IDs to reweight\n+        reweight_map = dict([\n+            (x.get('id'), x.get('reweight', None))\n+            for x in self.get('osd_map_tree')['nodes']\n+        ])\n+\n+        # Build OSD data objects\n+        for osd in osds:\n+            osd['pools'] = pools_map[osd['osd']]\n+            osd['server'] = osd_metadata.get(str(osd['osd']), {}).get('hostname', None)\n+\n+            osd['reweight'] = reweight_map.get(osd['osd'], 0.0)\n+\n+            if osd['up']:\n+                osd['valid_commands'] = common.OSD_IMPLEMENTED_COMMANDS\n+            else:\n+                osd['valid_commands'] = []\n+\n+        # Filter by pool\n+        if pool_id:\n+            pool_id = int(pool_id)\n+            osds = filter(\n+                lambda x: pool_id in x['pools'],\n+                osds\n+            )\n+\n+        return osds\n+\n+\n+    def get_osd_by_id(self, osd_id):\n+        osd = filter(\n+            lambda x: x['osd'] == osd_id,\n+            self.get('osd_map')['osds']\n+        )\n+\n+        if len(osd) != 1:\n+            return None\n+\n+        return osd[0]\n+\n+\n+    def get_pool_by_id(self, pool_id):\n+        pool = filter(\n+            lambda x: x['pool'] == pool_id,\n+            self.get('osd_map')['pools'],\n+        )\n+\n+        if len(pool) != 1:\n+            return None\n+\n+        return pool[0]\n+\n+\n+    def submit_request(self, _request, **kwargs):\n+        request = CommandsRequest(_request)\n+        with self.requests_lock:\n+            self.requests.append(request)\n+        if kwargs.get('wait', 0):\n+            while not request.is_finished():\n+                time.sleep(0.001)\n+        return request\n+\n+\n+    def run_command(self, command):","path":"src/pybind/mgr/restful/module.py","position":496,"original_position":496,"commit_id":"d5cabf68802b0a77f21a2ea3583c760d0c618481","original_commit_id":"d5cabf68802b0a77f21a2ea3583c760d0c618481","user":{"login":"b-ranto","id":8439091,"avatar_url":"https://avatars1.githubusercontent.com/u/8439091?v=3","gravatar_id":"","url":"https://api.github.com/users/b-ranto","html_url":"https://github.com/b-ranto","followers_url":"https://api.github.com/users/b-ranto/followers","following_url":"https://api.github.com/users/b-ranto/following{/other_user}","gists_url":"https://api.github.com/users/b-ranto/gists{/gist_id}","starred_url":"https://api.github.com/users/b-ranto/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/b-ranto/subscriptions","organizations_url":"https://api.github.com/users/b-ranto/orgs","repos_url":"https://api.github.com/users/b-ranto/repos","events_url":"https://api.github.com/users/b-ranto/events{/privacy}","received_events_url":"https://api.github.com/users/b-ranto/received_events","type":"User","site_admin":false},"body":"You are right. It was used by the cephx-based authentication to get info about the key right away but that is no longer the case so it is unused.","created_at":"2017-05-10T12:26:53Z","updated_at":"2017-05-10T12:26:53Z","html_url":"https://github.com/ceph/ceph/pull/14457#discussion_r115726050","pull_request_url":"https://api.github.com/repos/ceph/ceph/pulls/14457","_links":{"self":{"href":"https://api.github.com/repos/ceph/ceph/pulls/comments/115726050"},"html":{"href":"https://github.com/ceph/ceph/pull/14457#discussion_r115726050"},"pull_request":{"href":"https://api.github.com/repos/ceph/ceph/pulls/14457"}}},"pull_request":{"url":"https://api.github.com/repos/ceph/ceph/pulls/14457","id":115305240,"html_url":"https://github.com/ceph/ceph/pull/14457","diff_url":"https://github.com/ceph/ceph/pull/14457.diff","patch_url":"https://github.com/ceph/ceph/pull/14457.patch","issue_url":"https://api.github.com/repos/ceph/ceph/issues/14457","number":14457,"state":"open","locked":false,"title":"ceph-mgr: Implement new pecan-based rest api","user":{"login":"b-ranto","id":8439091,"avatar_url":"https://avatars1.githubusercontent.com/u/8439091?v=3","gravatar_id":"","url":"https://api.github.com/users/b-ranto","html_url":"https://github.com/b-ranto","followers_url":"https://api.github.com/users/b-ranto/followers","following_url":"https://api.github.com/users/b-ranto/following{/other_user}","gists_url":"https://api.github.com/users/b-ranto/gists{/gist_id}","starred_url":"https://api.github.com/users/b-ranto/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/b-ranto/subscriptions","organizations_url":"https://api.github.com/users/b-ranto/orgs","repos_url":"https://api.github.com/users/b-ranto/repos","events_url":"https://api.github.com/users/b-ranto/events{/privacy}","received_events_url":"https://api.github.com/users/b-ranto/received_events","type":"User","site_admin":false},"body":"The new rest API uses Flask with flask-restful extension and simplifies\r\nthe code significantly. It should be mostly equivalent in functionality\r\nto the django-based rest API.\r\n\r\nThe new API uses Ceph auth system (requires caps mon allow *) and is\r\nself-documenting via /doc endpoint.\r\n\r\nSigned-off-by: Boris Ranto <branto@redhat.com>","created_at":"2017-04-11T12:10:39Z","updated_at":"2017-05-10T12:26:53Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8eb7a7ea73e4441742b11e356bf441d49eefd777","assignee":null,"assignees":[],"requested_reviewers":[],"milestone":null,"commits_url":"https://api.github.com/repos/ceph/ceph/pulls/14457/commits","review_comments_url":"https://api.github.com/repos/ceph/ceph/pulls/14457/comments","review_comment_url":"https://api.github.com/repos/ceph/ceph/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ceph/ceph/issues/14457/comments","statuses_url":"https://api.github.com/repos/ceph/ceph/statuses/d5cabf68802b0a77f21a2ea3583c760d0c618481","head":{"label":"ceph:wip-rest-flask","ref":"wip-rest-flask","sha":"d5cabf68802b0a77f21a2ea3583c760d0c618481","user":{"login":"ceph","id":1015767,"avatar_url":"https://avatars0.githubusercontent.com/u/1015767?v=3","gravatar_id":"","url":"https://api.github.com/users/ceph","html_url":"https://github.com/ceph","followers_url":"https://api.github.com/users/ceph/followers","following_url":"https://api.github.com/users/ceph/following{/other_user}","gists_url":"https://api.github.com/users/ceph/gists{/gist_id}","starred_url":"https://api.github.com/users/ceph/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ceph/subscriptions","organizations_url":"https://api.github.com/users/ceph/orgs","repos_url":"https://api.github.com/users/ceph/repos","events_url":"https://api.github.com/users/ceph/events{/privacy}","received_events_url":"https://api.github.com/users/ceph/received_events","type":"Organization","site_admin":false},"repo":{"id":2310495,"name":"ceph","full_name":"ceph/ceph","owner":{"login":"ceph","id":1015767,"avatar_url":"https://avatars0.githubusercontent.com/u/1015767?v=3","gravatar_id":"","url":"https://api.github.com/users/ceph","html_url":"https://github.com/ceph","followers_url":"https://api.github.com/users/ceph/followers","following_url":"https://api.github.com/users/ceph/following{/other_user}","gists_url":"https://api.github.com/users/ceph/gists{/gist_id}","starred_url":"https://api.github.com/users/ceph/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ceph/subscriptions","organizations_url":"https://api.github.com/users/ceph/orgs","repos_url":"https://api.github.com/users/ceph/repos","events_url":"https://api.github.com/users/ceph/events{/privacy}","received_events_url":"https://api.github.com/users/ceph/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/ceph/ceph","description":"Ceph is a distributed object, block, and file storage platform ","fork":false,"url":"https://api.github.com/repos/ceph/ceph","forks_url":"https://api.github.com/repos/ceph/ceph/forks","keys_url":"https://api.github.com/repos/ceph/ceph/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ceph/ceph/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ceph/ceph/teams","hooks_url":"https://api.github.com/repos/ceph/ceph/hooks","issue_events_url":"https://api.github.com/repos/ceph/ceph/issues/events{/number}","events_url":"https://api.github.com/repos/ceph/ceph/events","assignees_url":"https://api.github.com/repos/ceph/ceph/assignees{/user}","branches_url":"https://api.github.com/repos/ceph/ceph/branches{/branch}","tags_url":"https://api.github.com/repos/ceph/ceph/tags","blobs_url":"https://api.github.com/repos/ceph/ceph/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ceph/ceph/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ceph/ceph/git/refs{/sha}","trees_url":"https://api.github.com/repos/ceph/ceph/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ceph/ceph/statuses/{sha}","languages_url":"https://api.github.com/repos/ceph/ceph/languages","stargazers_url":"https://api.github.com/repos/ceph/ceph/stargazers","contributors_url":"https://api.github.com/repos/ceph/ceph/contributors","subscribers_url":"https://api.github.com/repos/ceph/ceph/subscribers","subscription_url":"https://api.github.com/repos/ceph/ceph/subscription","commits_url":"https://api.github.com/repos/ceph/ceph/commits{/sha}","git_commits_url":"https://api.github.com/repos/ceph/ceph/git/commits{/sha}","comments_url":"https://api.github.com/repos/ceph/ceph/comments{/number}","issue_comment_url":"https://api.github.com/repos/ceph/ceph/issues/comments{/number}","contents_url":"https://api.github.com/repos/ceph/ceph/contents/{+path}","compare_url":"https://api.github.com/repos/ceph/ceph/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ceph/ceph/merges","archive_url":"https://api.github.com/repos/ceph/ceph/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ceph/ceph/downloads","issues_url":"https://api.github.com/repos/ceph/ceph/issues{/number}","pulls_url":"https://api.github.com/repos/ceph/ceph/pulls{/number}","milestones_url":"https://api.github.com/repos/ceph/ceph/milestones{/number}","notifications_url":"https://api.github.com/repos/ceph/ceph/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ceph/ceph/labels{/name}","releases_url":"https://api.github.com/repos/ceph/ceph/releases{/id}","deployments_url":"https://api.github.com/repos/ceph/ceph/deployments","created_at":"2011-09-01T21:41:26Z","updated_at":"2017-05-10T08:39:32Z","pushed_at":"2017-05-10T12:25:15Z","git_url":"git://github.com/ceph/ceph.git","ssh_url":"git@github.com:ceph/ceph.git","clone_url":"https://github.com/ceph/ceph.git","svn_url":"https://github.com/ceph/ceph","homepage":"http://ceph.com","size":209309,"stargazers_count":3190,"watchers_count":3190,"language":"C++","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1870,"mirror_url":null,"open_issues_count":456,"forks":1870,"open_issues":456,"watchers":3190,"default_branch":"master"}},"base":{"label":"ceph:master","ref":"master","sha":"bfcd2fffe19fdb656fd5535ee4134b0085e075db","user":{"login":"ceph","id":1015767,"avatar_url":"https://avatars0.githubusercontent.com/u/1015767?v=3","gravatar_id":"","url":"https://api.github.com/users/ceph","html_url":"https://github.com/ceph","followers_url":"https://api.github.com/users/ceph/followers","following_url":"https://api.github.com/users/ceph/following{/other_user}","gists_url":"https://api.github.com/users/ceph/gists{/gist_id}","starred_url":"https://api.github.com/users/ceph/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ceph/subscriptions","organizations_url":"https://api.github.com/users/ceph/orgs","repos_url":"https://api.github.com/users/ceph/repos","events_url":"https://api.github.com/users/ceph/events{/privacy}","received_events_url":"https://api.github.com/users/ceph/received_events","type":"Organization","site_admin":false},"repo":{"id":2310495,"name":"ceph","full_name":"ceph/ceph","owner":{"login":"ceph","id":1015767,"avatar_url":"https://avatars0.githubusercontent.com/u/1015767?v=3","gravatar_id":"","url":"https://api.github.com/users/ceph","html_url":"https://github.com/ceph","followers_url":"https://api.github.com/users/ceph/followers","following_url":"https://api.github.com/users/ceph/following{/other_user}","gists_url":"https://api.github.com/users/ceph/gists{/gist_id}","starred_url":"https://api.github.com/users/ceph/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ceph/subscriptions","organizations_url":"https://api.github.com/users/ceph/orgs","repos_url":"https://api.github.com/users/ceph/repos","events_url":"https://api.github.com/users/ceph/events{/privacy}","received_events_url":"https://api.github.com/users/ceph/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/ceph/ceph","description":"Ceph is a distributed object, block, and file storage platform ","fork":false,"url":"https://api.github.com/repos/ceph/ceph","forks_url":"https://api.github.com/repos/ceph/ceph/forks","keys_url":"https://api.github.com/repos/ceph/ceph/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ceph/ceph/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ceph/ceph/teams","hooks_url":"https://api.github.com/repos/ceph/ceph/hooks","issue_events_url":"https://api.github.com/repos/ceph/ceph/issues/events{/number}","events_url":"https://api.github.com/repos/ceph/ceph/events","assignees_url":"https://api.github.com/repos/ceph/ceph/assignees{/user}","branches_url":"https://api.github.com/repos/ceph/ceph/branches{/branch}","tags_url":"https://api.github.com/repos/ceph/ceph/tags","blobs_url":"https://api.github.com/repos/ceph/ceph/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ceph/ceph/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ceph/ceph/git/refs{/sha}","trees_url":"https://api.github.com/repos/ceph/ceph/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ceph/ceph/statuses/{sha}","languages_url":"https://api.github.com/repos/ceph/ceph/languages","stargazers_url":"https://api.github.com/repos/ceph/ceph/stargazers","contributors_url":"https://api.github.com/repos/ceph/ceph/contributors","subscribers_url":"https://api.github.com/repos/ceph/ceph/subscribers","subscription_url":"https://api.github.com/repos/ceph/ceph/subscription","commits_url":"https://api.github.com/repos/ceph/ceph/commits{/sha}","git_commits_url":"https://api.github.com/repos/ceph/ceph/git/commits{/sha}","comments_url":"https://api.github.com/repos/ceph/ceph/comments{/number}","issue_comment_url":"https://api.github.com/repos/ceph/ceph/issues/comments{/number}","contents_url":"https://api.github.com/repos/ceph/ceph/contents/{+path}","compare_url":"https://api.github.com/repos/ceph/ceph/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ceph/ceph/merges","archive_url":"https://api.github.com/repos/ceph/ceph/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ceph/ceph/downloads","issues_url":"https://api.github.com/repos/ceph/ceph/issues{/number}","pulls_url":"https://api.github.com/repos/ceph/ceph/pulls{/number}","milestones_url":"https://api.github.com/repos/ceph/ceph/milestones{/number}","notifications_url":"https://api.github.com/repos/ceph/ceph/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ceph/ceph/labels{/name}","releases_url":"https://api.github.com/repos/ceph/ceph/releases{/id}","deployments_url":"https://api.github.com/repos/ceph/ceph/deployments","created_at":"2011-09-01T21:41:26Z","updated_at":"2017-05-10T08:39:32Z","pushed_at":"2017-05-10T12:25:15Z","git_url":"git://github.com/ceph/ceph.git","ssh_url":"git@github.com:ceph/ceph.git","clone_url":"https://github.com/ceph/ceph.git","svn_url":"https://github.com/ceph/ceph","homepage":"http://ceph.com","size":209309,"stargazers_count":3190,"watchers_count":3190,"language":"C++","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1870,"mirror_url":null,"open_issues_count":456,"forks":1870,"open_issues":456,"watchers":3190,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/ceph/ceph/pulls/14457"},"html":{"href":"https://github.com/ceph/ceph/pull/14457"},"issue":{"href":"https://api.github.com/repos/ceph/ceph/issues/14457"},"comments":{"href":"https://api.github.com/repos/ceph/ceph/issues/14457/comments"},"review_comments":{"href":"https://api.github.com/repos/ceph/ceph/pulls/14457/comments"},"review_comment":{"href":"https://api.github.com/repos/ceph/ceph/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ceph/ceph/pulls/14457/commits"},"statuses":{"href":"https://api.github.com/repos/ceph/ceph/statuses/d5cabf68802b0a77f21a2ea3583c760d0c618481"}}}}
{"actor":{"display_login":"b-ranto"}}