Skip to content

Commit

Permalink
Add documentation, improve specs, refactor Output module
Browse files Browse the repository at this point in the history
Output module needed to be simplified structurally.
Also added docs badge & example to README.
  • Loading branch information
pgeraghty committed Sep 26, 2019
1 parent bde2d41 commit 4bfb139
Show file tree
Hide file tree
Showing 13 changed files with 224 additions and 71 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
[![Build Status](https://travis-ci.com/pgeraghty/ansible-wrapper-ruby.svg?branch=master)](https://travis-ci.com/pgeraghty/ansible-wrapper-ruby)
[![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)
[![Code Climate](https://codeclimate.com/github/pgeraghty/ansible-wrapper-ruby/badges/gpa.svg)](https://codeclimate.com/github/pgeraghty/ansible-wrapper-ruby)
[![Documentation](http://inch-ci.org/github/pgeraghty/ansible-wrapper-ruby.svg?branch=master)](http://inch-ci.org/github/pgeraghty/ansible-wrapper-ruby)

#### A lightweight Ruby wrapper around Ansible that allows for ad-hoc commands and playbook execution. The primary purpose is to support easy streaming output.

## Requirements

Ensure [Ansible](http://docs.ansible.com/intro_getting_started.html) is installed and available to shell commands i.e. in PATH.
[Tested](https://travis-ci.org/pgeraghty/ansible-wrapper-ruby) with Ansible versions 1.9.4 and 2.0.0.2, but please create an issue if you use a version that fails.
[Tested](https://travis-ci.org/pgeraghty/ansible-wrapper-ruby) with Ansible versions 2.0.2 thru 2.8.5 and Ruby 2.1+, but please create an issue if you use a version that fails.

## Installation

Expand Down Expand Up @@ -72,9 +73,9 @@ A['all -i localhost, --list-hosts'] # alias for Ansible::AdHoc.run
A << '-i localhost, spec/fixtures/mock_playbook.yml' # alias for Ansible::Playbook.stream
```

## Coming Soon
## Examples

* Streaming output example using Sinatra
* For a streaming output example using Sinatra, see the [examples/streaming](examples/streaming) folder.

## Development

Expand Down
2 changes: 2 additions & 0 deletions ansible-wrapper.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'ansible/version'

# TODO set required ruby version??

Gem::Specification.new do |spec|
spec.name = 'ansible-wrapper'
spec.version = Ansible::VERSION
Expand Down
4 changes: 4 additions & 0 deletions lib/ansible-wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
require 'ansible/playbook'
require 'ansible/output'

# A lightweight Ruby wrapper around Ansible that allows for ad-hoc commands and playbook execution.
# The primary purpose is to support easy streaming output.
module Ansible
include Ansible::Config
include Ansible::Methods
include Ansible::PlaybookMethods

extend self

# Enables shortcuts
# @see ansible/shortcuts.rb
def enable_shortcuts!
require 'ansible/shortcuts'
end
Expand Down
39 changes: 33 additions & 6 deletions lib/ansible/ad_hoc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,41 @@
require 'json'

module Ansible
# Ansible Ad-Hoc methods
module Methods
# executable that runs Ansible Ad-Hoc commands
BIN = 'ansible'

def one_off cmd
# Run an Ad-Hoc Ansible command
# @param cmd [String] the Ansible command to execute
# @return [String] the output
# @example Run a simple shell command with an inline inventory that only contains localhost
# one_off 'all -c local -a "echo hello"'
def one_off(cmd)
# TODO if debug then puts w/ colour
`#{config.to_s "#{BIN} #{cmd}"}`
end
alias :[] :one_off

def list_hosts cmd
# Ask Ansible to list hosts
# @param cmd [String] the Ansible command to execute
# @return [String] the output
# @example List hosts with an inline inventory that only contains localhost
# list_hosts 'all -i localhost,'
def list_hosts(cmd)
output = one_off("#{cmd} --list-hosts").gsub!(/\s+hosts.*:\n/, '').strip
output.split("\n").map(&:strip)
end

def parse_host_vars(host, inv_file, filter = 'hostvars[inventory_hostname]')
cmd = "all -m debug -a 'var=#{filter}' -i #{inv_file} -l #{host}"
# Fetches host variables via Ansible's debug module
# @param host [String] the +<host-pattern>+ for target host(s)
# @param inv [String] the inventory host path or comma-separated host list
# @param filter [String] the variable filter
# @return [Hash] the variables pertaining to the host
# @example List variables for localhost
# parse_host_vars 'localhost', 'localhost,'
def parse_host_vars(host, inv, filter = 'hostvars[inventory_hostname]')
cmd = "all -m debug -a 'var=#{filter}' -i #{inv} -l #{host}"
json = self[cmd].split(/>>|=>/).last

# remove any colour added to console output
Expand All @@ -31,11 +51,18 @@ def parse_host_vars(host, inv_file, filter = 'hostvars[inventory_hostname]')
end
end

# Provides static access to Ad-Hoc methods
module AdHoc
include Ansible::Config
include Ansible::Methods
include Config
include Methods

extend self

# Run an Ad-Hoc Ansible command
# @see Methods#one_off
# @param cmd [String] the Ansible command to execute
# @return [String] the output
# @since 0.2.1
alias :run :one_off
end
end
27 changes: 22 additions & 5 deletions lib/ansible/config.rb
Original file line number Diff line number Diff line change
@@ -1,45 +1,60 @@
module Ansible
# Ansible configuration
module Config
PATH = 'lib/ansible/'
# 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/
SKIP_HOSTVARS = %w(ansible_version inventory_dir inventory_file inventory_hostname inventory_hostname_short group_names groups omit playbook_dir)
VERSION = `ansible --version`.split("\n").first.split.last rescue nil # nil when Ansible not installed

# Default configuration options
DefaultConfig = Struct.new(:env, :extra_vars, :params) do
# @!attribute env
# @return [Hash] environment variables
# @!attribute params
# @return [Hash] parameters
# @!attribute extra_vars
# @return [Hash] extra variables to pass to Ansible

def initialize
self.env = {
'ANSIBLE_FORCE_COLOR' => 'True',
'ANSIBLE_HOST_KEY_CHECKING' => 'False'
}

self.params = {
debug: false
}

# options
self.extra_vars = {
# skip creation of .retry files
'retry_files_enabled' => 'False'
}
# TODO support --ssh-common-args, --ssh-extra-args
# e.g. ansible-playbook --ssh-common-args="-o ServerAliveInterval=60" -i inventory install.yml

self.params = {
debug: false
}
end

# Pass additional options to Ansible
# NB: --extra-vars can also accept JSON string, see http://stackoverflow.com/questions/25617273/pass-array-in-extra-vars-ansible
# @return [String] command-line options
def options
x = extra_vars.each_with_object('--extra-vars=\'') { |kv, a| a << "#{kv.first}=\"#{kv.last}\" " }.strip+'\'' if extra_vars unless extra_vars.empty?
# can test with configure { |config| config.extra_vars.clear }

[x, '--ssh-extra-args=\'-o UserKnownHostsFile=/dev/null\'']*' '
end

# Output configuration as a string for the command-line
# @param cmd [String] command to be appended to the command-line produced
# @return [Config, DefaultConfig] the configuration
def to_s(cmd)
entire_cmd = [env.each_with_object([]) { |kv, a| a << kv*'=' } * ' ', cmd, options]*' '
puts entire_cmd if params[:debug]
entire_cmd
end
end

# Create and yield configuration
# @return [Config, DefaultConfig] the configuration
def configure
@config ||= DefaultConfig.new
yield(@config) if block_given?
Expand All @@ -48,6 +63,8 @@ def configure
block_given? ? self : @config
end

# accessor for config
# @return [DefaultConfig] the configuration
def config
@config || configure
end
Expand Down
118 changes: 80 additions & 38 deletions lib/ansible/output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,91 @@
require 'erb'

module Ansible
# Output module provides formatting of Ansible output
module Output
COLOR = {
'1' => 'font-weight: bold',
'30' => 'color: black',
'31' => 'color: red',
'32' => 'color: green',
'33' => 'color: yellow',
'34' => 'color: blue',
'35' => 'color: magenta',
'36' => 'color: cyan',
'37' => 'color: white',
'90' => 'color: grey'
}

def self.to_html(line, stream='')
s = StringScanner.new(ERB::Util.h line)
while(!s.eos?)
if s.scan(/\e\[([0-1])?[;]?(3[0-7]|90|1)m/)
bold, colour = s[1], s[2]
styles = []

styles << COLOR[bold] if bold.to_i == 1
styles << COLOR[colour]

span =
# in case of invalid colours, although this may be impossible
if styles.compact.empty?
%{<span>}
else
%{<span style="#{styles*'; '};">}
end

stream << span
elsif s.scan(/\e\[0m/)
stream << %{</span>}
elsif s.scan(/\e\[[^0]*m/)
stream << '<span>'
# Generate HTML for an output string formatted with ANSI escape sequences representing colours and styling
# @param ansi [String] an output string formatted with escape sequences to represent formatting
# @param stream [String] a stream or string (that supports +<<+) to which generated HTML will be appended
# @return the stream provided or a new String
# @example List hosts with an inline inventory that only contains localhost
# to_html "\e[90mGrey\e[0m" => '<span style="color: grey;">Grey</span>'
def self.to_html(ansi, stream='')
Ansi2Html.new(ansi).to_html stream
end

# Converter for strings containing with ANSI escape sequences
class Ansi2Html
# Hash of colors to convert shell colours to CSS
COLOR = {
'1' => 'font-weight: bold',
'30' => 'color: black',
'31' => 'color: red',
'32' => 'color: green',
'33' => 'color: yellow',
'34' => 'color: blue',
'35' => 'color: magenta',
'36' => 'color: cyan',
'37' => 'color: white',
'90' => 'color: grey'
}

SUPPORTED_STYLE_PATTERN = /\e\[([0-1])?[;]?(3[0-7]|90|1)m/
END_ESCAPE_SEQUENCE_PATTERN = /\e\[0m/
UNSUPPORTED_STYLE_PATTERN = /\e\[[^0]*m/
IGNORED_OUTPUT = /./m

OPEN_SPAN_TAG = %{<span>}
CLOSE_SPAN_TAG = %{</span>}

# Create StringScanner for string
# @param line [String] a stream or string (that supports +<<+) to which generated HTML will be appended
def initialize(line)
# ensure any HTML tag characters are escaped
@strscan = StringScanner.new(ERB::Util.h line)
end

# Generate HTML from string formatted with ANSI escape sequences
# @return [String, IO] the HTML
def to_html(stream)
until @strscan.eos?
stream << generate_html
end

stream
end


private

# Scan string and generate HTML
def generate_html
if @strscan.scan SUPPORTED_STYLE_PATTERN
open_tag
elsif @strscan.scan END_ESCAPE_SEQUENCE_PATTERN
CLOSE_SPAN_TAG
elsif @strscan.scan UNSUPPORTED_STYLE_PATTERN
OPEN_SPAN_TAG
else
stream << s.scan(/./m)
@strscan.scan IGNORED_OUTPUT
end
end

stream
# Generate opening HTML tag, which may contain a style attribute
# @return [String] opening tag
def open_tag
bold, colour = @strscan[1], @strscan[2]
styles = []

styles << COLOR[bold] if bold.to_i == 1
styles << COLOR[colour]

# in case of invalid colours, although this may be impossible
if styles.compact.empty?
OPEN_SPAN_TAG
else
%{<span style="#{styles*'; '};">}
end
end
end
end
end
37 changes: 26 additions & 11 deletions lib/ansible/playbook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,53 @@
require 'ansible/safe_pty'

module Ansible
# Ansible Playbook methods
module PlaybookMethods
# executable that runs Ansible Playbooks
BIN = 'ansible-playbook'

def playbook pb
# Run playbook, returning output
# @param pb [String] path to playbook
# @return [String] output
def playbook(pb)
# TODO if debug then puts w/ colour
`#{config.to_s "#{BIN} #{pb}"}`
end
alias :<< :playbook

def stream pb
# Use PTY because otherwise output is buffered
SafePty.spawn config.to_s("#{BIN} #{pb}") do |r,w,p| # add -vvvv here for verbose
# Stream execution of a playbook using PTY because otherwise output is buffered
# @param pb [String] path to playbook
# @return [Integer] exit status
def stream(pb)
cmd = config.to_s("#{BIN} #{pb}")

SafePty.spawn cmd do |r,_,_| # add -vvvv here for verbose
until r.eof? do
line = r.gets
block_given? ? yield(line) : puts(line)

raise "FAILED: #{line}" if line.include?('fatal: [')
raise Playbook::Exception.new("ERROR: #{line}") if line.include?('ERROR!')
# TODO raise if contains FAILED!
case line
when /fatal: \[/ then raise Playbook::Exception.new("FAILED: #{line}")
when /ERROR!/,/FAILED!/ then raise Playbook::Exception.new("ERROR: #{line}")
end
end
end
end
end
end

module Ansible
# Provides static access to Playbook methods
module Playbook
include Ansible::Config
include Ansible::PlaybookMethods
include Config
include PlaybookMethods

extend self

# Run playbook, returning output
# @param pb [String] path to playbook
# @return [String] output
alias :run :playbook

# Exception to represent Playbook failures
class Exception < RuntimeError; end
end
end
Loading

0 comments on commit 4bfb139

Please sign in to comment.