Skip to content

Commit 44ca366

Browse files
authored
sysrc: refactor (#10417)
* sysrc: refactor * sysrc: refactor changelog fragment * sysrc: forgot the os import * sysrc: update test to edit the correct file * sysrc: Added copyright info to the test conf file * sysrc: Added full copyright info to the test conf file * sysrc: Detect permission denied when using sysrc * sysrc: Fixed the permission check and 2.7 compatibility * sysrc: Fix typo of import * sysrc: Fix err.find check * sysrc: Add bugfixes changelog fragment * sysrc: Use `StateModuleHelper` * sysrc: updated imports * sysrc: remove re import and set errno.EACCES on the OSError * sysrc: format code properly * sysrc: fix Python 2.7 compatibility and set changed manually * sysrc: add missing name format check Also use `self.module.fail_json` through out * sysrc: Removed os import by accident * sysrc: updated per review, and the way the existing value is retrieved
1 parent 15d3ea1 commit 44ca366

File tree

4 files changed

+169
-128
lines changed

4 files changed

+169
-128
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
minor_changes:
2+
- sysrc - adjustments to the code (https://github.com/ansible-collections/community.general/pull/10417).
3+
bugfixes:
4+
- sysrc - fixes parsing with multi-line variables (https://github.com/ansible-collections/community.general/issues/10394, https://github.com/ansible-collections/community.general/pull/10417).

plugins/modules/sysrc.py

Lines changed: 91 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# SPDX-License-Identifier: GPL-3.0-or-later
88

99
from __future__ import absolute_import, division, print_function
10+
1011
__metaclass__ = type
1112

1213
DOCUMENTATION = r"""
@@ -95,157 +96,121 @@
9596
"""
9697

9798

98-
from ansible.module_utils.basic import AnsibleModule
99+
from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper
100+
101+
import os
99102
import re
100103

101104

102-
class Sysrc(object):
103-
def __init__(self, module, name, value, path, delim, jail):
104-
self.module = module
105-
self.name = name
106-
self.changed = False
107-
self.value = value
108-
self.path = path
109-
self.delim = delim
110-
self.jail = jail
111-
self.sysrc = module.get_bin_path('sysrc', True)
112-
113-
def has_unknown_variable(self, out, err):
114-
# newer versions of sysrc use stderr instead of stdout
115-
return err.find("unknown variable") > 0 or out.find("unknown variable") > 0
116-
117-
def exists(self):
118-
"""
119-
Tests whether the name is in the file. If parameter value is defined,
120-
then tests whether name=value is in the file. These tests are necessary
121-
because sysrc doesn't use exit codes. Instead, let sysrc read the
122-
file's content and create a dictionary comprising the configuration.
123-
Use this dictionary to preform the tests.
124-
"""
125-
(rc, out, err) = self.run_sysrc('-e', '-a')
126-
conf = dict([i.split('=', 1) for i in out.splitlines()])
127-
if self.value is None:
128-
return self.name in conf
129-
else:
130-
return self.name in conf and conf[self.name] == '"%s"' % self.value
131-
132-
def contains(self):
133-
(rc, out, err) = self.run_sysrc('-n', self.name)
134-
if self.has_unknown_variable(out, err):
135-
return False
136-
137-
return self.value in out.strip().split(self.delim)
138-
139-
def present(self):
140-
if self.exists():
141-
return
105+
class Sysrc(StateModuleHelper):
106+
module = dict(
107+
argument_spec=dict(
108+
name=dict(type='str', required=True),
109+
value=dict(type='str', default=None),
110+
state=dict(type='str', default='present', choices=['absent', 'present', 'value_present', 'value_absent']),
111+
path=dict(type='str', default='/etc/rc.conf'),
112+
delim=dict(type='str', default=' '),
113+
jail=dict(type='str', default=None)
114+
),
115+
supports_check_mode=True
116+
)
117+
output_params = ('value',)
118+
use_old_vardict = False
142119

143-
if not self.module.check_mode:
144-
(rc, out, err) = self.run_sysrc("%s=%s" % (self.name, self.value))
120+
def __init_module__(self):
121+
# OID style names are not supported
122+
if not re.match(r'^\w+$', self.vars.name, re.ASCII):
123+
self.module.fail_json(msg="Name may only contain alpha-numeric and underscore characters")
145124

146-
self.changed = True
125+
self.sysrc = self.module.get_bin_path('sysrc', True)
147126

148-
def absent(self):
149-
if not self.exists():
150-
return
127+
def _contains(self):
128+
value = self._get()
129+
if value is None:
130+
return False, None
151131

152-
# inversed since we still need to mark as changed
153-
if not self.module.check_mode:
154-
(rc, out, err) = self.run_sysrc('-x', self.name)
155-
if self.has_unknown_variable(out, err):
156-
return
132+
value = value.split(self.vars.delim)
157133

158-
self.changed = True
134+
return self.vars.value in value, value
159135

160-
def value_present(self):
161-
if self.contains():
162-
return
136+
def _get(self):
137+
if not os.path.exists(self.vars.path):
138+
return None
163139

164-
if self.module.check_mode:
165-
self.changed = True
166-
return
140+
(rc, out, err) = self._sysrc('-v', '-n', self.vars.name)
141+
if "unknown variable" in err or "unknown variable" in out:
142+
# Prior to FreeBSD 11.1 sysrc would write "unknown variable" to stdout and not stderr
143+
# https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=229806
144+
return None
167145

168-
setstring = '%s+=%s%s' % (self.name, self.delim, self.value)
169-
(rc, out, err) = self.run_sysrc(setstring)
170-
if out.find("%s:" % self.name) == 0:
171-
values = out.split(' -> ')[1].strip().split(self.delim)
172-
if self.value in values:
173-
self.changed = True
146+
if out.startswith(self.vars.path):
147+
return out.split(':', 1)[1].strip()
174148

175-
def value_absent(self):
176-
if not self.contains():
177-
return
149+
return None
178150

179-
if self.module.check_mode:
180-
self.changed = True
181-
return
151+
def _modify(self, op, changed):
152+
(rc, out, err) = self._sysrc("%s%s=%s%s" % (self.vars.name, op, self.vars.delim, self.vars.value))
153+
if out.startswith("%s:" % self.vars.name):
154+
return changed(out.split(' -> ')[1].strip().split(self.vars.delim))
182155

183-
setstring = '%s-=%s%s' % (self.name, self.delim, self.value)
184-
(rc, out, err) = self.run_sysrc(setstring)
185-
if out.find("%s:" % self.name) == 0:
186-
values = out.split(' -> ')[1].strip().split(self.delim)
187-
if self.value not in values:
188-
self.changed = True
189-
190-
def run_sysrc(self, *args):
191-
cmd = [self.sysrc, '-f', self.path]
192-
if self.jail:
193-
cmd += ['-j', self.jail]
156+
return False
157+
158+
def _sysrc(self, *args):
159+
cmd = [self.sysrc, '-f', self.vars.path]
160+
if self.vars.jail:
161+
cmd += ['-j', self.vars.jail]
194162
cmd.extend(args)
195163

196164
(rc, out, err) = self.module.run_command(cmd)
165+
if "Permission denied" in err:
166+
self.module.fail_json(msg="Permission denied for %s" % self.vars.path)
197167

198-
return (rc, out, err)
168+
return rc, out, err
199169

170+
def state_absent(self):
171+
if self._get() is None:
172+
return
200173

201-
def main():
202-
module = AnsibleModule(
203-
argument_spec=dict(
204-
name=dict(type='str', required=True),
205-
value=dict(type='str', default=None),
206-
state=dict(type='str', default='present', choices=['absent', 'present', 'value_present', 'value_absent']),
207-
path=dict(type='str', default='/etc/rc.conf'),
208-
delim=dict(type='str', default=' '),
209-
jail=dict(type='str', default=None),
210-
),
211-
supports_check_mode=True,
212-
)
174+
if not self.check_mode:
175+
self._sysrc('-x', self.vars.name)
213176

214-
name = module.params.pop('name')
215-
# OID style names are not supported
216-
if not re.match('^[a-zA-Z0-9_]+$', name):
217-
module.fail_json(
218-
msg="Name may only contain alphanumeric and underscore characters"
219-
)
220-
221-
value = module.params.pop('value')
222-
state = module.params.pop('state')
223-
path = module.params.pop('path')
224-
delim = module.params.pop('delim')
225-
jail = module.params.pop('jail')
226-
result = dict(
227-
name=name,
228-
state=state,
229-
value=value,
230-
path=path,
231-
delim=delim,
232-
jail=jail
233-
)
177+
self.changed = True
178+
179+
def state_present(self):
180+
value = self._get()
181+
if value == self.vars.value:
182+
return
183+
184+
if self.vars.value is None:
185+
self.vars.set('value', value)
186+
return
187+
188+
if not self.check_mode:
189+
self._sysrc("%s=%s" % (self.vars.name, self.vars.value))
190+
191+
self.changed = True
234192

235-
rc_value = Sysrc(module, name, value, path, delim, jail)
193+
def state_value_absent(self):
194+
(contains, _unused) = self._contains()
195+
if not contains:
196+
return
236197

237-
if state == 'present':
238-
rc_value.present()
239-
elif state == 'absent':
240-
rc_value.absent()
241-
elif state == 'value_present':
242-
rc_value.value_present()
243-
elif state == 'value_absent':
244-
rc_value.value_absent()
198+
self.changed = self.check_mode or self._modify('-', lambda values: self.vars.value not in values)
245199

246-
result['changed'] = rc_value.changed
200+
def state_value_present(self):
201+
(contains, value) = self._contains()
202+
if contains:
203+
return
247204

248-
module.exit_json(**result)
205+
if self.vars.value is None:
206+
self.vars.set('value', value)
207+
return
208+
209+
self.changed = self.check_mode or self._modify('+', lambda values: self.vars.value in values)
210+
211+
212+
def main():
213+
Sysrc.execute()
249214

250215

251216
if __name__ == '__main__':
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) Ansible Project
2+
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
k1="v1"
5+
jail_list="
6+
foo
7+
bar"

tests/integration/targets/sysrc/tasks/main.yml

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,14 +369,79 @@
369369
- "value_2 == sysrc_equals_sign_2.value"
370370
- "value_2 == conf.spamd_flags"
371371

372+
##
373+
## sysrc - #10004 state=absent when using default settings will report `changed=true`
374+
##
375+
- name: Test that a key from /etc/defaults/rc.conf is not used to mark changed
376+
sysrc:
377+
name: dumpdev
378+
state: absent
379+
path: /tmp/10004.conf
380+
register: sysrc_10004_absent
381+
failed_when: sysrc_10004_absent.changed
382+
383+
- name: Test that a delimited key from /etc/defaults/rc.conf is not used to mark changed
384+
sysrc:
385+
name: rc_conf_files
386+
state: value_absent
387+
path: /tmp/10004.conf
388+
register: sysrc_10004_value_absent
389+
failed_when: sysrc_10004_value_absent.changed
390+
391+
- name: Test that a key from /etc/defaults/rc.conf is not used to mark changed without a path
392+
sysrc:
393+
name: static_routes
394+
state: absent
395+
register: sysrc_absent_default
396+
failed_when: sysrc_absent_default.changed
397+
398+
##
399+
## sysrc - #10394 Ensure that files with multi-line values work
400+
##
401+
- name: Copy 10394.conf
402+
copy:
403+
src: 10394.conf
404+
dest: /tmp/10394.conf
405+
406+
- name: Change value for k1
407+
sysrc:
408+
name: k1
409+
value: v2
410+
path: /tmp/10394.conf
411+
register: sysrc_10394_changed
412+
413+
- name: Get file content
414+
shell: "cat /tmp/10394.conf"
415+
register: sysrc_10394_content
416+
417+
- name: Ensure sysrc changed k1 from v1 to v2
418+
assert:
419+
that:
420+
- sysrc_10394_changed.changed
421+
- >
422+
'k1="v2"' in sysrc_10394_content.stdout_lines
423+
424+
##
425+
## sysrc - additional tests
426+
##
427+
- name: Ensure failure on OID style name since sysrc does not support them
428+
sysrc:
429+
name: not.valid.var
430+
value: test
431+
register: sysrc_name_check
432+
failed_when:
433+
- sysrc_name_check is not failed
434+
- >
435+
'Name may only contain alpha-numeric and underscore characters' != sysrc_name_check.msg
436+
372437
always:
373438

374439
- name: Restore /etc/rc.conf
375440
copy:
376-
content: "{{ cached_etc_rcconf_content }}"
441+
content: "{{ cached_etc_rcconf_content.stdout }}"
377442
dest: /etc/rc.conf
378443

379444
- name: Restore /boot/loader.conf
380445
copy:
381-
content: "{{ cached_boot_loaderconf_content }}"
446+
content: "{{ cached_boot_loaderconf_content.stdout }}"
382447
dest: /boot/loader.conf

0 commit comments

Comments
 (0)