chef_handler_foreman 0.0.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: 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: