scooter 0.0.0 → 3.2.19

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 (47) hide show
  1. checksums.yaml +15 -0
  2. data/.env +5 -0
  3. data/.gitignore +47 -19
  4. data/Gemfile +3 -0
  5. data/HISTORY.md +1539 -0
  6. data/README.md +69 -10
  7. data/Rakefile +7 -0
  8. data/docs/http_dispatchers.md +79 -0
  9. data/lib/scooter.rb +11 -3
  10. data/lib/scooter/httpdispatchers.rb +12 -0
  11. data/lib/scooter/httpdispatchers/activity.rb +46 -0
  12. data/lib/scooter/httpdispatchers/activity/v1/v1.rb +50 -0
  13. data/lib/scooter/httpdispatchers/classifier.rb +376 -0
  14. data/lib/scooter/httpdispatchers/classifier/v1/v1.rb +99 -0
  15. data/lib/scooter/httpdispatchers/code_manager.rb +31 -0
  16. data/lib/scooter/httpdispatchers/code_manager/v1/v1.rb +17 -0
  17. data/lib/scooter/httpdispatchers/consoledispatcher.rb +132 -0
  18. data/lib/scooter/httpdispatchers/httpdispatcher.rb +168 -0
  19. data/lib/scooter/httpdispatchers/orchestrator/v1/v1.rb +87 -0
  20. data/lib/scooter/httpdispatchers/orchestratordispatcher.rb +83 -0
  21. data/lib/scooter/httpdispatchers/puppetdb/v4/v4.rb +51 -0
  22. data/lib/scooter/httpdispatchers/puppetdbdispatcher.rb +390 -0
  23. data/lib/scooter/httpdispatchers/rbac.rb +231 -0
  24. data/lib/scooter/httpdispatchers/rbac/v1/directory_service.rb +68 -0
  25. data/lib/scooter/httpdispatchers/rbac/v1/v1.rb +116 -0
  26. data/lib/scooter/ldap.rb +349 -0
  27. data/lib/scooter/ldap/ldap_fixtures.rb +60 -0
  28. data/lib/scooter/middleware/rbac_auth_token.rb +35 -0
  29. data/lib/scooter/utilities.rb +9 -0
  30. data/lib/scooter/utilities/beaker_utilities.rb +41 -0
  31. data/lib/scooter/utilities/string_utilities.rb +32 -0
  32. data/lib/scooter/version.rb +3 -1
  33. data/scooter.gemspec +23 -6
  34. data/spec/scooter/beaker_utilities_spec.rb +53 -0
  35. data/spec/scooter/httpdispatchers/activity/activity_spec.rb +218 -0
  36. data/spec/scooter/httpdispatchers/classifier/classifier_spec.rb +542 -0
  37. data/spec/scooter/httpdispatchers/code_manager/code-manager_spec.rb +67 -0
  38. data/spec/scooter/httpdispatchers/consoledispatcher_spec.rb +80 -0
  39. data/spec/scooter/httpdispatchers/httpdispatcher_spec.rb +91 -0
  40. data/spec/scooter/httpdispatchers/middleware/rbac_auth_token_spec.rb +58 -0
  41. data/spec/scooter/httpdispatchers/orchestratordispatcher_spec.rb +195 -0
  42. data/spec/scooter/httpdispatchers/puppetdbdispatcher_spec.rb +246 -0
  43. data/spec/scooter/httpdispatchers/rbac/rbac_spec.rb +387 -0
  44. data/spec/scooter/string_utilities_spec.rb +83 -0
  45. data/spec/spec_helper.rb +8 -0
  46. metadata +270 -18
  47. data/LICENSE.txt +0 -15
