sabisu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ # api routes
2
+ module Sabisu
3
+ # server class
4
+ class Server
5
+ get '/api/events' do
6
+ params = request.env['rack.request.query_hash']
7
+ events = Event.all(params)
8
+ JSON.pretty_generate(events)
9
+ end
10
+
11
+ get '/api/events/search' do
12
+ params = request.env['rack.request.query_hash']
13
+ if params.key?('query')
14
+ query = params['query']
15
+ params.delete('query')
16
+ else
17
+ return 'Must supply \'query\' parameter'
18
+ end
19
+ events = Event.search(query, params)
20
+ JSON.pretty_generate(events)
21
+ end
22
+
23
+ get '/api/events/stale' do
24
+ params = request.env['rack.request.query_hash']
25
+ stale = Event.stale(params)
26
+ JSON.pretty_generate(stale: stale)
27
+ end
28
+
29
+ get '/api/events/changes' do
30
+ params = request.env['rack.request.query_hash']
31
+ JSON.pretty_generate(CURRENT_DB.changes(params))
32
+ end
33
+
34
+ get '/api/configuration/fields' do
35
+ JSON.pretty_generate(FIELDS)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,13 @@
1
+ # client routes
2
+ module Sabisu
3
+ # server class
4
+ class Server
5
+ get '/clients/:client' do
6
+ haml :client, locals: { client: params[:client] }
7
+ end
8
+
9
+ get '/clients/:client/:check' do
10
+ haml :client_check, locals: { client: params[:client], check: params[:check] }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ # event routes
2
+ module Sabisu
3
+ # server class
4
+ class Server
5
+ get '/events' do
6
+ haml :events
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'events'
2
+ require_relative 'api'
3
+ require_relative 'sensu'
4
+ require_relative 'client'
@@ -0,0 +1,44 @@
1
+ # sensu routes
2
+ module Sabisu
3
+ # server class
4
+ class Server
5
+ def sensu(request)
6
+ sensu = Sensu.new
7
+ tmp_path = request.path_info.split('/')
8
+ tmp_path.delete_at(1)
9
+ path = tmp_path.join('/')
10
+ opts = {
11
+ path: path,
12
+ method: request.request_method,
13
+ ssl: API_SSL
14
+ }
15
+ begin
16
+ opts[:payload] = JSON.parse(request.body.read) if request.post?
17
+ rescue StandardError
18
+ puts "unable to parse: #{request.body.read}"
19
+ end
20
+ sensu.request(opts)
21
+ end
22
+
23
+ route :get, :post, '/sensu/stashes' do
24
+ res = sensu(request)
25
+ status res.code
26
+ headers 'content-type' => 'application/json'
27
+ body res.body
28
+ end
29
+
30
+ delete '/sensu/stashes/*' do
31
+ res = sensu(request)
32
+ status res.code
33
+ headers 'content-type' => 'application/json'
34
+ body res.body
35
+ end
36
+
37
+ post '/sensu/resolve' do
38
+ res = sensu(request)
39
+ status res.code
40
+ headers 'content-type' => 'application/json'
41
+ body res.body
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,67 @@
1
+ ## Heavily borrowed from sensu-cli
2
+ ## https://github.com/agent462/sensu-cli/blob/master/lib/sensu-cli/api.rb
3
+
4
+ require 'net/https'
5
+ require 'json'
6
+
7
+ # make api requests to sensu
8
+ module Sabisu
9
+ class Server
10
+ # sensu passthrough api
11
+ class Sensu
12
+ def request(opts)
13
+ http = Net::HTTP.new(API_URL, API_PORT)
14
+ http.read_timeout = 15
15
+ http.open_timeout = 5
16
+ if API_SSL == true
17
+ http.use_ssl = true
18
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
19
+ end
20
+ proxy_header = { 'api-proxy' => 'true' }
21
+ case opts[:method].upcase
22
+ when 'GET'
23
+ req = Net::HTTP::Get.new(opts[:path], proxy_header)
24
+ when 'DELETE'
25
+ req = Net::HTTP::Delete.new(opts[:path], proxy_header)
26
+ when 'POST'
27
+ req = Net::HTTP::Post.new(
28
+ opts[:path],
29
+ proxy_header.merge!('Content-Type' => 'application/json')
30
+ )
31
+ req.body = opts[:payload].to_json
32
+ end
33
+ req.basic_auth(API_USER, API_PASSWORD) unless API_USER.nil? && API_PASSWORD.nil?
34
+ http.request(req)
35
+ end
36
+
37
+ def response(code, body, command = nil)
38
+ case code
39
+ when '200'
40
+ JSON.parse(body)
41
+ when '201'
42
+ puts 'The stash has been created.' if command == 'stashes' || command == 'silence'
43
+ when '202'
44
+ puts 'The item was submitted for processing.'
45
+ when '204'
46
+ puts 'Sensu is healthy' if command == 'health'
47
+ if command == 'aggregates' || command == 'stashes'
48
+ puts 'The item was successfully deleted.'
49
+ end
50
+ when '400'
51
+ puts 'The payload is malformed.'.color(:red)
52
+ when '401'
53
+ puts 'The request requires user authentication.'.color(:red)
54
+ when '404'
55
+ puts 'The item does not exist.'.color(:cyan)
56
+ else
57
+ if command == 'health'
58
+ puts 'Sensu is not healthy.'.color(:red)
59
+ else
60
+ puts 'There was an error while trying to complete your request. ' +
61
+ "Response code: #{code}".color(:red)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,110 @@
1
+ gem 'thin'
2
+ gem 'sinatra', '1.4.4'
3
+
4
+ # load gems
5
+ require 'sinatra'
6
+ require 'sinatra/base'
7
+ require 'sinatra/multi_route'
8
+ require 'couchrest'
9
+ require 'restclient'
10
+ require 'cgi'
11
+ require 'json'
12
+ require 'pp'
13
+
14
+ # load version number
15
+ require_relative 'version'
16
+
17
+ module Sabisu
18
+ # run sabisu as a server
19
+ class Server < Sinatra::Base
20
+ register Sinatra::MultiRoute
21
+
22
+ # load classes
23
+ require_relative 'sensu'
24
+ require_relative 'event'
25
+
26
+ # load configuration settings
27
+ require_relative 'config'
28
+
29
+ # load routes
30
+ require_relative 'routes/init'
31
+
32
+ before '/:name' do
33
+ unless params[:name] == 'login'
34
+ session[:url] = request.fullpath
35
+ force_session_auth
36
+ end
37
+ end
38
+
39
+ before '/api/*' do
40
+ protected!
41
+ end
42
+
43
+ get '/' do
44
+ redirect '/login'
45
+ end
46
+
47
+ get '/login' do
48
+ if logged_in?
49
+ redirect '/events'
50
+ else
51
+ haml :login, locals: { remember_me: session[:remember_me] }
52
+ end
53
+ end
54
+
55
+ post '/login' do
56
+ if validate(params[:username], params['password'])
57
+ session[:logged_in] = true
58
+ session[:username] = params[:username]
59
+ session[:remember_me] = params[:username] if params[:remember_me] == 'on'
60
+ session[:url] = '/events' unless session.key?(:url)
61
+ redirect session[:url]
62
+ else
63
+ haml :login, locals: { message: 'Incorrect username and/or password' }
64
+ end
65
+ end
66
+
67
+ get '/logout' do
68
+ clear_session
69
+ redirect '/login'
70
+ end
71
+
72
+ helpers do
73
+ def validate(username, password)
74
+ username == UI_USERNAME && password == UI_PASSWORD
75
+ end
76
+
77
+ def logged_in?
78
+ NOAUTH == true || (session[:logged_in] == true && session[:username])
79
+ end
80
+
81
+ def force_session_auth
82
+ if logged_in?
83
+ return true
84
+ else
85
+ redirect '/login'
86
+ return false
87
+ end
88
+ end
89
+
90
+ def clear_session
91
+ session.clear
92
+ end
93
+
94
+ def authorized?
95
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
96
+ @auth.provided? &&
97
+ @auth.basic? &&
98
+ @auth.credentials &&
99
+ @auth.credentials == [UI_USERNAME, UI_PASSWORD]
100
+ end
101
+
102
+ def protected!
103
+ unless authorized? || logged_in?
104
+ response['WWW-Authenticate'] = "Basic realm='Sabisu requires authentication'"
105
+ throw(:halt, [401, "Not authorized\n"])
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,268 @@
1
+ %link{:rel => 'stylesheet', :href => '/css/events.css'}
2
+ %script{:src => '/js/typeahead.js'}
3
+ %script{:src => 'https://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/md5.js'}
4
+
5
+ #eventsController.row{'ng-controller' => 'eventsController'}
6
+ -# last update time in corner
7
+ %span#corner_status
8
+
9
+ -# search box (and sort dropdown)
10
+ #search.col-md-12.pull-right.main-content
11
+ .panel
12
+ #search_field.collapse.in{:role => 'search'}
13
+ .panel-body
14
+ %form.form-inline{:role => 'search'}
15
+ #search_form_group.form-group.col-md-9
16
+ %input#search_input.form-control{:type => 'text', :placeholder => 'Search', 'ng-model' => 'search_field', 'search-typeahead' => ''}
17
+ %small.help-block
18
+ %a{'data-toggle' => 'modal', 'data-target' => '#howdoisearch'} How do I search?
19
+ .form-group.col-md-2
20
+ %select#sort.form-control{'ng-model' => 'sort_field'}
21
+ %option{disabled: true} Sort by
22
+ %option{:value => 'client'} client
23
+ %option{:value => '-client'} client (descending)
24
+ %option{:value => 'check'} check
25
+ %option{:value => '-check'} check (descending)
26
+ %option{:value => 'status'} status
27
+ %option{:value => '-status'} status (descending)
28
+ %option{:value => 'age', :selected => 'selected'} age
29
+ %option{:value => '-age'} age (descending)
30
+ %option{:value => 'issued'} issued
31
+ %option{:value => '-issued'} issued (descending)
32
+ %option{:value => 'output'} output
33
+ %option{:value => '-output'} output (descending)
34
+ %input.btn.btn-success.pull-right{:type => 'submit', :value => 'Search', 'ng-click' => 'updateParams()'}
35
+
36
+ -# left sidebar with stats
37
+ #stats.col-md-3.pull-left.main-content
38
+ .panel
39
+ #stats_field.collapse.in{:role => 'stats'}
40
+ .panel-body{'ng-hide' => 'events.length == 0 || events_spin'}
41
+ -# stats by status
42
+ #stats_status{'ng-hide' => 'events_spin'}
43
+ #totals
44
+ %span.label.label-warning.pointer{'ng-click' => 'appendQuery(1, "status", false)'}= ' - '
45
+ %span.label.label-danger.pointer{'ng-click' => 'appendQuery(2, "status", false)'}= ' - '
46
+ %span.label.label-info.pointer{'ng-click' => 'appendQuery(3, "status", false)'}= ' - '
47
+
48
+ -# stats by field
49
+ #stats_by_count{'ng-repeat' => '(name, stat) in stats'}
50
+ %h5
51
+ {{name | uppercase }} stats
52
+ %table.table.table-striped{'ng-hide' => 'events_spin'}
53
+ %tr
54
+ %td {{name}} name
55
+ %td Quantity
56
+ %tr{'ng-repeat' => 's in stat | slice:0:10'}
57
+ %td.col-md-3
58
+ %span.appendQuery{'ng-click' => 'appendQuery(s[0], name)'}
59
+ {{s[0]}}
60
+ %td.col-md-1.text-center
61
+ {{s[1]}}
62
+
63
+ -# all events
64
+ #events.col-md-9.pull-right.main-content{:style => 'margin-bottom: 100px'}
65
+ .panel
66
+ #events_field.collapse.in{:role => 'events'}
67
+ %ul.list-group
68
+ -# events options
69
+ .progress.progress-striped.active{'ng-show' => 'events_spin', :style => 'margin-top: 50px;'}
70
+ .progress-bar.progress-bar-success{:role => 'progressbar', 'aria-valuenow' => '100', 'aria-valuemin' => '0', 'aria-valuemax' => '100', :style => 'width: 100%'}
71
+ %span.sr-only getting events...
72
+ .text-center{'ng-hide' => 'events.length || events_spin'} No Events
73
+ .pull-right{'ng-hide' => 'events.length == 0 || events_spin'}
74
+ %span.view View
75
+ %select#limit{'ng-model' => 'limit_field'}
76
+ %option{:value => '10'} 10
77
+ %option{:value => '20'} 20
78
+ %option{:value => '50', :selected => "selected"} 50
79
+ %option{:value => '100'} 100
80
+ %option{:value => '200'} 200
81
+ %a#events-accordian-toggle.text-right.pull-left{'ng-click' => 'bulkToggleDetails()', 'ng-hide' => 'events.length == 0 || events_spin'} {{bulk}} all details
82
+
83
+ -# an event
84
+ %br
85
+ %li.list-group-item{'ng-repeat' => 'event in events'}
86
+ .row
87
+ .panel
88
+ -# event header
89
+ .panel-heading.flat-heading.event_line{:panel => '{{event.client.name}}/{{event.check.name}}'}
90
+ .col-md-2
91
+ %h4
92
+ %span.label.pull-right.pointer{'class' => 'label-{{event.color}}', 'ng-click' => 'appendQuery(event.check.status, "status", false)'} {{event.wstatus}}
93
+ %h5.event_title.pull-left
94
+ %span.silenceBtn.glyphicon.glyphicon-volume-off{'ng-show' => 'event.client.silenced', 'ng-click' => 'updateSilenceDetails(event.client.silence_stash)', 'data-toggle' => 'modal', 'data-target' => '#silence_window_mini'}
95
+ %span.appendQuery{'ng-click' => 'appendQuery(event.client.name, "client")'}
96
+ {{event.client.name}}
97
+ %span= ' / '
98
+ %span.silenceBtn.glyphicon.glyphicon-volume-off{'ng-show' => 'event.check.silenced', 'ng-click' => 'updateSilenceDetails(event.check.silence_stash)', 'data-toggle' => 'modal', 'data-target' => '#silence_window_mini'}
99
+ %span.appendQuery{'ng-click' => 'appendQuery(event.check.name, "check")'}
100
+ {{event.check.name}}
101
+ %small.text-muted.muted-output
102
+ \- {{event.check.output}}
103
+ %h5
104
+ %span.glyphicon.glyphicon-collapse-down.toggleBtnIcon.pull-right{'ng-click' => 'toggleDetails(event.id)'}
105
+
106
+ .panel-body.panel-collapse.collapse{:class => '{{event.showdetails}}', :id => '{{event.id}}'}
107
+ -# event actions
108
+ .dropdown.actions.pull-right
109
+ %button.btn.btn-link.dropdown-toggle{'data-toggle' => 'dropdown'}
110
+ Actions
111
+ %span.caret
112
+ %ul.dropdown-menu
113
+ %li
114
+ %a{:href => '#', 'ng-click' => 'resolveEvent(event.client.name, event.check.name)'} Resolve
115
+ %li.divider
116
+ %li.dropdown-header Silence
117
+ %li
118
+ %a{:href => '#', 'ng-click' => 'createSilenceDetails(event.client.name)', 'data-toggle' => 'modal', 'data-target' => '#silence_window'} Client
119
+ %li
120
+ %a{:href => '#', 'ng-click' => 'createSilenceDetails(event.client.name, event.check.name)', 'data-toggle' => 'modal', 'data-target' => '#silence_window'} Check
121
+
122
+ -# event attributes
123
+ %dl.dl-horizontal.col-md-5.pull-left
124
+ %dt.attr_title{'ng-repeat-start' => 'attr in event.attributes.left'} {{attr[0]}}
125
+ %dd.attr_value{'ng-repeat-end' => '', 'ng-bind-html' => 'attr[1]'}
126
+ %dl.dl-horizontal.col-md-5.pull-left
127
+ %dt.attr_title{'ng-repeat-start' => 'attr in event.attributes.right'} {{attr[0]}}
128
+ %dd.attr_value{'ng-repeat-end' => '', 'ng-bind-html' => 'attr[1]'}
129
+
130
+ -# event output
131
+ .col-md-12.pull-left.output
132
+ {{event.check.output}}
133
+
134
+ -# mini silence dialog (for seeing existing silences)
135
+ #silence_window_mini.silence_window.modal.fade
136
+ .modal-dialog
137
+ .modal-content
138
+ .modal-header
139
+ Silence Details for {{silencePath}}
140
+ %button.btn.btn-link.btn-xs.pull-right.close_popover{:type => 'button', 'data-dismiss' => 'modal'}
141
+ %span.glyphicon.glyphicon-remove
142
+ close
143
+
144
+ .modal-body
145
+ %dl.dl-horizontal
146
+ %dt{'ng-show' => 'silenceCreated'} Created
147
+ %dd{'ng-show' => 'silenceCreated'} {{silenceCreated}}
148
+
149
+ %dt{'ng-show' => 'silenceOwner'} Owner
150
+ %dd{'ng-show' => 'silenceOwner'} {{silenceOwner}}
151
+
152
+ %dt{:class => 'text-{{silenceExpirationClass}}', 'ng-show' => 'silenceExpires'} Expires
153
+ %dd{:class => 'text-{{silenceExpirationClass}}', 'ng-show' => 'silenceExpires'} {{silenceExpires}}
154
+
155
+ %dt{'ng-show' => 'silenceReason'} Reason
156
+ %dd{'ng-show' => 'silenceReason'} {{silenceReason}}
157
+
158
+ .modal-footer
159
+ %button.deleteSilenceBtn.btn.btn-danger.btn-sm.pull-right{:type => 'button', 'ng-click' => "deleteSilence('{{silencePath}}')"}
160
+ %span.glyphicon.glyphicon-remove
161
+ Delete
162
+
163
+ -# regular silence dialog (for creating a new silence)
164
+ #silence_window.modal.fade
165
+ .modal-dialog
166
+ .modal-content
167
+ .modal-header
168
+ %button.close.pull-right{:type => 'button', 'data-dismiss' => 'modal', 'aria-hidden' => 'true'}
169
+ %h4.modal-title Silence {{silencePath}}
170
+ .modal-body
171
+ %form#silence_form.form-horizontal{:role => 'form'}
172
+ .form-group.silence_owner.has-feedback
173
+ %label.col-sm-2.control-label{:for => 'owner'} Owner*
174
+ .col-sm-10
175
+ %input#owner.form-control{:type => 'text', :placeholder => 'Joe Smith'}
176
+ .form-group.silence_reason.has-feedback
177
+ %label.col-sm-2.control-label{:for => 'reason'} Reason*
178
+ .col-sm-10
179
+ %textarea#reason.form-control{:placeholder => 'Enter reason here', :rows => '3'}
180
+ .form-group
181
+ %label.col-sm-2.control-label{:for => 'expires'} Expiration*
182
+ .col-sm-10
183
+ .radio
184
+ %label
185
+ %input#resolve{:type => 'radio', :name => 'expiration', :value => 'resolve', :checked => ''}
186
+ On resolve
187
+ %span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"On resolve" will cause this silence to be deleted once the check clears for check, clients require all checks to clear. A minimum of 1 hour is enforced.'}
188
+ .radio
189
+ %label
190
+ %input#timer{:type => 'radio', :name => 'expiration', :value => 'timer'}
191
+ %span.pull-left Timer
192
+ .col-sm-3.silence_timer_val.has-feedback
193
+ %input#timer_val.input-sm.form-control{:type => 'text', :name => 'expirationl', :placeholder => ''}
194
+ %span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"Timer" will cause this silence to be deleted in a predetermined time span. The expires value should be inputted as a number followed by a single character. m = minutes, h = hours, d = days, w = weeks. (examples 15m, 2h, 1d, 5w)'}
195
+ .radio
196
+ %label
197
+ %input#never{:type => 'radio', :name => 'expiration', :value => 'never'}
198
+ Never
199
+ %span.glyphicon.glyphicon-question-sign{'data-toggle' => 'tooltip', 'data-placement' => 'right', :title => '"Never" will cause this silence to, surprise surprise, never expire. It will remain silence until yourself or someone else manually deletes the silence. It is highly discourage to permanently silence anything. Its extremely rare that that is ever the correct action.'}
200
+ .modal-footer
201
+ %button.btn.btn-default.pull-right{:type => 'submit', 'ng-click' => 'saveSilence()'} Create
202
+
203
+ -# help dialog
204
+ #howdoisearch.modal.fade
205
+ .modal-dialog
206
+ .modal-content
207
+ .modal-header
208
+ %button.close{:type => 'button', 'data-dismiss' => 'modal', 'aria-hidden' => 'true'}
209
+ %h4.modal-title How Do I Search?
210
+ .modal-body
211
+ %h4 Available Fields
212
+ %p.well.well-sm
213
+ {{event_fields_name | joinBy : ', '}}
214
+ %h4 Query Syntax
215
+ %h5 Examples
216
+ %table.table.table-striped
217
+ %tr
218
+ %td Desired Result
219
+ %td Query
220
+ %tr
221
+ %td For clients STARTING with "web"
222
+ %td client:web*
223
+ %tr
224
+ %td For checks ENDING with "process"
225
+ %td check:process
226
+ %tr
227
+ %td For checks MATCHING with "disk_usage"
228
+ %td check:"disk_usage"
229
+ %tr
230
+ %td For statuses warning or critical
231
+ %td status:[1 TO 2]
232
+ %tr
233
+ %td For "web" clients and critical
234
+ %td client:web* AND status:2
235
+ %tr
236
+ %td Search for everything, except checks that start with "disk"
237
+ %td *:* AND NOT check:disk*
238
+ %tr
239
+ %td For "web" or "dbs" clients and status is unknown
240
+ %td client:(web* OR db*) AND status:3
241
+ %tr
242
+ %td Full text search for "failure"
243
+ %td failure
244
+ %tr
245
+ %td Events since Jan 1, 2014
246
+ %td issued:[1388534400 TO Infinity]
247
+ %p
248
+ The Cloudant search query syntax is based on the
249
+ %a{:href => 'http://lucene.apache.org/core/4_2_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description'}
250
+ Lucene
251
+ syntax. Search queries take the form of "name:value" (unless the name is omitted, in which case they hit the default field).
252
+ %p
253
+ Queries over multiple fields can be logically combined and groups and fields can be grouped. The available logical operators are: "AND", "+", "OR", "NOT" and "-", and are case sensitive. Range queries can run over strings or numbers.
254
+ %p
255
+ If you want a fuzzy search you can run a query with "~" to find terms like the search term, for instance "look~" will find terms book and took.
256
+ %p
257
+ You can also increase the importance of a search term by using the boost character "^". This makes matches containing the term more relevant, e.g. cloudant "data layer"^4 will make results containing "data layer" 4 times more relevant. The default boost value is 1. Boost values must be positive, but can be less than 1 (e.g. 0.5 to reduce importance).
258
+ %p
259
+ Wild card searches are supported, for both single ("?") and multiple ("*") character searches. "dat?" would match date and data, "dat*" would match date, data, database, dates etc. Wildcards must come after a search term, you cannot do a query like "*base".
260
+
261
+ %p
262
+ The following characters require escaping if you want to search on them
263
+
264
+ %p.well.well-sm
265
+ + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
266
+
267
+ .modal-footer
268
+ %button.btn.btn-default{:type => 'button', 'data-dismiss' => 'modal'} Close