{"action":"created","comment":{"url":"https://api.github.com/repos/lsst/qserv/pulls/comments/29033512","id":29033512,"diff_hunk":"@@ -1,311 +1,381 @@\n #!/usr/bin/env python\n \n+\n+# LSST Data Management System\n+# Copyright 2014 AURA/LSST.\n+#\n+# This product includes software developed by the\n+# LSST Project (http://www.lsst.org/).\n+#\n+# This program is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+#\n+# This program is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+#\n+# You should have received a copy of the LSST License Statement and\n+# the GNU General Public License along with this program.  If not,\n+# see <http://www.lsstcorp.org/LegalNotices/>.\n+\n+\"\"\"\n+User-friendly configuration script for Qserv\n+\n+Can configure mono/master/worker instance.\n+\n+Create a Qserv run directory which contains:\n+ - a meta-configuration file containing Qserv main parameters\n+   and information about the binaries used for the run\n+ - configuration/startup files for each services\n+ - log files\n+ - pid files\n+ - data file for zookeeper, MySQL and Qserv\n+\n+A Qserv run directory can only run one Qserv instance at a time.\n+\n+@author  Fabrice Jammes, IN2P3\n+\n+\"\"\"\n+\n+# --------------------------------\n+#  Imports of standard modules --\n+#--------------------------------\n import argparse\n import ConfigParser\n-import fileinput\n-from lsst.qserv.admin import configure, commons\n import logging\n import os\n import shutil\n from subprocess import check_output\n import sys\n \n-\n-def parseArgs():\n-\n-    qserv_version = check_output([\"qserv-version.sh\"])\n-    qserv_version = qserv_version.strip(' \\t\\n\\r')\n-    default_qserv_run_dir = os.path.join(\n-        os.path.expanduser(\"~\"), \"qserv-run\", qserv_version)\n-\n-    parser = argparse.ArgumentParser(\n-        description='''Qserv configuration tool. Creates an execution\n-directory (qserv_run_dir) which will contains configuration and execution\n-data for a given Qserv instance. Deploys values from meta-config file $qserv_run_dir/qserv-meta.conf\n-in all Qserv configuration files and databases. Default behaviour will configure a mono-node\n-instance in ''' + default_qserv_run_dir + '''. IMPORTANT : --all MUST BE USED\n-FOR A  SETUP FROM SCRATCH.''',\n-        formatter_class=argparse.ArgumentDefaultsHelpFormatter\n-    )\n-\n-    parser.add_argument(\"-a\", \"--all\", dest=\"all\", action='store_true',\n-                        default=False,\n-                        help='''clean execution directory and then run all configuration steps'''\n-                        )\n-    # Defining option of each configuration step\n-    for step_name in configure.STEP_LIST:\n-        parser.add_argument(\n-            \"-{0}\".format(configure.STEP_ABBR[step_name]),\n-            \"--{0}\".format(step_name),\n-            dest=\"step_list\",\n-            action='append_const',\n-            const=step_name,\n-            help=configure.STEP_DOC[step_name]\n+#-----------------------------\n+# Imports for other modules --\n+#-----------------------------\n+from lsst.qserv.admin import configure, commons\n+import lsst.qserv.admin.logger\n+\n+#----------------------------------\n+# Local non-exported definitions --\n+#----------------------------------\n+_LOG = logging.getLogger()\n+\n+#------------------------\n+# Exported definitions --\n+#------------------------\n+class Configurator(object):\n+    \"\"\"\n+    Application class for configuration application\n+    \"\"\"\n+\n+    def __init__(self):\n+        \"\"\"\n+        Constructor parse all arguments and prepares for execution.\n+        \"\"\"\n+\n+        qserv_version = check_output([\"qserv-version.sh\"])\n+        qserv_version = qserv_version.strip(' \\t\\n\\r')\n+        default_qserv_run_dir = os.path.join(\n+            os.path.expanduser(\"~\"), \"qserv-run\", qserv_version)\n+\n+        parser = argparse.ArgumentParser(\n+            description='''Qserv configuration tool. Creates an execution\n+            directory (qserv_run_dir) which will contains configuration and execution\n+            data for a given Qserv instance. Deploys values from meta-config file $qserv_run_dir/qserv-meta.conf\n+            in all Qserv configuration files and databases. Default behaviour will configure a mono-node\n+            instance in ''' + default_qserv_run_dir + '''. IMPORTANT : --all MUST BE USED\n+            FOR A SETUP FROM SCRATCH.''',\n+            formatter_class=argparse.ArgumentDefaultsHelpFormatter\n         )\n \n-    # Logging management\n-    verbose_dict = {\n-        'DEBUG': logging.DEBUG,\n-        'INFO': logging.INFO,\n-        'WARNING': logging.WARNING,\n-        'ERROR': logging.ERROR,\n-        'FATAL': logging.FATAL,\n-    }\n-    verbose_arg_values = verbose_dict.keys()\n-    parser.add_argument(\"-v\", \"--verbose-level\", dest=\"verbose_str\",\n-                        choices=verbose_arg_values,\n-                        default='INFO',\n-                        help=\"verbosity level\"\n-                        )\n-\n-    # forcing options which may ask user confirmation\n-    parser.add_argument(\"-f\", \"--force\", dest=\"force\", action='store_true',\n-                        default=False,\n-                        help=\"forcing removal of existing execution data\"\n-                        )\n-\n-    # run dir, all mutable data related to a qserv running instance are\n-    # located here\n-    parser.add_argument(\"-R\", \"--qserv-run-dir\", dest=\"qserv_run_dir\",\n-                        default=default_qserv_run_dir,\n-                        help=\"full path to qserv_run_dir\"\n-                        )\n-\n-    # meta-configuration file whose parameters will be dispatched in Qserv\n-    # services configuration files\n-    args = parser.parse_args()\n-    default_meta_config_file = os.path.join(\n-        args.qserv_run_dir, \"qserv-meta.conf\")\n-    parser.add_argument(\"-m\", \"--metaconfig\", dest=\"meta_config_file\",\n-                        default=default_meta_config_file,\n-                        help=\"full path to Qserv meta-configuration file\"\n-                        )\n-\n-    args = parser.parse_args()\n-\n-    if args.all:\n-        args.step_list = configure.STEP_LIST\n-    elif args.step_list is None:\n-        args.step_list = configure.STEP_RUN_LIST\n-\n-    args.verbose_level = verbose_dict[args.verbose_str]\n-\n-    return args\n-\n-\n-def main():\n-\n-    args = parseArgs()\n-\n-    logging.basicConfig(\n-        format='%(levelname)s: %(message)s', level=args.verbose_level)\n-\n-    logging.info(\"Qserv configuration tool\\n\" +\n-                 \"=======================================================================\"\n-                 )\n-\n-    qserv_dir = os.path.abspath(\n-        os.path.join(\n-            os.path.dirname(os.path.realpath(__file__)),\n-            \"..\")\n-    )\n-\n-    if configure.PREPARE in args.step_list:\n-\n-        if os.path.exists(args.qserv_run_dir):\n-            if args.force or configure.user_yes_no_query(\n-                \"WARNING : Do you want to erase all configuration\" +\n-                \" data in {0} ?\".format(args.qserv_run_dir)\n-            ):\n-                shutil.rmtree(args.qserv_run_dir)\n-            else:\n-                logging.info(\n-                    \"Stopping Qserv configuration, please specify an other configuration directory\")\n-                sys.exit(1)\n+        parser.add_argument('-v', '--verbose', dest='verbose', default=[],\n+                            action='append_const',\n+                            const=None,\n+                            help='More verbose output, can use several times.')\n+        parser = lsst.qserv.admin.logger.add_logfile_opt(parser)\n+\n+        parser.add_argument(\"-a\", \"--all\", dest=\"all\", action='store_true',\n+                            default=False,\n+                            help='''clean execution directory and then run all configuration steps'''\n+                            )\n+        # Defining option of each configuration step\n+        for step_name in configure.STEP_LIST:\n+            parser.add_argument(\n+                \"-{0}\".format(configure.STEP_ABBR[step_name]),\n+                \"--{0}\".format(step_name),\n+                dest=\"step_list\",\n+                action='append_const',\n+                const=step_name,\n+                help=configure.STEP_DOC[step_name]\n+            )\n \n-        in_config_dir = os.path.join(qserv_dir, \"cfg\")\n-        in_template_config_dir = os.path.join(in_config_dir, \"templates\")\n-        out_template_config_dir = os.path.join(args.qserv_run_dir, \"templates\")\n-        logging.info(\"Copying template configuration from {0} to {1}\"\n-                     .format(in_template_config_dir, args.qserv_run_dir)\n-                     )\n-        shutil.copytree(in_template_config_dir, out_template_config_dir)\n-\n-        in_meta_config_file = os.path.join(in_config_dir, \"qserv-meta.conf\")\n-        logging.info(\"Creating meta-configuration file: {0}\"\n-                     .format(args.meta_config_file)\n-                     )\n-        params_dict = {\n-            'RUN_BASE_DIR': args.qserv_run_dir\n-        }\n-        configure.apply_tpl_once(\n-            in_meta_config_file, args.meta_config_file, params_dict)\n+        # forcing options which may ask user confirmation\n+        parser.add_argument(\"-f\", \"--force\", dest=\"force\", action='store_true',\n+                            default=False,\n+                            help=\"forcing removal of existing execution data\"\n+                            )\n+\n+        # run dir, all mutable data related to a qserv running instance are\n+        # located here\n+        parser.add_argument(\"-R\", \"--qserv-run-dir\", dest=\"qserv_run_dir\",\n+                            default=default_qserv_run_dir,\n+                            help=\"full path to qserv_run_dir\"\n+                            )\n+\n+        # meta-configuration file whose parameters will be dispatched in Qserv\n+        # services configuration files\n+        self.args = parser.parse_args()\n+        default_meta_config_file = os.path.join(\n+            self.args.qserv_run_dir, \"qserv-meta.conf\")\n+        parser.add_argument(\"-m\", \"--metaconfig\", dest=\"meta_config_file\",\n+                            default=default_meta_config_file,\n+                            help=\"full path to Qserv meta-configuration file\"\n+                            )\n+\n+        self.args = parser.parse_args()\n+\n+        verbosity = len(self.args.verbose)\n+        if verbosity != 0:\n+            levels = {0: logging.WARNING, 1: logging.INFO, 2: logging.DEBUG}\n+            simple_format = \"[%(levelname)s] %(name)s: %(message)s\"\n+            level = levels.get(verbosity, logging.DEBUG)\n+            logging.basicConfig(format=simple_format, level=level)\n+        else:\n+            # if -v(vv) option isn't used then\n+            # switch to global configuration file for logging\n+            lsst.qserv.admin.logger.setup_logging(self.args.log_conf)\n+\n+        if self.args.all:\n+            self.args.step_list = configure.STEP_LIST\n+        elif self.args.step_list is None:\n+            self.args.step_list = configure.STEP_RUN_LIST\n+\n+\n+        qserv_dir = os.path.abspath(\n+            os.path.join(\n+                os.path.dirname(os.path.realpath(__file__)),\n+                \"..\")\n+        )\n+        self._in_config_dir = os.path.join(qserv_dir, \"cfg\")\n+        self._template_root = os.path.join(self._in_config_dir, \"templates\")\n \n-    def intersect(seq1, seq2):\n+    @staticmethod\n+    def _intersect(seq1, seq2):\n         ''' returns subset of seq1 which is contained in seq2 keeping original ordering of items '''\n         seq2 = set(seq2)\n         return [item for item in seq1 if item in seq2]\n \n-    def contains_configuration_step(step_list):\n-        return bool(intersect(step_list, configure.STEP_RUN_LIST))\n-\n-    ###################################\n-    #\n-    # Running configuration procedure\n-    #\n-    ###################################\n-    if contains_configuration_step(args.step_list):\n+    @staticmethod\n+    def _contains_configuration_step(step_list):\n+        return bool(Configurator._intersect(step_list, configure.STEP_RUN_LIST))\n+\n+    def _template_to_symlink(self, filename, symlink):\n+        \"\"\"\n+        Generate qserv_run_dir/etc/filename from\n+        qserv template and symlink it\n+        \"\"\"\n+        template_file = os.path.join(\n+            self._template_root, \"etc\", filename\n+        )\n+        cfg_file = os.path.join(\n+            self.args.qserv_run_dir, \"etc\", filename\n+        )\n+        configure.apply_tpl_once(\n+            template_file,\n+            cfg_file\n+        )\n+        _LOG.debug(\n+            \"Client configuration file created: {0}\".format(cfg_file)\n+        )\n \n-        try:\n-            logging.info(\n-                \"Reading meta-configuration file {0}\".format(args.meta_config_file))\n-            config = commons.read_config(args.meta_config_file)\n+        if os.path.exists(symlink):\n+            try:\n+                is_symlink_correct = os.path.samefile(symlink, cfg_file)\n+            except os.error:\n+                # link is broken\n+                is_symlink_correct = False\n+\n+            if not is_symlink_correct:\n+                if self.args.force or configure.user_yes_no_query(\n+                        \"Do you want to update symbolic link {0} to {1}?\".format(os.path.realpath(symlink),\n+                                                                                 cfg_file)):\n+                    os.remove(symlink)\n+                    os.symlink(cfg_file, symlink)\n+                else:\n+                    _LOG.fatal(\"Symbolic link to client \" +\n+                               \"configuration unmodified. Exiting.\")\n+                    sys.exit(1)\n+        else:\n+            try:\n+                os.remove(symlink)\n+                _LOG.debug(\"Removing broken symbolic link : {0}\".format(symlink))\n+            except os.error:\n+                pass\n+            os.symlink(cfg_file, symlink)\n+\n+        _LOG.info(\n+            \"{0} points to: {1}\".format(symlink, cfg_file)\n+        )\n \n-            # used in templates targets comments\n-            config['qserv']['meta_config_file'] = args.meta_config_file\n+    def _template_to_client_config(self, product):\n+        \"\"\"\n+        Create configuration files for a product from qserv_run_dir\n+        templates files, and symlink it to client configuration\n+        (for example ~/.lsst/ directory for Qserv product)\n+        \"\"\"\n+        homedir = os.path.expanduser(\"~\")\n+        if product == configure.QSERV:\n+            # might need to create directory first\n+            try:\n+                os.makedirs(os.path.join(homedir, \".lsst\"))\n+                _LOG.debug(\n+                    \"Creating client configuration directory : ~/.lsst\")\n+            except os.error:\n+                pass\n+            self._template_to_symlink(\"qserv-client.conf\", os.path.join(homedir, \".lsst\", \"qserv.conf\"))\n+            self._template_to_symlink(\"logging.ini\", os.path.join(homedir, \".lsst\", \"logging.ini\"))\n+        elif product == configure.MYSQL:\n+            self._template_to_symlink(\"my-client.cnf\",\n+                                      os.path.join(homedir, \".my.cnf\"))\n+        else:\n+            _LOG.fatal(\"Unable to apply configuration template \" +\n+                       \"for product %s\", product)\n+            sys.exit(1)\n \n-        except ConfigParser.NoOptionError, exc:\n-            logging.fatal(\"Missing option in meta-configuration file: %s\", exc)\n+    def run(self):\n+        \"\"\"\n+        Do actual configuration based on parameters provided on command-line-interface and qserv-meta.conf file\n+        This will throw exception if anything goes wrong.\n+        \"\"\"\n+        _LOG.info(\"Qserv configuration tool\\n\" +\n+                  \"=======================================================================\"\n+                  )\n+\n+        if commons.status(self.args.qserv_run_dir) not in [commons.NO_STATUS_SCRIPT, commons.DOWN]:\n+            _LOG.fatal(\n+                \"Qserv services are still running \"\n+                \"for this Qserv run directory (%s),\"\n+                \" stop it before running this script.\", self.args.qserv_run_dir)\n             sys.exit(1)\n \n-        if configure.DIRTREE in args.step_list:\n-            logging.info(\"Defining main directory structure\")\n-            configure.check_root_dirs()\n-            configure.check_root_symlinks()\n+        if configure.PREPARE in self.args.step_list:\n \n-        ##########################################\n-        #\n-        # Creating Qserv services configuration\n-        # using templates and meta_config_file\n-        #\n-        ##########################################\n-        run_base_dir = config['qserv']['run_base_dir']\n-        if configure.ETC in args.step_list:\n-            logging.info(\n-                \"Creating configuration files in {0}\".format(os.path.join(run_base_dir, \"etc\")) +\n-                \" and scripts in {0}\".format(os.path.join(run_base_dir, \"tmp\"))\n-            )\n-            template_root = os.path.join(run_base_dir, \"templates\")\n-            dest_root = os.path.join(run_base_dir)\n-            configure.apply_tpl_all(\n-                template_root,\n-                dest_root\n-            )\n+            if os.path.exists(self.args.qserv_run_dir):\n \n-        components_to_configure = intersect(\n-            args.step_list, configure.COMPONENTS)\n-        if len(components_to_configure) > 0:\n-            logging.info(\"Running configuration scripts\")\n-            configuration_scripts_dir = os.path.join(\n-                run_base_dir, 'tmp', 'configure'\n-            )\n-\n-            if config['qserv']['node_type'] in ['master']:\n-                logging.info(\n-                    \"Master instance : not configuring \" +\n-                    \"%s and %s\",\n-                    configure.SCISQL,\n-                    configure.WORKER\n-                )\n-                components_to_configure.remove(configure.SCISQL)\n-                components_to_configure.remove(configure.WORKER)\n-            elif config['qserv']['node_type'] in ['worker']:\n-                logging.info(\n-                    \"Worker instance : not configuring \" +\n-                    \"{0}\".format(configure.CZAR)\n-                )\n-                components_to_configure.remove(configure.CZAR)\n-\n-            for comp in components_to_configure:\n-                cfg_script = os.path.join(\n-                    configuration_scripts_dir, comp + \".sh\")\n-                if os.path.isfile(cfg_script):\n-                    commons.run_command([cfg_script])\n-\n-            def template_to_client_config(product):\n-                \"\"\"\n-                Create configuration files for a product from qserv_run_dir\n-                templates files, and symlink it to client configuration\n-                (for example ~/.lsst/ directory for Qserv product)\n-                \"\"\"\n-                homedir = os.path.expanduser(\"~\")\n-                if product == configure.QSERV:\n-                    # might need to create directory first\n-                    try:\n-                        os.makedirs(os.path.join(homedir, \".lsst\"))\n-                        logging.debug(\n-                            \"Creating client configuration directory : ~/.lsst\")\n-                    except os.error:\n-                        pass\n-                    _template_to_symlink(\"qserv-client.conf\",\n-                                         os.path.join(homedir,\n-                                                      \".lsst\",\n-                                                      \"qserv.conf\"))\n-                    _template_to_symlink(\"logging.ini\",os.path.join(homedir,\n-                                                      \".lsst\",\n-                                                      \"logging.ini\"))\n-                elif product == configure.MYSQL:\n-                    _template_to_symlink(\"my-client.cnf\",\n-                                         os.path.join(homedir, \".my.cnf\"))\n+                if self.args.force or configure.user_yes_no_query(\n+                                \"WARNING : Do you want to erase all configuration\" +\n+                                \" data in {0} ?\".format(self.args.qserv_run_dir)\n+                ):","path":"admin/bin/qserv-configure.py","position":489,"original_position":494,"commit_id":"55b5c932653131e836855618cbee123cb595a44e","original_commit_id":"7af2c92a74fe00f9fdc3a361d8e62c352a738226","user":{"login":"fjammes","id":290045,"avatar_url":"https://avatars.githubusercontent.com/u/290045?v=3","gravatar_id":"","url":"https://api.github.com/users/fjammes","html_url":"https://github.com/fjammes","followers_url":"https://api.github.com/users/fjammes/followers","following_url":"https://api.github.com/users/fjammes/following{/other_user}","gists_url":"https://api.github.com/users/fjammes/gists{/gist_id}","starred_url":"https://api.github.com/users/fjammes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fjammes/subscriptions","organizations_url":"https://api.github.com/users/fjammes/orgs","repos_url":"https://api.github.com/users/fjammes/repos","events_url":"https://api.github.com/users/fjammes/events{/privacy}","received_events_url":"https://api.github.com/users/fjammes/received_events","type":"User","site_admin":false},"body":"ok","created_at":"2015-04-24T08:52:04Z","updated_at":"2015-04-24T08:52:04Z","html_url":"https://github.com/lsst/qserv/pull/61#discussion_r29033512","pull_request_url":"https://api.github.com/repos/lsst/qserv/pulls/61","_links":{"self":{"href":"https://api.github.com/repos/lsst/qserv/pulls/comments/29033512"},"html":{"href":"https://github.com/lsst/qserv/pull/61#discussion_r29033512"},"pull_request":{"href":"https://api.github.com/repos/lsst/qserv/pulls/61"}}},"pull_request":{"url":"https://api.github.com/repos/lsst/qserv/pulls/61","id":33774521,"html_url":"https://github.com/lsst/qserv/pull/61","diff_url":"https://github.com/lsst/qserv/pull/61.diff","patch_url":"https://github.com/lsst/qserv/pull/61.patch","issue_url":"https://api.github.com/repos/lsst/qserv/issues/61","number":61,"state":"open","locked":false,"title":"U/fjammes/dm 2341","user":{"login":"jbecla","id":626419,"avatar_url":"https://avatars.githubusercontent.com/u/626419?v=3","gravatar_id":"","url":"https://api.github.com/users/jbecla","html_url":"https://github.com/jbecla","followers_url":"https://api.github.com/users/jbecla/followers","following_url":"https://api.github.com/users/jbecla/following{/other_user}","gists_url":"https://api.github.com/users/jbecla/gists{/gist_id}","starred_url":"https://api.github.com/users/jbecla/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jbecla/subscriptions","organizations_url":"https://api.github.com/users/jbecla/orgs","repos_url":"https://api.github.com/users/jbecla/repos","events_url":"https://api.github.com/users/jbecla/events{/privacy}","received_events_url":"https://api.github.com/users/jbecla/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-04-21T15:43:15Z","updated_at":"2015-04-24T08:52:04Z","closed_at":null,"merged_at":null,"merge_commit_sha":"e840b6ec67d4c497e09a8c65e4f48281554b7e62","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/lsst/qserv/pulls/61/commits","review_comments_url":"https://api.github.com/repos/lsst/qserv/pulls/61/comments","review_comment_url":"https://api.github.com/repos/lsst/qserv/pulls/comments{/number}","comments_url":"https://api.github.com/repos/lsst/qserv/issues/61/comments","statuses_url":"https://api.github.com/repos/lsst/qserv/statuses/55b5c932653131e836855618cbee123cb595a44e","head":{"label":"lsst:u/fjammes/DM-2341","ref":"u/fjammes/DM-2341","sha":"55b5c932653131e836855618cbee123cb595a44e","user":{"login":"lsst","id":1134504,"avatar_url":"https://avatars.githubusercontent.com/u/1134504?v=3","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"repo":{"id":23013460,"name":"qserv","full_name":"lsst/qserv","owner":{"login":"lsst","id":1134504,"avatar_url":"https://avatars.githubusercontent.com/u/1134504?v=3","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/lsst/qserv","description":"LSST Query Services","fork":false,"url":"https://api.github.com/repos/lsst/qserv","forks_url":"https://api.github.com/repos/lsst/qserv/forks","keys_url":"https://api.github.com/repos/lsst/qserv/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lsst/qserv/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lsst/qserv/teams","hooks_url":"https://api.github.com/repos/lsst/qserv/hooks","issue_events_url":"https://api.github.com/repos/lsst/qserv/issues/events{/number}","events_url":"https://api.github.com/repos/lsst/qserv/events","assignees_url":"https://api.github.com/repos/lsst/qserv/assignees{/user}","branches_url":"https://api.github.com/repos/lsst/qserv/branches{/branch}","tags_url":"https://api.github.com/repos/lsst/qserv/tags","blobs_url":"https://api.github.com/repos/lsst/qserv/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lsst/qserv/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lsst/qserv/git/refs{/sha}","trees_url":"https://api.github.com/repos/lsst/qserv/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lsst/qserv/statuses/{sha}","languages_url":"https://api.github.com/repos/lsst/qserv/languages","stargazers_url":"https://api.github.com/repos/lsst/qserv/stargazers","contributors_url":"https://api.github.com/repos/lsst/qserv/contributors","subscribers_url":"https://api.github.com/repos/lsst/qserv/subscribers","subscription_url":"https://api.github.com/repos/lsst/qserv/subscription","commits_url":"https://api.github.com/repos/lsst/qserv/commits{/sha}","git_commits_url":"https://api.github.com/repos/lsst/qserv/git/commits{/sha}","comments_url":"https://api.github.com/repos/lsst/qserv/comments{/number}","issue_comment_url":"https://api.github.com/repos/lsst/qserv/issues/comments{/number}","contents_url":"https://api.github.com/repos/lsst/qserv/contents/{+path}","compare_url":"https://api.github.com/repos/lsst/qserv/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lsst/qserv/merges","archive_url":"https://api.github.com/repos/lsst/qserv/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lsst/qserv/downloads","issues_url":"https://api.github.com/repos/lsst/qserv/issues{/number}","pulls_url":"https://api.github.com/repos/lsst/qserv/pulls{/number}","milestones_url":"https://api.github.com/repos/lsst/qserv/milestones{/number}","notifications_url":"https://api.github.com/repos/lsst/qserv/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lsst/qserv/labels{/name}","releases_url":"https://api.github.com/repos/lsst/qserv/releases{/id}","created_at":"2014-08-16T07:50:36Z","updated_at":"2015-04-23T07:47:46Z","pushed_at":"2015-04-24T08:46:27Z","git_url":"git://github.com/lsst/qserv.git","ssh_url":"git@github.com:lsst/qserv.git","clone_url":"https://github.com/lsst/qserv.git","svn_url":"https://github.com/lsst/qserv","homepage":"http://dm.lsst.org/","size":19838,"stargazers_count":4,"watchers_count":4,"language":"C++","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":5,"mirror_url":null,"open_issues_count":11,"forks":5,"open_issues":11,"watchers":4,"default_branch":"master"}},"base":{"label":"lsst:master","ref":"master","sha":"7ff5fd648a341ee7687a56e2ef2a6b195f6e0a34","user":{"login":"lsst","id":1134504,"avatar_url":"https://avatars.githubusercontent.com/u/1134504?v=3","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"repo":{"id":23013460,"name":"qserv","full_name":"lsst/qserv","owner":{"login":"lsst","id":1134504,"avatar_url":"https://avatars.githubusercontent.com/u/1134504?v=3","gravatar_id":"","url":"https://api.github.com/users/lsst","html_url":"https://github.com/lsst","followers_url":"https://api.github.com/users/lsst/followers","following_url":"https://api.github.com/users/lsst/following{/other_user}","gists_url":"https://api.github.com/users/lsst/gists{/gist_id}","starred_url":"https://api.github.com/users/lsst/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lsst/subscriptions","organizations_url":"https://api.github.com/users/lsst/orgs","repos_url":"https://api.github.com/users/lsst/repos","events_url":"https://api.github.com/users/lsst/events{/privacy}","received_events_url":"https://api.github.com/users/lsst/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/lsst/qserv","description":"LSST Query Services","fork":false,"url":"https://api.github.com/repos/lsst/qserv","forks_url":"https://api.github.com/repos/lsst/qserv/forks","keys_url":"https://api.github.com/repos/lsst/qserv/keys{/key_id}","collaborators_url":"https://api.github.com/repos/lsst/qserv/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/lsst/qserv/teams","hooks_url":"https://api.github.com/repos/lsst/qserv/hooks","issue_events_url":"https://api.github.com/repos/lsst/qserv/issues/events{/number}","events_url":"https://api.github.com/repos/lsst/qserv/events","assignees_url":"https://api.github.com/repos/lsst/qserv/assignees{/user}","branches_url":"https://api.github.com/repos/lsst/qserv/branches{/branch}","tags_url":"https://api.github.com/repos/lsst/qserv/tags","blobs_url":"https://api.github.com/repos/lsst/qserv/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/lsst/qserv/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/lsst/qserv/git/refs{/sha}","trees_url":"https://api.github.com/repos/lsst/qserv/git/trees{/sha}","statuses_url":"https://api.github.com/repos/lsst/qserv/statuses/{sha}","languages_url":"https://api.github.com/repos/lsst/qserv/languages","stargazers_url":"https://api.github.com/repos/lsst/qserv/stargazers","contributors_url":"https://api.github.com/repos/lsst/qserv/contributors","subscribers_url":"https://api.github.com/repos/lsst/qserv/subscribers","subscription_url":"https://api.github.com/repos/lsst/qserv/subscription","commits_url":"https://api.github.com/repos/lsst/qserv/commits{/sha}","git_commits_url":"https://api.github.com/repos/lsst/qserv/git/commits{/sha}","comments_url":"https://api.github.com/repos/lsst/qserv/comments{/number}","issue_comment_url":"https://api.github.com/repos/lsst/qserv/issues/comments{/number}","contents_url":"https://api.github.com/repos/lsst/qserv/contents/{+path}","compare_url":"https://api.github.com/repos/lsst/qserv/compare/{base}...{head}","merges_url":"https://api.github.com/repos/lsst/qserv/merges","archive_url":"https://api.github.com/repos/lsst/qserv/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/lsst/qserv/downloads","issues_url":"https://api.github.com/repos/lsst/qserv/issues{/number}","pulls_url":"https://api.github.com/repos/lsst/qserv/pulls{/number}","milestones_url":"https://api.github.com/repos/lsst/qserv/milestones{/number}","notifications_url":"https://api.github.com/repos/lsst/qserv/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/lsst/qserv/labels{/name}","releases_url":"https://api.github.com/repos/lsst/qserv/releases{/id}","created_at":"2014-08-16T07:50:36Z","updated_at":"2015-04-23T07:47:46Z","pushed_at":"2015-04-24T08:46:27Z","git_url":"git://github.com/lsst/qserv.git","ssh_url":"git@github.com:lsst/qserv.git","clone_url":"https://github.com/lsst/qserv.git","svn_url":"https://github.com/lsst/qserv","homepage":"http://dm.lsst.org/","size":19838,"stargazers_count":4,"watchers_count":4,"language":"C++","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":5,"mirror_url":null,"open_issues_count":11,"forks":5,"open_issues":11,"watchers":4,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/lsst/qserv/pulls/61"},"html":{"href":"https://github.com/lsst/qserv/pull/61"},"issue":{"href":"https://api.github.com/repos/lsst/qserv/issues/61"},"comments":{"href":"https://api.github.com/repos/lsst/qserv/issues/61/comments"},"review_comments":{"href":"https://api.github.com/repos/lsst/qserv/pulls/61/comments"},"review_comment":{"href":"https://api.github.com/repos/lsst/qserv/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/lsst/qserv/pulls/61/commits"},"statuses":{"href":"https://api.github.com/repos/lsst/qserv/statuses/55b5c932653131e836855618cbee123cb595a44e"}}}}