2019-09-07 20:17:41 -03:00

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(service,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