topological_inventory-providers-common 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile +15 -0
- data/LICENSE.txt +21 -0
- data/README.md +3 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/topological_inventory/providers/common.rb +15 -0
- data/lib/topological_inventory/providers/common/collector.rb +178 -0
- data/lib/topological_inventory/providers/common/collector/inventory_collection_storage.rb +69 -0
- data/lib/topological_inventory/providers/common/collector/inventory_collection_wrapper.rb +25 -0
- data/lib/topological_inventory/providers/common/collector/parser.rb +34 -0
- data/lib/topological_inventory/providers/common/collectors_pool.rb +135 -0
- data/lib/topological_inventory/providers/common/logging.rb +22 -0
- data/lib/topological_inventory/providers/common/operations/endpoint_client.rb +62 -0
- data/lib/topological_inventory/providers/common/operations/processor.rb +135 -0
- data/lib/topological_inventory/providers/common/operations/sources_api_client.rb +85 -0
- data/lib/topological_inventory/providers/common/operations/topology_api_client.rb +28 -0
- data/lib/topological_inventory/providers/common/save_inventory/exception.rb +14 -0
- data/lib/topological_inventory/providers/common/save_inventory/saver.rb +124 -0
- data/lib/topological_inventory/providers/common/version.rb +7 -0
- data/topological_inventory-providers-common.gemspec +35 -0
- metadata +198 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
require "config"
|
2
|
+
|
3
|
+
module TopologicalInventory
|
4
|
+
module Providers
|
5
|
+
module Common
|
6
|
+
class CollectorsPool
|
7
|
+
SECRET_FILENAME = "credentials".freeze
|
8
|
+
|
9
|
+
def initialize(config_name, metrics, collector_poll_time: 60, thread_pool_size: 2)
|
10
|
+
self.config_name = config_name
|
11
|
+
self.collector_status = Concurrent::Map.new
|
12
|
+
self.metrics = metrics
|
13
|
+
self.collector_poll_time = collector_poll_time
|
14
|
+
self.secrets = nil
|
15
|
+
self.thread_pool = Concurrent::FixedThreadPool.new(thread_pool_size)
|
16
|
+
self.updated_at = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def run!
|
20
|
+
loop do
|
21
|
+
reload_config
|
22
|
+
reload_secrets
|
23
|
+
|
24
|
+
# Secret is deployed just after config map, we should wait for it
|
25
|
+
queue_collectors if secrets_newer_than_config?
|
26
|
+
|
27
|
+
sleep(5)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def stop!
|
32
|
+
collectors.each_value(&:stop)
|
33
|
+
|
34
|
+
thread_pool.shutdown
|
35
|
+
# Wait for end of collectors to ensure metrics are stopped after them
|
36
|
+
thread_pool.wait_for_termination
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
attr_accessor :collectors, :collector_poll_time, :collector_status, :thread_pool, :config_name,
|
42
|
+
:metrics, :secrets, :updated_at
|
43
|
+
|
44
|
+
def reload_config
|
45
|
+
config_file = File.join(path_to_config, "#{sanitize_filename(config_name)}.yml")
|
46
|
+
raise "Configuration file #{config_file} doesn't exist" unless File.exist?(config_file)
|
47
|
+
|
48
|
+
::Config.load_and_set_settings(config_file)
|
49
|
+
end
|
50
|
+
|
51
|
+
def reload_secrets
|
52
|
+
path = File.join(path_to_secrets, SECRET_FILENAME)
|
53
|
+
raise "Secrets file missing at #{path}" unless File.exists?(path)
|
54
|
+
file = File.read(path)
|
55
|
+
self.secrets = JSON.parse(file)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @param [Hash] source from Settings
|
59
|
+
# @return [Hash|nil] {"username":, "password":}
|
60
|
+
def secrets_for_source(source)
|
61
|
+
secrets[source.source]
|
62
|
+
end
|
63
|
+
|
64
|
+
def queue_collectors
|
65
|
+
::Settings.sources.to_a.each do |source|
|
66
|
+
# Skip if collector is running/queued or just finished
|
67
|
+
next if queued_or_updated_recently?(source)
|
68
|
+
|
69
|
+
# Check if secrets for this source are present
|
70
|
+
next if (source_secret = secrets_for_source(source)).nil?
|
71
|
+
|
72
|
+
# Check if necessary endpoint/auth data are not blank (provider specific)
|
73
|
+
next unless source_valid?(source, source_secret)
|
74
|
+
|
75
|
+
collector_status[source.source] = {:status => :queued}
|
76
|
+
# Add source to collector's queue
|
77
|
+
thread_pool.post do
|
78
|
+
begin
|
79
|
+
collector = new_collector(source, source_secret)
|
80
|
+
collector.collect!
|
81
|
+
ensure
|
82
|
+
collector_status[source.source] = {:status => :ready, :last_updated_at => Time.now}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def queued_or_updated_recently?(source)
|
89
|
+
return false if collector_status[source.source].nil?
|
90
|
+
return true if collector_status[source.source][:status] == :queued
|
91
|
+
|
92
|
+
if (last_updated_at = collector_status[source.source][:last_updated_at]).nil?
|
93
|
+
# should never happen
|
94
|
+
last_updated_at = Time.now
|
95
|
+
collector_status[source.source] = {:status => :ready, :last_updated_at => last_updated_at}
|
96
|
+
end
|
97
|
+
|
98
|
+
last_updated_at > Time.now - collector_poll_time.to_i
|
99
|
+
end
|
100
|
+
|
101
|
+
def secrets_newer_than_config?
|
102
|
+
return false if ::Settings.updated_at.nil? || secrets["updated_at"].nil?
|
103
|
+
|
104
|
+
updated_at[:config] = Time.parse(::Settings.updated_at)
|
105
|
+
updated_at[:secret] = Time.parse(secrets["updated_at"])
|
106
|
+
|
107
|
+
logger.info("Reloading Sources data => Config [updated_at: #{updated_at[:config].to_s}], Secrets [updated at: #{updated_at[:secret]}]") if updated_at[:config] <= updated_at[:secret]
|
108
|
+
|
109
|
+
updated_at[:config] <= updated_at[:secret]
|
110
|
+
end
|
111
|
+
|
112
|
+
def source_valid?(source, secret)
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
def path_to_config
|
117
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
118
|
+
end
|
119
|
+
|
120
|
+
def path_to_secrets
|
121
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
122
|
+
end
|
123
|
+
|
124
|
+
def sanitize_filename(filename)
|
125
|
+
# Remove any character that aren't 0-9, A-Z, or a-z, / or -
|
126
|
+
filename.gsub(/[^0-9A-Z\/\-]/i, '_')
|
127
|
+
end
|
128
|
+
|
129
|
+
def new_collector(source, source_secret)
|
130
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "manageiq/loggers"
|
2
|
+
|
3
|
+
module TopologicalInventory
|
4
|
+
module Providers
|
5
|
+
module Common
|
6
|
+
class << self
|
7
|
+
attr_writer :logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.logger
|
11
|
+
@logger ||= ManageIQ::Loggers::Container.new
|
12
|
+
@logger
|
13
|
+
end
|
14
|
+
|
15
|
+
module Logging
|
16
|
+
def logger
|
17
|
+
TopologicalInventory::Providers::Common.logger
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "topological_inventory/providers/common/operations/topology_api_client"
|
2
|
+
require "topological_inventory/providers/common/operations/sources_api_client"
|
3
|
+
|
4
|
+
module TopologicalInventory
|
5
|
+
module Providers
|
6
|
+
module Common
|
7
|
+
module Operations
|
8
|
+
class EndpointClient
|
9
|
+
include TopologyApiClient
|
10
|
+
|
11
|
+
def initialize(source_id, task_id, identity = nil)
|
12
|
+
self.identity = identity
|
13
|
+
self.source_id = source_id
|
14
|
+
self.task_id = task_id
|
15
|
+
end
|
16
|
+
|
17
|
+
def order_service(service_offering, service_plan, order_params)
|
18
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
19
|
+
end
|
20
|
+
|
21
|
+
def source_ref_of(endpoint_svc_instance)
|
22
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
23
|
+
end
|
24
|
+
|
25
|
+
def wait_for_provision_complete(source_id, endpoint_svc_instance, context = {})
|
26
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
27
|
+
end
|
28
|
+
|
29
|
+
def provisioned_successfully?(endpoint_svc_instance)
|
30
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
31
|
+
end
|
32
|
+
|
33
|
+
# Endpoint for conversion of provisioned service's status to
|
34
|
+
# TopologicalInventory Task's status
|
35
|
+
def task_status_for(endpoint_svc_instance)
|
36
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_accessor :identity, :task_id, :source_id
|
42
|
+
|
43
|
+
def sources_api
|
44
|
+
@sources_api ||= SourcesApiClient.new(identity)
|
45
|
+
end
|
46
|
+
|
47
|
+
def default_endpoint
|
48
|
+
@default_endpoint ||= sources_api.fetch_default_endpoint(source_id)
|
49
|
+
end
|
50
|
+
|
51
|
+
def authentication
|
52
|
+
@authentication ||= sources_api.fetch_authentication(source_id, default_endpoint)
|
53
|
+
end
|
54
|
+
|
55
|
+
def verify_ssl_mode
|
56
|
+
default_endpoint.verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require "topological_inventory/providers/common/operations/topology_api_client"
|
2
|
+
|
3
|
+
module TopologicalInventory
|
4
|
+
module Providers
|
5
|
+
module Common
|
6
|
+
module Operations
|
7
|
+
class Processor
|
8
|
+
include Logging
|
9
|
+
include TopologyApiClient
|
10
|
+
|
11
|
+
SLEEP_POLL = 10
|
12
|
+
POLL_TIMEOUT = 1800
|
13
|
+
|
14
|
+
def self.process!(message)
|
15
|
+
model, method = message.headers['message_type'].to_s.split(".")
|
16
|
+
new(model, method, message.payload).process
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param payload [Hash] https://github.com/ManageIQ/topological_inventory-api/blob/master/app/controllers/api/v0/service_plans_controller.rb#L32-L41
|
20
|
+
def initialize(model, method, payload, metrics = nil)
|
21
|
+
self.model = model
|
22
|
+
self.method = method
|
23
|
+
self.params = payload["params"]
|
24
|
+
self.identity = payload["request_context"]
|
25
|
+
self.metrics = metrics
|
26
|
+
end
|
27
|
+
|
28
|
+
def process
|
29
|
+
logger.info("Processing #{model}##{method} [#{params}]...")
|
30
|
+
result = order_service(params)
|
31
|
+
logger.info("Processing #{model}##{method} [#{params}]...Complete")
|
32
|
+
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_accessor :identity, :model, :method, :metrics, :params
|
39
|
+
|
40
|
+
def endpoint_client(source_id, task_id, identity)
|
41
|
+
raise NotImplementedError, "#{__method__} must be implemented in a subclass as kind of TopologicalInventory::Providers::Common::EndpointClient class"
|
42
|
+
end
|
43
|
+
|
44
|
+
def order_service(params)
|
45
|
+
task_id, service_offering_id, service_plan_id, order_params = params.values_at("task_id", "service_offering_id", "service_plan_id", "order_params")
|
46
|
+
|
47
|
+
service_plan = topology_api_client.show_service_plan(service_plan_id) if service_plan_id.present?
|
48
|
+
service_offering_id = service_plan.service_offering_id if service_offering_id.nil? && service_plan.present?
|
49
|
+
service_offering = topology_api_client.show_service_offering(service_offering_id)
|
50
|
+
|
51
|
+
source_id = service_offering.source_id
|
52
|
+
client = endpoint_client(source_id, task_id, identity)
|
53
|
+
|
54
|
+
logger.info("Ordering #{service_offering.name}...")
|
55
|
+
remote_service_instance = client.order_service(service_offering, service_plan.presence, order_params)
|
56
|
+
logger.info("Ordering #{service_offering.name}...Complete")
|
57
|
+
|
58
|
+
poll_order_complete_thread(task_id, source_id, remote_service_instance)
|
59
|
+
rescue StandardError => err
|
60
|
+
metrics&.record_error
|
61
|
+
logger.error("[Task #{task_id}] Ordering error: #{err}\n#{err.backtrace.join("\n")}")
|
62
|
+
update_task(task_id, :state => "completed", :status => "error", :context => {:error => err.to_s})
|
63
|
+
end
|
64
|
+
|
65
|
+
def poll_order_complete_thread(task_id, source_id, remote_svc_instance)
|
66
|
+
Thread.new do
|
67
|
+
begin
|
68
|
+
poll_order_complete(task_id, source_id, remote_svc_instance)
|
69
|
+
rescue StandardError => err
|
70
|
+
logger.error("[Task #{task_id}] Waiting for complete: #{err}\n#{err.backtrace.join("\n")}")
|
71
|
+
update_task(task_id, :state => "completed", :status => "warn", :context => {:error => err.to_s})
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def poll_order_complete(task_id, source_id, remote_svc_instance)
|
77
|
+
client = endpoint_client(source_id, task_id, identity)
|
78
|
+
|
79
|
+
context = {
|
80
|
+
:service_instance => {
|
81
|
+
:source_id => source_id,
|
82
|
+
:source_ref => client.source_ref_of(remote_svc_instance)
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
remote_svc_instance = client.wait_for_provision_complete(task_id, remote_svc_instance, context)
|
87
|
+
|
88
|
+
if client.provisioned_successfully?(remote_svc_instance)
|
89
|
+
if (service_instance = load_topological_svc_instance(source_id, client.source_ref_of(remote_svc_instance))).present?
|
90
|
+
context[:service_instance][:id] = service_instance.id
|
91
|
+
context[:service_instance][:url] = svc_instance_url(service_instance)
|
92
|
+
else
|
93
|
+
logger.warn("Failed to get service_instance API URL (endpoint's service instance: #{remote_svc_instance.inspect})")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
update_task(task_id, :state => "completed", :status => client.task_status_for(remote_svc_instance), :context => context)
|
97
|
+
end
|
98
|
+
|
99
|
+
def load_topological_svc_instance(source_id, source_ref)
|
100
|
+
api = topology_api_client.api_client
|
101
|
+
|
102
|
+
count = 0
|
103
|
+
timeout_count = POLL_TIMEOUT / SLEEP_POLL
|
104
|
+
|
105
|
+
header_params = { 'Accept' => api.select_header_accept(['application/json']) }
|
106
|
+
query_params = { :'source_id' => source_id, :'source_ref' => source_ref }
|
107
|
+
return_type = 'ServiceInstancesCollection'
|
108
|
+
|
109
|
+
service_instance = nil
|
110
|
+
loop do
|
111
|
+
data, _status_code, _headers = api.call_api(:GET, "/service_instances",
|
112
|
+
:header_params => header_params,
|
113
|
+
:query_params => query_params,
|
114
|
+
:auth_names => ['UserSecurity'],
|
115
|
+
:return_type => return_type)
|
116
|
+
|
117
|
+
service_instance = data.data&.first if data.meta.count > 0
|
118
|
+
break if service_instance.present?
|
119
|
+
|
120
|
+
break if (count += 1) >= timeout_count
|
121
|
+
|
122
|
+
sleep(SLEEP_POLL) # seconds
|
123
|
+
end
|
124
|
+
|
125
|
+
if service_instance.nil?
|
126
|
+
logger.error("Failed to find service_instance by source_id [#{source_id}] source_ref [#{source_ref}]")
|
127
|
+
end
|
128
|
+
|
129
|
+
service_instance
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require "sources-api-client"
|
2
|
+
|
3
|
+
module TopologicalInventory
|
4
|
+
module Providers
|
5
|
+
module Common
|
6
|
+
module Operations
|
7
|
+
class SourcesApiClient < ::SourcesApiClient::ApiClient
|
8
|
+
INTERNAL_API_PATH = '//internal/v1.0'.freeze
|
9
|
+
|
10
|
+
def initialize(identity = nil)
|
11
|
+
super(::SourcesApiClient::Configuration.default)
|
12
|
+
self.identity = identity
|
13
|
+
self.api = init_default_api
|
14
|
+
end
|
15
|
+
|
16
|
+
def init_default_api
|
17
|
+
default_headers.merge!(identity) if identity.present?
|
18
|
+
::SourcesApiClient::DefaultApi.new(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch_default_endpoint(source_id)
|
22
|
+
endpoints = api.list_source_endpoints(source_id)&.data || []
|
23
|
+
endpoint = endpoints.find(&:default)
|
24
|
+
|
25
|
+
raise "Sources API: Endpoint not found! (source id: #{source_id})" if endpoint.nil?
|
26
|
+
|
27
|
+
endpoint
|
28
|
+
end
|
29
|
+
|
30
|
+
def fetch_authentication(source_id, default_endpoint = nil)
|
31
|
+
endpoint = default_endpoint || fetch_default_endpoint(source_id)
|
32
|
+
return if endpoint.nil?
|
33
|
+
|
34
|
+
endpoint_authentications = api.list_endpoint_authentications(endpoint.id.to_s).data || []
|
35
|
+
return if endpoint_authentications.empty?
|
36
|
+
|
37
|
+
auth_id = endpoint_authentications.first.id
|
38
|
+
fetch_authentication_with_password(auth_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_accessor :identity, :api, :custom_base_path
|
44
|
+
|
45
|
+
def fetch_authentication_with_password(auth_id)
|
46
|
+
on_internal_api do
|
47
|
+
local_var_path = "/authentications/#{auth_id}"
|
48
|
+
|
49
|
+
query_params = "expose_encrypted_attribute[]=password"
|
50
|
+
|
51
|
+
header_params = { 'Accept' => select_header_accept(['application/json']) }
|
52
|
+
return_type = 'Authentication'
|
53
|
+
data, _, _ = call_api(:GET, local_var_path,
|
54
|
+
:header_params => header_params,
|
55
|
+
:query_params => query_params,
|
56
|
+
:auth_names => ['UserSecurity'],
|
57
|
+
:return_type => return_type)
|
58
|
+
data
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_request_url(path)
|
63
|
+
# Add leading and trailing slashes to path
|
64
|
+
path = "/#{path}".gsub(/\/+/, '/')
|
65
|
+
URI.encode((custom_base_url || @config.base_url) + path)
|
66
|
+
end
|
67
|
+
|
68
|
+
def custom_base_url
|
69
|
+
return nil if custom_base_path.nil?
|
70
|
+
|
71
|
+
url = "#{@config.scheme}://#{[@config.host, custom_base_path].join('/').gsub(/\/+/, '/')}".sub(/\/+\z/, '')
|
72
|
+
URI.encode(url)
|
73
|
+
end
|
74
|
+
|
75
|
+
def on_internal_api
|
76
|
+
self.custom_base_path = INTERNAL_API_PATH
|
77
|
+
yield
|
78
|
+
ensure
|
79
|
+
self.custom_base_path = nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module TopologicalInventory
|
2
|
+
module Providers
|
3
|
+
module Common
|
4
|
+
module Operations
|
5
|
+
module TopologyApiClient
|
6
|
+
def topology_api_client
|
7
|
+
@topology_api_client ||=
|
8
|
+
begin
|
9
|
+
api_client = TopologicalInventoryApiClient::ApiClient.new
|
10
|
+
api_client.default_headers.merge!(identity) if identity.present?
|
11
|
+
TopologicalInventoryApiClient::DefaultApi.new(api_client)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def update_task(task_id, state:, status:, context:)
|
16
|
+
task = TopologicalInventoryApiClient::Task.new("state" => state, "status" => status, "context" => context)
|
17
|
+
topology_api_client.update_task(task_id, task)
|
18
|
+
end
|
19
|
+
|
20
|
+
def svc_instance_url(service_instance)
|
21
|
+
rest_api_path = '/service_instances/{id}'.sub('{' + 'id' + '}', service_instance&.id.to_s)
|
22
|
+
topology_api_client.api_client.build_request(:GET, rest_api_path).url
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|