summaryrefslogtreecommitdiff
path: root/library/system/sysctl
blob: c1f2281b86df74cd7cb1e40775ba95413e984a0b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
#!/usr/bin/python
# -*- coding: utf-8 -*-

# (c) 2012, David "DaviXX" CHANIAL <david.chanial@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: Permit to handle sysctl.conf entries
description:
    - This module manipulates sysctl entries and performs a I(/sbin/sysctl -p) after changing them.
version_added: "1.0"
options:
    name:
        description:
            - this is the short path, decimal separated, to the sysctl entry
        required: true
        default: null
        aliases: [ 'key' ]
    value:
        description:
            - set the sysctl value to this entry
        required: false
        default: null
        aliases: [ 'val' ]
    state:
        description:
            - whether the entry should be present or absent
        choices: [ "present", "absent" ]
        default: present
    checks:
        description:
            - if C(checks)=I(none) no smart/facultative checks will be made
            - if C(checks)=I(before) some checks performed before any update (ie. does the sysctl key is writable ?)
            - if C(checks)=I(after) some checks performed after an update (ie. does kernel give back the set value ?)
            - if C(checks)=I(both) all the smart checks I(before and after) are performed
        choices: [ "none", "before", "after", "both" ]
        default: both
    reload:
        description:
            - if C(reload=yes), performs a I(/sbin/sysctl -p) if the C(sysctl_file) is updated
            - if C(reload=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 /etc/sysctl.conf
        required: false
        default: /etc/sysctl.conf
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, 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 check=before reload=no
'''

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

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

    # 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, type='bool'),
            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.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

    # 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)
    sys.exit(0)

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()