diff --git a/plugins/modules/sysrc.py b/plugins/modules/sysrc.py new file mode 120000 index 0000000000..25c66335f5 --- /dev/null +++ b/plugins/modules/sysrc.py @@ -0,0 +1 @@ +./system/sysrc.py \ No newline at end of file diff --git a/plugins/modules/system/sysrc.py b/plugins/modules/system/sysrc.py new file mode 100644 index 0000000000..cee82cf827 --- /dev/null +++ b/plugins/modules/system/sysrc.py @@ -0,0 +1,251 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2019 David Lundgren +# +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +--- +author: + - David Lundgren (@dlundgren) +module: sysrc +short_description: Manage FreeBSD using sysrc +version_added: '2.0.0' +description: + - Manages C(/etc/rc.conf) for FreeBSD. +options: + name: + description: + - Name of variable in C(/etc/rc.conf) to manage. + type: str + required: true + value: + description: + - The value to set when I(state=present). + - The value to add when I(state=value_present). + - The value to remove when I(state=value_absent). + type: str + state: + description: + - Use I(present) to add the variable. + - Use I(absent) to remove the variable. + - Use I(value_present) to add the value to the existing variable. + - Use I(value_absent) to remove the value from the existing variable. + type: str + default: "present" + choices: [ absent, present, value_present, value_absent ] + path: + description: + - Path to file to use instead of C(/etc/rc.conf). + type: str + default: "/etc/rc.conf" + delim: + description: + - Delimiter to be used instead of C( ). + - Only used when I(state=value_present) or I(state=value_absent). + default: " " + type: str + jail: + description: + - Name or ID of the jail to operate on. + type: str +notes: + - The C(name) cannot contain periods as sysrc does not support OID style names. +''' + +EXAMPLES = r''' +--- +# enable mysql in the /etc/rc.conf +- name: Configure mysql pid file + community.general.sysrc: + name: mysql_pidfile + value: "/var/run/mysqld/mysqld.pid" + +# enable accf_http kld in the boot loader +- name: Enable accf_http kld + community.general.sysrc: + name: accf_http_load + state: present + value: "YES" + path: /boot/loader.conf + +# add gif0 to cloned_interfaces +- name: Add gif0 interface + community.general.sysrc: + name: cloned_interfaces + state: value_present + value: "gif0" + +# enable nginx on a jail +- name: Enable nginx in test jail + community.general.sysrc: + name: nginx_enable + value: "YES" + jail: testjail +''' + +RETURN = r''' +changed: + description: Return changed for sysrc actions. + returned: always + type: bool + sample: true +''' + +from ansible.module_utils.basic import AnsibleModule +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): + # sysrc doesn't really use exit codes + (rc, out, err) = self.run_sysrc(self.name) + if self.value is None: + regex = "%s: " % re.escape(self.name) + else: + regex = "%s: %s$" % (re.escape(self.name), re.escape(self.value)) + + return not self.has_unknown_variable(out, err) and re.match(regex, out) is not None + + 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 + + if self.module.check_mode: + self.changed = True + return + + (rc, out, err) = self.run_sysrc("%s=%s" % (self.name, self.value)) + if out.find("%s:" % self.name) == 0 and re.search("-> %s$" % re.escape(self.value), out) is not None: + self.changed = True + + def absent(self): + if not self.exists(): + return + + # 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 + + self.changed = True + + def value_present(self): + if self.contains(): + return + + if self.module.check_mode: + self.changed = True + return + + 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 + + def value_absent(self): + if not self.contains(): + return + + if self.module.check_mode: + self.changed = True + return + + 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] + cmd.extend(args) + + (rc, out, err) = self.module.run_command(cmd) + + return (rc, out, err) + + +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, + ) + + 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 alpha-numeric 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 + ) + + rc_value = Sysrc(module, name, value, path, delim, jail) + + 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() + + result['changed'] = rc_value.changed + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/sysrc/aliases b/tests/integration/targets/sysrc/aliases new file mode 100644 index 0000000000..360849e61b --- /dev/null +++ b/tests/integration/targets/sysrc/aliases @@ -0,0 +1,5 @@ +shippable/posix/group1 +needs/root +skip/docker +skip/osx +skip/rhel diff --git a/tests/integration/targets/sysrc/tasks/main.yml b/tests/integration/targets/sysrc/tasks/main.yml new file mode 100644 index 0000000000..c8b7de4160 --- /dev/null +++ b/tests/integration/targets/sysrc/tasks/main.yml @@ -0,0 +1,318 @@ +--- +- name: Test on FreeBSD VMs + when: + - ansible_facts.virtualization_type != 'docker' + - ansible_facts.distribution == 'FreeBSD' + block: + - name: Cache original contents of /etc/rc.conf + shell: "cat /etc/rc.conf" + register: sysrc_original_content + + ## + ## sysrc - example - set mysqlpidfile + ## + - name: Configure mysql pid file + sysrc: + name: mysql_pidfile + value: /tmp/mysql.pid + register: sysrc_example1 + + - name: Configure mysql pid file (checkmode) + sysrc: + name: mysql_pidfile + value: checkmode + check_mode: yes + register: sysrc_example1_checkmode + + - name: Configure mysql pid file (idempotent) + sysrc: + name: mysql_pidfile + value: /tmp/mysql.pid + register: sysrc_example1_idempotent + + - name: Get file content + shell: "cat /etc/rc.conf | egrep -v ^\\#" + register: sysrc_example1_content + + - name: Ensure sysrc updates rc.conf properly + assert: + that: + - sysrc_example1.changed + - sysrc_example1_checkmode.changed + - not sysrc_example1_idempotent.changed + - "'mysql_pidfile=\"/tmp/mysql.pid\"' in sysrc_example1_content.stdout_lines" + - "'mysql_pidfile=\"checkmode\"' not in sysrc_example1_content.stdout_lines" + + ## + ## sysrc - example - Enable accf_http kld in /boot/loader.conf + ## + - name: Enable accf_http kld in /boot/loader.conf + sysrc: + name: accf_http_load + state: present + value: "YES" + path: /boot/loader.conf + register: sysrc_example2 + + - name: Enable accf_http kld in /boot/loader.conf (checkmode) + sysrc: + name: accf_http_load + state: present + value: "NO" + path: /boot/loader.conf + check_mode: yes + register: sysrc_example2_checkmode + + - name: Enable accf_http kld in /boot/loader.conf (idempotent) + sysrc: + name: accf_http_load + state: present + value: "YES" + path: /boot/loader.conf + register: sysrc_example2_idempotent + + - name: Get file content + shell: "cat /boot/loader.conf | egrep -v ^\\#" + register: sysrc_example2_content + + - name: Ensure sysrc did not change the file, but marked as changed + assert: + that: + - sysrc_example2.changed + - sysrc_example2_checkmode.changed + - not sysrc_example2_idempotent.changed + - "'accf_http_load=\"YES\"' in sysrc_example2_content.stdout_lines" + - "'accf_http_load=\"NO\"' not in sysrc_example2_content.stdout_lines" + + ## + ## sysrc - example - Add gif0 interface + ## + - name: Set cloned_interfaces + sysrc: + name: cloned_interfaces + value: "lo0" + + - name: Add gif0 interface + sysrc: + name: cloned_interfaces + state: value_present + value: "gif0" + register: sysrc_example3 + + - name: Add gif1 interface (checkmode) + sysrc: + name: cloned_interfaces + state: value_present + value: "gif1" + check_mode: yes + register: sysrc_example3_checkmode + + - name: Add gif0 interface (idempotent) + sysrc: + name: cloned_interfaces + state: value_present + value: "gif0" + register: sysrc_example3_idempotent + + - name: Get file content + shell: "cat /etc/rc.conf | egrep -v ^\\#" + register: sysrc_example3_content + + - name: Ensure sysrc did not change the file, but marked as changed + assert: + that: + - sysrc_example3.changed + - sysrc_example3_checkmode.changed + - not sysrc_example3_idempotent.changed + - "'cloned_interfaces=\"lo0 gif0\"' in sysrc_example3_content.stdout_lines" + + ## + ## sysrc - example - Enable nginx in testjail + ## + - name: Test within jail + block: + - name: Setup testjail + include: setup-testjail.yml + + - name: Enable nginx in test jail + sysrc: + name: nginx_enable + value: "YES" + jail: testjail + register: sysrc_example4 + + - name: Enable nginx in test jail (checkmode) + sysrc: + name: nginx_enable + value: "NO" + jail: testjail + check_mode: yes + register: sysrc_example4_checkmode + + - name: Enable nginx in test jail (idempotent) + sysrc: + name: nginx_enable + value: "YES" + jail: testjail + register: sysrc_example4_idempotent + + - name: Get file content + shell: "cat /usr/jails/testjail/etc/rc.conf | grep nginx_enable" + register: sysrc_example4_content + + - name: Ensure sysrc worked in testjail + assert: + that: + - sysrc_example4.changed + - sysrc_example4_checkmode.changed + - not sysrc_example4_idempotent.changed + - "'nginx_enable=\"YES\"' in sysrc_example4_content.stdout_lines" + always: + - name: Stop and remove testjail + failed_when: false + changed_when: false + command: "ezjail-admin delete -wf testjail" + + ## + ## sysrc - Test Absent + ## + - name: Set sysrc_absent to test removal + sysrc: + name: sysrc_absent + value: test + + - name: Remove sysrc_absent (checkmode) + sysrc: + name: sysrc_absent + state: absent + check_mode: yes + register: sysrc_absent_checkmode + + - name: Remove sysrc_absent + sysrc: + name: sysrc_absent + state: absent + register: sysrc_absent + + - name: Remove sysrc_absent (idempotent) + sysrc: + name: sysrc_absent + state: absent + register: sysrc_absent_idempotent + + - name: Get file content + shell: "cat /etc/rc.conf | egrep -v ^\\#" + register: sysrc_absent_content + + - name: Ensure sysrc did as intended + assert: + that: + - sysrc_absent_checkmode.changed + - sysrc_absent.changed + - not sysrc_absent_idempotent.changed + - "'sysrc_absent=\"test\"' not in sysrc_absent_content.stdout_lines" + + ## + ## sysrc - Test alternate delimiter + ## + - name: Set sysrc_delim to known value + sysrc: + name: sysrc_delim + value: "t1,t2" + + - name: Add to value with delimiter (not-exists) + sysrc: + name: sysrc_delim_create + state: value_present + delim: "," + value: t3 + register: sysrc_delim_create + + - name: Add to value with delimiter + sysrc: + name: sysrc_delim + state: value_present + delim: "," + value: t3 + register: sysrc_delim + + - name: Add to value with delimiter (checkmode) + sysrc: + name: sysrc_delim + state: value_present + delim: "," + value: t4 + check_mode: yes + register: sysrc_delim_checkmode + + - name: Add to value with delimiter (idempotent) + sysrc: + name: sysrc_delim + state: value_present + delim: "," + value: t3 + register: sysrc_delim_idempotent + + - name: Get file content + shell: "cat /etc/rc.conf | egrep -v ^\\#" + register: sysrc_delim_content + + - name: Ensure sysrc did as intended + assert: + that: + - sysrc_delim_create.changed + - sysrc_delim.changed + - sysrc_delim_checkmode.changed + - not sysrc_delim_idempotent.changed + - "'sysrc_delim=\"t1,t2,t3\"' in sysrc_delim_content.stdout_lines" + - "'sysrc_delim_create=\"t3\"' in sysrc_delim_content.stdout_lines" + + ## + ## sysrc - value_absent + ## + - name: Remove value (when not exists) + sysrc: + name: sysrc_value_absent_delete + state: value_absent + delim: "," + value: t3 + register: sysrc_value_absent_ignored + + - name: Remove value from sysrc_delim + sysrc: + name: sysrc_delim + state: value_absent + value: t3 + delim: "," + register: sysrc_value_absent + + - name: Remove value from sysrc_delim (checkmode) + sysrc: + name: sysrc_delim + state: value_absent + value: t2 + delim: "," + check_mode: yes + register: sysrc_value_absent_checkmode + + - name: Remove value from sysrc_delim (idempotent + sysrc: + name: sysrc_delim + state: value_absent + value: t3 + delim: "," + register: sysrc_value_absent_idempotent + + - name: Get file content + shell: "cat /etc/rc.conf | egrep -v ^\\#" + register: sysrc_delim_content + + - name: Ensure sysrc did as intended with value_absent + assert: + that: + - not sysrc_value_absent_ignored.changed + - sysrc_value_absent.changed + - sysrc_value_absent_checkmode.changed + - not sysrc_value_absent_idempotent.changed + - "'sysrc_delim=\"t1,t2\"' in sysrc_delim_content.stdout_lines" + - "'sysrc_delim_delete' not in sysrc_delim_content.stdout_lines" diff --git a/tests/integration/targets/sysrc/tasks/setup-testjail.yml b/tests/integration/targets/sysrc/tasks/setup-testjail.yml new file mode 100644 index 0000000000..9bd15320ae --- /dev/null +++ b/tests/integration/targets/sysrc/tasks/setup-testjail.yml @@ -0,0 +1,62 @@ +--- +# +# Instructions for setting up a jail +# https://www.freebsd.org/doc/en_US.ISO8859-1/books/handbook/jails-ezjail.html +# +- name: Setup cloned interfaces + lineinfile: + dest: /etc/rc.conf + regexp: ^cloned_interfaces=lo1 + line: cloned_interfaces=lo1 + +- name: Activate cloned interfaces + command: "service netif cloneup" + changed_when: false + +- name: Install ezjail + pkgng: + name: ezjail + +- name: Configure ezjail to use archive for old freebsd releases + when: ansible_distribution_version is version('11.01', '<=') + lineinfile: + dest: /usr/local/etc/ezjail.conf + regexp: ^ezjail_ftphost + line: ezjail_ftphost=ftp-archive.freebsd.org + +- name: Start ezjail + ignore_errors: yes + service: + name: ezjail + state: started + enabled: yes + +- name: Has ezjail + register: ezjail_base_jail + stat: + path: /usr/jails/basejail + +- name: Setup ezjail base + when: not ezjail_base_jail.stat.exists + shell: "ezjail-admin install >> /tmp/ezjail.log" + changed_when: false + +- name: Has testjail + register: ezjail_test_jail + stat: + path: /usr/jails/testjail + +- name: Create testjail + when: not ezjail_test_jail.stat.exists + shell: "ezjail-admin create testjail 'lo1|127.0.1.1' >> /tmp/ezjail.log" + changed_when: false + +- name: Is testjail running + shell: "jls | grep testjail" + changed_when: false + failed_when: false + register: is_testjail_up + +- name: Start testjail + when: is_testjail_up.rc == 1 + command: "ezjail-admin start testjail"