runit-man 1.2

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.
@@ -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