|
|
@ -0,0 +1,360 @@ |
|
|
|
#!/usr/bin/python |
|
|
|
|
|
|
|
ANSIBLE_METADATA = { |
|
|
|
'metadata_version': '1.1', |
|
|
|
'status': ['preview'], |
|
|
|
'supported_by': 'community' |
|
|
|
} |
|
|
|
|
|
|
|
DOCUMENTATION = ''' |
|
|
|
--- |
|
|
|
module: jsonconfig |
|
|
|
|
|
|
|
short_description: Ensure a particular configuration is added to a json-formatted configuration file |
|
|
|
|
|
|
|
version_added: "2.4" |
|
|
|
|
|
|
|
description: |
|
|
|
- This module will add configuration to a json-formatted configuration file. |
|
|
|
|
|
|
|
options: |
|
|
|
dest: |
|
|
|
description: |
|
|
|
- The file to modify. |
|
|
|
required: true |
|
|
|
aliases: [ name, destfile ] |
|
|
|
json: |
|
|
|
description: |
|
|
|
- The configuration in json format to apply. |
|
|
|
required: false |
|
|
|
default: '{}' |
|
|
|
merge: |
|
|
|
description: |
|
|
|
- Used with C(state=present). If specified, it will merge the configuration. Othwerwise |
|
|
|
the configuration will be overwritten. |
|
|
|
required: false |
|
|
|
choices: [ "yes", "no" ] |
|
|
|
default: "yes" |
|
|
|
state: |
|
|
|
description: |
|
|
|
- Whether the configuration should be there or not. |
|
|
|
required: false |
|
|
|
choices: [ present, absent ] |
|
|
|
default: "present" |
|
|
|
create: |
|
|
|
description: |
|
|
|
- Used with C(state=present). If specified, the file will be created |
|
|
|
if it does not already exist. By default it will fail if the file |
|
|
|
is missing. |
|
|
|
required: false |
|
|
|
choices: [ "yes", "no" ] |
|
|
|
default: "no" |
|
|
|
backup: |
|
|
|
description: |
|
|
|
- Create a backup file including the timestamp information so you can |
|
|
|
get the original file back if you somehow clobbered it incorrectly. |
|
|
|
required: false |
|
|
|
choices: [ "yes", "no" ] |
|
|
|
default: "no" |
|
|
|
others: |
|
|
|
description: |
|
|
|
- All arguments accepted by the M(file) module also work here. |
|
|
|
required: false |
|
|
|
|
|
|
|
extends_documentation_fragment: |
|
|
|
- files |
|
|
|
- validate |
|
|
|
|
|
|
|
author: |
|
|
|
- "Greg Szabo (@greg-szabo)" |
|
|
|
''' |
|
|
|
|
|
|
|
EXAMPLES = ''' |
|
|
|
# Add a new section to a json file |
|
|
|
- name: Add comment section |
|
|
|
jsonconfig: |
|
|
|
dest: /etc/something.json |
|
|
|
json: '{ "comment": { "comment1": "mycomment" } }' |
|
|
|
|
|
|
|
# Rewrite a json file with the configuration |
|
|
|
- name: Create or overwrite config.json |
|
|
|
jsonconfig: |
|
|
|
dest: /etc/config.json |
|
|
|
json: '{ "regedit": { "freshfile": true } }' |
|
|
|
merge: no |
|
|
|
create: yes |
|
|
|
|
|
|
|
''' |
|
|
|
|
|
|
|
RETURN = ''' |
|
|
|
changed: |
|
|
|
description: True if the configuration changed. |
|
|
|
type: bool |
|
|
|
msg: |
|
|
|
description: Description of the change |
|
|
|
type: str |
|
|
|
''' |
|
|
|
|
|
|
|
from ansible.module_utils.basic import AnsibleModule |
|
|
|
from ansible.module_utils.six import b |
|
|
|
from ansible.module_utils._text import to_bytes, to_native |
|
|
|
import tempfile |
|
|
|
import json |
|
|
|
import copy |
|
|
|
import os |
|
|
|
|
|
|
|
def write_changes(module, b_lines, dest): |
|
|
|
|
|
|
|
tmpfd, tmpfile = tempfile.mkstemp() |
|
|
|
f = os.fdopen(tmpfd, 'wb') |
|
|
|
f.writelines(b_lines) |
|
|
|
f.close() |
|
|
|
|
|
|
|
validate = module.params.get('validate', None) |
|
|
|
valid = not validate |
|
|
|
if validate: |
|
|
|
if "%s" not in validate: |
|
|
|
module.fail_json(msg="validate must contain %%s: %s" % (validate)) |
|
|
|
(rc, out, err) = module.run_command(to_bytes(validate % tmpfile, errors='surrogate_or_strict')) |
|
|
|
valid = rc == 0 |
|
|
|
if rc != 0: |
|
|
|
module.fail_json(msg='failed to validate: ' |
|
|
|
'rc:%s error:%s' % (rc, err)) |
|
|
|
if valid: |
|
|
|
module.atomic_move(tmpfile, |
|
|
|
to_native(os.path.realpath(to_bytes(dest, errors='surrogate_or_strict')), errors='surrogate_or_strict'), |
|
|
|
unsafe_writes=module.params['unsafe_writes']) |
|
|
|
|
|
|
|
|
|
|
|
def check_file_attrs(module, changed, message, diff): |
|
|
|
|
|
|
|
file_args = module.load_file_common_arguments(module.params) |
|
|
|
if module.set_fs_attributes_if_different(file_args, False, diff=diff): |
|
|
|
|
|
|
|
if changed: |
|
|
|
message += " and " |
|
|
|
changed = True |
|
|
|
message += "ownership, perms or SE linux context changed" |
|
|
|
|
|
|
|
return message, changed |
|
|
|
|
|
|
|
|
|
|
|
#Merge dict d2 into dict d1 and return a new object |
|
|
|
def deepmerge(d1, d2): |
|
|
|
if d1 is None: |
|
|
|
return copy.deepcopy(d2) |
|
|
|
if d2 is None: |
|
|
|
return copy.deepcopy(d1) |
|
|
|
if d1 == d2: |
|
|
|
return copy.deepcopy(d1) |
|
|
|
if isinstance(d1, dict) and isinstance(d2, dict): |
|
|
|
result={} |
|
|
|
for key in set(d1.keys()+d2.keys()): |
|
|
|
da = db = None |
|
|
|
if key in d1: |
|
|
|
da = d1[key] |
|
|
|
if key in d2: |
|
|
|
db = d2[key] |
|
|
|
result[key] = deepmerge(da, db) |
|
|
|
return result |
|
|
|
else: |
|
|
|
return copy.deepcopy(d2) |
|
|
|
|
|
|
|
|
|
|
|
#Remove dict d2 from dict d1 and return a new object |
|
|
|
def deepdiff(d1, d2): |
|
|
|
if d1 is None or d2 is None: |
|
|
|
return None |
|
|
|
if d1 == d2: |
|
|
|
return None |
|
|
|
if isinstance(d1, dict) and isinstance(d2, dict): |
|
|
|
result = {} |
|
|
|
for key in d1.keys(): |
|
|
|
if key in d2: |
|
|
|
dd = deepdiff(d1[key],d2[key]) |
|
|
|
if dd is not None: |
|
|
|
result[key] = dd |
|
|
|
else: |
|
|
|
result[key] = d1[key] |
|
|
|
return result |
|
|
|
else: |
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
def present(module, dest, conf, jsonbool, merge, create, backup): |
|
|
|
|
|
|
|
diff = {'before': '', |
|
|
|
'after': '', |
|
|
|
'before_header': '%s (content)' % dest, |
|
|
|
'after_header': '%s (content)' % dest} |
|
|
|
|
|
|
|
b_dest = to_bytes(dest, errors='surrogate_or_strict') |
|
|
|
if not os.path.exists(b_dest): |
|
|
|
if not create: |
|
|
|
module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) |
|
|
|
b_destpath = os.path.dirname(b_dest) |
|
|
|
if not os.path.exists(b_destpath) and not module.check_mode: |
|
|
|
os.makedirs(b_destpath) |
|
|
|
b_lines = [] |
|
|
|
else: |
|
|
|
f = open(b_dest, 'rb') |
|
|
|
b_lines = f.readlines() |
|
|
|
f.close() |
|
|
|
|
|
|
|
lines = to_native(b('').join(b_lines)) |
|
|
|
|
|
|
|
if module._diff: |
|
|
|
diff['before'] = lines |
|
|
|
|
|
|
|
b_conf = to_bytes(conf, errors='surrogate_or_strict') |
|
|
|
|
|
|
|
jsonconfig = json.loads(lines) |
|
|
|
config = eval(b_conf) |
|
|
|
|
|
|
|
if not isinstance(config, dict): |
|
|
|
module.fail_json(msg="Invalid value in json parameter: {0}".format(config)) |
|
|
|
|
|
|
|
b_lines_new = b_lines |
|
|
|
msg = '' |
|
|
|
changed = False |
|
|
|
|
|
|
|
if not merge: |
|
|
|
if jsonconfig != config: |
|
|
|
b_lines_new = to_bytes(json.dumps(config, sort_keys=True, indent=4, separators=(',', ': '))) |
|
|
|
msg = 'config overwritten' |
|
|
|
changed = True |
|
|
|
else: |
|
|
|
mergedconfig = deepmerge(jsonconfig,config) |
|
|
|
if jsonconfig != mergedconfig: |
|
|
|
b_lines_new = to_bytes(json.dumps(mergedconfig), sort_keys=True, indent=4, separators=(',', ': '))) |
|
|
|
msg = 'config merged' |
|
|
|
changed = True |
|
|
|
|
|
|
|
if module._diff: |
|
|
|
diff['after'] = to_native(b('').join(b_lines_new)) |
|
|
|
|
|
|
|
backupdest = "" |
|
|
|
if changed and not module.check_mode: |
|
|
|
if backup and os.path.exists(b_dest): |
|
|
|
backupdest = module.backup_local(dest) |
|
|
|
write_changes(module, b_lines_new, dest) |
|
|
|
|
|
|
|
if module.check_mode and not os.path.exists(b_dest): |
|
|
|
module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=diff) |
|
|
|
|
|
|
|
attr_diff = {} |
|
|
|
msg, changed = check_file_attrs(module, changed, msg, attr_diff) |
|
|
|
|
|
|
|
attr_diff['before_header'] = '%s (file attributes)' % dest |
|
|
|
attr_diff['after_header'] = '%s (file attributes)' % dest |
|
|
|
|
|
|
|
difflist = [diff, attr_diff] |
|
|
|
module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist) |
|
|
|
|
|
|
|
|
|
|
|
def absent(module, dest, conf, jsonbool, backup): |
|
|
|
|
|
|
|
b_dest = to_bytes(dest, errors='surrogate_or_strict') |
|
|
|
if not os.path.exists(b_dest): |
|
|
|
module.exit_json(changed=False, msg="file not present") |
|
|
|
|
|
|
|
msg = '' |
|
|
|
diff = {'before': '', |
|
|
|
'after': '', |
|
|
|
'before_header': '%s (content)' % dest, |
|
|
|
'after_header': '%s (content)' % dest} |
|
|
|
|
|
|
|
f = open(b_dest, 'rb') |
|
|
|
b_lines = f.readlines() |
|
|
|
f.close() |
|
|
|
|
|
|
|
lines = to_native(b('').join(b_lines)) |
|
|
|
b_conf = to_bytes(conf, errors='surrogate_or_strict') |
|
|
|
|
|
|
|
lines = to_native(b('').join(b_lines)) |
|
|
|
jsonconfig = json.loads(lines) |
|
|
|
config = eval(b_conf) |
|
|
|
|
|
|
|
if not isinstance(config, dict): |
|
|
|
module.fail_json(msg="Invalid value in json parameter: {0}".format(config)) |
|
|
|
|
|
|
|
if module._diff: |
|
|
|
diff['before'] = to_native(b('').join(b_lines)) |
|
|
|
|
|
|
|
b_lines_new = b_lines |
|
|
|
msg = '' |
|
|
|
changed = False |
|
|
|
|
|
|
|
diffconfig = deepdiff(jsonconfig,config) |
|
|
|
if diffconfig is None: |
|
|
|
diffconfig = {} |
|
|
|
if jsonconfig != diffconfig: |
|
|
|
b_lines_new = to_bytes(json.dumps(diffconfig, sort_keys=True, indent=4, separators=(',', ': '))) |
|
|
|
msg = 'config removed' |
|
|
|
changed = True |
|
|
|
|
|
|
|
if module._diff: |
|
|
|
diff['after'] = to_native(b('').join(b_lines_new)) |
|
|
|
|
|
|
|
backupdest = "" |
|
|
|
if changed and not module.check_mode: |
|
|
|
if backup: |
|
|
|
backupdest = module.backup_local(dest) |
|
|
|
write_changes(module, b_lines_new, dest) |
|
|
|
|
|
|
|
attr_diff = {} |
|
|
|
msg, changed = check_file_attrs(module, changed, msg, attr_diff) |
|
|
|
|
|
|
|
attr_diff['before_header'] = '%s (file attributes)' % dest |
|
|
|
attr_diff['after_header'] = '%s (file attributes)' % dest |
|
|
|
|
|
|
|
difflist = [diff, attr_diff] |
|
|
|
|
|
|
|
module.exit_json(changed=changed, msg=msg, backup=backupdest, diff=difflist) |
|
|
|
|
|
|
|
|
|
|
|
def main(): |
|
|
|
|
|
|
|
# define the available arguments/parameters that a user can pass to |
|
|
|
# the module |
|
|
|
module_args = dict( |
|
|
|
dest=dict(type='str', required=True), |
|
|
|
json=dict(default=None, required=True), |
|
|
|
merge=dict(type='bool', default=True), |
|
|
|
state=dict(default='present', choices=['absent', 'present']), |
|
|
|
create=dict(type='bool', default=False), |
|
|
|
backup=dict(type='bool', default=False), |
|
|
|
validate=dict(default=None, type='str') |
|
|
|
) |
|
|
|
|
|
|
|
# the AnsibleModule object will be our abstraction working with Ansible |
|
|
|
# this includes instantiation, a couple of common attr would be the |
|
|
|
# args/params passed to the execution, as well as if the module |
|
|
|
# supports check mode |
|
|
|
module = AnsibleModule( |
|
|
|
argument_spec=module_args, |
|
|
|
add_file_common_args=True, |
|
|
|
supports_check_mode=True |
|
|
|
) |
|
|
|
|
|
|
|
params = module.params |
|
|
|
create = params['create'] |
|
|
|
merge = params['merge'] |
|
|
|
backup = params['backup'] |
|
|
|
dest = params['dest'] |
|
|
|
|
|
|
|
b_dest = to_bytes(dest, errors='surrogate_or_strict') |
|
|
|
|
|
|
|
if os.path.isdir(b_dest): |
|
|
|
module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) |
|
|
|
|
|
|
|
conf = params['json'] |
|
|
|
|
|
|
|
if params['state'] == 'present': |
|
|
|
present(module, dest, conf, jsonbool, merge, create, backup) |
|
|
|
else: |
|
|
|
absent(module, dest, conf, jsonbool, backup) |
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
|
main() |
|
|
|
|