chef_handler_foreman 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5f732d147f13678919bd92180c402f03a1557cd3
4
+ data.tar.gz: 5fb453eeffa51588e2931ea9ebd9ee4d24075902
5
+ SHA512:
6
+ metadata.gz: 34dd72612b5e564bddd19caea1f5cacedc4ad98c111787185940737f665b0faa6a41684eb5e734eebbc0b1fb149581e6cffa694530ba5242383cda7b00ee2c08
7
+ data.tar.gz: a50c75a58979130d250e2ddaf97073458937649099e73ea6307695e4f58031f33ea3011e087b0d016a1a79e003d8bdbd0aa23fe0627d5d8776df7719065c1ac7
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
19
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chef_handler_foreman.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Marek Hulan
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Description
2
+
3
+ This gem adds Chef report and attributes handlers that send reports to TheForeman Project.
4
+ You need Foreman 1.3+ to use it.
5
+ See: http://www.theforeman.org
6
+
7
+ ## Installation
8
+
9
+
10
+ Since it's released as a gem you can simply run
11
+ ```sh
12
+ # gem install chef_foreman_handler
13
+ ```
14
+ ## Usage:
15
+
16
+ In /etc/chef/config.rb:
17
+
18
+ ```ruby
19
+ # this adds new functions to chef configuration
20
+ require 'chef_handler_foreman'
21
+ # here you can specify your connection options
22
+ foreman_server_options :url => 'http://your.server/foreman'
23
+ # add following line if you want to upload node attributes (facts in Foreman language)
24
+ foreman_facts_upload true
25
+ # add following line if you want to upload reports
26
+ foreman_reports_upload true
27
+ ```
28
+
29
+ You can also specify a second argument to foreman_reports_upload which is a number:
30
+ - 1 (default) for reporter based on more detailed ResourceReporter
31
+ - 2 not so verbose based just on run_status, actually just counts applied resources
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chef_handler_foreman/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "chef_handler_foreman"
8
+ spec.version = ChefHandlerForeman::VERSION
9
+ spec.authors = ["Marek Hulan"]
10
+ spec.email = ["mhulan@redhat.com"]
11
+ spec.description = %q{Chef handlers to integrate with foreman}
12
+ spec.summary = %q{This gem adds chef handlers so your chef-client can upload attributes (facts) and reports to Foreman}
13
+ spec.homepage = "https://github.com/theforeman/chef-handler-foreman"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
@@ -0,0 +1,5 @@
1
+ require "chef_handler_foreman/version"
2
+ require 'chef'
3
+ require 'chef_handler_foreman/foreman_hooks'
4
+
5
+ Chef::Config.send :extend, ChefHandlerForeman::ForemanHooks
@@ -0,0 +1,75 @@
1
+ #This program is free software: you can redistribute it and/or modify
2
+ #it under the terms of the GNU General Public License as published by
3
+ #the Free Software Foundation, either version 3 of the License, or
4
+ #(at your option) any later version.
5
+ #
6
+ #This program is distributed in the hope that it will be useful,
7
+ #but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9
+ #GNU General Public License for more details.
10
+ #
11
+ #You should have received a copy of the GNU General Public License
12
+ #along with this program. If not, see <http://www.gnu.org/licenses/>
13
+
14
+ require 'chef/handler'
15
+
16
+ module ChefHandlerForeman
17
+ class ForemanFacts < Chef::Handler
18
+ attr_accessor :uploader
19
+
20
+ def report
21
+ send_attributes(prepare_facts)
22
+ end
23
+
24
+ private
25
+
26
+ def prepare_facts
27
+ { :name => node.name,
28
+ :facts => plain_attributes.merge({
29
+ :operatingsystem => node.lsb[:id],
30
+ :operatingsystemrelease => node.lsb.release,
31
+ :_timestamp => Time.now,
32
+ :_type => 'foreman_chef'
33
+ })
34
+ }
35
+ end
36
+
37
+ def plain_attributes
38
+ plainify(node.attributes.to_hash).flatten.inject(&:merge)
39
+ end
40
+
41
+ def plainify(hash, prefix = nil)
42
+ result = []
43
+ hash.each_pair do |key, value|
44
+ if value.is_a?(Hash)
45
+ result.push plainify(value, get_key(key, prefix))
46
+ elsif value.is_a?(Array)
47
+ result.push plainify(array_to_hash(value), get_key(key, prefix))
48
+ else
49
+ new = {}
50
+ new[get_key(key, prefix)] = value
51
+ result.push new
52
+ end
53
+ end
54
+ result
55
+ end
56
+
57
+ def array_to_hash(array)
58
+ new = {}
59
+ array.each_with_index { |v, index| new[index.to_s] = v }
60
+ new
61
+ end
62
+
63
+ def get_key(key, prefix)
64
+ [prefix, key].compact.join('::')
65
+ end
66
+
67
+ def send_attributes(attributes)
68
+ if uploader
69
+ uploader.foreman_request('/api/hosts/facts', attributes, node.name)
70
+ else
71
+ Chef::Log.error "No uploader registered for foreman facts, skipping facts upload"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,45 @@
1
+ require 'chef_handler_foreman/foreman_facts'
2
+ require 'chef_handler_foreman/foreman_reporting'
3
+ require 'chef_handler_foreman/foreman_resource_reporter'
4
+ require 'chef_handler_foreman/foreman_uploader'
5
+
6
+ module ChefHandlerForeman
7
+ module ForemanHooks
8
+ # {:url => '', ...}
9
+ def foreman_server_options(options)
10
+ { :client_key => client_key || '/etc/chef/client.pem' }.merge(options)
11
+ @foreman_uploader = ForemanUploader.new(options)
12
+ end
13
+
14
+ def foreman_facts_upload(upload)
15
+ if upload
16
+ foreman_facts_handler = ForemanFacts.new
17
+ foreman_facts_handler.uploader = @foreman_uploader
18
+ report_handlers << foreman_facts_handler
19
+ exception_handlers << foreman_facts_handler
20
+ end
21
+ end
22
+
23
+ def foreman_reports_upload(upload, mode = 1)
24
+ if upload
25
+ case mode
26
+ when 1
27
+ foreman_reporter = ForemanResourceReporter.new(nil)
28
+ foreman_reporter.uploader = @foreman_uploader
29
+ if Chef::Config[:event_handlers].is_a?(Array)
30
+ Chef::Config[:event_handlers].push foreman_reporter
31
+ else
32
+ Chef::Config[:event_handlers] = [foreman_reporter]
33
+ end
34
+ when 2
35
+ foreman_handler = ForemanReporting.new
36
+ foreman_handler.uploader = uploader
37
+ report_handlers << foreman_handler
38
+ exception_handlers << foreman_handler
39
+ else
40
+ raise ArgumentError, 'unknown mode: ' + mode.to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,90 @@
1
+ #This program is free software: you can redistribute it and/or modify
2
+ #it under the terms of the GNU General Public License as published by
3
+ #the Free Software Foundation, either version 3 of the License, or
4
+ #(at your option) any later version.
5
+ #
6
+ #This program is distributed in the hope that it will be useful,
7
+ #but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9
+ #GNU General Public License for more details.
10
+ #
11
+ #You should have received a copy of the GNU General Public License
12
+ #along with this program. If not, see <http://www.gnu.org/licenses/>
13
+
14
+ require 'chef/handler'
15
+
16
+ module ChefHandlerForeman
17
+ class ForemanReporting < ::Chef::Handler
18
+ attr_accessor :uploader
19
+
20
+ def report
21
+ report = { 'host' => node.fqdn, 'reported_at' => Time.now.utc.to_s }
22
+ report_status = Hash.new(0)
23
+
24
+
25
+ report_status['failed'] = 1 if failed?
26
+ report_status['applied'] = run_status.updated_resources.count
27
+ report['status'] = report_status
28
+
29
+ # I can't compute much metrics for now
30
+ metrics = {}
31
+ metrics['resources'] = { 'total' => run_status.all_resources.count }
32
+
33
+ times = {}
34
+ run_status.all_resources.each do |resource|
35
+ resource_name = resource.resource_name
36
+ if times[resource_name].nil?
37
+ times[resource_name] = resource.elapsed_time
38
+ else
39
+ times[resource_name] += resource.elapsed_time
40
+ end
41
+ end
42
+ metrics['time'] = times.merge!({ 'total' => run_status.elapsed_time })
43
+ report['metrics'] = metrics
44
+
45
+ logs = []
46
+ run_status.updated_resources.each do |resource|
47
+ l = { 'log' => { 'sources' => {}, 'messages' => {}, 'level' => 'notice' } }
48
+
49
+ case resource.resource_name.to_s
50
+ when 'template', 'cookbook_file'
51
+ message = resource.diff
52
+ when 'package'
53
+ message = "Installed #{resource.package_name} package in #{resource.version}"
54
+ else
55
+ message = resource.action.to_s
56
+ end
57
+ l['log']['messages']['message'] = message
58
+ l['log']['sources']['source'] = [resource.resource_name.to_s, resource.name].join(' ')
59
+ #Chef::Log.info("Diff is #{l['log']['messages']['message']}")
60
+ logs << l
61
+ end
62
+
63
+ # I only set failed to 1 if chef run failed
64
+ if failed?
65
+ logs << {
66
+ 'log' => {
67
+ 'sources' => { 'source' => 'chef' },
68
+ 'messages' => { 'message' => run_status.exception },
69
+ 'level' => 'err' }
70
+ }
71
+ end
72
+
73
+ report['logs'] = logs
74
+ full_report = { 'report' => report }
75
+
76
+ send_report(full_report)
77
+ end
78
+
79
+ private
80
+
81
+ def send_report(report)
82
+ if uploader
83
+ uploader.foreman_request('/api/reports', report, node.name)
84
+ else
85
+ Chef::Log.error "No uploader registered for foreman reporting, skipping report upload"
86
+ end
87
+
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,155 @@
1
+ module ChefHandlerForeman
2
+ class ForemanResourceReporter < ::Chef::ResourceReporter
3
+ attr_accessor :uploader
4
+
5
+ def initialize(*args)
6
+ @total_up_to_date = 0
7
+ @total_skipped = 0
8
+ @total_updated = 0
9
+ @total_failed = 0
10
+ @total_restarted = 0
11
+ @total_failed_restart = 0
12
+ @all_resources = []
13
+ super
14
+ end
15
+
16
+ def run_started(run_status)
17
+ @run_status = run_status
18
+ end
19
+
20
+ def run_completed(node)
21
+ @status = "success"
22
+ post_reporting_data
23
+ end
24
+
25
+ def run_failed(exception)
26
+ @exception = exception
27
+ @status = "failure"
28
+ # If we failed before we received the run_started callback, there's not much we can do
29
+ # in terms of reporting
30
+ if @run_status
31
+ post_reporting_data
32
+ end
33
+ end
34
+
35
+ def resource_current_state_loaded(new_resource, action, current_resource)
36
+ super
37
+ @all_resources.push @pending_update unless @pending_update.nil?
38
+ end
39
+
40
+
41
+ def resource_up_to_date(new_resource, action)
42
+ @total_up_to_date += 1
43
+ super
44
+ end
45
+
46
+ def resource_skipped(resource, action, conditional)
47
+ @total_skipped += 1
48
+ super
49
+ end
50
+
51
+ def resource_updated(new_resource, action)
52
+ @total_updated += 1
53
+ @total_restarted += 1 if action.to_s == 'restart'
54
+ super
55
+ end
56
+
57
+ def resource_failed(new_resource, action, exception)
58
+ @total_failed += 1
59
+ @total_failed_restart += 1 if action.to_s == 'restart'
60
+ super
61
+ end
62
+
63
+ def resource_completed(new_resource)
64
+ if @pending_update && !nested_resource?(new_resource)
65
+ @pending_update.finish
66
+ @updated_resources << @pending_update
67
+ @pending_update = nil
68
+ end
69
+ end
70
+
71
+ def post_reporting_data
72
+ if reporting_enabled?
73
+ run_data = prepare_run_data
74
+ Chef::Log.info("Sending resource update report to foreman (run-id: #{@run_id})")
75
+ Chef::Log.debug run_data.inspect
76
+ begin
77
+ Chef::Log.debug("Sending data...")
78
+ if uploader
79
+ uploader.foreman_request('/api/reports', { "report" => run_data }, node.name)
80
+ else
81
+ Chef::Log.error "No uploader registered for foreman reporting, skipping report upload"
82
+ end
83
+ rescue => e
84
+ Chef::Log.error "Sending failed with #{e.class} #{e.message}"
85
+ Chef::Log.error e.backtrace.join("\n")
86
+ end
87
+ else
88
+ Chef::Log.debug("Reporting disabled, skipping report upload")
89
+ end
90
+ end
91
+
92
+ def prepare_run_data
93
+ run_data = {}
94
+ run_data["host"] = node_name
95
+ run_data["reported_at"] = end_time.to_s
96
+ run_data["status"] = resources_per_status
97
+
98
+ run_data["metrics"] = {
99
+ "resources" => { "total" => @total_res_count },
100
+ "time" => resources_per_time
101
+ }
102
+
103
+ run_data["logs"] = resources_logs + [chef_log]
104
+ run_data
105
+ end
106
+
107
+ def resources_per_status
108
+ { "applied" => @total_updated,
109
+ "restarted" => @total_restarted,
110
+ "failed" => @total_failed,
111
+ "failed_restarts" => @total_failed_restart,
112
+ "skipped" => @total_skipped,
113
+ "pending" => 0
114
+ }
115
+ end
116
+
117
+ def resources_per_time
118
+ @run_status.all_resources.inject({}) do |memo, resource|
119
+ name, time = resource.resource_name.to_s, resource.elapsed_time || 0
120
+ memo[name] = memo[name] ? memo[name] + time : time
121
+ memo
122
+ end
123
+ end
124
+
125
+ def resources_logs
126
+ @all_resources.map do |resource|
127
+ action = resource.new_resource.action
128
+ message = action.is_a?(Array) ? action.first.to_s : action.to_s
129
+ message += " (#{resource.exception.class} #{resource.exception.message})" unless resource.exception.nil?
130
+ { "log" => {
131
+ "sources" => { "source" => resource.new_resource.to_s },
132
+ "messages" => { "message" => message },
133
+ "level" => resource.exception.nil? ? "notice" : 'err'
134
+ } }
135
+ end
136
+ end
137
+
138
+ def chef_log
139
+ message = 'run'
140
+ if @status == 'success' && exception.nil?
141
+ level = 'notice'
142
+ else
143
+ message += " (#{exception.class} #{exception.message})"
144
+ level = 'err'
145
+ end
146
+
147
+ { "log" => {
148
+ "sources" => { "source" => 'Chef' },
149
+ "messages" => { "message" => message },
150
+ "level" => level
151
+ } }
152
+ end
153
+
154
+ end
155
+ end
@@ -0,0 +1,64 @@
1
+ #This program is free software: you can redistribute it and/or modify
2
+ #it under the terms of the GNU General Public License as published by
3
+ #the Free Software Foundation, either version 3 of the License, or
4
+ #(at your option) any later version.
5
+ #
6
+ #This program is distributed in the hope that it will be useful,
7
+ #but WITHOUT ANY WARRANTY; without even the implied warranty of
8
+ #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9
+ #GNU General Public License for more details.
10
+ #
11
+ #You should have received a copy of the GNU General Public License
12
+ #along with this program. If not, see <http://www.gnu.org/licenses/>
13
+
14
+ require 'net/http'
15
+ require 'net/https'
16
+ require 'uri'
17
+ require 'openssl'
18
+ require 'digest/sha2'
19
+ require 'base64'
20
+
21
+ module ChefHandlerForeman
22
+ class ForemanUploader
23
+ attr_reader :options
24
+
25
+ def initialize(opts)
26
+ @options = opts
27
+ end
28
+
29
+ def foreman_request(path, body, client_name)
30
+ uri = URI.parse(options[:url])
31
+ http = Net::HTTP.new(uri.host, uri.port)
32
+ http.use_ssl = uri.scheme == 'https'
33
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
34
+
35
+ if http.use_ssl?
36
+ if options[:foreman_ssl_ca] && !options[:foreman_ssl_ca].empty?
37
+ http.ca_file = options[:foreman_ssl_ca]
38
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
39
+ end
40
+
41
+ if options[:foreman_ssl_cert] && !options[:foreman_ssl_cert].empty? && options[:foreman_ssl_key] && !options[:foreman_ssl_key].empty?
42
+ http.cert = OpenSSL::X509::Certificate.new(File.read(options[:foreman_ssl_cert]))
43
+ http.key = OpenSSL::PKey::RSA.new(File.read(options[:foreman_ssl_key]), nil)
44
+ end
45
+ end
46
+
47
+ req = Net::HTTP::Post.new("#{uri.path}/#{path}")
48
+ req.add_field('Accept', 'application/json,version=2')
49
+ req.content_type = 'application/json'
50
+ body_json = body.to_json
51
+ req.body = body_json
52
+ req.add_field('X-Foreman-Signature', sign_request(body_json, options[:client_key]))
53
+ req.add_field('X-Foreman-Client', client_name)
54
+ response = http.request(req)
55
+ end
56
+
57
+ def sign_request(body_json, key_path)
58
+ hash_body = Digest::SHA256.hexdigest(body_json)
59
+ key = OpenSSL::PKey::RSA.new(File.read(key_path))
60
+ # Base64.encode64 is adding \n in the string
61
+ signature = Base64.encode64(key.sign(OpenSSL::Digest::SHA256.new, hash_body)).gsub("\n",'')
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module ChefHandlerForeman
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chef_handler_foreman
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Marek Hulan
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
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
+ description: Chef handlers to integrate with foreman
42
+ email:
43
+ - mhulan@redhat.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - .gitignore
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - chef_handler_foreman.gemspec
54
+ - lib/chef_handler_foreman.rb
55
+ - lib/chef_handler_foreman/foreman_facts.rb
56
+ - lib/chef_handler_foreman/foreman_hooks.rb
57
+ - lib/chef_handler_foreman/foreman_reporting.rb
58
+ - lib/chef_handler_foreman/foreman_resource_reporter.rb
59
+ - lib/chef_handler_foreman/foreman_uploader.rb
60
+ - lib/chef_handler_foreman/version.rb
61
+ homepage: https://github.com/theforeman/chef-handler-foreman
62
+ licenses:
63
+ - MIT
64
+ metadata: {}
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.0.3
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: This gem adds chef handlers so your chef-client can upload attributes (facts)
85
+ and reports to Foreman
86
+ test_files: []
87
+ has_rdoc: