kitchen-ansiblepush 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 82964df38e42759e837ed505d7f760afaff977cb
4
+ data.tar.gz: 75caa47b0cbc04aaf182fc12e1e6fecd5364b658
5
+ SHA512:
6
+ metadata.gz: 1cf540b68aa64e6d803f41f7f8345f483e907552bf8477d349998958864b5445da5fc3760a1002c8f23aec5b55bb1f036ee6f86dff142a10f9df70776f7ceff6
7
+ data.tar.gz: f0f45bd5780a607f2c1fbe6bc1cdbe3b3abe43d719164ef56ead52f6c31ce175c6ace4fd04445e4c7eb9cfb51bb3e90d7a2b3017b352d05b81f83c3a107f3cc2
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # kitchen-ansiblepush
2
+
3
+ A test-kitchen plugin that adds the support for ansible in push mode
4
+
5
+ ## TODO
6
+ * Tests
7
+
8
+ ## Intro
9
+ This kitchen plugin adds ansible as a provisioner in push mode. Ansible will run from your host rather than run from guest machines.
10
+
11
+ ## How to install
12
+
13
+ ### Ruby gem
14
+ ```
15
+ gem install kitchen-ansiblepush
16
+ ```
17
+
18
+ ### To install from code
19
+ ```
20
+ git clone git@github.com:ahelal/kitchen-ansiblepush.git
21
+ cd kitchen-ansiblepush
22
+ gem build kitchen-ansiblepush.gemspec
23
+ sudo gem install kitchen-ansiblepush-<version>.gem
24
+ ```
25
+
26
+ ## kitchen.yml Options
27
+ ```yaml
28
+ provisioner :
29
+ ## required options
30
+ name : ansible_push
31
+ playbook : "../../plays/web.yml" # Path to Play yaml
32
+ ##
33
+ ## Optional argument
34
+ ansible_config : "/path/to/ansible/ansible.cfg" # path to ansible config file
35
+ verbose : "vvvv" # verbose level v, vv, vvv, vvvv
36
+ diff : true # print file diff
37
+ mygroup : "web" # ansible group
38
+ raw_arguments : "--timeout=200"
39
+ extra_vars : "@vars.yml"
40
+ tags : [ "that", "this" ]
41
+ skip_tags : [ "notme", "orme" ]
42
+ start_at_task : [ "five" ]
43
+ # Hash of other groups
44
+ groups :
45
+ db :
46
+ - db01
47
+ sudo : true
48
+ sudo_user : root
49
+ remote_user : ubuntu
50
+ private_key : "/path..../id_rsa"
51
+ ask_vault_pass : true
52
+ vault_password_file : "/..../file"
53
+ host_key_checking : false
54
+ generate_inv : true
55
+
56
+ ```
57
+ ## idempotency test
58
+ If you want to check your code is idempotent you can use the idempotency_test. Essentially, this will run Ansible twice and check nothing changed in the next run. If something changed it will list the tasks. Note: If your using Ansible callback in your config this might conflict.
59
+ ```yaml
60
+ idempotency_test: True
61
+ ```
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'json'
4
+ require 'kitchen-ansible/util-inventory.rb'
5
+
6
+ all = []
7
+ groups = Hash.new
8
+ hosts = Hash.new
9
+ if File.exist?(TEMP_GROUP_FILE)
10
+ groups = YAML::load_file TEMP_GROUP_FILE
11
+ end
12
+
13
+ Dir.glob(TEMP_INV_DIR + '/ansiblepush_host_*.yml') do |inv_yml|
14
+ vm = YAML::load_file inv_yml
15
+ vm.each do |host, host_attr|
16
+ if host_attr["mygroup"]
17
+ if host_attr["mygroup"].is_a?(Hash)
18
+ host_attr["mygroup"].each do | group |
19
+ groups[group] ||= []
20
+ groups[group] << host
21
+ end
22
+ elsif host_attr["mygroup"].kind_of?(String)
23
+ groups[host_attr["mygroup"]] ||= []
24
+ groups[host_attr["mygroup"]] << host
25
+ end
26
+ end
27
+ host_attr.delete("mygroup")
28
+ hosts[host] = host_attr
29
+ all << host
30
+ end
31
+ end
32
+
33
+ inventory = { "all" => all, "_meta" => { "hostvars" => hosts }}
34
+
35
+
36
+
37
+ inventory = groups.merge(inventory)
38
+
39
+ # Print our inventory
40
+ puts JSON.pretty_generate(inventory)
@@ -0,0 +1,97 @@
1
+ import json
2
+ import os
3
+ import errno
4
+
5
+ change_dir = "/tmp/kitchen_ansible_callback"
6
+ change_file = change_dir + "/changes"
7
+
8
+ class CallbackModule(object):
9
+ def __init__(self):
10
+ if not os.path.exists(change_dir):
11
+ os.makedirs(change_dir)
12
+
13
+ try:
14
+ os.remove(change_file)
15
+ except OSError as e: # this would be "except OSError, e:" before Python 2.6
16
+ if e.errno != errno.ENOENT: # errno.ENOENT = no such file or directory
17
+ raise # re-raise exception if a different error occured
18
+
19
+
20
+
21
+ def write_changed_to_file(self, host, res, name=None):
22
+ changed_data = dict()
23
+ invocation = res.get("invocation", {})
24
+ changed_data["changed_module_args"] = invocation.get("module_args", "")
25
+ changed_data["changed_module_name"] = invocation.get("module_name", "")
26
+ changed_data["host"] = host
27
+ if name:
28
+ changed_data["task_name"] = name
29
+ changed_data["changed_msg"] = res.get("msg", "")
30
+
31
+ try:
32
+ with open(change_file, 'at') as the_file:
33
+ the_file.write(json.dumps(changed_data) + "\n")
34
+ except Exception, e:
35
+ print "Ansible callback idempotency: Write to file failed '%s' error:'%s'" % (change_file, e)
36
+ exit(1)
37
+
38
+ def on_any(self, *args, **kwargs):
39
+ pass
40
+
41
+ def runner_on_failed(self, host, res, ignore_errors=False):
42
+ pass
43
+
44
+ def runner_on_ok(self, host, res):
45
+ if res.get("changed"):
46
+ self.write_changed_to_file(host, res, self.current_task)
47
+
48
+ def runner_on_skipped(self, host, item=None):
49
+ pass
50
+
51
+ def runner_on_unreachable(self, host, res):
52
+ pass
53
+
54
+ def runner_on_no_hosts(self):
55
+ pass
56
+
57
+ def runner_on_async_poll(self, host, res, jid, clock):
58
+ pass
59
+
60
+ def runner_on_async_ok(self, host, res, jid):
61
+ pass
62
+
63
+ def runner_on_async_failed(self, host, res, jid):
64
+ pass
65
+
66
+ def playbook_on_start(self):
67
+ pass
68
+
69
+ def playbook_on_notify(self, host, handler):
70
+ pass
71
+
72
+ def playbook_on_no_hosts_matched(self):
73
+ pass
74
+
75
+ def playbook_on_no_hosts_remaining(self):
76
+ pass
77
+
78
+ def playbook_on_task_start(self, name, is_conditional):
79
+ self.current_task = name
80
+
81
+ def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
82
+ pass
83
+
84
+ def playbook_on_setup(self):
85
+ pass
86
+
87
+ def playbook_on_import_for_host(self, host, imported_file):
88
+ pass
89
+
90
+ def playbook_on_not_import_for_host(self, host, missing_file):
91
+ pass
92
+
93
+ def playbook_on_play_start(self, name):
94
+ pass
95
+
96
+ def playbook_on_stats(self, stats):
97
+ pass
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ $:.unshift File.expand_path('../lib', __FILE__)
4
+ require 'kitchen-ansible/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "kitchen-ansiblepush"
8
+ gem.version = Kitchen::AnsiblePush::VERSION
9
+ gem.authors = ["Adham Helal"]
10
+ gem.email = ["adham.helal@gmail.com"]
11
+ gem.licenses = ['MIT']
12
+ gem.homepage = "https://github.com/ahelal/kitchen-ansiblepush"
13
+ gem.summary = "ansible provisioner for test-kitchen"
14
+ candidates = Dir.glob("{lib}/**/*") + ['README.md', 'kitchen-ansiblepush.gemspec', 'callback/changes_callback.py']
15
+ gem.files = candidates.sort
16
+ gem.platform = Gem::Platform::RUBY
17
+ gem.require_paths = ['lib']
18
+ gem.executables = ['kitchen-ansible-inventory']
19
+ gem.rubyforge_project = '[none]'
20
+ gem.description = <<-EOF
21
+ == DESCRIPTION:
22
+
23
+ Ansible push Provisioner for Test Kitchen
24
+
25
+ == FEATURES:
26
+
27
+ Supports running ansible in push mode
28
+
29
+ EOF
30
+
31
+ gem.add_runtime_dependency 'test-kitchen'
32
+ gem.add_development_dependency 'rspec'
33
+ gem.add_development_dependency 'pry'
34
+
35
+ end
@@ -0,0 +1,30 @@
1
+ TEMP_INV_DIR = ".kitchen/ansiblepush"
2
+ TEMP_GROUP_FILE = "#{TEMP_INV_DIR}/ansiblepush_groups_inventory.yml"
3
+
4
+
5
+
6
+ def write_instance_inventory(name, host, mygroup, instance_connection_option)
7
+ Dir.mkdir TEMP_INV_DIR if !File.exist?(TEMP_INV_DIR)
8
+ port = instance_connection_option[:port]
9
+ keys = instance_connection_option[:keys]
10
+ user = instance_connection_option[:user]
11
+
12
+ temp_hash = Hash.new
13
+ temp_hash["ansible_ssh_host"] = host
14
+ temp_hash["ansible_ssh_port"] = port if port
15
+ temp_hash["ansible_ssh_private_key_file"] = keys[0] if keys
16
+ temp_hash["ansible_ssh_user"] = user if user
17
+ temp_hash["mygroup"] = mygroup if mygroup
18
+
19
+ host = { name => temp_hash }
20
+ File.open("%s/ansiblepush_host_%s.yml" % [TEMP_INV_DIR, name], "w") do |file|
21
+ file.write host.to_yaml
22
+ end
23
+ end
24
+
25
+ def write_group_inventory(groups)
26
+ Dir.mkdir TEMP_INV_DIR if !File.exist?(TEMP_INV_DIR)
27
+ File.open(TEMP_GROUP_FILE, "w") do |file|
28
+ file.write groups.to_yaml
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ module Kitchen
2
+ module AnsiblePush
3
+ VERSION = "0.3.1"
4
+ end
5
+ end
@@ -0,0 +1,242 @@
1
+ require 'kitchen'
2
+ require 'kitchen/provisioner/base'
3
+ require 'kitchen-ansible/util-inventory.rb'
4
+ require 'json'
5
+
6
+ module Kitchen
7
+
8
+ class Busser
9
+ def non_suite_dirs
10
+ %w{data}
11
+ end
12
+ end
13
+
14
+ module Provisioner
15
+ class AnsiblePush < Base
16
+ kitchen_provisioner_api_version 2
17
+ default_config :ansible_config, nil
18
+ default_config :verbose, nil
19
+ default_config :diff, nil
20
+ default_config :groups, nil
21
+ default_config :extra_vars, nil
22
+ default_config :sudo, nil
23
+ default_config :sudo_user, nil
24
+ default_config :remote_user, nil
25
+ default_config :private_key, nil
26
+ default_config :ask_vault_pass, nil
27
+ default_config :vault_password_file, nil
28
+ default_config :limit, nil
29
+ default_config :tags, nil
30
+ default_config :skip_tags, nil
31
+ default_config :start_at_task, nil
32
+ default_config :host_key_checking, false
33
+ default_config :mygroup, nil
34
+ default_config :playbook, nil
35
+ default_config :generate_inv, true
36
+ default_config :raw_arguments, nil
37
+ default_config :idempotency_test, false
38
+
39
+ # For tests disable if not needed
40
+ default_config :chef_bootstrap_url, "https://www.getchef.com/chef/install.sh"
41
+
42
+ def prepare_command
43
+ validate_config
44
+ prepare_inventory
45
+ complie_config
46
+ end
47
+
48
+ def install_command
49
+ # Must install chef for busser and serverspec to work :(
50
+ info("*************** AnsiblePush install_command ***************")
51
+ omnibus_download_dir = config[:omnibus_cachier] ? "/tmp/vagrant-cache/omnibus_chef" : "/tmp"
52
+ chef_url = config[:chef_bootstrap_url]
53
+ <<-INSTALL
54
+ sh -c '
55
+ #{Util.shell_helpers}
56
+ if [ ! -d "/opt/chef" ]
57
+ then
58
+ echo "-----> Installing Chef Omnibus"
59
+ mkdir -p #{omnibus_download_dir}
60
+ if [ ! -x #{omnibus_download_dir}/install.sh ]
61
+ then
62
+ do_download #{chef_url} #{omnibus_download_dir}/install.sh
63
+ fi
64
+
65
+ sudo sh #{omnibus_download_dir}/install.sh -d #{omnibus_download_dir}
66
+ echo "-----> End Installing Chef Omnibus"
67
+ fi
68
+
69
+ # Fix for https://github.com/test-kitchen/busser/issues/12
70
+ if [ -h /usr/bin/ruby ]; then
71
+ L=$(readlink -f /usr/bin/ruby)
72
+ sudo rm /usr/bin/ruby
73
+ sudo ln -s $L /usr/bin/ruby
74
+ fi
75
+ '
76
+ INSTALL
77
+
78
+ end
79
+
80
+ def run_command
81
+ info("*************** AnsiblePush run ***************")
82
+ exec_command(@command_env, @command, "ansible-playbook")
83
+ # idempotency test
84
+ if config[:idempotency_test]
85
+ info("*************** idempotency test ***************")
86
+ @command_env["ANSIBLE_CALLBACK_PLUGINS"] = "#{File.dirname(__FILE__)}/../../../callback/"
87
+ exec_command(@command_env, @command, "ansible-playbook")
88
+ # Check ansible callback if changes has occured in the second run
89
+ file_path = "/tmp/kitchen_ansible_callback/changes"
90
+ if File.file?(file_path)
91
+ task = 0
92
+ info("idempotency test [Failed]")
93
+ File.open(file_path, "r") do |f|
94
+ f.each_line do |line|
95
+ task += 1
96
+ info(" #{task}> #{line.strip}")
97
+ end
98
+ end
99
+ raise "idempotency test Failed. Number of non idemptent tasks: #{task}"
100
+
101
+ else
102
+ info("idempotency test [passed]")
103
+ end
104
+ end
105
+ info("*************** AnsiblePush end run *******************")
106
+ debug("[#{name}] Converge completed (#{config[:sleep]}s).")
107
+ return nil
108
+ end
109
+
110
+ protected
111
+
112
+ def exec_command(env, command, desc)
113
+ debug("env=%s command=%s" % [env, command] )
114
+ system(env, "#{command}")
115
+ exit_code = $?.exitstatus
116
+ debug("ansible-playbook exit code = #{exit_code}")
117
+ if exit_code.to_i != 0
118
+ raise "%s returned a non zeroo '%s'. Please see the output above." % [ desc, exit_code.to_s ]
119
+ end
120
+ end
121
+
122
+ def prepare_inventory
123
+ @machine_name = instance.to_str.gsub(/[<>]/, '').split("-").drop(1).join("-")
124
+ @instance_connection_option = instance.transport.instance_variable_get(:@connection_options)
125
+ hostname = @instance_connection_option[:hostname]
126
+ debug("instance_connection_option=" + @instance_connection_option.to_s)
127
+ write_instance_inventory(@machine_name, hostname, config[:mygroup], @instance_connection_option)
128
+ end
129
+
130
+ def complie_config()
131
+ debug("compile_config")
132
+ options = []
133
+ options << "--extra-vars=#{self.get_extra_vars_argument}" if config[:extra_vars]
134
+ options << "--sudo" if config[:sudo]
135
+ options << "--sudo-user=#{config[:sudo_user]}" if config[:sudo_user]
136
+ options << "--user=#{config[:remote_user]}" if self.get_remote_user
137
+ options << "--private-key=#{config[:private_key]}" if config[:private_key]
138
+ options << "#{self.get_verbosity_argument}" if config[:verbose]
139
+ options << "--diff" if config[:diff]
140
+ options << "--ask-sudo-pass" if config[:ask_sudo_pass]
141
+ options << "--ask-vault-pass" if config[:ask_vault_pass]
142
+ options << "--vault-password-file=#{config[:vault_password_file]}" if config[:vault_password_file]
143
+ options << "--tags=%s" % self.as_list_argument(config[:tags]) if config[:tags]
144
+ options << "--skip-tags=%s" % self.as_list_argument(config[:skip_tags]) if config[:skip_tags]
145
+ options << "--start-at-task=#{config[:start_at_task]}" if config[:start_at_task]
146
+ options << "--inventory-file=`which kitchen-ansible-inventory`" if config[:generate_inv]
147
+ ##options << "--inventory-file=#{ssh_inv}," if ssh_inv
148
+
149
+ # By default we limit by the current machine,
150
+ if config[:limit]
151
+ options << "--limit=#{as_list_argument(config[:limit])}"
152
+ else
153
+ options << "--limit=#{@machine_name}"
154
+ end
155
+
156
+ #Add raw argument as final thing
157
+ options << config[:raw_arguments] if config[:raw_arguments]
158
+
159
+ @command = (%w(ansible-playbook) << options << config[:playbook]).flatten.join(" ")
160
+ debug("Ansible push command= %s" % @command)
161
+ @command_env = {
162
+ "PYTHONUNBUFFERED" => "1", # Ensure Ansible output isn't buffered
163
+ "ANSIBLE_FORCE_COLOR" => "true",
164
+ "ANSIBLE_HOST_KEY_CHECKING" => "#{config[:host_key_checking]}",
165
+ }
166
+ @command_env["ANSIBLE_CONFIG"]=config[:ansible_config] if config[:ansible_config]
167
+
168
+ info("Ansible push compile conig done")
169
+ end
170
+
171
+ def validate_config()
172
+ if !config[:playbook]
173
+ raise 'No playbook defined. Please specify one in .kitchen.yml'
174
+ end
175
+
176
+ if !File.exist?(config[:playbook])
177
+ raise "playbook '%s' could not be found. Please check path" % config[:playbook]
178
+ end
179
+
180
+ if config[:vault_password_file] and !File.exist?(config[:vault_password_file])
181
+ raise "Vault password '%s' could not be found. Please check path" % config[:vault_password_file]
182
+ end
183
+
184
+ # Validate that extra_vars is either a hash, or a path to an existing file
185
+ if config[:extra_vars]
186
+ extra_vars_is_valid = config[:extra_vars].kind_of?(Hash) || config[:extra_vars].kind_of?(String)
187
+ if config[:extra_vars].kind_of?(String)
188
+ # Accept the usage of '@' (e.g. '@vars.yml' and 'vars.yml' are both supported)
189
+ match_data = /^@?(.+)$/.match(config[:extra_vars])
190
+ extra_vars_path = match_data[1].to_s
191
+ expanded_path = Pathname.new(extra_vars_path).expand_path(Dir.pwd)
192
+ extra_vars_is_valid = expanded_path.exist?
193
+ if extra_vars_is_valid
194
+ @extra_vars = '@' + extra_vars_path
195
+ end
196
+ end
197
+ if !extra_vars_is_valid
198
+ raise "ansible extra_vars is in valid type: %s value: %s" % [config[:extra_vars].class.to_s, config[:extra_vars].to_s]
199
+ end
200
+ end
201
+ info("Ansible push config validated")
202
+ end
203
+
204
+ def get_extra_vars_argument()
205
+ if config[:extra_vars].kind_of?(String) and config[:extra_vars] =~ /^@.+$/
206
+ # A JSON or YAML file is referenced (requires Ansible 1.3+)
207
+ return config[:extra_vars]
208
+ else
209
+ # Expected to be a Hash after config validation. (extra_vars as
210
+ # JSON requires Ansible 1.2+, while YAML requires Ansible 1.3+)
211
+ return config[:extra_vars].to_json
212
+ end
213
+ end
214
+
215
+ def get_remote_user
216
+ if config[:remote_user]
217
+ return config[:remote_user]
218
+ elsif @instance_connection_option[:username]
219
+ config[:remote_user] = @instance_connection_option[:username]
220
+ return @instance_connection_option[:username]
221
+ else
222
+ return nil
223
+ end
224
+ end
225
+
226
+ def as_list_argument(v)
227
+ v.kind_of?(Array) ? v.join(',') : v
228
+ end
229
+
230
+ def get_verbosity_argument
231
+ if config[:verbose].to_s =~ /^v+$/
232
+ # ansible-playbook accepts "silly" arguments like '-vvvvv' as '-vvvv' for now
233
+ return "-#{config[:verbose]}"
234
+ else
235
+ # safe default, in case input strays
236
+ return '-v'
237
+ end
238
+ end
239
+
240
+ end
241
+ end
242
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kitchen-ansiblepush
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Adham Helal
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: test-kitchen
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: |+
56
+ == DESCRIPTION:
57
+
58
+ Ansible push Provisioner for Test Kitchen
59
+
60
+ == FEATURES:
61
+
62
+ Supports running ansible in push mode
63
+
64
+ email:
65
+ - adham.helal@gmail.com
66
+ executables:
67
+ - kitchen-ansible-inventory
68
+ extensions: []
69
+ extra_rdoc_files: []
70
+ files:
71
+ - README.md
72
+ - callback/changes_callback.py
73
+ - kitchen-ansiblepush.gemspec
74
+ - lib/kitchen-ansible/util-inventory.rb
75
+ - lib/kitchen-ansible/version.rb
76
+ - lib/kitchen/provisioner/ansible_push.rb
77
+ - bin/kitchen-ansible-inventory
78
+ homepage: https://github.com/ahelal/kitchen-ansiblepush
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project: '[none]'
98
+ rubygems_version: 2.0.14
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: ansible provisioner for test-kitchen
102
+ test_files: []