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.
- 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
|