ferris-bueller 0.0.3 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e42b63a6a4a472e0a57bf203fd2b40df207691f0
4
- data.tar.gz: e17b58dd6390318e6540015e1d5ead60a8b5fd27
3
+ metadata.gz: f388396864494d4cdd8a39e1855e9e9ad00429cc
4
+ data.tar.gz: f255231cafc28d0003c9bf199ee02dec7c3ada56
5
5
  SHA512:
6
- metadata.gz: 5e63ebf4b536ae282d08365cd580796e843a89685d53ef498f936bee99e592fd04a484cd6a9398296882ff17b3602a0eb373e0bc991dc9c379e4359d17f39a84
7
- data.tar.gz: ae7e81ccd67840d08b36d0855fd7adcfb441e9249d7ede8916f8fb0f148e4722e6b208ce6c0dfe81e676e5d454735856b9a44eded1cafbf5c140c178e0305731
6
+ metadata.gz: 912d96dbb25ca30dcf194268416ce60196a7109eb1fb2cb82a3a821bc5eee05a90ddd2f034288398af6f219b21fb4ded7efdd9c51860e6db296f4e60c637df33
7
+ data.tar.gz: f672c22d2ab7578a697c4659f1dc0e9dcece80f160bad36f0e07be81d970e033445b5cc4be3da291eeffc0c7a2d860b74122bfa795f160f26733c35f5223d0b6
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.3
1
+ 0.1.1
@@ -33,5 +33,17 @@ module FerrisBueller
33
33
  }
34
34
 
35
35
  SEVERITY_FIELD = SHOW_FIELDS.key('Severity')
36
+
37
+ HELP_TEXT = <<-END.gsub(/^ +/,'')
38
+ /inc help - print this message
39
+ /inc resolve <inc> - resolve incident number <inc>
40
+ /inc close <inc> - close incident number <inc>
41
+ /inc whoami - test to see if bueller can tell who you are
42
+ /inc list - list incidents
43
+ /inc summary - summary of incidents
44
+ /inc show <inc> - show incident info for incident <inc>
45
+ /inc comment <inc> <comment> - comment on incident <inc> with comment <comment>
46
+ /inc open <severity> <summary> - open incident with severity of 1(high)-5(low) <severity>
47
+ END
36
48
  end
37
- end
49
+ end
@@ -18,7 +18,7 @@ module FerrisBueller
18
18
  def store ; @store end
19
19
 
20
20
 
21
- def start_your_day_off
21
+ def start_your_day_off queue
22
22
  Web.set :environment, options.environment
23
23
  Web.set :port, options.port
24
24
  Web.set :bind, options.bind
@@ -33,8 +33,10 @@ module FerrisBueller
33
33
  logger: log
34
34
  )
35
35
  Web.set :jira_project, options.jira_project
36
+ Web.set :jira_url, options.jira_url
36
37
  Web.set :jira_type, options.jira_type
37
38
  Web.set :refresh_rate, options.incident_refresh
39
+ Web.set :post_queue, queue
38
40
 
39
41
  if log.level >= ::Logger::DEBUG
40
42
  Web.set :raise_errors, true
@@ -48,6 +50,26 @@ module FerrisBueller
48
50
  Web.run!
49
51
  end
50
52
 
53
+ def go_handle_postbacks queue
54
+ Thread.new do
55
+ loop do
56
+ post_lambda = queue.pop
57
+ response, uri_string = post_lambda.call
58
+ uri = URI uri_string
59
+ log.debug \
60
+ event: 'sending Slack response',
61
+ path: uri_string,
62
+ data: response
63
+ Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') do |http|
64
+ req = Net::HTTP::Post.new uri
65
+ req['Content-Type'] = 'application/json'
66
+ req['Accept'] = 'application/json'
67
+ req.body = JSON.generate response
68
+ http.request req
69
+ end
70
+ end
71
+ end
72
+ end
51
73
 
52
74
  def go_refresh_jira_users
53
75
  Thread.new do
@@ -100,9 +122,8 @@ module FerrisBueller
100
122
  log.error \
101
123
  error: 'could not refresh users',
102
124
  event: 'exception',
103
- exception: e.inspect,
104
125
  class: e.class,
105
- message: e.message,
126
+ message: e.message.inspect,
106
127
  backtrace: e.backtrace,
107
128
  remediation: 'pausing breifly before retrying'
108
129
  sleep RETRY_DELAY
@@ -111,20 +132,36 @@ module FerrisBueller
111
132
 
112
133
 
113
134
  def refresh_jira_members
114
- data = jira_request 'group', \
115
- groupname: options.jira_group,
116
- expand: 'users'
135
+ req_path = 'rest/api/2/group/member'
136
+ is_last, values, start = false, [], 0
137
+ until is_last
138
+ req_params = QueryParams.encode \
139
+ groupname: options.jira_group,
140
+ startAt: start
141
+
142
+ uri = URI(options.jira_url + req_path + '?' + req_params)
143
+ http = Net::HTTP.new uri.hostname, uri.port
144
+
145
+ req = Net::HTTP::Get.new uri
146
+ req.basic_auth options.jira_user, options.jira_pass
147
+ req['Content-Type'] = 'application/json'
148
+ req['Accept'] = 'application/json'
149
+
150
+ resp = http.request req
151
+ data = JSON.parse resp.body
152
+ values += data['values']
153
+ is_last = data['isLast']
154
+ start += data['maxResults']
155
+ end
117
156
 
118
- user_names = data[:users][:items].map { |u| u[:name] }
157
+ user_names = values.map { |u| u['name'] }
119
158
  store[:jira_members] = user_names
120
-
121
159
  rescue StandardError => e
122
160
  log.error \
123
161
  error: 'could not refresh members',
124
162
  event: 'exception',
125
- exception: e.inspect,
126
163
  class: e.class,
127
- message: e.message,
164
+ message: e.message.inspect,
128
165
  backtrace: e.backtrace,
129
166
  remediation: 'pausing breifly before retrying'
130
167
  sleep RETRY_DELAY
@@ -142,14 +179,12 @@ module FerrisBueller
142
179
  store[:jira_incidents] = data[:issues].map do |i|
143
180
  i[:num] = i[:key].split('-', 2).last ; i
144
181
  end
145
-
146
182
  rescue StandardError => e
147
183
  log.error \
148
184
  error: 'could not refresh incidents',
149
185
  event: 'exception',
150
- exception: e.inspect,
151
186
  class: e.class,
152
- message: e.message,
187
+ message: e.message.inspect,
153
188
  backtrace: e.backtrace,
154
189
  remediation: 'pausing breifly before retrying'
155
190
  sleep RETRY_DELAY
@@ -159,7 +194,7 @@ module FerrisBueller
159
194
 
160
195
  def jira_request path, params
161
196
  api_url = File.join options.jira_url, 'rest/api/latest', path
162
- log.trace \
197
+ log.debug \
163
198
  event: 'jira request',
164
199
  path: path,
165
200
  params: params,
@@ -183,4 +218,4 @@ module FerrisBueller
183
218
  end
184
219
 
185
220
  end
186
- end
221
+ end
@@ -14,7 +14,7 @@ module FerrisBueller
14
14
  @api_url = options.fetch :api_url
15
15
  @base_path = options.fetch :base_path, '/rest/api/2'
16
16
  @logger = options.fetch :logger, Slog.new
17
- log.trace event: 'Jira API client initialized'
17
+ log.debug event: 'Jira API client initialized'
18
18
  end
19
19
 
20
20
  def send path, data={}
@@ -26,20 +26,32 @@ module FerrisBueller
26
26
  req['Content-Type'] = 'application/json'
27
27
  req['Accept'] = 'application/json'
28
28
  req.body = JSON.generate data
29
- log.trace \
29
+ log.debug \
30
30
  event: 'sending Jira API request',
31
31
  path: path,
32
32
  data: data
