Bug#930776: unblock: ionit/0.3.2-1
Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
Usertags: unblock
Please unblock package ionit
ionit runs too late for /etc/network/interfaces (RC bug #919690). This
is fixed in 0.3.2-1. The debdiff is attached.
ionit is a quite new and very small tool (popcon count: 4), which is
developed and used by us. It has 100% test coverage (run at build time
and as autopkgtest).
unblock ionit/0.3.2-1
--
Benjamin Drung
System Developer
Debian & Ubuntu Developer
1&1 IONOS Cloud GmbH | Greifswalder Str. 207 | 10405 Berlin | Germany
E-mail: benjamin.drung@cloud.ionos.com | Web: www.ionos.de
Head Office: Berlin, Germany
District Court Berlin Charlottenburg, Registration number: HRB 125506 B
Executive Management: Christoph Steffens, Matthias Steinberg, Achim
Weiss
Member of United Internet
diff -Nru ionit-0.2.1/debian/changelog ionit-0.3.2/debian/changelog
--- ionit-0.2.1/debian/changelog 2019-01-07 14:22:30.000000000 +0100
+++ ionit-0.3.2/debian/changelog 2019-06-20 12:21:44.000000000 +0200
@@ -1,3 +1,13 @@
+ionit (0.3.2-1) unstable; urgency=medium
+
+ * New upstream release.
+ - Support specifying a configuration file
+ - Support specifying --config multiple times
+ - Run ionit.service before systemd-modules-load.service
+ - Run ionit.service before systemd-udev-trigger.service (Closes: #919690)
+
+ -- Benjamin Drung <benjamin.drung@cloud.ionos.com> Thu, 20 Jun 2019 12:21:44 +0200
+
ionit (0.2.1-1) unstable; urgency=medium
* New upstream release.
diff -Nru ionit-0.2.1/ionit ionit-0.3.2/ionit
--- ionit-0.2.1/ionit 2019-01-07 14:01:10.000000000 +0100
+++ ionit-0.3.2/ionit 2019-06-20 12:17:42.000000000 +0200
@@ -28,6 +28,7 @@
import ionit_plugin
+DEFAULT_CONFIG = "/etc/ionit"
LOG_FORMAT = '%(asctime)s %(name)s %(levelname)s: %(message)s'
SCRIPT_NAME = "ionit"
@@ -86,23 +87,34 @@
return context
-def collect_context(directory):
+def get_config_files(paths):
+ """Return files for the given paths (could either be files or directories)."""
+ logger = logging.getLogger(SCRIPT_NAME)
+ files = []
+ for path in paths:
+ logger.debug("Searching for configuration files in '%s'...", path)
+ try:
+ if os.path.isfile(path):
+ files.append(path)
+ else:
+ files += sorted([os.path.join(path, f) for f in os.listdir(path)])
+ except OSError as error:
+ logger.warning("Failed to read configuration directory: %s", error)
+ logger.debug("Configuration files: %s", files)
+ return files
+
+
+def collect_context(paths):
"""Collect context that will be used when rendering the templates"""
logger = logging.getLogger(SCRIPT_NAME)
- logger.debug("Collecting context from '%s'...", directory)
- try:
- files = sorted(os.listdir(directory))
- except OSError as error:
- logger.warning("Failed to read configuration directory: %s", error)
- files = []
+ logger.debug("Collecting context...")
failures = 0
context = {}
- for filename in files:
+ for file in get_config_files(paths):
file_context = None
- file = os.path.join(directory, filename)
- extension = os.path.splitext(filename)[1]
+ extension = os.path.splitext(file)[1]
try:
if extension == ".json":
logger.info("Reading configuration file '%s'...", file)
@@ -184,9 +196,9 @@
def main(argv):
"""Main function with argument parsing"""
parser = argparse.ArgumentParser()
- parser.add_argument("-c", "--config", default="/etc/ionit",
- help="Configuration directory containing context for rendering (default: "
- "%(default)s)")
+ parser.add_argument("-c", "--config", action="append",
+ help="Configuration directory/file containing context for rendering "
+ "(default: %s)" % (DEFAULT_CONFIG,))
parser.add_argument("-t", "--templates", default="/etc",
help="Directory to search for Jinja templates (default: %(default)s)")
parser.add_argument("-e", "--template-extension", default="jinja",
@@ -197,6 +209,8 @@
help="Decrease output verbosity to warnings and errors",
action="store_const", const=logging.WARNING)
args = parser.parse_args(argv)
+ if args.config is None:
+ args.config = [DEFAULT_CONFIG]
logging.basicConfig(level=args.log_level, format=LOG_FORMAT)
logger = logging.getLogger(SCRIPT_NAME)
diff -Nru ionit-0.2.1/ionit.py ionit-0.3.2/ionit.py
--- ionit-0.2.1/ionit.py 2019-01-07 14:01:10.000000000 +0100
+++ ionit-0.3.2/ionit.py 2019-06-20 12:17:42.000000000 +0200
@@ -28,6 +28,7 @@
import ionit_plugin
+DEFAULT_CONFIG = "/etc/ionit"
LOG_FORMAT = '%(asctime)s %(name)s %(levelname)s: %(message)s'
SCRIPT_NAME = "ionit"
@@ -86,23 +87,34 @@
return context
-def collect_context(directory):
+def get_config_files(paths):
+ """Return files for the given paths (could either be files or directories)."""
+ logger = logging.getLogger(SCRIPT_NAME)
+ files = []
+ for path in paths:
+ logger.debug("Searching for configuration files in '%s'...", path)
+ try:
+ if os.path.isfile(path):
+ files.append(path)
+ else:
+ files += sorted([os.path.join(path, f) for f in os.listdir(path)])
+ except OSError as error:
+ logger.warning("Failed to read configuration directory: %s", error)
+ logger.debug("Configuration files: %s", files)
+ return files
+
+
+def collect_context(paths):
"""Collect context that will be used when rendering the templates"""
logger = logging.getLogger(SCRIPT_NAME)
- logger.debug("Collecting context from '%s'...", directory)
- try:
- files = sorted(os.listdir(directory))
- except OSError as error:
- logger.warning("Failed to read configuration directory: %s", error)
- files = []
+ logger.debug("Collecting context...")
failures = 0
context = {}
- for filename in files:
+ for file in get_config_files(paths):
file_context = None
- file = os.path.join(directory, filename)
- extension = os.path.splitext(filename)[1]
+ extension = os.path.splitext(file)[1]
try:
if extension == ".json":
logger.info("Reading configuration file '%s'...", file)
@@ -184,9 +196,9 @@
def main(argv):
"""Main function with argument parsing"""
parser = argparse.ArgumentParser()
- parser.add_argument("-c", "--config", default="/etc/ionit",
- help="Configuration directory containing context for rendering (default: "
- "%(default)s)")
+ parser.add_argument("-c", "--config", action="append",
+ help="Configuration directory/file containing context for rendering "
+ "(default: %s)" % (DEFAULT_CONFIG,))
parser.add_argument("-t", "--templates", default="/etc",
help="Directory to search for Jinja templates (default: %(default)s)")
parser.add_argument("-e", "--template-extension", default="jinja",
@@ -197,6 +209,8 @@
help="Decrease output verbosity to warnings and errors",
action="store_const", const=logging.WARNING)
args = parser.parse_args(argv)
+ if args.config is None:
+ args.config = [DEFAULT_CONFIG]
logging.basicConfig(level=args.log_level, format=LOG_FORMAT)
logger = logging.getLogger(SCRIPT_NAME)
diff -Nru ionit-0.2.1/ionit.service ionit-0.3.2/ionit.service
--- ionit-0.2.1/ionit.service 2019-01-07 14:01:10.000000000 +0100
+++ ionit-0.3.2/ionit.service 2019-06-20 12:17:42.000000000 +0200
@@ -3,7 +3,7 @@
Documentation=man:ionit(1)
DefaultDependencies=no
After=local-fs.target
-Before=ferm.service network-pre.target openibd.service shutdown.target sysinit.target
+Before=ferm.service network-pre.target openibd.service shutdown.target sysinit.target systemd-modules-load.service systemd-udev-trigger.service
Wants=network-pre.target
RequiresMountsFor=/usr
diff -Nru ionit-0.2.1/NEWS ionit-0.3.2/NEWS
--- ionit-0.2.1/NEWS 2019-01-07 14:01:10.000000000 +0100
+++ ionit-0.3.2/NEWS 2019-06-20 12:17:42.000000000 +0200
@@ -1,3 +1,17 @@
+ionit 0.3.2 (2019-06-20)
+
+* Run ionit.service before systemd-udev-trigger.service
+ (fixes Debian bug #919690)
+
+ionit 0.3.1 (2019-04-11)
+
+* Run ionit.service before systemd-modules-load.service
+
+ionit 0.3.0 (2019-02-20)
+
+* Support specifying a configuration file (instead of a directory)
+* Support specifying --config multiple times
+
ionit 0.2.1 (2019-01-07)
* Remove unnecessary pass statement to make pylint 2.2.2 happy
diff -Nru ionit-0.2.1/setup.py ionit-0.3.2/setup.py
--- ionit-0.2.1/setup.py 2019-01-07 14:01:10.000000000 +0100
+++ ionit-0.3.2/setup.py 2019-06-20 12:17:42.000000000 +0200
@@ -34,7 +34,7 @@
if __name__ == "__main__":
setup(
name="ionit",
- version="0.2.1",
+ version="0.3.2",
description="Render configuration files from Jinja templates",
long_description=(
"ionit is a simple and small configuration templating tool. It collects a context and "
diff -Nru ionit-0.2.1/tests/test_ionit.py ionit-0.3.2/tests/test_ionit.py
--- ionit-0.2.1/tests/test_ionit.py 2019-01-07 14:01:10.000000000 +0100
+++ ionit-0.3.2/tests/test_ionit.py 2019-06-20 12:17:42.000000000 +0200
@@ -33,30 +33,35 @@
"""
def test_collect_function(self):
- """Test: Run collect_context("tests/config/function")"""
- failures, context = collect_context(os.path.join(CONFIG_DIR, "function"))
+ """Test: Run collect_context(["tests/config/function"])"""
+ failures, context = collect_context([os.path.join(CONFIG_DIR, "function")])
self.assertEqual(failures, 0)
self.assertEqual(set(context.keys()), set(["answer_to_all_questions"]))
self.assertEqual(context["answer_to_all_questions"](), 42)
def test_collect_static_context(self):
- """Test: Run collect_context("tests/config/static")"""
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "static")), (0, {
+ """Test: Run collect_context(["tests/config/static"])"""
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "static")]), (0, {
"first": 1,
"second": 2,
}))
+ def test_configuration_file(self):
+ """Test: Run collect_context(["tests/config/static/second.yaml"])"""
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "static", "second.yaml")]),
+ (0, {"second": 2}))
+
def test_context_stacking(self):
- """Test: Run collect_context("tests/config/stacking")"""
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "stacking")), (0, {
+ """Test: Run collect_context(["tests/config/stacking"])"""
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "stacking")]), (0, {
"big_number": 1071,
"small_number": 7,
}))
def test_empty_python_file(self):
- """Test: Run collect_context("tests/config/empty")"""
+ """Test: Run collect_context(["tests/config/empty"])"""
with self.assertLogs("ionit", level="WARNING") as context_manager:
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "empty")), (0, {}))
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "empty")]), (0, {}))
self.assertEqual(len(context_manager.output), 1)
self.assertRegex(context_manager.output[0], (
"WARNING:ionit:Python module '[^']+config/empty/empty.py' does "
@@ -64,10 +69,10 @@
r"\(using the ionit_plugin.function decorator\)."))
def test_empty_context(self):
- """Test: Run collect_context("tests/config/empty-context")"""
+ """Test: Run collect_context(["tests/config/empty-context"])"""
try:
with self.assertLogs("ionit", level="WARNING") as context_manager:
- failures, context = collect_context(os.path.join(CONFIG_DIR, "empty-context"))
+ failures, context = collect_context([os.path.join(CONFIG_DIR, "empty-context")])
except AssertionError:
pass
self.assertEqual(failures, 0)
@@ -75,9 +80,9 @@
self.assertEqual(context_manager.output, [])
def test_ignoring_additional_files(self):
- """Test: Run collect_context("tests/config/additional-file")"""
+ """Test: Run collect_context(["tests/config/additional-file"])"""
with self.assertLogs("ionit", level="INFO") as context_manager:
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "additional-file")),
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "additional-file")]),
(0, {"key": "value"}))
self.assertEqual(len(context_manager.output), 2)
self.assertRegex(context_manager.output[0], (
@@ -85,18 +90,19 @@
"because it does not end with .*"))
def test_invalid_json(self):
- """Test: Run collect_context("tests/config/invalid-json")"""
+ """Test: Run collect_context(["tests/config/invalid-json"])"""
with self.assertLogs("ionit", level="ERROR") as context_manager:
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "invalid-json")), (1, {}))
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "invalid-json")]), (1, {}))
self.assertEqual(len(context_manager.output), 1)
self.assertRegex(context_manager.output[0], (
"ERROR:ionit:Failed to read JSON from '[^']*config/invalid-json/invalid.json': "
r"Expecting property name enclosed in double quotes: line 3 column 1 \(char 22\)"))
def test_invalid_python(self):
- """Test: Run collect_context("tests/config/invalid-python")"""
+ """Test: Run collect_context(["tests/config/invalid-python"])"""
with self.assertLogs("ionit", level="ERROR") as context_manager:
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "invalid-python")), (1, {}))
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "invalid-python")]),
+ (1, {}))
self.assertEqual(len(context_manager.output), 1)
self.assertRegex(context_manager.output[0], re.compile(
"ERROR:ionit:Importing Python module '[^']*config/invalid-python/invalid.py' "
@@ -104,9 +110,9 @@
flags=re.DOTALL))
def test_invalid_yaml(self):
- """Test: Run collect_context("tests/config/invalid-yaml")"""
+ """Test: Run collect_context(["tests/config/invalid-yaml"])"""
with self.assertLogs("ionit", level="ERROR") as context_manager:
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "invalid-yaml")), (1, {}))
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "invalid-yaml")]), (1, {}))
self.assertEqual(len(context_manager.output), 1)
self.assertRegex(context_manager.output[0], (
"ERROR:ionit:Failed to read YAML from '[^']*config/invalid-yaml/invalid.yaml': "
@@ -116,7 +122,7 @@
def test_missing_directory(self):
"""Test: Non-existing context directory"""
with self.assertLogs("ionit", level="WARNING") as context_manager:
- self.assertEqual(collect_context(os.path.join(TESTS_DIR, "non-existing-directory")),
+ self.assertEqual(collect_context([os.path.join(TESTS_DIR, "non-existing-directory")]),
(0, {}))
self.assertEqual(len(context_manager.output), 1)
self.assertRegex(context_manager.output[0], (
@@ -124,9 +130,9 @@
r"No such file or directory: '\S*non-existing-directory'"))
def test_non_dict_context(self):
- """Test failure for collect_context("tests/config/non-dict")"""
+ """Test failure for collect_context(["tests/config/non-dict"])"""
with self.assertLogs("ionit", level="ERROR") as context_manager:
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "non-dict")), (1, {}))
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "non-dict")]), (1, {}))
self.assertEqual(len(context_manager.output), 1)
self.assertRegex(context_manager.output[0], (
"ERROR:ionit:Failed to update context with content from "
@@ -134,16 +140,16 @@
"element #0 has length 1; 2 is required"))
def test_python_module(self):
- """Test: Run collect_context("tests/config/python")"""
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "python")), (0, {
+ """Test: Run collect_context(["tests/config/python"])"""
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "python")]), (0, {
"small": 42,
"big": 8000,
}))
def test_raise_exception(self):
- """Test failure for collect_context("tests/config/exception")"""
+ """Test failure for collect_context(["tests/config/exception"])"""
with self.assertLogs("ionit", level="ERROR") as context_manager:
- self.assertEqual(collect_context(os.path.join(CONFIG_DIR, "exception")), (1, {}))
+ self.assertEqual(collect_context([os.path.join(CONFIG_DIR, "exception")]), (1, {}))
self.assertEqual(len(context_manager.output), 1)
self.assertRegex(context_manager.output[0], re.compile(
r"ERROR:ionit:Calling collect_context\(\) from '\S*config/exception/exception.py' "
@@ -230,6 +236,19 @@
class TestMain(unittest.TestCase):
"""Test main function"""
+ @unittest.mock.patch("ionit.DEFAULT_CONFIG", os.path.join(CONFIG_DIR, "function"))
+ def test_main_default_config(self):
+ """Test main() with default config"""
+ template_dir = os.path.join(TEMPLATE_DIR, "function")
+ try:
+ self.assertEqual(main(["-t", template_dir]), 0)
+ with open(os.path.join(template_dir, "Document")) as document_file:
+ self.assertEqual(document_file.read(), (
+ "The answer to the Ultimate Question of Life, The Universe, "
+ "and Everything is 42.\n"))
+ finally:
+ os.remove(os.path.join(template_dir, "Document"))
+
def test_main_static(self):
"""Test main() with static context"""
template_dir = os.path.join(TEMPLATE_DIR, "static")
Reply to: