onesnooper-server 0.0.1

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,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 }