Giuseppe Lavagetto has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/354509 )
Change subject: Add netlink-based Ipvsmanager implementation [WiP]
......................................................................
Add netlink-based Ipvsmanager implementation [WiP]
A new ipvs manager is implemented using the new FSM we added to pybal
and communicating directly with the kernel via the netlink interface.
Change-Id: I392694be6d0832e7d6fd02461b6608bdd6b922ac
---
A pybal/ipvs/manager.py
A pybal/ipvs/server.py
A pybal/ipvs/service.py
M pybal/test/fixtures.py
A pybal/test/test_ipvs_manager.py
A pybal/test/test_ipvs_server.py
A pybal/test/test_ipvs_service.py
M setup.py
8 files changed, 753 insertions(+), 2 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/operations/debs/pybal
refs/changes/09/354509/1
diff --git a/pybal/ipvs/manager.py b/pybal/ipvs/manager.py
new file mode 100644
index 0000000..0966760
--- /dev/null
+++ b/pybal/ipvs/manager.py
@@ -0,0 +1,84 @@
+from gnlpy.ipvs import IpvsClient
+import pybal.ipvs.service
+from pybal.ipvs import LVSService
+
+
+class NetlinkServiceManager(LVSService):
+ """Service manager that uses the netlink-based state machines"""
+
+ Debug = False
+
+ DryRun = False
+
+ def __init__(self, name, (protocol, ip, port, scheduler), configuration):
+ self.name = name
+ # Logical server state within pybal itself
+ self.servers = set()
+ # FSM to manage the state of the individual server
+ self.destinations = {}
+
+ if (protocol not in self.SVC_PROTOS or
+ scheduler not in self.SVC_SCHEDULERS):
+ raise ValueError('Invalid protocol or scheduler')
+
+ self.protocol = protocol
+ self.ip = ip
+ self.port = port
+ self.scheduler = scheduler
+
+ self.configuration = configuration
+ self.Debug = configuration.getboolean('debug', False)
+ self.DryRun = configuration.getboolean('dryrun', False)
+
+ self.nlClient = IpvsClient(verbose=self.Debug)
+
+ if self.configuration.getboolean('bgp', False):
+ from pybal import BGPFailover
+ # Add service ip to the BGP announcements
+ BGPFailover.addPrefix(self.ip)
+
+ self.fsm = pybal.ipvs.service.Service(self.nlClient, service)
+ self.createService()
+
+ def service(self):
+ """Returns a tuple (protocol, ip, port, scheduler) that
+ describes this LVS instance."""
+
+ return (self.protocol, self.ip, self.port, self.scheduler)
+
+ def createService(self):
+ """Initializes this LVS instance in LVS."""
+ d = self.fsm.toState('present')
+
+ def assignServers(self, newServers):
+ to_remove = self.servers - newServers
+ to_add = newServers
+ for server in to_remove:
+ self.removeServer(server)
+ for server in to_add:
+ self.addServer(server)
+
+ def _to_ipvs(self, server):
+ # TODO: check that server.host is always defined
+ if server.host not in self.destinations:
+ self.destinations[server.host] = pybal.ipvs.server.Server(
+ self.nlClient, server, self.fsm)
+ return self.destinations[server.host]
+
+ def removeServer(self, server):
+ def depool():
+ server.pooled = False
+ self.servers.remove(server) # May raise KeyError
+ ipvsserver = self._to_ipvs(server)
+ # For now this is blocking,
+ # might not be such in the future
+ d = ipvsserver.toState('unknown')
+ d.addCallback(depool)
+
+ def addServer(self, server):
+ def pool():
+ server.pooled = True
+
+ ipvsserver = self._to_ipvs(server)
+ d = ipvsserver.toState('present')
+ d.addCallback(pool)
diff --git a/pybal/ipvs/server.py b/pybal/ipvs/server.py
new file mode 100644
index 0000000..d13b1b7
--- /dev/null
+++ b/pybal/ipvs/server.py
@@ -0,0 +1,159 @@
+import socket
+
+from gnlpy import ipvs as netlink
+from pybal import fsm
+from pybal.ipvs import IpvsError
+from pybal.util import log
+
+
+class Server(netlink.Dest, fsm.FiniteStateMachine):
+
+ def __init__(self, client, server, service):
+ self.client = client
+ # This _will_ block, which is acceptable since we're
+ # in a blocking part of the code
+ self.ip_ = server.ip or socket.gethostbyname(server.host)
+ # Server desired weight
+ self.weight_ = server.weight or 1
+ self.port_ = server.port
+ self.fwd_method_ = netlink.IPVS_ROUTING
+ self.validate()
+ self.service = service
+ fsm.FiniteStateMachine.__init__(self)
+ self.setupFSM()
+ self.currentState = self.actualState
+
+ def validate(self):
+ # Fix incomplete validation in gnlpy
+ super(Server, self).validate()
+ assert isinstance(self.port_, int)
+
+ def setupFSM(self):
+ # S0: The IPVS state is unknown / has been removed
+ self.addState(fsm.State('unknown'))
+ # S1: The server is registered, but with weight 0
+ self.addState(fsm.State('drained'))
+ # S2: The server is registered with its desired weight
+ self.addState(fsm.State('up'))
+ # S3: The server is registered with a wrong weight
+ self.addState(fsm.State('refresh'))
+ # S0 => S2
+ # Add a server with its desired final weight
+ self.addTransition(self.states['unknown'],
+ self.states['up'],
+ self._add,
+ self.logError)
+ # S0 => S1
+ # Add a server, but with weight zero
+ self.addTransition(self.states['unknown'],
+ self.states['drained'],
+ self._add_weight_zero,
+ self.logError)
+
+ # S1 => S2
+ # Modify the weight of a server from zero to the current
+ # value
+ self.addTransition(self.states['drained'],
+ self.states['up'],
+ self._set,
+ self.logError)
+ # S2 => S1
+ # Set the weight of a pooled server to zero
+ self.addTransition(self.states['up'],
+ self.states['drained'],
+ self._drain,
+ self.logError)
+ # S2 => S0
+ # Remove a server from the pool
+ self.addTransition(self.states['up'],
+ self.states['unknown'],
+ self._del,
+ self.logError)
+ # S3 => S2
+ # Refresh a server to its desired state
+ self.addTransition(self.states['refresh'],
+ self.states['up'],
+ self._set,
+ self.logError)
+ # S3 => S0
+ # Remove a server that needed refreshing.
+ self.addTransition(self.states['refresh'],
+ self.states['unknown'],
+ self._del,
+ self.logError)
+
+ @property
+ def actualState(self):
+ # These are all blocking calls
+ if not self.service_ready():
+ # TODO: add a "waiting" state?
+ return self.states['unknown']
+
+ dests = self.client.get_dests(self.service.to_attr_list())
+ for dest in dests:
+ dest.service = self.service
+ if self.equals(dest):
+ w = dest.weight()
+ if w == self.weight():
+ # Server is present with its predefined weight
+ return self.states['up']
+ elif w == 0:
+ # Server is present but drained
+ return self.states['drained']
+ else:
+ # Server is present but needs a change in weight
+ return self.states['refresh']
+ return self.states['unknown']
+
+ def service_ready(self):
+ return (self.service.actualState.name == 'present')
+
+ def _del(self):
+ if not self.service_ready():
+ raise IpvsError("Tried to delete a server from service %s but "
+ "the service is not present",
+ self.service)
+ self.client.del_dest(self.service.vip(), self.port_, self.ip_,
+ protocol=self.service.proto())
+
+ def _add(self, weight=None):
+ if not self.service_ready():
+ raise IpvsError("Tried to add a server to service %s but "
+ "the service is not present",
+ self.service)
+ if weight is None:
+ weight = self.weight_
+ self.client.add_dest(self.service.vip(), self.port_, self.ip_,
+ protocol=self.service.proto(),
+ weight=weight, method=self.fwd_method_)
+
+ def _add_weight_zero(self):
+ return self._add(weight=0)
+
+ def _drain(self):
+ self._set(weight=0)
+
+ def _set(self, weight=None):
+ if not self.service_ready():
+ raise IpvsError("Tried to modify server of service %s but "
+ "the service is not present",
+ self.service)
+ if weight is None:
+ weight = self.weight_
+ if self.currentState.name == 'unknown':
+ return self._add(weight=weight)
+ self.client.update_dest(self.service.vip(), self.port_, self.ip_,
+ protocol=self.service.proto(),
+ weight=weight, method=self.fwd_method_)
+
+ def equals(self, server):
+ return (self.service.equals(server.service) and
+ self.ip() == server.ip() and self.port() == server.port())
+
+ def __eq__(self, server):
+ return (self.equals(server) and
+ self.currentState == server.currentState and
+ self.weight() == server.weight())
+
+ def __str__(self):
+ return "%s:%d with weight %d" % (self.ip_, self.port_, self.weight_)
diff --git a/pybal/ipvs/service.py b/pybal/ipvs/service.py
new file mode 100644
index 0000000..14a4881
--- /dev/null
+++ b/pybal/ipvs/service.py
@@ -0,0 +1,86 @@
+import socket
+
+from gnlpy import ipvs as netlink
+from pybal import fsm
+from pybal.util import log
+
+
+class Service(netlink.Service, fsm.FiniteStateMachine):
+ """
+ Handler for ipvs service using netlink
+ """
+ _protocols = {
+ 'tcp': socket.IPPROTO_TCP,
+ 'udp': socket.IPPROTO_UDP
+ }
+
+ @staticmethod
+ def from_tuple(service):
+ return {
+ 'proto': service[0],
+ 'vip': service[1],
+ 'port': service[2],
+ 'sched': service[3] if len(service) > 3 else 'rr'
+ }
+
+ def __init__(self, client, service):
+ s = Service.from_tuple(service)
+ self.client = client
+ netlink.Service.__init__(self, s, validate=True)
+ fsm.FiniteStateMachine.__init__(self)
+ self.setUpFSM()
+ self.currentState = self.actualState
+
+ def setUpFSM(self):
+ # S0: the service is unknown/deleted
+ self.addState(fsm.State('unknown'))
+ # S1: the service is present
+ self.addState(fsm.State('present'))
+ # S0 => S1
+ self.addTransition(self.states['unknown'],
+ self.states['present'],
+ self._add,
+ self.logError)
+ self.addTransition(self.states['present'],
+ self.states['unknown'],
+ self._del,
+ self.logError)
+ self.currentState = self.states['unknown']
+
+ def checkActualState(self):
+ # Blocking call
+ s = self.client.get_service(self.to_attr_list())
+ if s is not None and self.equals(s):
+ actualState = self.states['present']
+ else:
+ actualState = self.states['unknown']
+ if self.currentState != actualState:
+ raise IpvsError(
+ "")
+
+ def _add(self):
+ log.debug("Adding service %s" % self)
+ self.client.add_service(self.vip_, self.port_,
+ protocol=self.proto_num(),
+ sched_name=self.sched_)
+
+ def _del(self):
+ if self.currentState == self.states['unknown']:
+ log.info("Not removing already absent service %s" % self)
+ return
+
+ log.debug("Removing service %s" % self)
+ self.client.del_service(self.vip_, self.port_,
+ protocol=self.proto_num())
+
+ def equals(self, srv):
+ return (self.proto_ == srv.proto_ and self.vip() == srv.vip()
+ and self.port() == srv.port() and self.sched() == srv.sched())
+
+ def __eq__(self, srv):
+ return (self.equals(srv) and self.currentState == srv.currentState)
+
+ def __str__(self):
+ return "'%s://%s:%d' with scheduler %s" % (self.proto_,
+ self.vip_, self.port_,
+ self.sched_)
diff --git a/pybal/test/fixtures.py b/pybal/test/fixtures.py
index 8f584de..5516e5c 100644
--- a/pybal/test/fixtures.py
+++ b/pybal/test/fixtures.py
@@ -115,3 +115,42 @@
self.server = ServerStub(self.host, self.ip, self.port,
lvsservice=self.lvsservice)
self.reactor = twisted.test.proto_helpers.MemoryReactor()
+
+
+class IpvsTestCase(twisted.trial.unittest.TestCase):
+
+ def getMockClient(self):
+ c = mock.MagicMock(spec=netlink.IpvsClient)
+ return c
+
+ def getService(self):
+ # currentState will be overridden by the actual state
+ t = ('tcp', '192.168.1.1', 80)
+ s = service.Service(self.client, t)
+ s.currentState = s.states['present']
+ return s
+
+ def getServer(self, srv=None):
+ if srv is None:
+ srv = self.getLogicalServers()[0]
+ s = self.getService()
+ return server.Server(self.client, srv, s)
+
+ def getPool(self, dest_range=range(3)):
+ serv = service.Service(self.client, ('tcp', '192.168.1.1', 80))
+ dests = []
+ for i in dest_range:
+ d = netlink.Dest({'ip': '10.0.0.%d' % (i + 1), 'port': 80,
'weight': 10})
+ dests.append(d)
+ return {'service': serv, 'dests': dests}
+
+ def getLogicalServers(self):
+ s = [
+ ServerStub('www1.local', ip='10.0.0.1', port=80, weight=10),
+ ServerStub('www2.local', ip='10.0.0.2', port=80, weight=10),
+ ServerStub('www3.local', ip='10.0.0.3', port=80, weight=10)
+ ]
+ for srv in s:
+ srv.up = True
+ srv.pooled = True
+ return s
diff --git a/pybal/test/test_ipvs_manager.py b/pybal/test/test_ipvs_manager.py
new file mode 100644
index 0000000..8176927
--- /dev/null
+++ b/pybal/test/test_ipvs_manager.py
@@ -0,0 +1,25 @@
+import mock
+import socket
+
+from gnlpy import ipvs as netlink
+from twisted.trial import unittest
+
+from pybal.ipvs import service, server, IpvsError, manager
+from pybal.test.fixtures import ServerStub
+
+
+
+class TestIpvsServiceManager(unittest.TestCase):
+
+ def setUp(self):
+ service.Service = mock.MagicMock(spec=service.Service)
+ self.service = ('tcp', '192.168.1.1', 6379, 'wrr')
+ self.conf = mock.MagicMock()
+
+ def testInit(self):
+ self.conf.getboolean.return_value = False
+ mgr = manager.ServiceManager('foo', self.service, self.conf)
+ self.assertEquals(mgr.name, 'foo')
+ self.assertEquals(mgr.servers, set())
+ self.assertEquals(mgr.destinations, {})
+ self.assertEquals()
diff --git a/pybal/test/test_ipvs_server.py b/pybal/test/test_ipvs_server.py
new file mode 100644
index 0000000..3e8507b
--- /dev/null
+++ b/pybal/test/test_ipvs_server.py
@@ -0,0 +1,247 @@
+import mock
+import socket
+
+from gnlpy import ipvs as netlink
+
+from pybal.ipvs import service, server, IpvsError
+from pybal.test.fixtures import ServerStub, IpvsTestCase
+
+
+class IpvsServerTestCase(IpvsTestCase):
+
+ def setUp(self):
+ self.client = self.getMockClient()
+ self.server = self.getServer()
+
+ def testInit(self):
+ # An invalid IP address causes an exception
+ s = ServerStub('www1.local', '10.0.0,1', 80, 10)
+ self.assertRaises(AssertionError, server.Server, self.client, s,
+ self.getService())
+ # A non-numeric port causes an exception
+ s = ServerStub('www1.local', '10.0.0.1', '80', 10)
+ self.assertRaises(AssertionError, server.Server, self.client, s,
+ self.getService())
+
+ # A non-present weight defaults to 1
+ s = ServerStub('www1.local', '10.0.0.1', 80)
+ srv = server.Server(self.client, s, self.getService())
+ self.assertEquals(srv.weight_, 1)
+
+ # The FSM has been set up
+ states = ['unknown', 'drained', 'up', 'refresh']
+ for state in states:
+ self.assertIn(state, srv.states)
+
+ # Status should be 'unkown' since we have no service defined
+ self.assertEquals(srv.actualState.name, 'unknown')
+
+ def testActualState(self):
+ # All states behave like expected
+ # S0
+ self.client.get_service.return_value = None
+ self.assertEquals(self.server.actualState.name, 'unknown')
+ p = self.getPool()
+ self.client.get_service.return_value = p['service']
+ self.client.get_dests.return_value = []
+ self.assertEquals(self.server.actualState.name, 'unknown')
+ # S1
+ dests = p['dests']
+ dests[0].weight_ = 0
+ self.client.get_dests.return_value = dests
+ self.assertEquals(self.server.actualState.name, 'drained')
+ # S2
+ dests[0].weight_ = self.server.weight()
+ self.assertEquals(self.server.actualState.name, 'up')
+ # S3
+ dests[0].weight_ = 54
+ self.assertEquals(self.server.actualState.name, 'refresh')
+ # If the dests request raises an exception, it doesn't get caught
+ self.client.get_dests.side_effect = Exception('Boom!')
+ with self.assertRaises(Exception):
+ self.server.actualState
+
+ def testTransitionFromUnknown(self):
+ # Initial state: unknown
+ p = self.getPool()
+ dests = p['dests']
+ myDest = dests[0]
+ self.client.get_service.return_value = p['service']
+ self.client.get_dests.return_value = []
+ # To "up"
+ d = self.server.toState('up')
+ d.addCallback(
+ lambda _: self.client.add_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ weight=self.server.weight(),
+ method=self.server.fwd_method(),
+ protocol=self.server.service.proto(),
+ )
+ )
+ d.addCallback(
+ lambda _:
+ self.assertEquals(self.server.currentState.name, 'up')
+ )
+ # To "drained"
+ d = self.server.toState('drained')
+ d.addCallback(
+ lambda _: self.client.add_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ weight=0,
+ method=self.server.fwd_method(),
+ protocol=self.server.service.proto(),
+ )
+ )
+ d.addCallback(
+ lambda _:
+ self.assertEquals(self.server.currentState.name, 'drained')
+ )
+ # To "referesh" doesn't exist
+ with self.assertRaises(ValueError):
+ r = self.server.toState('refresh')
+
+ def testTransitionFromDrained(self):
+ p = self.getPool()
+ dests = p['dests']
+ myDest = dests[0]
+ myDest.weight_ = 0
+ self.client.get_service.return_value = p['service']
+ self.client.get_dests.return_value = dests
+ # To unknown
+ d = self.server.toState('unknown')
+ d.addCallback(self.assertEquals, self.server.states['unknown'])
+ d.addCallback(
+ lambda _: self.client.del_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ protocol=self.server.service.proto()
+ )
+ )
+ # To up
+ d = self.server.toState('up')
+ d.addCallback(self.assertEquals, self.server.states['up'])
+ d.addCallBack(
+ lambda _: self.client.update_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ protocol=self.server.service.proto(),
+ weight=self.server.weight(),
+ method=self.server.fwd_method(),
+ )
+ )
+ # To "referesh" doesn't exist
+ with self.assertRaises(ValueError):
+ r = self.server.toState('refresh')
+
+ def testTransitionFromUp(self):
+ p = self.getPool()
+ dests = p['dests']
+ myDest = dests[0]
+ self.client.get_service.return_value = p['service']
+ self.client.get_dests.return_value = dests
+ # To unknown
+ d = self.server.toState('unknown')
+ d.addCallback(self.assertEquals, self.server.states['unknown'])
+ d.addCallback(
+ lambda _: self.client.del_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ protocol=self.server.service.proto()
+ )
+ )
+ # To drained
+ d = self.server.toState('drained')
+ d.addCallback(self.assertEquals, self.server.states['drained'])
+ d.addCallBack(
+ lambda _: self.client.update_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ protocol=self.server.service.proto(),
+ weight=0,
+ method=self.server.fwd_method(),
+ )
+ )
+ # To "referesh" doesn't exist
+ with self.assertRaises(ValueError):
+ r = self.server.toState('refresh')
+
+
+ def testTransitionFromRefresh(self):
+ p = self.getPool()
+ dests = p['dests']
+ myDest = dests[0]
+ myDest.weight_ = 147
+ self.client.get_service.return_value = p['service']
+ self.client.get_dests.return_value = dests
+ # to unknown
+ d = self.server.toState('unknown')
+ d.addCallback(self.assertEquals, self.server.states['unknown'])
+ d.addCallback(
+ lambda _: self.client.del_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ protocol=self.server.service.proto()
+ )
+ )
+ # to up
+ d= self.server.toState('up')
+ d.addCallback(self.assertEquals, self.server.states['up'])
+ d.addCallBack(
+ lambda _: self.client.update_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ protocol=self.server.service.proto(),
+ weight=self.server.weight(),
+ method=self.server.fwd_method(),
+ )
+ )
+ # to drained
+ d= self.server.toState('drained')
+ d.addCallback(self.assertEquals, self.server.states['up'])
+ d.addCallBack(
+ lambda _: self.client.update_dest.assert_called_with(
+ self.server.service.vip(),
+ self.server.service.port(),
+ self.server.ip(),
+ protocol=self.server.service.proto(),
+ weight=0,
+ method=self.server.fwd_method(),
+ )
+ )
+
+
+ def testTransitionCornerCases(self):
+ # From unknown
+ p = self.getPool()
+ dests = p['dests']
+ myDest = dests[0]
+ self.client.get_service.return_value = p['service']
+ self.client.get_dests.return_value = []
+ ## If the service is absent, the transaction fails
+ self.client.get_service.return_value = None
+ d = self.server.toState('up')
+ d.addCallback(self.assertEquals, None)
+ ## If the add_dest method returns an exception, the transaction fails
+ self.client.add_dest.side_effect = Exception('Poww!')
+ self.client.get_service.return_value = p['service']
+ d = self.server.toState('up')
+ d.addCallback(self.assertEquals, None)
+ d.addCallback(lambda _: self.assertEquals(
+ self.server.currentState.name, 'unknown')
+ )
+ # Call from the already 'present' state should not be doing anything.
+ self.client.get_dests.return_value = dests
+ self.client.reset_mock()
+ d = self.server.toState('up')
+ d.addCallback(
+ lambda _: self.client.update_dest.assert_not_called())
diff --git a/pybal/test/test_ipvs_service.py b/pybal/test/test_ipvs_service.py
new file mode 100644
index 0000000..93c3ed9
--- /dev/null
+++ b/pybal/test/test_ipvs_service.py
@@ -0,0 +1,110 @@
+import mock
+import socket
+
+from gnlpy import ipvs as netlink
+
+from pybal.ipvs import service
+from pybal.test import fixtures
+
+
+class IpvsServiceTestCase(fixtures.IpvsTestCase):
+
+ def getPool(self):
+ serv = service.Service.from_tuple(('tcp', '192.168.1.1', 6379))
+ return netlink.Pool({'service': serv, 'dests': []})
+
+ def testFromTuple(self):
+ tup = ('proto', 'vip', 'port', 'sched')
+ s = service.Service.from_tuple(tup)
+ for k, v in s.items():
+ self.assertEquals(k, v)
+ # Default to round-robin scheduling
+ tup = ('proto', 'vip', 'port')
+ s = service.Service.from_tuple(tup)
+ self.assertEquals(s['sched'], 'rr')
+
+ def testInit(self):
+ c = self.getMockClient()
+ # An invalid tuple raises an exception
+ t = ('tcp', '192.168,0,0', 6379)
+ self.assertRaises(AssertionError, service.Service, c, t)
+ # A correct initialization will set up all the service variables
+ t = ('tcp', '192.168.1.1', 6379)
+ s = service.Service(c, t)
+ self.assertEquals(s.proto_, 'tcp')
+ self.assertEquals(s.vip_, '192.168.1.1')
+ self.assertEquals(s.port_, 6379)
+ self.assertEquals(s.sched_, 'rr')
+ # The FSM has been set up
+ self.assertIn('unknown', s.states)
+ self.assertIn('present', s.states)
+ self.assertIn('present', s.states['unknown'].transitions)
+ self.assertIn('unknown', s.states['present'].transitions)
+ # State should be 'unknown' given we gave no answer
+ self.assertEquals(s.actualState, s.states['unknown'])
+
+ def testActualState(self):
+ c = self.getMockClient()
+ # currentState will be overridden by the actual state
+ t = ('tcp', '192.168.1.1', 6379)
+ s = service.Service(c, t)
+ s.currentState = s.states['present']
+ self.assertEquals(s.actualState.name, 'unknown')
+ # if the pool is present, the actual state will become present
+ c.get_service.return_value = s
+ self.assertEquals(s.actualState.name, 'present')
+ # If the request for pools raises an exception,
+ # do not try to catch it.
+ with self.assertRaises(Exception):
+ c.get_service.side_effect = Exception('something!')
+ s.actualState
+
+ def testTransitionToPresent(self):
+ # First situation: service is not present, we transition to s1
(present)
+ c = self.getMockClient()
+ t = ('tcp', '192.168.1.1', 6379)
+ s = service.Service(c, t)
+ c.get_service.return_value = None
+ d = s.toState('present')
+ d.addCallback(self.assertEqual, s.states['present'])
+ d.addCallback(lambda _: s.client.add_service.assert_called_with(
+ '192.168.1.1', 6379, protocol=socket.IPPROTO_TCP,
+ sched_name='rr'))
+ # Check the current state has been set
+ print s.currentState.name
+ assert s.currentState == s.states['present']
+
+ def testTransitionToUnknown(self):
+ c = self.getMockClient()
+ t = ('tcp', '192.168.1.1', 6379)
+ s = service.Service(c, t)
+ c.get_service.return_value = s
+ assert s.actualState == s.states['present']
+ d = s.toState('unknown')
+ d.addCallback(self.assertEqual, s.states['unknown'])
+ d.addCallback(lambda _: c.del_service.assert_called_with(
+ '192.168.1.1', 6379, protocol=socket.IPPROTO_TCP))
+ # Now let's try to move to "unknown" again, nothing will happen
+ c.get_service.return_value = None
+ c.del_service.reset_mock()
+ d1 = s.toState('unknown')
+ d1.addCallback(lambda _: c.del_service.assert_not_called())
+
+ def testFaultyTransaction(self):
+ c = self.getMockClient()
+ t = ('tcp', '192.168.1.1', 6379)
+ s = service.Service(c, t)
+ c.get_service.return_value = s
+ # Simulate the netlink layer failing
+ c.del_service.side_effect = RuntimeError('welcome to the terrordome!')
+ # Failure doesn't propagate outside of the fsm
+ try:
+ d = s.toState('unknown')
+ except Exception as e:
+ self.fail("Unhandled failure in state transition, %s" % e)
+
+ # Verify that the deferred returns None in this case
+ d.addCallback(self.assertEquals, None)
+ # State is still present
+ d.addCallback(lambda _: self.assertEquals(s.currentState.name,
+ 'present'))
diff --git a/setup.py b/setup.py
index acbcfc1..e73b270 100644
--- a/setup.py
+++ b/setup.py
@@ -15,7 +15,7 @@
setup(
name='PyBal',
- version='1.12',
+ version='1.14-dev',
license='GPLv2+',
author='Mark Bergsma',
author_email='[email protected]',
@@ -42,7 +42,8 @@
),
requires=(
'twisted',
- 'PyOpenSSL'
+ 'PyOpenSSL',
+ 'gnlpy',
),
tests_require=(
'mock',
--
To view, visit https://gerrit.wikimedia.org/r/354509
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I392694be6d0832e7d6fd02461b6608bdd6b922ac
Gerrit-PatchSet: 1
Gerrit-Project: operations/debs/pybal
Gerrit-Branch: 2.0-dev
Gerrit-Owner: Giuseppe Lavagetto <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits