{"action":"created","comment":{"url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/comments/29684226","id":29684226,"diff_hunk":"@@ -0,0 +1,304 @@\n+#!/usr/bin/python\n+\n+import etcd\n+import json\n+from collections import defaultdict\n+from threading import Thread\n+from time import sleep\n+\n+from .constants import *\n+from .synchronization_fsm import SyncFSM\n+import urllib3\n+import logging\n+\n+_log = logging.getLogger(\"etcd_sync\")\n+\n+\n+class EtcdSynchronizer(object):\n+    PAUSE_BEFORE_RETRY = 30\n+\n+    def __init__(self, plugin, ip, etcd_ip=None, force_leave=False):\n+        self._fsm = SyncFSM(plugin, ip)\n+        self._ip = ip\n+        if etcd_ip:\n+            self._client = etcd.Client(etcd_ip, 4000)\n+        else:\n+            self._client = etcd.Client(ip, 4000)\n+        self._key = plugin.key()\n+        self._index = None\n+        self._last_cluster_view = None\n+        self._leaving_flag = False\n+        self._terminate_flag = False\n+        self.thread = Thread(target=self.main)\n+        self.force_leave = force_leave\n+\n+    def start_thread(self):\n+        self.thread.daemon = True\n+        self.thread.start()\n+\n+    def terminate(self):\n+        self._terminate_flag = True\n+        self.thread.join()\n+\n+    def main(self):\n+        # Continue looping while the FSM is running.\n+        while self._fsm.is_running():\n+            # This blocks on changes to the cluster in etcd.\n+            _log.debug(\"Waiting for state change from etcd\")\n+            cluster_view = self.read_from_etcd()\n+            if self._terminate_flag:\n+                break\n+            _log.debug(\"Got new state %s from etcd\" % cluster_view)\n+            cluster_state = self.calculate_cluster_state(cluster_view)\n+\n+            # This node can only leave the cluster if the cluster is in a stable\n+            # state. Check the leaving flag and the cluster state. If necessary,\n+            # set this node to WAITING_TO_LEAVE. Otherwise, kick the FSM.\n+            if self._leaving_flag and \\\n+                    (cluster_state == STABLE or\n+                     (self.force_leave and cluster_state == STABLE_WITH_ERRORS)):\n+                new_state = WAITING_TO_LEAVE\n+            else:\n+                local_state = self.calculate_local_state(cluster_view)\n+                new_state = self._fsm.next(local_state,\n+                                           cluster_state,\n+                                           cluster_view)\n+\n+            # If we have a new state, try and write it to etcd.\n+            if new_state:\n+                self.write_to_etcd(cluster_view, new_state)\n+            else:\n+                _log.debug(\"No state change\")\n+        self._fsm.quit()\n+\n+    def parse_cluster_view(self, view):\n+        try:\n+            return json.loads(view)\n+        except:\n+            return {}\n+\n+    # This node has been asked to leave the cluster. Check if the cluster is in\n+    # a stable state, in which case we can leave. Otherwise, set a flag and\n+    # leave at the next available opportunity.\n+    def leave_cluster(self):\n+        result = self._client.get(self._key)\n+        cluster_view = self.parse_cluster_view(result.value)\n+\n+        cluster_state = self.calculate_cluster_state(cluster_view)\n+\n+        if cluster_state == STABLE or \\\n+                (self.force_leave and cluster_state == STABLE_WITH_ERRORS):\n+            self.write_to_etcd(cluster_view, WAITING_TO_LEAVE)\n+        else:\n+            self._leaving_flag = True\n+\n+    def mark_node_failed(self):\n+        result = self._client.get(self._key)\n+        cluster_view = self.parse_cluster_view(result.value)\n+\n+        self.write_to_etcd(cluster_view, ERROR)\n+\n+    # Calculate the state of the cluster based on the state of all the nodes in\n+    # the cluster.\n+    def calculate_cluster_state(self, cluster_view):\n+        # Create a default dictionary. The default value of any key is 0.\n+        node_state_counts = defaultdict(int)\n+        node_states = cluster_view.values()\n+        node_count = 0\n+        error_count = 0\n+\n+        # Count the number of nodes in each state. This will make working out\n+        # the state of the cluster below easier.\n+        for state in node_states:\n+            node_state_counts[state] += 1\n+\n+            # Count the total number of nodes in the cluster. Ignore nodes in\n+            # ERROR state.\n+            if state != ERROR:\n+                node_count += 1\n+            else:\n+                error_count += 1\n+\n+        def state_check(zeroOrMore=None, oneOrMore=None):\n+            if not zeroOrMore:\n+                zeroOrMore = []\n+            if not oneOrMore:\n+                oneOrMore = []\n+\n+            states_to_sum = zeroOrMore + oneOrMore\n+\n+            total = sum([node_state_counts[i] for i in states_to_sum])\n+            has_minimum = sum([node_state_counts[i] for i in oneOrMore]) > 0\n+\n+            return has_minimum and (total == node_count)\n+\n+        if node_count == 0 and error_count == 0:\n+            return EMPTY\n+        elif node_state_counts[NORMAL] == node_count and error_count == 0:\n+            return STABLE\n+        elif node_state_counts[NORMAL] == node_count:\n+            return STABLE_WITH_ERRORS\n+        elif state_check(oneOrMore=[NORMAL, WAITING_TO_JOIN]):\n+            return JOIN_PENDING\n+        elif state_check(oneOrMore=[NORMAL, JOINING],\n+                         zeroOrMore=[NORMAL_ACKNOWLEDGED_CHANGE,\n+                                     JOINING_ACKNOWLEDGED_CHANGE]):\n+            return STARTED_JOINING\n+        elif state_check(oneOrMore=[NORMAL_ACKNOWLEDGED_CHANGE,\n+                                    JOINING_ACKNOWLEDGED_CHANGE],\n+                         zeroOrMore=[NORMAL_CONFIG_CHANGED,\n+                                     JOINING_CONFIG_CHANGED]):\n+            return JOINING_CONFIG_CHANGING\n+        elif state_check(oneOrMore=[NORMAL_CONFIG_CHANGED,\n+                                    JOINING_CONFIG_CHANGED],\n+                         zeroOrMore=[NORMAL]):\n+            return JOINING_RESYNCING\n+        elif state_check(oneOrMore=[NORMAL, WAITING_TO_LEAVE]):\n+            return LEAVE_PENDING\n+        elif state_check(oneOrMore=[NORMAL, LEAVING],\n+                         zeroOrMore=[NORMAL_ACKNOWLEDGED_CHANGE,\n+                                     LEAVING_ACKNOWLEDGED_CHANGE]):\n+            return STARTED_LEAVING\n+        elif state_check(oneOrMore=[NORMAL_ACKNOWLEDGED_CHANGE,\n+                                    LEAVING_ACKNOWLEDGED_CHANGE],\n+                         zeroOrMore=[NORMAL_CONFIG_CHANGED,\n+                                     LEAVING_CONFIG_CHANGED]):\n+            return LEAVING_CONFIG_CHANGING\n+        elif state_check(oneOrMore=[NORMAL_CONFIG_CHANGED,\n+                                    LEAVING_CONFIG_CHANGED],\n+                         zeroOrMore=[NORMAL, FINISHED]):\n+            return LEAVING_RESYNCING\n+        elif state_check(oneOrMore=[NORMAL, FINISHED]):\n+            return FINISHED_LEAVING\n+        else:\n+            # Cluster in unexpected state.\n+            return INVALID_CLUSTER_STATE\n+\n+    # Returns the local node's state in the cluster, and None if the local node\n+    # is not in the cluster.\n+    def calculate_local_state(self, cluster_view):\n+        return cluster_view.get(self._ip)\n+\n+    # Read the state of the cluster from etcd.\n+    def read_from_etcd(self):\n+        cluster_view = {}\n+\n+        try:\n+            result = self._client.get(self._key)\n+            cluster_view = self.parse_cluster_view(result.value)\n+\n+            # If the cluster view hasn't changed since we last saw it, then\n+            # wait for it to change before doing anything else.\n+            _log.info(\"Read cluster view {} from etcd, \"\n+                      \"comparing to last cluster view {}\".format(\n+                          cluster_view,\n+                          self._last_cluster_view))\n+            if cluster_view == self._last_cluster_view:\n+                while not self._terminate_flag:\n+                    try:\n+                        result = self._client.watch(self._key,\n+                                                    index=result.modifiedIndex+1,\n+                                                    timeout=5,\n+                                                    recursive=False)\n+                        break\n+                    except urllib3.exceptions.TimeoutError:\n+                        # Timeouts after 5 seconds are expected - just check if\n+                        # we're termninating and continue\n+                        pass\n+                    except ValueError:\n+                        # The index isn't valid to watch on, probably because\n+                        # there has been a snapshot between the get and the\n+                        # watch. Just start the read again.\n+                        _log.info(\"etcd index {} is invalid, retrying\".format(\n+                            result.modifiedIndex+1))","path":"src/metaswitch/clearwater/cluster_manager/etcd_synchronizer.py","position":213,"original_position":213,"commit_id":"b579193db8105077744675b3d4998e0e8421d4eb","original_commit_id":"b579193db8105077744675b3d4998e0e8421d4eb","user":{"login":"eleanor-merry","id":5425325,"avatar_url":"https://avatars.githubusercontent.com/u/5425325?v=3","gravatar_id":"","url":"https://api.github.com/users/eleanor-merry","html_url":"https://github.com/eleanor-merry","followers_url":"https://api.github.com/users/eleanor-merry/followers","following_url":"https://api.github.com/users/eleanor-merry/following{/other_user}","gists_url":"https://api.github.com/users/eleanor-merry/gists{/gist_id}","starred_url":"https://api.github.com/users/eleanor-merry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/eleanor-merry/subscriptions","organizations_url":"https://api.github.com/users/eleanor-merry/orgs","repos_url":"https://api.github.com/users/eleanor-merry/repos","events_url":"https://api.github.com/users/eleanor-merry/events{/privacy}","received_events_url":"https://api.github.com/users/eleanor-merry/received_events","type":"User","site_admin":false},"body":"Is there any other reason this exception could be thrown? I'm worried about this tight looping","created_at":"2015-05-05T16:06:57Z","updated_at":"2015-05-05T16:06:57Z","html_url":"https://github.com/Metaswitch/clearwater-etcd/pull/10#discussion_r29684226","pull_request_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10","_links":{"self":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/comments/29684226"},"html":{"href":"https://github.com/Metaswitch/clearwater-etcd/pull/10#discussion_r29684226"},"pull_request":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10"}}},"pull_request":{"url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10","id":34708813,"html_url":"https://github.com/Metaswitch/clearwater-etcd/pull/10","diff_url":"https://github.com/Metaswitch/clearwater-etcd/pull/10.diff","patch_url":"https://github.com/Metaswitch/clearwater-etcd/pull/10.patch","issue_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/10","number":10,"state":"open","locked":false,"title":"[Reviewer: Ellie] Cluster manager code","user":{"login":"rkd-msw","id":4363492,"avatar_url":"https://avatars.githubusercontent.com/u/4363492?v=3","gravatar_id":"","url":"https://api.github.com/users/rkd-msw","html_url":"https://github.com/rkd-msw","followers_url":"https://api.github.com/users/rkd-msw/followers","following_url":"https://api.github.com/users/rkd-msw/following{/other_user}","gists_url":"https://api.github.com/users/rkd-msw/gists{/gist_id}","starred_url":"https://api.github.com/users/rkd-msw/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rkd-msw/subscriptions","organizations_url":"https://api.github.com/users/rkd-msw/orgs","repos_url":"https://api.github.com/users/rkd-msw/repos","events_url":"https://api.github.com/users/rkd-msw/events{/privacy}","received_events_url":"https://api.github.com/users/rkd-msw/received_events","type":"User","site_admin":false},"body":"Key things:\r\n\r\n* the main code files are etcd_synchronizer.py (which talks to etcd) and synchronizer_fsm.py (a state machine that works out what to do next based on the current etcd states)\r\n* these files have about 96% code coverage in UT, excepting some INVALID_CLUSTER_STATE branches that we should never hit (coverage is about 80% overall, due to main.cpp and plugin_utils.cpp being 0%)\r\n* you can run the UTs with SLOW=T (to enable some slow tests, that introduce random pauses to shake out race conditions) or ETCD_IP=10.0.0.1 (to run against a real etcd node)\r\n* there are various diagnostics/recovery/upgrade scripts in clearwater-cluster-manager.root/usr/share/clearwater/cluster_manager/scripts:\r\n   * dump_etcd_state, which prints out the current state of an etcd key\r\n   * force_etcd_state, which loads a JSON file into an etcd key\r\n   * mark_node_failed, which forces a dead node out of the cluster\r\n  * load_from_cluster_settings, for upgrade, which creates the right etcd key from an existing /etc/clearwater/cluster_settings","created_at":"2015-05-05T09:43:15Z","updated_at":"2015-05-05T16:06:57Z","closed_at":null,"merged_at":null,"merge_commit_sha":"66faf7f76e589f6d3882c6bdc08376b476d417aa","assignee":{"login":"eleanor-merry","id":5425325,"avatar_url":"https://avatars.githubusercontent.com/u/5425325?v=3","gravatar_id":"","url":"https://api.github.com/users/eleanor-merry","html_url":"https://github.com/eleanor-merry","followers_url":"https://api.github.com/users/eleanor-merry/followers","following_url":"https://api.github.com/users/eleanor-merry/following{/other_user}","gists_url":"https://api.github.com/users/eleanor-merry/gists{/gist_id}","starred_url":"https://api.github.com/users/eleanor-merry/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/eleanor-merry/subscriptions","organizations_url":"https://api.github.com/users/eleanor-merry/orgs","repos_url":"https://api.github.com/users/eleanor-merry/repos","events_url":"https://api.github.com/users/eleanor-merry/events{/privacy}","received_events_url":"https://api.github.com/users/eleanor-merry/received_events","type":"User","site_admin":false},"milestone":null,"commits_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10/commits","review_comments_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10/comments","review_comment_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/comments{/number}","comments_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/10/comments","statuses_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/statuses/b579193db8105077744675b3d4998e0e8421d4eb","head":{"label":"Metaswitch:ready_for_review","ref":"ready_for_review","sha":"b579193db8105077744675b3d4998e0e8421d4eb","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":34358654,"name":"clearwater-etcd","full_name":"Metaswitch/clearwater-etcd","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/clearwater-etcd","description":"Wrappers around etcd for ease of integration into Project Clearwater","fork":false,"url":"https://api.github.com/repos/Metaswitch/clearwater-etcd","forks_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/forks","keys_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/teams","hooks_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/hooks","issue_events_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/events{/number}","events_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/events","assignees_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/assignees{/user}","branches_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/branches{/branch}","tags_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/tags","blobs_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/refs{/sha}","trees_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/statuses/{sha}","languages_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/languages","stargazers_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/stargazers","contributors_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/contributors","subscribers_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/subscribers","subscription_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/subscription","commits_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/commits{/sha}","git_commits_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/commits{/sha}","comments_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/comments{/number}","issue_comment_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/comments{/number}","contents_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/contents/{+path}","compare_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/merges","archive_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/downloads","issues_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues{/number}","pulls_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls{/number}","milestones_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/milestones{/number}","notifications_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/labels{/name}","releases_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/releases{/id}","created_at":"2015-04-22T00:03:08Z","updated_at":"2015-04-29T16:06:07Z","pushed_at":"2015-05-05T12:46:48Z","git_url":"git://github.com/Metaswitch/clearwater-etcd.git","ssh_url":"git@github.com:Metaswitch/clearwater-etcd.git","clone_url":"https://github.com/Metaswitch/clearwater-etcd.git","svn_url":"https://github.com/Metaswitch/clearwater-etcd","homepage":null,"size":3692,"stargazers_count":0,"watchers_count":0,"language":"Makefile","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":5,"forks":0,"open_issues":5,"watchers":0,"default_branch":"master"}},"base":{"label":"Metaswitch:master","ref":"master","sha":"40a966308762b2605b2d95aae2065025f2c12e7d","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":34358654,"name":"clearwater-etcd","full_name":"Metaswitch/clearwater-etcd","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/clearwater-etcd","description":"Wrappers around etcd for ease of integration into Project Clearwater","fork":false,"url":"https://api.github.com/repos/Metaswitch/clearwater-etcd","forks_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/forks","keys_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/teams","hooks_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/hooks","issue_events_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/events{/number}","events_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/events","assignees_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/assignees{/user}","branches_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/branches{/branch}","tags_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/tags","blobs_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/refs{/sha}","trees_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/statuses/{sha}","languages_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/languages","stargazers_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/stargazers","contributors_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/contributors","subscribers_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/subscribers","subscription_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/subscription","commits_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/commits{/sha}","git_commits_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/git/commits{/sha}","comments_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/comments{/number}","issue_comment_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/comments{/number}","contents_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/contents/{+path}","compare_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/merges","archive_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/downloads","issues_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues{/number}","pulls_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls{/number}","milestones_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/milestones{/number}","notifications_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/labels{/name}","releases_url":"https://api.github.com/repos/Metaswitch/clearwater-etcd/releases{/id}","created_at":"2015-04-22T00:03:08Z","updated_at":"2015-04-29T16:06:07Z","pushed_at":"2015-05-05T12:46:48Z","git_url":"git://github.com/Metaswitch/clearwater-etcd.git","ssh_url":"git@github.com:Metaswitch/clearwater-etcd.git","clone_url":"https://github.com/Metaswitch/clearwater-etcd.git","svn_url":"https://github.com/Metaswitch/clearwater-etcd","homepage":null,"size":3692,"stargazers_count":0,"watchers_count":0,"language":"Makefile","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":5,"forks":0,"open_issues":5,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10"},"html":{"href":"https://github.com/Metaswitch/clearwater-etcd/pull/10"},"issue":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/10"},"comments":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/issues/10/comments"},"review_comments":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10/comments"},"review_comment":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/pulls/10/commits"},"statuses":{"href":"https://api.github.com/repos/Metaswitch/clearwater-etcd/statuses/b579193db8105077744675b3d4998e0e8421d4eb"}}}}