Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Evich <cevich@redhat.com>
  • Loading branch information
cevich committed Dec 5, 2017
0 parents commit d035e51
Show file tree
Hide file tree
Showing 12 changed files with 1,068 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

# Ansible bits
*.retry

# Test bits
tests/path/

# Created by travis
ansible.cfg
50 changes: 50 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
language: python
branch:
only:
- master
python:
- 2.7

matrix:
fast_finish: true

git:
submodules: false

env:
global:
- TYPOS="'ecoh' 'roel' 'fixup!' 'squash!' 'FIXME' '<<<<<<<' '=======' '>>>>>>>'"
# fix vim syntax highlighting: "

before_install:
- sudo apt-get update -qq
- pip install ansible==2.3

install:
# Add ansible.cfg to pick up roles path.
- echo -e '[defaults]\nroles_path = ../' > ansible.cfg
# Galaxy would normally install this with a cevich prefix
- export ROLEBASE="$(basename $PWD)" && ln -sfv "$ROLEBASE" "../cevich.$ROLEBASE"

script:
- >
echo "$(git log -1 --format=%H origin/master)" > /tmp/start;
echo "$(git log -1 --format=%H HEAD)" > /tmp/end;
git log -p $(cat /tmp/start)..$(cat /tmp/end) -- . ':!.travis.yml' &> /tmp/commits;
echo "Typos found:";
egrep -a -i -2 "$TYPOS" /tmp/commits | tee /tmp/typos;
test "$(cat /tmp/typos | wc -l)" -eq "0" || exit 1;
- stat /tmp/foobar || true
- ansible-playbook -i tests/inventory tests/test.yml --verbose --syntax-check
- stat /tmp/foobar || true
- ansible-playbook -i tests/inventory tests/test.yml --verbose
- >
ansible-playbook -i tests/inventory tests/again.yml --verbose | tee /tmp/idempotence;
grep -q 'changed=0.*failed=0' /tmp/idempotence \
&& (echo 'Idempotence test: pass' && exit 0) \
|| (echo 'Idempotence test: fail' && exit 1);
- stat /tmp/foobar || true

notifications:
webhooks: https://galaxy.ansible.com/api/v1/notifications/
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
Touchstone
===========

Ansible role to easily make sets of plays, roles or tasks idempotent.
This is critical for some sequence declarations. For example
if one role does partitioning, and another does formatting. Re-applying
that sequence in the future stands a good chance of wrecking your data.

Requirements
------------

Same as stock Ansible 2.3+

Role Variables
--------------

``touch_touchstone``:
When true, mark the end-state or completion identified by ``stone_name``.

``stone_name``:
Optional, identification string to use when multiple end-states must be
tracked. For example multiple playbooks. Defaults to the base, directory
name of the current playbook.

``touchstone_filepath``:
Directory path where the touchstone ``stone_name`` will exist. Must
be a permanent and writable directory for ``ansible_user``, i.e. not
a ``tmpdir`` based ``/tmp``.

``stone_touched``:
A boolean value, set during the role to reflect the current touchstone
state. When ``True``, it indicates the stone was touched at least once
in the past.

Dependencies
------------

A systemd-based machine with a unique /etc/machine-id.

Example Playbook
----------------

::

- hosts: all
roles:
- cevich.parallel_git_repos

License
-------

Easily make sets of plays, roles or tasks idempotent.
Copyright (C) 2017 Christopher C. Evich

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.


Author Information
------------------

Causing trouble and inciting mayhem with Linux since Windows 98


Continuous Integration
-----------------------

Travis CI: |ci_status|

.. |ci_status| image:: https://travis-ci.org/cevich/touchstone.svg?branch=master
:target: https://travis-ci.org/cevich/touchstone
10 changes: 10 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---

# When true, touch touchstone identified by touchstone_name.
touch_touchstone: False

# The name of the touchstone, in case there's more than one.
stone_name: "{{ playbook_dir | basename }}"

# Full/absolute path where touchstone lives, must be permanent and readable/writable.
touchstone_filepath: '{{ ansible_user_dir | default("/var/tmp") }}'
84 changes: 84 additions & 0 deletions files/touchstone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env python3

import errno
import sys
import os
import argparse
import fcntl
from contextlib import contextmanager

@contextmanager
def flock(filepath):
if os.path.basename(filepath).startswith('.'):
pfx_fmt = '{0}.lock'
else:
pfx_fmt = '.{0}.lock'
filepath = os.path.join(os.path.dirname(filepath),
pfx_fmt.format(os.path.basename(filepath)))
with open(filepath, 'a') as lockfile:
try:
fcntl.flock(lockfile, fcntl.LOCK_EX)
yield lockfile
finally:
fcntl.flock(lockfile, fcntl.LOCK_UN)


def touch_touchstone(filepath, lines):
already_touched = False
contents = '\n'.join(lines)
with flock(filepath):
try:
with open(filepath, 'rU') as touchstone:
contents = touchstone.read()
already_touched = True
except IOError as xcept:
if xcept.errno != errno.ENOENT:
raise
with open(filepath, 'w') as touchstone:
touchstone.write(contents)
if not contents.endswith('\n'):
touchstone.write('\n')
return already_touched


def is_touched(filepath):
with flock(filepath):
try:
with open(filepath, 'rU') as touchstone:
return (True, touchstone.read())
except IOError:
return (False, None)


