Added config reader to the project.
This commit is contained in:
parent
5ced3b5ef9
commit
9697589318
|
@ -0,0 +1,163 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""This module encapsulates the configuration for pgbouncemgr.
|
||||||
|
The configuration is read from a YAML file."""
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
class ConfigException(Exception):
|
||||||
|
"""Used for all exceptions that are raised from pgbouncemgr.config."""
|
||||||
|
|
||||||
|
class ConfigNotFound(ConfigException):
|
||||||
|
"""Raised when the requested configuration file does not exist."""
|
||||||
|
def __init__(self, path):
|
||||||
|
super().__init__(
|
||||||
|
"Configuration file not found (path=%s)" % path)
|
||||||
|
|
||||||
|
class InvalidConfigSection(ConfigException):
|
||||||
|
"""Raised when the configuration file contains an invalid section."""
|
||||||
|
def __init__(self, section):
|
||||||
|
super().__init__(
|
||||||
|
"Configuration file contains an invalid section (section=%s)" % section)
|
||||||
|
|
||||||
|
class InvalidConfigKey(ConfigException):
|
||||||
|
"""Raised when the configuration file contains an invalid key."""
|
||||||
|
def __init__(self, section, key):
|
||||||
|
super().__init__(
|
||||||
|
"Configuration file contains an invalid key (key=%s.%s)" %
|
||||||
|
(section, key))
|
||||||
|
|
||||||
|
class InvalidConfigValue(ConfigException):
|
||||||
|
"""Raised when the configuration file contains an invalid value."""
|
||||||
|
def __init__(self, reason, section, key, value):
|
||||||
|
super().__init__(
|
||||||
|
"Configuration file contains an invalid value: %s (key=%s.%s, value=%s)" %
|
||||||
|
(reason, section, key, value))
|
||||||
|
|
||||||
|
|
||||||
|
class Config():
|
||||||
|
def __init__(self, path):
|
||||||
|
self._set_defaults()
|
||||||
|
parser = self._read(path)
|
||||||
|
self._build(parser)
|
||||||
|
self._resolve_db_connection_defaults()
|
||||||
|
|
||||||
|
def _set_defaults(self):
|
||||||
|
self.run_user = "postgres"
|
||||||
|
self.run_group = "postgres"
|
||||||
|
self.state_file = "/var/lib/pgbouncemgr/state.json"
|
||||||
|
self.poll_interval_in_sec = 2
|
||||||
|
self.db_connection_defaults = {
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 5432,
|
||||||
|
"connect_timeout": 1,
|
||||||
|
"user": "pgbouncemgr",
|
||||||
|
"password": None,
|
||||||
|
"name": "template1",
|
||||||
|
}
|
||||||
|
self.pgbouncer = {
|
||||||
|
"pgbouncer_config": "/etc/pgbouncer/pgbouncer.ini",
|
||||||
|
"port": 6432,
|
||||||
|
}
|
||||||
|
self.nodes = {}
|
||||||
|
|
||||||
|
def _read(self, path):
|
||||||
|
try:
|
||||||
|
with open(path, 'r') as stream:
|
||||||
|
return yaml.safe_load(stream)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise ConfigNotFound(path)
|
||||||
|
|
||||||
|
def _build(self, parser):
|
||||||
|
if not hasattr(parser, "items"):
|
||||||
|
return
|
||||||
|
for section, data in parser.items():
|
||||||
|
if section == "main":
|
||||||
|
self._build_main(data)
|
||||||
|
elif section == "pgbouncer":
|
||||||
|
self._build_pgbouncer(data)
|
||||||
|
elif section == "db_connection_defaults":
|
||||||
|
self._build_db_connection_defaults(data)
|
||||||
|
elif section == "nodes":
|
||||||
|
self._build_nodes(data)
|
||||||
|
else:
|
||||||
|
raise InvalidConfigSection(section)
|
||||||
|
|
||||||
|
def _build_main(self, data):
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
for key in data:
|
||||||
|
if not hasattr(self, key):
|
||||||
|
raise InvalidConfigKey("main", key)
|
||||||
|
value = data[key]
|
||||||
|
if key == "poll_interval_in_sec":
|
||||||
|
value = self._to_int("main", key, value)
|
||||||
|
else:
|
||||||
|
self._check_for_empty_value("main", key, value)
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def _build_pgbouncer(self, data):
|
||||||
|
for key in data:
|
||||||
|
if key not in self.pgbouncer:
|
||||||
|
raise InvalidConfigKey("pgbouncer", key)
|
||||||
|
value = data[key]
|
||||||
|
if key in ["port", "connect_timeout"]:
|
||||||
|
value = self._to_int("pgbouncer", key, value)
|
||||||
|
else:
|
||||||
|
self._check_for_empty_value("pgbouncer", key, value)
|
||||||
|
self.pgbouncer[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def _build_db_connection_defaults(self, data):
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
for key in data:
|
||||||
|
if key not in self.db_connection_defaults:
|
||||||
|
raise InvalidConfigKey("db_connection_defaults", key)
|
||||||
|
value = data[key]
|
||||||
|
if key in ["port", "connect_timeout"]:
|
||||||
|
value = self._to_int("db_connection_defaults", key, value)
|
||||||
|
else:
|
||||||
|
self._check_for_empty_value("db_connection_defaults", key, value)
|
||||||
|
self.db_connection_defaults[key] = value
|
||||||
|
|
||||||
|
def _build_nodes(self, data):
|
||||||
|
if not hasattr(data, "items"):
|
||||||
|
return
|
||||||
|
for node_name, node_data in data.items():
|
||||||
|
self._build_node(node_name, node_data)
|
||||||
|
|
||||||
|
def _build_node(self, node_name, data):
|
||||||
|
node = dict()
|
||||||
|
if not data:
|
||||||
|
return
|
||||||
|
for key in data:
|
||||||
|
value = data[key]
|
||||||
|
if key in ["port", "connect_timeout"]:
|
||||||
|
value = self._to_int("node", key, value)
|
||||||
|
else:
|
||||||
|
self._check_for_empty_value("node[%s]" % node_name, key, value)
|
||||||
|
node[key] = value
|
||||||
|
self.nodes[node_name] = node
|
||||||
|
|
||||||
|
def _resolve_db_connection_defaults(self):
|
||||||
|
# Resolve defaults for the node connections.
|
||||||
|
for node in self.nodes.values():
|
||||||
|
for key, default_value in self.db_connection_defaults.items():
|
||||||
|
if key not in node:
|
||||||
|
node[key] = default_value
|
||||||
|
|
||||||
|
# Resolve defaults for the pgbouncer console connection.
|
||||||
|
for key, default_value in self.db_connection_defaults.items():
|
||||||
|
if key not in self.pgbouncer or self.pgbouncer[key] is None:
|
||||||
|
self.pgbouncer[key] = default_value
|
||||||
|
|
||||||
|
def _to_int(self, section, key, value):
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise InvalidConfigValue("it must be an integer", section, key, value)
|
||||||
|
|
||||||
|
def _check_for_empty_value(self, section, key, value):
|
||||||
|
if value is None or (str(value)).strip() == "":
|
||||||
|
value = value if value is None else repr(value)
|
||||||
|
raise InvalidConfigValue("it must not be an empty value", section, key, value)
|
|
@ -44,7 +44,7 @@ class NodeCannotBePromoted(StateException):
|
||||||
class InvalidSystemId(StateException):
|
class InvalidSystemId(StateException):
|
||||||
"""Raised when an invalid system_id is used."""
|
"""Raised when an invalid system_id is used."""
|
||||||
def __init__(self, invalid):
|
def __init__(self, invalid):
|
||||||
display = "None" if invalid is None else "'%s'" % invalid
|
display = invalid if invalid is None else repr(invalid)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
"Invalid system_id provided; it must be a non-empty string " +
|
"Invalid system_id provided; it must be a non-empty string " +
|
||||||
"(invalid system_id=%s)" % display)
|
"(invalid system_id=%s)" % display)
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import json
|
||||||
|
from pgbouncemgr.config import *
|
||||||
|
|
||||||
|
def load_test_config(name):
|
||||||
|
path = os.path.join(
|
||||||
|
os.path.dirname(os.path.realpath(__file__)),
|
||||||
|
"testfiles", name)
|
||||||
|
return Config(path)
|
||||||
|
|
||||||
|
class ConfigTests(unittest.TestCase):
|
||||||
|
def test_ConfigThatDoesNotExist_RaisesException(self):
|
||||||
|
with self.assertRaises(ConfigNotFound) as context:
|
||||||
|
load_test_config("/path/doesnotexist.yaml")
|
||||||
|
self.assertIn("path=/path/doesnotexist.yaml", str(context.exception))
|
||||||
|
|
||||||
|
def test_ConfigContainingInvalidSection_RaisesException(self):
|
||||||
|
with self.assertRaises(InvalidConfigSection) as context:
|
||||||
|
load_test_config("invalid_section.yaml")
|
||||||
|
self.assertIn("section=unknown_section", str(context.exception))
|
||||||
|
|
||||||
|
def test_ConfigContainingInvalidKey_RaisesException(self):
|
||||||
|
with self.assertRaises(InvalidConfigKey) as context:
|
||||||
|
load_test_config("invalid_key.yaml")
|
||||||
|
self.assertIn("key=main.improbability_drive_level", str(context.exception))
|
||||||
|
|
||||||
|
def test_ConfigContainingInvalidIntegerValue_RaisesException(self):
|
||||||
|
with self.assertRaises(InvalidConfigValue) as context:
|
||||||
|
load_test_config("invalid_main_interval_in_sec.yaml")
|
||||||
|
self.assertIn("it must be an integer", str(context.exception))
|
||||||
|
self.assertIn("key=main.poll_interval_in_sec, value=ten seconds", str(context.exception))
|
||||||
|
|
||||||
|
def test_ConfigContainingEmptyValue_RaisesException(self):
|
||||||
|
with self.assertRaises(InvalidConfigValue) as context:
|
||||||
|
load_test_config("invalid_node_empty_host.yaml")
|
||||||
|
self.assertIn("it must not be an empty value", str(context.exception))
|
||||||
|
self.assertIn(r"key=node[nodeA].host, value=' \t\r\n '", str(context.exception))
|
||||||
|
|
||||||
|
def test_ConfigFromEmptyConfigurationFile_UsesDefaultValues(self):
|
||||||
|
config = load_test_config("empty.yaml")
|
||||||
|
# main
|
||||||
|
self.assertEqual("postgres", config.run_user)
|
||||||
|
self.assertEqual("postgres", config.run_group)
|
||||||
|
self.assertEqual("/var/lib/pgbouncemgr/state.json", config.state_file)
|
||||||
|
self.assertEqual(2, config.poll_interval_in_sec)
|
||||||
|
# db connection defaults
|
||||||
|
self.assertEqual("0.0.0.0", config.db_connection_defaults["host"])
|
||||||
|
self.assertEqual(5432, config.db_connection_defaults["port"])
|
||||||
|
self.assertEqual(1, config.db_connection_defaults["connect_timeout"])
|
||||||
|
self.assertEqual("pgbouncemgr", config.db_connection_defaults["user"])
|
||||||
|
self.assertEqual(None, config.db_connection_defaults["password"])
|
||||||
|
self.assertEqual("template1", config.db_connection_defaults["name"])
|
||||||
|
# pgbouncer
|
||||||
|
self.assertEqual("/etc/pgbouncer/pgbouncer.ini", config.pgbouncer["pgbouncer_config"])
|
||||||
|
self.assertEqual(6432, config.pgbouncer["port"])
|
||||||
|
# nodes
|
||||||
|
self.assertEqual({}, config.nodes)
|
||||||
|
|
||||||
|
def test_ConfigFromBasicConfigurationFile_DbConnectionDefaultsAreApplied(self):
|
||||||
|
config = load_test_config("basic.yaml")
|
||||||
|
self.assertEqual({
|
||||||
|
"connect_timeout": 1,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"name": "template1",
|
||||||
|
"password": "Wilmaaaaa!!!",
|
||||||
|
"pgbouncer_config": "/etc/pgbouncer/pgbouncer.ini",
|
||||||
|
"port": 6432,
|
||||||
|
"user": "pgbouncemgr"
|
||||||
|
}, config.pgbouncer)
|
||||||
|
self.assertEqual({
|
||||||
|
"nodeA": {
|
||||||
|
"connect_timeout": 1,
|
||||||
|
"host": "1.2.3.4",
|
||||||
|
"name": "template1",
|
||||||
|
"password": "Wilmaaaaa!!!",
|
||||||
|
"port": 8888,
|
||||||
|
"user": "pgbouncemgr"
|
||||||
|
},
|
||||||
|
"nodeB": {
|
||||||
|
"connect_timeout": 1,
|
||||||
|
"host": "2.3.4.5",
|
||||||
|
"name": "template1",
|
||||||
|
"password": "Wilmaaaaa!!!",
|
||||||
|
"port": 7777,
|
||||||
|
"user": "pgbouncemgr"
|
||||||
|
}
|
||||||
|
}, config.nodes)
|
|
@ -309,7 +309,7 @@ class NodeTests(unittest.TestCase):
|
||||||
|
|
||||||
with self.assertRaises(InvalidSystemId) as context:
|
with self.assertRaises(InvalidSystemId) as context:
|
||||||
node.system_id = " \t\r\n\t "
|
node.system_id = " \t\r\n\t "
|
||||||
self.assertIn("invalid system_id=' \t\r\n\t '", str(context.exception))
|
self.assertIn(r"invalid system_id=' \t\r\n\t '", str(context.exception))
|
||||||
self.assertFalse(state.modified)
|
self.assertFalse(state.modified)
|
||||||
|
|
||||||
def test_SetSystemId_SetsSystemId_AndNotifiesChangeToState(self):
|
def test_SetSystemId_SetsSystemId_AndNotifiesChangeToState(self):
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
---
|
||||||
|
db_connection_defaults:
|
||||||
|
port: 8888
|
||||||
|
password: Wilmaaaaa!!!
|
||||||
|
|
||||||
|
nodes:
|
||||||
|
nodeA:
|
||||||
|
host: 1.2.3.4
|
||||||
|
nodeB:
|
||||||
|
host: 2.3.4.5
|
||||||
|
port: 7777
|
|
@ -0,0 +1 @@
|
||||||
|
# Empty configuration file.
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
main:
|
||||||
|
improbability_drive_level: 42
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
main:
|
||||||
|
poll_interval_in_sec: ten seconds
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
nodes:
|
||||||
|
nodeA:
|
||||||
|
host: " \t\r\n "
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
main:
|
||||||
|
db_connection_defaults:
|
||||||
|
nodes:
|
||||||
|
unknown_section:
|
Loading…
Reference in New Issue