sensu_generator 0.0.25

Sign up to get free protection for your applications and to get access to all the features.
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