1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2019-2020, Andrew Klaus <andrewklaus@gmail.com>
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import (absolute_import, division, print_function)
8__metaclass__ = type
9
10DOCUMENTATION = r'''
11---
12module: syspatch
13
14short_description: Manage OpenBSD system patches
15
16
17description:
18    - "Manage OpenBSD system patches using syspatch."
19
20options:
21    revert:
22        description:
23            - Revert system patches.
24        type: str
25        choices: [ all, one ]
26
27author:
28    - Andrew Klaus (@precurse)
29'''
30
31EXAMPLES = '''
32- name: Apply all available system patches
33  community.general.syspatch:
34
35- name: Revert last patch
36  community.general.syspatch:
37    revert: one
38
39- name: Revert all patches
40  community.general.syspatch:
41    revert: all
42
43# NOTE: You can reboot automatically if a patch requires it:
44- name: Apply all patches and store result
45  community.general.syspatch:
46  register: syspatch
47
48- name: Reboot if patch requires it
49  ansible.builtin.reboot:
50  when: syspatch.reboot_needed
51'''
52
53RETURN = r'''
54rc:
55  description: The command return code (0 means success)
56  returned: always
57  type: int
58stdout:
59  description: syspatch standard output.
60  returned: always
61  type: str
62  sample: "001_rip6cksum"
63stderr:
64  description: syspatch standard error.
65  returned: always
66  type: str
67  sample: "syspatch: need root privileges"
68reboot_needed:
69  description: Whether or not a reboot is required after an update.
70  returned: always
71  type: bool
72  sample: True
73'''
74
75from ansible.module_utils.basic import AnsibleModule
76
77
78def run_module():
79    # define available arguments/parameters a user can pass to the module
80    module_args = dict(
81        revert=dict(type='str', choices=['all', 'one'])
82    )
83
84    module = AnsibleModule(
85        argument_spec=module_args,
86        supports_check_mode=True,
87    )
88
89    result = syspatch_run(module)
90
91    module.exit_json(**result)
92
93
94def syspatch_run(module):
95    cmd = module.get_bin_path('syspatch', True)
96    changed = False
97    reboot_needed = False
98    warnings = []
99
100    # Set safe defaults for run_flag and check_flag
101    run_flag = ['-c']
102    check_flag = ['-c']
103    if module.params['revert']:
104        check_flag = ['-l']
105
106        if module.params['revert'] == 'all':
107            run_flag = ['-R']
108        else:
109            run_flag = ['-r']
110    else:
111        check_flag = ['-c']
112        run_flag = []
113
114    # Run check command
115    rc, out, err = module.run_command([cmd] + check_flag)
116
117    if rc != 0:
118        module.fail_json(msg="Command %s failed rc=%d, out=%s, err=%s" % (cmd, rc, out, err))
119
120    if len(out) > 0:
121        # Changes pending
122        change_pending = True
123    else:
124        # No changes pending
125        change_pending = False
126
127    if module.check_mode:
128        changed = change_pending
129    elif change_pending:
130        rc, out, err = module.run_command([cmd] + run_flag)
131
132        # Workaround syspatch ln bug:
133        # http://openbsd-archive.7691.n7.nabble.com/Warning-applying-latest-syspatch-td354250.html
134        if rc != 0 and err != 'ln: /usr/X11R6/bin/X: No such file or directory\n':
135            module.fail_json(msg="Command %s failed rc=%d, out=%s, err=%s" % (cmd, rc, out, err))
136        elif out.lower().find('create unique kernel') >= 0:
137            # Kernel update applied
138            reboot_needed = True
139        elif out.lower().find('syspatch updated itself') >= 0:
140            warnings.append('Syspatch was updated. Please run syspatch again.')
141
142        # If no stdout, then warn user
143        if len(out) == 0:
144            warnings.append('syspatch had suggested changes, but stdout was empty.')
145
146        changed = True
147    else:
148        changed = False
149
150    return dict(
151        changed=changed,
152        reboot_needed=reboot_needed,
153        rc=rc,
154        stderr=err,
155        stdout=out,
156        warnings=warnings
157    )
158
159
160def main():
161    run_module()
162
163
164if __name__ == '__main__':
165    main()
166