Skip to content

sysrc: refactor #10417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelogs/fragments/10417-sysrc-refactor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
minor_changes:
- sysrc - adjustments to the code (https://github.com/ansible-collections/community.general/pull/10417).
bugfixes:
- 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).
216 changes: 90 additions & 126 deletions plugins/modules/sysrc.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function

__metaclass__ = type

DOCUMENTATION = r"""
Expand Down Expand Up @@ -102,157 +103,120 @@
sample: true
"""

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper

import os
import re


class Sysrc(object):
def __init__(self, module, name, value, path, delim, jail):
self.module = module
self.name = name
self.changed = False
self.value = value
self.path = path
self.delim = delim
self.jail = jail
self.sysrc = module.get_bin_path('sysrc', True)

def has_unknown_variable(self, out, err):
# newer versions of sysrc use stderr instead of stdout
return err.find("unknown variable") > 0 or out.find("unknown variable") > 0

def exists(self):
"""
Tests whether the name is in the file. If parameter value is defined,
then tests whether name=value is in the file. These tests are necessary
because sysrc doesn't use exit codes. Instead, let sysrc read the
file's content and create a dictionary comprising the configuration.
Use this dictionary to preform the tests.
"""
(rc, out, err) = self.run_sysrc('-e', '-a')
conf = dict([i.split('=', 1) for i in out.splitlines()])
if self.value is None:
return self.name in conf
else:
return self.name in conf and conf[self.name] == '"%s"' % self.value

def contains(self):
(rc, out, err) = self.run_sysrc('-n', self.name)
if self.has_unknown_variable(out, err):
return False

return self.value in out.strip().split(self.delim)

def present(self):
if self.exists():
return
class Sysrc(StateModuleHelper):
module = dict(
argument_spec=dict(
name=dict(type='str', required=True),
value=dict(type='str', default=None),
state=dict(type='str', default='present', choices=['absent', 'present', 'value_present', 'value_absent']),
path=dict(type='str', default='/etc/rc.conf'),
delim=dict(type='str', default=' '),
jail=dict(type='str', default=None)
),
supports_check_mode=True
)
output_params = ('value')
use_old_vardict = False

if not self.module.check_mode:
(rc, out, err) = self.run_sysrc("%s=%s" % (self.name, self.value))
def __init_module__(self):
# OID style names are not supported
if not re.match('^[a-zA-Z0-9_]+$', self.vars.name):
self.module.fail_json(msg="Name may only contain alpha-numeric and underscore characters")

self.changed = True
self.sysrc = self.module.get_bin_path('sysrc', True)

def absent(self):
if not self.exists():
return
def _contains(self):
value = self._get()
if value is None:
return False, None

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

self.changed = True
return self.vars.value in value, value

def value_present(self):
if self.contains():
return
def _get(self):
if not os.path.exists(self.vars.path):
return None

if self.module.check_mode:
self.changed = True
return
(rc, out, err) = self._sysrc('-c', self.vars.name)
if rc == 1:
return None

setstring = '%s+=%s%s' % (self.name, self.delim, self.value)
(rc, out, err) = self.run_sysrc(setstring)
if out.find("%s:" % self.name) == 0:
values = out.split(' -> ')[1].strip().split(self.delim)
if self.value in values:
self.changed = True
(rc, out, err) = self._sysrc('-n', self.vars.name)
if "unknown variable" in err or "unknown variable" in out:
return None

def value_absent(self):
if not self.contains():
return
return out.strip()

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

setstring = '%s-=%s%s' % (self.name, self.delim, self.value)
(rc, out, err) = self.run_sysrc(setstring)
if out.find("%s:" % self.name) == 0:
values = out.split(' -> ')[1].strip().split(self.delim)
if self.value not in values:
self.changed = True

def run_sysrc(self, *args):
cmd = [self.sysrc, '-f', self.path]
if self.jail:
cmd += ['-j', self.jail]
return False

def _sysrc(self, *args):
cmd = [self.sysrc, '-f', self.vars.path]
if self.vars.jail:
cmd += ['-j', self.vars.jail]
cmd.extend(args)

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

return (rc, out, err)
return rc, out, err

def state_absent(self):
if self._get() is None:
return

