ferris-bueller 0.0.3 → 0.1.1

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