├── .github └── workflows │ └── test_via_docker.yml ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── ansible-wrapper.gemspec ├── bin ├── console ├── rake └── setup ├── examples └── streaming │ ├── Gemfile │ ├── Gemfile.lock │ ├── config.ru │ └── run.rb ├── lib ├── ansible-wrapper.rb └── ansible │ ├── ad_hoc.rb │ ├── config.rb │ ├── output.rb │ ├── playbook.rb │ ├── safe_pty.rb │ ├── shortcuts.rb │ └── version.rb └── spec ├── ansible ├── ad_hoc_spec.rb ├── ansible_spec.rb ├── output_spec.rb └── playbook_spec.rb ├── fixtures ├── fail_playbook.yml ├── failure_then_ignored_error_playbook.yml ├── ignored_error_playbook.yml ├── ignored_error_then_failure_playbook.yml ├── ignored_errors_playbook.yml └── mock_playbook.yml └── spec_helper.rb /.github/workflows/test_via_docker.yml: -------------------------------------------------------------------------------- 1 | name: Test using different Ansible & Ruby versions 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ansible: 11 | - 2.9.6 12 | - 2.8.10 13 | - 2.7.16 14 | - 2.6.20 15 | - 2.5.15 16 | - 2.4.6 17 | - 2.3.3 18 | - 2.2.3 19 | - 2.1.6 20 | - 2.0.2 21 | steps: 22 | - uses: actions/checkout@v1 23 | - name: Test against Ruby 2.7.0 24 | env: 25 | RUBY_VERSION: 2.7.0 26 | ANSIBLE_VERSION: ${{ matrix.ansible }} 27 | run: > 28 | docker run --rm -v $PWD:/app pgeraghty/ansible-ruby:$RUBY_VERSION-$ANSIBLE_VERSION 29 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && 30 | COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_REPO_TOKEN }} COVERALLS_RUN_LOCALLY=true bundle exec rake" 31 | 32 | - name: Test against Ruby 2.6.5 33 | env: 34 | RUBY_VERSION: 2.6.5 35 | ANSIBLE_VERSION: ${{ matrix.ansible }} 36 | run: > 37 | docker run --rm -v $PWD:/app pgeraghty/ansible-ruby:$RUBY_VERSION-$ANSIBLE_VERSION 38 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && bundle exec rake" 39 | 40 | - name: Test against Ruby 2.5.7 41 | env: 42 | RUBY_VERSION: 2.5.7 43 | ANSIBLE_VERSION: ${{ matrix.ansible }} 44 | run: > 45 | docker run --rm -v $PWD:/app pgeraghty/ansible-ruby:$RUBY_VERSION-$ANSIBLE_VERSION 46 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && bundle exec rake" 47 | 48 | - name: Test against Ruby 2.4.9 49 | env: 50 | RUBY_VERSION: 2.4.9 51 | ANSIBLE_VERSION: ${{ matrix.ansible }} 52 | run: > 53 | docker run --rm -v $PWD:/app pgeraghty/ansible-ruby:$RUBY_VERSION-$ANSIBLE_VERSION 54 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && bundle exec rake" 55 | 56 | - name: Test against Ruby 2.3.7 57 | env: 58 | RUBY_VERSION: 2.3.7 59 | ANSIBLE_VERSION: ${{ matrix.ansible }} 60 | run: > 61 | docker run --rm -v $PWD:/app pgeraghty/ansible-ruby:$RUBY_VERSION-$ANSIBLE_VERSION 62 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && bundle exec rake" 63 | 64 | - name: Test against Ruby 2.2.7 65 | env: 66 | RUBY_VERSION: 2.2.7 67 | ANSIBLE_VERSION: ${{ matrix.ansible }} 68 | run: > 69 | docker run --rm -v $PWD:/app pgeraghty/ansible-ruby:$RUBY_VERSION-$ANSIBLE_VERSION 70 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && bundle exec rake" 71 | 72 | - name: Test against Ruby 2.1.10 73 | env: 74 | RUBY_VERSION: 2.1.10 75 | ANSIBLE_VERSION: ${{ matrix.ansible }} 76 | run: > 77 | docker run --rm -v $PWD:/app pgeraghty/ansible-ruby:$RUBY_VERSION-$ANSIBLE_VERSION 78 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && bundle exec rake" 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.idea/ 3 | /.yardoc 4 | /Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | .ruby-gemset 12 | .ruby-version 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: bash 2 | services: 3 | - docker 4 | env: 5 | global: 6 | - CC_TEST_REPORTER_ID=2c0e6ce9e61357e6f15cf76a915b3868235d93e0b849218220242e456bfaa2aa 7 | matrix: 8 | - ANSIBLE_VERSION=2.8.10 9 | - ANSIBLE_VERSION=2.7.16 10 | - ANSIBLE_VERSION=2.6.20 11 | - ANSIBLE_VERSION=2.5.15 12 | - ANSIBLE_VERSION=2.4.6 13 | - ANSIBLE_VERSION=2.3.3 14 | - ANSIBLE_VERSION=2.2.3 15 | - ANSIBLE_VERSION=2.1.6 16 | - ANSIBLE_VERSION=2.0.2 17 | before_script: 18 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 19 | - chmod +x ./cc-test-reporter 20 | script: 21 | - > 22 | RUBY=2.7.0 && docker run --rm -e TRAVIS -e TRAVIS_JOB_ID -e TRAVIS_BRANCH -e TRAVIS_PULL_REQUEST 23 | -e CC_TEST_REPORTER_ID -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION /bin/sh -c 24 | "cd /app && bundle install --jobs=3 --retry=3 && 25 | ./cc-test-reporter before-build && 26 | CI=true bundle exec rake && rm Gemfile.lock" 27 | - > 28 | RUBY=2.6.5 && docker run --rm -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION 29 | /bin/sh -c "cp -r /app /tmp/app && cd /tmp/app && bundle install --jobs=3 --retry=3 && bundle exec rake" 30 | - > 31 | RUBY=2.5.7 && docker run --rm -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION 32 | /bin/sh -c "cd /app && bundle install --jobs=3 --retry=3 && bundle exec rake" 33 | - > 34 | RUBY=2.4.9 && docker run --rm -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION 35 | /bin/sh -c "cd /app && bundle install --jobs=3 --retry=3 && bundle exec rake" 36 | - > 37 | RUBY=2.3.7 && docker run --rm -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION 38 | /bin/sh -c "cd /app && bundle install --jobs=3 --retry=3 && bundle exec rake" 39 | - > 40 | RUBY=2.2.7 && docker run --rm -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION 41 | /bin/sh -c "cd /app && bundle install --jobs=3 --retry=3 && bundle exec rake" 42 | - > 43 | RUBY=2.1.10 && docker run --rm -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION 44 | /bin/sh -c "cd /app && bundle install --jobs=3 --retry=3 && bundle exec rake" 45 | after_script: 46 | - RUBY=2.6.4 && docker run --rm -e TRAVIS -e TRAVIS_JOB_ID -e TRAVIS_BRANCH -e TRAVIS_PULL_REQUEST 47 | -e TRAVIS_TEST_RESULT -e CC_TEST_REPORTER_ID -it -v $PWD:/app pgeraghty/ansible-ruby:$RUBY-$ANSIBLE_VERSION 48 | /bin/sh -c "cd /app && ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.0 4 | 5 | ### Breaking changes 6 | `Playbook.stream` has been refactored to include a new `raise_on_failure` parameter. 7 | This is part of a change in behaviour for this method to no longer raise exceptions by default 8 | and stream the entire Playbook execution. 9 | 10 | To previous behaviour can be obtained by passing `raise_on_failure: :during` 11 | to the `stream`method i.e. 12 | 13 | ```ruby 14 | Playbook.stream "some_playbook.yml", raise_on_failure: :during 15 | ``` 16 | 17 | Alternatively, if you'd still like to raise exception for failures, but only after output has finished streaming, you can use: 18 | ```ruby 19 | Playbook.stream "some_playbook.yml", raise_on_failure: :after 20 | ``` 21 | 22 | In addition to the breaking changes above,`Playbook.stream` now handles tasks that 23 | are skipped according to Ansible's output. 24 | This should include tasks which have `ignore_errors` set. -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ansible-wrapper.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Paul Geraghty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Wrapper 2 | 3 | [![Gem Version](https://badge.fury.io/rb/ansible-wrapper.svg)](http://badge.fury.io/rb/ansible-wrapper) 4 | [![Build Status](https://travis-ci.com/pgeraghty/ansible-wrapper-ruby.svg?branch=master)](https://travis-ci.com/pgeraghty/ansible-wrapper-ruby) 5 | [![Coverage Status](https://coveralls.io/repos/github/pgeraghty/ansible-wrapper-ruby/badge.svg?branch=master)](https://coveralls.io/github/pgeraghty/ansible-wrapper-ruby?branch=master) 6 | [![Code Climate](https://codeclimate.com/github/pgeraghty/ansible-wrapper-ruby/badges/gpa.svg)](https://codeclimate.com/github/pgeraghty/ansible-wrapper-ruby) 7 | [![Documentation](http://inch-ci.org/github/pgeraghty/ansible-wrapper-ruby.svg?branch=master)](http://inch-ci.org/github/pgeraghty/ansible-wrapper-ruby) 8 | 9 | #### A lightweight Ruby wrapper around Ansible that allows for ad-hoc commands and playbook execution. The primary purpose is to support easy streaming output. 10 | 11 | ## Requirements 12 | 13 | Ensure [Ansible](http://docs.ansible.com/intro_getting_started.html) is installed and available to shell commands i.e. in PATH. 14 | [Tested](https://travis-ci.org/pgeraghty/ansible-wrapper-ruby) with Ansible versions 2.0.2 thru 2.9.6 and Ruby 2.1+, but please create an issue if you use a version that fails. 15 | 16 | ## Installation 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | ```ruby 21 | gem 'ansible-wrapper' 22 | ``` 23 | 24 | And then execute: 25 | 26 | $ bundle 27 | 28 | Or install it yourself as: 29 | 30 | $ gem install ansible-wrapper 31 | 32 | ## Usage 33 | 34 | ### Ad-hoc commands 35 | 36 | ```ruby 37 | Ansible::AdHoc.run 'all -i localhost, --list-hosts' 38 | ``` 39 | 40 | ```ruby 41 | Ansible::AdHoc.run 'all -m shell -a "echo Test" -i localhost,' 42 | ``` 43 | 44 | ### Playbooks 45 | 46 | ```ruby 47 | Ansible::Playbook.run '-i localhost, spec/fixtures/mock_playbook.yml' 48 | ``` 49 | 50 | ```ruby 51 | Ansible::Playbook.stream('-i localhost, spec/fixtures/mock_playbook.yml') # defaults to standard output 52 | ``` 53 | 54 | ```ruby 55 | Ansible::Playbook.stream('-i localhost, spec/fixtures/mock_playbook.yml') { |line_of_output| puts line_of_output } 56 | ``` 57 | 58 | ### Shortcuts 59 | 60 | To enable shortcuts: 61 | 62 | ```ruby 63 | Ansible.enable_shortcuts! 64 | ``` 65 | 66 | You can then access Ansible via the `A` alias and use the following syntax: 67 | 68 | ```ruby 69 | A['all -i localhost, --list-hosts'] # alias for Ansible::AdHoc.run 70 | ``` 71 | 72 | ```ruby 73 | A << '-i localhost, spec/fixtures/mock_playbook.yml' # alias for Ansible::Playbook.stream 74 | ``` 75 | 76 | ## Examples 77 | 78 | * For a streaming output example using Sinatra, see the [examples/streaming](examples/streaming) folder. 79 | 80 | ## Development 81 | 82 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 83 | 84 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 85 | 86 | ## Contributing 87 | 88 | Bug reports and pull requests are welcome on GitHub at https://github.com/pgeraghty/ansible-wrapper-ruby. 89 | 90 | 91 | ## License 92 | 93 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 94 | 95 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /ansible-wrapper.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'ansible/version' 5 | 6 | # TODO set required ruby version?? 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = 'ansible-wrapper' 10 | spec.version = Ansible::VERSION 11 | spec.authors = ['Paul Geraghty'] 12 | spec.email = ['muse@appsthatcould.be'] 13 | 14 | spec.summary = %q{Ruby wrapper around Ansible} 15 | spec.description = %q{A lightweight Ruby wrapper around Ansible that allows for ad-hoc commands and playbook execution.} 16 | spec.homepage = 'https://github.com/pgeraghty/ansible-wrapper-ruby' 17 | spec.license = 'MIT' 18 | 19 | # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or 20 | # delete this section to allow pushing this gem to any host. 21 | # if spec.respond_to?(:metadata) 22 | # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" 23 | # else 24 | # raise "RubyGems 2.0 or newer is required to protect against public gem pushes." 25 | # end 26 | 27 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 28 | spec.bindir = 'exe' 29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 30 | spec.require_paths = ['lib'] 31 | 32 | spec.add_dependency 'json' 33 | 34 | spec.add_development_dependency 'bundler' 35 | spec.add_development_dependency 'rake', '~> 10.0' 36 | spec.add_development_dependency 'rspec' 37 | spec.add_development_dependency 'coveralls' 38 | spec.add_development_dependency 'json', '~> 1.8.4' 39 | end 40 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'ansible-wrapper' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "pathname" 12 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 13 | Pathname.new(__FILE__).realpath) 14 | 15 | bundle_binstub = File.expand_path("../bundle", __FILE__) 16 | 17 | if File.file?(bundle_binstub) 18 | if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ 19 | load(bundle_binstub) 20 | else 21 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 22 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 23 | end 24 | end 25 | 26 | require "rubygems" 27 | require "bundler/setup" 28 | 29 | load Gem.bin_path("rake", "rake") 30 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /examples/streaming/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'sinatra' 4 | gem 'thin' 5 | 6 | gem 'ansible-wrapper', path: '../../' #github: 'pgeraghty/ansible-wrapper-ruby' -------------------------------------------------------------------------------- /examples/streaming/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../.. 3 | specs: 4 | ansible-wrapper (0.2.0) 5 | json 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | daemons (1.3.1) 11 | eventmachine (1.2.7) 12 | json (2.3.0) 13 | mustermann (1.0.3) 14 | rack (2.0.8) 15 | rack-protection (2.0.7) 16 | rack 17 | sinatra (2.0.7) 18 | mustermann (~> 1.0) 19 | rack (~> 2.0) 20 | rack-protection (= 2.0.7) 21 | tilt (~> 2.0) 22 | thin (1.7.2) 23 | daemons (~> 1.0, >= 1.0.9) 24 | eventmachine (~> 1.0, >= 1.0.4) 25 | rack (>= 1, < 3) 26 | tilt (2.0.9) 27 | 28 | PLATFORMS 29 | ruby 30 | 31 | DEPENDENCIES 32 | ansible-wrapper! 33 | sinatra 34 | thin 35 | 36 | BUNDLED WITH 37 | 1.17.3 38 | -------------------------------------------------------------------------------- /examples/streaming/config.ru: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require './run' 3 | 4 | run Sinatra::Application -------------------------------------------------------------------------------- /examples/streaming/run.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | require 'ansible-wrapper' 4 | 5 | set server: 'thin' 6 | set :environment, :production 7 | set :bind, '0.0.0.0' 8 | set :port, 4567 9 | 10 | Process.setproctitle 'Ansible Streaming Example' 11 | 12 | 13 | CONSOLE_OUTPUT_START = %{ 14 | 15 | 16 | 17 | 52 | 53 |
}
54 | 
55 | CONSOLE_OUTPUT_END = '
' 56 | 57 | 58 | get '/' do 59 | "Ansible #{Ansible::Config::VERSION}" 60 | end 61 | 62 | get '/streaming' do 63 | #content_type 'text/plain' 64 | stream do |out| 65 | out << CONSOLE_OUTPUT_START 66 | Ansible.stream ['-i', 'localhost,', File.expand_path('../../../spec/fixtures/mock_playbook.yml', __FILE__)]*' ' do |line| 67 | Ansible::Output.to_html line, out 68 | end 69 | out << CONSOLE_OUTPUT_END 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/ansible-wrapper.rb: -------------------------------------------------------------------------------- 1 | require 'ansible/version' 2 | require 'ansible/ad_hoc' 3 | require 'ansible/playbook' 4 | require 'ansible/output' 5 | 6 | # A lightweight Ruby wrapper around Ansible that allows for ad-hoc commands and playbook execution. 7 | # The primary purpose is to support easy streaming output. 8 | module Ansible 9 | include Ansible::Config 10 | include Ansible::Methods 11 | include Ansible::PlaybookMethods 12 | 13 | extend self 14 | 15 | # Enables shortcuts 16 | # @see ansible/shortcuts.rb 17 | def enable_shortcuts! 18 | require 'ansible/shortcuts' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/ansible/ad_hoc.rb: -------------------------------------------------------------------------------- 1 | require 'ansible/config' 2 | require 'json' 3 | 4 | module Ansible 5 | # Ansible Ad-Hoc methods 6 | module Methods 7 | # executable that runs Ansible Ad-Hoc commands 8 | BIN = 'ansible' 9 | 10 | # Run an Ad-Hoc Ansible command 11 | # @param cmd [String] the Ansible command to execute 12 | # @return [String] the output 13 | # @example Run a simple shell command with an inline inventory that only contains localhost 14 | # one_off 'all -c local -a "echo hello"' 15 | def one_off(cmd) 16 | # TODO if debug then puts w/ colour 17 | `#{config.to_s "#{BIN} #{cmd}"}` 18 | end 19 | alias :[] :one_off 20 | 21 | # Ask Ansible to list hosts 22 | # @param cmd [String] the Ansible command to execute 23 | # @return [String] the output 24 | # @example List hosts with an inline inventory that only contains localhost 25 | # list_hosts 'all -i localhost,' 26 | def list_hosts(cmd) 27 | output = one_off("#{cmd} --list-hosts").gsub!(/\s+hosts.*:\n/, '').strip 28 | output.split("\n").map(&:strip) 29 | end 30 | 31 | # Fetches host variables via Ansible's debug module 32 | # @param host [String] the ++ for target host(s) 33 | # @param inv [String] the inventory host path or comma-separated host list 34 | # @param filter [String] the variable filter 35 | # @return [Hash] the variables pertaining to the host 36 | # @example List variables for localhost 37 | # parse_host_vars 'localhost', 'localhost,' 38 | def parse_host_vars(host, inv, filter = 'hostvars[inventory_hostname]') 39 | cmd = "all -m debug -a 'var=#{filter}' -i #{inv} -l #{host}" 40 | json = self[cmd].split(/>>|=>/).last 41 | 42 | # remove any colour added to console output 43 | # TODO move to Output module as #bleach, perhaps use term-ansicolor 44 | # possibly replace regexp with /\e\[(?:(?:[349]|10)[0-7]|[0-9]|[34]8;5;\d{1,3})?m/ 45 | # possibly use ANSIBLE_NOCOLOR? or --nocolor 46 | json = json.strip.chomp.gsub(/\e\[[0-1][;]?(3[0-7]|90|1)?m/, '') 47 | 48 | hostvars = JSON.parse(json) 49 | 50 | hostvars[filter] 51 | end 52 | end 53 | 54 | # Provides static access to Ad-Hoc methods 55 | module AdHoc 56 | include Config 57 | include Methods 58 | 59 | extend self 60 | 61 | # Run an Ad-Hoc Ansible command 62 | # @see Methods#one_off 63 | # @param cmd [String] the Ansible command to execute 64 | # @return [String] the output 65 | # @since 0.2.1 66 | alias :run :one_off 67 | end 68 | end -------------------------------------------------------------------------------- /lib/ansible/config.rb: -------------------------------------------------------------------------------- 1 | module Ansible 2 | # Ansible configuration 3 | module Config 4 | PATH = 'lib/ansible/' 5 | # IP_OR_HOSTNAME = /((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})$|^((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))\n/ 6 | SKIP_HOSTVARS = %w(ansible_version inventory_dir inventory_file inventory_hostname inventory_hostname_short group_names groups omit playbook_dir) 7 | VERSION = `ansible --version`.split("\n").first.split.last rescue nil # nil when Ansible not installed 8 | 9 | # Default configuration options 10 | DefaultConfig = Struct.new(:env, :extra_vars, :params) do 11 | # @!attribute env 12 | # @return [Hash] environment variables 13 | # @!attribute params 14 | # @return [Hash] parameters 15 | # @!attribute extra_vars 16 | # @return [Hash] extra variables to pass to Ansible 17 | 18 | def initialize 19 | self.env = { 20 | 'ANSIBLE_FORCE_COLOR' => 'True', 21 | 'ANSIBLE_HOST_KEY_CHECKING' => 'False' 22 | } 23 | 24 | self.extra_vars = { 25 | # skip creation of .retry files 26 | 'retry_files_enabled' => 'False' 27 | } 28 | # TODO support --ssh-common-args, --ssh-extra-args 29 | # e.g. ansible-playbook --ssh-common-args="-o ServerAliveInterval=60" -i inventory install.yml 30 | 31 | self.params = { 32 | debug: false 33 | } 34 | end 35 | 36 | # Pass additional options to Ansible 37 | # NB: --extra-vars can also accept JSON string, see http://stackoverflow.com/questions/25617273/pass-array-in-extra-vars-ansible 38 | # @return [String] command-line options 39 | def options 40 | x = extra_vars.each_with_object('--extra-vars=\'') { |kv, a| a << "#{kv.first}=\"#{kv.last}\" " }.strip+'\'' if extra_vars unless extra_vars.empty? 41 | # can test with configure { |config| config.extra_vars.clear } 42 | 43 | [x, '--ssh-extra-args=\'-o UserKnownHostsFile=/dev/null\'']*' ' 44 | end 45 | 46 | # Output configuration as a string for the command-line 47 | # @param cmd [String] command to be appended to the command-line produced 48 | # @return [Config, DefaultConfig] the configuration 49 | def to_s(cmd) 50 | entire_cmd = [env.each_with_object([]) { |kv, a| a << kv*'=' } * ' ', cmd, options]*' ' 51 | puts entire_cmd if params[:debug] 52 | entire_cmd 53 | end 54 | end 55 | 56 | # Create and yield configuration 57 | # @return [Config, DefaultConfig] the configuration 58 | def configure 59 | @config ||= DefaultConfig.new 60 | yield(@config) if block_given? 61 | 62 | # allow chaining if block given 63 | block_given? ? self : @config 64 | end 65 | 66 | # accessor for config 67 | # @return [DefaultConfig] the configuration 68 | def config 69 | @config || configure 70 | end 71 | end 72 | end -------------------------------------------------------------------------------- /lib/ansible/output.rb: -------------------------------------------------------------------------------- 1 | require 'strscan' 2 | require 'erb' 3 | 4 | module Ansible 5 | # Output module provides formatting of Ansible output 6 | module Output 7 | # Generate HTML for an output string formatted with ANSI escape sequences representing colours and styling 8 | # @param ansi [String] an output string formatted with escape sequences to represent formatting 9 | # @param stream [String] a stream or string (that supports +<<+) to which generated HTML will be appended 10 | # @return the stream provided or a new String 11 | # @example List hosts with an inline inventory that only contains localhost 12 | # to_html "\e[90mGrey\e[0m" => 'Grey' 13 | def self.to_html(ansi, stream='') 14 | Ansi2Html.new(ansi).to_html stream 15 | end 16 | 17 | # Converter for strings containing with ANSI escape sequences 18 | class Ansi2Html 19 | # Hash of colors to convert shell colours to CSS 20 | COLOR = { 21 | '1' => 'font-weight: bold', 22 | '30' => 'color: black', 23 | '31' => 'color: red', 24 | '32' => 'color: green', 25 | '33' => 'color: yellow', 26 | '34' => 'color: blue', 27 | '35' => 'color: magenta', 28 | '36' => 'color: cyan', 29 | '37' => 'color: white', 30 | '90' => 'color: grey' 31 | } 32 | 33 | SUPPORTED_STYLE_PATTERN = /\e\[([0-1])?[;]?(3[0-7]|90|1)m/ 34 | END_ESCAPE_SEQUENCE_PATTERN = /\e\[0m/ 35 | UNSUPPORTED_STYLE_PATTERN = /\e\[[^0]*m/ 36 | IGNORED_OUTPUT = /./m 37 | 38 | OPEN_SPAN_TAG = %{} 39 | CLOSE_SPAN_TAG = %{} 40 | 41 | # Create StringScanner for string 42 | # @param line [String] a stream or string (that supports +<<+) to which generated HTML will be appended 43 | def initialize(line) 44 | # ensure any HTML tag characters are escaped 45 | @strscan = StringScanner.new(ERB::Util.h line) 46 | end 47 | 48 | # Generate HTML from string formatted with ANSI escape sequences 49 | # @return [String, IO] the HTML 50 | def to_html(stream) 51 | until @strscan.eos? 52 | stream << generate_html 53 | end 54 | 55 | stream 56 | end 57 | 58 | 59 | private 60 | 61 | # Scan string and generate HTML 62 | def generate_html 63 | if @strscan.scan SUPPORTED_STYLE_PATTERN 64 | open_tag 65 | elsif @strscan.scan END_ESCAPE_SEQUENCE_PATTERN 66 | CLOSE_SPAN_TAG 67 | elsif @strscan.scan UNSUPPORTED_STYLE_PATTERN 68 | OPEN_SPAN_TAG 69 | else 70 | @strscan.scan IGNORED_OUTPUT 71 | end 72 | end 73 | 74 | # Generate opening HTML tag, which may contain a style attribute 75 | # @return [String] opening tag 76 | def open_tag 77 | bold, colour = @strscan[1], @strscan[2] 78 | styles = [] 79 | 80 | styles << COLOR[bold] if bold.to_i == 1 81 | styles << COLOR[colour] 82 | 83 | # in case of invalid colours, although this may be impossible 84 | if styles.compact.empty? 85 | OPEN_SPAN_TAG 86 | else 87 | %{} 88 | end 89 | end 90 | end 91 | end 92 | end -------------------------------------------------------------------------------- /lib/ansible/playbook.rb: -------------------------------------------------------------------------------- 1 | require 'ansible/config' 2 | require 'ansible/safe_pty' 3 | 4 | module Ansible 5 | # Ansible Playbook methods 6 | module PlaybookMethods 7 | # executable that runs Ansible Playbooks 8 | BIN = 'ansible-playbook' 9 | 10 | # Run playbook, returning output 11 | # @param pb [String] path to playbook 12 | # @return [String] output 13 | def playbook(pb) 14 | # TODO if debug then puts w/ colour 15 | `#{config.to_s "#{BIN} #{pb}"}` 16 | end 17 | alias :<< :playbook 18 | 19 | # Stream execution of a playbook using PTY because otherwise output is buffered 20 | # @param pb [String] path to playbook 21 | # @param raise_on_failure [Symbol] Specifies if streaming should raise an exception when a Playbook failure occurs. 22 | # Defaults to +:false+, can also be +:during+ to raise as soon as an error occurs or +:after+ to allow all output to stream first. 23 | # @raise [Playbook::Exception] if +raise_on_failure+ is truthy 24 | # @return [Integer] exit status 25 | def stream(pb, raise_on_failure: false) 26 | cmd = config.to_s("#{BIN} #{pb}") 27 | error_at_line = {} 28 | 29 | pid = SafePty.spawn cmd do |r,_,_| # add -vvvv here for verbose 30 | line_num = 0 31 | 32 | until r.eof? do 33 | line = r.gets 34 | line_num += 1 35 | 36 | block_given? ? yield(line) : puts(line) 37 | 38 | # track errors in output by line 39 | if raise_on_failure 40 | case line 41 | when /(fatal|failed): \[/ then error_at_line[line_num] ||= "FAILED: #{line}" 42 | when /ERROR!/, /FAILED!/ then error_at_line[line_num] ||= "ERROR: #{line}" 43 | # allow errors on previous line to be ignored 44 | when /...ignoring/ then error_at_line.delete(line_num-1) 45 | end 46 | 47 | if raise_on_failure == :during 48 | # trigger failure unless it was ignored 49 | fatal_unskipped_error = error_at_line[line_num-1] 50 | raise Playbook::Exception.new(fatal_unskipped_error) if fatal_unskipped_error 51 | end 52 | end 53 | end 54 | end 55 | 56 | if raise_on_failure 57 | # at this point, all output has been streamed 58 | fatal_unskipped_error = error_at_line.first 59 | raise Playbook::Exception.new(fatal_unskipped_error.last) if fatal_unskipped_error 60 | end 61 | 62 | pid 63 | end 64 | end 65 | 66 | # Provides static access to Playbook methods 67 | module Playbook 68 | include Config 69 | include PlaybookMethods 70 | 71 | extend self 72 | 73 | # Run playbook, returning output 74 | # @param pb [String] path to playbook 75 | # @return [String] output 76 | alias :run :playbook 77 | 78 | # Exception to represent Playbook failures 79 | class Exception < RuntimeError; end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/ansible/safe_pty.rb: -------------------------------------------------------------------------------- 1 | require 'pty' 2 | 3 | # Wrapper for PTY pseudo-terminal 4 | module Ansible::SafePty 5 | # Spawns process for command 6 | # @param command [String] command 7 | # @return [Integer] exit status 8 | def self.spawn(command) 9 | 10 | PTY.spawn(command) do |r,w,p| 11 | begin 12 | yield r,w,p if block_given? 13 | rescue Errno::EIO 14 | nil # ignore Errno::EIO: Input/output error @ io_fillbuf 15 | ensure 16 | Process.wait p 17 | end 18 | end 19 | 20 | $?.exitstatus 21 | end 22 | end -------------------------------------------------------------------------------- /lib/ansible/shortcuts.rb: -------------------------------------------------------------------------------- 1 | module Ansible 2 | extend self 3 | 4 | # shortcut for executing an Ad-Hoc command 5 | # @param cmd [String] the command-line to pass 6 | # @see AdHoc#run 7 | def [](cmd) 8 | AdHoc.run cmd 9 | end 10 | 11 | # shortcut to run a Playbook, streaming the output 12 | # @param cmd [String] the command-line to pass 13 | # @see Playbook#stream 14 | def <<(cmd) 15 | Playbook.stream cmd 16 | end 17 | end 18 | A = Ansible unless defined?(A) -------------------------------------------------------------------------------- /lib/ansible/version.rb: -------------------------------------------------------------------------------- 1 | module Ansible 2 | VERSION = '0.3.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/ansible/ad_hoc_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Ansible 4 | describe AdHoc do 5 | before(:all) do 6 | disable_host_key_checking 7 | end 8 | 9 | describe '.run' do 10 | it 'can execute a basic ad-hoc Ansible command on localhost' do 11 | expect(AdHoc.run 'all -i localhost, --list-hosts').to match /localhost/ 12 | end 13 | end 14 | 15 | describe '.list_hosts' do 16 | it 'can list hosts for an inline inventory' do 17 | inline_inv = %w(localhost 99.99.99.99) 18 | cmd = "all -i #{inline_inv*','}, --list-hosts" 19 | 20 | expect(AdHoc.list_hosts cmd).to match inline_inv 21 | end 22 | end 23 | 24 | describe '.parse_host_vars' do 25 | it 'can parse and return default host vars' do 26 | host_vars = AdHoc.parse_host_vars 'all', 'localhost,' 27 | expect(host_vars['inventory_hostname']).to match 'localhost' 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/ansible/ansible_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ansible do 4 | it 'has a version number' do 5 | expect(Ansible::Config::VERSION).not_to be nil 6 | end 7 | 8 | it 'can configure environment variables' do 9 | expect(Ansible.config.to_s '').not_to include 'SOME_ENV_VAR' 10 | 11 | expect { 12 | Ansible.configure { |config| config.env['SOME_ENV_VAR'] = 'False' } 13 | }.to change { Ansible.config.env['SOME_ENV_VAR'] }.from(nil).to('False') 14 | 15 | expect(Ansible.config.to_s '').to include 'SOME_ENV_VAR=False' 16 | end 17 | 18 | pending 'check Config params debug output' do 19 | fail 20 | end 21 | 22 | before { suppress_output } 23 | it 'can run via shortcut methods when enabled' do 24 | Ansible.enable_shortcuts! 25 | disable_host_key_checking 26 | 27 | cmd = '-i localhost, spec/fixtures/mock_playbook.yml' 28 | expect(A << cmd).to be_a Integer 29 | expect(A['all -i localhost, --list-hosts']).to match /localhost/ 30 | end 31 | 32 | before { suppress_output } 33 | it 'can be included' do 34 | module Nodes 35 | include Ansible 36 | 37 | extend self 38 | 39 | def install!(ip) 40 | stream ['-i', ip, 'spec/fixtures/mock_playbook.yml']*' ' 41 | end 42 | end 43 | 44 | expect(Nodes.install! 'localhost,').to be_a Integer 45 | end 46 | end -------------------------------------------------------------------------------- /spec/ansible/output_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Ansible 4 | describe Output do 5 | describe '.to_html' do 6 | context 'can convert ANSI escape sequence colours to HTML styles' do 7 | it 'ignores text without escape sequences' do 8 | output = "Plain\nText\nHere" 9 | expect(Output.to_html output).to eq output 10 | end 11 | 12 | it 'opens a span tag with style when the appropriate sequence is detected' do 13 | output = "\e[31mRed" 14 | expect(Output.to_html output).to match /Red/ 15 | end 16 | 17 | it 'ignores unstyled text before an escape sequence' do 18 | output = "Default \e[31mRed" 19 | expect(Output.to_html output).to match /Default Red/ 20 | end 21 | 22 | it 'closes a span tag when the appropriate sequence is detected' do 23 | output = "\e[32mGreen\e[0m" 24 | expect(Output.to_html output).to eq %{Green} 25 | end 26 | 27 | it 'ignores unstyled text after an escape sequence' do 28 | output = "\e[90mGrey\e[0mDefault" 29 | expect(Output.to_html output).to eq %{GreyDefault} 30 | end 31 | 32 | it 'ignores newlines' do 33 | output = "\e[32mGreen\e[0m\n" 34 | expect(Output.to_html output).to eq %{Green\n} 35 | end 36 | 37 | it 'ignores tags left open' do 38 | output = "\e[0m\n\e[0;32mGreen\e[0m\n\e[0;34mBlue" 39 | expect(Output.to_html output).to eq %{\nGreen\nBlue} 40 | end 41 | 42 | it 'handles bold output alongside colour with dual styles in a single tag' do 43 | output = "\e[1;35mBold Magenta\e[0m" 44 | expect(Output.to_html output).to eq %{Bold Magenta} 45 | end 46 | 47 | it 'scrubs unsupported escape sequences' do 48 | output = "\e[38;5;118mBright Green - unsupported\e[0m" 49 | expect(Output.to_html output).to eq "Bright Green - unsupported" 50 | end 51 | 52 | it 'handles some malformed cases (missing semicolon)' do 53 | output = "\e[132mBright Green" 54 | expect(Output.to_html output).to eq 'Bright Green' 55 | end 56 | 57 | # This code may be entirely unreachable as regexp appears to be very specific 58 | it 'handles situations where no style attribute should be added to the tag' do 59 | output = "\e[0;99Nothing\e[0m" 60 | 61 | s = instance_double("StringScanner") 62 | allow(s).to receive(:eos?).and_return(false, true) 63 | allow(s).to receive(:scan).and_return(true, '') 64 | allow(s).to receive(:[]).and_return(nil, nil) 65 | 66 | class_double("StringScanner", new: s).as_stubbed_const 67 | 68 | expect(Output.to_html output).to match // 69 | end 70 | 71 | it 'correctly formats output of a streamed playbook' do 72 | output = '' 73 | Ansible.stream('-i localhost, spec/fixtures/mock_playbook.yml') do |line| 74 | output << Ansible::Output.to_html(line) 75 | end 76 | 77 | expect(output).to match /ok/ 78 | end 79 | 80 | it 'includes original stream content alongside formatted output of a streamed playbook' do 81 | output = '

