Mini Shell
"""
Support for Postfix
This module is currently little more than a config file viewer and editor. It
is able to read the master.cf file (which is one style) and files in the style
of main.cf (which is a different style, that is used in multiple postfix
configuration files).
The design of this module is such that when files are edited, a minimum of
changes are made to them. Each file should look as if it has been edited by
hand; order, comments and whitespace are all preserved.
"""
import logging
import re
import salt.utils.files
import salt.utils.path
import salt.utils.stringutils
SWWS = re.compile(r"^\s")
log = logging.getLogger(__name__)
MAIN_CF = "/etc/postfix/main.cf"
MASTER_CF = "/etc/postfix/master.cf"
def __virtual__():
"""
Only load the module if Postfix is installed
"""
if salt.utils.path.which("postfix"):
return True
return (False, "postfix execution module not loaded: postfix not installed.")
def _parse_master(path=MASTER_CF):
"""
Parse the master.cf file. This file is essentially a whitespace-delimited
columnar file. The columns are: service, type, private (yes), unpriv (yes),
chroot (yes), wakeup (never), maxproc (100), command + args.
This function parses out the columns, leaving empty lines and comments
intact. Where the value doesn't detract from the default, a dash (-) will
be used.
Returns a dict of the active config lines, and a list of the entire file,
in order. These compliment each other.
"""
with salt.utils.files.fopen(path, "r") as fh_:
full_conf = salt.utils.stringutils.to_unicode(fh_.read())
# Condense the file based on line continuations, but keep order, comments
# and whitespace
conf_list = []
conf_dict = {}
for line in full_conf.splitlines():
if not line.strip() or line.strip().startswith("#"):
conf_list.append(line)
continue
comps = line.strip().split()
conf_line = {
"service": comps[0],
"conn_type": comps[1],
"private": comps[2],
"unpriv": comps[3],
"chroot": comps[4],
"wakeup": comps[5],
"maxproc": comps[6],
"command": " ".join(comps[7:]),
}
dict_key = f"{comps[0]} {comps[1]}"
conf_list.append(conf_line)
conf_dict[dict_key] = conf_line
return conf_dict, conf_list
def show_master(path=MASTER_CF):
"""
Return a dict of active config values. This does not include comments,
spacing or order.
The data returned from this function should not be used for direct
modification of the main.cf file; other functions are available for that.
CLI Examples:
.. code-block:: bash
salt <minion> postfix.show_master
salt <minion> postfix.show_master path=/path/to/master.cf
"""
conf_dict, conf_list = _parse_master(path) # pylint: disable=W0612
return conf_dict
def set_master(
service,
conn_type,
private="y",
unpriv="y",
chroot="y",
wakeup="n",
maxproc="100",
command="",
write_conf=True,
path=MASTER_CF,
):
"""
Set a single config value in the master.cf file. If the value does not
already exist, it will be appended to the end.
Because of shell parsing issues, '-' cannot be set as a value, as is normal
in the master.cf file; either 'y', 'n' or a number should be used when
calling this function from the command line. If the value used matches the
default, it will internally be converted to a '-'. Calling this function
from the Python API is not affected by this limitation
The settings and their default values, in order, are: service (required),
conn_type (required), private (y), unpriv (y), chroot (y), wakeup (n),
maxproc (100), command (required).
By default, this function will write out the changes to the master.cf file,
and then returns the full contents of the file. By setting the
``write_conf`` option to ``False``, it will skip writing the file.
CLI Example:
.. code-block:: bash
salt <minion> postfix.set_master smtp inet n y n n 100 smtpd
"""
conf_dict, conf_list = _parse_master(path)
new_conf = []
dict_key = f"{service} {conn_type}"
new_line = _format_master(
service,
conn_type,
private,
unpriv,
chroot,
wakeup,
maxproc,
command,
)
for line in conf_list:
if isinstance(line, dict):
if line["service"] == service and line["conn_type"] == conn_type:
# This is the one line that we're changing
new_conf.append(new_line)
else:
# No changes to this line, but it still needs to be
# formatted properly
new_conf.append(_format_master(**line))
else:
# This line is a comment or is empty
new_conf.append(line)
if dict_key not in conf_dict:
# This config value does not exist, so append it to the end
new_conf.append(new_line)
if write_conf:
_write_conf(new_conf, path)
return "\n".join(new_conf)
def _format_master(
service, conn_type, private, unpriv, chroot, wakeup, maxproc, command
):
"""
Format the given values into the style of line normally used in the
master.cf file.
"""
# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (yes) (never) (100)
# ==========================================================================
# smtp inet n - n - - smtpd
if private == "y":
private = "-"
if unpriv == "y":
unpriv = "-"
if chroot == "y":
chroot = "-"
if wakeup == "n":
wakeup = "-"
maxproc = str(maxproc)
if maxproc == "100":
maxproc = "-"
conf_line = "{:9s} {:5s} {:7s} {:7s} {:7s} {:7s} {:7s} {}".format(
service,
conn_type,
private,
unpriv,
chroot,
wakeup,
maxproc,
command,
)
# print(conf_line)
return conf_line
def _parse_main(path=MAIN_CF):
"""
Parse files in the style of main.cf. This is not just a "name = value" file;
there are other rules:
* Comments start with #
* Any whitespace at the beginning of a line denotes that that line is a
continuation from the previous line.
* The whitespace rule applies to comments.
* Keys defined in the file may be referred to as variables further down in
the file.
"""
with salt.utils.files.fopen(path, "r") as fh_:
full_conf = salt.utils.stringutils.to_unicode(fh_.read())
# Condense the file based on line continuations, but keep order, comments
# and whitespace
conf_list = []
for line in full_conf.splitlines():
if not line.strip():
conf_list.append(line)
continue
if re.match(SWWS, line):
if not conf_list:
# This should only happen at the top of the file
conf_list.append(line)
continue
if not isinstance(conf_list[-1], str):
conf_list[-1] = ""
# This line is a continuation of the previous line
conf_list[-1] = "\n".join([conf_list[-1], line])
else:
conf_list.append(line)
# Extract just the actual key/value pairs
pairs = {}
for line in conf_list:
if not line.strip():
continue
if line.startswith("#"):
continue
comps = line.split("=")
pairs[comps[0].strip()] = "=".join(comps[1:]).strip()
# Return both sets of data, they compliment each other elsewhere
return pairs, conf_list
def show_main(path=MAIN_CF):
"""
Return a dict of active config values. This does not include comments,
spacing or order. Bear in mind that order is functionally important in the
main.cf file, since keys can be referred to as variables. This means that
the data returned from this function should not be used for direct
modification of the main.cf file; other functions are available for that.
CLI Examples:
.. code-block:: bash
salt <minion> postfix.show_main
salt <minion> postfix.show_main path=/path/to/main.cf
"""
pairs, conf_list = _parse_main(path) # pylint: disable=W0612
return pairs
def set_main(key, value, path=MAIN_CF):
"""
Set a single config value in the main.cf file. If the value does not already
exist, it will be appended to the end.
CLI Example:
.. code-block:: bash
salt <minion> postfix.set_main mailq_path /usr/bin/mailq
"""
pairs, conf_list = _parse_main(path)
new_conf = []
key_line_match = re.compile(f"^{re.escape(key)}([\\s=]|$)")
if key in pairs:
for line in conf_list:
if re.match(key_line_match, line):
new_conf.append(f"{key} = {value}")
else:
new_conf.append(line)
else:
conf_list.append(f"{key} = {value}")
new_conf = conf_list
_write_conf(new_conf, path)
return new_conf
def _write_conf(conf, path=MAIN_CF):
"""
Write out configuration file.
"""
with salt.utils.files.fopen(path, "w") as fh_:
for line in conf:
line = salt.utils.stringutils.to_str(line)
if isinstance(line, dict):
fh_.write(" ".join(line))
else:
fh_.write(line)
fh_.write("\n")
def show_queue():
"""
Show contents of the mail queue
CLI Example:
.. code-block:: bash
salt '*' postfix.show_queue
"""
cmd = "mailq"
out = __salt__["cmd.run"](cmd).splitlines()
queue = []
queue_pattern = re.compile(
r"(?P<queue_id>^[A-Z0-9]+)\s+(?P<size>\d+)\s(?P<timestamp>\w{3}\s\w{3}\s\d{1,2}\s\d{2}\:\d{2}\:\d{2})\s+(?P<sender>.+)"
)
recipient_pattern = re.compile(r"^\s+(?P<recipient>.+)")
queue_id, size, timestamp, sender, recipient = None, None, None, None, None
for line in out:
if re.match("^[-|postqueue:|Mail]", line):
# discard in-queue wrapper
continue
if re.match(queue_pattern, line):
m = re.match(queue_pattern, line)
queue_id = m.group("queue_id")
size = m.group("size")
timestamp = m.group("timestamp")
sender = m.group("sender")
elif re.match(recipient_pattern, line): # recipient/s
m = re.match(recipient_pattern, line)
recipient = m.group("recipient")
elif not line: # end of record
if all((queue_id, size, timestamp, sender, recipient)):
queue.append(
{
"queue_id": queue_id,
"size": size,
"timestamp": timestamp,
"sender": sender,
"recipient": recipient,
}
)
return queue
def delete(queue_id):
"""
Delete message(s) from the mail queue
CLI Example:
.. code-block:: bash
salt '*' postfix.delete 5C33CA0DEA
salt '*' postfix.delete ALL
"""
ret = {"message": "", "result": True}
if not queue_id:
log.error("Require argument queue_id")
if not queue_id == "ALL":
queue = show_queue()
_message = None
for item in queue:
if item["queue_id"] == queue_id:
_message = item
if not _message:
ret["message"] = f"No message in queue with ID {queue_id}"
ret["result"] = False
return ret
cmd = f"postsuper -d {queue_id}"
result = __salt__["cmd.run_all"](cmd)
if result["retcode"] == 0:
if queue_id == "ALL":
ret["message"] = "Successfully removed all messages"
else:
ret["message"] = "Successfully removed message with queue id {}".format(
queue_id
)
else:
if queue_id == "ALL":
ret["message"] = "Unable to removed all messages"
else:
ret["message"] = "Unable to remove message with queue id {}: {}".format(
queue_id, result["stderr"]
)
return ret
def hold(queue_id):
"""
Put message(s) on hold from the mail queue
CLI Example:
.. code-block:: bash
salt '*' postfix.hold 5C33CA0DEA
salt '*' postfix.hold ALL
"""
ret = {"message": "", "result": True}
if not queue_id:
log.error("Require argument queue_id")
if not queue_id == "ALL":
queue = show_queue()
_message = None
for item in queue:
if item["queue_id"] == queue_id:
_message = item
if not _message:
ret["message"] = f"No message in queue with ID {queue_id}"
ret["result"] = False
return ret
cmd = f"postsuper -h {queue_id}"
result = __salt__["cmd.run_all"](cmd)
if result["retcode"] == 0:
if queue_id == "ALL":
ret["message"] = "Successfully placed all messages on hold"
else:
ret["message"] = (
f"Successfully placed message on hold with queue id {queue_id}"
)
else:
if queue_id == "ALL":
ret["message"] = "Unable to place all messages on hold"
else:
ret["message"] = (
"Unable to place message on hold with queue id {}: {}".format(
queue_id, result["stderr"]
)
)
return ret
def unhold(queue_id):
"""
Set held message(s) in the mail queue to unheld
CLI Example:
.. code-block:: bash
salt '*' postfix.unhold 5C33CA0DEA
salt '*' postfix.unhold ALL
"""
ret = {"message": "", "result": True}
if not queue_id:
log.error("Require argument queue_id")
if not queue_id == "ALL":
queue = show_queue()
_message = None
for item in queue:
if item["queue_id"] == queue_id:
_message = item
if not _message:
ret["message"] = f"No message in queue with ID {queue_id}"
ret["result"] = False
return ret
cmd = f"postsuper -H {queue_id}"
result = __salt__["cmd.run_all"](cmd)
if result["retcode"] == 0:
if queue_id == "ALL":
ret["message"] = "Successfully set all message as unheld"
else:
ret["message"] = (
f"Successfully set message as unheld with queue id {queue_id}"
)
else:
if queue_id == "ALL":
ret["message"] = "Unable to set all message as unheld."
else:
ret["message"] = (
"Unable to set message as unheld with queue id {}: {}".format(
queue_id, result["stderr"]
)
)
return ret
def requeue(queue_id):
"""
Requeue message(s) in the mail queue
CLI Example:
.. code-block:: bash
salt '*' postfix.requeue 5C33CA0DEA
salt '*' postfix.requeue ALL
"""
ret = {"message": "", "result": True}
if not queue_id:
log.error("Required argument queue_id")
if not queue_id == "ALL":
queue = show_queue()
_message = None
for item in queue:
if item["queue_id"] == queue_id:
_message = item
if not _message:
ret["message"] = f"No message in queue with ID {queue_id}"
ret["result"] = False
return ret
cmd = f"postsuper -r {queue_id}"
result = __salt__["cmd.run_all"](cmd)
if result["retcode"] == 0:
if queue_id == "ALL":
ret["message"] = "Successfully requeued all messages"
else:
ret["message"] = "Successfully requeued message with queue id {}".format(
queue_id
)
else:
if queue_id == "ALL":
ret["message"] = "Unable to requeue all messages"
else:
ret["message"] = "Unable to requeue message with queue id {}: {}".format(
queue_id, result["stderr"]
)
return ret
Zerion Mini Shell 1.0