3scale_toolbox 0.4.0 → 0.5.0

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