def parse_arguments(argv):
adhf = argparse.ArgumentDefaultsHelpFormatter
parser = argparse.ArgumentParser(formatter_class=adhf,
epilog='Prints "True"/"False" to stdout,'
' indicating touchstone status')
parser.add_argument("filepath", default=None,
help="Path to the touchstone file to examine or update.")
parser.add_argument('-t', '--touch', default=False, action='store_true',
help="Ensure the touchstone <filepath> exists, on creation"
" populate it with all [line]")
parser.add_argument('line', default=None, nargs="*",
help="Lines to store in <filepath> on creation.")
try:
return parser.parse_args(argv[1:])
except IndexError:
return parser.parse_args(argv)


def main(argv):
args = parse_arguments(argv)
already_touched = False
if args.touch:
already_touched = touch_touchstone(args.filepath, args.line)
state, contents = is_touched(args.filepath)
sys.stdout.write('{0}\n'.format(state))
# Do not print contents upon first touch as "changed" signal to ansible
if state and already_touched:
sys.stderr.write('{0}'.format(contents))


if __name__ == "__main__":
main(sys.argv)
22 changes: 22 additions & 0 deletions meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
galaxy_info:
author: Chris Evich
description: Easily make sets of plays, roles or tasks idempotent.
company: Red Hat
license: GPLv3
min_ansible_version: 2.3
platforms:
- name: all
versions:
- all
galaxy_tags:
- sequence
- ending
- unidirectional
- complete
- stop
- play
- task
- role
- idempotent

dependencies: []
59 changes: 59 additions & 0 deletions tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---

- name: stone_touched fact is initially false
set_fact:
stone_touched: False

- name: Input expectations are verified
assert:
that:
- 'hostvars.localhost.ansible_machine_id | default("", True) | trim | length'
- 'stone_touched == False' # make sure it's not read-only
- 'touchstone_filepath | default("", True) | trim | length'
- 'touch_touchstone | default(False) in [True, False]'
- '"{{ role_path }}/files/touchstone.py" | is_file'

- name: Touchstone script command-line arguments are initialized
set_fact:
result: 'touchstone.py'

- name: Touchstone script command-line has --touch appended
set_fact:
result: '{{ result }} --touch'
when: touch_touchstone | default(False)

- name: Touchstone script command-line has complete touchstone_filepath appended
set_fact:
result: '{{ result }} {{ touchstone_filepath }}/.{{ stone_name }}.touchstone'

- name: Touchstone script command-line has content lines appended
set_fact:
result: '{{ result }} {{ item | quote }}'
when: touch_touchstone | default(False)
with_items:
- 'Touched by {{ hostvars.localhost.ansible_nodename }}'
- 'Machine_id {{ hostvars.localhost.ansible_machine_id }}'
- 'On {{ hostvars.localhost.ansible_date_time.iso8601 }}'

- name: Touchstone script is executed to modify and/or retrieve stone status
script: '{{ result }}'
changed_when: touch_touchstone and
result.stdout | trim | lower == "true" and
result.stderr | trim == ''
register: result

- name: Script output expectations are verified
assert:
that: 'result.stdout | trim | lower in ["true","false"]'

- name: Touchstone result is converted into boolean
set_fact:
stone_touched: '{{ result.stdout | trim | lower == "true" }}'

- name: Touchstone variables are debugged
debug:
var: '{{ item }}'
with_items:
- stone_name
- stone_touched
- result.stderr
37 changes: 37 additions & 0 deletions tests/again.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---

- hosts: all
gather_subset: network
vars:
stone_name: foobar
touchstone_filepath: /tmp
pre_tasks:
- assert: that='stone_touched is defined'
roles:
- cevich.touchstone
post_tasks:
- assert: that='stone_touched == True'

- hosts: all
vars:
stone_name: foobar
touchstone_filepath: /tmp
pre_tasks:
- assert: that='stone_touched == True'
roles:
- role: cevich.touchstone
touch_touchstone: True
post_tasks:
- assert: that='stone_touched == True'

- hosts: all
vars:
stone_name: foobar
touchstone_filepath: /tmp
pre_tasks:
- assert: that='stone_touched == True'
roles:
- role: cevich.touchstone
touch_touchstone: True
post_tasks:
- assert: that='stone_touched == True'
1 change: 1 addition & 0 deletions tests/inventory
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
localhost ansible_connection=local
37 changes: 37 additions & 0 deletions tests/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---

- hosts: all
gather_subset: network
vars:
stone_name: foobar
touchstone_filepath: /tmp
pre_tasks:
- assert: that='stone_touched is defined'
roles:
- cevich.touchstone
post_tasks:
- assert: that='stone_touched == False'

- hosts: all
vars:
stone_name: foobar
touchstone_filepath: /tmp
pre_tasks:
- assert: that='stone_touched == False'
roles:
- role: cevich.touchstone
touch_touchstone: True
post_tasks:
- assert: that='stone_touched == True'

- hosts: all
vars:
stone_name: foobar
touchstone_filepath: /tmp
pre_tasks:
- assert: that='stone_touched == True'
roles:
- role: cevich.touchstone
touch_touchstone: True
post_tasks:
- assert: that='stone_touched == True'
Loading

0 comments on commit d035e51

Please sign in to comment.