From ca055844954ac74116b606feee9a1ef498865da0 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 13 Jan 2014 18:53:02 -0500 Subject: [PATCH] Fixes #5469 Refactor sysctl module into object oriented code, and add a sysctl_set parameter to manage the values in /proc --- library/system/sysctl | 425 ++++++++++++++++++------------------------ 1 file changed, 182 insertions(+), 243 deletions(-) diff --git a/library/system/sysctl b/library/system/sysctl index 1a4fd7fe47a..54f116c6c11 100644 --- a/library/system/sysctl +++ b/library/system/sysctl @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # (c) 2012, David "DaviXX" CHANIAL +# (c) 2014, James Tanner # # This file is part of Ansible # @@ -41,19 +42,9 @@ options: aliases: [ 'val' ] state: description: - - Whether the entry should be present or absent. + - Whether the entry should be present or absent in the sysctl file. choices: [ "present", "absent" ] default: present - checks: - description: - - If C(none), no smart/facultative checks will be made. If - C(before), some checks are performed before any update (i.e. is - the sysctl key writable?). If C(after), some checks are performed - after an update (i.e. does kernel return the set value?). If - C(both), all of the smart checks (C(before) and C(after)) are - performed. - choices: [ "none", "before", "after", "both" ] - default: both reload: description: - If C(yes), performs a I(/sbin/sysctl -p) if the C(sysctl_file) is @@ -66,6 +57,13 @@ options: - Specifies the absolute path to C(sysctl.conf), if not C(/etc/sysctl.conf). required: false default: /etc/sysctl.conf + sysctl_set: + description: + - Verify token value with the sysctl command and set with -w if necessary + choices: [ "yes", "no" ] + required: false + version_added: 1.5 + default: False notes: [] requirements: [] author: David "DaviXX" CHANIAL @@ -78,10 +76,14 @@ EXAMPLES = ''' # Remove kernel.panic entry from /etc/sysctl.conf - sysctl: name=kernel.panic state=absent sysctl_file=/etc/sysctl.conf -# Set kernel.panic to 3 in /tmp/test_sysctl.conf, check if the sysctl key -# seems writable, but do not reload sysctl, and do not check kernel value -# after (not needed, because the real /etc/sysctl.conf was not updated) -- sysctl: name=kernel.panic value=3 sysctl_file=/tmp/test_sysctl.conf checks=before reload=no +# Set kernel.panic to 3 in /tmp/test_sysctl.conf +- sysctl: name=kernel.panic value=3 sysctl_file=/tmp/test_sysctl.conf reload=no + +# Set ip fowarding on in /proc and do not reload the sysctl file +- sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes + +# Set ip forwarding on in /proc and in the sysctl file and reload if necessary +- sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes state=present reload=yes ''' # ============================================================== @@ -90,137 +92,168 @@ import os import tempfile import re -# ============================================================== - -def reload_sysctl(module, **sysctl_args): - # update needed ? - if not sysctl_args['reload']: - return 0, '' - - # do it - if get_platform().lower() == 'freebsd': - # freebsd doesn't support -p, so reload the sysctl service - rc,out,err = module.run_command('/etc/rc.d/sysctl reload') - else: - # system supports reloading via the -p flag to sysctl, so we'll use that - sysctl_cmd = module.get_bin_path('sysctl', required=True) - rc,out,err = module.run_command([sysctl_cmd, '-p', sysctl_args['sysctl_file']]) - - return rc,out+err - -# ============================================================== - -def write_sysctl(module, lines, **sysctl_args): - # open a tmp file - fd, tmp_path = tempfile.mkstemp('.conf', '.ansible_m_sysctl_', os.path.dirname(sysctl_args['sysctl_file'])) - f = open(tmp_path,"w") - try: - for l in lines: - f.write(l) - except IOError, e: - module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, str(e))) - f.flush() - f.close() - - # replace the real one - module.atomic_move(tmp_path, sysctl_args['sysctl_file']) - - # end - return sysctl_args - -# ============================================================== - -def sysctl_args_expand(**sysctl_args): - if get_platform().lower() == 'freebsd': - # FreeBSD does not use the /proc file system, and instead - # just uses the sysctl command to set the values - sysctl_args['key_path'] = None - else: - sysctl_args['key_path'] = sysctl_args['name'].replace('.' ,'/') - sysctl_args['key_path'] = '/proc/sys/' + sysctl_args['key_path'] - return sysctl_args - -# ============================================================== - -def sysctl_args_collapse(**sysctl_args): - # go ahead - if sysctl_args.get('key_path') is not None: - del sysctl_args['key_path'] - if sysctl_args['state'] == 'absent' and 'value' in sysctl_args: - del sysctl_args['value'] - - # end - return sysctl_args - -# ============================================================== - -def sysctl_check(module, current_step, **sysctl_args): - - # no smart checks at this step ? - if sysctl_args['checks'] == 'none': - return 0, '' - if current_step == 'before' and sysctl_args['checks'] not in ['before', 'both']: - return 0, '' - if current_step == 'after' and sysctl_args['checks'] not in ['after', 'both']: - return 0, '' - - # checking coherence - if sysctl_args['state'] == 'absent' and sysctl_args['value'] is not None: - return 1, 'value=x must not be supplied when state=absent' - - if sysctl_args['state'] == 'present' and sysctl_args['value'] is None: - return 1, 'value=x must be supplied when state=present' - - if not sysctl_args['reload'] and sysctl_args['checks'] in ['after', 'both']: - return 1, 'checks cannot be set to after or both if reload=no' - - if sysctl_args['key_path'] is not None: - # getting file stat - if not os.access(sysctl_args['key_path'], os.F_OK): - return 1, 'key_path is not an existing file, key %s seems invalid' % sysctl_args['key_path'] - if not os.access(sysctl_args['key_path'], os.R_OK): - return 1, 'key_path is not a readable file, key seems to be uncheckable' - - # checks before - if current_step == 'before' and sysctl_args['checks'] in ['before', 'both']: - if sysctl_args['key_path'] is not None and not os.access(sysctl_args['key_path'], os.W_OK): - return 1, 'key_path is not a writable file, key seems to be read only' - return 0, '' - - # checks after - if current_step == 'after' and sysctl_args['checks'] in ['after', 'both']: - if sysctl_args['value'] is not None: - if sysctl_args['key_path'] is not None: - # reading the virtual file - f = open(sysctl_args['key_path'],'r') - output = f.read() - f.close() - else: - # we're on a system without /proc (ie. freebsd), so just - # use the sysctl command to get the currently set value - sysctl_cmd = module.get_bin_path('sysctl', required=True) - rc,output,stderr = module.run_command("%s -n %s" % (sysctl_cmd, sysctl_args['name'])) - if rc != 0: - return 1, 'failed to lookup the value via the sysctl command' - - output = output.strip(' \t\n\r') - output = re.sub(r'\s+', ' ', output) - - # normal case, found value must be equal to the submitted value, and - # we compare the exploded values to handle any whitepsace differences - if output.split() != sysctl_args['value'].split(): - return 1, 'key seems not set to value even after update/sysctl, founded : <%s>, wanted : <%s>' % (output, sysctl_args['value']) - - return 0, '' +class SysctlModule(object): + + def __init__(self, module): + self.module = module + self.args = self.module.params + + self.sysctl_cmd = self.module.get_bin_path('sysctl', required=True) + self.sysctl_file = self.args['sysctl_file'] + + self.proc_value = None # current token value in proc fs + self.file_value = None # current token value in file + self.file_lines = [] # all lines in the file + self.file_values = {} # dict of token values + + self.changed = False # will change occur + self.set_proc = False # does sysctl need to set value + self.write_file = False # does the sysctl file need to be reloaded + + self.process() + + # ============================================================== + # LOGIC + # ============================================================== + + def process(self): + + # Whitespace is bad + self.args['name'] = self.args['name'].strip() + self.args['value'] = self.args['value'].strip() + + thisname = self.args['name'] + + # get the current proc fs value + self.proc_value = self.get_token_curr_value(thisname) + + # get the currect sysctl file value + self.read_sysctl_file() + if thisname not in self.file_values: + self.file_values[thisname] = None + + # update file contents with desired token/value + self.fix_lines() + + # what do we need to do now? + if self.file_values[thisname] is None and self.args['state'] == "present": + self.changed = True + self.write_file = True + elif self.file_values[thisname] != self.args['value']: + self.changed = True + self.write_file = True + if self.args['sysctl_set']: + if self.proc_value is None: + self.changed = True + elif self.proc_value != self.args['value']: + self.changed = True + self.set_proc = True + + # Do the work + if not self.module.check_mode: + if self.write_file: + self.write_sysctl() + if self.write_file and self.args['reload']: + self.reload_sysctl() + if self.set_proc: + self.set_token_value(self.args['name'], self.args['value']) + + # ============================================================== + # SYSCTL COMMAND MANAGEMENT + # ============================================================== + + # Use the sysctl command to find the current value + def get_token_curr_value(self, token): + thiscmd = "%s -e -n %s" % (self.sysctl_cmd, token) + rc,out,err = self.module.run_command(thiscmd) + if rc != 0: + return None + else: + return shlex.split(out)[-1] + + # Use the sysctl command to set the current value + def set_token_value(self, token, value): + thiscmd = "%s -w %s=%s" % (self.sysctl_cmd, token, value) + rc,out,err = self.module.run_command(thiscmd) + if rc != 0: + self.module.fail_json(msg='setting %s failed: %s' (token, out + err)) + else: + return rc + + # Run sysctl -p + def reload_sysctl(self): + # do it + if get_platform().lower() == 'freebsd': + # freebsd doesn't support -p, so reload the sysctl service + rc,out,err = self.module.run_command('/etc/rc.d/sysctl reload') else: - # no value was supplied, so we're checking to make sure - # the associated name is absent. We just fudge this since - # the sysctl isn't really gone, just removed from the conf - # file meaning it will be whatever the system default is - return 0, '' + # system supports reloading via the -p flag to sysctl, so we'll use that + rc,out,err = self.module.run_command([self.sysctl_cmd, '-p', self.sysctl_file]) + + return rc,out+err + + # ============================================================== + # SYSCTL FILE MANAGEMENT + # ============================================================== + + # Get the token value from the sysctl file + def read_sysctl_file(self): + lines = open(self.sysctl_file, "r").readlines() + for line in lines: + line = line.strip() + self.file_lines.append(line) + + # don't split empty lines or comments + if not line or line.startswith("#"): + continue + + k, v = line.split('=',1) + k = k.strip() + v = v.strip() + self.file_values[k] = v.strip() + + # Fix the value in the sysctl file content + def fix_lines(self): + checked = [] + self.fixed_lines = [] + for line in self.file_lines: + if not line.strip() or line.strip().startswith("#"): + self.fixed_lines.append(line) + continue + tmpline = line.strip() + k, v = line.split('=',1) + k = k.strip() + v = v.strip() + if k not in checked: + checked.append(k) + if k == self.args['name']: + if self.args['state'] == "present": + new_line = "%s = %s\n" % (k, self.args['value']) + self.fixed_lines.append(new_line) + else: + new_line = "%s = %s\n" % (k, v) + self.fixed_lines.append(new_line) + + if self.args['name'] not in checked and self.args['state'] == "present": + new_line = "%s = %s\n" % (self.args['name'], self.args['value']) + self.fixed_lines.append(new_line) + + # Completely rewrite the sysctl file + def write_sysctl(self): + # open a tmp file + fd, tmp_path = tempfile.mkstemp('.conf', '.ansible_m_sysctl_', os.path.dirname(self.sysctl_file)) + f = open(tmp_path,"w") + try: + for l in self.fixed_lines: + f.write(l) + except IOError, e: + self.module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, str(e))) + f.flush() + f.close() + + # replace the real one + self.module.atomic_move(tmp_path, self.sysctl_file) - # weird end - return 1, 'unexpected position reached' # ============================================================== # main @@ -233,110 +266,16 @@ def main(): name = dict(aliases=['key'], required=True), value = dict(aliases=['val'], required=False), state = dict(default='present', choices=['present', 'absent']), - checks = dict(default='both', choices=['none', 'before', 'after', 'both']), reload = dict(default=True, type='bool'), + sysctl_set = dict(default=True, type='bool'), sysctl_file = dict(default='/etc/sysctl.conf') - ) + ), + supports_check_mode=True ) - # defaults - sysctl_args = { - 'changed': False, - 'name': module.params['name'], - 'state': module.params['state'], - 'checks': module.params['checks'], - 'reload': module.params['reload'], - 'value': module.params.get('value'), - 'sysctl_file': module.params['sysctl_file'] - } - - # prepare vars - sysctl_args = sysctl_args_expand(**sysctl_args) - if get_platform().lower() == 'freebsd': - # freebsd does not like spaces around the equal sign - pattern = "%s=%s\n" - else: - pattern = "%s = %s\n" - new_line = pattern % (sysctl_args['name'], sysctl_args['value']) - to_write = [] - founded = False - - # make checks before act - res,msg = sysctl_check(module, 'before', **sysctl_args) - if res != 0: - module.fail_json(msg='checks_before failed with: ' + msg) - - if not os.access(sysctl_args['sysctl_file'], os.W_OK): - try: - f = open(sysctl_args['sysctl_file'],'w') - f.close() - except IOError, e: - module.fail_json(msg='unable to create supplied sysctl file (destination directory probably missing)') - - # reading the file - for line in open(sysctl_args['sysctl_file'], 'r').readlines(): - if not line.strip(): - to_write.append(line) - continue - if line.strip().startswith('#'): - to_write.append(line) - continue - - # write line if not the one searched - ld = {} - ld['name'], ld['val'] = line.split('=',1) - ld['name'] = ld['name'].strip() - - if ld['name'] != sysctl_args['name']: - to_write.append(line) - continue - - # should be absent ? - if sysctl_args['state'] == 'absent': - # not writing the founded line - # mark as changed - sysctl_args['changed'] = True - - # should be present - if sysctl_args['state'] == 'present': - # is the founded line equal to the wanted one ? - ld['val'] = ld['val'].strip() - if ld['val'] == sysctl_args['value']: - # line is equal, writing it without update (but cancel repeats) - if sysctl_args['changed'] == False and founded == False: - to_write.append(line) - founded = True - else: - # update the line (but cancel repeats) - if sysctl_args['changed'] == False and founded == False: - to_write.append(new_line) - sysctl_args['changed'] = True - continue + result = SysctlModule(module) - # if not changed, but should be present, so we have to add it - if sysctl_args['state'] == 'present' and sysctl_args['changed'] == False and founded == False: - to_write.append(new_line) - sysctl_args['changed'] = True - - # has changed ? - res = 0 - if sysctl_args['changed'] == True: - sysctl_args = write_sysctl(module, to_write, **sysctl_args) - res,msg = reload_sysctl(module, **sysctl_args) - - # make checks after act - res,msg = sysctl_check(module, 'after', **sysctl_args) - if res != 0: - module.fail_json(msg='checks_after failed with: ' + msg) - - # look at the next link to avoid this workaround - # https://groups.google.com/forum/?fromgroups=#!topic/ansible-project/LMY-dwF6SQk - changed = sysctl_args['changed'] - del sysctl_args['changed'] - - # end - sysctl_args = sysctl_args_collapse(**sysctl_args) - module.exit_json(changed=changed, **sysctl_args) + module.exit_json(changed=result.changed) sys.exit(0) # import module snippets