smart_proxy_abrt 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.
data/README ADDED
@@ -0,0 +1,4 @@
1
+ This is an ABRT plugin for Foreman's smart proxy. Please see the README of the
2
+ Foreman ABRT plugin for more information:
3
+
4
+ https://github.com/abrt/foreman_abrt/blob/master/README.md
@@ -0,0 +1,25 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ # Test for 1.9
5
+ if (RUBY_VERSION.split('.').map{|s|s.to_i} <=> [1,9,0]) > 0 then
6
+ PLATFORM = RUBY_PLATFORM
7
+ end
8
+
9
+ desc 'Default: run unit tests.'
10
+ task :default => :test
11
+
12
+ desc 'Test the Foreman Proxy plugin.'
13
+ Rake::TestTask.new(:test) do |t|
14
+ t.libs << '.'
15
+ t.libs << 'lib'
16
+ t.libs << 'test'
17
+ files = FileList['test/**/*_test.rb']
18
+ if PLATFORM =~ /mingw/
19
+ files = FileList['test/**/server_ms_test*']
20
+ else
21
+ files = FileList['test/**/*_test.rb'].delete_if{|f| f =~ /_ms_/}
22
+ end
23
+ t.test_files = files
24
+ t.verbose = true
25
+ end
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift '/usr/share/foreman-proxy/lib' #XXX!
4
+ $LOAD_PATH.unshift '/usr/share/foreman-proxy/modules' #XXX!
5
+
6
+ # We rely on the main smart_proxy module to initialize the plugins so that we
7
+ # can access our module's settings. Also used from smart-proxy code: global
8
+ # settings, logging, foreman requests.
9
+ require 'smart_proxy'
10
+
11
+ require 'smart_proxy_abrt'
12
+ require 'smart_proxy_abrt/abrt_lib'
13
+
14
+ # Substitute our own logger so that we don't log to the main smart-proxy logfile.
15
+ module Proxy
16
+ module Log
17
+ @@logger = ::Logger.new(Proxy::Abrt::Plugin.settings.abrt_send_log_file, 6, 1024*1024*10)
18
+ @@logger.level = ::Logger.const_get(Proxy::SETTINGS.log_level.upcase)
19
+ end
20
+ end
21
+ include Proxy::Log
22
+
23
+ # Don't run if ABRT plugin is disabled.
24
+ exit unless Proxy::Abrt::Plugin.settings.enabled == true
25
+
26
+ if !Proxy::SETTINGS.foreman_url
27
+ logger.error "Foreman URL not configured"
28
+ exit false
29
+ end
30
+
31
+ begin
32
+ require 'satyr' if Proxy::Abrt::Plugin.settings.aggregate_reports
33
+ rescue LoadError
34
+ logger.error "The satyr gem required for report aggregation was not found. "\
35
+ "You need to either install it or disable the aggregation."
36
+ end
37
+
38
+ def send_reports_from_spool
39
+ reports_by_host = {}
40
+
41
+ # load reports from disk
42
+ reports = Proxy::Abrt::HostReport.load_from_spool
43
+
44
+ # aggregate reports by host and by duplication hash if possible
45
+ reports.each do |report|
46
+ if reports_by_host.has_key? report.host
47
+ begin
48
+ reports_by_host[report.host].merge report
49
+ rescue StandardError => e
50
+ logger.error "Failed to merge #{report.files[0]} " \
51
+ "into #{reports_by_host[report.host].files}: #{e}"
52
+ end
53
+ else
54
+ reports_by_host[report.host] = report
55
+ end
56
+ end
57
+
58
+ # send reports
59
+ reports_by_host.each_value do |hr|
60
+ begin
61
+ result = hr.send_to_foreman
62
+ rescue StandardError => e
63
+ logger.error "Unable to forward to Foreman server: #{e}"
64
+ next
65
+ end
66
+ unless result.is_a? Net::HTTPSuccess
67
+ logger.error "Foreman server rejected report (status #{result.code}): #{result.body}"
68
+ end
69
+ begin
70
+ hr.unlink
71
+ rescue StandardError => e
72
+ logger.error "Cannot delete #{hr.files}: #{e}"
73
+ end
74
+ end
75
+ end
76
+
77
+ if ARGV[0] == "--daemon"
78
+ if PLATFORM =~ /mingw/
79
+ puts "Daemon mode is not supported on Windows."
80
+ exit false
81
+ end
82
+ require 'daemon'
83
+ Process.daemon true
84
+
85
+ sleep_interval = Proxy::Abrt::Plugin.settings.abrt_send_period || 600
86
+ loop do
87
+ send_reports_from_spool
88
+ sleep sleep_interval
89
+ end
90
+ else
91
+ send_reports_from_spool
92
+ end
@@ -0,0 +1,6 @@
1
+ # Drop this file into bundler.d/ in smart-proxy root
2
+ gem 'smart_proxy_abrt'
3
+
4
+ group :abrt do
5
+ # plugin dependencies go here
6
+ end
@@ -0,0 +1,2 @@
1
+ # Send collected ABRT reports once every 10 minutes
2
+ */30 * * * * foreman-proxy smart-proxy-abrt-send
@@ -0,0 +1,110 @@
1
+ %global gem_name smart_proxy_abrt
2
+
3
+ %global foreman_proxy_bundlerd_dir /usr/share/foreman-proxy/bundler.d
4
+ %global foreman_proxy_pluginconf_dir /etc/foreman-proxy/settings.d
5
+ %global spool_dir /var/spool/foreman-proxy-abrt
6
+
7
+ Name: rubygem-%{gem_name}
8
+ Version: 0.0.1
9
+ Release: 1%{?dist}
10
+ Summary: Automatic Bug Reporting Tool plugin for Foreman's smart proxy
11
+ Group: Applications/Internet
12
+ License: GPLv3
13
+ URL: http://github.com/abrt/smart-proxy-abrt
14
+ Source0: https://fedorahosted.org/released/abrt/%{gem_name}-%{version}.gem
15
+ Requires: ruby(release)
16
+ Requires: ruby(rubygems)
17
+ Requires: rubygem(ffi)
18
+ Requires: foreman-proxy >= 1.6.0
19
+ Requires: crontabs
20
+ ## does not exist in repository yet
21
+ #Requires: rubygem-satyr
22
+ BuildRequires: ruby(release)
23
+ BuildRequires: rubygems-devel
24
+ BuildRequires: ruby
25
+ BuildRequires: rubygem(ffi)
26
+ BuildRequires: rubygem(minitest)
27
+ BuildArch: noarch
28
+ Provides: rubygem(%{gem_name}) = %{version}
29
+
30
+ %description
31
+ This smart proxy plugin, together with a Foreman plugin, add the capability to
32
+ send ABRT micro-reports from your managed hosts to Foreman.
33
+
34
+ %package doc
35
+ Summary: Documentation for %{name}
36
+ Group: Documentation
37
+ Requires:%{name} = %{version}-%{release}
38
+
39
+ %description doc
40
+ Documentation for %{name}
41
+
42
+ %prep
43
+ gem unpack %{SOURCE0}
44
+ %setup -q -D -T -n %{gem_name}-%{version}
45
+ gem spec %{SOURCE0} -l --ruby > %{gem_name}.gemspec
46
+
47
+ %build
48
+ # Create the gem as gem install only works on a gem file
49
+ gem build %{gem_name}.gemspec
50
+
51
+ # %%gem_install compiles any C extensions and installs the gem into ./%gem_dir
52
+ # by default, so that we can move it into the buildroot in %%install
53
+ %gem_install
54
+
55
+ %install
56
+ # Packaging guidelines say: Do not ship tests
57
+ rm -r .%{gem_instdir}/test .%{gem_instdir}/Rakefile
58
+ rm .%{gem_instdir}/extra/*.spec
59
+
60
+ mkdir -p %{buildroot}%{gem_dir}
61
+ cp -a .%{gem_dir}/* \
62
+ %{buildroot}%{gem_dir}/
63
+
64
+ # executables
65
+ mkdir -p %{buildroot}%{_bindir}
66
+ cp -a .%{_bindir}/* \
67
+ %{buildroot}%{_bindir}
68
+
69
+ # bundler file
70
+ mkdir -p %{buildroot}%{foreman_proxy_bundlerd_dir}
71
+ mv %{buildroot}%{gem_instdir}/bundler.d/abrt.rb \
72
+ %{buildroot}%{foreman_proxy_bundlerd_dir}
73
+
74
+ # sample config
75
+ mkdir -p %{buildroot}%{foreman_proxy_pluginconf_dir}
76
+ mv %{buildroot}%{gem_instdir}/settings.d/abrt.yml.example \
77
+ %{buildroot}%{foreman_proxy_pluginconf_dir}/
78
+
79
+ # crontab
80
+ mkdir -p %{buildroot}%{_sysconfdir}/cron.d/
81
+ mv %{buildroot}%{gem_instdir}/extra/foreman-proxy-abrt-send.cron \
82
+ %{buildroot}%{_sysconfdir}/cron.d/%{name}
83
+
84
+ # create spool directory
85
+ mkdir -p %{buildroot}%{spool_dir}
86
+
87
+ #%check
88
+ #testrb -Ilib test
89
+
90
+ %files
91
+ %dir %{gem_instdir}
92
+ %{gem_libdir}
93
+ %exclude %{gem_cache}
94
+ %{gem_spec}
95
+ %{gem_instdir}/bin
96
+
97
+ %dir %attr(0755, foreman-proxy, foreman-proxy) %{spool_dir}
98
+ %{foreman_proxy_bundlerd_dir}/abrt.rb
99
+ %{_bindir}/smart-proxy-abrt-send
100
+ %doc %{foreman_proxy_pluginconf_dir}/abrt.yml.example
101
+ %config(noreplace) %{_sysconfdir}/cron.d/%{name}
102
+
103
+ %files doc
104
+ %{gem_docdir}
105
+ %{gem_instdir}/README
106
+ %{gem_instdir}/LICENSE
107
+
108
+ %changelog
109
+ * Tue Jul 15 2014 Martin Milata <mmilata@redhat.com> - 0.0.1-1
110
+ - Initial package
@@ -0,0 +1,3 @@
1
+ require 'smart_proxy_abrt/abrt_plugin'
2
+
3
+ module Proxy::Abrt; end
@@ -0,0 +1,76 @@
1
+ require 'openssl'
2
+ require 'json'
3
+
4
+ require 'smart_proxy_abrt/abrt_lib'
5
+
6
+ STATUS_ACCEPTED = 202
7
+
8
+ module Proxy::Abrt
9
+ class Api < ::Sinatra::Base
10
+ include ::Proxy::Log
11
+ helpers ::Proxy::Helpers
12
+
13
+ post '/reports/new/' do
14
+ begin
15
+ cn = Proxy::Abrt::common_name request
16
+ rescue Proxy::Error::Unauthorized => e
17
+ log_halt 403, "Client authentication failed: #{e.message}"
18
+ end
19
+
20
+ ureport_json = request['file'][:tempfile].read
21
+ ureport = JSON.parse(ureport_json)
22
+
23
+ #forward to FAF
24
+ response = nil
25
+ if Proxy::Abrt::Plugin.settings.server_url
26
+ begin
27
+ result = Proxy::Abrt::faf_request "/reports/new/", ureport_json
28
+ response = result.body if result.code.to_s == STATUS_ACCEPTED.to_s
29
+ rescue StandardError => e
30
+ logger.error "Unable to forward to ABRT server: #{e}"
31
+ end
32
+ end
33
+ unless response
34
+ # forwarding is not configured or failed
35
+ # FAF source that generates replies is in src/webfaf/reports/views.py
36
+ response = { "result" => false,
37
+ "message" => "Report queued" }
38
+ if Proxy::SETTINGS.foreman_url
39
+ foreman_url = Proxy::SETTINGS.foreman_url
40
+ foreman_url += "/" if url[-1] != "/"
41
+ foreman_url += "hosts/#{cn}/abrt_reports"
42
+ response["reported_to"] = [{ "reporter" => "Foreman",
43
+ "type" => "url",
44
+ "value" => foreman_url }]
45
+ end
46
+ response = response.to_json
47
+ end
48
+
49
+ #save report to disk
50
+ begin
51
+ Proxy::Abrt::HostReport.save cn, ureport
52
+ rescue StandardError => e
53
+ log_halt 500, "Failed to save the report: #{e}"
54
+ end
55
+
56
+ status STATUS_ACCEPTED
57
+ response
58
+ end
59
+
60
+ post '/reports/:action/' do
61
+ # pass through to real FAF if configured
62
+ if Proxy::Abrt::Plugin.settings.server_url
63
+ body = request['file'][:tempfile].read
64
+ begin
65
+ result = Proxy::Abrt::faf_request "/reports/#{params[:action]}/", body
66
+ rescue StandardError => e
67
+ log_halt 503, "ABRT server unavailable: #{e}"
68
+ end
69
+ status result.code
70
+ result.body
71
+ else
72
+ log_halt 501, "foreman-proxy does not implement /reports/#{params[:action]}/"
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,244 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'time'
5
+ require 'fileutils'
6
+
7
+ require 'proxy/log'
8
+ require 'proxy/request'
9
+
10
+ module Proxy::Abrt
11
+ def self.random_alpha_string(length)
12
+ base = ('a'..'z').to_a
13
+ result = ""
14
+ length.times { result << base[rand(base.size)] }
15
+ result
16
+ end
17
+
18
+ # Generate multipart boundary separator
19
+ def self.suggest_separator
20
+ separator = "-"*28
21
+ separator + self.random_alpha_string(16)
22
+ end
23
+
24
+ # It seems that Net::HTTP does not support multipart/form-data - this function
25
+ # is adapted from http://stackoverflow.com/a/213276 and lib/proxy/request.rb
26
+ def self.form_data_file(content, file_content_type)
27
+ # Assemble the request body using the special multipart format
28
+ thepart = "Content-Disposition: form-data; name=\"file\"; filename=\"*buffer*\"\r\n" +
29
+ "Content-Type: #{ file_content_type }\r\n\r\n#{ content }\r\n"
30
+
31
+ boundary = self.suggest_separator
32
+ while thepart.include? boundary
33
+ boundary = self.suggest_separator
34
+ end
35
+
36
+ body = "--" + boundary + "\r\n" + thepart + "--" + boundary + "--\r\n"
37
+ headers = {
38
+ "User-Agent" => "foreman-proxy/#{Proxy::VERSION}",
39
+ "Content-Type" => "multipart/form-data; boundary=#{ boundary }",
40
+ "Content-Length" => body.length.to_s
41
+ }
42
+
43
+ return headers, body
44
+ end
45
+
46
+ def self.faf_request(path, content, content_type="application/json")
47
+ uri = URI.parse(Proxy::Abrt::Plugin.settings.server_url.to_s)
48
+ http = Net::HTTP.new(uri.host, uri.port)
49
+ http.use_ssl = uri.scheme == 'https'
50
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
51
+
52
+ if Proxy::Abrt::Plugin.settings.server_ssl_noverify
53
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
54
+ end
55
+
56
+ if Proxy::Abrt::Plugin.settings.server_ssl_cert && !Proxy::Abrt::Plugin.settings.server_ssl_cert.to_s.empty? \
57
+ && Proxy::Abrt::Plugin.settings.server_ssl_key && !Proxy::Abrt::Plugin.settings.server_ssl_key.to_s.empty?
58
+ http.cert = OpenSSL::X509::Certificate.new(File.read(Proxy::Abrt::Plugin.settings.server_ssl_cert))
59
+ http.key = OpenSSL::PKey::RSA.new(File.read(Proxy::Abrt::Plugin.settings.server_ssl_key), nil)
60
+ end
61
+
62
+ headers, body = self.form_data_file content, content_type
63
+
64
+ path = [uri.path, path].join unless uri.path.empty?
65
+ response = http.start { |con| con.post(path, body, headers) }
66
+
67
+ response
68
+ end
69
+
70
+ def self.common_name(request)
71
+ client_cert = request.env['SSL_CLIENT_CERT']
72
+ raise Proxy::Error::Unauthorized, "Client certificate required" if client_cert.to_s.empty?
73
+
74
+ begin
75
+ client_cert = OpenSSL::X509::Certificate.new(client_cert)
76
+ rescue OpenSSL::OpenSSLError => e
77
+ raise Proxy::Error::Unauthorized, e.message
78
+ end
79
+
80
+ cn = client_cert.subject.to_a.detect { |name, value| name == 'CN' }
81
+ cn = cn[1] unless cn.nil?
82
+ raise Proxy::Error::Unauthorized, "Common Name not found in the certificate" unless cn
83
+
84
+ return cn
85
+ end
86
+
87
+ class HostReport
88
+ include Proxy::Log
89
+
90
+ class AggregatedReport
91
+ attr_accessor :report, :count, :hash, :reported_at
92
+ def initialize(report, count, hash, reported_at)
93
+ @report = report
94
+ @count = count
95
+ @hash = hash
96
+ @reported_at = Time.parse reported_at
97
+ end
98
+ end
99
+
100
+ class Error < RuntimeError; end
101
+
102
+ attr_reader :host, :reports, :files, :by_hash
103
+
104
+ def initialize(fname)
105
+ contents = IO.read(fname)
106
+ json = JSON.parse(contents)
107
+
108
+ report = json["report"]
109
+ hash = HostReport.duphash report
110
+ ar = AggregatedReport.new(json["report"], 1, hash, json["reported_at"])
111
+ @reports = [ar]
112
+ # index the array elements by duphash, if they have one
113
+ @by_hash = {}
114
+ @by_hash[hash] = ar unless hash.nil?
115
+ @files = [fname]
116
+ @host = json["host"]
117
+ end
118
+
119
+ def merge(other)
120
+ raise HostReport::Error, "Host names do not match" unless @host == other.host
121
+
122
+ other.reports.each do |ar|
123
+ if !ar.hash.nil? && @by_hash.has_key?(ar.hash)
124
+ # we already have this report, just increment the counter
125
+ found_report = @by_hash[ar.hash]
126
+ found_report.count += ar.count
127
+ found_report.reported_at = [found_report.reported_at, ar.reported_at].min
128
+ else
129
+ # we either don't have this report or it has no hash
130
+ @reports << ar
131
+ @by_hash[ar.hash] = ar unless ar.hash.nil?
132
+ end
133
+ end
134
+ @files += other.files
135
+ end
136
+
137
+ def send_to_foreman
138
+ foreman_report = create_foreman_report
139
+ logger.debug "Sending #{foreman_report}"
140
+ Proxy::HttpRequest::ForemanRequest.new.send_request("/api/abrt_reports", foreman_report.to_json)
141
+ end
142
+
143
+ def unlink
144
+ @files.each do |fname|
145
+ logger.debug "Deleting #{fname}"
146
+ File.unlink(fname)
147
+ end
148
+ end
149
+
150
+ def self.save(host, report, reported_at=nil)
151
+ # create the spool dir if it does not exist
152
+ FileUtils.mkdir_p HostReport.spooldir
153
+
154
+ reported_at ||= Time.now.utc
155
+ on_disk_report = { "host" => host, "report" => report , "reported_at" => reported_at.to_s }
156
+
157
+ # write report to temporary file
158
+ temp_fname = with_unique_filename "new-" do |temp_fname|
159
+ File.open temp_fname, File::WRONLY|File::CREAT|File::EXCL do |tmpfile|
160
+ tmpfile.write(on_disk_report.to_json)
161
+ end
162
+ end
163
+
164
+ # rename it
165
+ with_unique_filename ("ureport-" + DateTime.now.iso8601 + "-") do |final_fname|
166
+ File.link temp_fname, final_fname
167
+ File.unlink temp_fname
168
+ end
169
+ end
170
+
171
+ def self.load_from_spool
172
+ reports = []
173
+ report_files = Dir[File.join(HostReport.spooldir, "ureport-*")]
174
+ report_files.each do |fname|
175
+ begin
176
+ reports << new(fname)
177
+ rescue StandardError => e
178
+ logger.error "Failed to parse report #{fname}: #{e}"
179
+ end
180
+ end
181
+ reports
182
+ end
183
+
184
+ private
185
+
186
+ def format_reports
187
+ @reports.collect do |ar|
188
+ r = {
189
+ "count" => ar.count,
190
+ "reported_at" => ar.reported_at.utc.to_s,
191
+ "full" => ar.report
192
+ }
193
+ r["duphash"] = ar.hash unless ar.hash.nil?
194
+ r
195
+ end
196
+ end
197
+
198
+ # http://projects.theforeman.org/projects/foreman/wiki/Json-report-format
199
+ # To be replaced once Foreman understands other report types than from Puppet.
200
+ def create_foreman_report
201
+ { "abrt_report" => {
202
+ "host" => @host,
203
+ "reports" => format_reports
204
+ }
205
+ }
206
+ end
207
+
208
+ def self.duphash(report)
209
+ return nil if !Proxy::Abrt::Plugin.settings.aggregate_reports
210
+
211
+ begin
212
+ satyr_report = Satyr::Report.new report.to_json
213
+ stacktrace = satyr_report.stacktrace
214
+ thread = stacktrace.find_crash_thread
215
+ thread.duphash
216
+ rescue StandardError => e
217
+ logger.error "Error computing duphash: #{e}"
218
+ nil
219
+ end
220
+ end
221
+
222
+ def self.unique_filename(prefix)
223
+ File.join(HostReport.spooldir, prefix + Proxy::Abrt::random_alpha_string(8))
224
+ end
225
+
226
+ def self.with_unique_filename(prefix)
227
+ filename = unique_filename prefix
228
+ tries_left = 5
229
+ begin
230
+ yield filename
231
+ rescue Errno::EEXIST => e
232
+ filename = unique_filename prefix
233
+ tries_left -= 1
234
+ retry if tries_left > 0
235
+ raise HostReport::Error, "Unable to create unique file"
236
+ end
237
+ filename
238
+ end
239
+
240
+ def self.spooldir
241
+ Proxy::Abrt::Plugin.settings.spooldir || File.join(APP_ROOT, "spool/foreman-proxy-abrt")
242
+ end
243
+ end
244
+ end