From e5dbf96cb448bc6fba5783ed034d4e950d4c0ee1 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Thu, 5 Dec 2019 15:38:49 +0100 Subject: [PATCH] Work in progress on state store. --- pgbouncemgr/manager.py | 46 +++++-- pgbouncemgr/node_config.py | 6 + pgbouncemgr/state.py | 67 ++++++++++- pgbouncemgr/state_store.py | 15 ++- tests/stubs.py | 54 --------- tests/test_manager.py | 134 +++++++++++++++++++++ tests/test_state.py | 81 +++++++------ tests/test_state_store.py | 15 +-- tests/testfiles/basic.yaml | 3 + tests/testfiles/basic_with_state_file.yaml | 14 +++ tests/testfiles/state.json | 2 +- 11 files changed, 315 insertions(+), 122 deletions(-) delete mode 100644 tests/stubs.py create mode 100644 tests/test_manager.py create mode 100644 tests/testfiles/basic_with_state_file.yaml diff --git a/pgbouncemgr/manager.py b/pgbouncemgr/manager.py index 2ed299b..e22455c 100644 --- a/pgbouncemgr/manager.py +++ b/pgbouncemgr/manager.py @@ -3,42 +3,62 @@ PostgreSQL cluster and that reconfigures pgbouncer when needed.""" from argparse import ArgumentParser -from pgbouncemgr.logger import Logger +from pgbouncemgr.logger import Logger, ConsoleLogTarget, SyslogLogTarget from pgbouncemgr.config import Config +from pgbouncemgr.state import State +from pgbouncemgr.state_store import StateStore DEFAULT_CONFIG = "/etc/pgbouncer/pgbouncemgr.yaml" DEFAULT_LOG_FACILITY = "LOG_LOCAL1" -def main(): - """Starts the pgbouncemgr main application.""" - args = _parse_arguments() - config = Config(args.config) - log = Logger("pgbouncemgr", args.log_facility, args.debug, args.verbose) +class Manager(): + def __init__(self, argv=None): + args = _parse_arguments(argv) + self.config = Config(args.config) + self._create_logger(args) + self._create_state() + + def _create_logger(self, args): + self.log = Logger() + self.log.append(ConsoleLogTarget(args.verbose, args.debug)) + if args.log_facility.lower() != 'none': + self.log.append(SyslogLogTarget("pgbouncemgr", args.log_facility)) + + def _create_state(self): + self.state = State.fromConfig(self.config, self.log) + self.state_store = StateStore(self.config.state_file, self.state) + self.state_store.load() + + def start(self): + self.log.info("Not yet!") + self.log.debug("Work in progres...") + self.log.warning("Beware!") + self.log.error("I will crash now") -def _parse_arguments(): +def _parse_arguments(args): parser = ArgumentParser(description="pgbouncemgr") parser.add_argument( "-v", "--verbose", default=False, action="store_true", - help="enable verbose output (default: disabled)") + help="enable verbose console output (default: disabled)") parser.add_argument( "-d", "--debug", default=False, action="store_true", - help="enable debugging output (default: disabled)") + help="enable debugging console output (default: disabled)") parser.add_argument( "-f", "--log-facility", default=DEFAULT_LOG_FACILITY, - help="syslog facility to use (default: %s)" % DEFAULT_LOG_FACILITY) + help="syslog facility to use or 'none' to disable syslog logging " + + "(default: %s)" % DEFAULT_LOG_FACILITY) parser.add_argument( "--config", default=DEFAULT_CONFIG, help="config file (default: %s)" % DEFAULT_CONFIG) - args = parser.parse_args() - return args + return parser.parse_args(args) if __name__ == "__main__": - main() + Manager(None).start() diff --git a/pgbouncemgr/node_config.py b/pgbouncemgr/node_config.py index 3b10ecd..2f65391 100644 --- a/pgbouncemgr/node_config.py +++ b/pgbouncemgr/node_config.py @@ -7,6 +7,10 @@ class NodeConfig(): self._pgbouncer_config = None self.host = None self.port = None + self.connect_timeout = 1 + self.name = 'template1' + self.user = 'pgbouncemgr' + self.password = None @property def pgbouncer_config(self): @@ -21,6 +25,8 @@ class NodeConfig(): self._pgbouncer_config = path def export(self): + """Exports the data for the node configuration, that we want + to end up in the state data.""" return { "pgbouncer_config": self.pgbouncer_config, "host": self.host, diff --git a/pgbouncemgr/state.py b/pgbouncemgr/state.py index b14a7dd..2a151b9 100644 --- a/pgbouncemgr/state.py +++ b/pgbouncemgr/state.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # no-pylint: disable=missing-docstring,broad-except,too-many-instance-attributes +import os from pgbouncemgr.node_config import NodeConfig @@ -85,6 +86,20 @@ class InvalidLeaderStatus(StateException): ("The leader_status cannot be set to '%s' " + "(valid values are: %s)") % (status, ", ".join(LEADER_STATUSES))) +class UnknownLeaderNodeId(StateException): + """Raised when leader_Node_id is set to a non-existing node_id value.""" + def __init__(self, leader_node_id): + super().__init__( + "The leader_node_id does not exist " + + "(leader_node_id=%s)" % leader_node_id) + +class ActivePgbouncerConfigDoesNotExist(StateException): + """Raised when active_pgbouncer_config is set to a non-existing path.""" + def __init__(self, active_pgbouncer_config): + super().__init__( + "The active_pgbouncer_config file does not exist " + + "(path=%s)" % active_pgbouncer_config) + class InvalidNodeStatus(StateException): """Raised when the status for a node is set to an invalid value.""" def __init__(self, status): @@ -93,12 +108,23 @@ class InvalidNodeStatus(StateException): "(valid values are: %s)") % (status, ", ".join(NODE_STATUSES))) class State(): - def __init__(self): + @staticmethod + def fromConfig(config, logger): + state = State(logger) + for node_id, settings in config.nodes.items(): + node_config = NodeConfig(node_id) + for k, v in settings.items(): + setattr(node_config, k, v) + state.add_node(node_config) + return state + + def __init__(self, logger): + self.log = logger self.modified = False self._system_id = None self._timeline_id = None self._active_pgbouncer_config = None - self.leader_node_id = None + self._leader_node_id = None self._leader_error = None self._leader_status = LEADER_UNKNOWN self.nodes = {} @@ -204,6 +230,26 @@ class State(): self._leader_status = status self.modified = True + @property + def leader_node_id(self): + return self._leader_node_id + + @leader_node_id.setter + def leader_node_id(self, leader_node_id): + """Sets the id of the leader node.""" + try: + node = self.get_node(leader_node_id) + except UnknownNodeRequested: + self.log.warning( + "The laeder_node_id value points to a non-existing " + + "node id: node config changed? (leader_node_id=%s)" % + leader_node_id) + raise UnknownLeaderNodeId(leader_node_id) + if self._leader_node_id == leader_node_id: + return + self._leader_node_id = leader_node_id + self.modified = True + @property def leader_error(self): return self._leader_error @@ -223,13 +269,22 @@ class State(): return self._active_pgbouncer_config @active_pgbouncer_config.setter - def active_pgbouncer_config(self, pgbouncer_config): + def active_pgbouncer_config(self, active_pgbouncer_config): """Sets the pgbouncer configuration file that must be activated in order to connect pgbouncer to the correct backend - PostgreSQL server.""" - if self._active_pgbouncer_config == pgbouncer_config: + PostgreSQL server. + When the active config is set to a non-existing file (this can + happen when the node configuration changes), a warning will be + logged.""" + if not os.path.exists(active_pgbouncer_config): + self.log.warning( + "The active_pgbouncer_config value points to a non-existing " + + "file: node config changed? (path=%s)" % + active_pgbouncer_config) + raise ActivePgbouncerConfigDoesNotExist(active_pgbouncer_config) + if self._active_pgbouncer_config == active_pgbouncer_config: return - self._active_pgbouncer_config = pgbouncer_config + self._active_pgbouncer_config = active_pgbouncer_config self.modified = True def export(self): diff --git a/pgbouncemgr/state_store.py b/pgbouncemgr/state_store.py index d82b98c..02c5387 100644 --- a/pgbouncemgr/state_store.py +++ b/pgbouncemgr/state_store.py @@ -6,6 +6,8 @@ from datetime import datetime from os import rename from os.path import isfile from pgbouncemgr.logger import format_ex +from pgbouncemgr.state import \ + UnknownLeaderNodeId, ActivePgbouncerConfigDoesNotExist class StateStoreException(Exception): @@ -44,7 +46,18 @@ class StateStore(): "timeline_id", "active_pgbouncer_config", "leader_node_id"]: - setattr(self.state, key, loaded_state[key]) + if key in loaded_state: + try: + setattr(self.state, key, loaded_state[key]) + except (UnknownLeaderNodeId, + ActivePgbouncerConfigDoesNotExist): + # These exception can occur when the node configuration + # has changed and some fields are no longer value. + # In such case, we don't update the attributes here + # and simply ignore the specific errors. After the + # application has started, the correct values will be + # filled by the manager process. + pass def store(self): """Store the current state in the state file. Returns True when diff --git a/tests/stubs.py b/tests/stubs.py deleted file mode 100644 index fd72235..0000000 --- a/tests/stubs.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- - -from pgbouncer.manager import DEFAULT_CONFIG, DEFAULT_LOG_FACILITY - - -class ArgsStub(): - config = DEFAULT_CONFIG - log_facility = DEFAULT_LOG_FACILITY - verbose = False - debug = False - - -class LoggerStub(): - def __init(self, ident, facility, debug, verbose): - pass - - def debug(self, msg): - pass - - def info(self, msg): - pass - - def warning(self, msg): - pass - - def error(self, msg): - pass - - def format_ex(self, exception): - return "FMTEX" - - -class StateStoreStub(): - old_state = None - test_with_nr_of_errors = 0 - test_with_state = None - - def load(self) - state = State() - - def store(self, state): - new_state = json.dumps(state_to_store, sort_keys=True, indent=2) - if self.old_state and self.old_state == new_state: - return STORE_NOCHANGE - sel.old_state = new_state - - if test_with_nr_of_errors: - test_with_nr_of_errors -= 1 - self.err = "Storing state failed: just testing" - return STORE_ERROR - - return STORE_UPDATED - - diff --git a/tests/test_manager.py b/tests/test_manager.py new file mode 100644 index 0000000..fcd7b52 --- /dev/null +++ b/tests/test_manager.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- + +import os +import shutil +import unittest +from pgbouncemgr.manager import * + + +CONFIG = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "testfiles", "basic.yaml") + +CONFIG_WITH_STATE_FILE = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "testfiles", "basic_with_state_file.yaml") + +STATE_FILE_TEMPLATE = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "testfiles", "state.json") + +STATE_FILE = "/tmp/pgbouncemgr.state" + + +class ManagerTests(unittest.TestCase): + def test_CreateManagerUsingCommandLineOptions(self): + """Check if a Manager object can be created, making use of some + command line arguments.""" + mgr = Manager([ + '-f', 'none', # no syslog + '-d', # debug (and verbose) enabled + '--config', CONFIG, # the config file to use + '-f', 'none' # the syslog facility to use, 'none' disables syslog + ]) + + # Check the logging setup. + self.assertEqual(1, len(mgr.log)) + console = mgr.log[0] + self.assertEqual('ConsoleLogTarget', type(console).__name__) + self.assertTrue(console.verbose_enabled) + self.assertTrue(console.debug_enabled) + + def test_GivenNoStateFile_WhenCreatingManager_DefaultsAreUsed(self): + mgr = Manager(['--config', CONFIG]) + + # Check if the config file was read. + # If yes, then the state should be filled with data for the + # the configured nodes and all state should be in the initial + # configuration. + self.maxDiff = 5000 + self.assertEqual({ + 'system_id': None, + 'timeline_id': None, + 'active_pgbouncer_config': None, + 'leader_error': None, + 'leader_node_id': None, + 'leader_status': 'LEADER_UNKNOWN', + 'nodes': { + 'nodeA': { + 'config': { + 'host': '1.2.3.4', + 'port': 8888, + 'pgbouncer_config': None + }, + 'node_id': 'nodeA', + 'is_leader': False, + 'system_id': None, + 'timeline_id': None, + 'status': 'NODE_UNKNOWN', + 'error': None + }, + 'nodeB': { + 'config': { + 'host': '2.3.4.5', + 'port': 7777, + 'pgbouncer_config': None + }, + 'node_id': 'nodeB', + 'is_leader': False, + 'system_id': None, + 'timeline_id': None, + 'status': 'NODE_UNKNOWN', + 'error': None + } + } + }, mgr.state.export()) + + def test_GivenStateFile_WhenCreatingManager_StoredStateIsApplied(self): + mgr = Manager(['--config', CONFIG_WITH_STATE_FILE]) + + # Create the state file as expected by the loaded config. + shutil.copy(STATE_FILE_TEMPLATE, STATE_FILE) + + # Check if the config file was read. + # If yes, then the state should be filled with data for the + # the configured nodes and all state should be in the initial + # configuration. + self.maxDiff = 5000 + self.assertEqual({ + 'system_id': 'A', + 'timeline_id': 42, + 'active_pgbouncer_config': None, # because /myt/config.ini from the state does not exit + 'leader_error': None, + 'leader_node_id': 'nodeA', + 'leader_status': 'LEADER_UNKNOWN', + 'nodes': { + 'nodeA': { + 'config': { + 'host': '1.2.3.4', + 'port': 8888, + 'pgbouncer_config': None + }, + 'node_id': 'nodeA', + 'is_leader': True, + 'system_id': None, + 'timeline_id': None, + 'status': 'NODE_UNKNOWN', + 'error': None + }, + 'nodeB': { + 'config': { + 'host': '2.3.4.5', + 'port': 7777, + 'pgbouncer_config': None + }, + 'node_id': 'nodeB', + 'is_leader': False, + 'system_id': None, + 'timeline_id': None, + 'status': 'NODE_UNKNOWN', + 'error': None + } + } + }, mgr.state.export()) + diff --git a/tests/test_state.py b/tests/test_state.py index 6412b45..6cd1175 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -2,6 +2,7 @@ import os import unittest +from pgbouncemgr.logger import * from pgbouncemgr.state import * @@ -12,7 +13,7 @@ PGBOUNCER_CONFIG = os.path.join( class StateTests(unittest.TestCase): def test_GivenFreshState_DefaultsAreSetCorrectly(self): - state = State() + state = State(Logger()) self.assertIsNone(state.system_id) self.assertIsNone(state.timeline_id) self.assertIsNone(state.active_pgbouncer_config) @@ -23,29 +24,29 @@ class StateTests(unittest.TestCase): self.assertFalse(state.modified) def test_GivenFreshState_ModifiedFlagIsNotSet(self): - state = State() + state = State(Logger()) self.assertFalse(state.modified) def test_GivenFreshState_IsCleanSlate_ReturnsTrue(self): - state = State() + state = State(Logger()) self.assertTrue(state.is_clean_slate()) def test_GivenStateWithSystemIdAssigned_IsCleanSlate_ReturnsFalse(self): - state = State() + state = State(Logger()) state.system_id = "whatever" self.assertFalse(state.is_clean_slate()) def test_GivenFreshState_SetSystemId_SetsSystemId(self): - state = State() + state = State(Logger()) state.system_id = "booya" self.assertEqual("booya", state.system_id) def test_GivenStateWithSystemId_SetSameSystemId_IsOk(self): - state = State() + state = State(Logger()) state.system_id = "booya" state.modified = False state.system_id = "booya" @@ -53,7 +54,7 @@ class StateTests(unittest.TestCase): self.assertFalse(state.modified) def test_GivenStateWithSystemId_SetDifferentSystemId_RaisesException(self): - state = State() + state = State(Logger()) state.system_id = "booya" state.modified = False @@ -62,20 +63,20 @@ class StateTests(unittest.TestCase): self.assertIn("from=booya, to=baayo", str(context.exception)) def test_GivenFreshState_SetTimelineId_SetsTimelineId(self): - state = State() + state = State(Logger()) state.timeline_id = 666 self.assertEqual(666, state.timeline_id) self.assertTrue(state.modified) def test_WithNonIntegerInput_SetTimelineId_RaisesException(self): - state = State() + state = State(Logger()) with self.assertRaises(InvalidTimelineId) as context: state.timeline_id = "one hundred" self.assertIn("invalid value=one hundred", str(context.exception)) def test_GivenStateWithTimelineId_SetLowerTimelineId_RaisesException(self): - state = State() + state = State(Logger()) state.timeline_id = "50" with self.assertRaises(TimelineIdLowered) as context: @@ -83,7 +84,7 @@ class StateTests(unittest.TestCase): self.assertIn("(from=50, to=49)", str(context.exception)) def test_GivenStateWithTimelineId_SetSameTimelineId_IsOk(self): - state = State() + state = State(Logger()) state.timeline_id = 50 state.modified = False @@ -92,7 +93,7 @@ class StateTests(unittest.TestCase): self.assertFalse(state.modified) def test_GivenStateWithTimelineId_SetHigherTimelineId_IsOk(self): - state = State() + state = State(Logger()) state.timeline_id = 50 state.modified = False @@ -102,7 +103,7 @@ class StateTests(unittest.TestCase): self.assertTrue(state.modified) def test_SetLeaderStatus_SetsLeaderStatus(self): - state = State() + state = State(Logger()) state.modified = False state.leader_status = LEADER_CONNECTED @@ -111,7 +112,7 @@ class StateTests(unittest.TestCase): self.assertTrue(state.modified) def test_SetLeaderStatus_ToNonExistentStatus_RaisesException(self): - state = State() + state = State(Logger()) state.modified = False with self.assertRaises(InvalidLeaderStatus) as context: @@ -119,7 +120,7 @@ class StateTests(unittest.TestCase): self.assertIn("'gosh'", str(context.exception)) def test_SetLeaderError_SetsLeaderError(self): - state = State() + state = State(Logger()) state.modified = False state.leader_error = "God disappeared in a puff of logic" @@ -136,14 +137,14 @@ class StateTests(unittest.TestCase): class NodeCollectionTests(unittest.TestCase): def test_WithNodeNotYetInState_AddNode_AddsNodeState(self): - state = State() + state = State(Logger()) node = state.add_node(1) self.assertEqual(1, node.node_id) self.assertTrue(state.modified) def test_WithNodeAlreadyInState_AddNode_RaisesException(self): - state = State() + state = State(Logger()) state.add_node(123) with self.assertRaises(DuplicateNodeAdded) as context: @@ -151,13 +152,13 @@ class NodeCollectionTests(unittest.TestCase): self.assertIn("duplicate node_id=123", str(context.exception)) def test_WithNodeNotInState_getNode_RaisesException(self): - state = State() + state = State(Logger()) with self.assertRaises(UnknownNodeRequested): state.get_node("that does not exist") def test_WithNodeInState_getNode_ReturnsNode(self): - state = State() + state = State(Logger()) node = state.add_node("that does exist") node_from_state = state.get_node("that does exist") @@ -165,15 +166,15 @@ class NodeCollectionTests(unittest.TestCase): self.assertEqual(node, node_from_state) def test_WithUnknownNode_SetLeaderNode_RaisesException(self): - state1 = State() - state2 = State() + state1 = State(Logger()) + state2 = State(Logger()) node = state2.add_node(1337) with self.assertRaises(UnknownNodeRequested) as context: state1.promote_node(node) self.assertIn("unknown node_id=1337", str(context.exception)) def test_WithNodeWithoutSystemId_SetLeaderNode_RaisesException(self): - state = State() + state = State(Logger()) node = state.add_node(1) node.timeline_id = 1 node.config.pgbouncer_config = PGBOUNCER_CONFIG @@ -183,7 +184,7 @@ class NodeCollectionTests(unittest.TestCase): self.assertIn("node has no system_id", str(context.exception)) def test_WithNodeWithoutTimelineId_SetLeaderNode_RaisesException(self): - state = State() + state = State(Logger()) node = state.add_node(1) node.system_id = "system" node.config.pgbouncer_config = PGBOUNCER_CONFIG @@ -193,7 +194,7 @@ class NodeCollectionTests(unittest.TestCase): self.assertIn("node has no timeline_id", str(context.exception)) def test_WithNodeWithoutPgbouncerConfig_SetLeaderNode_RaisesException(self): - state = State() + state = State(Logger()) node = state.add_node(1) node.system_id = "a7d8a9347df789saghdfs" node.timeline_id = 11111111111 @@ -203,7 +204,7 @@ class NodeCollectionTests(unittest.TestCase): self.assertIn("node has no pgbouncer_config", str(context.exception)) def test_SetLeaderNode_SetsLeaderNode_WithUnknownLeaderStatus(self): - state = State() + state = State(Logger()) node = state.add_node(1) node.config.pgbouncer_config = PGBOUNCER_CONFIG node.system_id = "SuperCluster" @@ -218,7 +219,7 @@ class NodeCollectionTests(unittest.TestCase): self.assertTrue(state.modified) def test_SetLeaderNode_ToSameLeader_ResetsLeaderNode_WithUnknownLeaderStatus(self): - state = State() + state = State(Logger()) node = state.add_node(1) node.config.pgbouncer_config = PGBOUNCER_CONFIG node.system_id = "1.2.3.4.5.6.7.8.9.10.11" @@ -234,7 +235,7 @@ class NodeCollectionTests(unittest.TestCase): self.assertTrue(state.modified) def test_SetLeaderNode_ToNodeWithDifferentSystemId_RaisesException(self): - state = State() + state = State(Logger()) node1 = state.add_node(1) node1.config.pgbouncer_config = PGBOUNCER_CONFIG node1.system_id = "systemA" @@ -249,7 +250,7 @@ class NodeCollectionTests(unittest.TestCase): node2.promote() def test_SetLeaderNode_ToNodeWithLowerTimelineId_RaisesException(self): - state = State() + state = State(Logger()) node1 = state.add_node(1) node1.config.pgbouncer_config = PGBOUNCER_CONFIG node1.system_id = "systemX" @@ -264,7 +265,7 @@ class NodeCollectionTests(unittest.TestCase): node2.promote() def test_SetLeaderNode_ToNodeWithSameOrHigherTimelineId_IsOk(self): - state = State() + state = State(Logger()) node1 = state.add_node(1) node1.config.pgbouncer_config = PGBOUNCER_CONFIG node1.system_id = "systemX" @@ -299,7 +300,7 @@ class NodeCollectionTests(unittest.TestCase): class NodeTests(unittest.TestCase): def test_WithNoneValue_SetSystemId_RaisesException(self): - state = State() + state = State(Logger()) node = state.add_node("break me") state.modified = False @@ -309,7 +310,7 @@ class NodeTests(unittest.TestCase): self.assertFalse(state.modified) def test_WithEmptyString_SetSystemId_RaisesException(self): - state = State() + state = State(Logger()) node = state.add_node("break me") state.modified = False @@ -319,7 +320,7 @@ class NodeTests(unittest.TestCase): self.assertFalse(state.modified) def test_SetSystemId_SetsSystemId_AndNotifiesChangeToState(self): - state = State() + state = State(Logger()) node = state.add_node(1) state.modified = False @@ -329,7 +330,7 @@ class NodeTests(unittest.TestCase): self.assertTrue(state.modified) def test_WithNonIntegerInput_SetTimelineId_RaisesException(self): - state = State() + state = State(Logger()) node = state.add_node("break me") with self.assertRaises(InvalidTimelineId) as context: @@ -337,7 +338,7 @@ class NodeTests(unittest.TestCase): self.assertIn("invalid value=TARDIS", str(context.exception)) def test_SetTimelineId_SetsTimelineId_AndNotifiesChangeToState(self): - state = State() + state = State(Logger()) node = state.add_node(1) state.modified = False @@ -347,7 +348,7 @@ class NodeTests(unittest.TestCase): self.assertTrue(state.modified) def test_SetStatus_SetsStatus_AndNotifiesChangeToState(self): - state = State() + state = State(Logger()) node = state.add_node(1) state.modified = False @@ -357,7 +358,7 @@ class NodeTests(unittest.TestCase): self.assertTrue(state.modified) def test_WithInvalidStatus_SetStatus_RaisesException(self): - state = State() + state = State(Logger()) node = state.add_node(1) state.modified = False @@ -367,7 +368,7 @@ class NodeTests(unittest.TestCase): self.assertFalse(state.modified) def test_SetError_ToString_SetsError_AndNotifiesChangeToState(self): - state = State() + state = State(Logger()) node = state.add_node(1) state.modified = False @@ -377,7 +378,7 @@ class NodeTests(unittest.TestCase): self.assertTrue(state.modified) def test_SetError_ToNone_ClearsError_AndNotifiesChangeToState(self): - state = State() + state = State(Logger()) node = state.add_node(1) node.set_error("Mouse lost its ball") state.modified = False @@ -388,7 +389,7 @@ class NodeTests(unittest.TestCase): self.assertTrue(state.modified) def test_WhenNothingChanges_NoChangeIsNotifiedToState(self): - state = State() + state = State(Logger()) node = state.add_node("x") node.system_id = "aaaaaaa" node.timeline_id = 55 @@ -408,7 +409,7 @@ class StateExportTests(unittest.TestCase): def test_GivenFilledState_Export_ExportsDataForStore(self): self.maxDiff = 5000; - state = State() + state = State(Logger()) node1 = state.add_node(1) node1.config.pgbouncer_config = PGBOUNCER_CONFIG diff --git a/tests/test_state_store.py b/tests/test_state_store.py index fbb3e38..329e8b7 100644 --- a/tests/test_state_store.py +++ b/tests/test_state_store.py @@ -4,6 +4,7 @@ import os import json import unittest from tempfile import NamedTemporaryFile +from pgbouncemgr.logger import * from pgbouncemgr.state import * from pgbouncemgr.state_store import * @@ -15,7 +16,7 @@ def make_test_file_path(filename): class StateStoreTests(unittest.TestCase): def test_GivenNonExistingStateFile_OnLoad_StateStoreDoesNotLoadState(self): - state = State() + state = State(Logger()) before = state.export() StateStore("/non-existent/state.json", state) after = state.export() @@ -23,7 +24,7 @@ class StateStoreTests(unittest.TestCase): self.assertEqual(before, after) def test_GivenExistingStateFile_ContainingInvalidJson_OnLoad_StateStoreDoesNotLoadState(self): - state = State() + state = State(Logger()) before = state.export() StateStore(make_test_file_path("invalid.json"), state) after = state.export() @@ -31,14 +32,14 @@ class StateStoreTests(unittest.TestCase): self.assertEqual(before, after) def test_GivenExistingStateFile_OnLoad_StateStoreUpdatesState(self): - state = State() + state = State(Logger()) - # These are the fields as + # These are the fields as defined in the state.json file. expected = state.export() expected["system_id"] = "A" expected["timeline_id"] = 42 - expected["leader_node_id"] = "NODE1" - expected["active_pgbouncer_config"] = "/my/config.ini" + expected["leader_node_id"] = None # because leader node id 'nodeA' does not exist. + expected["active_pgbouncer_config"] = None # because stored /my/config.ini does not exist. StateStore(make_test_file_path("state.json"), state) @@ -46,7 +47,7 @@ class StateStoreTests(unittest.TestCase): def test_GivenState_OnSave_StateStoreStoresState(self): try: - state = State() + state = State(Logger()) tmpfile = NamedTemporaryFile(delete=False) StateStore(tmpfile.name, state).store() diff --git a/tests/testfiles/basic.yaml b/tests/testfiles/basic.yaml index a884ca0..8b01e10 100644 --- a/tests/testfiles/basic.yaml +++ b/tests/testfiles/basic.yaml @@ -1,4 +1,7 @@ --- +main: + state_file: /make/sure/no/state/file/exists/here + db_connection_defaults: port: 8888 password: Wilmaaaaa!!! diff --git a/tests/testfiles/basic_with_state_file.yaml b/tests/testfiles/basic_with_state_file.yaml new file mode 100644 index 0000000..a714a2d --- /dev/null +++ b/tests/testfiles/basic_with_state_file.yaml @@ -0,0 +1,14 @@ +--- +main: + state_file: "/tmp/pgbouncemgr.state" + +db_connection_defaults: + port: 8888 + password: Wilmaaaaa!!! + +nodes: + nodeA: + host: 1.2.3.4 + nodeB: + host: 2.3.4.5 + port: 7777 diff --git a/tests/testfiles/state.json b/tests/testfiles/state.json index f073024..a5544bc 100644 --- a/tests/testfiles/state.json +++ b/tests/testfiles/state.json @@ -2,7 +2,7 @@ "active_pgbouncer_config": "/my/config.ini", "system_id": "A", "timeline_id": 42, - "leader_node_id": "NODE1", + "leader_node_id": "nodeA", "any": "other", "state": "properties", "are": "ignored",