Skip to content

Instantly share code, notes, and snippets.

@mgedmin
Last active February 6, 2024 14:21
Show Gist options
  • Save mgedmin/5f8ac034df0c371444be to your computer and use it in GitHub Desktop.
Save mgedmin/5f8ac034df0c371444be to your computer and use it in GitHub Desktop.
Ansible module for postfix configuration
#!/usr/bin/python
import subprocess
DOCUMENTATION = '''
---
module: postfix
short_description: changes postfix configuration parameters
description:
- The M(postfix) module changes postfix configuration by invoking 'postconf'.
This is needed if you don't want to use M(template) for the entire main.cf,
because M(lineinfile) cannot handle multi-line configuration values, and
solutions involving M(command) are cumbersone or don't work correctly
in check mode.
- Be sure to run C(postfix reload) (or, for settings like inet_interfaces,
C(service postfix restart)) afterwards.
options:
name:
description:
- the name of the setting
required: true
default: null
value:
description:
- the value for that setting
required: true
default: null
author:
- Marius Gedminas <[email protected]>
'''
EXAMPLES = '''
- postfix: name=myhostname value={{ ansible_fqdn }}
- postfix: name=mynetworks value="127.0.0.0/8, [::1]/128, 192.168.1.0/24"
- postfix: name={{ item.name }} value="{{ item.value }}"
with_items:
- { name: inet_interfaces, value: loopback-only }
- { name: inet_protocols, value: ipv4 }
'''
def run(args, module):
try:
cmd = subprocess.Popen(args, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = cmd.communicate()
rc = cmd.returncode
except (OSError, IOError) as e:
module.fail_json(rc=e.errno, msg=str(e), cmd=args)
if rc != 0 or err:
module.fail_json(rc=rc, msg=err, cmd=args)
if not isinstance(out, str):
out = out.decode('UTF-8')
return out
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
value=dict(type='str', required=True),
),
supports_check_mode=True,
)
name = module.params['name']
value = ' '.join(module.params['value'].split())
old_value = run(['postconf', '-h', name], module).strip()
if value == old_value:
module.exit_json(
msg="",
changed=False,
)
if not module.check_mode:
run(['postconf', '{}={}'.format(name, value)], module)
module.exit_json(
msg="setting changed",
diff=dict(
before_header='postconf -h {}'.format(name),
after_header='postconf -h {}'.format(name),
before=old_value + '\n',
after=value + '\n'),
changed=True,
)
from ansible.module_utils.basic import * # noqa
if __name__ == '__main__':
main()
@mgedmin
Copy link
Author

mgedmin commented Sep 13, 2019

You can drop this into library/postfix.py and then use it like this:

- name: make sure postfix is not accessible externally
  postfix: name=inet_interfaces value=loopback-only

@FibreFoX
Copy link

Thanks for sharing this, it really is annoying that this is not part of ansible-core. Still works running Debian Buster provided Ansible 2.7.7.

@DasSkelett
Copy link

DasSkelett commented May 5, 2021

Thanks a lot for this module, saved me a bunch of work! Only getting a small exception running it:

Resolved (see comment below), expand for details
Traceback (most recent call last):
  File \"/root/.ansible/tmp/ansible-tmp-1620216737.1389186-12444-250592887051818/AnsiballZ_postfix.py\", line 102, in <module>
    _ansiballz_main()
  File \"/root/.ansible/tmp/ansible-tmp-1620216737.1389186-12444-250592887051818/AnsiballZ_postfix.py\", line 94, in _ansiballz_main
    invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)
  File \"/root/.ansible/tmp/ansible-tmp-1620216737.1389186-12444-250592887051818/AnsiballZ_postfix.py\", line 40, in invoke_module
    runpy.run_module(mod_name='ansible.modules.postfix', init_globals=None, run_name='__main__', alter_sys=True)
  File \"/usr/lib/python3.8/runpy.py\", line 207, in run_module
    return _run_module_code(code, init_globals, run_name, mod_spec)
  File \"/usr/lib/python3.8/runpy.py\", line 97, in _run_module_code
    _run_code(code, mod_globals, init_globals,
  File \"/usr/lib/python3.8/runpy.py\", line 87, in _run_code
    exec(code, run_globals)
  File \"/tmp/ansible_postfix_payload_8ia_r6da/ansible_postfix_payload.zip/ansible/modules/postfix.py\", line 89, in <module>
  File \"/tmp/ansible_postfix_payload_8ia_r6da/ansible_postfix_payload.zip/ansible/modules/postfix.py\", line 79, in main
TypeError: can't concat str to bytes

I think this is caused by some changes in subprocess in Python3.6/3.8. It returns bytes instead of strings by default now:

If encoding or errors are specified, or text is true, the file objects stdin, stdout and stderr are opened in text mode with the specified encoding and errors, as described above in Frequently Used Arguments. The universal_newlines argument is equivalent to text and is provided for backwards compatibility. By default, file objects are opened in binary mode.

  • New in version 3.6: encoding and errors were added.
  • New in version 3.7: text was added as a more readable alias for universal_newlines.

https://docs.python.org/3/library/subprocess.html#popen-constructor

It was easy enough to fix by setting text=True in the subprocess.Popen() call:

diff --git a/library/postfix.py b/library/postfix.py
index ba95d12..7ccfc1a 100644
--- a/library/postfix.py
+++ b/library/postfix.py
@@ -43,7 +43,7 @@ EXAMPLES = '''

def run(args, module):
try:
-        cmd = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        cmd = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
out, err = cmd.communicate()
rc = cmd.returncode
except (OSError, IOError) as e:

My environment:

[ansible-host] $ ansible --version
ansible 2.10.5
    python version = 3.9.4 (default, Apr  4 2021, 19:38:44) [GCC 10.2.1 20210401]
[target-server] $ python3 --version
Python 3.8.5

@mgedmin
Copy link
Author

mgedmin commented May 5, 2021

Ahh, yes, sorry, I've fixed this in my private git repo but forgot to update the gist. I've pushed the latest version that works for me now, it should support both Python 2 and Python 3 without relying on subprocess.Popen() having a text argument.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment