flapjack_configurator 1.0.0

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.
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