From 2a83142d9539efc237aa9be518be4c02e73a54a5 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 10 Sep 2024 14:05:13 -0500 Subject: [PATCH 1/2] feat: add basic postfix config helpers --- docs/api/wuttamess.postfix.rst | 6 +++ docs/index.rst | 1 + src/wuttamess/postfix.py | 67 ++++++++++++++++++++++++++++++++++ tests/test_postfix.py | 54 +++++++++++++++++++++++++++ 4 files changed, 128 insertions(+) create mode 100644 docs/api/wuttamess.postfix.rst create mode 100644 src/wuttamess/postfix.py create mode 100644 tests/test_postfix.py diff --git a/docs/api/wuttamess.postfix.rst b/docs/api/wuttamess.postfix.rst new file mode 100644 index 0000000..c5b9132 --- /dev/null +++ b/docs/api/wuttamess.postfix.rst @@ -0,0 +1,6 @@ + +``wuttamess.postfix`` +===================== + +.. automodule:: wuttamess.postfix + :members: diff --git a/docs/index.rst b/docs/index.rst index 3869c2e..50e3061 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,4 +31,5 @@ project. api/wuttamess api/wuttamess.apt + api/wuttamess.postfix api/wuttamess.sync diff --git a/src/wuttamess/postfix.py b/src/wuttamess/postfix.py new file mode 100644 index 0000000..e5e9730 --- /dev/null +++ b/src/wuttamess/postfix.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# WuttaMess -- Fabric Automation Helpers +# Copyright © 2024 Lance Edgar +# +# This file is part of Wutta Framework. +# +# Wutta Framework is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Wutta Framework is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# Wutta Framework. If not, see . +# +################################################################################ +""" +Postfix mail service +""" + + +def set_config(c, setting, value): + """ + Configure the given setting with the given value. + """ + c.run(f"postconf -e '{setting}={value}'") + + +def set_myhostname(c, hostname): + """ + Configure the ``myhostname`` setting with the given string. + """ + set_config(c, 'myhostname', hostname) + + +def set_myorigin(c, origin): + """ + Configure the ``myorigin`` setting with the given string. + """ + set_config(c, 'myorigin', origin) + + +def set_mydestination(c, *destinations): + """ + Configure the ``mydestinations`` setting with the given strings. + """ + set_config(c, 'mydestination', ', '.join(destinations)) + + +def set_mynetworks(c, *networks): + """ + Configure the ``mynetworks`` setting with the given strings. + """ + set_config(c, 'mynetworks', ' '.join(networks)) + + +def set_relayhost(c, relayhost): + """ + Configure the ``relayhost`` setting with the given string + """ + set_config(c, 'relayhost', relayhost) diff --git a/tests/test_postfix.py b/tests/test_postfix.py new file mode 100644 index 0000000..1f75253 --- /dev/null +++ b/tests/test_postfix.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8; -*- + +from unittest import TestCase +from unittest.mock import MagicMock + +from wuttamess import postfix as mod + + +class TestSetConfig(TestCase): + + def test_basic(self): + c = MagicMock() + mod.set_config(c, 'foo', 'bar') + c.run.assert_called_once_with("postconf -e 'foo=bar'") + + +class TestSetMyhostname(TestCase): + + def test_basic(self): + c = MagicMock() + mod.set_myhostname(c, 'test.example.com') + c.run.assert_called_once_with("postconf -e 'myhostname=test.example.com'") + + +class TestSetMyorigin(TestCase): + + def test_basic(self): + c = MagicMock() + mod.set_myorigin(c, 'example.com') + c.run.assert_called_once_with("postconf -e 'myorigin=example.com'") + + +class TestSetMydestination(TestCase): + + def test_basic(self): + c = MagicMock() + mod.set_mydestination(c, 'example.com', 'test.example.com', 'localhost') + c.run.assert_called_once_with("postconf -e 'mydestination=example.com, test.example.com, localhost'") + + +class TestSetMynetworks(TestCase): + + def test_basic(self): + c = MagicMock() + mod.set_mynetworks(c, '127.0.0.0/8', '[::1]/128') + c.run.assert_called_once_with("postconf -e 'mynetworks=127.0.0.0/8 [::1]/128'") + + +class TestSetRelayhost(TestCase): + + def test_basic(self): + c = MagicMock() + mod.set_relayhost(c, 'mail.example.com') + c.run.assert_called_once_with("postconf -e 'relayhost=mail.example.com'") From e3b593d62870bd3d4369dbed1ced1f4f7a39011e Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 10 Sep 2024 20:10:15 -0500 Subject: [PATCH 2/2] fix: add `sync.make_selector()` convenience function wraps `fabsync.ItemSelector.new()` --- src/wuttamess/sync.py | 32 ++++++++++++++++++++++++++++---- tests/test_sync.py | 18 +++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/wuttamess/sync.py b/src/wuttamess/sync.py index fa48447..6147ca7 100644 --- a/src/wuttamess/sync.py +++ b/src/wuttamess/sync.py @@ -44,20 +44,41 @@ def make_root(path, dest='/'): return fabsync.load(path, dest) -def isync(c, root, selector=None, echo=True, **kwargs): +def make_selector(subpath=None, **kwargs): + """ + Make and return an "item selector" for use with a sync call. + + This is a convenience wrapper around + :meth:`fabsync:fabsync.ItemSelector.new()`. + + :param subpath: (Optional) Relative subpath of the file tree to + sync, e.g. ``'etc/postfix'``. + + :param tags: Optional iterable of tags to include; excluding any + files which are not so tagged. E.g. ``{'foo'}`` + """ + return fabsync.ItemSelector.new(subpath, **kwargs) + + +def isync(c, root, selector=None, tags=None, echo=True, **kwargs): """ Sync files, yielding the result for each as it goes. This is a convenience wrapper around :func:`fabsync:fabsync.isync()`. - :param c: Connection object. + :param c: Fabric connection. :param root: File tree "root" object as obtained from :func:`make_root()`. :param selector: This can be a simple "subpath" string, indicating - a section of the file tree. For instance: ``'etc/postfix'`` + a section of the file tree (e.g. ``'etc/postfix'``). Or can be + a :class:`fabsync.ItemSelector` instance. + + :param tags: Optional iterable of tags to select. If ``selector`` + is a subpath string, and you specify ``tags`` then they will be + included when creating the actual selector. :param echo: Flag indicating whether the path for each file synced should be echoed to stdout. Generally thought to be useful but @@ -68,7 +89,10 @@ def isync(c, root, selector=None, echo=True, **kwargs): """ if selector: if not isinstance(selector, fabsync.ItemSelector): - selector = fabsync.ItemSelector.new(selector) + kw = {} + if tags: + kw['tags'] = tags + selector = make_selector(selector, **kw) kwargs['selector'] = selector for result in fabsync.isync(c, root, **kwargs): diff --git a/tests/test_sync.py b/tests/test_sync.py index 23f4714..0a11084 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -18,6 +18,14 @@ class TestMakeRoot(TestCase): self.assertEqual(root.dest, Path('/')) +class TestMakeSelector(TestCase): + + def test_basic(self): + selector = mod.make_selector('etc/postfix') + self.assertIsInstance(selector, ItemSelector) + self.assertEqual(selector.subpath, Path('etc/postfix')) + + class TestIsync(TestCase): def test_basic(self): @@ -40,7 +48,7 @@ class TestIsync(TestCase): self.assertEqual(results, [result]) fabsync.isync.assert_called_once_with(c, root) - # sync with selector + # sync with selector (subpath) fabsync.isync.reset_mock() result = MagicMock(path='/foo', modified=True) fabsync.isync.return_value = [result] @@ -48,6 +56,14 @@ class TestIsync(TestCase): self.assertEqual(results, [result]) fabsync.isync.assert_called_once_with(c, root, selector=fabsync.ItemSelector.new('foo')) + # sync with selector (subpath + tags) + fabsync.isync.reset_mock() + result = MagicMock(path='/foo', modified=True) + fabsync.isync.return_value = [result] + results = list(mod.isync(c, root, 'foo', tags={'bar'})) + self.assertEqual(results, [result]) + fabsync.isync.assert_called_once_with(c, root, selector=fabsync.ItemSelector.new('foo', tags={'bar'})) + class TestCheckIsync(TestCase):