#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2012, David "DaviXX" CHANIAL <david.chanial@gmail.com>
# (c) 2014, James Tanner <tanner.jc@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
#

DOCUMENTATION = '''
---
module: sysctl
short_description: Manage entries in sysctl.conf.
description:
    - This module manipulates sysctl entries and optionally performs a C(/sbin/sysctl -p) after changing them.
version_added: "1.0"
options:
    name:
        description:
            - The dot-separated path (aka I(key)) specifying the sysctl variable.
        required: true
        default: null
        aliases: [ 'key' ]
    value:
        description:
            - Desired value of the sysctl key.
        required: false
        default: null
        aliases: [ 'val' ]
    state:
        description:
            - Whether the entry should be present or absent in the sysctl file.
        choices: [ "present", "absent" ]
        default: present
    reload:
        description:
            - If C(yes), performs a I(/sbin/sysctl -p) if the C(sysctl_file) is
              updated. If C(no), does not reload I(sysctl) even if the
              C(sysctl_file) is updated.
        choices: [ "yes", "no" ]
        default: "yes"
    sysctl_file:
        description:
            - 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 <david.chanial@gmail.com>
'''

EXAMPLES = '''
# Set vm.swappiness to 5 in /etc/sysctl.conf
- sysctl: name=vm.swappiness value=5 state=present

# 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
- 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
'''

# ==============================================================

import os
import tempfile
import re

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:
            # 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])

        if rc != 0:            
            self.module.fail_json(msg="Failed to reload sysctl: %s" % str(out) + str(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) 


# ==============================================================
# 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']),
            reload = dict(default=True, type='bool'),
            sysctl_set = dict(default=True, type='bool'),
            sysctl_file = dict(default='/etc/sysctl.conf')
        ),
        supports_check_mode=True
    )

    result = SysctlModule(module)    

    module.exit_json(changed=result.changed)
    sys.exit(0)

# import module snippets
from ansible.module_utils.basic import *
main()