33
- res = JSON.parse http.request(req).body, symbolize_names: true
34
- log.debug \
35
- event: 'Jira API request returned',
36
- path: path,
37
- data: data,
38
- response: res
39
- res
33
+ raw_res = http.request(req).body
34
+ begin
35
+ return nil unless raw_res
36
+ res = JSON.parse raw_res, symbolize_names: true
37
+ log.debug \
38
+ event: 'Jira API request returned',
39
+ path: path,
40
+ data: data,
41
+ response: res
42
+ res
43
+ rescue => e
44
+ log.error \
45
+ event: 'exception parsing jira response',
46
+ response: raw_res.inspect,
47
+ exception: e.class,
48
+ message: e.message.inspect,
49
+ backtrace: e.backtrace
50
+ raise e
51
+ end
40
52
  end
41
53
 
42
54
  private
43
55
  def log ; @logger end
44
56
  end
45
- end
57
+ end
@@ -112,8 +112,11 @@ module FerrisBueller
112
112
  go_refresh_jira_users
113
113
  go_refresh_jira_members
114
114
  go_refresh_jira_incidents
115
- start_your_day_off
115
+
116
+ queue = Queue.new
117
+ go_handle_postbacks queue
118
+ start_your_day_off queue
116
119
  end
117
120
 
118
121
  end
119
- end
122
+ end
@@ -29,15 +29,15 @@ module FerrisBueller
29
29
  # Every project deserves its own ASCII art
30
30
  ART = <<-'EOART' % VERSION
31
31
 
