Giuseppe Lavagetto has submitted this change and it was merged.
Change subject: Allow the etcd driver to handle deleted or inactive nodes
......................................................................
Allow the etcd driver to handle deleted or inactive nodes
As it was designed now, it was practically impossible to remove a node
from a running pybal instance.
Bug: T125397
Change-Id: I909efea01a33b842e4e0299264c240423b59d952
---
M pybal/etcd.py
M pybal/test/test_config.py
A pybal/test/test_etcd.py
3 files changed, 154 insertions(+), 6 deletions(-)
Approvals:
Giuseppe Lavagetto: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pybal/etcd.py b/pybal/etcd.py
index c0f3f20..7bb4268 100644
--- a/pybal/etcd.py
+++ b/pybal/etcd.py
@@ -25,8 +25,13 @@
def decode_node(node):
"""Decode an individual node from an etcd response."""
key = node['key'].rsplit('/', 1)[-1]
+ # handle deletions by returning the key and a value of None
+ if 'value' not in node:
+ return key, None
value = json.loads(node['value'])
pooled = value.pop('pooled', None)
+ if pooled == 'inactive':
+ return key, None
if pooled is not None:
value['enabled'] = pooled == 'yes'
return key, value
@@ -141,7 +146,18 @@
else:
self.waitIndex = etcdIdx + 1
config = copy.deepcopy(self.lastConfig)
- config.update(decode_etcd_data(update))
+
+ # Read new data
+ new_data = decode_etcd_data(update)
+ config.update(new_data)
+
+ # Remove deleted/inactive nodes
+ to_remove = [k for k, v in new_data.items() if v is None]
+ for k in to_remove:
+ if k in config:
+ del config[k]
+
+ # Now update pybal config
if config != self.lastConfig:
self.coordinator.onConfigUpdate(copy.deepcopy(config))
self.lastConfig = config
diff --git a/pybal/test/test_config.py b/pybal/test/test_config.py
index e32e5aa..c604ff3 100644
--- a/pybal/test/test_config.py
+++ b/pybal/test/test_config.py
@@ -6,15 +6,12 @@
This module contains tests for `pybal.config`.
"""
-import json
-import os
-import tempfile
-
-from twisted.python.failure import Failure
import mock
+import json
import pybal
import pybal.config
+import pybal.etcd
from .fixtures import PyBalTestCase, MockClientGetPage
diff --git a/pybal/test/test_etcd.py b/pybal/test/test_etcd.py
new file mode 100644
index 0000000..09418d1
--- /dev/null
+++ b/pybal/test/test_etcd.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+"""
+ PyBal unit tests
+ ~~~~~~~~~~~~~~~~
+
+ This module contains tests for `pybal.etcd`.
+
+"""
+
+import copy
+import mock
+import urlparse
+
+import pybal
+import pybal.config
+import pybal.etcd
+from .fixtures import PyBalTestCase
+
+
+
+class EtcdConfigurationObserverTestCase(PyBalTestCase):
+ def setUp(self):
+ super(EtcdConfigurationObserverTestCase, self).setUp()
+ self.observer = self.getObserver()
+
+ def getObserver(self, url='etcd://example.com/config/text'):
+ return pybal.etcd.EtcdConfigurationObserver(
+ self.coordinator, url)
+
+ def testParseConfigUrl(self):
+ """Test initialization"""
+ obs = self.getObserver()
+ self.assertEquals(obs.host, 'example.com')
+ self.assertEquals(obs.port, 2379)
+ self.assertEquals(obs.key, '/config/text')
+ self.assertEquals(obs.waitIndex, None)
+
+ def testGetPath(self):
+ self.assertEquals(self.observer.getPath(),
+ '/v2/keys/config/text?recursive=true')
+ self.observer.waitIndex = 4
+ url = urlparse.urlparse(self.observer.getPath())
+ self.assertEquals(url.path, '/v2/keys/config/text')
+ self.assertDictEqual(dict(urlparse.parse_qsl(url.query)),
+ {'wait': 'true', 'waitIndex': '4',
+ 'recursive': 'true' })
+
+ def testGetMaxModifiedIndex(self):
+ nodes = {
+ 'modifiedIndex': 40,
+ 'nodes': [
+ {'modifiedIndex': 4},
+ {'modifiedIndex': 3},
+ {'modifiedIndex': 15},
+ ]
+ }
+ self.assertEquals(40, self.observer.getMaxModifiedIndex(nodes))
+
+ def testOnUpdate(self):
+ create = {
+ "action":"set",
+ "node":
+ {
+ "key": "/testdir/1",
+ "value": "{\"pooled\": \"yes\", \"weight\": 10}",
+ "modifiedIndex": 11,
+ "createdIndex": 11,
+ }
+ }
+ create_another = copy.deepcopy(create)
+ create_another['node']['key'] = "/testdir/2"
+ depool = {
+ "action": "set",
+ "node": {
+ "key": "/testdir/1",
+ "value": "{\"pooled\": \"no\", \"weight\": 10}",
+ "modifiedIndex": 12,
+ "createdIndex": 12
+ },
+ "prevNode": {
+ "key": "/testdir/1",
+ "value": "{\"pooled\": \"yes\", \"weight\": 10}",
+ "modifiedIndex": 11,
+ "createdIndex": 11
+ }
+ }
+ inactive = copy.deepcopy(depool)
+ inactive['node']['value'] = "{\"pooled\": \"inactive\", \"weight\":
10}"
+ delete = {
+ "action": "delete",
+ "node": {
+ "key": "/testdir/1",
+ "modifiedIndex": 13,
+ "createdIndex": 12
+ },
+ "prevNode": {
+ "key": "/testdir/1",
+ "value": "{\"pooled\": \"no\", \"weight\": 10}",
+ "modifiedIndex": 12,
+ "createdIndex": 12
+ }
+ }
+ self.observer.coordinator.onConfigUpdate = mock.MagicMock()
+ self.observer.lastConfig = {}
+ # Add a node
+ self.observer.onUpdate(create, 0)
+ self.observer.coordinator.onConfigUpdate.assert_called_with({'1':
{'enabled': True, u'weight': 10}})
+ # Add another one
+ self.observer.onUpdate(create_another, 11)
+ self.observer.coordinator.onConfigUpdate.assert_called_with(
+ {'1': {'enabled': True, u'weight': 10},
+ '2': {'enabled': True, u'weight': 10}}
+ )
+
+ # Depool a server
+ self.observer.onUpdate(depool, 12)
+ self.observer.coordinator.onConfigUpdate.assert_called_with(
+ {'1': {'enabled': False, u'weight': 10},
+ '2': {'enabled': True, u'weight': 10}}
+ )
+
+ # Set it to inactive
+ self.observer.onUpdate(inactive, 12)
+ self.observer.coordinator.onConfigUpdate.assert_called_with(
+ {'2': {'enabled': True, u'weight': 10}}
+ )
+
+ # repool it
+ self.observer.onUpdate(create, 11)
+
+ # Delete a server
+ self.observer.onUpdate(delete, 13)
+ self.observer.coordinator.onConfigUpdate.assert_called_with(
+ {'2': {'enabled': True, u'weight': 10}}
+ )
--
To view, visit https://gerrit.wikimedia.org/r/268361
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I909efea01a33b842e4e0299264c240423b59d952
Gerrit-PatchSet: 3
Gerrit-Project: operations/debs/pybal
Gerrit-Branch: master
Gerrit-Owner: Giuseppe Lavagetto <[email protected]>
Gerrit-Reviewer: Giuseppe Lavagetto <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits