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.
- checksums.yaml +4 -4
- data/README.md +155 -5
- data/exe/3scale +3 -9
- data/lib/3scale_toolbox/base_command.rb +28 -0
- data/lib/3scale_toolbox/cli.rb +16 -72
- data/lib/3scale_toolbox/commands/3scale_command.rb +28 -0
- data/lib/3scale_toolbox/commands/copy_command/copy_service.rb +218 -0
- data/lib/3scale_toolbox/commands/copy_command.rb +20 -0
- data/lib/3scale_toolbox/commands/help_command.rb +13 -0
- data/lib/3scale_toolbox/commands/import_command/import_csv.rb +175 -0
- data/lib/3scale_toolbox/commands/import_command.rb +20 -0
- data/lib/3scale_toolbox/commands/update_command/update_service.rb +247 -0
- data/lib/3scale_toolbox/commands/update_command.rb +20 -0
- data/lib/3scale_toolbox/commands.rb +24 -0
- data/lib/3scale_toolbox/version.rb +1 -1
- data/lib/3scale_toolbox.rb +7 -3
- metadata +49 -17
- data/exe/3scale-copy +0 -222
- data/exe/3scale-help +0 -9
- data/exe/3scale-import +0 -178
- data/exe/3scale-update +0 -270
@@ -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,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
|