hammer_cli_import 0.10.21
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 +7 -0
- data/LICENSE +674 -0
- data/README.md +115 -0
- data/channel_data_pretty.json +10316 -0
- data/config/import/config_macros.yml +16 -0
- data/config/import/interview_answers.yml +13 -0
- data/config/import/role_map.yml +10 -0
- data/config/import.yml +2 -0
- data/lib/hammer_cli_import/activationkey.rb +156 -0
- data/lib/hammer_cli_import/all.rb +253 -0
- data/lib/hammer_cli_import/asynctasksreactor.rb +187 -0
- data/lib/hammer_cli_import/autoload.rb +27 -0
- data/lib/hammer_cli_import/base.rb +585 -0
- data/lib/hammer_cli_import/configfile.rb +392 -0
- data/lib/hammer_cli_import/contenthost.rb +243 -0
- data/lib/hammer_cli_import/contentview.rb +198 -0
- data/lib/hammer_cli_import/csvhelper.rb +68 -0
- data/lib/hammer_cli_import/deltahash.rb +86 -0
- data/lib/hammer_cli_import/fixtime.rb +27 -0
- data/lib/hammer_cli_import/hostcollection.rb +52 -0
- data/lib/hammer_cli_import/import.rb +31 -0
- data/lib/hammer_cli_import/importtools.rb +351 -0
- data/lib/hammer_cli_import/organization.rb +110 -0
- data/lib/hammer_cli_import/persistentmap.rb +225 -0
- data/lib/hammer_cli_import/repository.rb +91 -0
- data/lib/hammer_cli_import/repositoryenable.rb +250 -0
- data/lib/hammer_cli_import/templatesnippet.rb +67 -0
- data/lib/hammer_cli_import/user.rb +155 -0
- data/lib/hammer_cli_import/version.rb +25 -0
- data/lib/hammer_cli_import.rb +53 -0
- metadata +117 -0
@@ -0,0 +1,585 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 Red Hat Inc.
|
3
|
+
#
|
4
|
+
# This file is part of hammer-cli-import.
|
5
|
+
#
|
6
|
+
# hammer-cli-import is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# hammer-cli-import is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with hammer-cli-import. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'csv'
|
21
|
+
require 'json'
|
22
|
+
require 'set'
|
23
|
+
|
24
|
+
require 'apipie-bindings'
|
25
|
+
require 'hammer_cli'
|
26
|
+
|
27
|
+
module HammerCLIImport
|
28
|
+
class MissingObjectError < RuntimeError
|
29
|
+
end
|
30
|
+
|
31
|
+
class ImportRecoveryError < RuntimeError
|
32
|
+
end
|
33
|
+
|
34
|
+
class BaseCommand < HammerCLI::Apipie::Command
|
35
|
+
extend PersistentMap::Extend
|
36
|
+
extend ImportTools::ImportLogging::Extend
|
37
|
+
extend AsyncTasksReactor::Extend
|
38
|
+
|
39
|
+
include PersistentMap::Include
|
40
|
+
include ImportTools::ImportLogging::Include
|
41
|
+
include ImportTools::Task::Include
|
42
|
+
include ImportTools::Exceptional::Include
|
43
|
+
include AsyncTasksReactor::Include
|
44
|
+
|
45
|
+
def initialize(*list)
|
46
|
+
super(*list)
|
47
|
+
|
48
|
+
# wrap API parameters into extra hash
|
49
|
+
@wrap_out = {
|
50
|
+
:users => :user,
|
51
|
+
:template_snippets => :config_template
|
52
|
+
}
|
53
|
+
# APIs return objects encapsulated in extra hash
|
54
|
+
#@wrap_in = {:organizations => 'organization'}
|
55
|
+
@wrap_in = {}
|
56
|
+
# entities that needs organization to be listed
|
57
|
+
@prerequisite = {
|
58
|
+
:activation_keys => :organizations,
|
59
|
+
:content_views => :organizations,
|
60
|
+
:content_view_versions => :organizations,
|
61
|
+
:host_collections => :organizations,
|
62
|
+
:products => :organizations,
|
63
|
+
:repositories => :organizations,
|
64
|
+
:repository_sets => :products,
|
65
|
+
:systems => :organizations
|
66
|
+
}
|
67
|
+
# cache imported objects (created/lookuped)
|
68
|
+
@cache = {}
|
69
|
+
class << @cache
|
70
|
+
def []=(key, val)
|
71
|
+
raise "@cache: #{val.inspect} is not a hash!" unless val.is_a? Hash
|
72
|
+
super
|
73
|
+
end
|
74
|
+
end
|
75
|
+
@summary = {}
|
76
|
+
# Initialize AsyncTaskReactor
|
77
|
+
atr_init
|
78
|
+
end
|
79
|
+
|
80
|
+
# What spacewalk-report do we expect to use for a given subcommand
|
81
|
+
class << self; attr_accessor :reportname end
|
82
|
+
|
83
|
+
option ['--csv-file'], 'FILE_NAME', 'CSV file with data to be imported', :required => true \
|
84
|
+
do |filename|
|
85
|
+
raise ArgumentError, "File #{filename} does not exist" unless File.exist? filename
|
86
|
+
missing = CSVHelper.csv_missing_columns filename, self.class.csv_columns
|
87
|
+
raise ArgumentError, "Bad CSV file #{filename}, missing columns: #{missing.inspect}" unless missing.empty?
|
88
|
+
filename
|
89
|
+
end
|
90
|
+
|
91
|
+
option ['--delete'], :flag, 'Delete entities from CSV file', :default => false
|
92
|
+
|
93
|
+
# TODO: Implement logic for verify
|
94
|
+
# option ['--verify'], :flag, 'Verify entities from CSV file'
|
95
|
+
|
96
|
+
option ['--recover'], 'RECOVER', 'Recover strategy, can be: rename (default), map, none', :default => :rename \
|
97
|
+
do |strategy|
|
98
|
+
raise ArgumentError, "Unknown '#{strategy}' strategy argument." \
|
99
|
+
unless [:rename, :map, :none].include? strategy.to_sym
|
100
|
+
strategy.to_sym
|
101
|
+
end
|
102
|
+
add_logging_options
|
103
|
+
|
104
|
+
class << self
|
105
|
+
# Which columns have to be be present in CSV.
|
106
|
+
def csv_columns(*list)
|
107
|
+
return @csv_columns if list.empty?
|
108
|
+
raise 'set more than once' if @csv_columns
|
109
|
+
@csv_columns = list
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class << self
|
114
|
+
# Initialize API. Needed to be called before any +api_call+ calls.
|
115
|
+
# If used in shell, it may be called multiple times
|
116
|
+
def api_init
|
117
|
+
@api = HammerCLIForeman.foreman_api_connection.api
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
|
121
|
+
# Call API. Ideally accessed via +api_call+ instance method.
|
122
|
+
# This is supposed to be the only way to access @api.
|
123
|
+
def api_call(resource, action, params = {}, headers = {}, dbg = false)
|
124
|
+
if resource == :organizations && action == :create
|
125
|
+
params[:organization] ||= {}
|
126
|
+
params[:organization][:name] = params[:name]
|
127
|
+
end
|
128
|
+
@api.resource(resource).call(action, params, headers)
|
129
|
+
rescue
|
130
|
+
error("Error on api.resource(#{resource.inspect}).call(#{action.inspect}, #{params.inspect}):") if dbg
|
131
|
+
raise
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Call API. Convenience method for calling +api_call+ class method.
|
136
|
+
def api_call(*list)
|
137
|
+
self.class.api_call(*list)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Call API on corresponding resource (defined by +map_target_entity+).
|
141
|
+
def mapped_api_call(entity_type, *list)
|
142
|
+
api_call(map_target_entity[entity_type], *list)
|
143
|
+
end
|
144
|
+
|
145
|
+
def data_dir
|
146
|
+
File.join(File.expand_path('~'), '.transition_data')
|
147
|
+
end
|
148
|
+
|
149
|
+
# This method is called to process single CSV line when
|
150
|
+
# importing.
|
151
|
+
def import_single_row(_row)
|
152
|
+
error 'Import not implemented.'
|
153
|
+
end
|
154
|
+
|
155
|
+
# This method is called to process single CSV line when
|
156
|
+
# deleting
|
157
|
+
def delete_single_row(_row)
|
158
|
+
error 'Delete not implemented.'
|
159
|
+
end
|
160
|
+
|
161
|
+
def get_cache(entity_type)
|
162
|
+
@cache[map_target_entity[entity_type]]
|
163
|
+
end
|
164
|
+
|
165
|
+
def load_cache
|
166
|
+
maps.collect { |map_sym| map_target_entity[map_sym] } .uniq.each do |entity_type|
|
167
|
+
list_server_entities entity_type
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def lookup_entity(entity_type, entity_id, online_lookup = false)
|
172
|
+
if (!get_cache(entity_type)[entity_id] || online_lookup)
|
173
|
+
get_cache(entity_type)[entity_id] = mapped_api_call(entity_type, :show, {'id' => entity_id})
|
174
|
+
else
|
175
|
+
debug "#{to_singular(entity_type).capitalize} #{entity_id} taken from cache."
|
176
|
+
end
|
177
|
+
return get_cache(entity_type)[entity_id]
|
178
|
+
end
|
179
|
+
|
180
|
+
def was_translated(entity_type, import_id)
|
181
|
+
return @pm[entity_type].to_hash.value?(import_id)
|
182
|
+
end
|
183
|
+
|
184
|
+
def _compare_hash(entity_hash, search_hash)
|
185
|
+
equal = nil
|
186
|
+
search_hash.each do |key, value|
|
187
|
+
if value.is_a? Hash
|
188
|
+
equal = _compare_hash(entity_hash[key], search_hash[key])
|
189
|
+
else
|
190
|
+
equal = entity_hash[key] == value
|
191
|
+
end
|
192
|
+
return false unless equal
|
193
|
+
end
|
194
|
+
return true
|
195
|
+
end
|
196
|
+
|
197
|
+
def lookup_entity_in_cache(entity_type, search_hash)
|
198
|
+
get_cache(entity_type).each do |_entity_id, entity_hash|
|
199
|
+
return entity_hash if _compare_hash(entity_hash, search_hash)
|
200
|
+
end
|
201
|
+
return nil
|
202
|
+
end
|
203
|
+
|
204
|
+
def lookup_entity_in_array(array, search_hash)
|
205
|
+
return nil if array.nil?
|
206
|
+
array.each do |entity_hash|
|
207
|
+
return entity_hash if _compare_hash(entity_hash, search_hash)
|
208
|
+
end
|
209
|
+
return nil
|
210
|
+
end
|
211
|
+
|
212
|
+
def last_in_cache?(entity_type, id)
|
213
|
+
return get_cache(entity_type).size == 1 && get_cache(entity_type).first[0] == id
|
214
|
+
end
|
215
|
+
|
216
|
+
# Method for use when writing messages to user.
|
217
|
+
# > to_singular(:contentveiws)
|
218
|
+
# "contentview"
|
219
|
+
# > to_singular(:repositories)
|
220
|
+
# "repository"
|
221
|
+
def to_singular(plural)
|
222
|
+
return plural.to_s.gsub(/_/, ' ').sub(/s$/, '').sub(/ie$/, 'y')
|
223
|
+
end
|
224
|
+
|
225
|
+
def split_multival(multival, convert_to_int = true, separator = ';')
|
226
|
+
arr = (multival || '').split(separator).delete_if { |v| v == 'None' }
|
227
|
+
arr.map!(&:to_i) if convert_to_int
|
228
|
+
return arr
|
229
|
+
end
|
230
|
+
|
231
|
+
# Method to call when you have created/deleted/found/mapped... something.
|
232
|
+
# Collected data used for summary reporting.
|
233
|
+
#
|
234
|
+
# :found is used for situation, when you want to create something,
|
235
|
+
# but you found out, it is already created.
|
236
|
+
def report_summary(verb, item)
|
237
|
+
raise "Not summary supported action: #{verb}" unless
|
238
|
+
[:created, :deleted, :found, :mapped, :skipped, :uploaded, :wrote, :failed].include? verb
|
239
|
+
@summary[verb] ||= {}
|
240
|
+
@summary[verb][item] = @summary[verb].fetch(item, 0) + 1
|
241
|
+
end
|
242
|
+
|
243
|
+
def print_summary
|
244
|
+
progress 'Summary'
|
245
|
+
@summary.each do |verb, what|
|
246
|
+
what.each do |entity, count|
|
247
|
+
noun = if count == 1
|
248
|
+
to_singular entity
|
249
|
+
else
|
250
|
+
entity
|
251
|
+
end
|
252
|
+
report = " #{verb.to_s.capitalize} #{count} #{noun}."
|
253
|
+
if verb == :found
|
254
|
+
info report
|
255
|
+
else
|
256
|
+
progress report
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
progress ' No action taken.' if (@summary.keys - [:found]).empty?
|
261
|
+
end
|
262
|
+
|
263
|
+
def get_translated_id(entity_type, entity_id)
|
264
|
+
if @pm[entity_type] && @pm[entity_type][entity_id]
|
265
|
+
return @pm[entity_type][entity_id]
|
266
|
+
end
|
267
|
+
raise MissingObjectError, 'Unable to import, first import ' + to_singular(entity_type) + \
|
268
|
+
' with id ' + entity_id.inspect
|
269
|
+
end
|
270
|
+
|
271
|
+
# this method returns a *first* found original_id
|
272
|
+
# (since we're able to map several organizations into one)
|
273
|
+
def get_original_id(entity_type, import_id)
|
274
|
+
if was_translated(entity_type, import_id)
|
275
|
+
# find original_ids
|
276
|
+
@pm[entity_type].to_hash.each do |key, value|
|
277
|
+
return key if value == import_id
|
278
|
+
end
|
279
|
+
else
|
280
|
+
debug "Unknown imported #{to_singular(entity_type)} [#{import_id}]."
|
281
|
+
end
|
282
|
+
return nil
|
283
|
+
end
|
284
|
+
|
285
|
+
def list_server_entities(entity_type, extra_hash = {}, use_cache = false)
|
286
|
+
if @prerequisite[entity_type]
|
287
|
+
list_server_entities(@prerequisite[entity_type]) unless @cache[@prerequisite[entity_type]]
|
288
|
+
end
|
289
|
+
|
290
|
+
@cache[entity_type] ||= {}
|
291
|
+
results = []
|
292
|
+
|
293
|
+
if !extra_hash.empty? || @prerequisite[entity_type].nil?
|
294
|
+
if use_cache
|
295
|
+
@list_cache ||= {}
|
296
|
+
if @list_cache[entity_type]
|
297
|
+
return @list_cache[entity_type][extra_hash] if @list_cache[entity_type][extra_hash]
|
298
|
+
else
|
299
|
+
@list_cache[entity_type] ||= {}
|
300
|
+
end
|
301
|
+
end
|
302
|
+
entities = api_call(entity_type, :index, {'per_page' => 999999}.merge(extra_hash))
|
303
|
+
results = entities['results']
|
304
|
+
@list_cache[entity_type][extra_hash] = results if use_cache
|
305
|
+
elsif @prerequisite[entity_type] == :organizations
|
306
|
+
# check only entities in imported orgs (not all of them)
|
307
|
+
@pm[:organizations].to_hash.values.each do |org_id|
|
308
|
+
entities = api_call(entity_type, :index, {'per_page' => 999999, 'organization_id' => org_id})
|
309
|
+
results += entities['results']
|
310
|
+
end
|
311
|
+
else
|
312
|
+
@cache[@prerequisite[entity_type]].each do |pre_id, _|
|
313
|
+
entities = api_call(
|
314
|
+
entity_type,
|
315
|
+
:index,
|
316
|
+
{
|
317
|
+
'per_page' => 999999,
|
318
|
+
@prerequisite[entity_type].to_s.sub(/s$/, '_id').to_sym => pre_id
|
319
|
+
})
|
320
|
+
results += entities['results']
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
results.each do |entity|
|
325
|
+
entity['id'] = entity['uuid'] if entity_type == :systems
|
326
|
+
@cache[entity_type][entity['id']] = entity
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def map_entity(entity_type, original_id, id)
|
331
|
+
if @pm[entity_type][original_id]
|
332
|
+
info "#{to_singular(entity_type).capitalize} [#{original_id}->#{@pm[entity_type][original_id]}] already mapped. " \
|
333
|
+
'Skipping.'
|
334
|
+
report_summary :found, entity_type
|
335
|
+
return
|
336
|
+
end
|
337
|
+
info "Mapping #{to_singular(entity_type)} [#{original_id}->#{id}]."
|
338
|
+
@pm[entity_type][original_id] = id
|
339
|
+
report_summary :mapped, entity_type
|
340
|
+
return get_cache(entity_type)[id]
|
341
|
+
end
|
342
|
+
|
343
|
+
def unmap_entity(entity_type, target_id)
|
344
|
+
deleted = @pm[entity_type].delete_value(target_id)
|
345
|
+
info " Unmapped #{to_singular(entity_type)} with id #{target_id}: #{deleted}x" if deleted > 1
|
346
|
+
end
|
347
|
+
|
348
|
+
def find_uniq(arr)
|
349
|
+
uniq = nil
|
350
|
+
uniq = arr[0] if arr[1].is_a?(Array) &&
|
351
|
+
(arr[1][0] =~ /has already been taken/ ||
|
352
|
+
arr[1][0] =~ /already exists/ ||
|
353
|
+
arr[1][0] =~ /must be unique within one organization/)
|
354
|
+
return uniq
|
355
|
+
end
|
356
|
+
|
357
|
+
def found_errors(err)
|
358
|
+
return err && err['errors'] && err['errors'].respond_to?(:each)
|
359
|
+
end
|
360
|
+
|
361
|
+
def recognizable_error(arr)
|
362
|
+
return arr.is_a?(Array) && arr.size >= 2
|
363
|
+
end
|
364
|
+
|
365
|
+
def process_error(err, entity_hash)
|
366
|
+
uniq = nil
|
367
|
+
err['errors'].each do |arr|
|
368
|
+
next unless recognizable_error(arr)
|
369
|
+
uniq = find_uniq(arr)
|
370
|
+
break if uniq && entity_hash.key?(uniq.to_sym)
|
371
|
+
uniq = nil # otherwise uniq is not usable
|
372
|
+
end
|
373
|
+
return uniq
|
374
|
+
end
|
375
|
+
|
376
|
+
# Create entity, with recovery strategy.
|
377
|
+
#
|
378
|
+
# * +:map+ - Use existing entity
|
379
|
+
# * +:rename+ - Change name
|
380
|
+
# * +nil+ - Fail
|
381
|
+
def create_entity(entity_type, entity_hash, original_id, recover = nil, retries = 2)
|
382
|
+
raise ImportRecoveryError, "Creation of #{entity_type} not recovered by " \
|
383
|
+
"'#{recover || option_recover.to_sym}' strategy" if retries < 0
|
384
|
+
uniq = nil
|
385
|
+
begin
|
386
|
+
return _create_entity(entity_type, entity_hash, original_id)
|
387
|
+
rescue RestClient::UnprocessableEntity => ue
|
388
|
+
error " Creation of #{to_singular(entity_type)} failed."
|
389
|
+
uniq = nil
|
390
|
+
err = JSON.parse(ue.response)
|
391
|
+
err = err['error'] if err.key?('error')
|
392
|
+
if found_errors(err)
|
393
|
+
uniq = process_error(err, entity_hash)
|
394
|
+
end
|
395
|
+
raise ue unless uniq
|
396
|
+
end
|
397
|
+
|
398
|
+
uniq = uniq.to_sym
|
399
|
+
|
400
|
+
case recover || option_recover.to_sym
|
401
|
+
when :rename
|
402
|
+
entity_hash[uniq] = original_id.to_s + '-' + entity_hash[uniq]
|
403
|
+
info " Recovering by renaming to: \"#{uniq}\"=\"#{entity_hash[uniq]}\""
|
404
|
+
return create_entity(entity_type, entity_hash, original_id, recover, retries - 1)
|
405
|
+
when :map
|
406
|
+
entity = lookup_entity_in_cache(entity_type, {uniq.to_s => entity_hash[uniq]})
|
407
|
+
if entity
|
408
|
+
info " Recovering by remapping to: #{entity['id']}"
|
409
|
+
return map_entity(entity_type, original_id, entity['id'])
|
410
|
+
else
|
411
|
+
warn "Creation of #{entity_type} not recovered by \'#{recover}\' strategy."
|
412
|
+
raise ImportRecoveryError, "Creation of #{entity_type} not recovered by \'#{recover}\' strategy."
|
413
|
+
end
|
414
|
+
else
|
415
|
+
fatal 'No recover strategy.'
|
416
|
+
raise ue
|
417
|
+
end
|
418
|
+
nil
|
419
|
+
end
|
420
|
+
|
421
|
+
# Use +create_entity+ instead.
|
422
|
+
def _create_entity(entity_type, entity_hash, original_id)
|
423
|
+
type = to_singular(entity_type)
|
424
|
+
if @pm[entity_type][original_id]
|
425
|
+
info type.capitalize + ' [' + original_id.to_s + '->' + @pm[entity_type][original_id].to_s + '] already imported.'
|
426
|
+
report_summary :found, entity_type
|
427
|
+
return get_cache(entity_type)[@pm[entity_type][original_id]]
|
428
|
+
else
|
429
|
+
info 'Creating new ' + type + ': ' + entity_hash.values_at(:name, :label, :login).compact[0]
|
430
|
+
entity_hash = {@wrap_out[entity_type] => entity_hash} if @wrap_out[entity_type]
|
431
|
+
debug "entity_hash: #{entity_hash.inspect}"
|
432
|
+
entity = mapped_api_call(entity_type, :create, entity_hash)
|
433
|
+
debug "created entity: #{entity.inspect}"
|
434
|
+
entity = entity[@wrap_in[entity_type]] if @wrap_in[entity_type]
|
435
|
+
# workaround for Bug
|
436
|
+
entity['id'] = entity['uuid'] if entity_type == :systems
|
437
|
+
@pm[entity_type][original_id] = entity['id']
|
438
|
+
get_cache(entity_type)[entity['id']] = entity
|
439
|
+
debug "@pm[#{entity_type}]: #{@pm[entity_type].inspect}"
|
440
|
+
report_summary :created, entity_type
|
441
|
+
return entity
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def update_entity(entity_type, id, entity_hash)
|
446
|
+
info "Updating #{to_singular(entity_type)} with id: #{id}"
|
447
|
+
mapped_api_call(entity_type, :update, {:id => id}.merge!(entity_hash))
|
448
|
+
end
|
449
|
+
|
450
|
+
# Delete entity by original (Sat5) id
|
451
|
+
def delete_entity(entity_type, original_id)
|
452
|
+
type = to_singular(entity_type)
|
453
|
+
unless @pm[entity_type][original_id]
|
454
|
+
error 'Unknown ' + type + ' to delete [' + original_id.to_s + '].'
|
455
|
+
return nil
|
456
|
+
end
|
457
|
+
info 'Deleting imported ' + type + ' [' + original_id.to_s + '->' + @pm[entity_type][original_id].to_s + '].'
|
458
|
+
begin
|
459
|
+
mapped_api_call(entity_type, :destroy, {:id => @pm[entity_type][original_id]})
|
460
|
+
# delete from cache
|
461
|
+
get_cache(entity_type).delete(@pm[entity_type][original_id])
|
462
|
+
# delete from pm
|
463
|
+
unmap_entity(entity_type, @pm[entity_type][original_id])
|
464
|
+
report_summary :deleted, entity_type
|
465
|
+
rescue => e
|
466
|
+
warn "Delete of #{to_singular(entity_type)} [#{original_id}] failed with #{e.class}: #{e.message}"
|
467
|
+
report_summary :failed, entity_type
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
# Delete entity by target (Sat6) id
|
472
|
+
def delete_entity_by_import_id(entity_type, import_id, delete_key = 'id')
|
473
|
+
type = to_singular(entity_type)
|
474
|
+
original_id = get_original_id(entity_type, import_id)
|
475
|
+
if original_id.nil?
|
476
|
+
error 'Unknown imported ' + type + ' to delete [' + import_id.to_s + '].'
|
477
|
+
return nil
|
478
|
+
end
|
479
|
+
info "Deleting imported #{type} [#{original_id}->#{@pm[entity_type][original_id]}]."
|
480
|
+
if delete_key == 'id'
|
481
|
+
delete_id = import_id
|
482
|
+
else
|
483
|
+
delete_id = get_cache(entity_type)[import_id][delete_key]
|
484
|
+
end
|
485
|
+
begin
|
486
|
+
mapped_api_call(entity_type, :destroy, {:id => delete_id})
|
487
|
+
# delete from cache
|
488
|
+
get_cache(entity_type).delete(import_id)
|
489
|
+
# delete from pm
|
490
|
+
@pm[entity_type].delete original_id
|
491
|
+
report_summary :deleted, entity_type
|
492
|
+
rescue => e
|
493
|
+
warn "Delete of #{to_singular(entity_type)} [#{delete_id}] failed with #{e.class}: #{e.message}"
|
494
|
+
report_summary :failed, entity_type
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
# Wait for asynchronous task.
|
499
|
+
#
|
500
|
+
# * +uuid+ - UUID of async task.
|
501
|
+
# * +start_wait+ - Seconds to wait before first check.
|
502
|
+
# * +delta_wait+ - How much longer will every next wait be (unless +max_wait+ is reached).
|
503
|
+
# * +max_wait+ - Maximum time to wait between two checks.
|
504
|
+
def wait_for_task(uuid, start_wait = 0, delta_wait = 1, max_wait = 10)
|
505
|
+
wait_time = start_wait
|
506
|
+
if option_quiet?
|
507
|
+
info "Waiting for the task [#{uuid}] "
|
508
|
+
else
|
509
|
+
print "Waiting for the task [#{uuid}] "
|
510
|
+
end
|
511
|
+
|
512
|
+
loop do
|
513
|
+
sleep wait_time
|
514
|
+
wait_time = [wait_time + delta_wait, max_wait].min
|
515
|
+
print '.' unless option_quiet?
|
516
|
+
STDOUT.flush unless option_quiet?
|
517
|
+
task = api_call(:foreman_tasks, :show, {:id => uuid})
|
518
|
+
next unless task['state'] == 'stopped'
|
519
|
+
print "\n" unless option_quiet?
|
520
|
+
return task['return'] == 'success'
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def cvs_iterate(filename, action)
|
525
|
+
CSVHelper.csv_each filename, self.class.csv_columns do |data|
|
526
|
+
handle_missing_and_supress "processing CSV line:\n#{data.inspect}" do
|
527
|
+
action.call(data)
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
def import(filename)
|
533
|
+
cvs_iterate(filename, (method :import_single_row))
|
534
|
+
end
|
535
|
+
|
536
|
+
def post_import(_csv_file)
|
537
|
+
# empty by default
|
538
|
+
end
|
539
|
+
|
540
|
+
def post_delete(_csv_file)
|
541
|
+
# empty by default
|
542
|
+
end
|
543
|
+
|
544
|
+
def delete(filename)
|
545
|
+
cvs_iterate(filename, (method :delete_single_row))
|
546
|
+
end
|
547
|
+
|
548
|
+
def execute
|
549
|
+
# Get set up to do logging as soon as reasonably possible
|
550
|
+
setup_logging
|
551
|
+
# create a storage directory if not exists yet
|
552
|
+
Dir.mkdir data_dir unless File.directory? data_dir
|
553
|
+
|
554
|
+
# initialize apipie binding
|
555
|
+
self.class.api_init
|
556
|
+
load_persistent_maps
|
557
|
+
load_cache
|
558
|
+
prune_persistent_maps @cache
|
559
|
+
# TODO: This big ugly thing might need some cleanup
|
560
|
+
begin
|
561
|
+
if option_delete?
|
562
|
+
info "Deleting from #{option_csv_file}"
|
563
|
+
delete option_csv_file
|
564
|
+
handle_missing_and_supress 'post_delete' do
|
565
|
+
post_delete option_csv_file
|
566
|
+
end
|
567
|
+
else
|
568
|
+
info "Importing from #{option_csv_file}"
|
569
|
+
import option_csv_file
|
570
|
+
handle_missing_and_supress 'post_import' do
|
571
|
+
post_import option_csv_file
|
572
|
+
end
|
573
|
+
end
|
574
|
+
atr_exit
|
575
|
+
rescue StandardError, SystemExit, Interrupt => e
|
576
|
+
error "Exiting: #{e}"
|
577
|
+
logtrace e
|
578
|
+
end
|
579
|
+
save_persistent_maps
|
580
|
+
print_summary
|
581
|
+
HammerCLI::EX_OK
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=ruby
|