icinga-cert-service 0.18.4
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 +7 -0
- data/LICENSE +504 -0
- data/README.md +344 -0
- data/bin/icinga2-cert-service.rb +278 -0
- data/bin/installer.sh +33 -0
- data/bin/test.rb +28 -0
- data/lib/cert-service.rb +323 -0
- data/lib/cert-service/backup.rb +42 -0
- data/lib/cert-service/certificate_handler.rb +466 -0
- data/lib/cert-service/configure_icinga.rb +1 -0
- data/lib/cert-service/download.rb +24 -0
- data/lib/cert-service/endpoint_handler.rb +98 -0
- data/lib/cert-service/executor.rb +34 -0
- data/lib/cert-service/in-memory-cache.rb +43 -0
- data/lib/cert-service/templates.rb +62 -0
- data/lib/cert-service/version.rb +5 -0
- data/lib/cert-service/zone_handler.rb +71 -0
- data/lib/logging.rb +61 -0
- data/lib/monkey_patches.rb +128 -0
- data/lib/util.rb +94 -0
- data/lib/validator.rb +38 -0
- metadata +246 -0
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
module IcingaCertService
|
3
|
+
# Client Class to create on-the-fly a certificate to connect automaticly as satellite to an icinga2-master
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module Download
|
7
|
+
|
8
|
+
# allows you to download a static file which is stored in the directory assets
|
9
|
+
#
|
10
|
+
# currently only 'icinga2_certificates.sh'
|
11
|
+
#
|
12
|
+
def download(params)
|
13
|
+
|
14
|
+
file_name = validate( params, required: true, var: 'file_name', type: String )
|
15
|
+
request = validate( params, required: true, var: 'request', type: Hash )
|
16
|
+
|
17
|
+
whitelist = ['icinga2_certificates.sh']
|
18
|
+
|
19
|
+
return { status: 500, message: 'file are unknown' } unless( whitelist.include?(file_name) )
|
20
|
+
|
21
|
+
{ status: 200, path: format('%s/assets', @base_directory), file_name: file_name }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
|
2
|
+
module IcingaCertService
|
3
|
+
# Client Class to create on-the-fly a certificate to connect automaticly as satellite to an icinga2-master
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module EndpointHandler
|
7
|
+
|
8
|
+
# add a Endpoint for distributed monitoring to the icinga2-master configuration
|
9
|
+
#
|
10
|
+
# @param [Hash, #read] params
|
11
|
+
# @option params [String] :host
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# add_endpoint( host: 'icinga2-satellite' )
|
15
|
+
#
|
16
|
+
# @return [Hash, #read] if config already created:
|
17
|
+
# * :status [Integer] 204
|
18
|
+
# * :message [String] Message
|
19
|
+
#
|
20
|
+
# @return nil if successful
|
21
|
+
#
|
22
|
+
def add_endpoint(params)
|
23
|
+
|
24
|
+
host = validate( params, required: true, var: 'host', type: String )
|
25
|
+
satellite = validate( params, required: false, var: 'satellite', type: Boolean ) || false
|
26
|
+
|
27
|
+
return { status: 500, message: 'no hostname to create an endpoint' } if host.nil?
|
28
|
+
|
29
|
+
# add the API user
|
30
|
+
#
|
31
|
+
add_api_user(params)
|
32
|
+
|
33
|
+
# add the zone object
|
34
|
+
#
|
35
|
+
ret = add_zone(host)
|
36
|
+
|
37
|
+
# logger.debug( ret )
|
38
|
+
|
39
|
+
if( satellite )
|
40
|
+
file_name = '/etc/icinga2/zones.conf'
|
41
|
+
else
|
42
|
+
zone_directory = format('/etc/icinga2/zones.d/%s', host)
|
43
|
+
file_name = format('%s/%s.conf', zone_directory, host)
|
44
|
+
|
45
|
+
begin
|
46
|
+
FileUtils.mkpath(zone_directory) unless File.exist?(zone_directory)
|
47
|
+
rescue
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
if( File.exist?(file_name) )
|
53
|
+
|
54
|
+
file = File.open(file_name, 'r')
|
55
|
+
contents = file.read
|
56
|
+
|
57
|
+
regexp_long = / # Match she-bang style C-comment
|
58
|
+
\/\* # Opening delimiter.
|
59
|
+
[^*]*\*+ # {normal*} Zero or more non-*, one or more *
|
60
|
+
(?: # Begin {(special normal*)*} construct.
|
61
|
+
[^*\/] # {special} a non-*, non-\/ following star.
|
62
|
+
[^*]*\*+ # More {normal*}
|
63
|
+
)* # Finish "Unrolling-the-Loop"
|
64
|
+
\/ # Closing delimiter.
|
65
|
+
/x
|
66
|
+
result = contents.gsub(regexp_long, '')
|
67
|
+
|
68
|
+
scan_endpoint = result.scan(/object Endpoint(.*)"(?<endpoint>.+\S)"/).flatten
|
69
|
+
|
70
|
+
return { status: 200, message: format('the configuration for the endpoint %s already exists', host) } if( scan_endpoint.include?(host) == true )
|
71
|
+
end
|
72
|
+
|
73
|
+
logger.debug(format('i miss an configuration for endpoint \'%s\'', host))
|
74
|
+
|
75
|
+
begin
|
76
|
+
|
77
|
+
result = write_template(
|
78
|
+
template: 'templates/zones.d/endpoint.conf.erb',
|
79
|
+
destination_file: file_name,
|
80
|
+
environment: {
|
81
|
+
host: host
|
82
|
+
}
|
83
|
+
)
|
84
|
+
|
85
|
+
create_backup
|
86
|
+
|
87
|
+
# logger.debug( result )
|
88
|
+
rescue => error
|
89
|
+
|
90
|
+
logger.debug(error)
|
91
|
+
end
|
92
|
+
|
93
|
+
{ status: 200, message: format('configuration for endpoint \'%s\' has been created', host) }
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
module IcingaCertService
|
5
|
+
# SubModule to execute some commands
|
6
|
+
#
|
7
|
+
#
|
8
|
+
module Executor
|
9
|
+
|
10
|
+
# execute system commands with a Open3.popen2() call
|
11
|
+
#
|
12
|
+
# @param [Hash, #read] params
|
13
|
+
# @option params [String] :cmd
|
14
|
+
#
|
15
|
+
# @return [Hash, #read]
|
16
|
+
# * :exit [Integer] Exit-Code
|
17
|
+
# * :message [String] Message
|
18
|
+
def exec_command( params )
|
19
|
+
|
20
|
+
cmd = params.dig(:cmd)
|
21
|
+
|
22
|
+
return { code: 1, message: 'no command found' } if( cmd.nil? )
|
23
|
+
|
24
|
+
result = {}
|
25
|
+
|
26
|
+
Open3.popen2( cmd ) do |_stdin, stdout_err, wait_thr|
|
27
|
+
return_value = wait_thr.value
|
28
|
+
result = { code: return_value.success?, message: stdout_err.gets }
|
29
|
+
end
|
30
|
+
|
31
|
+
result
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
module IcingaCertService
|
3
|
+
|
4
|
+
# small in-memory Cache
|
5
|
+
#
|
6
|
+
module InMemoryDataCache
|
7
|
+
|
8
|
+
# create a new Instance
|
9
|
+
#
|
10
|
+
def initialize
|
11
|
+
@storage = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# save data
|
15
|
+
#
|
16
|
+
# @param [String, #read] id
|
17
|
+
# @param [misc, #read] data
|
18
|
+
#
|
19
|
+
def save(id, data)
|
20
|
+
@storage ||= {}
|
21
|
+
@storage[id] ||= {}
|
22
|
+
@storage[id] = data
|
23
|
+
end
|
24
|
+
|
25
|
+
# get data
|
26
|
+
#
|
27
|
+
# @param [String, #read] id
|
28
|
+
#
|
29
|
+
def find_by_id(id)
|
30
|
+
if( !@storage.nil? )
|
31
|
+
@storage.dig(id) || {}
|
32
|
+
else
|
33
|
+
{}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# get all data
|
38
|
+
#
|
39
|
+
def entries
|
40
|
+
@storage
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
module IcingaCertService
|
3
|
+
# Client Class to create on-the-fly a certificate to connect automaticly as satellite to an icinga2-master
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module Templates
|
7
|
+
|
8
|
+
# write files based on a template
|
9
|
+
#
|
10
|
+
# @param [Hash, #read] params
|
11
|
+
# @option params [String] :template
|
12
|
+
# @option params [String] :destination_file
|
13
|
+
# @option params [Hash] :environment
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# write_template(
|
17
|
+
# template: 'templates/zones.d/endpoint.conf.erb',
|
18
|
+
# destination_file: file_name,
|
19
|
+
# environment: {
|
20
|
+
# host: host
|
21
|
+
# }
|
22
|
+
# )
|
23
|
+
#
|
24
|
+
#
|
25
|
+
def write_template(params)
|
26
|
+
|
27
|
+
template = validate( params, required: true, var: 'template', type: String )
|
28
|
+
destination_file = validate( params, required: true, var: 'destination_file', type: String )
|
29
|
+
environment = validate( params, required: true, var: 'environment', type: Hash )
|
30
|
+
|
31
|
+
template = format( '%s/%s', @base_directory, template )
|
32
|
+
|
33
|
+
return { status: 500, message: "template '#{template}' not found." } if( ! File.exist?(template) )
|
34
|
+
|
35
|
+
begin
|
36
|
+
template = ERB.new File.new(template).read
|
37
|
+
date = Time.now
|
38
|
+
template = template.result( binding )
|
39
|
+
|
40
|
+
begin
|
41
|
+
logger.debug( "write to file: #{destination_file}" )
|
42
|
+
file = File.open(destination_file, 'a')
|
43
|
+
file.write(template)
|
44
|
+
rescue => error
|
45
|
+
logger.error(error.to_s)
|
46
|
+
{ status: 500, message: error.to_s }
|
47
|
+
ensure
|
48
|
+
file.close unless file.nil?
|
49
|
+
end
|
50
|
+
rescue => error
|
51
|
+
logger.error(error.to_s)
|
52
|
+
logger.debug( error.backtrace.join("\n") )
|
53
|
+
|
54
|
+
{ status: 500, message: error.to_s }
|
55
|
+
end
|
56
|
+
|
57
|
+
{ status: 200, message: format('file \'%s\' has been created', destination_file) }
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
module IcingaCertService
|
3
|
+
# Client Class to create on-the-fly a certificate to connect automaticly as satellite to an icinga2-master
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module ZoneHandler
|
7
|
+
|
8
|
+
# add a satellite zone to 'zones.conf'
|
9
|
+
#
|
10
|
+
# @param [String] zone
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# add_zone('icinga2-satellite')
|
14
|
+
#
|
15
|
+
# @return [Hash, #read] if config already created:
|
16
|
+
# * :status [Integer] 200
|
17
|
+
# * :message [String] Message
|
18
|
+
#
|
19
|
+
# @return nil if successful
|
20
|
+
#
|
21
|
+
def add_zone( zone = nil )
|
22
|
+
|
23
|
+
return { status: 500, message: 'no zone defined' } if zone.nil?
|
24
|
+
|
25
|
+
zone_file = '/etc/icinga2/zones.conf'
|
26
|
+
|
27
|
+
if(File.exist?(zone_file))
|
28
|
+
|
29
|
+
file = File.open(zone_file, 'r')
|
30
|
+
contents = file.read
|
31
|
+
|
32
|
+
regexp_long = / # Match she-bang style C-comment
|
33
|
+
\/\* # Opening delimiter.
|
34
|
+
[^*]*\*+ # {normal*} Zero or more non-*, one or more *
|
35
|
+
(?: # Begin {(special normal*)*} construct.
|
36
|
+
[^*\/] # {special} a non-*, non-\/ following star.
|
37
|
+
[^*]*\*+ # More {normal*}
|
38
|
+
)* # Finish "Unrolling-the-Loop"
|
39
|
+
\/ # Closing delimiter.
|
40
|
+
/x
|
41
|
+
result = contents.gsub(regexp_long, '')
|
42
|
+
scan_zone = result.scan(/object Zone(.*)"(?<zone>.+\S)"/).flatten
|
43
|
+
|
44
|
+
return { status: 200, message: format('the configuration for the zone %s already exists', zone) } if( scan_zone.include?(zone) == true )
|
45
|
+
end
|
46
|
+
|
47
|
+
logger.debug(format('i miss an configuration for zone \'%s\'', zone))
|
48
|
+
|
49
|
+
begin
|
50
|
+
result = write_template(
|
51
|
+
template: 'templates/zones.conf.erb',
|
52
|
+
destination_file: zone_file,
|
53
|
+
environment: {
|
54
|
+
zone: zone,
|
55
|
+
icinga_master: @icinga_master
|
56
|
+
}
|
57
|
+
)
|
58
|
+
# logger.debug(result)
|
59
|
+
|
60
|
+
rescue => error
|
61
|
+
logger.error(error.to_s)
|
62
|
+
|
63
|
+
return { status: 500, message: error.to_s }
|
64
|
+
end
|
65
|
+
|
66
|
+
{ status: 200, message: format('configuration for zone %s has been created', zone) }
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
data/lib/logging.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
# -------------------------------------------------------------------------------------------------
|
5
|
+
|
6
|
+
module Logging
|
7
|
+
|
8
|
+
# global function to use the logger instance
|
9
|
+
#
|
10
|
+
def logger
|
11
|
+
@logger ||= Logging.logger_for( self.class.name )
|
12
|
+
end
|
13
|
+
|
14
|
+
# Use a hash class-ivar to cache a unique Logger per class:
|
15
|
+
@loggers = {}
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
# create an logger instance for classname
|
20
|
+
#
|
21
|
+
def logger_for( classname )
|
22
|
+
@loggers[classname] ||= configure_logger_for( classname )
|
23
|
+
end
|
24
|
+
|
25
|
+
# configure the logger
|
26
|
+
#
|
27
|
+
def configure_logger_for( classname )
|
28
|
+
|
29
|
+
log_level = ENV.fetch('LOG_LEVEL', 'DEBUG' )
|
30
|
+
level = log_level.dup
|
31
|
+
|
32
|
+
# DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN
|
33
|
+
log_level = case level.upcase
|
34
|
+
when 'DEBUG'
|
35
|
+
Logger::DEBUG # Low-level information for developers.
|
36
|
+
when 'INFO'
|
37
|
+
Logger::INFO # Generic (useful) information about system operation.
|
38
|
+
when 'WARN'
|
39
|
+
Logger::WARN # A warning.
|
40
|
+
when 'ERROR'
|
41
|
+
Logger::ERROR # A handleable error condition.
|
42
|
+
when 'FATAL'
|
43
|
+
Logger::FATAL # An unhandleable error that results in a program crash.
|
44
|
+
else
|
45
|
+
Logger::UNKNOWN # An unknown message that should always be logged.
|
46
|
+
end
|
47
|
+
|
48
|
+
$stdout.sync = true
|
49
|
+
logger = Logger.new($stdout)
|
50
|
+
logger.level = log_level
|
51
|
+
logger.datetime_format = '%Y-%m-%d %H:%M:%S'
|
52
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
53
|
+
"[#{datetime.strftime( logger.datetime_format )}] #{severity.ljust(5)} #{msg}\n"
|
54
|
+
end
|
55
|
+
|
56
|
+
logger
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# -------------------------------------------------------------------------------------------------
|
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
# -----------------------------------------------------------------------------
|
3
|
+
# Monkey patches
|
4
|
+
|
5
|
+
# Modify `Object`
|
6
|
+
#
|
7
|
+
# original from (https://gist.github.com/Integralist/9503099)
|
8
|
+
#
|
9
|
+
# None of the above solutions work with a multi-level hash
|
10
|
+
# They only work on the first level: {:foo=>"bar", :level1=>{"level2"=>"baz"}}
|
11
|
+
# The following two variations solve the problem in the same way
|
12
|
+
# transform hash keys to symbols
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# multi_hash = { 'foo' => 'bar', 'level1' => { 'level2' => 'baz' } }
|
16
|
+
# multi_hash = multi_hash.deep_string_keys
|
17
|
+
#
|
18
|
+
class Object
|
19
|
+
|
20
|
+
# transform hash keys to symbols
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# multi_hash = { 'foo' => 'bar', 'level1' => { 'level2' => 'baz' } }.deep_string_keys
|
24
|
+
#
|
25
|
+
# @return
|
26
|
+
# { foo: 'bar', level1: { 'level2' => 'baz' } }
|
27
|
+
#
|
28
|
+
def deep_symbolize_keys
|
29
|
+
if( is_a?( Hash ) )
|
30
|
+
return inject({}) do |memo, (k, v)|
|
31
|
+
memo.tap { |m| m[k.to_sym] = v.deep_string_keys }
|
32
|
+
end
|
33
|
+
elsif( is_a?( Array ) )
|
34
|
+
return map(&:deep_string_keys)
|
35
|
+
end
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
# transform hash keys to strings
|
41
|
+
#
|
42
|
+
#
|
43
|
+
def deep_string_keys
|
44
|
+
if( is_a?( Hash ) )
|
45
|
+
return inject({}) do |memo, (k, v)|
|
46
|
+
memo.tap { |m| m[k.to_s] = v.deep_string_keys }
|
47
|
+
end
|
48
|
+
elsif( is_a?( Array ) )
|
49
|
+
return map(&:deep_string_keys)
|
50
|
+
end
|
51
|
+
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
# -----------------------------------------------------------------------------
|
58
|
+
|
59
|
+
# check if is a checksum
|
60
|
+
#
|
61
|
+
class Object
|
62
|
+
|
63
|
+
REGEX = /\A[0-9a-f]{32,128}\z/i
|
64
|
+
CHARS = {
|
65
|
+
md2: 32,
|
66
|
+
md4: 32,
|
67
|
+
md5: 32,
|
68
|
+
sha1: 40,
|
69
|
+
sha224: 56,
|
70
|
+
sha256: 64,
|
71
|
+
sha384: 96,
|
72
|
+
sha512: 128
|
73
|
+
}
|
74
|
+
|
75
|
+
# return if this a checksum
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# checksum.be_a_checksum
|
79
|
+
#
|
80
|
+
def be_a_checksum
|
81
|
+
!!(self =~ REGEX)
|
82
|
+
end
|
83
|
+
|
84
|
+
# return true if the checksum created by spezified type
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# checksum.produced_by(:md5)
|
88
|
+
#
|
89
|
+
# checksum.produced_by(:sha256)
|
90
|
+
#
|
91
|
+
#
|
92
|
+
def produced_by( name )
|
93
|
+
function = name.to_s.downcase.to_sym
|
94
|
+
|
95
|
+
raise ArgumentError, "unknown algorithm given to be_a_checksum.produced_by: #{function}" unless CHARS.include?(function)
|
96
|
+
|
97
|
+
return true if( size == CHARS[function] )
|
98
|
+
false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# -----------------------------------------------------------------------------
|
103
|
+
|
104
|
+
# Monkey Patch to implement an Boolean Check
|
105
|
+
# original from: https://stackoverflow.com/questions/3028243/check-if-ruby-object-is-a-boolean/3028378#3028378
|
106
|
+
#
|
107
|
+
#
|
108
|
+
module Boolean; end
|
109
|
+
class TrueClass; include Boolean; end
|
110
|
+
class FalseClass; include Boolean; end
|
111
|
+
|
112
|
+
true.is_a?(Boolean) #=> true
|
113
|
+
false.is_a?(Boolean) #=> true
|
114
|
+
|
115
|
+
# -----------------------------------------------------------------------------
|
116
|
+
|
117
|
+
# add minutes
|
118
|
+
#
|
119
|
+
class Time
|
120
|
+
|
121
|
+
# add minutes 'm' to Time Object
|
122
|
+
#
|
123
|
+
def add_minutes(m)
|
124
|
+
self + (60 * m)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# -----------------------------------------------------------------------------
|