Some tag

' 82 | Ansible.stream('-i localhost, spec/fixtures/mock_playbook.yml') do |line| 83 | Ansible::Output.to_html(line, output) 84 | end 85 | 86 | expect(output).to match /^

Some tag<\/h1>/ 87 | expect(output).to match /ok/ 88 | end 89 | 90 | context 'for a non-existent playbook' do 91 | it 'raises on failures when requested' do 92 | expect { 93 | Ansible.stream('-i localhost, does_not_exist.yml', raise_on_failure: :during) { next } 94 | }.to raise_error(Ansible::Playbook::Exception, /ERROR! the playbook: does_not_exist.yml could not be found/) 95 | end 96 | 97 | output = '' 98 | 99 | it 'does not handle failures otherwise' do 100 | expect { 101 | Ansible.stream('-i localhost, does_not_exist.yml') do |line| 102 | output << Ansible::Output.to_html(line) 103 | end 104 | }.not_to raise_error 105 | end 106 | 107 | it 'outputs an HTML error message' do 108 | expect(output).to match /ERROR! the playbook: does_not_exist.yml could not be found<\/span>/ 109 | end 110 | end 111 | end 112 | end 113 | end 114 | end -------------------------------------------------------------------------------- /spec/ansible/playbook_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Ansible 4 | describe Playbook do 5 | before(:all) do 6 | disable_host_key_checking 7 | end 8 | 9 | describe '.run' do 10 | it 'can execute a basic Ansible Playbook command on localhost' do 11 | expect(Playbook.run '-i localhost, spec/fixtures/mock_playbook.yml --list-hosts').to match /localhost/ 12 | end 13 | 14 | before { suppress_output } 15 | it 'can execute a basic Ansible Playbook' do 16 | expect(Playbook.run '-i localhost, spec/fixtures/mock_playbook.yml').to match /TASK(.?) \[Test task\]/ 17 | end 18 | end 19 | 20 | describe '.stream' do 21 | it 'can stream the output from execution of an Ansible Playbook' do 22 | cmd = '-i localhost, spec/fixtures/mock_playbook.yml' 23 | 24 | expect { |b| Playbook.stream cmd, &b }.to yield_control 25 | expect(Playbook.stream(cmd) { |l| next }).to be_a Integer 26 | expect(Playbook.stream(cmd) { |l| break l if l[/PLAY \[Testing Playbook\]/] }).to match /Testing Playbook/ 27 | end 28 | 29 | context 'for a non-existent playbook' do 30 | it 'raises an error when requested' do 31 | expect { 32 | Playbook.stream('-i localhost, file_not_found.yml --list-hosts', 33 | raise_on_failure: :during) { next } 34 | }.to raise_exception(Playbook::Exception, /could not be found/) 35 | end 36 | 37 | it 'provides output otherwise' do 38 | expect { 39 | Playbook.stream('-i localhost, file_not_found.yml --list-hosts') 40 | }.to output(/could not be found/).to_stdout 41 | end 42 | end 43 | 44 | # TODO add this for Ad-Hoc 45 | context 'upon a fatal outcome (unreachable node)' do 46 | it 'raises an error when requested and ceases output' do 47 | output = "" 48 | 49 | expect { 50 | Playbook.stream('-i localhost, spec/fixtures/fail_playbook.yml', 51 | raise_on_failure: :during) { |line| output << line } 52 | }.to raise_exception(Playbook::Exception, (/FAILED/ && /fatal/)) # && /UNREACHABLE/ 53 | 54 | expect(output).to_not include('PLAY RECAP') 55 | end 56 | 57 | it 'provides output otherwise' do # { |l| next } 58 | expect { 59 | Playbook.stream('-i localhost, spec/fixtures/fail_playbook.yml') 60 | }.to output(/PLAY RECAP/).to_stdout 61 | end 62 | end 63 | 64 | context 'continues to stream output despite failures' do 65 | it 'by default' do 66 | expect { 67 | Playbook.stream('-i localhost, spec/fixtures/fail_playbook.yml') 68 | }.to output(/PLAY RECAP/).to_stdout 69 | end 70 | 71 | it 'when raise_on_failure is set to :after' do 72 | expect { 73 | Playbook.stream('-i localhost, spec/fixtures/fail_playbook.yml', raise_on_failure: :after) 74 | }.to raise_exception(Playbook::Exception).and output(/PLAY RECAP/).to_stdout 75 | end 76 | 77 | it 'but not when raise_on_failure is :during' do 78 | output = "" 79 | 80 | expect { 81 | Playbook.stream('-i localhost, spec/fixtures/fail_playbook.yml', 82 | raise_on_failure: :during) { |line| output << line } 83 | }.to raise_exception(Playbook::Exception) 84 | 85 | expect(output).to_not include('PLAY RECAP') 86 | end 87 | end 88 | 89 | it 'continues to stream output despite failures when requested' do 90 | expect { 91 | Playbook.stream('-i localhost, spec/fixtures/fail_playbook.yml', raise_on_failure: :after) 92 | }.to raise_exception(Playbook::Exception).and output(/PLAY RECAP/).to_stdout 93 | end 94 | 95 | context 'where ignore_errors is set for tasks' do 96 | it 'skips a single failure when ignored' do 97 | expect { 98 | Playbook.stream('-i localhost, spec/fixtures/ignored_errors_playbook.yml', raise_on_failure: :during) { |l| next } 99 | }.not_to raise_exception 100 | end 101 | 102 | it 'skips multiple failures when they are ignored' do 103 | expect { 104 | Playbook.stream('-i localhost, spec/fixtures/ignored_errors_playbook.yml', raise_on_failure: :during) { |l| next } 105 | }.not_to raise_exception 106 | end 107 | 108 | it 'skips failures when they are ignored, but still reports later failures' do 109 | expect { 110 | Playbook.stream('-i localhost, spec/fixtures/ignored_error_then_failure_playbook.yml', 111 | raise_on_failure: :during) { |l| next } 112 | }.to raise_exception(Playbook::Exception) 113 | end 114 | 115 | it 'skips failures when they are ignored, but still reports earlier failures' do 116 | expect { 117 | Playbook.stream('-i localhost, spec/fixtures/failure_then_ignored_error_playbook.yml', 118 | raise_on_failure: :during) { |l| next } 119 | }.to raise_exception(Playbook::Exception) 120 | end 121 | end 122 | 123 | it 'defaults to standard output when streaming an Ansible Playbook if no block is given' do 124 | expect { 125 | Playbook.stream '-i localhost, spec/fixtures/mock_playbook.yml' 126 | }.to output(/Test task/).to_stdout 127 | end 128 | 129 | it 'returns a warning as part of the output when the inventory does not exist' do 130 | # TODO should probably raise an error for this behaviour (perhaps switch to pending) 131 | expect { Playbook.stream '-i localhost spec/fixtures/mock_playbook.yml' }.to output(/Unable to parse|Host file not found/).to_stdout 132 | end 133 | end 134 | end 135 | end -------------------------------------------------------------------------------- /spec/fixtures/fail_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Testing Playbook 4 | hosts: all 5 | gather_facts: no 6 | connection: ssh 7 | 8 | tasks: 9 | - name: Test task 10 | ping: -------------------------------------------------------------------------------- /spec/fixtures/failure_then_ignored_error_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Testing Playbook 4 | hosts: all 5 | gather_facts: no 6 | connection: local 7 | 8 | tasks: 9 | - name: Notice that this file does not exist 10 | shell: cat non_existent_file_20200309b 11 | - name: Ignore that this file does not exist 12 | shell: cat non_existent_file_20200309a 13 | ignore_errors: yes -------------------------------------------------------------------------------- /spec/fixtures/ignored_error_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Ignore single failure 4 | hosts: all 5 | gather_facts: no 6 | connection: local 7 | 8 | tasks: 9 | - name: Ignore that this file does not exist 10 | shell: cat non_existent_file_20200309a 11 | ignore_errors: yes -------------------------------------------------------------------------------- /spec/fixtures/ignored_error_then_failure_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Testing Playbook 4 | hosts: all 5 | gather_facts: no 6 | connection: local 7 | 8 | tasks: 9 | - name: Ignore that this file does not exist 10 | shell: cat non_existent_file_20200309a 11 | ignore_errors: yes 12 | - name: Notice that this file does not exist 13 | shell: cat non_existent_file_20200309b -------------------------------------------------------------------------------- /spec/fixtures/ignored_errors_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Ignore multiple failures 4 | hosts: all 5 | gather_facts: no 6 | connection: local 7 | 8 | tasks: 9 | - name: Ignore that this file does not exist (skipped failure 1) 10 | shell: cat non_existent_file_20200309a 11 | ignore_errors: yes 12 | - name: Ignore that this file does not exist (skipped failure 2) 13 | shell: cat non_existent_file_20200309b 14 | ignore_errors: yes -------------------------------------------------------------------------------- /spec/fixtures/mock_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Testing Playbook 4 | hosts: all 5 | gather_facts: no 6 | connection: local 7 | 8 | tasks: 9 | - name: Test task 10 | register: test_msg 11 | changed_when: "'Test' in test_msg.stderr" 12 | shell: echo 'Test' -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'coveralls' 3 | Coveralls.wear! 4 | 5 | require 'ansible-wrapper' 6 | 7 | def disable_host_key_checking 8 | Ansible.configure { |config| config.env['ANSIBLE_HOST_KEY_CHECKING'] = 'False' } 9 | end 10 | 11 | def suppress_output 12 | allow($stdout).to receive(:puts) 13 | allow($stdout).to receive(:write) 14 | end --------------------------------------------------------------------------------