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