sensu_generator 0.0.25

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: aa5b40932e162763cc44bfc51cd2051806b3d46a
4
+ data.tar.gz: 23194a317cde4a606d1358ef06fa70c5e7974bcd
5
+ SHA512:
6
+ metadata.gz: 5d6afad7404f580509f9f189f92fc70b5ca0c11158fa4561325ed7b2aea4b29abebbd064ebfc5be466693a8de7f5304ae9ea8084c5c90a8f6f9833d308e5bb56
7
+ data.tar.gz: d818013b9f39db3898ba74924063754c505ed5327f0bc473a05f3e37c922272c25c8724270181b2373180d2d129e3ffcc8adc24dcaf09ddabe61951b9b23248f
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /work
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.11.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in sensu_generator.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # SensuGenerator
2
+
3
+ SensuGenerator is an intermediate layer between Consul and Sensu. It helps to set up dynamic monitoring systems. It generates check configurations from ERB templates according to *tags* listed in the KV and Consul service properties. It watches for changes Consul services state and special key in the KV. It triggers the following:
4
+ Sensu check configuration files are generated from the templates, the result will be synced via *rsync* and Sensu servers will be restarted using http Supervisord API. All files are generated when application starts and only changes will be processed.
5
+
6
+ All service checks *tag* are stored in the Consul Key-Value storage in *service/kv_tags_path* path, default *kv_tags_path* is "checks". Tag is the beginning of a service check template name and should be specified as a part of the template name in the Consul KV storage. Note that value should be comma-separated tags list. Rsync repo shuold be named as sensu service name.
7
+
8
+ It can can be used master server with multiple clients which send processed templates via tcp.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'sensu_generator'
16
+ ```
17
+
18
+ Install it yourself as:
19
+
20
+ $ gem install sensu_generator
21
+
22
+ ## Usage
23
+
24
+ sensu_generator start|stop|status|run -- [options]
25
+
26
+ ##### Example:
27
+
28
+ **consul_url***/kv/nginx/checks*
29
+ ```
30
+ check-http, check-tcp
31
+ ```
32
+
33
+ Use ***svc*** (contains service data form consul) and ***check*** (contains *tag* name) in the ERB template.
34
+ ***svc.kv_svc_props(key: key)*** can be used to access to ***svc/key*** data.
35
+ If key is not specified it will be requested the whole ***svc/*** folder.
36
+
37
+ Use Slack as notifier if you want.
38
+
39
+ ##### Check ERB template example:
40
+
41
+ ```
42
+ {
43
+ "checks": {
44
+ <% svc.properties.each do |instance| %>
45
+ <% next if instance.ServiceTags.include? "udp" %>
46
+ "check-ports-tcp-<%= "#{svc.name}-#{instance.ServiceAddress}-#{instance.ServicePort}" %>": {
47
+ "command": "check-ports.rb -h <%= instance.ServiceAddress %> -p <%= instance.ServicePort %>",
48
+ "subscribers": ["roundrobin:sensu-checker-node"],
49
+ "handlers": ["slack"],
50
+ "source": "<%= svc.name %>.service"
51
+ }<%= "," if instance != svc.properties.last %>
52
+ <% end %>
53
+ }
54
+ }
55
+
56
+
57
+ ```
58
+
59
+ ## Configuration
60
+
61
+ ##### server configuration:
62
+
63
+ ```
64
+ "mode": "server",
65
+ "server": {
66
+ "addr": "", //ip address to listen or left it empty to listen on 0.0.0.0
67
+ "port": 12345 //listen port
68
+ }
69
+ ```
70
+
71
+ ##### client configuration:
72
+
73
+ ```
74
+ "mode": "client",
75
+ "server": {
76
+ "addr": "", //ip address or domain to connect to
77
+ "port": 12345 //server port
78
+ }
79
+ ```
80
+
81
+ See *sensu-generator.config.example* for more information.
82
+
83
+ ## Development
84
+
85
+ ## Contributing
86
+
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aksentyev/sensu_generator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sensu_generator"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ require 'sensu_generator'
3
+ require 'optparse'
4
+ require 'daemons'
5
+
6
+ module SensuGenerator
7
+ class << self
8
+ def parse_args!
9
+ args = ARGV.dup
10
+
11
+ # get elements after '--' because of Daemons
12
+ args = args[(args.index('--')+1)..-1] if args.include? ('--')
13
+ config = nil
14
+ optparse = OptionParser.new do |opts|
15
+ opts.banner = "sensu-generator run|start|stop|status -- [options]"
16
+
17
+ opts.on("-c", "--config File", String, "Path to config file") do |item|
18
+ config = item
19
+ end
20
+ opts.on_tail("--version", "Show version") do
21
+ puts VERSION
22
+ exit
23
+ end
24
+
25
+ opts.on_tail("-h", "--help", "Show this message") do
26
+ puts opts
27
+ exit
28
+ end
29
+ end
30
+
31
+ optparse.parse!(args)
32
+ config ? File.expand_path(config) : nil
33
+ end
34
+
35
+ def run(config_file = nil)
36
+ config = Config.new(config_file)
37
+ logger = Logger.new(config.get[:logger])
38
+ logger.level = eval("Logger::#{config.get[:logger][:log_level].upcase}")
39
+ notifier = Notifier.new(config.get[:slack])
40
+ trigger = Trigger.new
41
+
42
+ Application.new(config: config, logger: logger, notifier: notifier, trigger: trigger).run
43
+ rescue => e
44
+ msg = %("Sensu_generator exited with non-zero code.\n #{e.to_s} \n#{e.backtrace.join("\n\t")}")
45
+ Logger.new(file: STDOUT).fatal msg
46
+ end
47
+ end
48
+ end
49
+
50
+ config = SensuGenerator::parse_args!
51
+
52
+ Daemons.run_proc(__FILE__) do
53
+ SensuGenerator::run(config)
54
+ end
@@ -0,0 +1,134 @@
1
+ require 'thread'
2
+
3
+ module SensuGenerator
4
+ class Application
5
+ class << self
6
+ def logger
7
+ @@logger
8
+ end
9
+
10
+ def notifier
11
+ @@notifier
12
+ end
13
+
14
+ def config
15
+ @@config
16
+ end
17
+
18
+ def trigger
19
+ @@trigger
20
+ end
21
+ end
22
+
23
+ def initialize(config:, logger:, notifier:, trigger:)
24
+ @@logger = logger
25
+ @@notifier = notifier
26
+ @@config = config
27
+ @@trigger = trigger
28
+ @threads = []
29
+ end
30
+
31
+ def logger
32
+ @@logger
33
+ end
34
+
35
+ def notifier
36
+ @@notifier
37
+ end
38
+
39
+ def config
40
+ @@config
41
+ end
42
+
43
+ def trigger
44
+ @@trigger
45
+ end
46
+
47
+ def run_restarter
48
+ logger.info "Starting restarter..."
49
+ loop do
50
+ logger.info 'Restarter is alive!'
51
+ if restarter.need_to_apply_new_configs?
52
+ restarter.perform_restart
53
+ end
54
+ sleep 60
55
+ end
56
+ rescue => e
57
+ raise ApplicationError, "Restarter error:\n\t #{e.to_s}\n\t #{e.backtrace}"
58
+ end
59
+
60
+ def run_generator
61
+ logger.info "Starting generator..."
62
+ generator.flush_results if config.get[:mode] == 'server'
63
+ state = ConsulState.new
64
+ loop do
65
+ logger.info 'Generator is alive!'
66
+ if state.changed? && state.actualized?
67
+ generator.services = state.changes
68
+ list = generator.generate!
69
+ logger.info "#{list.size} files processed: #{list.join(', ')}"
70
+ if config.get[:mode] == 'server' && list.empty? && state.changes.any? { |svc| svc.name == config.get[:sensu][:service] }
71
+ logger.info "Sensu-server service state was changed"
72
+ trigger.touch
73
+ end
74
+ end
75
+ sleep 60
76
+ state.actualize
77
+ end
78
+ rescue => e
79
+ raise ApplicationError, "Generator error:\n\t #{e.to_s}\n\t #{e.backtrace}"
80
+ end
81
+
82
+ def run_server
83
+ server = Server.new
84
+ rescue => e
85
+ server&.close
86
+ raise ApplicationError, "Server error:\n\t #{e.to_s}\n\t #{e.backtrace}"
87
+ end
88
+
89
+ def run
90
+ logger.info "Starting application #{VERSION}v in #{config.get[:mode]} mode"
91
+ threads = %w(generator)
92
+ if config.get[:mode] == 'server'
93
+ threads << 'restarter'
94
+ threads << 'server' if config.get[:server][:port]
95
+ end
96
+ threads.each do |thr|
97
+ @threads << run_thread(thr)
98
+ end
99
+
100
+ loop do
101
+ @threads.each do |thr|
102
+ unless thr.alive?
103
+ @threads.delete thr
104
+ @threads << run_thread(thr.name)
105
+ logger.error "#{thr.name.capitalize} is NOT ALIVE. Trying to restart."
106
+ end
107
+ end
108
+ sleep 60
109
+ end
110
+ end
111
+
112
+ private
113
+
114
+ def consul
115
+ @consul ||= Consul.new
116
+ end
117
+
118
+ def generator
119
+ @generator ||= Generator.new
120
+ end
121
+
122
+ def restarter
123
+ list = consul.sensu_servers
124
+ logger.info "Sensu servers discovered: #{list.map(&:address).join(', ')}"
125
+ Restarter.new(list)
126
+ end
127
+
128
+ def run_thread(name)
129
+ thr = eval("Thread.new { run_#{name} }")
130
+ thr.name = name
131
+ thr
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,27 @@
1
+ require 'fileutils'
2
+
3
+ module SensuGenerator
4
+ class CheckFile
5
+ def self.remove_all_with(prefix)
6
+ FileUtils.rm(Dir.glob("#{Application.config.result_dir}/#{prefix}*"))
7
+ end
8
+
9
+ def initialize(filename)
10
+ @config = Application.config
11
+ @trigger = Application.trigger
12
+ @filename = filename
13
+ @fullpath = File.join(@config.result_dir, @filename)
14
+ end
15
+
16
+ def write(data)
17
+ file = File.open(@fullpath, 'w+')
18
+ file.write data
19
+ file.close
20
+ @trigger.touch
21
+ end
22
+
23
+ def remove
24
+ FileUtils.rm @fullpath
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,60 @@
1
+ require 'json'
2
+ require 'socket'
3
+
4
+ module SensuGenerator
5
+ class Client
6
+ def initialize
7
+ @logger = Application.logger
8
+ @config = Application.config
9
+ connection
10
+ end
11
+
12
+ attr_reader :config, :logger
13
+
14
+ def connection
15
+ @connection ||= connect
16
+ end
17
+
18
+ def connect
19
+ logger.info "Client: connecting to server #{server_addr}:#{server_port}"
20
+ s = TCPSocket.new(server_addr, server_port)
21
+ logger.info "Client: connected"
22
+ s
23
+ rescue => e
24
+ raise ClientError, "Client: connection failed #{e.inspect} #{e.backtrace}\n"
25
+ end
26
+
27
+ def write_file(data)
28
+ connection.puts data
29
+ logger.info "Client: data transferred successfully"
30
+ true
31
+ rescue => e
32
+ close
33
+ raise ClientError, "Client: write failed #{e.inspect} #{e.backtrace}\n"
34
+ end
35
+
36
+ def flush_results
37
+ connection.puts JSON.fast_generate({"FLUSH_WITH_PREFIX" => "#{config.file_prefix}" })
38
+ rescue => e
39
+ close
40
+ raise ClientError, "Client: write failed #{e.inspect} #{e.backtrace}\n"
41
+ close
42
+ end
43
+
44
+ def close
45
+ @connection.close
46
+ @connection = nil
47
+ logger.info "Client: connection closed"
48
+ end
49
+
50
+ private
51
+
52
+ def server_addr
53
+ @server_addr ||= config.get[:server][:addr]
54
+ end
55
+
56
+ def server_port
57
+ @server_port ||= config.get[:server][:port]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,64 @@
1
+ require 'socket'
2
+ require 'json'
3
+
4
+ module SensuGenerator
5
+ class Config
6
+ @@default = {
7
+ :sensu => {
8
+ :check_default_params => {
9
+ :refresh => 86400,
10
+ :interval => 60,
11
+ :aggregate => true
12
+ },
13
+ :minimal_to_restart => 2,
14
+ :service => "sensu-server",
15
+ :rsync_repo => "sensu-server",
16
+ :supervisor => {:user => "", :password => ""},
17
+ },
18
+ :mode => 'server',
19
+ :server => {
20
+ :addr => '',
21
+ :port => nil
22
+ },
23
+ :result_dir => "work/result",
24
+ :templates_dir => "work/templates",
25
+ :logger => {
26
+ :file => STDOUT,
27
+ :notify_level => "error",
28
+ :log_level => "debug"
29
+ },
30
+ :slack => {
31
+ :url => nil,
32
+ :channel => nil,
33
+ :level => "error"
34
+ },
35
+ :kv_tags_path => "checks",
36
+ # See diplomat documentation to set proper consul parameters
37
+ :consul => {
38
+ :url => "http://consul.service.consul:8500"
39
+ }
40
+ }
41
+
42
+ def initialize(path = nil)
43
+ @config = process(path)
44
+ end
45
+
46
+ def get
47
+ @config
48
+ end
49
+
50
+ def process(path)
51
+ custom = path ? JSON(File.read(path), :symbolize_names => true) : {}
52
+ @config = @@default.deep_merge(custom)
53
+ end
54
+
55
+ def result_dir
56
+ raise(GeneratorError, "Result dir is not defined!") unless get[:result_dir]
57
+ File.expand_path(get[:result_dir])
58
+ end
59
+
60
+ def file_prefix
61
+ @file_prefix ||= get[:mode] == 'server' ? "local_" : "#{Socket.gethostname}_"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,47 @@
1
+ module SensuGenerator
2
+ class ConsulService < Consul
3
+
4
+ attr_reader :name, :properties, :checks
5
+
6
+ def initialize(name:)
7
+ @name = name
8
+ @changed = true
9
+ super()
10
+ all_properties
11
+ self
12
+ end
13
+
14
+ def all_properties
15
+ properties = get_props.class == Array ? get_props.map {|el| el.to_h} : get_props.to_h
16
+ @all_properties ||= { checks: get_checks, properties: properties }
17
+ end
18
+
19
+ alias :get_all_properties :all_properties
20
+
21
+ def get_checks
22
+ @checks ||= kv_svc_props(key: config.get[:kv_tags_path])
23
+ end
24
+
25
+ def get_props
26
+ @properties ||= get_service_props(name)
27
+ end
28
+
29
+ def update
30
+ old_all_properties = all_properties.clone
31
+ reset
32
+ get_all_properties
33
+ @changed = true if all_properties != old_all_properties
34
+ end
35
+
36
+ def changed?
37
+ @changed
38
+ end
39
+
40
+ def reset
41
+ @all_properties = nil
42
+ @properties = nil
43
+ @checks = nil
44
+ @changed = false
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,45 @@
1
+ module SensuGenerator
2
+ class ConsulState < Consul
3
+ def initialize
4
+ @actual_state = []
5
+ super()
6
+ actualize
7
+ end
8
+
9
+ def show
10
+ @actual_state
11
+ end
12
+
13
+ def actualize
14
+ reset
15
+ @svc_list_diff = services.map {|name, _| name.to_s } - @actual_state.map { |svc| svc.name.to_s}
16
+ @actual_state.each(&:update)
17
+ @svc_list_diff.each do |name|
18
+ @actual_state << ConsulService.new(name: name)
19
+ end
20
+ @actualized = true
21
+ logger.debug "Services actualized list: #{@actual_state.map { |svc| svc.name.to_s} }"
22
+ self
23
+ end
24
+
25
+ def changed?
26
+ state = !(@svc_list_diff || []).empty? || !changes.empty?
27
+ logger.debug "Consul state was changed: #{state.to_s}"
28
+ state
29
+ end
30
+
31
+ def changes
32
+ @svc_changes ||= @actual_state.select(&:changed?)
33
+ end
34
+
35
+ def reset
36
+ @actualized = false
37
+ @svc_changes = nil
38
+ @svc_list_diff = nil
39
+ end
40
+
41
+ def actualized?
42
+ @actualized ? true : false # For the case when nil
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,66 @@
1
+ require 'json'
2
+ require 'diplomat'
3
+
4
+ module SensuGenerator
5
+ class Consul
6
+ attr_writer :config, :logger
7
+
8
+ def initialize
9
+ @config = config
10
+ Diplomat.configure do |consul|
11
+ config.get[:consul].each do |k, v|
12
+ consul.public_send("#{k}=", v)
13
+ end
14
+ end
15
+ end
16
+
17
+ def sensu_servers
18
+ get_service_props(config.get[:sensu][:service]).map {|el| el.ServiceAddress}.uniq.
19
+ map {|addr| SensuServer.new(address: addr)}
20
+ end
21
+
22
+ def services
23
+ Diplomat::Service.get_all.to_h
24
+ end
25
+
26
+ def get_service_props(svc)
27
+ result = Diplomat::Service.get(svc, :all)
28
+ result.class == Array ? result.map {|el| el.remove_consul_indexes} : result.remove_consul_indexes
29
+ end
30
+
31
+ def kv_svc_props(svc: name, key: nil)
32
+ opts = key ? nil : {recurse: true}
33
+ response = Diplomat::Kv.get("#{svc}/#{key}", opts)
34
+ key ? JSON(response) : response # Maybe the feature of JSON check configuration will be implemented
35
+ rescue
36
+ if response
37
+ if response.match(/\s+/) || key.to_s == config.get[:kv_tags_path] # tags value is designed to be a list even if it has only one element
38
+ response.gsub(/\s+/, '').split(',')
39
+ else
40
+ response
41
+ end
42
+ else
43
+ []
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def config
50
+ @config ||= Application.config
51
+ end
52
+
53
+ def logger
54
+ @logger ||= Application.logger
55
+ end
56
+ end
57
+ end
58
+
59
+ class OpenStruct
60
+ def remove_consul_indexes
61
+ %w(CreateIndex ModifyIndex).each do |f|
62
+ self.delete_field(f) if self.respond_to?(f)
63
+ end
64
+ self
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ module SensuGenerator
2
+ %w(ApplicationError RestarterError GeneratorError SensuServerError ClientError ServerError).each do |e|
3
+ eval(
4
+ %Q(
5
+ class #{e} < StandardError
6
+ def initialize(msg)
7
+ Application.logger.error msg
8
+ end
9
+ end
10
+ )
11
+ )
12
+ end
13
+ end
14
+
15
+ module Diplomat
16
+ class PathNotFound < StandardError
17
+ def initialize(*args)
18
+ ::SensuGenerator::Application.logger.error "Could not connect to consul with provided url"
19
+ end
20
+ end
21
+ end