topological_inventory-providers-common 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []