3scale_toolbox 0.4.0 → 0.5.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,20 @@
1
+ require 'cri'
2
+ require '3scale_toolbox/base_command'
3
+ require '3scale_toolbox/commands/copy_command/copy_service'
4
+
5
+ module ThreeScaleToolbox
6
+ module Commands
7
+ module CopyCommand
8
+ extend ThreeScaleToolbox::Command
9
+ def self.command
10
+ Cri::Command.define do
11
+ name 'copy'
12
+ usage 'copy <command> [options]'
13
+ summary '3scale copy command'
14
+ description '3scale copy command.'
15
+ end
16
+ end
17
+ add_subcommand(CopyServiceSubcommand)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ require 'cri'
2
+ require '3scale_toolbox/base_command'
3
+
4
+ module ThreeScaleToolbox
5
+ module Commands
6
+ module HelpCommand
7
+ extend ThreeScaleToolbox::Command
8
+ def self.command
9
+ Cri::Command.new_basic_help
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,175 @@
1
+ require 'cri'
2
+ require 'uri'
3
+ require 'csv'
4
+ require '3scale/api'
5
+ require '3scale_toolbox/base_command'
6
+
7
+ module ThreeScaleToolbox
8
+ module Commands
9
+ module ImportCommand
10
+ module ImportCsvSubcommand
11
+ extend ThreeScaleToolbox::Command
12
+ def self.command
13
+ Cri::Command.define do
14
+ name 'csv'
15
+ usage 'csv [opts] -d <dst> -f <file>'
16
+ summary 'Import csv file'
17
+ description 'Create new services, metrics, methods and mapping rules from CSV formatted file'
18
+
19
+ required :d, :destination, '3scale target instance. Format: "http[s]://<provider_key>@3scale_url"'
20
+ required :f, 'file', 'CSV formatted file'
21
+
22
+ run do |opts, args, _|
23
+ ImportCsvSubcommand.run opts, args
24
+ end
25
+ end
26
+ end
27
+
28
+ def self.exit_with_message(message)
29
+ puts message
30
+ exit 1
31
+ end
32
+
33
+ def self.fetch_required_option(options, key)
34
+ options.fetch(key) { exit_with_message "error: Missing argument #{key}" }
35
+ end
36
+
37
+ def self.provider_key_from_url(url)
38
+ URI(url).user
39
+ end
40
+
41
+ def self.endpoint_from_url(url)
42
+ uri = URI(url)
43
+ uri.user = nil
44
+
45
+ uri.to_s
46
+ end
47
+
48
+ def self.auth_app_key_according_service(service)
49
+ case service['backend_version']
50
+ when '1'
51
+ 'user_key'
52
+ when '2'
53
+ 'app_id'
54
+ when 'oauth'
55
+ 'oauth'
56
+ end
57
+ end
58
+
59
+ def self.import_csv(destination, file_path, insecure)
60
+ endpoint = endpoint_from_url destination
61
+ provider_key = provider_key_from_url destination
62
+
63
+ client = ThreeScale::API.new(endpoint: endpoint,
64
+ provider_key: provider_key,
65
+ verify_ssl: !insecure
66
+ )
67
+ data = CSV.read file_path
68
+ headings = data.shift
69
+ services = {}
70
+ stats = { services: 0, metrics: 0, methods: 0 , mapping_rules: 0 }
71
+
72
+ # prepare services data
73
+ data.each do |row|
74
+ service_name = row[headings.find_index('service_name')]
75
+ item = {}
76
+
77
+ services[service_name] ||= {}
78
+ services[service_name][:items] ||= []
79
+
80
+ (headings - ['service_name']).each do |heading|
81
+ item[heading] = row[headings.find_index(heading)]
82
+ end
83
+
84
+ services[service_name][:items].push item
85
+ end
86
+
87
+ services.keys.each do |service_name|
88
+ # create service
89
+ service = client.create_service name: service_name
90
+
91
+ if service['errors'].nil?
92
+ stats[:services] += 1
93
+ puts "Service #{service_name} has been created."
94
+ else
95
+ abort "Service has not been saved. Errors: #{service['errors']}"
96
+ end
97
+
98
+ # find hits metric (default)
99
+ hits_metric = client.list_metrics(service['id']).find do |metric|
100
+ metric['system_name'] == 'hits'
101
+ end
102
+
103
+ services[service_name][:items].each do |item|
104
+
105
+ metric, method = {}
106
+
107
+ case item['type']
108
+ # create a metric
109
+ when 'metric'
110
+ metric = client.create_metric(service['id'], {
111
+ system_name: item['endpoint_system_name'],
112
+ friendly_name: item['endpoint_name'],
113
+ unit: 'unit'
114
+ })
115
+
116
+ if metric['errors'].nil?
117
+ stats[:metrics] += 1
118
+ puts "Metric #{item['endpoint_name']} has been created."
119
+ else
120
+ puts "Metric has not been saved. Errors: #{metric['errors']}"
121
+ end
122
+ # create a method
123
+ when 'method'
124
+ method = client.create_method(service['id'], hits_metric['id'], {
125
+ system_name: item['endpoint_system_name'],
126
+ friendly_name: item['endpoint_name'],
127
+ unit: 'unit'
128
+ })
129
+
130
+ if method['errors'].nil?
131
+ stats[:methods] += 1
132
+ puts "Method #{item['endpoint_name']} has been created."
133
+ else
134
+ puts "Method has not been saved. Errors: #{method['errors']}"
135
+ end
136
+ end
137
+
138
+ # create a mapping rule
139
+ if (metric_id = metric['id'] || method['id'])
140
+ mapping_rule = client.create_mapping_rule(service['id'], {
141
+ metric_id: metric_id,
142
+ pattern: item['endpoint_path'],
143
+ http_method: item['endpoint_http_method'],
144
+ metric_system_name: item['endpoint_system_name'],
145
+ auth_app_key: auth_app_key_according_service(service),
146
+ delta: 1
147
+ })
148
+
149
+ if mapping_rule['errors'].nil?
150
+ stats[:mapping_rules] += 1
151
+ puts "Mapping rule #{item['endpoint_system_name']} has been created."
152
+ else
153
+ puts "Mapping rule has not been saved. Errors: #{mapping_rule['errors']}"
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ puts "#{services.keys.count} services in CSV file"
160
+ puts "#{stats[:services]} services have been created"
161
+ puts "#{stats[:metrics]} metrics have been created"
162
+ puts "#{stats[:methods]} methods have beeen created"
163
+ puts "#{stats[:mapping_rules]} mapping rules have been created"
164
+ end
165
+
166
+ def self.run(opts, _)
167
+ destination = fetch_required_option(opts, :destination)
168
+ file_path = fetch_required_option(opts, :file)
169
+ insecure = opts[:insecure] || false
170
+ import_csv(destination, file_path, insecure)
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,20 @@
1
+ require 'cri'
2
+ require '3scale_toolbox/base_command'
3
+ require '3scale_toolbox/commands/import_command/import_csv'
4
+
5
+ module ThreeScaleToolbox
6
+ module Commands
7
+ module ImportCommand
8
+ extend ThreeScaleToolbox::Command
9
+ def self.command
10
+ Cri::Command.define do
11
+ name 'import'
12
+ usage 'import <command> [options]'
13
+ summary '3scale import command'
14
+ description '3scale import command.'
15
+ end
16
+ end
17
+ add_subcommand(ImportCsvSubcommand)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,247 @@
1
+ require 'cri'
2
+ require '3scale_toolbox/base_command'
3
+
4
+ module ThreeScaleToolbox
5
+ module Commands
6
+ module UpdateCommand
7
+ module UpdateServiceSubcommand
8
+ extend ThreeScaleToolbox::Command
9
+ def self.command
10
+ Cri::Command.define do
11
+ name 'service'
12
+ usage 'service [opts] -s <src> -d <dst> <src_service_id> <dst_service_id>'
13
+ summary 'Update service'
14
+ description 'Will update existing service, update proxy settings, metrics, methods, application plans and mapping rules.'
15
+
16
+ required :s, :source, '3scale source instance. Format: "http[s]://<provider_key>@3scale_url"'
17
+ required :d, :destination, '3scale target instance. Format: "http[s]://<provider_key>@3scale_url"'
18
+ flag :f, :force, 'Overwrites the mapping rules by deleting all rules from target service first'
19
+ flag :r, 'rules-only', 'Updates only the mapping rules'
20
+
21
+ run do |opts, args, _|
22
+ UpdateServiceSubcommand.run opts, args
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.exit_with_message(message)
28
+ puts message
29
+ exit 1
30
+ end
31
+
32
+ def self.fetch_required_option(options, key)
33
+ options.fetch(key) { exit_with_message "error: Missing argument #{key}" }
34
+ end
35
+
36
+ class ServiceUpdater
37
+ attr_reader :source_client, :target_client, :source_service_id, :target_service_id
38
+
39
+ def initialize (source, source_service_id, destination, target_service_id, insecure)
40
+ @source_client = ThreeScale::API.new(
41
+ endpoint: endpoint_from_url(source),
42
+ provider_key: provider_key_from_url(source),
43
+ verify_ssl: !insecure
44
+ )
45
+ @target_client = ThreeScale::API.new(
46
+ endpoint: endpoint_from_url(destination),
47
+ provider_key: provider_key_from_url(destination),
48
+ verify_ssl: !insecure
49
+ )
50
+ @source_service_id = source_service_id
51
+ @target_service_id = target_service_id
52
+ end
53
+
54
+ def compare_hashes(first, second, keys)
55
+ keys.map{ |key| first.fetch(key) } == keys.map{ |key| second.fetch(key) }
56
+ end
57
+
58
+ def provider_key_from_url(url)
59
+ URI(url).user
60
+ end
61
+
62
+ def endpoint_from_url(url)
63
+ uri = URI(url)
64
+ uri.user = nil
65
+
66
+ uri.to_s
67
+ end
68
+
69
+ def target_service_params(source)
70
+ source.select { |k, v| Commands.service_valid_params.include?(k) && v }
71
+ end
72
+
73
+ def source_metrics
74
+ @source_metrics ||= source_client.list_metrics(source_service_id)
75
+ end
76
+
77
+ def metrics_mapping
78
+ @metrics_mapping ||= target_client.list_metrics(target_service_id).map do |target|
79
+ metric = source_metrics.find{|metric| metric.fetch('system_name') == target.fetch('system_name') }
80
+ metric ||= {}
81
+
82
+ [metric['id'], target['id']]
83
+ end.to_h
84
+ end
85
+
86
+ def copy_service_settings
87
+ source_service = source_client.show_service(source_service_id)
88
+ puts "updating service settings for service id #{target_service_id}..."
89
+ target_update_response = target_client.update_service(target_service_id, target_service_params(source_service))
90
+ raise "Service has not been saved. Errors: #{target_update_response['errors']}" unless target_update_response['errors'].nil?
91
+ end
92
+
93
+ def copy_proxy_settings
94
+ puts "updating proxy configuration for service id #{target_service_id}..."
95
+ proxy = source_client.show_proxy(source_service_id)
96
+ target_client.update_proxy(target_service_id, proxy)
97
+ puts "updated proxy of #{target_service_id} to match the source #{source_service_id}"
98
+ end
99
+
100
+ def copy_metrics_and_methods
101
+ target_metrics = target_client.list_metrics(target_service_id)
102
+
103
+ source_hits = source_metrics.find{ |metric| metric['system_name'] == 'hits' } or raise 'missing hits metric'
104
+ target_hits = target_metrics.find{ |metric| metric['system_name'] == 'hits' } or raise 'missing hits metric'
105
+
106
+ source_methods = source_client.list_methods(source_service_id, source_hits['id'])
107
+ target_methods = target_client.list_methods(target_service_id, target_hits['id'])
108
+
109
+ puts "source service hits metric #{source_hits['id']} has #{source_methods.size} methods"
110
+ puts "target service hits metric #{target_hits['id']} has #{target_methods.size} methods"
111
+
112
+ missing_methods = source_methods.reject { |source_method| target_methods.find{|target_method| compare_hashes(source_method, target_method, ['system_name']) } }
113
+
114
+ puts "creating #{missing_methods.size} missing methods on target service..."
115
+ missing_methods.each do |method|
116
+ target = { friendly_name: method['friendly_name'], system_name: method['system_name'] }
117
+ target_client.create_method(target_service_id, target_hits['id'], target)
118
+ end
119
+
120
+ target_metrics = target_client.list_metrics(target_service_id)
121
+
122
+ puts "source service has #{source_metrics.size} metrics"
123
+ puts "target service has #{target_metrics.size} metrics"
124
+
125
+ missing_metrics = source_metrics.reject { |source_metric| target_metrics.find{|target_metric| compare_hashes(source_metric, target_metric, ['system_name']) } }
126
+
127
+ missing_metrics.map do |metric|
128
+ metric.delete('links')
129
+ target_client.create_metric(target_service_id, metric)
130
+ end
131
+
132
+ puts "created #{missing_metrics.size} metrics on the target service"
133
+ end
134
+
135
+ def copy_application_plans
136
+ source_plans = source_client.list_service_application_plans(source_service_id)
137
+ target_plans = target_client.list_service_application_plans(target_service_id)
138
+
139
+ puts "source service has #{source_plans.size} application plans"
140
+ puts "target service has #{target_plans.size} application plans"
141
+
142
+ missing_application_plans = source_plans.reject { |source_plan| target_plans.find{|target_plan| source_plan.fetch('system_name') == target_plan.fetch('system_name') } }
143
+
144
+ puts "creating #{missing_application_plans.size} missing application plans..."
145
+
146
+ missing_application_plans.each do |plan|
147
+ plan.delete('links')
148
+ plan.delete('default') # TODO: handle default plans
149
+
150
+ if plan.delete('custom') # TODO: what to do with custom plans?
151
+ puts "skipping custom plan #{plan}"
152
+ else
153
+ target_client.create_application_plan(target_service_id, plan)
154
+ end
155
+ end
156
+
157
+ puts "updating limits for application plans..."
158
+
159
+ application_plan_mapping = target_client.list_service_application_plans(target_service_id).map do |plan_target|
160
+ plan = source_plans.find{|plan| plan.fetch('system_name') == plan_target.fetch('system_name') }
161
+ plan ||= {}
162
+ [plan['id'], plan_target['id']]
163
+ end.to_h.reject { |key, value| !key }
164
+
165
+ application_plan_mapping.each do |source_id, target_id|
166
+ source_limits = source_client.list_application_plan_limits(source_id)
167
+ target_limits = target_client.list_application_plan_limits(target_id)
168
+
169
+ missing_limits = source_limits.reject { |limit| target_limits.find{|limit_target| limit.fetch('period') == limit_target.fetch('period') } }
170
+
171
+ puts "target application plan #{target_id} is missing #{missing_limits.size} from the source plan #{source_id}"
172
+
173
+ missing_limits.each do |limit|
174
+ limit.delete('links')
175
+ target_client.create_application_plan_limit(target_id, metrics_mapping.fetch(limit.fetch('metric_id')), limit)
176
+ end
177
+
178
+ end
179
+ end
180
+
181
+ def copy_mapping_rules force_mapping_rules
182
+ source_mapping_rules = source_client.list_mapping_rules(source_service_id)
183
+ target_mapping_rules = target_client.list_mapping_rules(target_service_id)
184
+
185
+ puts "the source service has #{source_mapping_rules.size} mapping rules"
186
+ puts "the target has #{target_mapping_rules.size} mapping rules"
187
+
188
+ if force_mapping_rules
189
+ puts "force mode was chosen, deleting existing mapping rules on target service..."
190
+ target_mapping_rules.each do |rule|
191
+ target_client.delete_mapping_rule(target_service_id, rule['id'])
192
+ end
193
+ missing_mapping_rules = source_mapping_rules
194
+ else
195
+ unique_target_mapping_rules = target_mapping_rules.dup
196
+
197
+ missing_mapping_rules = source_mapping_rules.reject do |mapping_rule|
198
+ matching_metric = unique_target_mapping_rules.find do |target|
199
+ compare_hashes(mapping_rule, target, %w(pattern http_method delta)) &&
200
+ metrics_mapping.fetch(mapping_rule.fetch('metric_id')) == target.fetch('metric_id')
201
+ end
202
+
203
+ unique_target_mapping_rules.delete(matching_metric)
204
+ end
205
+ end
206
+
207
+ puts "missing #{missing_mapping_rules.size} mapping rules"
208
+
209
+ missing_mapping_rules.each do |mapping_rule|
210
+ mapping_rule.delete('links')
211
+ mapping_rule['metric_id'] = metrics_mapping.fetch(mapping_rule.delete('metric_id'))
212
+ target_client.create_mapping_rule(target_service_id, mapping_rule)
213
+ end
214
+ puts "created #{missing_mapping_rules.size} mapping rules"
215
+ end
216
+
217
+ def update_service force_mapping_rules=false
218
+ copy_service_settings
219
+ copy_proxy_settings
220
+ copy_metrics_and_methods
221
+ copy_application_plans
222
+ copy_mapping_rules force_mapping_rules
223
+ end
224
+ end
225
+
226
+ def self.run(opts, args)
227
+ source = fetch_required_option(opts, :source)
228
+ destination = fetch_required_option(opts, :destination)
229
+ insecure = opts[:insecure] || false
230
+ exit_with_message 'error: missing source_service_id argument' if args.empty?
231
+ exit_with_message 'error: missing target_service_id argument' if args.size < 2
232
+ source_service_id = args[0]
233
+ target_service_id = args[1]
234
+ force_update = opts[:force] || false
235
+ rules_only = opts[:'rules-only'] || false
236
+ updater = ServiceUpdater.new(source, source_service_id, destination, target_service_id, insecure)
237
+
238
+ if rules_only
239
+ updater.copy_mapping_rules force_update
240
+ else
241
+ updater.update_service force_update
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end