diff options
Diffstat (limited to 'ansible')
-rw-r--r-- | ansible/docker/.gitignore | 3 | ||||
-rw-r--r-- | ansible/docker/Dockerfile | 34 | ||||
-rwxr-xr-x | ansible/docker/build_ansible_image.sh | 52 | ||||
-rwxr-xr-x | ansible/docker/create_docker_chroot.sh | 220 | ||||
-rwxr-xr-x | ansible/docker/run_chroot.sh | 465 | ||||
-rw-r--r-- | ansible/library/json_add.py | 90 |
6 files changed, 864 insertions, 0 deletions
diff --git a/ansible/docker/.gitignore b/ansible/docker/.gitignore new file mode 100644 index 00000000..7df2d855 --- /dev/null +++ b/ansible/docker/.gitignore @@ -0,0 +1,3 @@ +ansible_docker.tar +ansible_chroot.tgz +ansible_chroot diff --git a/ansible/docker/Dockerfile b/ansible/docker/Dockerfile new file mode 100644 index 00000000..b0172709 --- /dev/null +++ b/ansible/docker/Dockerfile @@ -0,0 +1,34 @@ +FROM alpine:3.8 + +ARG ansible_version=2.6.3 +LABEL ansible_version=$ansible_version vendor=Samsung + +# Install Ansible build dependencies +RUN apk --no-cache update \ +&& apk --no-cache --update add --virtual build-dependencies \ + gcc \ + make \ + musl-dev \ + libffi-dev \ + openssl-dev \ + python3-dev \ +&& apk add --no-cache \ + python3 \ + py3-pip \ + openssh-client \ + openssl \ + py3-openssl \ + openssh \ + sshpass \ +&& pip3 install --no-cache-dir --upgrade pip \ +&& pip3 install --no-cache-dir \ + ansible==$ansible_version \ + jmespath \ +&& apk del build-dependencies && rm -rf /var/cache/apk/* + +ENV ANSIBLE_HOST_KEY_CHECKING false +ENV ANSIBLE_RETRY_FILES_ENABLED false + +WORKDIR /ansible + +ENTRYPOINT ["ansible-playbook"] diff --git a/ansible/docker/build_ansible_image.sh b/ansible/docker/build_ansible_image.sh new file mode 100755 index 00000000..d54ddc43 --- /dev/null +++ b/ansible/docker/build_ansible_image.sh @@ -0,0 +1,52 @@ +#! /usr/bin/env bash + +# COPYRIGHT NOTICE STARTS HERE + +# Copyright 2018 © Samsung Electronics Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# COPYRIGHT NOTICE ENDS HERE + + +set -e + +ansible_version="$1" +image_name="${2:-ansible:latest}" + +script_path=$(readlink -f "$0") +script_dir=$(dirname "$script_path") + +git_commit=$(git rev-parse --revs-only HEAD) +build_date=$(date -I) + +if [ -z "$ansible_version" ]; then + docker build "$script_dir" -t "${image_name}" --label "git-commit=$git_commit" --label "build-date=$build_date" +else + docker build "$script_dir" -t "${image_name}" --label "git-commit=$git_commit" --label "build-date=$build_date" --build-arg ansible_version="$ansible_version" +fi + +# Export docker image into chroot and tararchive it. It takes ~40M of space and is packaged together with sw. +if "${script_dir}"/create_docker_chroot.sh convert "${image_name}" "${script_dir}"/ansible_chroot ; then + cd "$script_dir" + echo INFO: "Tarring and zipping the chroot directory..." >&2 + tar -czf ansible_chroot.tgz ansible_chroot + rm -rf "${script_dir}"/ansible_chroot + echo INFO: "Finished: ${script_dir}/ansible_chroot.tgz" >&2 + cd - +else + echo ERROR: "I failed to create a chroot environment" >&2 + exit 1 +fi + +exit 0
\ No newline at end of file diff --git a/ansible/docker/create_docker_chroot.sh b/ansible/docker/create_docker_chroot.sh new file mode 100755 index 00000000..f8e256da --- /dev/null +++ b/ansible/docker/create_docker_chroot.sh @@ -0,0 +1,220 @@ +#!/bin/sh + +# COPYRIGHT NOTICE STARTS HERE + +# Copyright 2018 © Samsung Electronics Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# COPYRIGHT NOTICE ENDS HERE + + +set -e + +CMD=$(basename "$0") + +help() +{ + echo " +NAME: + ${CMD} - create a chroot directory from docker image + +DESCRIPTION: + It will export docker image into a directory capable of chrooting. + It needs and will run these commands (requires docker service): + docker create + docker export + +USAGE: + ${CMD} [-h|--help|help] + This help + + ${CMD} convert <docker-name> <name-of-directory> + + It will convert docker image into directory - no chroot yet. + The name of the docker image must be imported already (not a file): + docker image ls + + The directory will be created and so this command will fail if some + directory or a file of this name (filepath) already exists! + There is another script run_chroot.sh with which you can do chroot + on this newly created directory - so it is expected that this + directory is kept clean and as it is. + If you don't care about this feature (run_chroot.sh) and you know + what are you doing, then do necessary mounts and execute: + chroot <name-of-directory>/chroot /bin/sh -l +" +} + +# +# PLEASE DON'T TOUCH ME +# + +# readme file for run_chroot.sh +readme() +{ + md_codequote='```' + +cat > "$CHROOT_METADIR"/README.md <<EOF +# RUN CHROOT COMMAND + +# usage: + +${md_codequote} +run_chroot.sh help +${md_codequote} + +**Don't modify insides of this directory (where this README.md lies).** + +The structure is needed as it is. + +If you wish to just run chroot by yourself, you can do: +${md_codequote} +chroot ./chroot /bin/sh -l +${md_codequote} + +# requirements: + +* root privileges +* docker service + +# directory structure: +${md_codequote} + README.md + chroot/ + .overlay + .workdir + .merged +${md_codequote} +EOF +} + +# arg: <docker-name> +check_docker_image() +{ + image="$1" + match=$(docker image ls --no-trunc -q "$image" | wc -l) + + case $match in + 0) + echo ERROR: "Docker image does not exist: ${DOCKER_IMAGE}" >&2 + exit 1 + ;; + 1) + : + ;; + *) + echo ERROR: "Multiple results for this docker name: ${DOCKER_IMAGE}" >&2 + exit 1 + ;; + esac + + return 0 +} + +cleanup() +{ + if [ -n "$DOCKER_CONTAINER" ] ; then + echo INFO: "Delete the export container: ${DOCKER_CONTAINER}" >&2 + if ! docker rm "$DOCKER_CONTAINER" > /dev/null ; then + echo ERROR: "Failed to delete: ${DOCKER_CONTAINER}" >&2 + fi + fi +} + +on_exit() +{ + set +e + cleanup +} + +action=nil +case "$1" in + ''|-h|--help|help) + help + exit 0 + ;; + convert) + action=convert + DOCKER_IMAGE="$2" + CHROOT_METADIR="$3" + ;; + *) + echo ERROR: "Bad usage" >&2 + help >&2 + exit 1 + ;; +esac + + +case "$action" in + ''|nil) + echo ERROR: "Nothing to do - missing command" >&2 + help >&2 + exit 1 + ;; + convert) + if [ -z "$DOCKER_IMAGE" ] || [ -z "$CHROOT_METADIR" ] ; then + echo ERROR: "Missing argument" >&2 + help >&2 + exit 1 + fi + + if [ -e "$CHROOT_METADIR" ] ; then + echo ERROR: "Filepath already exists: ${CHROOT_METADIR}" >&2 + echo ERROR: "Please rename it, remove it or use different name" >&2 + echo ERROR: "I need my working directory empty, thanks" >&2 + exit 1 + fi + + # check if docker image is there + check_docker_image "$DOCKER_IMAGE" + + # we must be root + if [ "$(id -u)" -ne 0 ] ; then + echo ERROR: "I need root privileges and you are not root: $(id -nu)" >&2 + exit 1 + fi + + # making sure that CHROOT_METADIR is absolute path + CHROOT_METADIR=$(readlink -f "$CHROOT_METADIR") + + # set trap + trap on_exit INT QUIT TERM EXIT + + # making readme + mkdir -p "$CHROOT_METADIR"/ + readme + + # create container + DOCKER_CONTAINER=$(docker create "$DOCKER_IMAGE") + if [ -z "$DOCKER_CONTAINER" ] ; then + echo ERROR: "I could not create a container from: ${DOCKER_IMAGE}" >&2 + exit 1 + fi + + # unpacking of image + mkdir -p "$CHROOT_METADIR"/chroot + echo INFO: "Export started - it can take a while to finish..." >&2 + if ! docker export "$DOCKER_CONTAINER" | tar -C "$CHROOT_METADIR"/chroot -xf - ; then + echo ERROR: "Unpacking failed - permissions?" >&2 + exit 1 + else + echo INFO: "Export success: $CHROOT_METADIR/chroot" >&2 + echo INFO: "Checkout the README file: $CHROOT_METADIR/README.md" >&2 + fi + ;; +esac + +exit 0 + diff --git a/ansible/docker/run_chroot.sh b/ansible/docker/run_chroot.sh new file mode 100755 index 00000000..b38c1295 --- /dev/null +++ b/ansible/docker/run_chroot.sh @@ -0,0 +1,465 @@ +#!/bin/sh + +# COPYRIGHT NOTICE STARTS HERE + +# Copyright 2018 © Samsung Electronics Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# COPYRIGHT NOTICE ENDS HERE + + +set -e + +CMD=$(basename "$0") +UMOUNT_TIMEOUT=120 # 2mins + + +# +# functions +# + +help() +{ + echo " +NAME: + ${CMD} - run command in chrooted directory + +DESCRIPTION: + It will do necessary steps to be able chroot, optional mounts and it will + run commands inside the requested chroot directory. + + It does overlay mount so nothing inside the chroot is modified - if there + is no way to do overlay mount it will just do chroot directly - which means + that user has power to render chroot useless - beware... + + The chroot is run in it's own namespace for better containerization. + Therefore the utility 'unshare' is necessary requirement. + + After exiting the chroot all of those necessary steps are undone. + +USAGE: + ${CMD} [-h|--help|help] + This help + + ${CMD} [OPTIONS] execute <chroot-directory> [<command with args>...] + + It will do some necessary steps after which it will execute chroot + command and gives you prompt inside the chroot. When you leave the + prompt it will undo those steps. + On top of the ordinary chroot it will make overlay, so every change + inside the chroot is only temporary and chroot is kept stateless - + like inside a docker container. If there is no way to do overlay - + ordinary chroot is done. + Default command is: /bin/sh -l + + OPTIONS: + + --mount (ro|rw):<src-dir>:<inner-dir> + This option will mount 'src-dir' which is full path on the host + system into the relative path 'inner-dir' within the chroot + directory. + It can be mounted as read-only (ro) or read-write (rw). + Multiple usage of this argument can be used to create complex + hierarchy. Order is significant. + For example: + --mount ro:/scripts/ANSIBLE_DIR:/ansible \ + --mount rw:/scripts/ANSIBLE_DIR/app:/ansible/app + This will mount directory ansible as read-only into chroot, + but it's subdirectory 'app' will be writeable. + + --workdir <inner-dir> + This will set working directory (PWD) inside the chroot. + +EXAMPLE: + ${CMD} --mount ro:/scripts/ansible:ansible \ + --mount rw:/scripts/ansible/app:ansible/app \ + --workdir /ansible execute /tmp/ansible_chroot + # pwd + /ansible + # mount + overlay on / type overlay ... + /dev/disk on /ansible type ext4 (ro,relatime,errors=remount-ro) + /dev/disk on /ansible/application type ext4 (rw,relatime,errors=remount-ro) + none on /proc type proc (rw,relatime) + none on /sys type sysfs (rw,relatime) + none on /dev/shm type tmpfs (rw,relatime) + + Directory /ansible inside the chroot is not writable but subdirectory + /ansible/app is. + + Rest of the chroot is under overlay and all changes will be lost when + chroot command ends. Only changes in app directory persists bacause it + was bind mounted as read-write and is not part of overlay. + + Note: as you can see app directory is mounted over itself but read-write. +" +} + +# arg: <directory> +is_mounted() +{ + mountpoint=$(echo "$1" | sed 's#//*#/#g') + + LANG=C mount | grep -q "^[^[:space:]]\+[[:space:]]\+on[[:space:]]\+${mountpoint}[[:space:]]\+type[[:space:]]\+" +} + +# layers are right to left! First is on the right, top/last is on the left +do_overlay_mount() +{ + if [ -d "$overlay" ] && is_mounted "$overlay" ; then + echo ERROR: "The overlay directory is already mounted: $overlay" >&2 + echo ERROR: "Fix the issue - cannot proceed" >&2 + exit 1 + fi + + # prepare dirs + rm -rf "$overlay" "$upperdir" "$workdir" + mkdir -p "$overlay" + mkdir -p "$upperdir" + mkdir -p "$workdir" + + # finally overlay mount + if ! mount -t overlay --make-rprivate \ + -o lowerdir="$lowerdir",upperdir="$upperdir",workdir="$workdir" \ + overlay "$overlay" ; + then + echo ERROR: "Failed to do overlay mount!" >&2 + echo ERROR: "Please check that your system supports overlay!" >&2 + echo NOTE: "Continuing with the ordinary chroot without overlay!" + + CHROOT_DIR="$lowerdir" + return 1 + fi + + CHROOT_DIR="$overlay" + + return 0 +} + +cleanup() +{ + case "$OVERLAY_MOUNT" in + yes) + echo INFO: "Umounting overlay..." >&2 + if ! umount_retry "$CHROOT_DIR" ; then + echo ERROR: "Cannot umount chroot: $CHROOT_DIR" >&2 + return 1 + fi + + ;; + no) + echo INFO: "No overlay to umount" >&2 + ;; + esac + + if ! is_mounted "$overlay" ; then + echo INFO: "Deleting of temp directories..." >&2 + rm -rf "$overlay" "$upperdir" "$workdir" + else + echo ERROR: "Overlay is still mounted: $CHROOT_DIR" >&2 + echo ERROR: "Cannot delete: $overlay" >&2 + echo ERROR: "Cannot delete: $upperdir" >&2 + echo ERROR: "Cannot delete: $workdir" >&2 + return 1 + fi +} + +check_external_mounts() +{ + echo "$EXTERNAL_MOUNTS" | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do + mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}') + external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}') + internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g') + + case "$mount_type" in + ro|rw) + : + ;; + *) + echo ERROR: "Wrong mount type (should be 'ro' or 'rw') in: ${mountexpr}" >&2 + exit 1 + ;; + esac + + if ! [ -d "$external" ] ; then + echo ERROR: "Directory for mounting does not exist: ${external}" >&2 + exit 1 + fi + + if echo "$internal" | grep -q '^/*$' ; then + echo ERROR: "Unacceptable internal path: ${internal}" >&2 + exit 1 + fi + done +} + +do_external_mounts() +{ + echo INFO: "Bind mounting of external mounts..." >&2 + echo "$EXTERNAL_MOUNTS" | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do + mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}') + external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}') + internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g') + + if is_mounted "${CHROOT_DIR}/${internal}" ; then + echo ERROR: "Mountpoint is already mounted: ${CHROOT_DIR}/${internal}" >&2 + echo ERROR: "Fix the issue - cannot proceed" >&2 + exit 1 + fi + + if ! mkdir -p "${CHROOT_DIR}/${internal}" ; then + echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2 + exit 1 + fi + + if ! mount --make-rprivate -o bind,${mount_type} "$external" "${CHROOT_DIR}/${internal}" ; then + echo ERROR: "Failed to mount: ${external} -> ${internal}" >&2 + exit 1 + else + echo INFO: "Mount: ${external} -> ${internal}" >&2 + fi + done +} + +# arg: <mountpoint> +umount_retry() +{ + mountpoint=$(echo "$1" | sed 's#//*#/#g') + timeout=${UMOUNT_TIMEOUT} + + umount "$mountpoint" 2>/dev/null + while is_mounted "$mountpoint" && [ $timeout -gt 0 ] ; do + umount "$mountpoint" 2>/dev/null + sleep 1 + timeout=$(( timeout - 1 )) + done + + if ! is_mounted "$mountpoint" ; then + return 0 + fi + + return 1 +} + +undo_external_mounts() +{ + echo INFO: "Umount external mount points..." >&2 + echo "$EXTERNAL_MOUNTS" | tac | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do + mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}') + external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}') + internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g') + if umount_retry "${CHROOT_DIR}/${internal}" ; then + echo INFO: "Unmounted: ${CHROOT_DIR}/${internal}" >&2 + else + echo ERROR: "Failed to umount: ${CHROOT_DIR}/${internal}" >&2 + fi + done +} + +install_wrapper() +{ + cat > "$CHROOT_DIR"/usr/local/bin/fakeshell.sh <<EOF +#!/bin/sh + +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +export PATH + +gid_tty=\$(getent group | sed -n '/^tty:/p' | cut -d: -f 3) + +mount -t proc proc /proc +mount -t sysfs none /sys +mount -t tmpfs none /dev + +mkdir -p /dev/shm +mkdir -p /dev/pts +mount -t devpts -o gid=\${gid_tty},mode=620 none /dev/pts + +[ -e /dev/full ] || mknod -m 666 /dev/full c 1 7 +[ -e /dev/ptmx ] || mknod -m 666 /dev/ptmx c 5 2 +[ -e /dev/random ] || mknod -m 644 /dev/random c 1 8 +[ -e /dev/urandom ] || mknod -m 644 /dev/urandom c 1 9 +[ -e /dev/zero ] || mknod -m 666 /dev/zero c 1 5 +[ -e /dev/tty ] || mknod -m 666 /dev/tty c 5 0 +[ -e /dev/console ] || mknod -m 622 /dev/console c 5 1 +[ -e /dev/null ] || mknod -m 666 /dev/null c 1 3 + +chown root:tty /dev/console +chown root:tty /dev/ptmx +chown root:tty /dev/tty + +mkdir -p "\$1" || exit 1 +cd "\$1" || exit 1 +shift + +exec "\$@" + +EOF + chmod +x "$CHROOT_DIR"/usr/local/bin/fakeshell.sh +} + +on_exit() +{ + set +e + echo + + if [ -n "$OVERLAY_MOUNT" ] ; then + undo_external_mounts + fi + cleanup +} + + +# +# parse arguments +# + +state=nil +action=nil +EXTERNAL_MOUNTS='' +CHROOT_WORKDIR='' +CHROOT_METADIR='' +CHROOT_DIR='' +COMMAND='' +while [ -n "$1" ] ; do + case "$state" in + nil) + case "$1" in + ''|-h|--help|help) + help + exit 0 + ;; + --mount) + EXTERNAL_MOUNTS=$(printf "%s\n%s\n" "$EXTERNAL_MOUNTS" "${2}") + state=next + ;; + --workdir) + if [ -z "$CHROOT_WORKDIR" ] ; then + CHROOT_WORKDIR="$2" + state=next + else + echo ERROR: "Multiple working directory argument" >&2 + help >&2 + exit 1 + fi + ;; + execute) + action=execute + state=execute + ;; + *) + echo ERROR: "Bad usage" >&2 + help >&2 + exit 1 + ;; + esac + ;; + next) + state=nil + ;; + execute) + CHROOT_METADIR="$1" + shift + break + ;; + esac + shift +done + + +case "$action" in + ''|nil) + echo ERROR: "Nothing to do - missing command" >&2 + help >&2 + exit 1 + ;; + execute) + # firstly do sanity checking ... + + if [ -z "$CHROOT_METADIR" ] ; then + echo ERROR: "Missing argument" >&2 + help >&2 + exit 1 + fi + + # making sure that CHROOT_METADIR is absolute path + CHROOT_METADIR=$(readlink -f "$CHROOT_METADIR") + + if ! [ -d "$CHROOT_METADIR"/chroot ] ; then + echo ERROR: "Filepath does not exist: ${CHROOT_METADIR}/chroot" >&2 + exit 1 + fi + + # check external mounts if there are any + check_external_mounts + + # check workdir + if [ -n "$CHROOT_WORKDIR" ] ; then + CHROOT_WORKDIR=$(echo "$CHROOT_WORKDIR" | sed -e 's#^/*##' -e 's#//*#/#g') + fi + + # we must be root + if [ "$(id -u)" -ne 0 ] ; then + echo ERROR: "Need to be root and you are not: $(id -nu)" >&2 + exit 1 + fi + + if ! which unshare >/dev/null 2>/dev/null ; then + echo ERROR: "'unshare' system command is missing - ABORT" >&2 + echo INFO: "Try to install 'util-linux' package" >&2 + exit 1 + fi + + # ... sanity checking done + + # setup paths + lowerdir="$CHROOT_METADIR"/chroot + upperdir="$CHROOT_METADIR"/.overlay + workdir="$CHROOT_METADIR"/.workdir + overlay="$CHROOT_METADIR"/.merged + + # set trap + trap on_exit QUIT TERM EXIT + + # mount overlay + OVERLAY_MOUNT='' + if do_overlay_mount ; then + # overlay chroot + OVERLAY_MOUNT=yes + else + # non overlay mount + OVERLAY_MOUNT=no + fi + + # do the user-specific mounts + do_external_mounts + + # I need this wrapper to do some setup inside the chroot... + install_wrapper + + # execute chroot + # copy resolv.conf + cp -a /etc/resolv.conf "$CHROOT_DIR"/etc/resolv.conf + + if [ -n "$1" ] ; then + : + else + set -- /bin/sh -l + fi + unshare -mfpi --propagation private \ + chroot "$CHROOT_DIR" /usr/local/bin/fakeshell.sh "${CHROOT_WORKDIR:-/}" "$@" + ;; +esac + +exit 0 + diff --git a/ansible/library/json_add.py b/ansible/library/json_add.py new file mode 100644 index 00000000..6aad2d7c --- /dev/null +++ b/ansible/library/json_add.py @@ -0,0 +1,90 @@ +#!/usr/bin/python + +from ansible.module_utils.basic import AnsibleModule +import json +import os + +DOCUMENTATION=""" +--- +module: json_add +descritption: + - This module will search top level objects in json and adds specified + value into list for specified key. + - If file does not exists module will create it automatically. + +options: + path: + required: true + aliases=[name, destfile, dest] + description: + - The json file to modify. + key: + required: true + description: + - Top level object. + value: + required: true + description: + - Value to add to specified key. +""" + +def load_json(path): + if os.path.exists(path): + with open(path, 'r') as f: + return json.load(f) + else: + return {} + +def value_is_set(path, key, value, json_obj): + return value in json_obj.get(key, []) + +def insert_to_json(path, key, value, check_mode=False): + json_obj = load_json(path) + if not value_is_set(path, key, value, json_obj): + if not check_mode: + json_obj.setdefault(key, []).append(value) + store_json(path, json_obj) + return True, 'Value %s added to %s.' % (value, key) + else: + return False, '' + +def store_json(path, json_obj): + with open(path, 'w') as f: + json.dump(json_obj, f, indent=4) + +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 += ' ' + changed = True + message += 'File attributes changed.' + + return changed, message + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + path=dict(type='path', required=True, aliases=['name', 'destfile', 'dest']), + key=dict(type='str', required=True), + value=dict(type='str', required=True), + ), + add_file_common_args=True, + supports_check_mode=True + ) + params = module.params + path = params['path'] + key = params['key'] + value = params['value'] + try: + changed, msg = insert_to_json(path, key, value, module.check_mode) + fs_diff = {} + changed, msg = check_file_attrs(module, changed, msg, fs_diff) + module.exit_json(changed=changed, msg=msg, file_attr_diff=fs_diff) + except IOError as e: + module.fail_json(msg=e.msg) + +if __name__ == '__main__': + run_module() + |