smart_proxy_abrt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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