def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', required=True),
value=dict(type='str', default=None),
state=dict(type='str', default='present', choices=['absent', 'present', 'value_present', 'value_absent']),
path=dict(type='str', default='/etc/rc.conf'),
delim=dict(type='str', default=' '),
jail=dict(type='str', default=None),
),
supports_check_mode=True,
)
if not self.check_mode:
self._sysrc('-x', self.vars.name)

name = module.params.pop('name')
# OID style names are not supported
if not re.match('^[a-zA-Z0-9_]+$', name):
module.fail_json(
msg="Name may only contain alphanumeric and underscore characters"
)

value = module.params.pop('value')
state = module.params.pop('state')
path = module.params.pop('path')
delim = module.params.pop('delim')
jail = module.params.pop('jail')
result = dict(
name=name,
state=state,
value=value,
path=path,
delim=delim,
jail=jail
)
self.changed = True

def state_present(self):
value = self._get()
if value == self.vars.value:
return

if self.vars.value is None:
self.vars.set('value', value)
return

if not self.check_mode:
self._sysrc("%s=%s" % (self.vars.name, self.vars.value))

self.changed = True

rc_value = Sysrc(module, name, value, path, delim, jail)
def state_value_absent(self):
(contains, _unused) = self._contains()
if not contains:
return

if state == 'present':
rc_value.present()
elif state == 'absent':
rc_value.absent()
elif state == 'value_present':
rc_value.value_present()
elif state == 'value_absent':
rc_value.value_absent()
self.changed = self.check_mode or self._modify('-', lambda values: self.vars.value not in values)

result['changed'] = rc_value.changed
def state_value_present(self):
(contains, value) = self._contains()
if contains:
return

module.exit_json(**result)
if self.vars.value is None:
self.vars.set('value', value)
return

self.changed = self.check_mode or self._modify('+', lambda values: self.vars.value in values)


def main():
Sysrc.execute()


if __name__ == '__main__':
Expand Down
7 changes: 7 additions & 0 deletions tests/integration/targets/sysrc/files/10394.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
k1="v1"
jail_list="
foo
bar"
70 changes: 68 additions & 2 deletions tests/integration/targets/sysrc/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -369,14 +369,80 @@
- "value_2 == sysrc_equals_sign_2.value"
- "value_2 == conf.spamd_flags"

##
## sysrc - #10004 state=absent when using default settings will report `changed=true`
##
- name: Test that a key from /etc/defaults/rc.conf is not used to mark changed
sysrc:
name: dumpdev
state: absent
path: /tmp/10004.conf
register: sysrc_10004_absent

- name: Ensure that the defaults are not consulted
assert:
that:
- not sysrc_10004_absent.changed

- name: Test that a delimited key from /etc/defaults/rc.conf is not used to mark changed
sysrc:
name: rc_conf_files
state: value_absent
path: /tmp/10004.conf
register: sysrc_10004_value_absent

- name: Ensure that the default is not consulted
assert:
that:
- not sysrc_10004_value_absent.changed

##
## sysrc - #10394 Ensure that files with multi-line values work
##
- name: Copy 10394.conf
copy:
src: 10394.conf
dest: /tmp/10394.conf

- name: Change value for k1
sysrc:
name: k1
value: v2
path: /tmp/10394.conf
register: sysrc_10394_changed

- name: Get file content
shell: "cat /tmp/10394.conf"
register: sysrc_10394_content

- name: Ensure sysrc changed k1 from v1 to v2
assert:
that:
- sysrc_10394_changed.changed
- >
'k1="v2"' in sysrc_10394_content.stdout_lines

##
## sysrc - additional tests
##
- name: Ensure failure on OID style name since sysrc does not support them
sysrc:
name: not.valid.var
value: test
register: sysrc_name_check
failed_when:
- sysrc_name_check is not failed
- >
'Name may only contain alpha-numeric and underscore characters' != sysrc_name_check.msg

always:

- name: Restore /etc/rc.conf
copy:
content: "{{ cached_etc_rcconf_content }}"
content: "{{ cached_etc_rcconf_content.stdout }}"
dest: /etc/rc.conf

- name: Restore /boot/loader.conf
copy:
content: "{{ cached_boot_loaderconf_content }}"
content: "{{ cached_boot_loaderconf_content.stdout }}"
dest: /boot/loader.conf