onesnooper-server 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,85 @@
1
+ # Request handler for validating and dispatching
2
+ # datagram processing on defined types of datagrams.
3
+ # Invalid or unknown datagrams will be processed by
4
+ # the `OnesnooperServer::Datagrams::InvalidDatagram`
5
+ # sub-handler.
6
+ class OnesnooperServer::RequestHandler
7
+
8
+ # Validation constant for incoming datagrams
9
+ MONITORING_DATA_REGEXP = /^MONITOR\s(?<result>[[:alpha:]]+)\s(?<host_id>\d+)\s(?<payload>\S+)$/
10
+
11
+ # Registered allowed datagram processing classes & the default fallback
12
+ DATAGRAMS = {
13
+ "SUCCESS" => ::OnesnooperServer::Datagrams::SuccessDatagram,
14
+ "FAILURE" => ::OnesnooperServer::Datagrams::FailureDatagram,
15
+ }
16
+ DATAGRAMS.default = ::OnesnooperServer::Datagrams::InvalidDatagram
17
+
18
+ # Registered allowed stores for monitoring data
19
+ STORES = {
20
+ "mongodb" => ::OnesnooperServer::Stores::MongodbStore,
21
+ "mysql" => ::OnesnooperServer::Stores::MysqlStore,
22
+ "sqlite" => ::OnesnooperServer::Stores::SqliteStore,
23
+ }
24
+ STORES.default = ::OnesnooperServer::Stores::InvalidStore
25
+
26
+ # Static parsing method for identifying types of incoming datagrams
27
+ # and choosing the right processing class for each datagram. Always
28
+ # returns an instance responding to `run(callback)`.
29
+ #
30
+ # @param monitoring_datagram [Object] datagram payload for processing
31
+ # @param source_ip [String] IP address of the client
32
+ # @param source_port [String] port number of the client
33
+ def self.parse(monitoring_datagram, source_ip, source_port)
34
+ unless valid_data?(monitoring_datagram)
35
+ ::OnesnooperServer::Log.fatal "[#{self.name}] Dropping invalid monitoring data #{monitoring_datagram.inspect}"
36
+ return DATAGRAMS.default.new
37
+ end
38
+
39
+ unless valid_peer?(source_ip)
40
+ ::OnesnooperServer::Log.warn "[#{self.name}] Dropping monitoring data from #{source_ip}, not allowed!"
41
+ return DATAGRAMS.default.new
42
+ end
43
+
44
+ match_data = monitoring_datagram.match(MONITORING_DATA_REGEXP)
45
+ return DATAGRAMS.default.new unless match_data
46
+
47
+ DATAGRAMS[match_data[:result]].new({
48
+ host_id: match_data[:host_id],
49
+ payload: match_data[:payload],
50
+ stores: store_instances,
51
+ })
52
+ end
53
+
54
+ private
55
+
56
+ # Identifies incoming datagrams as valid for further processing.
57
+ # Datagrams must be non-nil and String-like.
58
+ #
59
+ # @param monitoring_datagram [Object] incoming datagram for validation
60
+ # @return [Boolean] result
61
+ def self.valid_data?(monitoring_datagram)
62
+ monitoring_datagram && monitoring_datagram.kind_of?(String)
63
+ end
64
+
65
+ # Matches provided IP address against the list of known
66
+ # allowed peers.
67
+ #
68
+ # @param source_ip [String] IP address to match
69
+ # @return [Boolean] result
70
+ def self.valid_peer?(source_ip)
71
+ ::OnesnooperServer::Settings.allowed_sources.include? source_ip
72
+ end
73
+
74
+ # Retrieves a list of instances for allowed store backends.
75
+ #
76
+ # @return [Array] list of store instances
77
+ def self.store_instances
78
+ ::OnesnooperServer::Settings.store.collect do |store_name|
79
+ STORES[store_name].new(
80
+ ::OnesnooperServer::Settings.stores.respond_to?(store_name) ? ::OnesnooperServer::Settings.stores.send(store_name) : {}
81
+ )
82
+ end
83
+ end
84
+
85
+ end
@@ -0,0 +1,12 @@
1
+ # Wraps persistent setting taken from configuration
2
+ # files. This singleton is available throughout the
3
+ # whole application environment.
4
+ class OnesnooperServer::Settings < ::Settingslogic
5
+ gem_root = File.expand_path '../../..', __FILE__
6
+
7
+ source "#{ENV['HOME']}/.onesnooper-server" if File.readable?("#{ENV['HOME']}/.onesnooper-server")
8
+ source "/etc/onesnooper-server/onesnooper-server.yml" if File.readable?("/etc/onesnooper-server/onesnooper-server.yml")
9
+ source "#{gem_root}/config/onesnooper-server.yml"
10
+
11
+ namespace ENV['RAILS_ENV'] ? ENV['RAILS_ENV'] : 'production'
12
+ end
@@ -0,0 +1,65 @@
1
+ require 'sequel'
2
+
3
+ # Class wrapping common features of all SQL-based
4
+ # stores. Using 'sequel' to abstract differences.
5
+ class OnesnooperServer::SqlStore < ::OnesnooperServer::Store
6
+
7
+ # constant table name
8
+ SQL_TABLE_NAME = :one_monitoring
9
+
10
+ def save!(timestamp, data)
11
+ ::OnesnooperServer::Log.debug "[#{self.class.name}] Saving #{timestamp.to_s} => #{data.inspect}"
12
+ fail "DB connection has to be initialized from subclasses, " \
13
+ "::OnesnooperServer::SqlStore cannot be used directly!" unless @db_conn
14
+
15
+ if insert_data = data_in_vm_groups(timestamp, data)
16
+ @db_conn[SQL_TABLE_NAME].multi_insert(insert_data)
17
+ else
18
+ ::OnesnooperServer::Log.warn "[#{self.class.name}] Skipping SQL INSERT for an empty dataset"
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ # Converts parsed datagram payload into table rows
25
+ # for direct insert into the DB.
26
+ #
27
+ # @param timestamp [DateTime] payload time stamp
28
+ # @param data [Hash] hash-like parsed payload structure
29
+ # @return [Array, NilClass] array with row hashes
30
+ def data_in_vm_groups(timestamp, data)
31
+ data_ary = []
32
+
33
+ data['VM'].each do |vm_on_host|
34
+ next if vm_on_host.blank?
35
+ data_ary << {
36
+ :timestamp => timestamp,
37
+ :vm_id => vm_on_host['ID'],
38
+ :vm_deploy_id => vm_on_host['DEPLOY_ID'],
39
+ :vm_netrx => vm_on_host['POLL']['NETRX'],
40
+ :vm_nettx => vm_on_host['POLL']['NETTX'],
41
+ :vm_used_cpu => vm_on_host['POLL']['USEDCPU'],
42
+ :vm_used_memory => vm_on_host['POLL']['USEDMEMORY'],
43
+ :vm_state => vm_on_host['POLL']['STATE'],
44
+ :host_name => data['HOSTNAME'],
45
+ :host_arch => data['ARCH'],
46
+ :host_model => data['MODELNAME'],
47
+ :host_hypervisor => data['HYPERVISOR'],
48
+ :host_ds_total => data['DS_LOCATION_TOTAL_MB'],
49
+ :host_ds_used => data['DS_LOCATION_USED_MB'],
50
+ :host_ds_free => data['DS_LOCATION_FREE_MB'],
51
+ :host_total_cpu => data['TOTALCPU'],
52
+ :host_cpu_speed => data['CPUSPEED'],
53
+ :host_used_cpu => data['USEDCPU'],
54
+ :host_free_cpu => data['FREECPU'],
55
+ :host_total_memory => data['TOTALMEMORY'],
56
+ :host_free_memory => data['FREEMEMORY'],
57
+ :host_used_memory => data['USEDMEMORY'],
58
+ :one_version => data['VERSION'],
59
+ }
60
+ end
61
+
62
+ data_ary.empty? ? nil : data_ary
63
+ end
64
+
65
+ end
@@ -0,0 +1,23 @@
1
+ # Base class for all backend data stores. Implements key
2
+ # method stubs required for all specific backend data store
3
+ # implementations.
4
+ class OnesnooperServer::Store
5
+
6
+ # Initializes data store instance with given parameters.
7
+ #
8
+ # @param params [Hash] hash-like structure with parameters
9
+ def initialize(params = {})
10
+ @params = params
11
+ end
12
+
13
+ # Saves given data set into the underlying data store.
14
+ # Behavior is determined by the underlying data store
15
+ # implementation.
16
+ #
17
+ # @param timestamp [DateTime] current time
18
+ # @param data [Hash] data to be saved in the data store
19
+ def save!(timestamp, data)
20
+ fail "This method needs to be implemented in subclasses"
21
+ end
22
+
23
+ end
@@ -0,0 +1,7 @@
1
+ # Module housing available backend implementations for
2
+ # data stores. All stores must implement basic methods
3
+ # outlined in the `OnesnooperServer::Store` class.
4
+ module OnesnooperServer::Stores; end
5
+
6
+ # Load all available store types
7
+ Dir.glob(File.join(File.dirname(__FILE__), 'stores', "*.rb")) { |store_file| require store_file.chomp('.rb') }
@@ -0,0 +1,10 @@
1
+ # Placeholder store for invalid configuration handling and
2
+ # testing purposes.
3
+ class OnesnooperServer::Stores::InvalidStore < ::OnesnooperServer::Store
4
+
5
+ def save!(timestamp, data)
6
+ ::OnesnooperServer::Log.fatal "[#{self.class.name}] This is a dummy store, do not use it!"
7
+ fail "InvalidStore selected as fallback, check your configuration!"
8
+ end
9
+
10
+ end
@@ -0,0 +1,23 @@
1
+ require 'mongo'
2
+
3
+ # MongoDB-based store for production deployments and dynamic
4
+ # document structure.
5
+ class OnesnooperServer::Stores::MongodbStore < ::OnesnooperServer::Store
6
+
7
+ # constant collection name
8
+ MONGO_COLL_NAME = 'one_monitoring'
9
+
10
+ def initialize(params = {})
11
+ super
12
+ @db_conn = ::Mongo::MongoClient.new(params[:host], params[:port])
13
+ @db_active_db = @db_conn.db(params[:database])
14
+ @db_coll = @db_active_db.create_collection(MONGO_COLL_NAME)
15
+ end
16
+
17
+ def save!(timestamp, data)
18
+ ::OnesnooperServer::Log.debug "[#{self.class.name}] Saving #{timestamp.to_s} => #{data.inspect}"
19
+ data['TIMESTAMP'] = timestamp.to_time.utc
20
+ @db_coll.insert data
21
+ end
22
+
23
+ end
@@ -0,0 +1,17 @@
1
+ # MySQL-based store for production deployments with static
2
+ # table structure.
3
+ class OnesnooperServer::Stores::MysqlStore < ::OnesnooperServer::SqlStore
4
+
5
+ def initialize(params = {})
6
+ super
7
+ @db_conn = ::Sequel.connect(
8
+ :adapter => 'mysql2',
9
+ :database => params[:database],
10
+ :user => params[:username],
11
+ :password => params[:password],
12
+ :host => params[:host],
13
+ :port => params[:port],
14
+ )
15
+ end
16
+
17
+ end
@@ -0,0 +1,10 @@
1
+ # Sqlite3-based store for testing and small deployments.
2
+ class OnesnooperServer::Stores::SqliteStore < ::OnesnooperServer::SqlStore
3
+
4
+ def initialize(params = {})
5
+ super
6
+ @db_path = "sqlite://#{params[:database_file]}"
7
+ @db_conn = ::Sequel.connect(@db_path)
8
+ end
9
+
10
+ end
@@ -0,0 +1,50 @@
1
+ # Handler for incoming UDP datagrams. Implements required methods
2
+ # for direct use with EventMachine listeners. This handler will not
3
+ # respond to incoming datagrams as its primary purpose is to record
4
+ # mirrored monitoring traffic.
5
+ class OnesnooperServer::UDPHandler < ::EventMachine::Connection
6
+
7
+ DATAGRAM_PREFIX = 'MONITOR'
8
+
9
+ # Receives data and triggers processing of the given
10
+ # datagram. Main internal processing triggered from this
11
+ # method should always happen asynchronously (i.e., using
12
+ # EventMachine.defer or Deferrable classes).
13
+ #
14
+ # @param monitoring_datagram [String] incoming data payload
15
+ def receive_data(monitoring_datagram)
16
+ monitoring_datagram.chomp!
17
+ source_port, source_ip = Socket.unpack_sockaddr_in(get_peername)
18
+ unless monitoring_datagram.start_with?(DATAGRAM_PREFIX)
19
+ ::OnesnooperServer::Log.warn "[#{self.class.name}] Discarding datagram from " \
20
+ "#{source_ip}:#{source_port} (not #{DATAGRAM_PREFIX})"
21
+ return
22
+ end
23
+
24
+ ::OnesnooperServer::Log.debug "[#{self.class.name}] Received #{monitoring_datagram.inspect} " \
25
+ "from #{source_ip}:#{source_port}"
26
+ ::OnesnooperServer::RequestHandler.parse(
27
+ monitoring_datagram,
28
+ source_ip,
29
+ source_port
30
+ ).run(callback)
31
+ end
32
+
33
+ private
34
+
35
+ # Callable processing callback. As this implementation does
36
+ # not send responses to incoming datagrams, this methods is
37
+ # for logging and debugging purposes only.
38
+ def callback
39
+ def_deferr = ::EventMachine::DefaultDeferrable.new
40
+ proc_callback = Proc.new { |response| ::OnesnooperServer::Log.debug(
41
+ "[#{self.class.name}] Handled as: #{response}"
42
+ ) }
43
+
44
+ def_deferr.callback &proc_callback
45
+ def_deferr.errback &proc_callback
46
+
47
+ def_deferr
48
+ end
49
+
50
+ end
@@ -0,0 +1,4 @@
1
+ module OnesnooperServer
2
+ # Static version information
3
+ VERSION = "0.0.1" unless defined?(::OnesnooperServer::VERSION)
4
+ end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # -------------------------------------------------------------------------- #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may #
5
+ # not use this file except in compliance with the License. You may obtain #
6
+ # a copy of the License at #
7
+ # #
8
+ # http://www.apache.org/licenses/LICENSE-2.0 #
9
+ # #
10
+ # Unless required by applicable law or agreed to in writing, software #
11
+ # distributed under the License is distributed on an "AS IS" BASIS, #
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
13
+ # See the License for the specific language governing permissions and #
14
+ # limitations under the License. #
15
+ #--------------------------------------------------------------------------- #
16
+
17
+ require 'rubygems'
18
+ require 'sequel'
19
+
20
+ Sequel.migration do
21
+ up do
22
+ create_table(:one_monitoring) do
23
+ #
24
+ primary_key :id, :type => Bignum
25
+ DateTime :timestamp, :null => false, :index => true
26
+
27
+ #
28
+ String :vm_id, :null => false, :index => true
29
+ String :vm_deploy_id, :null => false, :index => true
30
+ BigDecimal :vm_netrx, :default => 0
31
+ BigDecimal :vm_nettx, :default => 0
32
+ Float :vm_used_cpu, :default => 0.0
33
+ BigDecimal :vm_used_memory, :default => 0
34
+ String :vm_state, :null => false, :index => true
35
+
36
+ #
37
+ String :host_name, :null => false, :index => true
38
+ String :host_arch, :default => 'unknown'
39
+ String :host_model, :default => 'unknown'
40
+ String :host_hypervisor, :null => false
41
+ BigDecimal :host_ds_total, :default => 0
42
+ BigDecimal :host_ds_used, :default => 0
43
+ BigDecimal :host_ds_free, :default => 0
44
+ Integer :host_total_cpu, :null => false
45
+ Integer :host_cpu_speed, :default => 0
46
+ Integer :host_used_cpu, :default => 0
47
+ Integer :host_free_cpu, :default => 0
48
+ BigDecimal :host_total_memory, :null => false
49
+ BigDecimal :host_free_memory, :default => 0
50
+ BigDecimal :host_used_memory, :default => 0
51
+
52
+ #
53
+ String :one_version, :null => false
54
+ end
55
+ end
56
+
57
+ down do
58
+ drop_table(:one_monitoring)
59
+ end
60
+ end
@@ -0,0 +1,41 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'onesnooper_server/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'onesnooper-server'
8
+ spec.version = OnesnooperServer::VERSION
9
+ spec.authors = ['Boris Parak']
10
+ spec.email = ['parak@cesnet.cz']
11
+ spec.summary = 'Simple server snooping on and recording OpenNebula\'s VM & Host monitoring traffic'
12
+ spec.description = 'Simple server snooping on and recording OpenNebula\'s VM & Host monitoring traffic'
13
+ spec.homepage = 'https://github.com/arax/onesnooper-server'
14
+ spec.license = 'Apache License, Version 2.0'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+ spec.required_ruby_version = '>= 1.9.3'
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.6'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rspec', '~> 3.0.0'
25
+ spec.add_development_dependency 'simplecov', '~> 0.9.0'
26
+ spec.add_development_dependency 'rubygems-tasks', '~> 0.2.4'
27
+
28
+ # internals
29
+ spec.add_runtime_dependency 'eventmachine', '~> 1.0.4'
30
+ spec.add_runtime_dependency 'activesupport', '~> 4.2.0'
31
+ spec.add_runtime_dependency 'settingslogic', '~> 2.0.9'
32
+
33
+ # SQL DB connectors
34
+ spec.add_runtime_dependency 'sequel', '~> 4.18.0'
35
+ spec.add_runtime_dependency 'sqlite3', '~> 1.3.10'
36
+ spec.add_runtime_dependency 'mysql2', '~> 0.3.17'
37
+
38
+ # NoSQL DB connectors
39
+ spec.add_runtime_dependency 'mongo', '~> 1.11.1'
40
+ spec.add_runtime_dependency 'bson_ext', '~> 1.11.1'
41
+ end
File without changes
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe OnesnooperServer do
4
+ subject { onesnooper_server }
5
+
6
+ it 'does something'
7
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+
3
+ # enable coverage reports
4
+ if ENV['COVERAGE']
5
+ require 'simplecov'
6
+
7
+ SimpleCov.add_filter "/spec/"
8
+ SimpleCov.start
9
+ end
10
+
11
+ require 'onesnooper-server'
12
+
13
+ Dir["#{File.dirname(__FILE__)}/helpers/*.rb"].each {|file| require file }