You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

360 lines
10 KiB

#!/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, 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, 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, merge, create, backup)
else:
absent(module, dest, conf, backup)
if __name__ == '__main__':
main()