upman-daemon 0.0.3

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.
data/bin/upman ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+
4
+ options = {:action => :run}
5
+
6
+ options = {}
7
+ version = "0.0.1"
8
+ daemonize_help = "run daemonized in the background (default: true)"
9
+ pidfile_help = "the pid filename (default: /var/run/upman.pid)"
10
+ logfile_help = "the log filename (default: /var/log/upman.log)"
11
+ include_help = "an additional $LOAD_PATH"
12
+ debug_help = "set $DEBUG to true"
13
+ warn_help = "enable warnings"
14
+
15
+ op = OptionParser.new
16
+ op.banner = "An example of how to daemonize upman."
17
+ op.separator ""
18
+ op.separator "Usage: upman [options]"
19
+ op.separator ""
20
+
21
+ op.separator "Process options:"
22
+ op.on("-d", "--daemonize", daemonize_help) { options[:daemonize] = true }
23
+ op.on("-p", "--pid PIDFILE", pidfile_help) { |value| options[:pidfile] = value }
24
+ op.on("-l", "--log LOGFILE", logfile_help) { |value| options[:logfile] = value }
25
+ op.separator ""
26
+
27
+ op.separator "Ruby options:"
28
+ op.on("-I", "--include PATH", include_help) { |value| $LOAD_PATH.unshift(*value.split(":").map { |v| File.expand_path(v) }) }
29
+ op.on("--debug", debug_help) { $DEBUG = true }
30
+ op.on("--warn", warn_help) { $-w = true }
31
+ op.separator ""
32
+
33
+ op.separator "Common options:"
34
+ op.on("-h", "--help") { puts op.to_s; exit }
35
+ op.on("-v", "--version") { puts version; exit }
36
+ op.separator ""
37
+
38
+ op.parse!(ARGV)
39
+ require_relative '../lib/upman' unless options[:action] == :help
40
+
41
+ case options[:action]
42
+ when :help then
43
+ puts op.to_s
44
+ when :version then
45
+ puts Upman::Core::Version.VERSION
46
+ else
47
+ Upman.run!(options)
48
+ end
@@ -0,0 +1,34 @@
1
+ module Upman
2
+ module Core
3
+ module Config
4
+ extend self
5
+ using SymbolizeHelper
6
+
7
+ @_config = {}
8
+ attr_reader :_config
9
+
10
+ def load!(filename, options = {})
11
+ newsets = YAML::load_file(filename).deep_symbolize_keys
12
+ newsets = newsets[options[:env].to_sym] if options[:env] && newsets[options[:env].to_sym]
13
+ deep_merge!(@_config, newsets)
14
+ end
15
+
16
+ def get!(key)
17
+ return @_config[key]
18
+ end
19
+
20
+ # Deep merging of hashes
21
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
22
+ def deep_merge!(target, data)
23
+ merger = proc {|key, v1, v2|
24
+ Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2}
25
+ target.merge! data, &merger
26
+ end
27
+
28
+ def method_missing(name, *args, &block)
29
+ @_config[name.to_sym] ||
30
+ fail(NoMethodError, "unknown configuration root #{name}", caller)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,157 @@
1
+ require 'fileutils'
2
+ require 'thread'
3
+
4
+ module Upman
5
+ module Core
6
+ class Daemon
7
+
8
+ include ::Upman::Utils::Helper
9
+
10
+ attr_reader :options, :quit
11
+
12
+ def initialize(options)
13
+ @options = options
14
+ options[:logfile] = File.expand_path(logfile) if logfile? # daemonization might change CWD so expand any relative paths in advance
15
+ options[:pidfile] = File.expand_path(pidfile) if pidfile? # (ditto)
16
+
17
+
18
+ end
19
+
20
+ def daemonize?
21
+ options[:daemonize]
22
+ end
23
+
24
+ def logfile
25
+ options[:logfile]
26
+ end
27
+
28
+ def pidfile
29
+ options[:pidfile]
30
+ end
31
+
32
+ def logfile?
33
+ !logfile.nil?
34
+ end
35
+
36
+ def pidfile?
37
+ !pidfile.nil?
38
+ end
39
+
40
+
41
+ #--------------------------------------------------------------------------
42
+
43
+ def run!
44
+
45
+ check_pid
46
+ daemonize if daemonize?
47
+ write_pid
48
+ trap_signals
49
+
50
+ if logfile?
51
+ redirect_output
52
+ elsif daemonize?
53
+ suppress_output
54
+ end
55
+
56
+ api = Upman::Utils::Api.new
57
+ unless api.check_connection
58
+ return nil
59
+ end
60
+
61
+ threads = []
62
+
63
+ Thread.abort_on_exception = true
64
+
65
+ threads << Thread.new do
66
+ server = Upman::Server::Socket.new
67
+ server.run!
68
+ end
69
+
70
+ threads << Thread.new do
71
+ worker = Upman::Core::Worker.new
72
+ worker.run!
73
+ end
74
+
75
+ trap("INT") do
76
+ warn "CTRL+C - Shutdown all threads"
77
+ threads.each { |t| Thread.kill t }
78
+ exit 130
79
+ end
80
+
81
+ threads.each { |t| t.join }
82
+
83
+ end
84
+
85
+ #==========================================================================
86
+ # DAEMONIZING, PID MANAGEMENT, and OUTPUT REDIRECTION
87
+ #==========================================================================
88
+
89
+ def daemonize
90
+ exit if fork
91
+ Process.setsid
92
+ exit if fork
93
+ Dir.chdir "/"
94
+ end
95
+
96
+ def redirect_output
97
+ FileUtils.mkdir_p(File.dirname(logfile), :mode => 0755)
98
+ FileUtils.touch logfile
99
+ File.chmod(0644, logfile)
100
+ $stderr.reopen(logfile, 'a')
101
+ $stdout.reopen($stderr)
102
+ $stdout.sync = $stderr.sync = true
103
+ end
104
+
105
+ def suppress_output
106
+ $stderr.reopen('/dev/null', 'a')
107
+ $stdout.reopen($stderr)
108
+ end
109
+
110
+ def write_pid
111
+ if pidfile?
112
+ begin
113
+ File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY) {|f| f.write("#{Process.pid}")}
114
+ at_exit {File.delete(pidfile) if File.exists?(pidfile)}
115
+ rescue Errno::EEXIST
116
+ check_pid
117
+ retry
118
+ end
119
+ end
120
+ end
121
+
122
+ def check_pid
123
+ if pidfile?
124
+ case pid_status(pidfile)
125
+ when :running, :not_owned
126
+ puts "A server is already running. Check #{pidfile}"
127
+ exit(1)
128
+ when :dead
129
+ File.delete(pidfile)
130
+ end
131
+ end
132
+ end
133
+
134
+ def pid_status(pidfile)
135
+ return :exited unless File.exists?(pidfile)
136
+ pid = ::File.read(pidfile).to_i
137
+ return :dead if pid == 0
138
+ Process.kill(0, pid)
139
+ :running
140
+ rescue Errno::ESRCH
141
+ :dead
142
+ rescue Errno::EPERM
143
+ :not_owned
144
+ end
145
+
146
+ #==========================================================================
147
+ # SIGNAL HANDLING
148
+ #==========================================================================
149
+
150
+ def trap_signals
151
+ trap(:QUIT) do # graceful shutdown
152
+ @quit = true
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,15 @@
1
+ module Upman
2
+ module Core
3
+ class ExtensionBase
4
+
5
+ include Upman::Utils::Helper
6
+
7
+ def register
8
+ fail "There is no register method defined in this extension."
9
+ end
10
+
11
+
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,38 @@
1
+ module Upman
2
+ module Core
3
+ class Worker
4
+
5
+ include Upman::Utils::Helper
6
+
7
+ def initialize
8
+ @stop_signal = true
9
+ end
10
+
11
+ def run!
12
+ _banner
13
+ while @stop_signal
14
+ self.perform
15
+ sleep(::Upman::Core::Config.daemon[:interval])
16
+ end
17
+
18
+ end
19
+
20
+ def shutdown
21
+ info "Stopping working thread"
22
+ @stop_signal = false
23
+ end
24
+
25
+
26
+ def perform
27
+ info "Doing some work"
28
+ end
29
+
30
+ private
31
+
32
+ def _banner
33
+ info "Running UpMan"
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Upman
6
+ module Extensions
7
+ # Get install history on debian system
8
+ class GetInstallHistory < ::Upman::Core::ExtensionBase
9
+ REQUEST_PATH = 'getInstallHistory'
10
+
11
+ def register
12
+ Servlet
13
+ end
14
+
15
+ # Servlet action for WEBrick
16
+ class Servlet < ::Upman::Server::BaseServlet
17
+
18
+ attr_accessor :param_since
19
+
20
+ # rubocop:disable Naming/MethodName
21
+ def do_GET(request, response)
22
+ # rubocop:enable Naming/MethodName
23
+
24
+ super(request, response)
25
+
26
+ unless (@param_since = get_param('since', 'date', false))
27
+ return nil
28
+ end
29
+
30
+ if (body = perform_action)
31
+ response = ok(response, body)
32
+ end
33
+ end
34
+
35
+ def perform_action
36
+ result = []
37
+ i = 0
38
+ hist_lines = fetch_history.split("\n")
39
+ hist_lines.each do |history|
40
+ fill_up(result, i, hist_lines, history) if i.even?
41
+ i += 1
42
+ end
43
+ result.to_json
44
+ end
45
+
46
+ private
47
+
48
+ def fetch_history
49
+ command = '(zcat $(ls -tr /var/log/apt/history.log*.gz); '\
50
+ 'cat /var/log/apt/history.log) ' \
51
+ '2>/dev/null | egrep \'^(Start-Date:|Commandline:)\' | '\
52
+ 'grep -v aptdaemon'
53
+ `#{command}`
54
+ end
55
+
56
+ def fill_up(result, line_index, hist_lines, history)
57
+ line = hist_lines[line_index + 1].sub('Commandline: ', '')
58
+ command = parse_command(line)
59
+ return nil if command.empty?
60
+
61
+ command[:start_date] = DateTime.parse(history.sub('Start-Date: ', ''))
62
+
63
+ if @param_since.instance_of?(Date)
64
+ if command[:start_date] >= @param_since
65
+ result.append command
66
+ end
67
+ else
68
+ result.append command
69
+ end
70
+ end
71
+
72
+ def parse_command(command_line)
73
+ package = {}
74
+ regexp = /(apt(-get)?)(( [\-\w]*)*) (install|upgrade|purge)(.*)?/
75
+ if (matches = command_line.match(regexp))
76
+ package[:action] = matches[5].strip
77
+
78
+ package[:options] = []
79
+ matches[3].split(' ').each { |name| package[:options].append name }
80
+
81
+ package[:packages] = []
82
+ matches[6].split(' ').each { |name| package[:packages].append name }
83
+ end
84
+ package
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,49 @@
1
+ module Upman
2
+ module Extensions
3
+ class GetInstalledPackages < ::Upman::Core::ExtensionBase
4
+ REQUEST_PATH = "getInstalledPackages"
5
+
6
+ def register
7
+ return Servlet
8
+ end
9
+
10
+ class Servlet < ::Upman::Server::BaseServlet
11
+
12
+ attr_accessor :requested
13
+
14
+ include ::Upman::Utils::Parser
15
+
16
+ # rubocop:disable Naming/MethodName
17
+ def do_GET(request, response)
18
+ # rubocop:enable Naming/MethodName
19
+
20
+ super(request, response)
21
+
22
+ @requested = get_param('requested', 'int')
23
+
24
+ if (body = perform_action)
25
+ response = ok(response, body)
26
+ end
27
+ end
28
+
29
+ def perform_action
30
+ result = []
31
+ extended_states = File.open('/var/lib/apt/extended_states', 'rb', &:read)
32
+ extended_states.split("\n\n").each do |chunk|
33
+ installed_packages = _get_hashed_values(chunk)
34
+ unless @requested.empty?
35
+ if installed_packages['auto_installed'] == @requested
36
+ result.append installed_packages
37
+ end
38
+ else
39
+ result.append installed_packages
40
+ end
41
+
42
+ end
43
+ result.to_json
44
+ end
45
+
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,26 @@
1
+ module Upman
2
+ module Extensions
3
+ class InstallPackage < ::Upman::Core::ExtensionBase
4
+
5
+ REQUEST_PATH = "installPackage"
6
+
7
+ def register
8
+ return Servlet
9
+ end
10
+
11
+ class Servlet < WEBrick::HTTPServlet::AbstractServlet
12
+ def do_GET request, response
13
+ response.status = 200
14
+ response['Content-Type'] = 'application/json'
15
+ response.body = self.perform_action
16
+ end
17
+
18
+
19
+ def perform_action
20
+ "installPackage"
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ module Upman
2
+ module Extensions
3
+ class Ping < ::Upman::Core::ExtensionBase
4
+ REQUEST_PATH = "ping"
5
+
6
+ def register
7
+ return Servlet
8
+ end
9
+
10
+ class Servlet < ::Upman::Server::BaseServlet
11
+
12
+ # rubocop:disable Naming/MethodName
13
+ def do_GET(request, response)
14
+ # rubocop:enable Naming/MethodName
15
+
16
+ super(request, response)
17
+
18
+ response = ok(response, perform_action)
19
+ end
20
+
21
+ def perform_action
22
+ "{\"pong\": true, \"message\": \"upman-#{::Upman::Version::VERSION} - Daemon is running\"}"
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,65 @@
1
+ module Upman
2
+ module Server
3
+ class BaseServlet < WEBrick::HTTPServlet::AbstractServlet
4
+
5
+ include ::Upman::Utils::Helper
6
+
7
+ attr_accessor :query, :response, :error_msg
8
+
9
+ def initialize(server, *options)
10
+ super(server, *options)
11
+ end
12
+
13
+ # rubocop:disable Naming/MethodName
14
+ def do_GET(_request, response)
15
+ # rubocop:enable Naming/MethodName
16
+
17
+ info "Process API request #{_request.path}"
18
+
19
+ @response = response
20
+ @query = _request.query
21
+ end
22
+
23
+ def ok(response, body)
24
+ response.status = 200
25
+ response['Content-Type'] = 'application/json'
26
+ response.body = body
27
+ response
28
+ end
29
+
30
+ def bad_request(response, body)
31
+ response.status = 400
32
+ response['Content-Type'] = 'application/json'
33
+ response.body = "{\"status\": \"bad_request\", \"message\": \"#{body}\"}"
34
+ response
35
+ end
36
+
37
+ def get_param(key, validate, require = false)
38
+ if @query.nil?
39
+ return ''
40
+ end
41
+
42
+ unless @query.include? key
43
+ unless require
44
+ return ''
45
+ else
46
+ return nil
47
+ end
48
+ end
49
+ unless validate.nil?
50
+ if validate == 'date'
51
+ begin
52
+ @query[key] = DateTime.parse(@query[key])
53
+ rescue
54
+ msg = "Invalid content for param #{key}, expected type: date - found #{@query[key]}"
55
+ bad_request(@response, msg)
56
+ fail msg
57
+ return false
58
+ end
59
+ end
60
+ end
61
+ @query[key]
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,66 @@
1
+ require 'openssl'
2
+ require 'webrick'
3
+ require 'webrick/https'
4
+
5
+ module Upman
6
+ module Server
7
+ class Socket
8
+
9
+ include ::Upman::Utils::Helper
10
+
11
+ include WEBrick
12
+
13
+ attr_reader :server
14
+
15
+ def initialize
16
+ @config = ::Upman::Core::Config.daemon
17
+
18
+ end
19
+
20
+ def run!
21
+
22
+ server = HTTPServer.new(
23
+ BindAddress: @config[:listen],
24
+ :Port => @config[:port],
25
+ :SSLEnable => @config[:ssl],
26
+ :debugger => false,
27
+ :daemonize => true,
28
+ :SSLPrivateKey => OpenSSL::PKey::RSA.new(
29
+ File.open(@config[:key]).read),
30
+ :SSLCertificate => OpenSSL::X509::Certificate.new(
31
+ File.open(@config[:cert]).read),
32
+ :SSLCertName => [["CN", WEBrick::Utils::getservername]],
33
+ Logger: WEBrick::Log.new("/dev/null"),
34
+ AccessLog: [],
35
+ )
36
+ _register_extensions(server)
37
+
38
+ protocol = @config[:ssl] ? 'https://' : 'http://'
39
+ info "Starting listener on #{protocol}#{@config[:listen]}:#{@config[:port]}"
40
+
41
+ server.start
42
+
43
+ end
44
+
45
+ private
46
+
47
+ def _dynload(str)
48
+ str.split('::').inject(Object) do |mod, class_name|
49
+ mod.const_get(class_name)
50
+ end
51
+ end
52
+
53
+
54
+ def _register_extensions(server)
55
+
56
+ @config[:extensions].each do |extension|
57
+ require_relative "../../upman/extensions/#{extension}"
58
+ ext_obj = _dynload("Upman::Extensions::#{extension.split('_').map(&:capitalize).join('')}")
59
+ info "Register /#{ext_obj ::REQUEST_PATH}.do for Upman::Extensions::#{extension.split('_').map(&:capitalize).join('')}"
60
+ ext_class = ext_obj .new
61
+ server.mount "/#{ext_obj ::REQUEST_PATH}.do", ext_class.register
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+
4
+ module Upman
5
+ module Utils
6
+ class Api
7
+
8
+ include ::Upman::Utils::Helper
9
+
10
+ def check_connection
11
+ response = self.get "status"
12
+ if response.nil?
13
+ false
14
+ end
15
+
16
+ unless response.code == 200
17
+ fail "Could not connect to API endpoint"
18
+ false
19
+ end
20
+ success "API Endpoint connected to Forman #{JSON.parse(response.body)["version"]} using API v#{JSON.parse(response.body)["api_version"]}."
21
+ true
22
+ end
23
+
24
+
25
+ def post(api_endoint, data)
26
+ begin
27
+ RestClient::Request.execute method: :post, url: _url + api_endoint, data: data, content_type: :json, accept: :json, user: Upman::Core::Config.foreman_api[:username], password: Upman::Core::Config.foreman_api[:password]
28
+ rescue RestClient::Unauthorized
29
+ fail "API Endpoint could not validate your credentials. Please check username or password"
30
+ rescue OpenSSL::SSL::SSLError
31
+ fail "Could not establish a secure connection to our API Endpoint"
32
+ rescue RestClient::NotFound
33
+ fail "API Endpoint route could not found"
34
+ rescue Errno::ECONNREFUSED
35
+ fail "Can not connect to Foreman instance on #{_url}"
36
+ end
37
+
38
+ end
39
+
40
+ def get(api_endoint)
41
+ begin
42
+ RestClient::Request.execute method: :get, url: _url + api_endoint, content_type: :json, accept: :json, user: Upman::Core::Config.foreman_api[:username], password: Upman::Core::Config.foreman_api[:password]
43
+ rescue RestClient::Unauthorized
44
+ fail "API Endpoint could not validate your credentials. Please check username or password"
45
+ rescue OpenSSL::SSL::SSLError
46
+ fail "Could not establish a secure connection to our API Endpoint"
47
+ rescue RestClient::NotFound
48
+ fail "API Endpoint route could not found"
49
+ rescue Errno::ECONNREFUSED
50
+ fail "Can not connect to your Foreman instance on #{_url}"
51
+ end
52
+ end
53
+
54
+ def _url
55
+ config = ::Upman::Core::Config.foreman_api
56
+ protocol = config[:ssl] ? 'https://' : 'http://'
57
+ "#{protocol}#{config[:host]}:#{config[:port]}/api/v#{config[:api_version]}/"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ module Upman
2
+ module Utils
3
+ class Files
4
+
5
+ end
6
+ end
7
+ end