@@ -0,0 +1,87 @@
1
+ module Scooter
2
+ module HttpDispatchers
3
+ module Orchestrator
4
+ # Methods here are generally representative of endpoints
5
+ module V1
6
+
7
+ def initialize(host)
8
+ super(host)
9
+ @version = 'v1'
10
+ end
11
+
12
+ #jobs endpoints
13
+ def get_last_jobs(n_jobs)
14
+ @connection.get("#{@version}/jobs") do |req|
15
+ req.body = {:limit => n_jobs} if n_jobs
16
+ end
17
+ end
18
+
19
+ def get_job(job_id)
20
+ @connection.get("#{@version}/jobs/#{job_id}")
21
+ end
22
+
23
+ def get_nodes(job_id)
24
+ @connection.get("#{@version}/jobs/#{job_id}/nodes")
25
+ end
26
+
27
+ def get_report(job_id)
28
+ @connection.get("#{@version}/jobs/#{job_id}/report")
29
+ end
30
+
31
+ def get_events(job_id)
32
+ @connection.get("#{@version}/jobs/#{job_id}/events")
33
+ end
34
+
35
+ #environments endpoints
36
+ def get_environment(environment)
37
+ @connection.get("#{@version}/environments/#{environment}")
38
+ end
39
+
40
+ def get_applications_in_environment(environment)
41
+ @connection.get("#{@version}/environments/#{environment}/applications")
42
+ end
43
+
44
+ def get_instances_in_environment(environment)
45
+ @connection.get("#{@version}/environments/#{environment}/instances")
46
+ end
47
+
48
+ #command endpoints
49
+ def post_deploy(payload)
50
+ @connection.post("#{@version}/command/deploy") do |req|
51
+ req.body = payload
52
+ end
53
+ end
54
+
55
+ def post_stop(payload)
56
+ @connection.post("#{@version}/command/stop") do |req|
57
+ req.body = payload
58
+ end
59
+ end
60
+
61
+ def post_plan(payload)
62
+ @connection.post("#{@version}/command/plan") do |req|
63
+ req.body = payload
64
+ end
65
+ end
66
+
67
+ #inventory endpoints
68
+ def get_inventory(node=nil)
69
+ url = "#{@version}/inventory"
70
+ url << "/#{node}" if node
71
+ @connection.get(url)
72
+ end
73
+
74
+ def post_inventory(payload)
75
+ @connection.post("#{@version}/inventory") do |req|
76
+ req.body = payload
77
+ end
78
+ end
79
+
80
+ #status endpoint
81
+ def get_status
82
+ @connection.get("#{@version}/status")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,83 @@
1
+ %w( v1 ).each do |lib|
2
+ require "scooter/httpdispatchers/orchestrator/v1/#{lib}"
3
+ end
4
+
5
+ module Scooter
6
+ module HttpDispatchers
7
+ class OrchestratorDispatcher < HttpDispatcher
8
+
9
+ include Scooter::HttpDispatchers::Orchestrator::V1
10
+
11
+ def initialize(host)
12
+ super(host)
13
+ @connection.url_prefix.path = '/orchestrator'
14
+ @connection.url_prefix.port = 8143
15
+ end
16
+
17
+ # @return [Faraday::Response] response object from Faraday http client
18
+ def list_jobs(n_jobs=nil)
19
+ get_last_jobs(n_jobs)
20
+ end
21
+
22
+ # @return [Faraday::Response] response object from Faraday http client
23
+ def list_job_details(job_id)
24
+ get_job(job_id)
25
+ end
26
+
27
+ # @return [Faraday::Response] response object from Faraday http client
28
+ def list_nodes_associated_with_job(job_id)
29
+ get_nodes(job_id)
30
+ end
31
+
32
+ # @return [Faraday::Response] response object from Faraday http client
33
+ def get_job_report(job_id)
34
+ get_report(job_id)
35
+ end
36
+
37
+ # @return [Faraday::Response] response object from Faraday http client
38
+ def get_job_events(job_id)
39
+ get_events(job_id)
40
+ end
41
+
42
+ # @return [Faraday::Response] response object from Faraday http client
43
+ def environment(environment)
44
+ get_environment(environment)
45
+ end
46
+
47
+ # @return [Faraday::Response] response object from Faraday http client
48
+ def list_applications(environment)
49
+ get_applications_in_environment(environment)
50
+ end
51
+
52
+ # @return [Faraday::Response] response object from Faraday http client
53
+ def list_app_instances(environment)
54
+ get_instances_in_environment(environment)
55
+ end
56
+
57
+ # @return [Faraday::Response] response object from Faraday http client
58
+ def deploy_environment(environment, opts={})
59
+ payload = opts
60
+ payload['environment'] = environment
61
+ post_deploy(payload)
62
+ end
63
+
64
+ # @return [Faraday::Response] response object from Faraday http client
65
+ def stop_job(job_id)
66
+ post_stop({'job' => "/jobs/#{job_id}"})
67
+ end
68
+
69
+ # @return [Faraday::Response] response object from Faraday http client
70
+ def plan_job(environment, opts={})
71
+ payload = opts
72
+ payload['environment'] = environment
73
+ post_plan(payload)
74
+ end
75
+
76
+ # @return [Faraday::Response] response object from Faraday http client
77
+ def nodes_connected_to_broker(node_list)
78
+ payload = {'nodes' => node_list}
79
+ post_inventory(payload)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,51 @@
1
+ module Scooter
2
+ module HttpDispatchers
3
+ class PuppetdbDispatcher < HttpDispatcher
4
+ # Methods here are generally representative of endpoints, and depending
5
+ # on the method, return either a Faraday response object or some sort of
6
+ # instance of the object created/modified.
7
+ module V4
8
+
9
+ # @param [String] ast_query_string - An AST query string: https://docs.puppet.com/puppetdb/latest/api/query/v4/ast.html
10
+ # @return [Object]
11
+ def query_nodes(ast_query_string=nil)
12
+ set_puppetdb_path
13
+ @connection.post('query/v4/nodes') do |request|
14
+ request.params['query'] = ast_query_string unless ast_query_string.nil?
15
+ request.headers['Content-Type'] = 'application/json'
16
+ end
17
+ end
18
+
19
+ # @param [String] ast_query_string - An AST query string: https://docs.puppet.com/puppetdb/latest/api/query/v4/ast.html
20
+ # @return [Object]
21
+ def query_catalogs(ast_query_string=nil)
22
+ set_puppetdb_path
23
+ @connection.post('query/v4/catalogs') do |request|
24
+ request.params['query'] = ast_query_string unless ast_query_string.nil?
25
+ request.headers['Content-Type'] = 'application/json'
26
+ end
27
+ end
28
+
29
+ # @param [String] ast_query_string - An AST query string: https://docs.puppet.com/puppetdb/latest/api/query/v4/ast.html
30
+ # @return [Object]
31
+ def query_reports(ast_query_string=nil)
32
+ set_puppetdb_path
33
+ @connection.post('query/v4/reports') do |request|
34
+ request.params['query'] = ast_query_string unless ast_query_string.nil?
35
+ request.headers['Content-Type'] = 'application/json'
36
+ end
37
+ end
38
+
39
+ # @param [String] ast_query_string - An AST query string: https://docs.puppet.com/puppetdb/latest/api/query/v4/ast.html
40
+ # @return [Object]
41
+ def query_facts(ast_query_string=nil)
42
+ set_puppetdb_path
43
+ @connection.post('query/v4/facts') do |request|
44
+ request.params['query'] = ast_query_string unless ast_query_string.nil?
45
+ request.headers['Content-Type'] = 'application/json'
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,390 @@
1
+ %w( v4 ).each do |lib|
2
+ require "scooter/httpdispatchers/puppetdb/v4/#{lib}"
3
+ end
4
+ module Scooter
5
+ module HttpDispatchers
6
+ class PuppetdbDispatcher < HttpDispatcher
7
+ include Scooter::HttpDispatchers::PuppetdbDispatcher::V4
8
+
9
+ # Sets the path for puppetdb
10
+ # @param [Object] connection - the Faraday connection
11
+ def set_puppetdb_path(connection=self.connection)
12
+ set_url_prefix
13
+ connection.url_prefix.path = '/pdb'
14
+ connection.url_prefix.port = 8081
15
+ end
16
+
17
+ # Used to compare replica puppetdb to master. Raises exception if it does not match.
18
+ # @param [BeakerHost] host_name
19
+ def database_matches_self?(host_name)
20
+ original_host_name = self.host
21
+ begin
22
+ self.host = host_name
23
+ initialize_connection
24
+ other_nodes = query_nodes.body
25
+ other_catalogs = query_catalogs.body
26
+ other_facts = query_facts.body
27
+ other_reports = query_reports.body
28
+ ensure
29
+ self.host = original_host_name
30
+ initialize_connection
31
+ end
32
+
33
+ self_nodes = query_nodes.body
34
+ self_catalogs = query_catalogs.body
35
+ self_facts = query_facts.body
36
+ self_reports = query_reports.body
37
+
38
+ nodes_match = nodes_match?(other_nodes, self_nodes)
39
+ catalogs_match = catalogs_match?(other_catalogs, self_catalogs)
40
+ facts_match = facts_match?(other_facts, self_facts)
41
+ reports_match = reports_match?(other_reports, self_reports)
42
+
43
+ errors = ''
44
+ errors << "Nodes do not match\r\n" unless nodes_match
45
+ errors << "Catalogs do not match\r\n" unless catalogs_match
46
+ errors << "Facts do not match\r\n" unless facts_match
47
+ errors << "Reports do not match\r\n" unless reports_match
48
+
49
+ @faraday_logger.warn(errors.chomp) unless errors.empty?
50
+ errors.empty?
51
+ end
52
+
53
+ # Compares Replica PuppetDB with Master PuppetDB, to make sure Master PuppetDB has synced to Replica PuppetDB.
54
+ #
55
+ # N.B.: this uses a weird definition of "synced". We're NOT making sure the two PuppetDBs are exactly the same.
56
+ # We're just checking that the replica DB doesn't contain any records that aren't also in the master, and that
57
+ # the replica has at least one report from each node. We do this because there's a race condition-y window where
58
+ # an agent may have delivered a report to the Master PuppetDB, but the Replica PuppetDB hasn't picked it up yet.
59
+ # @param [BeakerHost] replica_host_name
60
+ # @param [Array] agents all the agents in the SUT, in the form of BeakerHost instances
61
+ def replica_db_synced_with_master_db?(replica_host_name, agents)
62
+ master_host_name = self.host
63
+ begin
64
+ self.host = replica_host_name
65
+ initialize_connection
66
+ replica_nodes = query_nodes.body
67
+ replica_catalogs = query_catalogs.body
68
+ replica_facts = query_facts.body
69
+ replica_reports = query_reports.body
70
+ ensure
71
+ self.host = master_host_name
72
+ initialize_connection
73
+ end
74
+ master_nodes = query_nodes.body
75
+ master_catalogs = query_catalogs.body
76
+ master_facts = query_facts.body
77
+ master_reports = query_reports.body
78
+
79
+ nodes_synced = nodes_synced?(agents, replica_nodes, master_nodes)
80
+ catalogs_synced = catalogs_synced?(agents, replica_catalogs, master_catalogs)
81
+ facts_synced = facts_synced?(replica_facts, master_facts)
82
+ reports_synced = reports_synced?(agents, replica_reports, master_reports)
83
+
84
+ errors = ''
85
+ errors << "Nodes not synced\r\n" unless nodes_synced
86
+ errors << "Catalogs not synced\r\n" unless catalogs_synced
87
+ errors << "Facts not synced\r\n" unless facts_synced
88
+ errors << "Reports not synced\r\n" unless reports_synced
89
+
90
+ @faraday_logger.warn(errors.chomp) unless errors.empty?
91
+ errors.empty?
92
+ end
93
+
94
+ private
95
+
96
+ # Check to see if all nodes match between two query responses
97
+ # @param [Object] other_nodes - response from query_nodes
98
+ # @param [Object] self_nodes - response from query_nodes
99
+ # @return [Boolean]
100
+ def nodes_match?(other_nodes, self_nodes=nil)
101
+ self_nodes = query_nodes.body if self_nodes.nil?
102
+ return false unless other_nodes.size == self_nodes.size
103
+ other_nodes.each_index { |index| return false unless node_match? other_nodes[index], self_nodes[index] }
104
+ true
105
+ end
106
+
107
+ # Check to see if all catalogs match between two query responses
108
+ # @param [Object] other_catalogs - response from query_catalogs
109
+ # @param [Object] self_catalogs - response from query_catalogs
110
+ # @return [Boolean]
111
+ def catalogs_match?(other_catalogs, self_catalogs=nil)
112
+ self_catalogs = query_catalogs.body if self_catalogs.nil?
113
+ return false unless other_catalogs.size == self_catalogs.size
114
+ other_catalogs.each_index { |index| return false unless catalog_match?(other_catalogs[index], self_catalogs[index]) }
115
+ true
116
+ end
117
+
118
+ # Check to see if all facts match between two query responses
119
+ # @param [Object] other_facts - response from query_facts
120
+ # @param [Object] self_facts - response from query_facts
121
+ # @return [Boolean]
122
+ def facts_match?(other_facts, self_facts=nil)
123
+ self_facts = query_facts.body if self_facts.nil?
124
+ same_num_elements?(other_facts, self_facts) && same_fact_contents?(other_facts, self_facts)
125
+ end
126
+
127
+ # Check to see if all reports match between two query responses
128
+ # @param [Object] other_reports - response from query_reports
129
+ # @param [Object] self_reports - response from query_reports
130
+ # @return [Boolean]
131
+ def reports_match?(other_reports, self_reports=nil)
132
+ self_reports = query_reports.body if self_reports.nil?
133
+ return false unless other_reports.size == self_reports.size
134
+ other_reports.each_index { |index| return false unless report_match?(other_reports[index], self_reports[index]) }
135
+ true
136
+ end
137
+
138
+ # Check to see if a specific node matches between two query responses
139
+ # @param [Object] other_node - one node from query_nodes
140
+ # @param [Object] self_node - one node from query_nodes
141
+ # @return [Boolean]
142
+ def node_match?(other_node, self_node)
143
+ keys_with_expected_diffs = ['facts_timestamp', 'catalog_timestamp']
144
+ same_num_elements?(other_node, self_node) && same_contents?(other_node, self_node, keys_with_expected_diffs)
145
+ end
146
+
147
+ # Check to see if a specific catalog matches between two query responses.
148
+ # We check to make sure byte lengths are the same because often both catalogs contain the same data, but
149
+ # in different order. That means we can't just walk the hash keys and make sure all values match up. Instead,
150
+ # we check certain keys explicitly (everything except 'resources' and 'edges') and assume that if the total byte
151
+ # size of each catalog is the same, that the contents are the same even in the keys whose values we don't check.
152
+ # @param [Object] other_catalog - one catalog from query_catalog
153
+ # @param [Object] self_catalog - one catalog from query_catalog
154
+ # @return [Boolean]
155
+ def catalog_match?(other_catalog, self_catalog)
156
+ keys_with_expected_diffs = ['resources', 'edges']
157
+ same_num_elements?(other_catalog, self_catalog) &&
158
+ same_byte_length?(other_catalog, self_catalog) &&
159
+ same_contents?(other_catalog, self_catalog, keys_with_expected_diffs)
160
+ end
161
+
162
+ # Check to see if a specific report matches between two query responses
163
+ # @param [Object] other_report - one report from query_reports
164
+ # @param [Object] self_report - one report from query_reports
165
+ # @return [Boolean]
166
+ def report_match?(other_report, self_report)
167
+ keys_with_expected_diffs = ['receive_time', 'resource_events']
168
+ same_num_elements?(other_report, self_report) && same_contents?(other_report, self_report, keys_with_expected_diffs)
169
+ end
170
+
171
+ # See if two JSON representations of Nodes, Catalogs, Facts, or Reports have the same number of elements.
172
+ # @param [Hash] hash1 first JSON representation to compare
173
+ # @param [Hash] hash2 second JSON representation to compare
174
+ # @return [Boolean]
175
+ def same_num_elements?(hash1, hash2)
176
+ hash1.size == hash2.size
177
+ end
178
+
179
+ # See if two JSON representations of Nodes, Catalogs, or Reports have the same byte length.
180
+ # This is useful to make sure the representations contain all the same data even if that data is stored
181
+ # in different order. This is exactly what happens when you replicate Catalogs from one PuppetDB instance
182
+ # to another.
183
+ # @param [Hash] hash1 the first JSON representation to compare
184
+ # @param [Hash] hash2 the second JSON representation to compare
185
+ # @return [Boolean]
186
+ def same_byte_length?(hash1, hash2)
187
+ hash1.to_s.length == hash2.to_s.length
188
+ end
189
+
190
+ # See if two JSON representations of Nodes, Catalogs, or Reports (but not Facts!) have the same values for
191
+ # all fields.
192
+ # @param [Hash] hash1 the first JSON representation to compare
193
+ # @param [Hash] hash2 the second JSON representation to compare
194
+ # @param [Array] keys_to_ignore any keys for which it's OK to have different values
195
+ # @return [Boolean]
196
+ def same_contents?(hash1, hash2, keys_to_ignore=[])
197
+ hash1.keys.each do |key|
198
+ next if keys_to_ignore.include?(key)
199
+ return false unless hash1[key] == hash2[key]
200
+ end
201
+ true
202
+ end
203
+
204
+ # See if two JSON representations of Facts have the same values for all fields (though the facts' order may differ).
205
+ # Algorithm: for each fact in the first set, scan through the entire second set looking for a matching fact.
206
+ # @param [Array] fact_set_1 the first JSON representation of facts to compare
207
+ # @param [Array] fact_set_2 the second JSON representation of facts to compare
208
+ # @return [Boolean]
209
+ def same_fact_contents?(fact_set_1, fact_set_2)
210
+ fact_set_1.each do |fact_from_first_set|
211
+ found_match = false
212
+ fact_set_2.each do |fact_from_second_set|
213
+ if fact_from_second_set == fact_from_first_set
214
+ found_match = true
215
+ break
216
+ end
217
+ end
218
+ return false unless found_match
219
+ end
220
+ true
221
+ end
222
+
223
+
224
+ # - - - - - - - - - -
225
+ # below here are methods to verify PuppetDB syncing for HA
226
+ # (as opposed to the strict matching methods above, which are used to verify services DB syncing for HA)
227
+ # - - - - - - - - - -
228
+
229
+
230
+ # Make sure of 2 conditions:
231
+ # 1. Master PuppetDB contains all the nodes that Replica PuppetDB contains
232
+ # 2. Replica PuppetDB contains a node for each actual node in the environment
233
+ # These two conditions are a minimal way to check that PuppetDB's node records have synced from Master to
234
+ # Replica, while allowing for gaps that can happen due to syncing race conditions.
235
+ # @param [Array] agents all of the system's agents, as an Array of BeakerHost objects
236
+ # @param [Object] replica_nodes response from query_nodes
237
+ # @param [Object] master_nodes response from query_nodes
238
+ # @return [Boolean]
239
+ def nodes_synced?(agents, replica_nodes, master_nodes=nil)
240
+ master_nodes = query_nodes.body if master_nodes.nil?
241
+ replica_nodes.each { |replica_node| return false unless master_has_node?(replica_node, master_nodes) }
242
+ agents.each { |agent| return false unless replica_has_node_for_agent?(replica_nodes, agent) }
243
+ true
244
+ end
245
+
246
+ # Make sure of 2 conditions:
247
+ # 1. Master PuppetDB contains all the catalogs that Replica PuppetDB contains
248
+ # 2. Replica PuppetDB contains a catalog for each actual node in the environment
249
+ # These two conditions are a minimal way to check that PuppetDB's catalogs have synced from Master to
250
+ # Replica, while allowing for gaps that can happen due to syncing race conditions.
251
+ # @param [Array] agents all of the system's agents, as an Array of BeakerHost objects
252
+ # @param [Object] replica_catalogs response from query_catalogs
253
+ # @param [Object] master_catalogs response from query_catalogs
254
+ # @return [Boolean]
255
+ def catalogs_synced?(agents, replica_catalogs, master_catalogs=nil)
256
+ master_catalogs = query_catalogs.body if master_catalogs.nil?
257
+ replica_catalogs.each { |replica_catalog| return false unless master_has_catalog?(replica_catalog, master_catalogs) }
258
+ agents.each { |agent| return false unless replica_has_catalog_for_agent?(replica_catalogs, agent) }
259
+ true
260
+ end
261
+
262
+ # See if the Replica PuppetDB has a subset of the facts in Master PuppetDB. Note that values can differ
263
+ # due to race conditions involving syncing, but certname, name, and environment must all match between
264
+ # Replica and Master fact sets.
265
+ # @param [Object] replica_facts response from query_facts
266
+ # @param [Object] master_facts response from query_facts
267
+ # @return [Boolean]
268
+ def facts_synced?(replica_facts, master_facts=nil)
269
+ master_facts = query_facts.body if master_facts.nil?
270
+ replica_facts.each do |replica_fact|
271
+ # TECH DEBT: the 'agent_specified_environment' fact is set on the scheduled agent by Beaker when created,
272
+ # but then is unset after the scheduled agent first checks in. This makes a gap between replica and master facts.
273
+ # We don't want to wait 2 mins for that fact to sync over to the replica, so for now, ignore it.
274
+ # NOTE: this *might* be caused by PE-18113, and when that's resolved we might be able to start paying attention
275
+ # to the agent_specified_environment fact again. We'll have to test and find out.
276
+ next if replica_fact['name'] == 'agent_specified_environment'
277
+ return false unless fact_synced?(replica_fact, master_facts)
278
+ end
279
+ true
280
+ end
281
+
282
+ # See if a single fact that's in Replica PuppetDB is also in Master PuppetDB. Note that values can differ
283
+ # due to race conditions involving syncing, but certname, name, and environment must all match between
284
+ # Replica and Master facts.
285
+ # @param [Hash] replica_fact a single fact from Replica PuppetDB
286
+ # @param [Array] master_facts all facts from Master PuppetDB, stored as Hashes
287
+ # @return [Boolean]
288
+ def fact_synced?(replica_fact, master_facts)
289
+ master_facts.each do |master_fact|
290
+ return true if ['certname', 'name', 'environment'].all? { |key| replica_fact[key] == master_fact[key] }
291
+ end
292
+ @faraday_logger.warn("*** fact sync failure: no Master fact matches Replica fact: #{replica_fact}")
293
+ false
294
+ end
295
+
296
+ # Make sure of 2 conditions:
297
+ # 1. Master PuppetDB contains all the reports that Replica PuppetDB contains
298
+ # 2. Replica PuppetDB contains a report for each actual node in the environment
299
+ # These two conditions are a minimal way to check that PuppetDB's reports have synced from Master to
300
+ # Replica, while allowing for gaps that can happen due to syncing race conditions.
301
+ # @param [Array] agents all of the system's agents, as an Array of BeakerHost objects
302
+ # @param [Object] replica_reports response from query_reports
303
+ # @param [Object] master_reports response from query_reports
304
+ # @return [Boolean]
305
+ def reports_synced?(agents, replica_reports, master_reports=nil)
306
+ master_reports = query_reports.body if master_reports.nil?
307
+ replica_reports.each { |replica_report| return false unless master_has_report?(replica_report, master_reports) }
308
+ agents.each { |agent| return false unless replica_has_report_for_agent?(replica_reports, agent) }
309
+ true
310
+ end
311
+
312
+ # See if Master PuppetDB has a copy of a particular catalog that's in Replica PuppetDB.
313
+ # Note that all we're checking for is that the Master and Replica each contain a catalog with a
314
+ # particular certname; we're not checking any of the other content in the catalog because of a possible
315
+ # race condition: a node checks in with Master and updates some of the fields in the catalog for that node,
316
+ # then the test compares field contents for the two catalogs, then the Replica syncs the new catalog.
317
+ # In that case, the fields (except for 'certname') could be very different between Master and Replica catalogs
318
+ # for a given node.
319
+ # @param [Hash] replica_catalog catalog in Replica PuppetDB, that we want to look for on Master PuppetDB
320
+ # @param [Array] master_catalogs catalogs in Master PuppetDB
321
+ # @return [Boolean]
322
+ def master_has_catalog?(replica_catalog, master_catalogs)
323
+ master_catalogs.each { |master_catalog| return true if replica_catalog['certname'] == master_catalog['certname'] }
324
+ @faraday_logger.warn("master doesn't have catalog with hash '#{replica_catalog['certname']}', which is on replica")
325
+ false
326
+ end
327
+
328
+ # See if Master PuppetDB has a copy of a particular node record that's in Replica PuppetDB.
329
+ # Note that all we're checking for is that the Master and Replica each contain a node record with a
330
+ # particular certname; we're not checking any of the other content in the node record because of a possible
331
+ # race condition: a node checks in with Master and updates some of the fields in its record,
332
+ # then the test compares field contents for the two node records, then the Replica syncs the new node record.
333
+ # In that case, the fields (except for 'certname') could be very different between Master and Replica node
334
+ # records for a given node.
335
+ # @param [Hash] replica_node node in Replica PuppetDB, that we want to look for on Master PuppetDB
336
+ # @param [Array] master_nodes nodes in Master PuppetDB
337
+ # @return [Boolean]
338
+ def master_has_node?(replica_node, master_nodes)
339
+ master_nodes.each { |master_node| return true if replica_node['certname'] == master_node['certname'] }
340
+ @faraday_logger.warn("master doesn't have node with certname '#{replica_node['certname']}', which is on replica")
341
+ false
342
+ end
343
+
344
+ # See if Master PuppetDB has a copy of a particular report that's found on Replica PuppetDB
345
+ # @param [Hash] replica_report report in Replica PuppetDB, that we want to look for on Master PuppetDB
346
+ # @param [Array] master_reports reports in Master PuppetDB
347
+ # @return [Boolean]
348
+ def master_has_report?(replica_report, master_reports)
349
+ keys_with_expected_diffs = ['receive_time', 'resource_events']
350
+ master_reports.each do |master_report|
351
+ same_hash = (replica_report['hash'] == master_report['hash'])
352
+ same_contents = same_contents?(replica_report, master_report, keys_with_expected_diffs)
353
+ return true if same_hash && same_contents
354
+ end
355
+ @faraday_logger.warn("master doesn't have report with hash '#{replica_report['hash']}', which is on replica")
356
+ false
357
+ end
358
+
359
+ # See if the Replica PuppetDB has at least one node record for the given agent.
360
+ # @param [Array] replica_nodes JSON representations of the nodes stored in Replica PuppetDB
361
+ # @param [BeakerHost] agent the agent that Replica PuppetDB should contain a node record for
362
+ # @return [Boolean]
363
+ def replica_has_node_for_agent?(replica_nodes, agent)
364
+ replica_nodes.each { |replica_node| return true if replica_node['certname'] == agent.hostname }
365
+ @faraday_logger.warn("replica doesn't have any nodes for certname '#{agent.hostname}'")
366
+ false
367
+ end
368
+
369
+ # See if the Replica PuppetDB has at least one catalog for the given agent.
370
+ # @param [Array] replica_reports JSON representations of the reports stored in Replica PuppetDB
371
+ # @param [BeakerHost] agent the agent that Replica PuppetDB should contain at least one report from
372
+ # @return [Boolean]
373
+ def replica_has_report_for_agent?(replica_reports, agent)
374
+ replica_reports.each { |replica_report| return true if replica_report['certname'] == agent.hostname }
375
+ @faraday_logger.warn("replica doesn't have any reports for certname '#{agent.hostname}'")
376
+ false
377
+ end
378
+
379
+ # See if the Replica PuppetDB has at least one catalog for the given agent.
380
+ # @param [Array] replica_catalogs JSON representations of the catalogs stored in Replica PuppetDB
381
+ # @param [BeakerHost] agent agent that Replica PuppetDB should contain at least one catalog for
382
+ # @return [Boolean]
383
+ def replica_has_catalog_for_agent?(replica_catalogs, agent)
384
+ replica_catalogs.each { |replica_catalog| return true if replica_catalog['certname'] == agent.hostname }
385
+ @faraday_logger.warn("replica doesn't have any catalogs for certname '#{agent.hostname}'")
386
+ false
387
+ end
388
+ end
389
+ end
390
+ end