topological_inventory-providers-common 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0f2d31b001af718d1e0cb85a61754fcc3a7e74073718083aaf2706190906606e
4
+ data.tar.gz: 28c1849fa09d45ec3f33600d43db393521dbd70c1c51775f5092cba1bceba91d
5
+ SHA512:
6
+ metadata.gz: a0831719dc7226ba7aabb254b6d43fe154a779e69bac479c4d4fc6be3bf9c1990d86ffb9c4ee0a44fdd3695edd2ee83a522109bcf04cec2694c272ab5393999a
7
+ data.tar.gz: fc9e8ccc5478379cde68f8b6fd6dcdd00db2d0193c692dbf1563b6d4e0d753b58eca9fe9eed30688f8bf0a6194b3369b44f4ecfce5fcd008df632f654b78d1b6
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /bundler.d/
10
+ /Gemfile.lock
11
+
12
+ # rspec failure tracking
13
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,10 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.4.5
7
+ - 2.5.3
8
+ before_install:
9
+ - 'echo ''gem: --no-ri --no-rdoc --no-document'' > ~/.gemrc'
10
+ - gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "https://rubygems.org"
2
+
3
+ plugin 'bundler-inject', '~> 1.1'
4
+ require File.join(Bundler::Plugin.index.load_paths("bundler-inject")[0], "bundler-inject") rescue nil
5
+
6
+ # Specify your gem's dependencies in topological_inventory-providers-common.gemspec
7
+ gemspec
8
+
9
+ gem "sources-api-client", "~> 1.0"
10
+ gem "topological_inventory-ingress_api-client", "~> 1.0"
11
+
12
+ group :development, :test do
13
+ gem 'rake', '~> 12.0.0'
14
+ gem 'pry-byebug'
15
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Martin Slemr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,3 @@
1
+ # TopologicalInventory::Providers::Common
2
+
3
+ Common code for topological-inventory collectors and Operation workers.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "topological_inventory/providers/common"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,15 @@
1
+ require "topological_inventory/providers/common/version"
2
+ require "topological_inventory/providers/common/logging"
3
+ require "topological_inventory/providers/common/operations/processor"
4
+ require "topological_inventory/providers/common/operations/endpoint_client"
5
+ require "topological_inventory/providers/common/collectors_pool"
6
+ require "topological_inventory/providers/common/collector"
7
+
8
+ module TopologicalInventory
9
+ module Providers
10
+ module Common
11
+ class Error < StandardError; end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,162 @@
1
+ require "active_support/inflector"
2
+ require "concurrent"
3
+ require "topological_inventory-ingress_api-client"
4
+ require "topological_inventory/providers/common/collector/inventory_collection_storage"
5
+ require "topological_inventory/providers/common/collector/inventory_collection_wrapper"
6
+ require "topological_inventory/providers/common/collector/parser"
7
+ require "topological_inventory/providers/common/save_inventory/saver"
8
+
9
+ module TopologicalInventory
10
+ module Providers
11
+ module Common
12
+ class Collector
13
+ # @param poll_time [Integer] Waiting between collecting loops. Irrelevant for standalone_mode: true
14
+ # @param standalone_mode [Boolean] T/F if collector is created by collectors_pool
15
+ def initialize(source, default_limit: 1_000, poll_time: 30, standalone_mode: true)
16
+ self.collector_threads = Concurrent::Map.new
17
+ self.finished = Concurrent::AtomicBoolean.new(false)
18
+ self.poll_time = poll_time
19
+ self.limits = Hash.new(default_limit)
20
+ self.queue = Queue.new
21
+ self.source = source
22
+ self.standalone_mode = standalone_mode
23
+ end
24
+
25
+ def collect!
26
+ start_collector_threads
27
+
28
+ until finished? do
29
+ ensure_collector_threads
30
+
31
+ notices = []
32
+ notices << queue.pop until queue.empty?
33
+
34
+ targeted_refresh(notices) unless notices.empty?
35
+
36
+ standalone_mode ? sleep(poll_time) : stop
37
+ end
38
+ end
39
+
40
+ def stop
41
+ finished.value = true
42
+ end
43
+
44
+ protected
45
+
46
+ attr_accessor :collector_threads, :finished, :limits,
47
+ :poll_time, :queue, :source, :standalone_mode
48
+
49
+ def finished?
50
+ finished.value
51
+ end
52
+
53
+ def entity_types
54
+ endpoint_types.flat_map { |endpoint| send("#{endpoint}_entity_types") }
55
+ end
56
+
57
+ # Should be overriden by subclass
58
+ # Entity types collected from endpoints
59
+ def endpoint_types
60
+ %w()
61
+ end
62
+
63
+ def start_collector_threads
64
+ entity_types.each do |entity_type|
65
+ next if collector_threads[entity_type]&.alive?
66
+
67
+ collector_threads[entity_type] = start_collector_thread(entity_type)
68
+ end
69
+ end
70
+
71
+ def ensure_collector_threads
72
+ start_collector_threads
73
+ end
74
+
75
+ def start_collector_thread(entity_type)
76
+ connection = connection_for_entity_type(entity_type)
77
+ return if connection.nil?
78
+
79
+ Thread.new do
80
+ collector_thread(connection, entity_type)
81
+ end
82
+ end
83
+
84
+ # Connection to endpoint for each entity type
85
+ def connection_for_entity_type(_entity_type)
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # Thread's main for collecting one entity type's data
90
+ def collector_thread(_connection, _entity_type)
91
+ raise NotImplementedError
92
+ end
93
+
94
+ # @optional
95
+ # Listen to notices from threads
96
+ def targeted_refresh(notices)
97
+ end
98
+
99
+ def save_inventory(collections,
100
+ inventory_name,
101
+ schema,
102
+ refresh_state_uuid = nil,
103
+ refresh_state_part_uuid = nil)
104
+ return 0 if collections.empty?
105
+
106
+ SaveInventory::Saver.new(:client => ingress_api_client, :logger => logger).save(
107
+ :inventory => TopologicalInventoryIngressApiClient::Inventory.new(
108
+ :name => inventory_name,
109
+ :schema => TopologicalInventoryIngressApiClient::Schema.new(:name => schema),
110
+ :source => source,
111
+ :collections => collections,
112
+ :refresh_state_uuid => refresh_state_uuid,
113
+ :refresh_state_part_uuid => refresh_state_part_uuid,
114
+ )
115
+ )
116
+ rescue => e
117
+ response_body = e.response_body if e.respond_to? :response_body
118
+ response_headers = e.response_headers if e.respond_to? :response_headers
119
+ logger.error("Error when sending payload to Ingress API. Error message: #{e.message}. Body: #{response_body}. Header: #{response_headers}")
120
+ raise e
121
+ end
122
+
123
+ def sweep_inventory(inventory_name,
124
+ schema,
125
+ refresh_state_uuid,
126
+ total_parts,
127
+ sweep_scope)
128
+ return if !total_parts || sweep_scope.empty?
129
+
130
+ SaveInventory::Saver.new(:client => ingress_api_client, :logger => logger).save(
131
+ :inventory => TopologicalInventoryIngressApiClient::Inventory.new(
132
+ :name => inventory_name,
133
+ :schema => TopologicalInventoryIngressApiClient::Schema.new(:name => schema),
134
+ :source => source,
135
+ :collections => [],
136
+ :refresh_state_uuid => refresh_state_uuid,
137
+ :total_parts => total_parts,
138
+ :sweep_scope => sweep_scope,
139
+ )
140
+ )
141
+ rescue => e
142
+ response_body = e.response_body if e.respond_to? :response_body
143
+ response_headers = e.response_headers if e.respond_to? :response_headers
144
+ logger.error("Error when sending payload to Ingress API. Error message: #{e.message}. Body: #{response_body}. Header: #{response_headers}")
145
+ raise e
146
+ end
147
+
148
+ def inventory_name
149
+ "Default"
150
+ end
151
+
152
+ def schema_name
153
+ "Default"
154
+ end
155
+
156
+ def ingress_api_client
157
+ TopologicalInventoryIngressApiClient::DefaultApi.new
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,69 @@
1
+ module TopologicalInventory
2
+ module Providers
3
+ module Common
4
+ class Collector
5
+ class InventoryCollectionStorage
6
+ attr_accessor :data
7
+
8
+ delegate :values, :to => :data
9
+
10
+ def initialize
11
+ @data = {}
12
+ end
13
+
14
+ def add_collection(name, overwrite: true)
15
+ return @data[name] if !@data[name].nil? && !overwrite
16
+
17
+ if ingress_api_model_exists?(name)
18
+ @data[name] ||= InventoryCollectionWrapper.new(:name => name)
19
+ else
20
+ raise NameError, "TopologicalInventoryIngressApiClient::#{name.to_s.classify} doesn't exist"
21
+ end
22
+ end
23
+
24
+ # Creates collection automatically
25
+ def [](name)
26
+ add_collection(name, :overwrite => false)
27
+ end
28
+
29
+ # @return [Array<Symbol>] array of InventoryCollection object names of the persister
30
+ def inventory_collections_names
31
+ @data.keys
32
+ end
33
+
34
+ def method_missing(method_name, *arguments, &block)
35
+ add_collection(method_name, :overwrite => false) # init collection if not exist
36
+
37
+ if inventory_collections_names.include?(method_name)
38
+ self.class.define_collections_reader(method_name)
39
+ send(method_name)
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ # @return [Boolean] true if InventoryCollection with passed method_name name is defined
46
+ def respond_to_missing?(method_name, _include_private = false)
47
+ ingress_api_model_exists?(method_name) || super
48
+ end
49
+
50
+ protected
51
+
52
+ def ingress_api_model_exists?(method_name)
53
+ class_name = "TopologicalInventoryIngressApiClient::#{method_name.to_s.classify}"
54
+
55
+ # nil test is not enough due to sometimes weird namespace autoloading
56
+ class_name.safe_constantize.to_s == class_name
57
+ end
58
+
59
+ # Defines a new attr reader returning InventoryCollection object
60
+ def self.define_collections_reader(collection_key)
61
+ define_method(collection_key) do
62
+ add_collection(collection_key, :overwrite => false)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,25 @@
1
+ module TopologicalInventory
2
+ module Providers
3
+ module Common
4
+ class Collector
5
+ class InventoryCollectionWrapper < TopologicalInventoryIngressApiClient::InventoryCollection
6
+ def initialize(name:)
7
+ super(:name => name, :data => [])
8
+ end
9
+
10
+ def build(properties)
11
+ obj = get_model.new(properties)
12
+ data << obj
13
+ obj
14
+ end
15
+
16
+ protected
17
+
18
+ def get_model
19
+ "TopologicalInventoryIngressApiClient::#{name.to_s.classify}".constantize
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ require "active_support/inflector"
2
+
3
+ module TopologicalInventory
4
+ module Providers
5
+ module Common
6
+ class Collector
7
+ class Parser
8
+ attr_accessor :collections, :resource_timestamp
9
+
10
+ delegate :add_collection, :to => :collections
11
+
12
+ def initialize
13
+ @collections = InventoryCollectionStorage.new
14
+
15
+ self.resource_timestamp = Time.now.utc
16
+ end
17
+
18
+ def lazy_find(collection, reference, ref: :manager_ref)
19
+ return if reference.kind_of?(String) && reference.blank?
20
+
21
+ # Don't make lazy link if all reference values are blank
22
+ return if reference.kind_of?(Hash) && reference.values.select { |val| val.to_s.present? }.blank?
23
+
24
+ TopologicalInventoryIngressApiClient::InventoryObjectLazy.new(
25
+ :inventory_collection_name => collection,
26
+ :reference => reference,
27
+ :ref => ref,
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -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
@@ -0,0 +1,14 @@
1
+ module TopologicalInventory
2
+ module Providers
3
+ module Common
4
+ module SaveInventory
5
+ class Exception
6
+ class Error < StandardError;
7
+ end
8
+ class EntityTooLarge < Error;
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,124 @@
1
+ require "topological_inventory/providers/common/save_inventory/exception"
2
+
3
+ module TopologicalInventory
4
+ module Providers
5
+ module Common
6
+ module SaveInventory
7
+ class Saver
8
+ # As defined in:
9
+ # https://github.com/zendesk/ruby-kafka/blob/02f7e2816e1130c5202764c275e36837f57ca4af/lib/kafka/protocol/message.rb#L11-L17
10
+ # There is at least 112 bytes that are added as a message header, so we need to keep room for that. Lets make
11
+ # it 200 bytes, just for sure.
12
+ KAFKA_RESERVED_HEADER_SIZE = 200
13
+
14
+ def initialize(client:, logger:, max_bytes: 1_000_000)
15
+ @client = client
16
+ @logger = logger
17
+ @max_bytes = max_bytes - KAFKA_RESERVED_HEADER_SIZE
18
+ end
19
+
20
+ attr_reader :client, :logger, :max_bytes
21
+
22
+ # @return [Integer] A total number of parts that the payload was divided into
23
+ def save(data)
24
+ inventory = data[:inventory].to_hash
25
+
26
+ inventory_json = JSON.generate(inventory)
27
+ if inventory_json.size < max_bytes
28
+ save_inventory(inventory_json)
29
+ return 1
30
+ else
31
+ # GC can clean this up
32
+ inventory_json = nil
33
+ return save_payload_in_batches(inventory)
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def save_payload_in_batches(inventory)
40
+ parts = 0
41
+ new_inventory = build_new_inventory(inventory)
42
+
43
+ inventory[:collections].each do |collection|
44
+ new_collection = build_new_collection(collection)
45
+
46
+ data = collection[:data].map { |x| JSON.generate(x) }
47
+ # Lets compute sizes of the each data item, plus 1 byte for comma
48
+ data_sizes = data.map { |x| x.size + 1 }
49
+
50
+ # Size of the current inventory and new_collection wrapper, plus 2 bytes for array signs
51
+ wrapper_size = JSON.generate(new_inventory).size + JSON.generate(new_collection).size + 2
52
+ total_size = wrapper_size
53
+ counter = 0
54
+ data_sizes.each do |data_size|
55
+ counter += 1
56
+ total_size += data_size
57
+
58
+ if total_size > max_bytes
59
+ # Remove the last data part, that caused going over the max limit
60
+ counter -= 1
61
+
62
+ # Add the entities to new collection, so the total size is below max
63
+ if counter > 0
64
+ new_collection[:data] = collection[:data].shift(counter)
65
+ new_inventory[:collections] << new_collection
66
+ end
67
+
68
+ # Save the current batch
69
+ serialize_and_save_inventory(new_inventory)
70
+ parts += 1
71
+
72
+ # Create new data containers for a new batch
73
+ new_inventory = build_new_inventory(inventory)
74
+ new_collection = build_new_collection(collection)
75
+ wrapper_size = JSON.generate(new_inventory).size + JSON.generate(new_collection).size + 2
76
+
77
+ # Start with the data part we've removed from the currently saved payload
78
+ total_size = wrapper_size + data_size
79
+ counter = 1
80
+ end
81
+ end
82
+
83
+ # Store the rest of the collection
84
+ new_collection[:data] = collection[:data].shift(counter)
85
+ new_inventory[:collections] << new_collection
86
+ end
87
+
88
+ # save the rest
89
+ serialize_and_save_inventory(new_inventory)
90
+ parts += 1
91
+
92
+ return parts
93
+ end
94
+
95
+ def save_inventory(inventory_json)
96
+ client.save_inventory_with_http_info(inventory_json)
97
+ end
98
+
99
+ def serialize_and_save_inventory(inventory)
100
+ payload = JSON.generate(inventory)
101
+ if payload.size > max_bytes
102
+ raise Exception::EntityTooLarge,
103
+ "Entity is bigger than total limit and can't be split: #{payload}"
104
+ end
105
+
106
+ # Save the current batch
107
+ save_inventory(payload)
108
+ end
109
+
110
+ def build_new_collection(collection)
111
+ {:name => collection[:name], :data => []}
112
+ end
113
+
114
+ def build_new_inventory(inventory)
115
+ new_inventory = inventory.clone
116
+ new_inventory[:refresh_state_part_uuid] = SecureRandom.uuid
117
+ new_inventory[:collections] = []
118
+ new_inventory
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,7 @@
1
+ module TopologicalInventory
2
+ module Providers
3
+ module Common
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "topological_inventory/providers/common/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "topological_inventory-providers-common"
8
+ spec.version = TopologicalInventory::Providers::Common::VERSION
9
+ spec.authors = ["Martin Slemr"]
10
+ spec.email = ["mslemr@redhat.com"]
11
+
12
+ spec.summary = %q{Common classes for topological-inventory collectors/operations}
13
+ spec.description = %q{Common classes for topological-inventory collectors/operations}
14
+ spec.homepage = "https://github.com/slemrmartin/topological_inventory-providers-common"
15
+ spec.license = "Apache-2.0"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_runtime_dependency 'config', '~> 1.7', '>= 1.7.2'
27
+ spec.add_runtime_dependency "activesupport", "~> 5.2.2"
28
+ spec.add_runtime_dependency "manageiq-loggers", "~> 0.4.0", ">= 0.4.2"
29
+ spec.add_runtime_dependency 'json', '~> 2.1', '>= 2.1.0'
30
+ spec.add_runtime_dependency "topological_inventory-api-client", "~> 2.0"
31
+
32
+ spec.add_development_dependency "bundler", "~> 2.0"
33
+ spec.add_development_dependency "rake", "~> 10.0"
34
+ spec.add_development_dependency "rspec", "~> 3.0"
35
+ end
metadata ADDED
@@ -0,0 +1,197 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: topological_inventory-providers-common
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Slemr
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: config
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.7.2
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.7'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.7.2
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 5.2.2
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 5.2.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: manageiq-loggers
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.4.0
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 0.4.2
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: 0.4.0
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 0.4.2
67
+ - !ruby/object:Gem::Dependency
68
+ name: json
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '2.1'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 2.1.0
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '2.1'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 2.1.0
87
+ - !ruby/object:Gem::Dependency
88
+ name: topological_inventory-api-client
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - "~>"
92
+ - !ruby/object:Gem::Version
93
+ version: '2.0'
94
+ type: :runtime
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - "~>"
99
+ - !ruby/object:Gem::Version
100
+ version: '2.0'
101
+ - !ruby/object:Gem::Dependency
102
+ name: bundler
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - "~>"
106
+ - !ruby/object:Gem::Version
107
+ version: '2.0'
108
+ type: :development
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - "~>"
113
+ - !ruby/object:Gem::Version
114
+ version: '2.0'
115
+ - !ruby/object:Gem::Dependency
116
+ name: rake
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '10.0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '10.0'
129
+ - !ruby/object:Gem::Dependency
130
+ name: rspec
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: '3.0'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - "~>"
141
+ - !ruby/object:Gem::Version
142
+ version: '3.0'
143
+ description: Common classes for topological-inventory collectors/operations
144
+ email:
145
+ - mslemr@redhat.com
146
+ executables: []
147
+ extensions: []
148
+ extra_rdoc_files: []
149
+ files:
150
+ - ".gitignore"
151
+ - ".rspec"
152
+ - ".travis.yml"
153
+ - Gemfile
154
+ - LICENSE.txt
155
+ - README.md
156
+ - Rakefile
157
+ - bin/console
158
+ - bin/setup
159
+ - lib/topological_inventory/providers/common.rb
160
+ - lib/topological_inventory/providers/common/collector.rb
161
+ - lib/topological_inventory/providers/common/collector/inventory_collection_storage.rb
162
+ - lib/topological_inventory/providers/common/collector/inventory_collection_wrapper.rb
163
+ - lib/topological_inventory/providers/common/collector/parser.rb
164
+ - lib/topological_inventory/providers/common/collectors_pool.rb
165
+ - lib/topological_inventory/providers/common/logging.rb
166
+ - lib/topological_inventory/providers/common/operations/endpoint_client.rb
167
+ - lib/topological_inventory/providers/common/operations/processor.rb
168
+ - lib/topological_inventory/providers/common/operations/sources_api_client.rb
169
+ - lib/topological_inventory/providers/common/operations/topology_api_client.rb
170
+ - lib/topological_inventory/providers/common/save_inventory/exception.rb
171
+ - lib/topological_inventory/providers/common/save_inventory/saver.rb
172
+ - lib/topological_inventory/providers/common/version.rb
173
+ - topological_inventory-providers-common.gemspec
174
+ homepage: https://github.com/slemrmartin/topological_inventory-providers-common
175
+ licenses:
176
+ - Apache-2.0
177
+ metadata: {}
178
+ post_install_message:
179
+ rdoc_options: []
180
+ require_paths:
181
+ - lib
182
+ required_ruby_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - ">="
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ requirements: []
193
+ rubygems_version: 3.1.2
194
+ signing_key:
195
+ specification_version: 4
196
+ summary: Common classes for topological-inventory collectors/operations
197
+ test_files: []