32
-
33
-
34
- .--. . .
35
- | ) | |
36
- |--: .-. . . | | .-. .--.
37
- | )(.-' | | | |(.-' |
38
- '--' `--'`--`-`-`-`--''
39
-
32
+ /$$$$$$$ /$$ /$$
33
+ | $$__ $$ | $$| $$
34
+ | $$ \ $$ /$$ /$$ /$$$$$$ | $$| $$ /$$$$$$ /$$$$$$
35
+ | $$$$$$$ | $$ | $$ /$$__ $$| $$| $$ /$$__ $$ /$$__ $$
36
+ | $$__ $$| $$ | $$| $$$$$$$$| $$| $$| $$$$$$$$| $$ \__/
37
+ | $$ \ $$| $$ | $$| $$_____/| $$| $$| $$_____/| $$
38
+ | $$$$$$$/| $$$$$$/| $$$$$$$| $$| $$| $$$$$$$| $$
39
+ |_______/ \______/ \_______/|__/|__/ \_______/|__/
40
40
 
41
41
 
42
42
  EOART
43
- end
43
+ end
@@ -13,13 +13,11 @@ module FerrisBueller
13
13
  attachments: [
14
14
  {
15
15
  title: 'Slack User',
16
- # pretext: 'User found via Slack APIs',
17
16
  text: "```#{JSON.pretty_generate(u[:slack])}```",
18
17
  mrkdwn_in: %w[ text pretext ]
19
18
  },
20
19
  {
21
20
  title: 'Jira User',
22
- # pretext: 'User found via Jira APIs',
23
21
  text: "```#{JSON.pretty_generate(u[:jira])}```",
24
22
  mrkdwn_in: %w[ text pretext ]
25
23
  }
@@ -34,7 +32,7 @@ module FerrisBueller
34
32
 
35
33
 
36
34
  def reply_help params
37
- { text: "Help!" }
35
+ { text: HELP_TEXT }
38
36
  end
39
37
 
40
38
 
@@ -49,7 +47,7 @@ module FerrisBueller
49
47
  return { text: 'No open incidents at the moment' } if incidents.empty?
50
48
 
51
49
  attachments = incidents.map do |i|
52
- attach_incident(incident)
50
+ attach_incident(i)
53
51
  end
54
52
  {
55
53
  text: 'Found %d open incidents' % attachments.size,
@@ -60,13 +58,22 @@ module FerrisBueller
60
58
 
61
59
  def reply_summary params
62
60
  incidents = recent_incidents
63
- return { text: 'Could not list incidents' } if incidents.nil?
64
- return { text: 'No recent incidents' } if incidents.empty?
61
+ return { response_type: 'in_channel', text: 'Could not list incidents' } if incidents.nil?
62
+ return { response_type: 'in_channel', text: 'No recent incidents' } if incidents.empty?
65
63
 
66
- attachments = incidents.map do |i|
64
+ incidents = incidents.sort { |i| i[:id].to_i }.reverse
65
+
66
+ partitioned_incidents = []
67
+ severities = incidents.map { |i| i[:fields][:customfield_11250][:value] }.sort.uniq
68
+ severities.each do |severity|
69
+ partitioned_incidents += incidents.select { |i| i[:fields][:customfield_11250][:value] == severity }
70
+ end
71
+
72
+ attachments = partitioned_incidents.map do |i|
67
73
  attach_incident i
68
74
  end
69
75
  {
76
+ response_type: 'in_channel',
70
77
  text: 'Found %d recent incidents' % attachments.size,
71
78
  attachments: attachments
72
79
  }
@@ -78,76 +85,94 @@ module FerrisBueller
78
85
  return { text: 'Could not list incidents' } unless incident
79
86
 
80
87
  {
88
+ text: 'Incident info',
81
89
  attachments: [
82
- attach_incident(incident)
90
+ attach_incident(incident, true)
83
91
  ]
84
92
  }
85
93
  end
86
94
 
87
95
 
88
96
  def reply_resolve inc_num, params
89
- return { text: "You're not allowed to do that" } unless allowed? params
97
+ return { response_type: 'in_channel' }, lambda do
98
+ return { response_type: 'in_channel', text: "You're not allowed to do that" }, params[:response_url] unless allowed? params
90
99
 
91
- incident = select_incident inc_num
92
- return { text: 'Could not list incidents' } unless incident
100
+ incident = select_incident inc_num
101
+ return { response_type: 'in_channel', text: 'Could not list incidents' }, params[:response_url] unless incident
93
102
 
94
- resolution = resolve_incident incident
95
- return { text: 'Could not resolve incident' } if resolution.nil?
96
- return { text: 'Already resolved' } if resolution == false
103
+ resolution = resolve_incident incident
104
+ return { response_type: 'in_channel', text: 'Could not resolve incident' }, params[:response_url] if resolution.nil?
105
+ return { response_type: 'in_channel', text: 'Already resolved' } if resolution == false
97
106
 
98
- {
99
- text: 'Resolved incident',
100
- attachments: [
101
- attach_incident(incident)
102
- ]
103
- }
107
+ return {
108
+ response_type: 'in_channel',
109
+ text: 'Resolved incident',
110
+ attachments: [
111
+ attach_incident(incident)
112
+ ]
113
+ }, params[:response_url]
114
+ end
104
115
  end
105
116
 
106
117
 
107
118
  def reply_close inc_num, params
108
- return { text: "You're not allowed to do that" } unless allowed? params
119
+ return { response_type: 'in_channel' }, lambda do
120
+ return { response_type: 'in_channel', text: "You're not allowed to do that" }, params[:response_url] unless allowed? params
109
121
 
110
- incident = select_incident inc_num
111
- return { text: 'Could not list incidents' } unless incident
122
+ incident = select_incident inc_num
123
+ return { response_type: 'in_channel', text: 'Could not list incidents' }, params[:response_url] unless incident
112
124
 
113
- resolution = close_incident incident
114
- return { text: 'Could not close incident' } unless resolution
125
+ resolution = close_incident incident
126
+ return { response_type: 'in_channel', text: 'Could not close incident' }, params[:response_url] unless resolution
115
127
 
116
- {
117
- text: 'Closed incident',
118
- attachments: [
119
- attach_incident(incident)
120
- ]
121
- }
128
+ return {
129
+ response_type: 'in_channel',
130
+ text: 'Closed incident',
131
+ attachments: [
132
+ attach_incident(incident)
133
+ ]
134
+ }, params[:response_url]
135
+ end
122
136
  end
123
137
 
124
138
 
125
139
  def reply_open sev_num, summary, params
126
- return { text: "You're not allowed to do that" } unless allowed? params
127
-
128
- new_incident = construct_incident sev_num, summary, params
129
- incident = open_incident new_incident, summary
130
- return { text: 'Could not open incident' } unless incident
131
-
132
- incident = new_incident.merge incident
133
- {
134
- text: 'Opened incident',
135
- attachments: [
136
- attach_incident(incident)
137
- ]
138
- }
140
+ return { response_type: 'in_channel' }, lambda do
141
+ return { response_type: 'in_channel', text: "You're not allowed to do that" }, params[:response_url] unless allowed? params
142
+
143
+ new_incident = construct_incident sev_num, summary, params
144
+ incident = open_incident new_incident, summary
145
+ return { response_type: 'in_channel', text: 'Could not open incident' }, params[:response_url] unless incident
146
+
147
+ incident = new_incident.merge incident
148
+ log.debug \
149
+ event: 'created incident',
150
+ incident: incident
151
+ return {
152
+ response_type: 'in_channel',
153
+ text: 'Opened incident',
154
+ attachments: [
155
+ {
156
+ title: incident[:key],
157
+ title_link: File.join(settings.jira_url, 'browse', incident[:key]),
158
+ text: incident[:fields][:summary]
159
+ }
160
+ ]
161
+ }, params[:response_url]
162
+ end
139
163
  end
140
164
 
141
165
 
142
166
  def reply_comment inc_num, message, params
143
167
  incident = select_incident inc_num
144
- return { text: 'Could not list incidents' } unless incident
168
+ return { response_type: 'in_channel', text: 'Could not list incidents' } unless incident
145
169
 
146
170
  comment = construct_comment message, params
147
171
  annotation = comment_on_incident incident, comment
148
- return { text: 'Could not comment on incident' } unless annotation
172
+ return { response_type: 'in_channel', text: 'Could not comment on incident' } unless annotation
149
173
 
150
174
  {
175
+ response_type: 'in_channel',
151
176
  text: 'Commented on incident',
152
177
  attachments: [
153
178
  attach_incident(incident)
@@ -159,10 +184,49 @@ module FerrisBueller
159
184
 
160
185
  private
161
186
 
162
- def attach_incident i
187
+ def attach_incident i, detailed=false
188
+ severity_colors = {
189
+ '1' => '#e60000',
190
+ '2' => '#e60000',
191
+ '3' => '#ff6600',
192
+ '4' => '#ffff00',
193
+ '5' => '#ccff33'
194
+ }
195
+ severity = i[:fields][:customfield_11250][:value] rescue 'Unknown'
196
+ severity_color = severity_colors[severity[3]] rescue '#cccccc'
197
+
198
+ additional_fields = if detailed
199
+ [
200
+ {
201
+ title: 'Description',
202
+ value: i[:fields][:description]
203
+ }
204
+ ]
205
+ end
206
+
163
207
  {
164
208
  title: i[:key],
165
- text: i[:fields][:summary],
209
+ title_link: File.join(settings.jira_url, 'browse', i[:key]),
210
+ text: "*#{severity}*\n_#{i[:fields][:summary]}_",
211
+ fields: [
212
+ {
213
+ title: 'Created',
214
+ value: (DateTime.parse(i[:fields][:created]).iso8601 rescue nil),
215
+ short: true
216
+ },
217
+ {
218
+ title: 'Status',
219
+ value: (i[:fields][:status][:name] rescue nil),
220
+ short: true
221
+ },
222
+ {
223
+ title: 'Updated',
224
+ value: (DateTime.parse(i[:fields][:updated]).iso8601 rescue nil),
225
+ short: true
226
+ },
227
+ *additional_fields
228
+ ].reject { |f| f[:value].nil? || f[:value].empty? || f[:value] == 'Unknown' },
229
+ color: severity_color,
166
230
  mrkdwn_in: %w[ text pretext ]
167
231
  }
168
232
  end
@@ -172,7 +236,7 @@ module FerrisBueller
172
236
  status = normalize_value i[:fields][:status]
173
237
  return false if status =~ RESOLVED_STATE
174
238
 
175
- log.trace \
239
+ log.debug \
176
240
  event: 'resolving incident',
177
241
  incident: i
178
242
 
@@ -201,7 +265,7 @@ module FerrisBueller
201
265
  status = normalize_value i[:fields][:status]
202
266
  return false if status =~ CLOSED_STATE
203
267
 
204
- log.trace \
268
+ log.debug \
205
269
  event: 'closing incident',
206
270
  incident: i
207
271
 
@@ -329,12 +393,12 @@ module FerrisBueller
329
393
  24 * 60 * 60 # seconds/day
330
394
  end
331
395
 
396
+
332
397
  def allowed? params
333
398
  u = user_lookup params
334
399
  u && store[:jira_members] \
335
400
  && store[:jira_members].include?(u[:jira][:nick])
336
401
  end
337
402
 
338
-
339
403
  end
340
- end
404
+ end
@@ -12,13 +12,13 @@ module FerrisBueller
12
12
  @token = options.fetch :token
13
13
  @logger = options.fetch :logger, Slog.new
14
14
  @api_url = options.fetch :api_url, 'https://slack.com/api'
15
- log.trace event: 'Slack API client initialized'
15
+ log.debug event: 'Slack API client initialized'
16
16
  end
17
17
 
18
18
  def send method, options={}
19
19
  uri = URI File.join(@api_url, method)
20
20
  options = { token: @token }.merge(options)
21
- log.trace event: 'sending api request', method: method, options: options
21
+ log.debug event: 'sending api request', method: method, options: options
22
22
  res = Net::HTTP.post_form uri, options
23
23
  log.debug event: 'sent api request', method: method, options: options, response: res
24
24
  JSON.parse res.body, symbolize_names: true
@@ -27,4 +27,4 @@ module FerrisBueller
27
27
  private
28
28
  def log ; @logger end
29
29
  end
30
- end
30
+ end
@@ -42,7 +42,8 @@ module FerrisBueller
42
42
  halt 403
43
43
  else
44
44
  content_type :json
45
- reply = respond(params)
45
+ reply, post = respond(params)
46
+ settings.post_queue << post if post
46
47
  JSON.generate reply if reply
47
48
  end
48
49
  end
@@ -81,4 +82,4 @@ module FerrisBueller
81
82
  end
82
83
 
83
84
  end
84
- end
85
+ end
@@ -24,12 +24,16 @@ module FerrisBueller
24
24
  email: data[:user][:email]
25
25
  }
26
26
 
27
+ log.info \
28
+ event: 'matching user',
29
+ slack_user: slack_user
30
+
27
31
  jira_matches = store[:jira_users].values.map do |jira_user|
28
32
  distances = [ :name, :nick ].map do |k|
29
33
  compare slack_user[k], jira_user[k]
30
34
  end.compact
31
35
  mean_distance = 1.0 * distances.inject(:+) / distances.size
32
- if mean_distance > threshold
36
+ if mean_distance > threshold or distances.max > 0.99
33
37
  { user: jira_user, distance: mean_distance}
34
38
  end
35
39
  end.compact
@@ -56,7 +60,7 @@ module FerrisBueller
56
60
  event: 'exception',
57
61
  exception: e.inspect,
58
62
  class: e.class,
59
- message: e.message,
63
+ message: e.message.inspect,
60
64
  backtrace: e.backtrace
61
65
  return nil
62
66
  end
@@ -71,4 +75,4 @@ module FerrisBueller
71
75
  end
72
76
 
73
77
  end
74
- end
78
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ferris-bueller
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Clemmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-22 00:00:00.000000000 Z
11
+ date: 2016-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: slog
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1'
19
+ version: '2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1'
26
+ version: '2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: thor
29
29
  requirement: !ruby/object:Gem::Requirement