require 'logger' require 'net/http' require 'tilt/erb' require 'queryparams' require_relative 'metadata' require_relative 'mjolnir' require_relative 'helpers' require_relative 'web' require_relative 'bot' Thread.abort_on_exception = true module Bender class Main < Mjolnir include Helpers desc 'version', 'Echo the application version' def version puts VERSION end desc 'art', 'View the application art' def art puts "\n%s\n" % ART end desc 'bot', 'Start Bender HipChat bot and Web server' option :bind, \ type: :string, aliases: %w[ -b ], desc: 'Set Sinatra interface', default: '0.0.0.0' option :port, \ type: :numeric, aliases: %w[ -o ], desc: 'Set Sinatra port', default: 4567 option :environment, \ type: :string, aliases: %w[ -e ], desc: 'Set Sinatra environment', default: 'development' option :hipchat_token, \ type: :string, aliases: %w[ -t ], desc: 'Set HipChat v1 API token', required: true option :hipchat_v2_token, \ type: :string, aliases: %w[ -c ], desc: 'Set HipChat v2 API token', required: true option :primary_room_id, \ type: :string, aliases: %w[ -i ], desc: 'Set HipChat primary room ID', required: true option :jid, \ type: :string, aliases: %w[ -j ], desc: 'Set HipChat JID', required: true option :password, \ type: :string, aliases: %w[ -p ], desc: 'Set HipChat password', required: true option :nick, \ type: :string, aliases: %w[ -n ], desc: 'Set HipChat nick name', required: true option :mention, \ type: :string, aliases: %w[ -m ], desc: 'Set HipChat mention name', required: true option :rooms, \ type: :string, aliases: %w[ -r ], desc: 'Set HipChat rooms (comma-separated)', required: true option :database, \ type: :string, aliases: %w[ -d ], desc: 'Set path to application database', required: true option :jira_user, \ type: :string, aliases: %w[ -U ], desc: 'Set JIRA username', required: true option :jira_pass, \ type: :string, aliases: %w[ -P ], desc: 'Set JIRA password', required: true option :jira_site, \ type: :string, aliases: %w[ -S ], desc: 'Set JIRA site', required: true option :jira_project, \ type: :string, aliases: %w[ -J ], desc: 'Set JIRA project', required: true option :jira_group, \ type: :string, aliases: %w[ -G ], desc: 'Set JIRA group for write mode', required: true option :jira_type, \ type: :string, aliases: %w[ -T ], desc: 'Set JIRA issue type', required: true option :user_refresh, \ type: :numeric, aliases: %w[ -R ], desc: 'Set JIRA user refresh rate', default: 300 option :issue_refresh, \ type: :numeric, aliases: %w[ -S ], desc: 'Set JIRA issue refresh rate', default: 5 option :group_refresh, \ type: :numeric, aliases: %w[ -T ], desc: 'Set JIRA group refresh rate', default: 60 option :order, \ type: :boolean, aliases: %w[ -O ], desc: 'Reverse order of HipChat API loading (hack)', default: false include_common_options def start bot = start_bot periodically_refresh_group bot periodically_refresh_users bot periodically_refresh_incidents bot serve_web bot end private def start_bot Bot::Connection.configure do |config| config.jid = options.jid config.password = options.password config.nick = options.mention config.mention_name = options.nick config.rooms = options.rooms.split(',') Bot::Storage::YamlStore.file = options.database config.store = Bot::Storage::YamlStore config.logger = log end Bot.run! options end def set_room_name_and_topic room_id, incidents, hipchat, bot room = hipchat[room_id] new_room = room.get_room open_incidents = incidents.select do |i| status = normalize_value i['fields']['status'] severity = short_severity(i['fields'][SEVERITY_FIELD]['value']) is_open = !(status =~ /resolved|closed/i) is_severe = severity =~ /(SEV1|SEV2)/i is_open && is_severe end @room_name ||= bot.store['primary_room_name'] || new_room['name'] @room_topic ||= bot.store['primary_room_topic'] || new_room['topic'] @open = nil unless defined? @open log.info \ primary_room_name: bot.store['primary_room_name'], primary_room_topic: bot.store['primary_room_topic'], new_room_name: new_room['name'], new_room_topic: new_room['topic'], room_name: @room_name, room_topic: @room_topic, open: @open if open_incidents.empty? if @open.nil? || @open > 0 new_room['name'] = '[NONE] Production Incident' new_room['topic'] = 'Good news everyone! No high-severity incidents at the moment' begin room.update_room(new_room) rescue NoMethodError log.warn 'NoMethodError in set_room_name_and_topic' end end @open = 0 else if @open.nil? || @open.zero? @room_name = new_room['name'] @room_topic = new_room['topic'] end unless @open == open_incidents.size new_room['name'] = '[IN PROGRESS] Production Incident' tha_news = open_incidents.size == 1 ? "There's a high-severity incident" : "There are #{open_incidents.size} high-severity incidents" new_room['topic'] = "Terrible news everyone! #{tha_news}" begin room.update_room(new_room) rescue NoMethodError log.warn 'NoMethodError in set_room_name_and_topic' end end @open = open_incidents.size end bot.store['primary_room_name'] = @room_name bot.store['primary_room_topic'] = @room_topic end def periodically_refresh_incidents bot Thread.new do if options.order hipchat_v1 = HipChat::Client.new \ options.hipchat_token, api_version: 'v1' hipchat_v2 = HipChat::Client.new \ options.hipchat_v2_token, api_version: 'v2' else hipchat_v2 = HipChat::Client.new \ options.hipchat_v2_token, api_version: 'v2' hipchat_v1 = HipChat::Client.new \ options.hipchat_token, api_version: 'v1' end room_id = options.primary_room_id loop do is = refresh_incidents bot begin set_room_name_and_topic room_id, is, hipchat_v2, bot sleep options.issue_refresh rescue HipChat::UnknownResponseCode log.warn 'HipChat::UnknownResponseCode in set_room_name_and_topic' sleep 2 end end end end def periodically_refresh_users bot req_path = '/rest/api/2/user/assignable/search' req_params = QueryParams.encode \ project: options.jira_project, startAt: 0, maxResults: 1_000_000 uri = URI(options.jira_site + req_path + '?' + req_params) http = Net::HTTP.new uri.hostname, uri.port req = Net::HTTP::Get.new uri req.basic_auth options.jira_user, options.jira_pass req['Content-Type'] = 'application/json' req['Accept'] = 'application/json' Thread.new do loop do resp = http.request req data = JSON.parse(resp.body) users = data.inject({}) do |h, user| h[user['key']] = { nick: user['key'], name: user['displayName'], email: user['emailAddress'] } ; h end bot.store['users'] = users sleep options.user_refresh end end end def periodically_refresh_group bot req_path = '/rest/api/2/group' req_params = QueryParams.encode \ groupname: options.jira_group, expand: 'users' uri = URI(options.jira_site + req_path + '?' + req_params) http = Net::HTTP.new uri.hostname, uri.port req = Net::HTTP::Get.new uri req.basic_auth options.jira_user, options.jira_pass req['Content-Type'] = 'application/json' req['Accept'] = 'application/json' Thread.new do loop do resp = http.request req data = JSON.parse(resp.body) user_names = data['users']['items'].map { |u| u['displayName'] } bot.store['group'] = user_names sleep options.group_refresh end end end def serve_web bot Web.set :environment, options.environment Web.set :port, options.port Web.set :bind, options.bind Web.set :store, options.database if log.level >= ::Logger::DEBUG Web.set :raise_errors, true Web.set :dump_errors, true Web.set :show_exceptions, true Web.set :logging, ::Logger::DEBUG end Web.set :bot, bot Web.run! end end end