runit-man 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/runit-man.rb +19 -0
- data/i18n/en.yml +35 -0
- data/i18n/ru.yml +37 -0
- data/lib/runit-man/app.rb +77 -0
- data/lib/runit-man/erb-to-erubis.rb +17 -0
- data/lib/runit-man/helpers.rb +47 -0
- data/lib/runit-man/log_location_cache.rb +73 -0
- data/lib/runit-man/partials.rb +20 -0
- data/lib/runit-man/service_info.rb +156 -0
- data/public/css/runit-man.css +22 -0
- data/public/css/tripoli.simple.css +29 -0
- data/public/css/tripoli.simple.ie.css +14 -0
- data/public/css/tripoli.type.css +36 -0
- data/public/css/tripoli.visual.css +119 -0
- data/public/js/jquery-1.4.1.min.js +152 -0
- data/public/js/runit-man.js +66 -0
- data/sv/log/run +3 -0
- data/sv/run +3 -0
- data/views/_service_action.erb +3 -0
- data/views/_service_info.erb +29 -0
- data/views/_services.erb +20 -0
- data/views/index.erb +14 -0
- data/views/layout.erb +33 -0
- data/views/log.erb +9 -0
- metadata +159 -0
data/bin/runit-man.rb
ADDED
@@ -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!
|
data/i18n/en.yml
ADDED
@@ -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
|
data/i18n/ru.yml
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
runit:
|
2
|
+
label: runit - управляющий web-интерфейс
|
3
|
+
title: "%1 - %2"
|
4
|
+
header: "%1: %2"
|
5
|
+
error: Ошибка обращения к ресурсу
|
6
|
+
loading: "Идёт загрузка…"
|
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
|