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.
- checksums.yaml +15 -0
- data/.env +5 -0
- data/.gitignore +47 -19
- data/Gemfile +3 -0
- data/HISTORY.md +1539 -0
- data/README.md +69 -10
- data/Rakefile +7 -0
- data/docs/http_dispatchers.md +79 -0
- data/lib/scooter.rb +11 -3
- data/lib/scooter/httpdispatchers.rb +12 -0
- data/lib/scooter/httpdispatchers/activity.rb +46 -0
- data/lib/scooter/httpdispatchers/activity/v1/v1.rb +50 -0
- data/lib/scooter/httpdispatchers/classifier.rb +376 -0
- data/lib/scooter/httpdispatchers/classifier/v1/v1.rb +99 -0
- data/lib/scooter/httpdispatchers/code_manager.rb +31 -0
- data/lib/scooter/httpdispatchers/code_manager/v1/v1.rb +17 -0
- data/lib/scooter/httpdispatchers/consoledispatcher.rb +132 -0
- data/lib/scooter/httpdispatchers/httpdispatcher.rb +168 -0
- data/lib/scooter/httpdispatchers/orchestrator/v1/v1.rb +87 -0
- data/lib/scooter/httpdispatchers/orchestratordispatcher.rb +83 -0
- data/lib/scooter/httpdispatchers/puppetdb/v4/v4.rb +51 -0
- data/lib/scooter/httpdispatchers/puppetdbdispatcher.rb +390 -0
- data/lib/scooter/httpdispatchers/rbac.rb +231 -0
- data/lib/scooter/httpdispatchers/rbac/v1/directory_service.rb +68 -0
- data/lib/scooter/httpdispatchers/rbac/v1/v1.rb +116 -0
- data/lib/scooter/ldap.rb +349 -0
- data/lib/scooter/ldap/ldap_fixtures.rb +60 -0
- data/lib/scooter/middleware/rbac_auth_token.rb +35 -0
- data/lib/scooter/utilities.rb +9 -0
- data/lib/scooter/utilities/beaker_utilities.rb +41 -0
- data/lib/scooter/utilities/string_utilities.rb +32 -0
- data/lib/scooter/version.rb +3 -1
- data/scooter.gemspec +23 -6
- data/spec/scooter/beaker_utilities_spec.rb +53 -0
- data/spec/scooter/httpdispatchers/activity/activity_spec.rb +218 -0
- data/spec/scooter/httpdispatchers/classifier/classifier_spec.rb +542 -0
- data/spec/scooter/httpdispatchers/code_manager/code-manager_spec.rb +67 -0
- data/spec/scooter/httpdispatchers/consoledispatcher_spec.rb +80 -0
- data/spec/scooter/httpdispatchers/httpdispatcher_spec.rb +91 -0
- data/spec/scooter/httpdispatchers/middleware/rbac_auth_token_spec.rb +58 -0
- data/spec/scooter/httpdispatchers/orchestratordispatcher_spec.rb +195 -0
- data/spec/scooter/httpdispatchers/puppetdbdispatcher_spec.rb +246 -0
- data/spec/scooter/httpdispatchers/rbac/rbac_spec.rb +387 -0
- data/spec/scooter/string_utilities_spec.rb +83 -0
- data/spec/spec_helper.rb +8 -0
- metadata +270 -18
- 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
|