runit-man 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'optparse'
5
+
6
+ require 'runit-man/app'
7
+
8
+ RunitMan.set :active_services_directory, '/etc/service'
9
+ RunitMan.set :all_services_directory, '/etc/sv'
10
+
11
+ OptionParser.new { |op|
12
+ op.on('-s server') { |val| RunitMan.set :server, val }
13
+ op.on('-p port') { |val| RunitMan.set :port, val.to_i }
14
+ op.on('-b addr') { |val| RunitMan.set :bind, val }
15
+ op.on('-a active_services_directory (/etc/service by default)') { |val| RunitMan.set :active_services_directory, val }
16
+ op.on('-f all_services_directory (/etc/sv by default)') { |val| RunitMan.set :all_services_directory, val }
17
+ }.parse!(ARGV.dup)
18
+
19
+ RunitMan.run!
@@ -0,0 +1,35 @@
1
+ runit:
2
+ label: runit web management tool
3
+ title: "%1 - %2"
4
+ header: "%1: %2"
5
+ error: Error encountered while accessing resource
6
+ loading: "Please wait while loading…"
7
+ footer: "State of services updated automatically every %1 seconds."
8
+ services:
9
+ table:
10
+ caption: Services
11
+ headers:
12
+ pid: PID
13
+ name: Name
14
+ stat: Status
15
+ actions: Actions
16
+ log_file: Log file
17
+ values:
18
+ log_hint: Open tail 100 lines of %1 service log in new window
19
+ log_absent: Absent
20
+ footer: Updated at %1
21
+ subst:
22
+ inactive: <span class="inactive">inactive</span>
23
+ down: <span class="down">down<span>
24
+ run: <span class="run">run<span>
25
+ actions:
26
+ start: Start
27
+ stop: Stop
28
+ restart: Restart
29
+ switch_down: Deactivate
30
+ switch_up: Activate
31
+ log:
32
+ title: "Tail 100 lines of service %1 log - %2"
33
+ header: "Tail 100 lines of service <strong>%1</strong> log: %2"
34
+ updated: "Page updated at %1"
35
+ reload: Reload
@@ -0,0 +1,37 @@
1
+ runit:
2
+ label: runit - управляющий web-интерфейс
3
+ title: "%1 - %2"
4
+ header: "%1: %2"
5
+ error: Ошибка обращения к ресурсу
6
+ loading: "Идёт загрузка&hellip;"
7
+ footer: "Состояние сервисов обновляется автоматически каждые %1 секунд."
8
+ services:
9
+ table:
10
+ caption: Сервисы
11
+ headers:
12
+ pid: PID
13
+ name: Название
14
+ stat: Состояние
15
+ actions: Действия
16
+ log_file: Лог
17
+ values:
18
+ log_hint: Отрыть последние 100 строк лога сервиса %1 в новом окне
19
+ log_absent: Отсутствует
20
+ footer: Обновлено %1
21
+ subst:
22
+ inactive: <span class="inactive">неактивен</span>
23
+ down: <span class="down">остановлен</span>
24
+ run: <span class="run">работает</span>
25
+ got: получен
26
+ want: ожидается
27
+ actions:
28
+ start: Запустить
29
+ stop: Остановить
30
+ restart: Перезапустить
31
+ switch_down: Выключить
32
+ switch_up: Включить
33
+ log:
34
+ title: "Последние 100 строк лога сервиса %1 - %2"
35
+ header: "Последние 100 строк лога сервиса <strong>%1</strong>: %2"
36
+ updated: "Страница обновлена %1"
37
+ reload: Обновить страницу
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require 'sinatra/base'
5
+ require 'sinatra/r18n'
6
+ require 'runit-man/erb-to-erubis'
7
+ require 'runit-man/helpers'
8
+
9
+ R18n::Filters.on :variables
10
+
11
+ CONTENT_TYPES = {
12
+ :html => 'text/html',
13
+ :css => 'text/css',
14
+ :js => 'application/x-javascript',
15
+ :json => 'application/json'
16
+ }.freeze
17
+
18
+
19
+ class RunitMan < Sinatra::Base
20
+ set :environment, :production
21
+ set :static, true
22
+ set :logging, true
23
+ set :dump_errors, true
24
+ set :raise_errors, false
25
+ set :root, File.expand_path(File.join('..', '..'), File.dirname(__FILE__))
26
+
27
+ register Sinatra::R18n
28
+
29
+ helpers do
30
+ include Helpers
31
+ end
32
+
33
+ before do
34
+ base_content_type = case request.env['REQUEST_URI']
35
+ when /\.css$/ then :css
36
+ when /\.js$/ then :js
37
+ when /\.json$/ then :json
38
+ else :html
39
+ end
40
+ content_type CONTENT_TYPES[base_content_type], :charset => 'utf-8'
41
+ end
42
+
43
+ get '/' do
44
+ @scripts = [ 'jquery-1.4.1.min' ]
45
+ @title = host_name
46
+ erb :index
47
+ end
48
+
49
+ get '/services' do
50
+ partial :services
51
+ end
52
+
53
+ get '/:name/log' do |name|
54
+ srv = ServiceInfo[name]
55
+ return not_found if srv.nil? || !srv.logged?
56
+ @scripts = []
57
+ @title = t.runit.services.log.title(h(name), h(host_name))
58
+ erb :log, :locals => {
59
+ :name => name,
60
+ :text => `tail -n 100 #{srv.log_file_location}`
61
+ }
62
+ end
63
+
64
+ def log_action(name, text)
65
+ addr = request.env.include?('X_REAL_IP') ? request.env['X_REAL_IP'] : request.env['REMOTE_ADDR']
66
+ puts "#{addr} - - [#{Time.now}] \"Do #{text} on #{name}\""
67
+ end
68
+
69
+ post '/:name/:action' do |name, action|
70
+ srv = ServiceInfo[name]
71
+ action = "#{action}!".to_sym
72
+ return not_found if srv.nil? || !srv.respond_to?(action)
73
+ srv.send(action)
74
+ log_action(name, action)
75
+ ''
76
+ end
77
+ end
@@ -0,0 +1,17 @@
1
+ module Sinatra::Erb
2
+ def erb(content, options={})
3
+ begin
4
+ require 'erubis'
5
+ @@erb_class = Erubis::Eruby
6
+ rescue LoadError
7
+ require "erb"
8
+ @@erb_class = ::ERB
9
+ end
10
+ render(:erb, content, options)
11
+ end
12
+
13
+ private
14
+ def render_erb(content, options = {})
15
+ @@erb_class.new(content).result(binding)
16
+ end
17
+ end
@@ -0,0 +1,47 @@
1
+ require 'socket'
2
+ require 'runit-man/service_info'
3
+ require 'runit-man/partials'
4
+ require 'sinatra/content_for'
5
+
6
+ module Helpers
7
+ include Rack::Utils
8
+ include Sinatra::Partials
9
+ include Sinatra::ContentFor
10
+ alias_method :h, :escape_html
11
+
12
+ attr_accessor :even_or_odd_state
13
+
14
+ def host_name
15
+ unless @host_name
16
+ @host_name = Socket.gethostbyname(Socket.gethostname).first
17
+ end
18
+ @host_name
19
+ end
20
+
21
+ def service_infos
22
+ ServiceInfo.all
23
+ end
24
+
25
+ def service_action(name, action, label)
26
+ partial :service_action, :locals => {
27
+ :name => name,
28
+ :action => action,
29
+ :label => label
30
+ }
31
+ end
32
+
33
+ def even_or_odd
34
+ self.even_or_odd_state = !even_or_odd_state
35
+ even_or_odd_state
36
+ end
37
+
38
+ def stat_subst(s)
39
+ s.split(/\s/).map do |s|
40
+ if s =~ /(\w+)/ && t.runit.services.table.subst[$1].translated?
41
+ s.sub(/\w+/, t.runit.services.table.subst[$1].to_s)
42
+ else
43
+ s
44
+ end
45
+ end.join(' ')
46
+ end
47
+ end
@@ -0,0 +1,73 @@
1
+ class LogLocationCache
2
+ TIME_LIMIT = 6000
3
+
4
+ def initialize
5
+ clear
6
+ end
7
+
8
+ def [](pid)
9
+ pid = pid.to_i
10
+ unless pids.include?(pid)
11
+ set_pid_log_location(pid, get_pid_location(pid))
12
+ end
13
+ pids[pid][:value]
14
+ end
15
+
16
+ private
17
+ attr_accessor :query_counter
18
+ attr_accessor :pids
19
+
20
+ def clear
21
+ self.query_counter = 0
22
+ self.pids = {}
23
+ self
24
+ end
25
+
26
+ def remove_old_values
27
+ self.query_counter = query_counter + 1
28
+ if query_counter < 1000
29
+ return
30
+ end
31
+ self.query_counter = 0
32
+ limit = Time.now - TIME_LIMIT
33
+ pids.keys.each do |pid|
34
+ if pids[pid][:time] < limit
35
+ pids.remove(pid)
36
+ end
37
+ end
38
+ self
39
+ end
40
+
41
+ def get_pid_location(lpid)
42
+ folder = log_folder(lpid)
43
+ return nil if folder.nil?
44
+ File.join(folder, 'current')
45
+ end
46
+
47
+ def log_command(lpid)
48
+ return nil if lpid.nil?
49
+ ps_output = `ps -o args -p #{lpid} 2>&1`.split("\n")
50
+ ps_output.shift
51
+ cmd = ps_output.first
52
+ cmd = cmd.chomp unless cmd.nil?
53
+ cmd = nil if cmd == ''
54
+ cmd
55
+ end
56
+
57
+ def log_folder(lpid)
58
+ cmd = log_command(lpid)
59
+ return nil if cmd.nil?
60
+ args = cmd.split(/\s+/).select { |arg| arg !~ /^\-/ }
61
+ return nil if args.shift != 'svlogd'
62
+ args.shift
63
+ end
64
+
65
+ def set_pid_log_location(pid, log_location)
66
+ remove_old_values
67
+ pids[pid.to_i] = {
68
+ :value => log_location,
69
+ :time => Time.now
70
+ }
71
+ self
72
+ end
73
+ end
@@ -0,0 +1,20 @@
1
+ # stolen from http://github.com/cschneid/irclogger/blob/master/lib/partials.rb
2
+ # and made a lot more robust by me
3
+ # this implementation uses erb by default. if you want to use any other template mechanism
4
+ # then replace `erb` on line 13 and line 17 with `haml` or whatever
5
+ module Sinatra::Partials
6
+ def partial(template, *args)
7
+ template_array = template.to_s.split('/')
8
+ template = template_array[0..-2].join('/') + "/_#{template_array[-1]}"
9
+ options = args.last.is_a?(Hash) ? args.pop : {}
10
+ options.merge!(:layout => false)
11
+ if collection = options.delete(:collection) then
12
+ collection.inject([]) do |buffer, member|
13
+ buffer << erb(:"#{template}", options.merge(:layout =>
14
+ false, :locals => {template_array[-1].to_sym => member}))
15
+ end.join("\n")
16
+ else
17
+ erb(:"#{template}", options)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,156 @@
1
+ require 'runit-man/log_location_cache'
2
+
3
+ class ServiceInfo
4
+ attr_reader :name
5
+
6
+ def initialize(a_name)
7
+ @name = a_name
8
+ end
9
+
10
+ def logged?
11
+ File.directory?(log_supervise_folder)
12
+ end
13
+
14
+ def stat
15
+ return 'inactive' unless supervise?
16
+ r = 'indeterminate'
17
+ File.open(File.join(supervise_folder, 'stat'), 'r') { |f| r = f.gets }
18
+ r
19
+ end
20
+
21
+ def active?
22
+ File.directory?(active_service_folder) || File.symlink?(active_service_folder)
23
+ end
24
+
25
+ def switchable?
26
+ File.symlink?(active_service_folder) || File.directory?(inactive_service_folder)
27
+ end
28
+
29
+ def run?
30
+ stat =~ /\brun\b/
31
+ end
32
+
33
+ def up!
34
+ send_signal! :u
35
+ end
36
+
37
+ def down!
38
+ send_signal! :d
39
+ end
40
+
41
+ def switch_down!
42
+ down!
43
+ File.unlink(active_service_folder)
44
+ end
45
+
46
+ def switch_up!
47
+ File.symlink(inactive_service_folder, active_service_folder)
48
+ end
49
+
50
+ def restart!
51
+ down!
52
+ up!
53
+ end
54
+
55
+ def pid
56
+ r = nil
57
+ if supervise?
58
+ File.open(File.join(supervise_folder, 'pid'), 'r') { |f| r = f.gets }
59
+ end
60
+ r = r.chomp unless r.nil?
61
+ r = nil if r == ''
62
+ r
63
+ end
64
+
65
+ def log_pid
66
+ r = nil
67
+ if logged?
68
+ File.open(File.join(log_supervise_folder, 'pid'), 'r') { |f| r = f.gets }
69
+ end
70
+ r = r.chomp unless r.nil?
71
+ r = nil if r == ''
72
+ r
73
+ end
74
+
75
+ def log_file_location
76
+ rel_path = self.class.log_location_cache[log_pid]
77
+ return nil if rel_path.nil?
78
+ File.expand_path(rel_path, log_run_folder)
79
+ end
80
+
81
+ private
82
+ def inactive_service_folder
83
+ File.join(RunitMan.all_services_directory, name)
84
+ end
85
+
86
+ def active_service_folder
87
+ File.join(RunitMan.active_services_directory, name)
88
+ end
89
+
90
+ def supervise_folder
91
+ File.join(active_service_folder, 'supervise')
92
+ end
93
+
94
+ def log_run_folder
95
+ File.join(active_service_folder, 'log')
96
+ end
97
+
98
+ def log_supervise_folder
99
+ File.join(log_run_folder, 'supervise')
100
+ end
101
+
102
+ def supervise?
103
+ File.directory?(supervise_folder)
104
+ end
105
+
106
+ def send_signal!(signal)
107
+ return unless supervise?
108
+ File.open(File.join(supervise_folder, 'control'), 'w') { |f| f.print signal.to_s }
109
+ end
110
+
111
+ class << self
112
+ def all
113
+ all_service_names.sort.map do |name|
114
+ ServiceInfo.new(name)
115
+ end
116
+ end
117
+
118
+ def [](name)
119
+ all_service_names.include?(name) ? ServiceInfo.new(name) : nil
120
+ end
121
+
122
+ def log_location_cache
123
+ unless @log_location_cache
124
+ @log_location_cache = LogLocationCache.new
125
+ end
126
+ @log_location_cache
127
+ end
128
+
129
+ private
130
+ def itself_or_parent?(name)
131
+ name == '.' || name == '..'
132
+ end
133
+
134
+ def active_service_names
135
+ return [] unless File.directory?(RunitMan.active_services_directory)
136
+ Dir.entries(RunitMan.active_services_directory).reject do |name|
137
+ full_name = File.join(RunitMan.active_services_directory, name)
138
+ itself_or_parent?(name) || (!File.symlink?(full_name) && !File.directory?(full_name))
139
+ end
140
+ end
141
+
142
+ def inactive_service_names
143
+ return [] unless File.directory?(RunitMan.all_services_directory)
144
+ actives = active_service_names
145
+ Dir.entries(RunitMan.all_services_directory).reject do |name|
146
+ full_name = File.join(RunitMan.all_services_directory, name)
147
+ itself_or_parent?(name) || !File.directory?(full_name) || actives.include?(name)
148
+ end
149
+ end
150
+
151
+ def all_service_names
152
+ (active_service_names + inactive_service_names)
153
+ end
154
+
155
+ end
156
+ end