last-resort 0.0.8 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -15,4 +15,5 @@ rdoc
15
15
  spec/reports
16
16
  test/tmp
17
17
  test/version_tmp
18
- tmp
18
+ tmp
19
+ schedule.rb
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+ --drb
3
+ --format d
data/README.md CHANGED
@@ -1,41 +1,41 @@
1
- Last Resort is a Ruby gem for monitoring critical emails sent by automated services ("Server down!",
2
- "Resource limit matched!", etc.) and calling your phone to tell you about it. It can be deployed in a
3
- reliable environment and perform up to 1500 emergency calls for free, using the free and trial tiers
4
- available from [context.io](http://context.io), [twilio](http://twilio.com) and [heroku](http://heroku.com).
1
+ Last Resort is a Ruby gem for monitoring email sent by automated services (monit, logging packages,
2
+ external ping services, etc.) and calling your phone to tell you about the important ones. Using free and trial tiers
3
+ available from [context.io](http://context.io), [twilio](http://twilio.com) and [heroku](http://heroku.com),
4
+ Last Resort can be deployed in a reliable environment and perform up to 1500 emergency calls **for free**.
5
5
 
6
- ### Installation
6
+ ### Requirements
7
+ * Ruby 1.9.x
8
+ * Accounts with [context.io](http://context.io), [twilio](http://twilio.com) and optionally [heroku](http://heroku.com),
9
+ but don't worry -- our commandline utility will help you through the process.
10
+ * Git (if you're deploying to Heroku)
7
11
 
12
+ ### Installation
8
13
  ```sh
9
14
  $ gem install last-resort
10
15
  ```
11
16
 
12
17
  ### Getting started
13
-
14
18
  ```sh
15
19
  $ last-resort new my-awesome-project
16
20
  ```
17
- This will create a scheduling project with a sample `my-awesome-project/config.rb` file.
21
+ This will create a new monitoring project with a sample `my-awesome-project/schedule.rb` file, and all that's
22
+ needed to get up and running on a Rack server (or Heroku) quickly.
18
23
 
19
- ### Example config.rb file
24
+ ### Example schedule.rb file
20
25
 
21
26
  ```ruby
22
- configure :host => "",
23
- :twilio_sid => "",
24
- :twilio_auth_token => "",
25
- :contextio_account => "",
26
- :contextio_key => "",
27
- :contextio_secret => ""
27
+ configure :from_env
28
28
 
29
29
  # DEFINE YOUR CONTACTS
30
30
 
31
- contact :ian, ""
32
- contact :scott, ""
33
- contact :victor, ""
31
+ contact :ian, "416-123-1234"
32
+ contact :scott, "416-321-4321"
33
+ contact :victor, "416-123-4321"
34
34
 
35
35
  # DEFINE WHAT EMAILS YOU WANT TO WATCH FOR
36
36
 
37
- match :subject => /server down/ # rackspace ping
38
- match :subject => /resource limit reached/ # monit
37
+ match :subject => /Server down/ # external ping service
38
+ match :subject => /Resource limit matched/ # monit
39
39
 
40
40
  # DEFINE WHO TO CALL AND WHEN
41
41
 
@@ -52,7 +52,10 @@ between :all_hours, :on => :weekends do
52
52
  end
53
53
  ```
54
54
 
55
+ ### Roadmap
56
+ If there is sufficient demand, we plan on adding more complicated schedules.
57
+
55
58
  ### Credit
56
59
  Victor Mota ([@vimota](http://www.twitter.com/vimota))
57
60
  Scott Hyndman ([@scotthyndman](http://www.twitter.com/scotthyndman))
58
- Ian Ha ([@ianpha](http://www.twitter.com/ianpha))
61
+ Ian Ha ([@ianpha](http://www.twitter.com/ianpha))
data/Rakefile CHANGED
@@ -1,2 +1,7 @@
1
1
  #!/usr/bin/env rake
2
2
  require "bundler/gem_tasks"
3
+
4
+ desc "Run the rspec tests"
5
+ task :spec do
6
+ system "rspec spec"
7
+ end
@@ -1,5 +1,42 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- # Parse options
3
+ $: << File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib/last-resort')
4
4
 
5
- # Perform command
5
+ require 'version'
6
+ require 'commands'
7
+ require 'rubygems'
8
+ require 'colored'
9
+ require 'gli';
10
+
11
+ include GLI
12
+
13
+
14
+ program_desc 'Last Resort is a Ruby gem for monitoring critical emails sent by automated services (monit, logging packages, external ping services, etc.) and calling your phone to tell you about it.'
15
+ version LastResort::VERSION
16
+
17
+ desc 'Creates a new Last Resort project'
18
+ arg_name 'project name'
19
+ command :new do |c|
20
+ c.desc 'skip Q&A session'
21
+ c.switch [:s, :skip]
22
+
23
+ c.action do |global_options, options, args|
24
+ raise "Please specify a project name" if args.empty?
25
+ LastResort::Commands::q_and_a args[0]
26
+ end
27
+ end
28
+
29
+ desc 'Run the last-resort server'
30
+ command :run do |c|
31
+ c.action do |global_options, options, args|
32
+ LastResort::Commands.run_heroku_or_rackup
33
+ end
34
+ end
35
+
36
+ on_error do |exception|
37
+ # Error logic here
38
+ # return false to skip default error handling
39
+ true
40
+ end
41
+
42
+ exit GLI.run(ARGV)
@@ -4,23 +4,27 @@ require File.expand_path('../lib/last-resort/version', __FILE__)
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Ian Ha", "Victor Mota", "Scott Hyndman"]
6
6
  gem.email = ["ianha0@gmail.com", "vimota@gmail.com", "scotty.hyndman@gmail.com"]
7
- gem.description = "Call your phone when critical emails arrive" # TODO Improve this
8
- gem.summary = "Call your phone when critical emails arrive" # TODO Improve this
9
- gem.homepage = ""
7
+ gem.summary = "Calls your phone when critical emails arrive"
8
+ gem.description = "Last Resort is a Ruby gem for monitoring email sent by automated services (monit, logging packages, external ping services, etc.) and calling your phone to tell you about the important ones. Using free and trial tiers available from context.io, twilio and heroku, Last Resort can be deployed in a reliable environment and perform up to 1500 emergency calls for free."
9
+ gem.homepage = "https://github.com/ianha/LastResort"
10
10
 
11
11
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
12
  gem.files = `git ls-files`.split("\n")
13
13
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
14
  gem.name = "last-resort"
15
15
  gem.require_paths = ["lib"]
16
- gem.version = Last::Resort::VERSION
16
+ gem.version = LastResort::VERSION
17
17
 
18
18
  gem.add_dependency "sinatra", "~> 1.3"
19
19
  gem.add_dependency "twilio-ruby", "~> 3.6"
20
- gem.add_dependency "thor", "~> 0.14"
21
20
  gem.add_dependency "oauth", "~> 0.4"
22
21
  gem.add_dependency "gli", "~> 1.6"
22
+ gem.add_dependency "colored"
23
+ gem.add_dependency "launchy", "~> 2.1"
23
24
  gem.add_development_dependency 'rspec', '~> 2.9.0'
24
25
  gem.add_development_dependency 'webmock', '~> 1.8.5'
25
26
  gem.add_development_dependency "awesome_print"
27
+ gem.add_development_dependency "rack"
28
+ gem.add_development_dependency "rack-test"
29
+ gem.add_development_dependency "nokogiri"
26
30
  end
@@ -1,23 +1,16 @@
1
1
  require "rubygems"
2
2
  require "bundler/setup"
3
- require "sinatra"
3
+ require "sinatra/base"
4
4
 
5
5
  FILE_DIR = File.expand_path(File.dirname(__FILE__))
6
6
 
7
- # BRING IN DEPENDENCIES
7
+ require 'last-resort/version'
8
+ require 'last-resort/config'
9
+ require 'last-resort/contextio'
10
+ require 'last-resort/scheduler'
11
+ require 'last-resort/twilio'
12
+ require 'last-resort/webhooks'
13
+ require 'last-resort/application'
8
14
 
9
- $LOAD_PATH << FILE_DIR
10
- Dir["#{FILE_DIR}/last-resort/*.rb"].each do |file_path|
11
- require file_path
12
- end
13
-
14
-
15
- # SINATRA BASICS
16
-
17
- set :port, 80
18
-
19
- get "/" do
20
- "Last Resort server running"
21
- end
22
-
23
- # LastResort::WebHookCreator.create_hooks
15
+ # Run the application if called directly
16
+ LastResort::Application.run! if $0 == __FILE__
@@ -0,0 +1,120 @@
1
+ require 'sinatra/base'
2
+
3
+ module LastResort
4
+ # The main Sinatra application. Defines the API interface and webhooks called by Twilio and ContextIO
5
+ # for call handling logic and email matching, respectively.
6
+ class Application < Sinatra::Base
7
+
8
+ def initialize
9
+ super
10
+ end
11
+
12
+ # The exception session keeps state between twilio and context-io webhooks. Currently, the
13
+ # system can only handle one call session at a time, although we plan to change that in future
14
+ # versions.
15
+ def self.exception_session
16
+ @exception_session
17
+ end
18
+
19
+ def self.exception_session=(session)
20
+ @exception_session = session
21
+ end
22
+
23
+ # ====== SERVER CHECK ROUTE
24
+
25
+ get "/" do
26
+ "Last Resort server running"
27
+ end
28
+
29
+ # ====== CONTEXT-IO TWILIO ENDPOINTS
30
+
31
+ # This is the webhook callback from context-io, called context-io has detected a matching pattern
32
+ post '/matched_email' do
33
+ scheduler = new_scheduler
34
+ matching_schedule = scheduler.get_matching_schedule
35
+
36
+ return if matching_schedule.nil?
37
+
38
+ matched_email = JSON.parse(get_request_body)
39
+ contacts = matching_schedule[:contacts].map { |name| LastResort::Contact.new(name.to_s, @config.contacts[name][:phone]) }
40
+
41
+ Application.exception_session = new_exception_session(contacts, matched_email["message_data"]["subject"])
42
+ Application.exception_session.notify
43
+ end
44
+
45
+
46
+ # ====== SINATRA TWILIO ENDPOINTS
47
+
48
+ # Service check method
49
+ get '/twilio' do
50
+ "Twilio callbacks up and running!"
51
+ end
52
+
53
+ # Performs a test call based on the user's configuration
54
+ get '/twilio/test' do
55
+ Application.exception_session.notify
56
+ end
57
+
58
+ # Method invoked to determine how the machine should interact with the user.
59
+ post '/twilio/call' do
60
+ content_type 'text/xml'
61
+ puts "call with #{params.inspect}"
62
+
63
+ if params[:CallStatus] == 'no-answer'
64
+ return Twilio::TwiML::Response.new { |r| r.Hangup }.text
65
+ end
66
+
67
+ response = Twilio::TwiML::Response.new do |r|
68
+ r.Say "Hello #{Application.exception_session.callee_name}. The following error has occured: #{Application.exception_session.description}", :voice => 'man'
69
+ r.Gather :numDigits => 1, :action => "http://#{HOST}/twilio/gather_digits" do |d|
70
+ d.Say "Please enter 1 to handle this bug that you probably didn't even create or 0 or hangup to go back to spending quality time with your family."
71
+ end
72
+ end
73
+ response.text
74
+ end
75
+
76
+ # Called when a user's call ends
77
+ post '/twilio/status_callback' do
78
+ puts "status_callback with #{params.inspect}"
79
+
80
+ Application.exception_session.call_next
81
+ end
82
+
83
+ # Called to respond to user phone input
84
+ post '/twilio/gather_digits' do
85
+ puts "gather_digits with #{params.inspect}"
86
+
87
+ content_type 'text/xml'
88
+ digit = params[:Digits].to_i
89
+
90
+ case digit
91
+ when 1 # User handles call, so don't call anyone else
92
+ Application.exception_session.end
93
+
94
+ response = Twilio::TwiML::Response.new do |r|
95
+ r.Say "Thank you for handling this exception. Goodbye.", :voice => 'man'
96
+ r.Hangup
97
+ end
98
+ return response.text
99
+ else # Hangup this call and go to the next person
100
+ return Twilio::TwiML::Response.new {|r| r.Hangup}.text
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ # ===== The following methods are wrappers to make our code more testable.
107
+
108
+ def new_scheduler
109
+ LastResort::Scheduler.new
110
+ end
111
+
112
+ def get_request_body
113
+ request_body.read
114
+ end
115
+
116
+ def new_exception_session *args
117
+ LastResort::ExceptionSession.new(*args)
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,159 @@
1
+ require 'launchy'
2
+ require 'config'
3
+
4
+ module LastResort
5
+ class Commands
6
+ def self.q_and_a project_name
7
+ @last_resort_path = File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../../')
8
+
9
+ puts "#{"Last Resort".green} is a Ruby gem for monitoring critical emails sent by automated services (monit, logging packages, external \nping services, etc.) and calling your phone to tell you about it.\n\n"
10
+
11
+ get_twillio_info
12
+ get_contextio_info
13
+
14
+ create_project project_name
15
+ end
16
+
17
+ def self.ask_yes_no message
18
+ begin
19
+ answer = ask message
20
+ end while !(answer.casecmp('y') == 0 or answer.casecmp('n') == 0)
21
+ answer.casecmp('y') == 0
22
+ end
23
+
24
+ def self.ask message
25
+ begin
26
+ print message
27
+ response = $stdin.gets.strip
28
+ if response.empty?
29
+ puts "Sorry you must enter something."
30
+ end
31
+ end while response.empty?
32
+
33
+ response
34
+ end
35
+
36
+ def self.get_twillio_info
37
+ answer = ask_yes_no "Do you already have a #{'Twillio'.red} account? [Y/n]"
38
+ Launchy.open("http://www.twilio.com/try-twilio") unless answer
39
+ @twillio_sid = ask "#{'Twillio'.red} SID: "
40
+ @twillio_auth_token = ask "#{'Twillio'.red} Auth Token: "
41
+ puts ''
42
+ end
43
+
44
+ def self.get_contextio_info
45
+ answer = ask_yes_no "Do you already have a #{'ContextIO'.yellow} account? [Y/n]"
46
+ Launchy.open("http://www.context.io") unless answer
47
+ puts ''
48
+ puts "Please find the ContextIO key and secret tokens, as well as the ContextIO Account ID of the email account you wish to monitor."
49
+ @contextio_key = ask "#{'ContextIO'.yellow} Key: "
50
+ @contextio_secret = ask "#{'ContextIO'.yellow} Secret: "
51
+ @contextio_account = ask "#{'ContextIO'.yellow} Email Account ID: "
52
+ end
53
+
54
+ def self.create_project project_name
55
+ @project_path = project_name.to_s
56
+
57
+ create_project_folder
58
+ copy_files
59
+ copy_schedule_and_add_utc
60
+
61
+ old_dir = Dir.pwd
62
+ Dir.chdir("#{@project_path}")
63
+
64
+ `bundle install`
65
+ set_up_heroku unless ask_if_heroku
66
+ set_up_git unless @no_heroku
67
+ create_heroku_project unless @no_heroku
68
+ create_env
69
+ `git push heroku master` unless @no_heroku
70
+
71
+ Dir.chdir("#{old_dir}")
72
+ end
73
+
74
+ def self.create_project_folder
75
+ puts "\nCreating project folder"
76
+ FileUtils.mkdir @project_path
77
+ end
78
+
79
+ def self.copy_files
80
+ puts '* creating .gitignore'.green
81
+ FileUtils.cp "#{@last_resort_path}/support/dot_gitignore", "#{@project_path}/.gitignore"
82
+
83
+ puts '* creating config.ru'.green
84
+ FileUtils.cp "#{@last_resort_path}/support/config.ru", "#{@project_path}"
85
+
86
+ puts '* creating Gemfile'.green
87
+ FileUtils.cp "#{@last_resort_path}/support/Gemfile", "#{@project_path}"
88
+ end
89
+
90
+ def self.copy_schedule_and_add_utc
91
+ puts '* creating schedule.rb'.green
92
+ schedule_file = open(@project_path + '/schedule.rb', 'w') do |f|
93
+ f.puts open(@last_resort_path + '/support/schedule.rb').read % {
94
+ :utc_offset => Time.now.utc_offset/60/60
95
+ }
96
+ end
97
+ end
98
+
99
+ def self.ask_if_heroku
100
+ puts ''
101
+ @no_heroku = !(ask_yes_no "Do you want it to be hosted on #{'Heroku'.magenta} (recommended)? [Y/n] ")
102
+ end
103
+
104
+ def self.set_up_heroku
105
+ puts 'Installing heroku'.green
106
+ `gem install heroku --no-rdoc --no-ri`
107
+ `heroku plugins:install git://github.com/ddollar/heroku-config.git`
108
+
109
+ answer = ask "Do you already have a #{'Heroku'.magenta} account? [Y/n]"
110
+ Launchy.open("http://heroku.com") if answer.casecmp('n') == 0
111
+ puts 'Please login to Heroku'
112
+ system 'heroku login'
113
+ end
114
+
115
+ def self.set_up_git
116
+ puts 'Initiating git repo'.green
117
+ `git init`
118
+ `git add .`
119
+ `git commit -m "Initializing git"`
120
+ end
121
+
122
+ def self.create_heroku_project
123
+ puts 'Creating Heroku project'.green
124
+ heroku_output = `heroku create --stack cedar`
125
+ @host = heroku_output.match(/http(.*).com\//)[0]
126
+ end
127
+
128
+ def self.create_env
129
+ open('.env', 'w') do |f|
130
+ f.puts open(@last_resort_path + '/support/dot_env').read % {
131
+ :host => (@no_heroku) ? '' : @host,
132
+ :twilio_sid => @twillio_sid,
133
+ :twilio_auth_token => @twillio_auth_token,
134
+ :contextio_account => @contextio_account,
135
+ :contextio_key => @contextio_key,
136
+ :contextio_secret => @contextio_secret,
137
+ :no_heroku => @no_heroku
138
+ }
139
+ end
140
+
141
+ if @no_heroku
142
+ puts 'Settings for Last Resort are stored in a .env file that can be found in the project directory.'.yellow
143
+ puts 'In order to run your project, you must modify the .env to include the domain name of your server.'.yellow
144
+ end
145
+ end
146
+
147
+ # ====== RUN
148
+
149
+ def self.run_heroku_or_rackup
150
+ begin
151
+ LastResort::Config::populate_env_if_required
152
+ rescue
153
+ puts 'Make sure to run "last-resort run" from a last-resort project'.yellow
154
+ return
155
+ end
156
+
157
+ end
158
+ end
159
+ end