374 lines
14 KiB
Python
Executable File
374 lines
14 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
|
#
|
|
# (c) 2018, Mauro Torrez <maurotorrez@gmail.com>
|
|
#
|
|
# This program 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.
|
|
#
|
|
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
import re
|
|
from ansible.plugins.action import ActionBase
|
|
from ansible.utils.vars import merge_hash
|
|
from ansible.module_utils._text import to_text
|
|
from six import string_types
|
|
|
|
class PfWorkflowError(Exception):
|
|
pass
|
|
|
|
class ActionModule(ActionBase):
|
|
|
|
def getarg(self, key, default=True):
|
|
if isinstance(default, string_types):
|
|
if key == 'parameter':
|
|
return self._task.args.get(key, default)
|
|
return '{}'.format(self._task.args.get(key, default))
|
|
if default:
|
|
if key == 'state':
|
|
return '{}'.format(self._task.args.get(key, 'present'))
|
|
if key == 'parameter':
|
|
return self._task.args.get(key, '')
|
|
if key in ('service', 'command', 'value', 'type'):
|
|
return '{}'.format(self._task.args.get(key, ''))
|
|
if key in ('private', 'unprivileged', 'chroot', 'wakeup', 'process_limit'):
|
|
return '{}'.format(self._task.args.get(key, ''))
|
|
if key == 'container_name':
|
|
return '{}'.format(self._task.args.get(key, 'postfix'))
|
|
# no default value for key: return as-is
|
|
return self._task.args.get(key)
|
|
|
|
|
|
def runcmd(self, reg_name, cmd, param=None):
|
|
fullcmd = 'docker exec '+ self.getarg('container_name') + ' ' + cmd
|
|
try:
|
|
if param:
|
|
self.reg[param][reg_name] = self._execute_module(
|
|
module_name='command',
|
|
module_args=dict(_raw_params=fullcmd)
|
|
)
|
|
return
|
|
self.reg[reg_name] = self._execute_module(
|
|
module_name='command',
|
|
module_args=dict(_raw_params=fullcmd)
|
|
)
|
|
except Exception as e:
|
|
raise PfWorkflowError(
|
|
"{}: {}; failed command line: {}".format(
|
|
type(e).__name__,
|
|
to_text(e),
|
|
fullcmd
|
|
)
|
|
)
|
|
|
|
|
|
def validate_arguments(self):
|
|
'''assert arguments are valid'''
|
|
|
|
# validate service name
|
|
self.state = self.getarg('state')
|
|
if self.state not in ('present', 'absent', 'prepend', 'append'):
|
|
raise PfWorkflowError(
|
|
'Invalid state requested: use "present", "absent", "prepend" or "append"'
|
|
)
|
|
|
|
# validate service name
|
|
self.service = self.getarg('service')
|
|
if re.search('[^a-zA-Z0-9_]',self.service):
|
|
raise PfWorkflowError(
|
|
'Invalid character found in service identifier'
|
|
)
|
|
|
|
# validate service type text
|
|
self.stype = self.getarg('type')
|
|
if re.search('[^a-zA-Z0-9_]',self.stype):
|
|
raise PfWorkflowError(
|
|
'Invalid character found in service type identifier'
|
|
)
|
|
|
|
# validate flags
|
|
for p in ('private', 'unprivileged', 'chroot'):
|
|
if re.search('[^yn-]',self.getarg(p)):
|
|
raise PfWorkflowError(
|
|
'Invalid value for boolean flag {}'.format(p)
|
|
)
|
|
|
|
# validate flags
|
|
if re.search('[^0-9?-]',self.getarg('wakeup')):
|
|
raise PfWorkflowError(
|
|
'Invalid value for wakeup flag: {}'.format(self.getarg('wakeup'))
|
|
)
|
|
|
|
# validate flags
|
|
if re.search('[^0-9-]',self.getarg('process_limit')):
|
|
raise PfWorkflowError(
|
|
'Invalid value for process_limit flag: {}'.format(self.getarg('process_limit'))
|
|
)
|
|
|
|
|
|
def set_service(self):
|
|
'''
|
|
Set service entry in master.cf
|
|
'''
|
|
if not self.service:
|
|
self.reg['service_skipped'] = True
|
|
return
|
|
|
|
state = self.getarg('state')
|
|
|
|
# guess existing entry ------------------------------------------------
|
|
|
|
# get current service entry (if exists)
|
|
cmdline = 'postconf -M {}'.format(self.service)
|
|
if self.stype:
|
|
# append service type to the command line
|
|
cmdline = '{}/{}'.format(cmdline,self.stype)
|
|
|
|
self.runcmd('cmd_initial_check',cmdline)
|
|
|
|
# check service is defined and unambiguous by counting stdout lines
|
|
if len(self.reg['cmd_initial_check']['stdout_lines']) > 1:
|
|
raise PfWorkflowError(
|
|
'Ambiguous service identifier, please provide service type'
|
|
)
|
|
|
|
elif len(self.reg['cmd_initial_check']['stdout_lines']) == 1:
|
|
if not self.stype:
|
|
self.stype = self.reg['cmd_initial_check']['stdout'].split()[1]
|
|
|
|
# fill service fields as given by postconf
|
|
cmdline = 'postconf -Fh \
|
|
{0}/{1}/private {0}/{1}/unprivileged {0}/{1}/chroot \
|
|
{0}/{1}/wakeup {0}/{1}/process_limit {0}/{1}/command'.format(
|
|
self.service, self.stype)
|
|
self.runcmd('cmd_fields_check',cmdline)
|
|
|
|
private = self.getarg(
|
|
'private',
|
|
self.reg['cmd_fields_check']['stdout_lines'][0]
|
|
)
|
|
unprivileged = self.getarg(
|
|
'unprivileged',
|
|
self.reg['cmd_fields_check']['stdout_lines'][1]
|
|
)
|
|
chroot = self.getarg(
|
|
'chroot',
|
|
self.reg['cmd_fields_check']['stdout_lines'][2]
|
|
)
|
|
wakeup = self.getarg(
|
|
'wakeup',
|
|
self.reg['cmd_fields_check']['stdout_lines'][3]
|
|
)
|
|
process_limit = self.getarg(
|
|
'process_limit',
|
|
self.reg['cmd_fields_check']['stdout_lines'][4]
|
|
)
|
|
|
|
# change the command field when set explicitly
|
|
command = self.reg['cmd_fields_check']['stdout_lines'][5]
|
|
if self.getarg('command') and command.split()[0] != self.getarg('command'):
|
|
command = self.getarg('command')
|
|
|
|
else:
|
|
# no entry for service, create it if state != absent
|
|
if state != 'absent':
|
|
private = self.getarg('private','-')
|
|
unprivileged = self.getarg('unprivileged','-')
|
|
chroot = self.getarg('chroot','-')
|
|
wakeup = self.getarg('wakeup','-')
|
|
process_limit = self.getarg('process_limit','-')
|
|
command = self.getarg('command')
|
|
|
|
cmdline = 'postconf -M {0}/{1}="{0} {1} {2} {3} {4} {5} {6} {7}"'.format(
|
|
self.service,
|
|
self.stype,
|
|
private,
|
|
unprivileged,
|
|
chroot,
|
|
wakeup,
|
|
process_limit,
|
|
command
|
|
)
|
|
self.runcmd('cmd_add_service_entry',cmdline)
|
|
|
|
|
|
# state = absent ------------------------------------------------------
|
|
if state == 'absent':
|
|
if self.getarg('parameter'):
|
|
# state=absent with parameter does not remove service entry
|
|
pass
|
|
else:
|
|
# remove whole service definition
|
|
cmdline = 'postconf -M# {}/{}'.format(self.service,self.stype)
|
|
self.runcmd('cmd_remove_service',cmdline)
|
|
|
|
# non-absent states: set all fields -----------------------------------
|
|
else:
|
|
cmdline = 'postconf -F \
|
|
{0}/{1}/private={2} {0}/{1}/unprivileged={3} {0}/{1}/chroot={4} \
|
|
{0}/{1}/wakeup={5} {0}/{1}/process_limit={6} {0}/{1}/command={7}'.format(
|
|
self.service, self.stype, private, unprivileged, chroot, wakeup, process_limit,
|
|
'"' + command.replace(r'\\',r'\\\\').replace(r'"',r'\"') + '"'
|
|
)
|
|
self.runcmd('cmd_fields_set',cmdline)
|
|
|
|
|
|
# verify changes
|
|
cmdline = 'postconf -M {}/{}'.format(self.service,self.stype)
|
|
self.runcmd('cmd_final_check',cmdline)
|
|
|
|
self.reg['service_changed'] = True
|
|
if self.reg['cmd_initial_check']['stdout'] == self.reg['cmd_final_check']['stdout']:
|
|
self.reg['service_changed'] = False
|
|
|
|
|
|
def set_single_parameter(self, parameter='', value='', state=''):
|
|
'''
|
|
Set a single parameter value in main.cf (service=None) or master.cf
|
|
'''
|
|
|
|
# value should be string
|
|
if not isinstance(value, string_types):
|
|
try:
|
|
value = ', '.join(value)
|
|
except TypeError:
|
|
value = str(value)
|
|
|
|
# default command to execute for setting the parameter
|
|
postconf_cmd = 'postconf'
|
|
|
|
# validate parameter identifier
|
|
if not parameter or re.search('[^a-zA-Z0-9_-]', parameter):
|
|
raise PfWorkflowError(
|
|
'Invalid parameter identifier: «{}»'.format(parameter)
|
|
)
|
|
|
|
regkey = "param_{}".format(parameter)
|
|
self.reg[regkey] = {}
|
|
|
|
if self.service:
|
|
# change command to execute for setting the parameter
|
|
postconf_cmd = 'postconf -P'
|
|
# change parameter identifier to include service/type
|
|
parameter = '/'.join([self.service,self.stype,parameter])
|
|
|
|
#
|
|
# general logic for setting parameter
|
|
#
|
|
|
|
# 1: get current value
|
|
cmdline = "{} -h {}".format(postconf_cmd, parameter)
|
|
self.runcmd('cmd_initial_check',cmdline,regkey)
|
|
current_value = self.reg[regkey]['cmd_initial_check']['stdout']
|
|
|
|
# 2: act according to desired state
|
|
if state == 'absent':
|
|
cmdline = "{} -X {}".format(postconf_cmd, parameter)
|
|
self.runcmd('cmd_remove',cmdline,regkey)
|
|
|
|
elif state in ('append', 'prepend'):
|
|
# we assume the current value to be a comma-separated list,
|
|
# and check if desired value is contained in current value
|
|
|
|
if value not in current_value:
|
|
|
|
# only modify parameter if desired value not contained in current
|
|
if state == 'prepend':
|
|
new_value = ', '.join([value,current_value])
|
|
else:
|
|
new_value = ', '.join([current_value,value])
|
|
|
|
# escape value according to shlex posix mode
|
|
escaped_value= '"' + new_value.replace(
|
|
r'\\',r'\\\\').replace(r'"',r'\"')+'"'
|
|
cmdline = "{} -e {}={}".format(postconf_cmd, parameter, escaped_value)
|
|
self.runcmd('cmd_modify',cmdline,regkey)
|
|
|
|
elif state == 'present':
|
|
if value.strip() != current_value.strip():
|
|
# set value exactly as required
|
|
# escape value according to shlex posix mode
|
|
escaped_value= '"' + value.replace(
|
|
r'\\',r'\\\\').replace(r'"',r'\"')+'"'
|
|
cmdline = "{} -e {}={}".format(postconf_cmd, parameter, escaped_value)
|
|
self.runcmd('cmd_modify_exact',cmdline,regkey)
|
|
|
|
else:
|
|
# unsupported state
|
|
raise PfWorkflowError(
|
|
"Unsupported value in «state»"
|
|
)
|
|
|
|
# finally check how the value has changed
|
|
cmdline = "{} -h {}".format(postconf_cmd, parameter)
|
|
self.runcmd('cmd_final_check',cmdline,regkey)
|
|
|
|
changed = self.reg.get('parameter_changed', False)
|
|
final_value = self.reg[regkey]['cmd_final_check']['stdout']
|
|
if final_value != current_value:
|
|
changed = True
|
|
|
|
self.reg['parameter_changed'] = changed
|
|
|
|
|
|
def run(self, tmp=None, task_vars=None):
|
|
|
|
result = super(ActionModule, self).run(tmp, task_vars)
|
|
if result.get('skipped'):
|
|
return result
|
|
|
|
try:
|
|
self.validate_arguments()
|
|
except PfWorkflowError as e:
|
|
return {
|
|
'failed': True,
|
|
'msg': e.message
|
|
}
|
|
|
|
self.reg = {}
|
|
|
|
# process service
|
|
if self.getarg('service',False):
|
|
self.set_service()
|
|
|
|
parameter = self.getarg('parameter')
|
|
state = self.getarg('state')
|
|
if parameter:
|
|
# build (parameter, value) pairs for iteration
|
|
if isinstance(parameter, string_types):
|
|
pv = [ (parameter, self.getarg('value')) ]
|
|
elif isinstance(parameter, dict):
|
|
# if dict ignore new_value
|
|
pv = parameter.items()
|
|
else:
|
|
# assume list --> only supported for 'absent'
|
|
pv = zip(parameter, parameter)
|
|
|
|
for p in pv:
|
|
# loop over parameters and exit if error
|
|
self.set_single_parameter(p[0], p[1], state=state)
|
|
else:
|
|
self.reg['parameter_skipped'] = True
|
|
|
|
if all((self.reg.get('service_skipped',False),
|
|
self.reg.get('parameter_skipped',False))):
|
|
return {
|
|
'failed': True,
|
|
'msg': 'FATAL: no action requested!'
|
|
}
|
|
|
|
result['changed'] = any((self.reg.get('service_changed'),
|
|
self.reg.get('parameter_changed')))
|
|
result['reg'] = self.reg
|
|
return result
|