From 8efa62b0ea805bfe645fa5b497fae10234f4369b Mon Sep 17 00:00:00 2001 From: davixx Date: Mon, 31 Dec 2012 19:17:47 +0100 Subject: [PATCH] new module: sysctl to handle sysctl entry sysctl now capable to search for sys entry into /sys if not under /proc/sys module/sysctl rolling back to 0.5 write sysctl.conf safely, tempfile first, atomic replace after. See comments in https://github.com/ansible/ansible/pull/1810 Patch to replace .format with % () to handle python-2.4 , See also https://github.com/ansible/ansible/pull/1810 using name instead key for default arg name. key putted as alias. also, val become an alias of value arg name. See also : https://github.com/ansible/ansible/pull/1810 --- library/sysctl | 313 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 library/sysctl diff --git a/library/sysctl b/library/sysctl new file mode 100644 index 0000000000..31ddd48445 --- /dev/null +++ b/library/sysctl @@ -0,0 +1,313 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2012, David "DaviXX" CHANIAL +# +# This file is part of Ansible +# +# Ansible 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. +# +# Ansible 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 Ansible. If not, see . +# + +DOCUMENTATION = ''' +--- +module: sysctl +short_description: Permit to handle sysctl.conf entries +description: + - This module handle the entries in C(/etc/sysctl.conf), + and perform a I(/sbin/sysctl -p) after any change +version_added: "0.6" +options: + name: + description: + - also known as "key", + this is the short path, point separated to the sysctl entry eg: C(vm.swappiness)" + required: true + default: null + aliases: [ 'key' ] + value: + description: + - "value to affect to the sysctl entry, to not provide if state=absent" + required: false + default: null + aliases: [ 'val' ] + state: + description: + - state=present the entry is added if not exist, or updated if exist + state=absent the entry is removed if exist + choices: [ "present", "absent" ] + default: present + checks: + description: + - C(checks)=I(none) no smart/facultative checks will be made + C(checks)=I(before) some checks performed before any update (ie. does the sysctl key is writable ?) + C(checks)=I(after) some checks performed after an update (ie. does kernel give back the setted value ?) + C(checks)=I(both) all the smart checks I(before and after) are performed + choices: [ "none", "before", "after", "both" ] + default: both + reload: + description: + - C(reload=yes) perform a I(/sbin/sysctl -p) if C(sysctl_file) updated ! + C(reload=no) do not reload I(sysctl) even if C(sysctl_file) updated ! + choices: [ yes, no ] + default: yes + sysctl_file: + description: + - specify the absolute path to C(/etc/sysctl.conf) + required: false + default: /etc/sysctl.conf +examples: + - code: "sysctl: name=vm.swappiness value=5 state=present" + description: "Set vm.swappiness to 5 in /etc/sysctl.conf" + - code: "sysctl: name=kernel.panic state=absent sysctl_file=/etc/sysctl.conf" + description: "Remove kernel.panic entry from /etc/sysctl.conf" + - code: "sysctl: name=kernel.panic value=3 sysctl_file=/tmp/test_sysctl.conf check=before reload=no" + description: 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 not the real /etc/sysctl.conf updated) +notes: [] +requirements: [] +author: David "DaviXX" CHANIAL +''' + +# ============================================================== + +import os +import tempfile + +# ============================================================== + +def reload_sysctl(**sysctl_args): + # update needed ? + if not sysctl_args['reload']: + return 0, '' + + # do it + cmd = [ '/sbin/sysctl', '-p' ] + call = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = call.communicate() + if call.returncode == 0: + return 0, '' + else: + return call.returncode, 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_replace(tmp_path, sysctl_args['sysctl_file']) + + # end + return sysctl_args + +# ============================================================== + +def sysctl_args_expand(**sysctl_args): + # key_path + sysctl_args['key_path'] = sysctl_args['name'].replace('.' ,'/') + sysctl_args['key_path'] = '/proc/sys/' + sysctl_args['key_path']; + + # end + 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(current_step, **sysctl_args): + # 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' + + # getting file stat + if not os.access(sysctl_args['key_path'], os.F_OK): + return 1, 'key_path is not an existing file, key seems invalid' + 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' + + # sysctl file exists and openable ? + # TODO choose if prefered to use os.access() instead try/catch on open + if current_step == 'before': + try: + with open(sysctl_args['sysctl_file']) as f: pass + except IOError as e: + return 1, 'unable to open supplied sysctl.conf' + + # 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, '' + + # checks before + if current_step == 'before' and sysctl_args['checks'] in ['before', 'both']: + + if 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: + with open(sysctl_args['key_path'],'r') as f: + output = f.read() + output = output.strip(' \t\n\r') + if output != sysctl_args['value']: + return 1, 'key seems not set to value even after update/sysctl, founded : <%s>, wanted : <%s>' % (output, sysctl_args['value']) + + return 0, '' + + # weird end + return 1, 'unexpected position reached' + +# ============================================================== +# main + +def main(): + + # defining module + module = AnsibleModule( + argument_spec = dict( + 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, choices=BOOLEANS), + sysctl_file = dict(default='/etc/sysctl.conf') + ) + ) + + # defaults + sysctl_args = { + 'changed': False, + 'name': module.params['name'], + 'state': module.params['state'], + 'checks': module.params['checks'], + 'reload': module.boolean(module.params.get('reload', True)), + 'value': module.params.get('value'), + 'sysctl_file': module.params['sysctl_file'] + } + + # prepare vars + sysctl_args = sysctl_args_expand(**sysctl_args) + new_line = "%s = %s\n" % (sysctl_args['name'], sysctl_args['value']) + to_write = [] + founded = False + + # make checks before act + res,msg = sysctl_check('before', **sysctl_args) + if res != 0: + module.fail_json(msg='checks_before failed with: ' + msg) + + # 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 + if len(line.split('=')) != 2: + # not sure what this is or why it is here + # but it is not our fault so leave it be + to_write.append(line) + continue + + # write line if not the one searched + ld = {} + ld['name'], ld['val'] = line.split('=') + 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 + + # 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(**sysctl_args) + + # make checks after act + res,msg = sysctl_check('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) + sys.exit(0) + +# this is magic, see lib/ansible/module_common.py +#<> +main()