hell 0.0.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.
Files changed (38) hide show
  1. data/.document +5 -0
  2. data/Gemfile +14 -0
  3. data/LICENSE.txt +20 -0
  4. data/README.markdown +108 -0
  5. data/Rakefile +38 -0
  6. data/VERSION +1 -0
  7. data/bin/hell +12 -0
  8. data/lib/hell.rb +0 -0
  9. data/lib/hell/app.rb +128 -0
  10. data/lib/hell/config.ru +4 -0
  11. data/lib/hell/hell.rb +0 -0
  12. data/lib/hell/lib/helpers.rb +119 -0
  13. data/lib/hell/lib/monkey_patch.rb +79 -0
  14. data/lib/hell/public/assets/css/bootstrap-responsive.css +1088 -0
  15. data/lib/hell/public/assets/css/bootstrap-responsive.min.css +9 -0
  16. data/lib/hell/public/assets/css/bootstrap.css +5893 -0
  17. data/lib/hell/public/assets/css/bootstrap.min.css +9 -0
  18. data/lib/hell/public/assets/css/hell.css +158 -0
  19. data/lib/hell/public/assets/ico/favicon.ico +0 -0
  20. data/lib/hell/public/assets/ico/favicon.png +0 -0
  21. data/lib/hell/public/assets/img/glyphicons-halflings-white.png +0 -0
  22. data/lib/hell/public/assets/img/glyphicons-halflings.png +0 -0
  23. data/lib/hell/public/assets/js/backbone-localstorage.js +84 -0
  24. data/lib/hell/public/assets/js/backbone.js +1431 -0
  25. data/lib/hell/public/assets/js/backbone.min.js +38 -0
  26. data/lib/hell/public/assets/js/bankersbox.js +768 -0
  27. data/lib/hell/public/assets/js/bootstrap.growl.js +2 -0
  28. data/lib/hell/public/assets/js/bootstrap.js +2025 -0
  29. data/lib/hell/public/assets/js/bootstrap.min.js +6 -0
  30. data/lib/hell/public/assets/js/hashchange.min.js +9 -0
  31. data/lib/hell/public/assets/js/hell.js +444 -0
  32. data/lib/hell/public/assets/js/jquery.min.js +4 -0
  33. data/lib/hell/public/assets/js/timeago.js +152 -0
  34. data/lib/hell/public/assets/js/underscore.min.js +1 -0
  35. data/lib/hell/views/index.erb +146 -0
  36. data/test/helper.rb +18 -0
  37. data/test/test_hell.rb +7 -0
  38. metadata +201 -0
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "jeweler"
5
+ end
6
+
7
+
8
+ gem 'capistrano'
9
+ gem 'json'
10
+
11
+ gem 'sinatra'
12
+ gem 'sinatra-contrib'
13
+ gem 'thin'
14
+ gem 'websocket'
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Jose Diaz-Gonzalez
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # Hell
2
+
3
+ An interactive web ui for Capistrano that also provides a json api
4
+
5
+ ![http://cl.ly/image/0R2H1J0t0c35](http://cl.ly/image/0R2H1J0t0c35/s10e11_480.jpg)
6
+
7
+ ## Requirements
8
+
9
+ - Ruby 1.9.2
10
+ - Bundler
11
+
12
+ ## Installation
13
+
14
+ Simply add `hell` as a dependency in your Gemfile:
15
+
16
+ source 'http://rubygems.org'
17
+ source 'http://gems.github.com'
18
+
19
+ gem 'hell'
20
+
21
+ And then run the following in your app directory:
22
+
23
+ bundle install
24
+
25
+ ## Usage
26
+
27
+ Once installed, you'll want to run the `hell` binary:
28
+
29
+ hell
30
+
31
+ Note that if you have multiple capistrano versions installed for multiple repositories, you may need to instead use:
32
+
33
+ bundle exec hell
34
+
35
+ Then open `localhost:4567` in your browser to get an interactive interface around your capistrano recipes.
36
+
37
+ ### Running commands
38
+
39
+ Hell provides an autocompleted list of all capistrano tasks you have setup, and you can run this against a specific environment as necessary. By default, hell will start the command in the background, and then use an `EventSource` to read the generated logfile.
40
+
41
+ When you run a task, that task is added to your history so that you may re-read the logs at a later date, as well as re-run the tasks as necessary. This history is done using `localstorage`, and as such is not shared amongst different users of your application.
42
+
43
+ Note that Hell does not provide persistence of generated tasks, nor does it perform locking of individual tasks. For now, suggest using tools such as [Logstash](http://logstash.net/) to persist the logs, and using locking techniques within your capistrano tooling instead.
44
+
45
+
46
+ ### Capistrano as an API
47
+
48
+ Inspired by Github's internal [Heaven](https://github.com/blog/1241-deploying-at-github) app, Hell provides a simple, rest-like json api around Capistrano. XML-RPC implementation to come.
49
+
50
+ Available endpoints:
51
+
52
+ - `/tasks`: List all available tasks
53
+ - `/tasks/search/:pattern`: Search for a given, non-regex pattern
54
+ - `/tasks/:name/background`: Kicks off the execution of a task in the background and writes the output to a log file. Will respond with an id for future log file recovery.
55
+ - `/tasks/:name/exists`: Checks if a task exists
56
+ - `/tasks/:name/execute`: Kicks off the execution of a task and responds with the results using the sinatra streaming api. EventSource-compatible.
57
+ - `/logs/:id/tail`: Using the id provided by `/tasks/:name/background`, will start a sinatra stream on the log file in question.
58
+ - `/logs/:id/view`: Using the id provided by `/tasks/:name/background`, will output the current contents of a logfile. Useful for later recovery of the logs through the web api.
59
+
60
+ Note that the current response is subject to change, and as such is not documented, though we will attempt to augment rather than change them.
61
+
62
+ ### Configuration
63
+
64
+ The following environment variables are available for your use:
65
+
66
+ - `HELL_APP_ROOT`: Path from which capistrano should execute. Defaults to `Dir.pwd`.
67
+ - `HELL_ENVIRONMENTS`: Comma-separated list of environments, Default: `production,staging`.
68
+ - `HELL_REQUIRE_ENV`: Whether or not to require specifying an environment. Default: `1`.
69
+ - `HELL_LOG_PATH`: Path to which logs should be written to. Defaults to `Dir.pwd + '/log'`.
70
+ - `HELL_BASE_DIR`: Base directory to use in web ui. Useful for subdirectories. Defaults to `/`.
71
+ - `HELL_SENTINEL_STRINGS`: Sentinel string used to denote the end of a task run. Defaults to `Hellish Task Completed`.
72
+
73
+ ## TODO
74
+
75
+ * ~~Finish the execute task so that it sends output to the browser~~
76
+ * ~~Figure out where/how to store deploy logs on disk~~
77
+ * ~~Blacklist tasks from being displayed~~
78
+ * ~~Add support for environment variables~~
79
+ * ~~Add support for deployment environments~~
80
+ * ~~Add support for ad-hoc deploy callbacks~~
81
+ * Save favorite commands as buttons
82
+ * Save defaults in a cookie
83
+ * Use a slide-up element for task output
84
+ * Add optional task locking so that deploys cannot interfere with one-another
85
+ * Add ability to use pusher instead of sinatra streaming
86
+
87
+ ## License
88
+
89
+ Copyright (c) 2012 Jose Diaz-Gonzalez
90
+
91
+ Permission is hereby granted, free of charge, to any person obtaining
92
+ a copy of this software and associated documentation files (the
93
+ "Software"), to deal in the Software without restriction, including
94
+ without limitation the rights to use, copy, modify, merge, publish,
95
+ distribute, sublicense, and/or sell copies of the Software, and to
96
+ permit persons to whom the Software is furnished to do so, subject to
97
+ the following conditions:
98
+
99
+ The above copyright notice and this permission notice shall be
100
+ included in all copies or substantial portions of the Software.
101
+
102
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
103
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
104
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
105
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
106
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
107
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
108
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ require 'rake'
15
+ require 'jeweler'
16
+
17
+ Jeweler::Tasks.new do |gem|
18
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
19
+ gem.name = "hell"
20
+ gem.homepage = "http://github.com/seatgeek/hell"
21
+ gem.license = "MIT"
22
+ gem.summary = %Q{A web interface and api wrapper around Capistrano}
23
+ gem.description = %Q{Hell is an open source web interface that exposes a set of capistrano recipes as a json api, for usage within large teams}
24
+ gem.email = "jose@seatgeek.com"
25
+ gem.authors = ["Jose Diaz-Gonzalez"]
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rdoc/task'
31
+ Rake::RDocTask.new do |rdoc|
32
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
33
+
34
+ rdoc.rdoc_dir = 'rdoc'
35
+ rdoc.title = "hell #{version}"
36
+ rdoc.rdoc_files.include('README*')
37
+ rdoc.rdoc_files.include('lib/**/*.rb')
38
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ begin
4
+ require 'hell/app.rb'
5
+ rescue LoadError => e
6
+ require 'rubygems'
7
+ path = File.expand_path '../../lib', __FILE__
8
+ $:.unshift(path) if File.directory?(path) && !$:.include?(path)
9
+ require 'hell/app.rb'
10
+ end
11
+
12
+ Hell::App.run!
File without changes
@@ -0,0 +1,128 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'sinatra'
4
+ require 'sinatra/base'
5
+ require 'sinatra/json'
6
+ require 'sinatra/streaming'
7
+
8
+ require 'json'
9
+ require 'securerandom'
10
+ require 'websocket'
11
+
12
+ HELL_DIR = Dir.pwd
13
+ APP_ROOT = ENV.fetch('HELL_APP_ROOT', HELL_DIR)
14
+ ENVIRONMENTS = ENV.fetch('HELL_ENVIRONMENTS', 'production,staging').split(',')
15
+ BLACKLIST = ['invoke', 'shell', 'internal:ensure_env', 'internal:setup_env']
16
+ REQUIRE_ENV = ENV.fetch('HELL_REQUIRE_ENV', '1') == '1'
17
+ HELL_LOG_PATH = ENV.fetch('HELL_LOG_PATH', File.join(HELL_DIR, 'log'))
18
+ HELL_BASE_DIR = ENV.fetch('HELL_BASE_DIR', '/')
19
+ SENTINEL_STRINGS = ENV.fetch('HELL_SENTINEL_STRINGS', 'Hellish Task Completed').split(',')
20
+
21
+ require 'hell/lib/monkey_patch'
22
+ require 'hell/lib/helpers'
23
+
24
+
25
+ module Hell
26
+ class App < Sinatra::Base
27
+ helpers Sinatra::JSON
28
+ helpers Sinatra::Streaming
29
+ helpers Hell::Helpers
30
+
31
+ cap = Capistrano::CLI.parse(["-T"])
32
+
33
+ set :public_folder, File.join(File.expand_path('..', __FILE__), 'public')
34
+ set :root, HELL_DIR
35
+ set :server, :thin
36
+ set :static, true
37
+ set :views, File.join(File.expand_path('..', __FILE__), 'views')
38
+
39
+ configure :production, :development do
40
+ enable :logging
41
+ end
42
+
43
+ get '/' do
44
+ @tasks = cap.task_index.keys
45
+ @require_env = REQUIRE_ENV
46
+ @www_base_dir = HELL_BASE_DIR
47
+ @environments = ENVIRONMENTS
48
+ erb :index
49
+ end
50
+
51
+ get '/tasks' do
52
+ tasks = cap.task_index
53
+ json tasks
54
+ end
55
+
56
+ get '/tasks/search/:pattern' do
57
+ tasks = cap.task_index(params[:pattern])
58
+ json tasks
59
+ end
60
+
61
+ get '/tasks/:name/exists' do
62
+ tasks = cap.task_index(params[:name], {:exact => true})
63
+ response = { :exists => !tasks.empty?, :task => params[:name]}
64
+ json response
65
+ end
66
+
67
+ get '/tasks/:name/background' do
68
+ tasks, original_cmd = verify_task(cap, params[:name])
69
+ verbose = ""
70
+ verbose = "LOGGING=debug" if params[:verbose] == true
71
+
72
+ task_id = run_in_background!("bundle exec cap -l STDOUT %s %s" % [original_cmd, verbose]) unless tasks.empty?
73
+ response = {}
74
+ response[:status] = tasks.empty? ? 404 : 200,
75
+ response[:message] = tasks.empty? ? "Task not found" : "Running task in background",
76
+ response[:task_id] = task_id unless tasks.empty?
77
+ json response
78
+ end
79
+
80
+ get '/logs/:id/tail' do
81
+ content_type "text/event-stream"
82
+ if valid_log params[:id]
83
+ _stream_success("tail -f %s" % File.join(HELL_LOG_PATH, params[:id] + ".log"))
84
+ else
85
+ _stream_error("log file '#{params[:id]}' not found")
86
+ end
87
+ end
88
+
89
+ get '/logs/:id/view' do
90
+ log_path = File.join(HELL_LOG_PATH, params[:id] + ".log")
91
+ logger.info log_path
92
+ ansi_escape(File.read(log_path))
93
+ end
94
+
95
+ get '/tasks/:name/execute' do
96
+ tasks, original_cmd = verify_task(cap, params[:name])
97
+ content_type "text/event-stream"
98
+ if tasks.empty?
99
+ _stream_error("cap task '#{original_cmd}' not found")
100
+ else
101
+ _stream_success("bundle exec cap -l STDOUT #{original_cmd} LOGGING=debug 2>&1", {:prepend => true})
102
+ end
103
+ end
104
+
105
+ def _stream_error(message)
106
+ stream do |out|
107
+ out << "event: start\ndata:\n\n" unless out.closed?
108
+ out << "data: " + ws_message("<p>#{message}</p>") unless out.closed?
109
+ out << "event: end\ndata:\n\n" unless out.closed?
110
+ out.close
111
+ end
112
+ end
113
+
114
+ def _stream_success(command, opts = {})
115
+ opts = {:prepend => false}.merge(opts)
116
+ stream do |out|
117
+ out << "event: start\ndata:\n\n" unless out.closed?
118
+ out << "data: " + ws_message("<p>#{command}</p>") unless out.closed? or opts[:prepend] == false
119
+ IO.popen(command, 'rb') do |io|
120
+ io.each do |line|
121
+ process_line(line, out, io)
122
+ end
123
+ end
124
+ close_stream(out)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/ruby
2
+ require './app'
3
+
4
+ run Hell::App
File without changes
@@ -0,0 +1,119 @@
1
+ module Hell
2
+ class TailDone < StandardError; end
3
+
4
+ module Helpers
5
+ def escape_to_html(data)
6
+ {
7
+ 1 => :nothing,
8
+ 2 => :nothing,
9
+ 4 => :nothing,
10
+ 5 => :nothing,
11
+ 7 => :nothing,
12
+ 8 => :backspace,
13
+ 30 => "#303030",
14
+ 31 => "#D10915",
15
+ 32 => "#53A948",
16
+ 33 => "#CD7D3D",
17
+ 34 => "#3582E0",
18
+ 35 => :magenta,
19
+ 36 => "#30EFEF",
20
+ 37 => :white,
21
+ 40 => :nothing,
22
+ 41 => :nothing,
23
+ 43 => :nothing,
24
+ 44 => :nothing,
25
+ 45 => :nothing,
26
+ 46 => :nothing,
27
+ 47 => :nothing,
28
+ }.each do |key, value|
29
+ if value == :nothing
30
+ data.gsub!(/\e\[#{key}m/,"<span>")
31
+ elsif value == :backspace
32
+ data.gsub!(/.[\b]/, '')
33
+ else
34
+ data.gsub!(/\e\[#{key}m/,"<span style=\"color:#{value}\">")
35
+ end
36
+ end
37
+ data.gsub!(/\e\[0m/, '</span>')
38
+ data.gsub!(/\e\[0/, '</span>')
39
+ # data.gsub!(' ', '&nbsp;')
40
+ data
41
+ end
42
+
43
+ def ansi_escape(message)
44
+ escape_to_html(utf8_dammit(message))
45
+ end
46
+
47
+ def ws_message(message)
48
+ message = {:message => ansi_escape(message)}.to_json + "\n\n"
49
+ end
50
+
51
+ def process_line(line, out, io)
52
+ begin
53
+ out << "data: " + ws_message(line) unless out.closed?
54
+ raise TailDone if SENTINEL_STRINGS.any? { |w| line =~ /#{w}/ }
55
+ rescue
56
+ Process.kill("KILL", io.pid)
57
+ end
58
+ end
59
+
60
+ def close_stream(out)
61
+ out << "event: end\ndata:\n\n" unless out.closed?
62
+ out.close
63
+ end
64
+
65
+ def run_in_background!(background_cmd)
66
+ log_file = Time.now.to_i.to_s + '.' + SecureRandom.hex(2)
67
+ cmd = [
68
+ "cd #{APP_ROOT}",
69
+ "echo '#{background_cmd}' >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
70
+ "#{background_cmd} >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
71
+ "echo 'Hellish Task Completed' >> #{HELL_LOG_PATH}/#{log_file}.log 2>&1",
72
+ ].join(" && ")
73
+ system("sh -c \"#{cmd}\" &")
74
+
75
+ # Wait up to three seconds in case of server load
76
+ i = 0
77
+ while i < 3
78
+ i += 1
79
+ break if File.exists?(File.join(HELL_LOG_PATH, log_file + ".log"))
80
+ sleep 1
81
+ end
82
+
83
+ log_file
84
+ end
85
+
86
+ def verify_task(cap, name)
87
+ original_cmd = name.gsub('+', ' ')
88
+ cmd = original_cmd.split(' ')
89
+ cmd.shift if ENVIRONMENTS.include?(cmd.first)
90
+ cmd = cmd.join(' ')
91
+
92
+ tasks = cap.task_index(cmd, {:exact => true})
93
+ return tasks, original_cmd
94
+ end
95
+
96
+ def valid_log(id)
97
+ File.exists?(File.join(HELL_LOG_PATH, id + ".log"))
98
+ end
99
+
100
+ def utf8_dammit(s)
101
+ # Converting ASCII-8BIT to UTF-8 based domain-specific guesses
102
+ if s.is_a? String
103
+ begin
104
+ # Try it as UTF-8 directly
105
+ cleaned = s.dup.force_encoding('UTF-8')
106
+ unless cleaned.valid_encoding?
107
+ # Some of it might be old Windows code page
108
+ cleaned = s.encode( 'UTF-8', 'Windows-1252' )
109
+ end
110
+ s = cleaned
111
+ rescue EncodingError
112
+ # Force it to UTF-8, throwing out invalid bits
113
+ s.encode!('UTF-8', :invalid => :replace, :undef => :replace)
114
+ end
115
+ s
116
+ end
117
+ end
118
+ end
119
+ end