inventory-server 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.travis.yml +12 -0
- data/Dockerfile +22 -0
- data/Dockerfile-passenger +18 -0
- data/Dockerfile-unicorn +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +32 -0
- data/Rakefile +2 -0
- data/bin/inventory-server +33 -0
- data/circle.yml +26 -0
- data/config.ru +8 -0
- data/docker/passenger/passenger.conf +15 -0
- data/docker/passenger/run-httpd.sh +8 -0
- data/docker/unicorn/nginx.conf +45 -0
- data/docker/unicorn/run-unicorn.sh +5 -0
- data/docker/unicorn/unicorn.conf +31 -0
- data/docker/unicorn/unicorn.rb +19 -0
- data/fig.yml +18 -0
- data/inventory-server.gemspec +45 -0
- data/lib/inventory/server.rb +30 -0
- data/lib/inventory/server/cli.rb +59 -0
- data/lib/inventory/server/config.rb +80 -0
- data/lib/inventory/server/email_parser.rb +28 -0
- data/lib/inventory/server/http_server.rb +49 -0
- data/lib/inventory/server/inventory_error.rb +9 -0
- data/lib/inventory/server/loader.rb +60 -0
- data/lib/inventory/server/logger.rb +16 -0
- data/lib/inventory/server/smtp_server.rb +36 -0
- data/lib/inventory/server/version.rb +5 -0
- data/plugins/facts_parser.rb +95 -0
- data/plugins/index.rb +56 -0
- data/plugins/json_schema_validator.rb +33 -0
- data/plugins/log_failures_on_disk.rb +35 -0
- data/public/.gitignore +0 -0
- data/spec/integration/fixtures/facter.xml +4543 -0
- data/spec/integration/http_spec.rb +24 -0
- data/spec/spec_helper.rb +96 -0
- data/spec/unit/cli_spec.rb +54 -0
- data/spec/unit/config_spec.rb +65 -0
- data/spec/unit/email_parser_spec.rb +53 -0
- data/spec/unit/facts_parser_spec.rb +176 -0
- data/spec/unit/fixtures/simple_plugin.rb +2 -0
- data/spec/unit/http_server_spec.rb +58 -0
- data/spec/unit/index_spec.rb +77 -0
- data/spec/unit/json_schema_validator_spec.rb +126 -0
- data/spec/unit/loader_spec.rb +34 -0
- data/spec/unit/log_failures_on_disk_spec.rb +50 -0
- data/spec/unit/server_spec.rb +11 -0
- data/spec/unit/smtp_server_spec.rb +68 -0
- data/tmp/.gitignore +0 -0
- metadata +434 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'middleware'
|
2
|
+
require "inventory/server/version"
|
3
|
+
require "inventory/server/loader"
|
4
|
+
require "inventory/server/config"
|
5
|
+
require "inventory/server/logger"
|
6
|
+
|
7
|
+
module Inventory
|
8
|
+
module Server
|
9
|
+
class Server
|
10
|
+
attr_reader :config, :middlewares
|
11
|
+
|
12
|
+
def initialize(cli_config)
|
13
|
+
config = Config.generate(cli_config)
|
14
|
+
@config = config
|
15
|
+
|
16
|
+
InventoryLogger.setup(config[:logger])
|
17
|
+
InventoryLogger.logger.level = config[:log_level]
|
18
|
+
|
19
|
+
# Dynamically load plugins from plugins_path
|
20
|
+
plugin_names = config[:plugins].split(',')
|
21
|
+
@middlewares = Middleware::Builder.new do
|
22
|
+
plugins = Loader.new(config).load_plugins(*plugin_names)
|
23
|
+
plugins.each {|klass|
|
24
|
+
use klass, config
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require "inventory/server/version"
|
3
|
+
|
4
|
+
module Inventory
|
5
|
+
module Server
|
6
|
+
|
7
|
+
class CLI
|
8
|
+
|
9
|
+
attr_reader :options
|
10
|
+
|
11
|
+
def initialize()
|
12
|
+
@options = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse!(args)
|
16
|
+
OptionParser.new do|opts|
|
17
|
+
opts.banner = "Usage: server [options]"
|
18
|
+
opts.separator ""
|
19
|
+
opts.separator "Specific options:"
|
20
|
+
|
21
|
+
opts.on("--host HOST", String, "IP or hostname to listen on") do |h|
|
22
|
+
@options[:host] = h
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("--smtp_port PORT", Integer, "SMTP port to listen on") do |p|
|
26
|
+
@options[:smtp_port] = p
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("-es", "--es_host URL", String, "ElasticSerach HTTP URL") do |url|
|
30
|
+
@options[:es_host] = url
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-o", "--output OUTPUT", String, "Log destination stdout/stderr/file") do |o|
|
34
|
+
@options[:logger] = o
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("-l", "--level LEVEL", ['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'], "log level that will be printed DEBUG/INFO/WARN/ERROR/FATAL") do |l|
|
38
|
+
@options[:log_level] = l
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-d", "--[no-]debug", "how more details, need log_level on DEBUG") do |d|
|
42
|
+
@options[:debug] = d
|
43
|
+
end
|
44
|
+
|
45
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
46
|
+
puts opts
|
47
|
+
exit
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on_tail("--version", "Show version") do
|
51
|
+
puts VERSION
|
52
|
+
exit
|
53
|
+
end
|
54
|
+
end.parse! args
|
55
|
+
@options
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'app_configuration'
|
2
|
+
require 'fileutils'
|
3
|
+
require "inventory/server/logger"
|
4
|
+
|
5
|
+
module Inventory
|
6
|
+
module Server
|
7
|
+
|
8
|
+
module Config
|
9
|
+
DEFAULTS = {
|
10
|
+
:host => '127.0.0.1',
|
11
|
+
:smtp_port => 2525,
|
12
|
+
:max_connections => 4,
|
13
|
+
:debug => false,
|
14
|
+
:es_host => 'http://localhost:9200',
|
15
|
+
:es_index_prefix => 'inventory_',
|
16
|
+
:failed_facts_dir => '/var/log/inventory/failures',
|
17
|
+
:logger => 'stdout',
|
18
|
+
:log_level => 'INFO',
|
19
|
+
:type_key => 'type',
|
20
|
+
:type_default => 'facts',
|
21
|
+
:version_key => 'version',
|
22
|
+
:version_default => '1-0-0',
|
23
|
+
:json_schema_dir => '/etc/inventory/json_schema',
|
24
|
+
:plugins_path => '',
|
25
|
+
:plugins => 'log_failures_on_disk,facts_parser,json_schema_validator,index',
|
26
|
+
}
|
27
|
+
|
28
|
+
def self.generate(cli_config)
|
29
|
+
|
30
|
+
global = AppConfiguration.new('inventory.yml') do
|
31
|
+
base_global_path '/etc'
|
32
|
+
use_env_variables true
|
33
|
+
prefix 'inventory' # ENV prefix: INVENTORY_XXXXX
|
34
|
+
end
|
35
|
+
|
36
|
+
config = {}
|
37
|
+
|
38
|
+
DEFAULTS.each{|sym, default_value|
|
39
|
+
|
40
|
+
val = cli_config[sym] || global[sym.to_s] || default_value
|
41
|
+
result = val
|
42
|
+
|
43
|
+
# cast config values (ENV values are strings only)
|
44
|
+
if default_value.is_a? Integer
|
45
|
+
result = val.to_i
|
46
|
+
elsif !!default_value == default_value
|
47
|
+
# Boolean
|
48
|
+
if val == true || val =~ /^(true|t|yes|y|1)$/i
|
49
|
+
result = true
|
50
|
+
elsif val == false || val.blank? || val =~ /^(false|f|no|n|0)$/i
|
51
|
+
result = false
|
52
|
+
end
|
53
|
+
# Logging
|
54
|
+
elsif val == 'stdout'
|
55
|
+
result = $stdout
|
56
|
+
elsif val == 'stderr'
|
57
|
+
result = $stderr
|
58
|
+
# Logging Level
|
59
|
+
elsif val == 'INFO'
|
60
|
+
result = Logger::INFO
|
61
|
+
elsif val == 'DEBUG'
|
62
|
+
result = Logger::DEBUG
|
63
|
+
elsif val == 'WARN'
|
64
|
+
result = Logger::WARN
|
65
|
+
elsif val == 'ERROR'
|
66
|
+
result = Logger::ERROR
|
67
|
+
elsif val == 'FATAL'
|
68
|
+
result = Logger::FATAL
|
69
|
+
end
|
70
|
+
config[sym] = result
|
71
|
+
}
|
72
|
+
|
73
|
+
FileUtils.mkdir_p config[:failed_facts_dir]
|
74
|
+
|
75
|
+
config
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "mail"
|
2
|
+
require 'inventory/server/inventory_error'
|
3
|
+
require "inventory/server/logger"
|
4
|
+
|
5
|
+
module Inventory
|
6
|
+
module Server
|
7
|
+
class EmailParser
|
8
|
+
def self.parse(message)
|
9
|
+
InventoryLogger.logger.info "Email parser"
|
10
|
+
|
11
|
+
# Parse the email
|
12
|
+
email = Mail.read_from_string(message)
|
13
|
+
|
14
|
+
# Use email subject as an ID
|
15
|
+
email_subject = email.subject
|
16
|
+
raise InventoryError.new "email subject is missing" if email_subject.nil? || email_subject.empty?
|
17
|
+
id = email_subject
|
18
|
+
|
19
|
+
# Decode the email body
|
20
|
+
email_body = email.body.decoded
|
21
|
+
raise InventoryError.new "email body is missing" if email_body.nil? || email_body.empty?
|
22
|
+
body = email_body
|
23
|
+
|
24
|
+
return id, body
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
require 'json'
|
3
|
+
require 'inventory/server/inventory_error'
|
4
|
+
require "inventory/server/logger"
|
5
|
+
|
6
|
+
module Inventory
|
7
|
+
module Server
|
8
|
+
class HTTPServer < Sinatra::Base
|
9
|
+
|
10
|
+
configure do
|
11
|
+
logger = Object.new.tap do |proxy|
|
12
|
+
def proxy.<<(message)
|
13
|
+
InventoryLogger.logger.info message
|
14
|
+
end
|
15
|
+
def proxy.write(message)
|
16
|
+
InventoryLogger.logger.info message
|
17
|
+
end
|
18
|
+
def proxy.flush; end
|
19
|
+
end
|
20
|
+
|
21
|
+
use Rack::CommonLogger, logger
|
22
|
+
|
23
|
+
before {
|
24
|
+
env["rack.errors"] = logger
|
25
|
+
}
|
26
|
+
|
27
|
+
Rack::Utils.key_space_limit = 262144
|
28
|
+
|
29
|
+
HTTPServer.set :raise_errors, false
|
30
|
+
HTTPServer.set :dump_errors, false
|
31
|
+
end
|
32
|
+
|
33
|
+
post "/api/v1/facts/:id" do
|
34
|
+
content_type :json
|
35
|
+
id= params[:id]
|
36
|
+
env[:id] = id
|
37
|
+
InventoryLogger.logger.context_id = id
|
38
|
+
|
39
|
+
request.body.rewind
|
40
|
+
settings.middlewares.call(:id => id, :body => request.body.read, :config => settings.config)
|
41
|
+
{:id => id, :ok => true, :status => 200}.to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
error 400..500 do
|
45
|
+
{:id => env[:id], :ok => false, :status => status, :message => env['sinatra.error']}.to_json
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'inventory/server/inventory_error'
|
2
|
+
require 'inventory/server/logger'
|
3
|
+
|
4
|
+
PLUGINS_DIR = Pathname.new(File.join File.dirname(__FILE__), '..', '..', '..', 'plugins').cleanpath
|
5
|
+
|
6
|
+
module Inventory
|
7
|
+
module Server
|
8
|
+
class Loader
|
9
|
+
def initialize(config)
|
10
|
+
@plugins_path = [PLUGINS_DIR] + config[:plugins_path].split(',')
|
11
|
+
@plugins_path.each { |path|
|
12
|
+
raise InventoryError.new "plugins_path #{path} not found" unless File.directory? path
|
13
|
+
}
|
14
|
+
@loaded = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# search for plugins in the plugins_path and return a hash of plugin_filename:plugin_klass
|
18
|
+
def load_plugins(*plugins)
|
19
|
+
plugin_klasses = []
|
20
|
+
plugins.each {|plugin|
|
21
|
+
p = nil
|
22
|
+
@plugins_path.each {|plugin_path|
|
23
|
+
filepath = File.join plugin_path, "#{plugin}.rb"
|
24
|
+
next unless File.file? filepath
|
25
|
+
load_file filepath
|
26
|
+
klass_name = classify(plugin)
|
27
|
+
p = Object.const_get("Inventory").const_get("Server").const_get(klass_name)
|
28
|
+
}
|
29
|
+
raise InventoryError.new "Plugin #{plugin} not found" if !p
|
30
|
+
plugin_klasses << p
|
31
|
+
}
|
32
|
+
return plugin_klasses
|
33
|
+
end
|
34
|
+
|
35
|
+
# Load a ruby file if not already loaded
|
36
|
+
def load_file(file)
|
37
|
+
return if @loaded.include? file
|
38
|
+
|
39
|
+
begin
|
40
|
+
@loaded << file
|
41
|
+
kernel_load(file)
|
42
|
+
rescue => e
|
43
|
+
@loaded.delete(file)
|
44
|
+
InventoryLogger.logger.error("Fail to load #{file}: #{e}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Usefull for tests
|
49
|
+
def kernel_load(file)
|
50
|
+
InventoryLogger.logger.info "Load #{file}"
|
51
|
+
require file
|
52
|
+
end
|
53
|
+
|
54
|
+
# transform a snake case string into a upper camel case string
|
55
|
+
def classify(str)
|
56
|
+
str.split('_').collect!{ |w| w.capitalize }.join
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "midi-smtp-server"
|
3
|
+
require "inventory/server/email_parser"
|
4
|
+
require 'inventory/server/inventory_error'
|
5
|
+
require "inventory/server/logger"
|
6
|
+
|
7
|
+
module Inventory
|
8
|
+
module Server
|
9
|
+
|
10
|
+
# Create an SMTP Server
|
11
|
+
class SMTPServer < MidiSmtpServer
|
12
|
+
|
13
|
+
def initialize(config, middlewares)
|
14
|
+
@config = config
|
15
|
+
@middlewares = middlewares
|
16
|
+
@audit = @config[:debug]
|
17
|
+
super(config[:smtp_port], config[:host], config[:max_connections])
|
18
|
+
end
|
19
|
+
|
20
|
+
def on_message_data_event(ctx)
|
21
|
+
begin
|
22
|
+
# execute middlewares
|
23
|
+
id, body = EmailParser.parse(ctx[:message])
|
24
|
+
InventoryLogger.logger.context_id = id
|
25
|
+
@middlewares.call(:id => id, :body => body)
|
26
|
+
rescue => e
|
27
|
+
# dot not raise the error to avoid the SMTP server relay to defer malformed emails
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def log(msg)
|
32
|
+
InventoryLogger.logger.debug msg
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
require "base64"
|
5
|
+
require 'libxml_to_hash'
|
6
|
+
|
7
|
+
require 'ensure/encoding'
|
8
|
+
require 'inventory/server/inventory_error'
|
9
|
+
require "inventory/server/logger"
|
10
|
+
|
11
|
+
module Inventory
|
12
|
+
module Server
|
13
|
+
class FactsParser
|
14
|
+
def initialize(app, config)
|
15
|
+
@app = app
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
InventoryLogger.logger.info "Facts parser"
|
21
|
+
body = env[:body]
|
22
|
+
raise InventoryError.new "body missing" if body.nil? || body.empty?
|
23
|
+
|
24
|
+
format = guess_format body
|
25
|
+
raise InventoryError.new "bad format" if !format
|
26
|
+
|
27
|
+
env[:facts] = parse(format, body)
|
28
|
+
@app.call(env)
|
29
|
+
end
|
30
|
+
|
31
|
+
# guess about the format of `str`
|
32
|
+
def guess_format(str)
|
33
|
+
case str
|
34
|
+
when /\A\s*[\[\{]/ then :json
|
35
|
+
when /\A\s*</ then :xml
|
36
|
+
when /\A---\s/ then :yaml
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
# Parse str into a hash, the format will ne guessed
|
42
|
+
def parse(format, str)
|
43
|
+
case format
|
44
|
+
when :xml
|
45
|
+
# XML validation
|
46
|
+
|
47
|
+
hash = nil
|
48
|
+
begin
|
49
|
+
hash = Hash.from_libxml! str
|
50
|
+
xml = decode_base64(hash)
|
51
|
+
keys = xml.keys
|
52
|
+
if keys.length == 1
|
53
|
+
xml = xml[keys[0]]
|
54
|
+
end
|
55
|
+
raise "Expect < found #{xml.text} " if xml.is_a?(LibXmlNode) and xml.text != ""
|
56
|
+
return xml
|
57
|
+
rescue => e
|
58
|
+
raise $!, "Invalid XML #{$!}", $!.backtrace
|
59
|
+
end
|
60
|
+
when :json
|
61
|
+
JSON.parse str
|
62
|
+
when :yaml
|
63
|
+
YAML.load str
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# decode base64 if present in this deep structure
|
68
|
+
def decode_base64(something)
|
69
|
+
if something.is_a?(Hash)
|
70
|
+
return nil if something.empty?
|
71
|
+
something.each {|key, value|
|
72
|
+
something[key] = decode_base64(value)
|
73
|
+
}
|
74
|
+
elsif something.is_a?(Array)
|
75
|
+
return nil if something.empty?
|
76
|
+
something = something.map {|value|
|
77
|
+
decode_base64(value)
|
78
|
+
}
|
79
|
+
elsif something.is_a?(String) and something.include? "__base64__"
|
80
|
+
something.slice!("__base64__")
|
81
|
+
fix_bad_encoding Base64.decode64(something);
|
82
|
+
else
|
83
|
+
something
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def fix_bad_encoding(str)
|
88
|
+
str.ensure_encoding('UTF-8',
|
89
|
+
:external_encoding => [Encoding::UTF_8, Encoding::ISO_8859_1],
|
90
|
+
:invalid_characters => :transcode
|
91
|
+
)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|