flapjack_configurator 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +47 -0
  4. data/Dockerfile +11 -0
  5. data/Gemfile +3 -0
  6. data/README.md +139 -0
  7. data/Rakefile +19 -0
  8. data/bin/flapjack_configurator +82 -0
  9. data/example.yml +121 -0
  10. data/flapjack_configurator.gemspec +29 -0
  11. data/lib/flapjack_configurator.rb +32 -0
  12. data/lib/flapjack_configurator/entity_mapper.rb +70 -0
  13. data/lib/flapjack_configurator/flapjack_config.rb +72 -0
  14. data/lib/flapjack_configurator/flapjack_contact.rb +156 -0
  15. data/lib/flapjack_configurator/flapjack_media.rb +23 -0
  16. data/lib/flapjack_configurator/flapjack_notification_rule.rb +39 -0
  17. data/lib/flapjack_configurator/flapjack_object_base.rb +86 -0
  18. data/lib/flapjack_configurator/flapjack_pagerduty.rb +28 -0
  19. data/lib/flapjack_configurator/flapjack_sub_object_base.rb +33 -0
  20. data/lib/flapjack_configurator/user_configuration.rb +107 -0
  21. data/lib/flapjack_configurator/version.rb +6 -0
  22. data/spec/docker_test_wrapper.rb +52 -0
  23. data/spec/functional/all_entity_spec.rb +19 -0
  24. data/spec/functional/config_test_common.rb +58 -0
  25. data/spec/functional/configuration_contact_attributes_spec.rb +18 -0
  26. data/spec/functional/configuration_contact_entities_spec.rb +116 -0
  27. data/spec/functional/configuration_contact_notification_media_spec.rb +73 -0
  28. data/spec/functional/configuration_contact_notification_rules_spec.rb +58 -0
  29. data/spec/functional/configuration_contact_removal_spec.rb +83 -0
  30. data/spec/functional/test_configs/changes/attributes.yaml +24 -0
  31. data/spec/functional/test_configs/changes/notification_media.yaml +155 -0
  32. data/spec/functional/test_configs/changes/notification_rules.yaml +143 -0
  33. data/spec/functional/test_configs/entities.yaml +71 -0
  34. data/spec/functional/test_configs/initial/attributes.yaml +24 -0
  35. data/spec/functional/test_configs/initial/notification_media.yaml +155 -0
  36. data/spec/functional/test_configs/initial/notification_rules.yaml +143 -0
  37. data/spec/functional/test_configs/obj_removal_setup.yaml +106 -0
  38. data/spec/spec_helper.rb +9 -0
  39. metadata +211 -0
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'flapjack-diner'
4
+ require 'logger'
5
+ require 'flapjack_configurator/flapjack_config'
6
+ require 'flapjack_configurator/version'
7
+
8
+ # Flapjack Configuration Module
9
+ module FlapjackConfigurator
10
+ # Method to configure flapjack
11
+ def self.configure_flapjack(config, api_base_url = 'http://127.0.0.1:3081', logger = Logger.new(STDOUT), enable_all_entity = true)
12
+ ret_val = false
13
+ Flapjack::Diner.base_uri(api_base_url)
14
+
15
+ # The underlying classes treat the Flapjack::Diner module as if it is a class.
16
+ # This was done as it was fairly natural and will allow Flapjack::Diner to be
17
+ # replaced or wrapped very easily in the future.
18
+ config_obj = FlapjackConfig.new(config, Flapjack::Diner, logger)
19
+
20
+ if enable_all_entity
21
+ # Ensure the ALL entity is present
22
+ ret_val = true if config_obj.add_all_entity
23
+ end
24
+
25
+ # Update the contacts
26
+ # This will update media, PD creds, notification rules, and entity associations
27
+ # as they're associated to the contact.
28
+ ret_val = true if config_obj.update_contacts
29
+
30
+ return ret_val
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ require 'deep_clone'
3
+
4
+ module FlapjackConfigurator
5
+ # Walk though the entities and build the map of entities to contacts
6
+ # Built as a class for future proofing and because this is expected to
7
+ # be an expensive operation: so instantiate it once and pass it around.
8
+ class EntityMapper
9
+ attr_reader :entity_map
10
+
11
+ def initialize(config_obj, diner)
12
+ @entity_map = {}.tap { |em| config_obj.contact_ids.each { |cn| em[cn.to_sym] = [] } }
13
+ default_contacts = config_obj.default_contacts
14
+
15
+ # Walk the entities and compare each individually so that the whitelisting/blacklisting can be easily enforced
16
+ # This probably will need some optimization
17
+ diner.entities.each do |entity|
18
+ contact_defined = false
19
+ config_obj.contact_ids.each do |contact_id|
20
+ match_id = _check_entity(entity, config_obj.contact_config(contact_id))
21
+ if match_id
22
+ @entity_map[contact_id.to_sym].push(entity[:id])
23
+ contact_defined = true
24
+ end
25
+ end
26
+
27
+ # ALL is a special case entity, don't associate the default to it.
28
+ # Using next if per Rubocop :)
29
+ next if contact_defined || entity[:id] == 'ALL'
30
+ # No contacts match this entity, add it to the defaults
31
+ default_contacts.each do |contact_id|
32
+ @entity_map[contact_id.to_sym].push(entity[:id])
33
+ end
34
+ end
35
+
36
+ return @entity_map
37
+ end
38
+
39
+ # Check if entity should be included in contact
40
+ # Helper for _build_entity_map
41
+ # Returns the entity ID on match or nil on no match
42
+ def _check_entity(entity, contact)
43
+ # Priority 1: Exact Entries
44
+ return true if contact['entities']['exact'].include? entity[:name]
45
+
46
+ # Priority 2: Exact blacklist
47
+ return false if contact['entities_blacklist']['exact'].include? entity[:name]
48
+
49
+ # Priority 3: Regex blacklist
50
+ contact['entities_blacklist']['regex'].each do |bl_regex|
51
+ return false if /#{bl_regex}/.match(entity[:name])
52
+ end
53
+
54
+ # Priority 4: Match regex
55
+ contact['entities']['regex'].each do |m_regex|
56
+ return true if /#{m_regex}/.match(entity[:name])
57
+ end
58
+
59
+ # Fallthrough
60
+ return false
61
+ end
62
+
63
+ # Return the entities for the given contact
64
+ # Returns a clone so the returned object is modifyable
65
+ def entities_for_contact(id)
66
+ fail("ID #{id} not in entity map") unless @entity_map.key? id.to_sym
67
+ return DeepClone.clone @entity_map[id.to_sym]
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'user_configuration.rb'
4
+ require_relative 'flapjack_media.rb'
5
+ require_relative 'flapjack_pagerduty.rb'
6
+ require_relative 'flapjack_notification_rule.rb'
7
+ require_relative 'flapjack_contact.rb'
8
+
9
+ module FlapjackConfigurator
10
+ # Class representing the overall Flapjack config
11
+ class FlapjackConfig
12
+ attr_reader :config_obj, :contacts
13
+
14
+ def initialize(config, diner, logger)
15
+ @config_obj = UserConfiguration.new(config, diner, logger)
16
+ @diner = diner
17
+ @logger = logger
18
+
19
+ # Media will be tied in via the contacts, however pregenerate objects off a single API call for speed.
20
+ media = @diner.media.map { |api_media| FlapjackMedia.new(api_media, @diner, logger) }
21
+ # Also add PagerDuty creds into media. PD creds are handled separately by the API but can be grouped thanks to our class handling.
22
+ media += @diner.pagerduty_credentials.map { |api_pd| FlapjackPagerduty.new(api_pd, @diner, logger) }
23
+
24
+ # Prebuild notification rules for the same reason
25
+ notification_rules = @diner.notification_rules.map { |api_nr| FlapjackNotificationRule.new(nil, api_nr, @diner, logger) }
26
+
27
+ @contacts = {}.tap { |ce| @diner.contacts.map { |api_contact| ce[api_contact[:id]] = FlapjackContact.new(nil, api_contact, @diner, logger, media, notification_rules) } }
28
+ end
29
+
30
+ # Loop over the contacts and call update/create/remove methods as needed
31
+ # Builds the @contacts hash
32
+ def update_contacts
33
+ config_contact_ids = @config_obj.contact_ids
34
+ ret_val = false
35
+
36
+ # Iterate over a list of keys to avoid the iterator being impacted by deletes
37
+ @contacts.keys.each do |id|
38
+ if config_contact_ids.include? id
39
+ ret_val = true if @contacts[id].update(@config_obj)
40
+
41
+ # Delete the ID from the id array
42
+ # This will result in config_contact_ids being a list of IDs that need to be created at the end of the loop
43
+ config_contact_ids.delete(id)
44
+ else
45
+ # Delete contact from Flapjack
46
+ @contacts[id].delete
47
+ @contacts.delete(id)
48
+ ret_val = true
49
+ end
50
+ end
51
+
52
+ # Add new contacts to Flapjack
53
+ config_contact_ids.each do |new_id|
54
+ contact_obj = FlapjackContact.new(new_id, nil, @diner, @logger)
55
+ contact_obj.update(@config_obj)
56
+ @contacts[new_id] = contact_obj
57
+ end
58
+
59
+ # Return true if changes made
60
+ return ret_val || config_contact_ids.length > 0
61
+ end
62
+
63
+ # Ensure the ALL entity is present
64
+ # http://flapjack.io/docs/1.0/usage/Howto-Dynamic-Entity-Contact-Linking/
65
+ def add_all_entity
66
+ return false if @diner.entities('ALL')
67
+ @logger.info('Creating the ALL magic entity')
68
+ fail('Failed to create ALL entity') unless @diner.create_entities(id: 'ALL', name: 'ALL')
69
+ return true
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'flapjack_object_base.rb'
4
+ require_relative 'flapjack_media.rb'
5
+ require_relative 'flapjack_pagerduty.rb'
6
+ require_relative 'flapjack_notification_rule.rb'
7
+
8
+ module FlapjackConfigurator
9
+ # Class representing a Flapjack contact
10
+ class FlapjackContact < FlapjackObjectBase
11
+ attr_reader :media
12
+
13
+ def initialize(my_id, current_config, diner, logger, current_media = [], current_notification_rules = [])
14
+ @diner = diner
15
+ @logger = logger
16
+ super(my_id, current_config, diner.method(:contacts), diner.method(:create_contacts), diner.method(:update_contacts), diner.method(:delete_contacts), logger, 'contact')
17
+
18
+ # Select our media out from a premade hash of all media built from a single API call
19
+ @media = current_media.select { |m| m.config[:links][:contacts].include? id }
20
+
21
+ # Select notification rules the same way.
22
+ @notification_rules = current_notification_rules.select { |m| m.config[:links][:contacts].include? id }
23
+ end
24
+
25
+ # Update all the things
26
+ def update(config_obj)
27
+ ret_val = false
28
+ ret_val = true if update_attributes(config_obj)
29
+ ret_val = true if update_entities(config_obj)
30
+ ret_val = true if update_media(config_obj)
31
+ ret_val = true if update_notification_rules(config_obj)
32
+ return ret_val
33
+ end
34
+
35
+ # Define our own _create as it doesn't use an ID
36
+ def _create(config)
37
+ fail("Object #{id} exists") if @obj_exists
38
+ # AFAIK there is not an easy way to convert hash keys to symbols outside of Rails
39
+ config.each { |k, v| @config[k.to_sym] = v }
40
+ @logger.info("Creating contact #{id} with config #{@config}")
41
+ fail "Failed to create contact #{id}" unless @create_method.call([@config])
42
+ _reload_config
43
+
44
+ # Creating an entity auto generates notification rules
45
+ # Regenerate the notification rules
46
+ @notification_rules = []
47
+ @config[:links][:notification_rules].each do |nr_id|
48
+ @notification_rules << FlapjackNotificationRule.new(nr_id, nil, @diner, @logger)
49
+ end
50
+ end
51
+
52
+ # Update attributes from the config, creating the contact if needed
53
+ # (Chef definition of "update")
54
+ # Does not handle entites or notifications
55
+ def update_attributes(config_obj)
56
+ @logger.debug("Updating attributes for contact #{id}")
57
+ if @obj_exists
58
+ return _update(config_obj.contact_config(id)['details'])
59
+ else
60
+ _create(config_obj.contact_config(id)['details'])
61
+ return true
62
+ end
63
+ end
64
+
65
+ # Update entities for the contact, creating or removing as needed
66
+ def update_entities(config_obj)
67
+ fail("Contact #{id} doesn't exist yet") unless @obj_exists
68
+ @logger.debug("Updating entities for contact #{id}")
69
+
70
+ wanted_entities = config_obj.entity_map.entities_for_contact(id)
71
+ current_entities = @config[:links][:entities]
72
+ ret_val = false
73
+
74
+ (wanted_entities - current_entities).each do |entity_id|
75
+ @logger.info("Associating entity #{entity_id} to contact #{id}")
76
+ fail("Failed to add entity #{entity_id} to contact #{id}") unless @diner.update_contacts(id, add_entity: entity_id)
77
+ ret_val = true
78
+ end
79
+
80
+ (current_entities - wanted_entities).each do |entity_id|
81
+ @logger.info("Removing entity #{entity_id} from contact #{id}")
82
+ fail("Failed to remove entity #{entity_id} from contact #{id}") unless @diner.update_contacts(id, remove_entity: entity_id)
83
+ ret_val = true
84
+ end
85
+
86
+ _reload_config
87
+ return ret_val
88
+ end
89
+
90
+ # Update the media for the contact
91
+ def update_media(config_obj)
92
+ @logger.debug("Updating media for contact #{id}")
93
+ media_config = config_obj.media(id)
94
+ ret_val = false
95
+
96
+ media_config_types = media_config.keys
97
+ @media.each do |media_obj|
98
+ if media_config_types.include? media_obj.type
99
+ ret_val = true if media_obj.update(media_config[media_obj.type])
100
+ # Delete the ID from the type array. This will result in media_config_types being a list of types that need to be created at the end of the loop
101
+ media_config_types.delete(media_obj.type)
102
+ else
103
+ media_obj.delete
104
+ ret_val = true
105
+ end
106
+ end
107
+
108
+ # Delete outside the loop as deleting inside the loop messes up the each iterator
109
+ @media.delete_if { |obj| !obj.obj_exists }
110
+
111
+ media_config_types.each do |type|
112
+ # Pagerduty special case again
113
+ # TODO: Push this back up so that the if isn't done here
114
+ if type == 'pagerduty'
115
+ media_obj = FlapjackPagerduty.new(nil, @diner, @logger)
116
+ media_obj.create(id, media_config[type])
117
+ else
118
+ media_obj = FlapjackMedia.new(nil, @diner, @logger)
119
+ media_obj.create(id, type, media_config[type])
120
+ end
121
+ @media << media_obj
122
+ end
123
+
124
+ return ret_val || media_config_types.length > 0
125
+ end
126
+
127
+ def update_notification_rules(config_obj)
128
+ @logger.debug("Updating notification rules for contact #{id}")
129
+ nr_config = config_obj.notification_rules(id)
130
+ nr_config_ids = nr_config.keys
131
+ ret_val = false
132
+
133
+ @notification_rules.each do |nr_obj|
134
+ if nr_config_ids.include? nr_obj.id
135
+ ret_val = true if nr_obj.update(nr_config[nr_obj.id])
136
+ # Delete the ID from the type array. This will result in nr_config_ids being a list of types that need to be created at the end of the loop
137
+ nr_config_ids.delete(nr_obj.id)
138
+ else
139
+ nr_obj.delete
140
+ ret_val = true
141
+ end
142
+ end
143
+
144
+ # Delete outside the loop as deleting inside the loop messes up the each iterator
145
+ @notification_rules.delete_if { |obj| !obj.obj_exists }
146
+
147
+ nr_config_ids.each do |nr_id|
148
+ nr_obj = FlapjackNotificationRule.new(nr_id, nil, @diner, @logger)
149
+ nr_obj.create(id, nr_config[nr_id])
150
+ @notification_rules << (nr_obj)
151
+ end
152
+
153
+ return ret_val || nr_config_ids.length > 0
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'flapjack_sub_object_base.rb'
4
+
5
+ module FlapjackConfigurator
6
+ # Class representing Flapjack media
7
+ class FlapjackMedia < FlapjackSubObjectBase
8
+ def initialize(current_config, diner, logger)
9
+ super(nil, current_config, diner.method(:media), diner.method(:create_contact_media), diner.method(:update_media), diner.method(:delete_media), logger, 'media')
10
+ @allowed_config_keys = [:address, :interval, :rollup_threshold]
11
+ end
12
+
13
+ # Create a new entry
14
+ def create(contact_id, type, config)
15
+ _create(contact_id, _filter_config(config).merge(type: type))
16
+ end
17
+
18
+ # Helper to return the type
19
+ def type
20
+ return @config[:type]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative 'flapjack_sub_object_base.rb'
4
+
5
+ module FlapjackConfigurator
6
+ # Class representing notification rules
7
+ class FlapjackNotificationRule < FlapjackSubObjectBase
8
+ def initialize(conf_id, current_config, diner, logger)
9
+ super(conf_id, current_config, diner.method(:notification_rules), diner.method(:create_contact_notification_rules), diner.method(:update_notification_rules),
10
+ diner.method(:delete_notification_rules), logger, 'notification rule')
11
+ end
12
+
13
+ def create(contact_id, config)
14
+ # Flapjack will let you create a notification rule object with no attributes, but that sets nils whereas
15
+ # the default it creates has empty arrays.
16
+ # Set up a baseline config that matches what Flapjack creates by default
17
+ full_config = {
18
+ id: id,
19
+ tags: [],
20
+ regex_tags: [],
21
+ entities: [],
22
+ regex_entities: [],
23
+ time_restrictions: [],
24
+ warning_media: nil,
25
+ critical_media: nil,
26
+ unknown_media: nil,
27
+ unknown_blackhole: false,
28
+ warning_blackhole: false,
29
+ critical_blackhole: false
30
+ }.merge(config)
31
+
32
+ _create(contact_id, full_config)
33
+ end
34
+
35
+ def update(config)
36
+ return _update(config)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ module FlapjackConfigurator
4
+ # Baseline class representing a Flapjack object
5
+ class FlapjackObjectBase
6
+ attr_reader :config, :obj_exists
7
+
8
+ def initialize(my_id, current_config, getter_method, create_method, update_method, delete_method, logger, log_name)
9
+ @config = {}
10
+ @logger = logger
11
+ @log_name = log_name # A user friendly name for log entries
12
+
13
+ @getter_method = getter_method
14
+ @create_method = create_method
15
+ @update_method = update_method
16
+ @delete_method = delete_method
17
+
18
+ # Load the object from the API if needed
19
+ # The current config from Flapjack is passable to avoid polling the API for each individual contact
20
+ if my_id
21
+ @config[:id] = my_id
22
+ current_config = _load_from_api(my_id) unless current_config
23
+ end
24
+
25
+ if current_config
26
+ @config.merge! current_config
27
+ @obj_exists = true
28
+ else
29
+ @obj_exists = false
30
+ end
31
+ end
32
+
33
+ # Load the config from the API
34
+ def _load_from_api(my_id)
35
+ api_data = @getter_method.call(my_id)
36
+ return nil unless api_data
37
+
38
+ fail "Unexpected number of responses for #{@log_name} #{my_id}" unless api_data.length == 1
39
+ return api_data[0]
40
+ end
41
+
42
+ # Simple helper to return the ID
43
+ def id
44
+ return @config[:id]
45
+ end
46
+
47
+ def _reload_config
48
+ my_id = id
49
+ api_obj = @getter_method.call(my_id)
50
+ fail "Config reload failed for config ID #{my_id}: not found" unless api_obj
51
+ @config = api_obj[0]
52
+ fail "Config reload failed for config ID #{my_id}: parse error" unless @config
53
+ @obj_exists = true
54
+ end
55
+
56
+ # No base create object as the method arguments differ too much.
57
+
58
+ # Update the object
59
+ def _update(config)
60
+ fail("Object #{id} doesn't exist") unless @obj_exists
61
+ change_list = {}
62
+ config.each do |k, v|
63
+ k_sym = k.to_sym
64
+ if @config[k_sym] != v
65
+ change_list[k_sym] = v
66
+ end
67
+ end
68
+
69
+ return false if change_list.empty?
70
+
71
+ @logger.info("Updating #{@log_name} #{id}")
72
+ @logger.debug("#{@log_name} #{id} changes: #{change_list}")
73
+ fail "Failed to update #{id}" unless @update_method.call(id, change_list)
74
+ _reload_config
75
+ return true
76
+ end
77
+
78
+ # Delete the object
79
+ def delete
80
+ fail("Object #{id} doesn't exist") unless @obj_exists
81
+ @logger.info("Deleting #{@log_name} #{id}")
82
+ fail "Failed to delete #{id}" unless @delete_method.call(id)
83
+ @obj_exists = false
84
+ end
85
+ end
86
+ end