{"action":"created","comment":{"url":"https://api.github.com/repos/Metaswitch/calico/pulls/comments/29239440","id":29239440,"diff_hunk":"@@ -46,166 +46,299 @@\n              \"crit\":      logging.CRITICAL,\n              \"critical\":  logging.CRITICAL}\n \n+# Sources of a configuration parameter. The order is highest-priority first.\n+DEFAULT = \"Default\"\n+ENV = \"Environment variable\"\n+FILE = \"Configuration file\"\n+GLOBAL_ETCD = \"Global etcd configuration\"\n+LOCAL_ETCD = \"Host specific etcd configuration\"\n+DEFAULT_SOURCES = [ ENV, FILE, GLOBAL_ETCD, LOCAL_ETCD ]\n \n class ConfigException(Exception):\n-    def __init__(self, message, source):\n+    def __init__(self, message, parameter):\n         super(ConfigException, self).__init__(message)\n         self.message = message\n-        self.source = source\n+        self.parameter = parameter\n \n     def __str__(self):\n-        return \"%s (data source : %s)\" % (self.message, self.source)\n+        return \"%s (value %r for %s (%s), read from %r)\" \\\n+            % (self.message,\n+               self.parameter.value,\n+               self.parameter.name,\n+               self.parameter.description,\n+               self.parameter.active_source)\n+\n+class ConfigParameter(object):\n+    \"\"\"\n+    A configuration parameter. This contains the following information.\n+    - The name of the field.\n+    - Where the location can validly be read from\n+    - The current value\n+    - Where the value was read from\n+    \"\"\"\n+    def __init__(self, name, description, default,\n+                 sources=DEFAULT_SOURCES, int=False):\n+        \"\"\"\n+        Create a configuration parameter.\n+        :param str description: Description for logging\n+        :param list sources: List of valid sources to try\n+        :param str default: Default value\n+        :param bool int: Integer value?\n+        \"\"\"\n+        self.description = description\n+        self.name = name\n+        self.sources = sources\n+        self.value = default\n+        self.active_source = None\n+        self.value_is_int = int\n+\n+    def set(self, value, source):\n+        \"\"\"\n+        Set a value of a parameter - unless already set.\n+        :param value: value\n+        :param source: source; for example \"Configuration file /etc/felix.cfg\"\n+        \"\"\"\n+        if self.active_source is None:\n+            log.debug(\"Read value %r for %s (%s) from %r\",\n+                      value,\n+                      self.name,\n+                      self.description,\n+                      source)\n+\n+            self.active_source = source\n+\n+            if self.value_is_int:\n+                self.value = value\n+                try:\n+                    self.value = int(value)\n+                except ValueError:\n+                    raise ConfigException(\"Field was not integer\",\n+                                          self)\n+            else:\n+                # Calling str in principle can throw an exception, but it's\n+                # hard to see how in practice, so don't catch and wrap.\n+                self.value = str(value)\n+        else:\n+            log.warning(\"Ignore %r value for %s (%s) - already set from %r\",\n+                        source,\n+                        self.name,\n+                        self.description,\n+                        self.active_source)\n+\n \n class Config(object):\n     def __init__(self, config_path):\n         \"\"\"\n-        Create a config.\n+        Create a config. This reads data from the following sources.\n+        - Environment variables\n+        - Configuration file\n+        - per-host etcd (/calico/vX/config)\n+        - global etcd (/calico/vX/host/<host>/config)\n         :raises EtcdException\n         \"\"\"\n-        self._KnownSections = set()\n-        self._KnownObjects  = set()\n+        self.parameters = {}\n+\n+        self.add_parameter(\"EtcdAddr\", \"Address and port for etcd\",\n+                           \"localhost:4001\", sources = [ENV, FILE])\n+        self.add_parameter(\"FelixHostname\", \"Felix compute host hostname\",\n+                           socket.gethostname(), sources = [ENV, FILE])\n+\n+        self.add_parameter(\"StartupCleanupDelay\", \"Delay before cleanup starts\",\n+                           30, int = True)\n+        self.add_parameter(\"MetadataAddr\", \"Metadata IP address or hostname\",\n+                           \"127.0.0.1\")\n+        self.add_parameter(\"MetadataPort\", \"Metadata Port\", 8775, int=True)\n+        self.add_parameter(\"InterfacePrefix\", \"Interface name prefix\", None)\n+        self.add_parameter(\"LogFilePath\",\n+                           \"Path to log file\", \"/var/log/calico/felix.log\")\n+        self.add_parameter(\"LogSeverityFile\",\n+                           \"Log severity for logging to file\", \"INFO\")\n+        self.add_parameter(\"LogSeveritySys\",\n+                           \"Log severity for logging to syslog\", \"ERROR\")\n+        self.add_parameter(\"LogSeverityScreen\",\n+                           \"Log severity for logging to screen\", \"ERROR\")\n+\n+        # Read the environment variables, then the configuration file.\n+        self._read_env_vars()\n+        self._read_cfg_file(config_path)\n+        self._finish_update(final=False)\n+\n+    def add_parameter(self, name, description, default, **kwargs):\n+        \"\"\"\n+        Put a parameter in the parameter dictionary.\n+        \"\"\"\n+        self.parameters[name] = ConfigParameter(\n+            name, description, default, **kwargs)\n+\n+    def _finish_update(self, final=False):\n+        \"\"\"\n+        Config has been completely read. Called twice - once after reading from\n+        environment and config file (so we should be able to access etcd), and\n+        once after reading from etcd (so we have all the config ready to go).\n+\n+        Responsible for :\n+        - storing the parameters in the relevant fields in the structure\n+        - validating the configuration is valid (for this stage in the process)\n+        - updating logging parameters (if this is the final call)\n+        :param final: Have we completed (rather than just read env and config file)\n+        \"\"\"\n+        self.ETCD_ADDR = self.parameters[\"EtcdAddr\"].value\n+        self.HOSTNAME = self.parameters[\"FelixHostname\"].value\n+        self.STARTUP_CLEANUP_DELAY = self.parameters[\"StartupCleanupDelay\"].value\n+        self.METADATA_IP = self.parameters[\"MetadataAddr\"].value\n+        self.METADATA_PORT = self.parameters[\"MetadataPort\"].value\n+        self.IFACE_PREFIX = self.parameters[\"InterfacePrefix\"].value\n+        self.LOGFILE = self.parameters[\"LogFilePath\"].value\n+        self.LOGLEVFILE = self.parameters[\"LogSeverityFile\"].value\n+        self.LOGLEVSYS = self.parameters[\"LogSeveritySys\"].value\n+        self.LOGLEVSCR = self.parameters[\"LogSeverityScreen\"].value\n+\n+        self._validate_cfg(final=final)\n+\n+        if final:\n+            # Update logging.\n+            common.complete_logging(self.LOGFILE,\n+                                    self.LOGLEVFILE,\n+                                    self.LOGLEVSYS,\n+                                    self.LOGLEVSCR)\n+\n+            # Log configuration - the whole lot of it.\n+            for name, parameter in self.parameters.iteritems():\n+                log.info(\"Parameter %s (%s) has value %r read from %s\",\n+                         name,\n+                         parameter.description,\n+                         parameter.value,\n+                         parameter.active_source)\n+\n+    def _read_env_vars(self):\n+        \"\"\"\n+        Read all of the variables from the environment.\n+        \"\"\"\n+        for name, parameter in self.parameters.iteritems():\n+            # All currently defined config parameters have ENV as a valid source.\n+            assert(ENV in parameter.sources)\n+            # ENV is the first source, so we can assert that using defaults.\n+            assert(parameter.active_source is None)\n+\n+            env_var = (\"FELIX_%s\" % name).upper()\n+            if env_var in os.environ:\n+                parameter.set(os.environ[env_var],\n+                              \"Environment variable %s\" % env_var)\n+\n+    def _read_cfg_file(self, config_file):\n+        parser = ConfigParser.ConfigParser()\n+        parser.read(config_file)\n+        cfg_dict = {}\n+\n+        # Build up the cfg dictionary from the file.\n+        for section in parser.sections():\n+            cfg_dict.update(dict(parser.items(section)))\n+\n+        source = \"Configuration file %s\" % config_file\n+\n+        for name, parameter in self.parameters.iteritems():\n+            # Config parameters are lower-cased by ConfigParser\n+            name = name.lower()\n+            if FILE in parameter.sources and name in cfg_dict:\n+                # This can validly be read from file.\n+                parameter.set(cfg_dict.pop(name), source)\n+        self._warn_unused_cfg(cfg_dict, source)\n+\n+    def report_etcd_config(self, host_dict, global_dict):\n+        \"\"\"\n+        Report back configuration parameters read from etcd.\n+        :param host_dict: Dictionary of etcd parameters\n+        :param global_dict: Dictionary of global parameters\n+        :raises ConfigException\n+        \"\"\"\n+        log.debug(\"Configuration reported from etcd\")\n+        for source in (LOCAL_ETCD, GLOBAL_ETCD):\n+            if source == LOCAL_ETCD:\n+                cfg_dict = host_dict\n+            else:\n+                cfg_dict = global_dict\n+\n+            for name, parameter in self.parameters.iteritems():\n+                if source in parameter.sources and name in cfg_dict:\n+                    parameter.set(cfg_dict.pop(name), source)\n \n-        self._config_path = config_path\n-        self._items = {}\n+            self._warn_unused_cfg(cfg_dict, source)\n \n-        self.read_cfg_file(config_path)\n+        self._finish_update(final=True)","path":"calico/felix/config.py","position":242,"original_position":242,"commit_id":"be67c980bd932666fce8780a76bc983973e7a14d","original_commit_id":"be67c980bd932666fce8780a76bc983973e7a14d","user":{"login":"Lukasa","id":1382556,"avatar_url":"https://avatars.githubusercontent.com/u/1382556?v=3","gravatar_id":"","url":"https://api.github.com/users/Lukasa","html_url":"https://github.com/Lukasa","followers_url":"https://api.github.com/users/Lukasa/followers","following_url":"https://api.github.com/users/Lukasa/following{/other_user}","gists_url":"https://api.github.com/users/Lukasa/gists{/gist_id}","starred_url":"https://api.github.com/users/Lukasa/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lukasa/subscriptions","organizations_url":"https://api.github.com/users/Lukasa/orgs","repos_url":"https://api.github.com/users/Lukasa/repos","events_url":"https://api.github.com/users/Lukasa/events{/privacy}","received_events_url":"https://api.github.com/users/Lukasa/received_events","type":"User","site_admin":false},"body":"This function has a bad name and a bad docstring. \r\n\r\n- \"Report back configuration parameters read from etcd\": report them back to where?\r\n- Can I run this function multiple times? It calls something called `_finish_update`: what does that have to do with anything?","created_at":"2015-04-28T13:03:03Z","updated_at":"2015-04-28T13:03:03Z","html_url":"https://github.com/Metaswitch/calico/pull/472#discussion_r29239440","pull_request_url":"https://api.github.com/repos/Metaswitch/calico/pulls/472","_links":{"self":{"href":"https://api.github.com/repos/Metaswitch/calico/pulls/comments/29239440"},"html":{"href":"https://github.com/Metaswitch/calico/pull/472#discussion_r29239440"},"pull_request":{"href":"https://api.github.com/repos/Metaswitch/calico/pulls/472"}}},"pull_request":{"url":"https://api.github.com/repos/Metaswitch/calico/pulls/472","id":34253826,"html_url":"https://github.com/Metaswitch/calico/pull/472","diff_url":"https://github.com/Metaswitch/calico/pull/472.diff","patch_url":"https://github.com/Metaswitch/calico/pull/472.patch","issue_url":"https://api.github.com/repos/Metaswitch/calico/issues/472","number":472,"state":"open","locked":false,"title":"Allow for config to be read from config files.","user":{"login":"plwhite","id":8448749,"avatar_url":"https://avatars.githubusercontent.com/u/8448749?v=3","gravatar_id":"","url":"https://api.github.com/users/plwhite","html_url":"https://github.com/plwhite","followers_url":"https://api.github.com/users/plwhite/followers","following_url":"https://api.github.com/users/plwhite/following{/other_user}","gists_url":"https://api.github.com/users/plwhite/gists{/gist_id}","starred_url":"https://api.github.com/users/plwhite/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/plwhite/subscriptions","organizations_url":"https://api.github.com/users/plwhite/orgs","repos_url":"https://api.github.com/users/plwhite/repos","events_url":"https://api.github.com/users/plwhite/events{/privacy}","received_events_url":"https://api.github.com/users/plwhite/received_events","type":"User","site_admin":false},"body":"Logic is to take config from environment variables, config files, etcd per-host config, etcd global config in that order.\r\n\r\nResolves issue #407.","created_at":"2015-04-28T10:43:04Z","updated_at":"2015-04-28T13:03:03Z","closed_at":null,"merged_at":null,"merge_commit_sha":"bda4e1509f2a80dc1cd3d2874c808cb12aac6d65","assignee":{"login":"Lukasa","id":1382556,"avatar_url":"https://avatars.githubusercontent.com/u/1382556?v=3","gravatar_id":"","url":"https://api.github.com/users/Lukasa","html_url":"https://github.com/Lukasa","followers_url":"https://api.github.com/users/Lukasa/followers","following_url":"https://api.github.com/users/Lukasa/following{/other_user}","gists_url":"https://api.github.com/users/Lukasa/gists{/gist_id}","starred_url":"https://api.github.com/users/Lukasa/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lukasa/subscriptions","organizations_url":"https://api.github.com/users/Lukasa/orgs","repos_url":"https://api.github.com/users/Lukasa/repos","events_url":"https://api.github.com/users/Lukasa/events{/privacy}","received_events_url":"https://api.github.com/users/Lukasa/received_events","type":"User","site_admin":false},"milestone":{"url":"https://api.github.com/repos/Metaswitch/calico/milestones/4","html_url":"https://github.com/Metaswitch/calico/milestones/0.18","labels_url":"https://api.github.com/repos/Metaswitch/calico/milestones/4/labels","id":1075614,"number":4,"title":"0.18","description":"","creator":{"login":"Lukasa","id":1382556,"avatar_url":"https://avatars.githubusercontent.com/u/1382556?v=3","gravatar_id":"","url":"https://api.github.com/users/Lukasa","html_url":"https://github.com/Lukasa","followers_url":"https://api.github.com/users/Lukasa/followers","following_url":"https://api.github.com/users/Lukasa/following{/other_user}","gists_url":"https://api.github.com/users/Lukasa/gists{/gist_id}","starred_url":"https://api.github.com/users/Lukasa/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Lukasa/subscriptions","organizations_url":"https://api.github.com/users/Lukasa/orgs","repos_url":"https://api.github.com/users/Lukasa/repos","events_url":"https://api.github.com/users/Lukasa/events{/privacy}","received_events_url":"https://api.github.com/users/Lukasa/received_events","type":"User","site_admin":false},"open_issues":14,"closed_issues":3,"state":"open","created_at":"2015-04-20T16:37:46Z","updated_at":"2015-04-28T10:44:02Z","due_on":"2015-05-03T23:00:00Z","closed_at":null},"commits_url":"https://api.github.com/repos/Metaswitch/calico/pulls/472/commits","review_comments_url":"https://api.github.com/repos/Metaswitch/calico/pulls/472/comments","review_comment_url":"https://api.github.com/repos/Metaswitch/calico/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Metaswitch/calico/issues/472/comments","statuses_url":"https://api.github.com/repos/Metaswitch/calico/statuses/be67c980bd932666fce8780a76bc983973e7a14d","head":{"label":"Metaswitch:isssue407config","ref":"isssue407config","sha":"be67c980bd932666fce8780a76bc983973e7a14d","user":{"login":"Metaswitch","id":453759,"avatar_url":"https://avatars.githubusercontent.com/u/453759?v=3","gravatar_id":"","url":"https://api.github.com/users/Metaswitch","html_url":"https://github.com/Metaswitch","followers_url":"https://api.github.com/users/Metaswitch/followers","following_url":"https://api.github.com/users/Metaswitch/following{/other_user}","gists_url":"https://api.github.com/users/Metaswitch/gists{/gist_id}","starred_url":"https://api.github.com/users/Metaswitch/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Metaswitch/subscriptions","organizations_url":"https://api.github.com/users/Metaswitch/orgs","repos_url":"https://api.github.com/users/Metaswitch/repos","events_url":"https://api.github.com/users/Metaswitch/events{/privacy}","received_events_url":"https://api.github.com/users/Metaswitch/received_events","type":"Organization","site_admin":false},"repo":{"id":21242831,"name":"calico","full_name":"Metaswitch/calico","owner":{"login":"Metaswitch","id":453759,"avatar_url":"https://avatars.githubusercontent.com/u/453759?v=3","gravatar_id":"","url":"https://api.github.com/users/Metaswitch","html_url":"https://github.com/Metaswitch","followers_url":"https://api.github.com/users/Metaswitch/followers","following_url":"https://api.github.com/users/Metaswitch/following{/other_user}","gists_url":"https://api.github.com/users/Metaswitch/gists{/gist_id}","starred_url":"https://api.github.com/users/Metaswitch/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Metaswitch/subscriptions","organizations_url":"https://api.github.com/users/Metaswitch/orgs","repos_url":"https://api.github.com/users/Metaswitch/repos","events_url":"https://api.github.com/users/Metaswitch/events{/privacy}","received_events_url":"https://api.github.com/users/Metaswitch/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Metaswitch/calico","description":"Project Calico core repository.","fork":false,"url":"https://api.github.com/repos/Metaswitch/calico","forks_url":"https://api.github.com/repos/Metaswitch/calico/forks","keys_url":"https://api.github.com/repos/Metaswitch/calico/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Metaswitch/calico/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Metaswitch/calico/teams","hooks_url":"https://api.github.com/repos/Metaswitch/calico/hooks","issue_events_url":"https://api.github.com/repos/Metaswitch/calico/issues/events{/number}","events_url":"https://api.github.com/repos/Metaswitch/calico/events","assignees_url":"https://api.github.com/repos/Metaswitch/calico/assignees{/user}","branches_url":"https://api.github.com/repos/Metaswitch/calico/branches{/branch}","tags_url":"https://api.github.com/repos/Metaswitch/calico/tags","blobs_url":"https://api.github.com/repos/Metaswitch/calico/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Metaswitch/calico/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Metaswitch/calico/git/refs{/sha}","trees_url":"https://api.github.com/repos/Metaswitch/calico/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Metaswitch/calico/statuses/{sha}","languages_url":"https://api.github.com/repos/Metaswitch/calico/languages","stargazers_url":"https://api.github.com/repos/Metaswitch/calico/stargazers","contributors_url":"https://api.github.com/repos/Metaswitch/calico/contributors","subscribers_url":"https://api.github.com/repos/Metaswitch/calico/subscribers","subscription_url":"https://api.github.com/repos/Metaswitch/calico/subscription","commits_url":"https://api.github.com/repos/Metaswitch/calico/commits{/sha}","git_commits_url":"https://api.github.com/repos/Metaswitch/calico/git/commits{/sha}","comments_url":"https://api.github.com/repos/Metaswitch/calico/comments{/number}","issue_comment_url":"https://api.github.com/repos/Metaswitch/calico/issues/comments{/number}","contents_url":"https://api.github.com/repos/Metaswitch/calico/contents/{+path}","compare_url":"https://api.github.com/repos/Metaswitch/calico/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Metaswitch/calico/merges","archive_url":"https://api.github.com/repos/Metaswitch/calico/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Metaswitch/calico/downloads","issues_url":"https://api.github.com/repos/Metaswitch/calico/issues{/number}","pulls_url":"https://api.github.com/repos/Metaswitch/calico/pulls{/number}","milestones_url":"https://api.github.com/repos/Metaswitch/calico/milestones{/number}","notifications_url":"https://api.github.com/repos/Metaswitch/calico/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Metaswitch/calico/labels{/name}","releases_url":"https://api.github.com/repos/Metaswitch/calico/releases{/id}","created_at":"2014-06-26T14:08:43Z","updated_at":"2015-04-28T09:16:26Z","pushed_at":"2015-04-28T10:43:05Z","git_url":"git://github.com/Metaswitch/calico.git","ssh_url":"git@github.com:Metaswitch/calico.git","clone_url":"https://github.com/Metaswitch/calico.git","svn_url":"https://github.com/Metaswitch/calico","homepage":"http://projectcalico.org/","size":10817,"stargazers_count":49,"watchers_count":49,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":13,"mirror_url":null,"open_issues_count":75,"forks":13,"open_issues":75,"watchers":49,"default_branch":"master"}},"base":{"label":"Metaswitch:master","ref":"master","sha":"ddbd0ec15a4481c12ad562fe30a59e2e2d5b1518","user":{"login":"Metaswitch","id":453759,"avatar_url":"https://avatars.githubusercontent.com/u/453759?v=3","gravatar_id":"","url":"https://api.github.com/users/Metaswitch","html_url":"https://github.com/Metaswitch","followers_url":"https://api.github.com/users/Metaswitch/followers","following_url":"https://api.github.com/users/Metaswitch/following{/other_user}","gists_url":"https://api.github.com/users/Metaswitch/gists{/gist_id}","starred_url":"https://api.github.com/users/Metaswitch/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Metaswitch/subscriptions","organizations_url":"https://api.github.com/users/Metaswitch/orgs","repos_url":"https://api.github.com/users/Metaswitch/repos","events_url":"https://api.github.com/users/Metaswitch/events{/privacy}","received_events_url":"https://api.github.com/users/Metaswitch/received_events","type":"Organization","site_admin":false},"repo":{"id":21242831,"name":"calico","full_name":"Metaswitch/calico","owner":{"login":"Metaswitch","id":453759,"avatar_url":"https://avatars.githubusercontent.com/u/453759?v=3","gravatar_id":"","url":"https://api.github.com/users/Metaswitch","html_url":"https://github.com/Metaswitch","followers_url":"https://api.github.com/users/Metaswitch/followers","following_url":"https://api.github.com/users/Metaswitch/following{/other_user}","gists_url":"https://api.github.com/users/Metaswitch/gists{/gist_id}","starred_url":"https://api.github.com/users/Metaswitch/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Metaswitch/subscriptions","organizations_url":"https://api.github.com/users/Metaswitch/orgs","repos_url":"https://api.github.com/users/Metaswitch/repos","events_url":"https://api.github.com/users/Metaswitch/events{/privacy}","received_events_url":"https://api.github.com/users/Metaswitch/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/Metaswitch/calico","description":"Project Calico core repository.","fork":false,"url":"https://api.github.com/repos/Metaswitch/calico","forks_url":"https://api.github.com/repos/Metaswitch/calico/forks","keys_url":"https://api.github.com/repos/Metaswitch/calico/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Metaswitch/calico/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Metaswitch/calico/teams","hooks_url":"https://api.github.com/repos/Metaswitch/calico/hooks","issue_events_url":"https://api.github.com/repos/Metaswitch/calico/issues/events{/number}","events_url":"https://api.github.com/repos/Metaswitch/calico/events","assignees_url":"https://api.github.com/repos/Metaswitch/calico/assignees{/user}","branches_url":"https://api.github.com/repos/Metaswitch/calico/branches{/branch}","tags_url":"https://api.github.com/repos/Metaswitch/calico/tags","blobs_url":"https://api.github.com/repos/Metaswitch/calico/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Metaswitch/calico/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Metaswitch/calico/git/refs{/sha}","trees_url":"https://api.github.com/repos/Metaswitch/calico/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Metaswitch/calico/statuses/{sha}","languages_url":"https://api.github.com/repos/Metaswitch/calico/languages","stargazers_url":"https://api.github.com/repos/Metaswitch/calico/stargazers","contributors_url":"https://api.github.com/repos/Metaswitch/calico/contributors","subscribers_url":"https://api.github.com/repos/Metaswitch/calico/subscribers","subscription_url":"https://api.github.com/repos/Metaswitch/calico/subscription","commits_url":"https://api.github.com/repos/Metaswitch/calico/commits{/sha}","git_commits_url":"https://api.github.com/repos/Metaswitch/calico/git/commits{/sha}","comments_url":"https://api.github.com/repos/Metaswitch/calico/comments{/number}","issue_comment_url":"https://api.github.com/repos/Metaswitch/calico/issues/comments{/number}","contents_url":"https://api.github.com/repos/Metaswitch/calico/contents/{+path}","compare_url":"https://api.github.com/repos/Metaswitch/calico/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Metaswitch/calico/merges","archive_url":"https://api.github.com/repos/Metaswitch/calico/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Metaswitch/calico/downloads","issues_url":"https://api.github.com/repos/Metaswitch/calico/issues{/number}","pulls_url":"https://api.github.com/repos/Metaswitch/calico/pulls{/number}","milestones_url":"https://api.github.com/repos/Metaswitch/calico/milestones{/number}","notifications_url":"https://api.github.com/repos/Metaswitch/calico/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Metaswitch/calico/labels{/name}","releases_url":"https://api.github.com/repos/Metaswitch/calico/releases{/id}","created_at":"2014-06-26T14:08:43Z","updated_at":"2015-04-28T09:16:26Z","pushed_at":"2015-04-28T10:43:05Z","git_url":"git://github.com/Metaswitch/calico.git","ssh_url":"git@github.com:Metaswitch/calico.git","clone_url":"https://github.com/Metaswitch/calico.git","svn_url":"https://github.com/Metaswitch/calico","homepage":"http://projectcalico.org/","size":10817,"stargazers_count":49,"watchers_count":49,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":13,"mirror_url":null,"open_issues_count":75,"forks":13,"open_issues":75,"watchers":49,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Metaswitch/calico/pulls/472"},"html":{"href":"https://github.com/Metaswitch/calico/pull/472"},"issue":{"href":"https://api.github.com/repos/Metaswitch/calico/issues/472"},"comments":{"href":"https://api.github.com/repos/Metaswitch/calico/issues/472/comments"},"review_comments":{"href":"https://api.github.com/repos/Metaswitch/calico/pulls/472/comments"},"review_comment":{"href":"https://api.github.com/repos/Metaswitch/calico/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Metaswitch/calico/pulls/472/commits"},"statuses":{"href":"https://api.github.com/repos/Metaswitch/calico/statuses/be67c980bd932666fce8780a76bc983973e7a14d"}}}}