# -*- coding: utf-8 -*- # # (c) 2018, Mauro Torrez # # 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 . 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