envoi-mam-agent 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +8 -1
- data/docs/restore.md +43 -0
- data/envoi-mam-agent.gemspec +11 -4
- data/lib/cantemo/portal/agent/version.rb +7 -0
- data/lib/cantemo/portal/api/client.rb +82 -0
- data/lib/envoi/aspera/watch_service/client.rb +397 -0
- data/lib/envoi/aspera/watch_service/watch_folder.rb +322 -0
- data/lib/envoi/mam/agent.rb +34 -14
- data/lib/envoi/mam/agent/cli.rb +3 -3
- data/lib/envoi/mam/agent/cli/commands.rb +26 -16
- data/lib/envoi/mam/agent/cli/commands/iconik.rb +1 -1
- data/lib/envoi/mam/agent/cli/commands/mediasilo.rb +1 -1
- data/lib/envoi/mam/agent/cli/commands/restore.rb +52 -0
- data/lib/envoi/mam/agent/cli/commands/vidispine.rb +1 -1
- data/lib/envoi/mam/agent/cli/commands/wiredrive.rb +1 -1
- data/lib/envoi/mam/agent/transfer_client/aspera.rb +140 -18
- data/lib/envoi/mam/agent/transfer_client/s3.rb +13 -5
- data/lib/envoi/mam/agent/version.rb +1 -1
- data/lib/envoi/mam/cantemo/agent.rb +353 -0
- data/lib/envoi/mam/vidispine/agent.rb +7 -2
- data/lib/envoi/restore/agent.rb +73 -0
- data/lib/envoi/restore/glacier-restore-event-handler.rb +94 -0
- data/lib/envoi/restore/sqs-queue-worker.rb +68 -0
- metadata +50 -14
- data/.rbenv-gemsets +0 -3
- data/.ruby-version +0 -1
@@ -0,0 +1,322 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
require 'envoi/aspera/watch_service/client'
|
4
|
+
|
5
|
+
module Envoi
|
6
|
+
module Aspera
|
7
|
+
module WatchService
|
8
|
+
|
9
|
+
class WatchFolder
|
10
|
+
class State
|
11
|
+
|
12
|
+
attr_accessor :subscription
|
13
|
+
attr_accessor :known_path_map
|
14
|
+
attr_accessor :details
|
15
|
+
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@known_path_map = {}
|
19
|
+
@details = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
class Subscription
|
25
|
+
|
26
|
+
class Snapshot
|
27
|
+
|
28
|
+
class Entry < Hash
|
29
|
+
|
30
|
+
def initialize(data)
|
31
|
+
merge! data
|
32
|
+
end
|
33
|
+
|
34
|
+
def path;
|
35
|
+
self[:path]
|
36
|
+
end
|
37
|
+
|
38
|
+
def stat;
|
39
|
+
self[:stat] ||= ((json = self[:stat_as_json]) ? JSON.parse(json) : nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
def stat_as_json;
|
43
|
+
self[:stat_as_json]
|
44
|
+
end
|
45
|
+
|
46
|
+
if MatchData.method_defined?(:named_captures)
|
47
|
+
def self.new_from_match_data(match_data)
|
48
|
+
new(match_data.named_captures)
|
49
|
+
end
|
50
|
+
else
|
51
|
+
def self.new_from_match_data(match_data)
|
52
|
+
new(Hash[match_data.names.map(&:to_sym).zip(match_data.captures)])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Entry
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_accessor :subscription, :version, :entries
|
60
|
+
|
61
|
+
def initialize(subscription, version)
|
62
|
+
@subscription = subscription
|
63
|
+
@version = version
|
64
|
+
@entries = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def client
|
68
|
+
subscription.client
|
69
|
+
end
|
70
|
+
|
71
|
+
def logger
|
72
|
+
client.logger
|
73
|
+
end
|
74
|
+
|
75
|
+
def entries
|
76
|
+
@entries ||= client.subscription_snapshot_entries_get(subscription_id: subscription['identifier'],
|
77
|
+
snapshot_version: version)
|
78
|
+
end
|
79
|
+
|
80
|
+
def differential(from = nil)
|
81
|
+
(@differentials ||= {})[from] ||= differential_no_cache(from: from)
|
82
|
+
end
|
83
|
+
|
84
|
+
def differential_no_cache(args = {})
|
85
|
+
args_out = args.dup
|
86
|
+
args_out[:subscription_id] ||= subscription['identifier']
|
87
|
+
args_out[:to] ||= version
|
88
|
+
client.subscription_snapshot_differential(args_out)
|
89
|
+
end
|
90
|
+
|
91
|
+
def entries_by_path
|
92
|
+
@entries_by_hash ||= Hash[entries.map { |e| [e[:path], e] }]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Snapshot
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_accessor :attributes, :client, :definition, :snapshots
|
99
|
+
|
100
|
+
def initialize(data, client, definition = nil, snapshots = { })
|
101
|
+
@attributes = data
|
102
|
+
@client = client
|
103
|
+
@definition = definition
|
104
|
+
@snapshots = {}
|
105
|
+
end
|
106
|
+
|
107
|
+
def logger
|
108
|
+
client.logger
|
109
|
+
end
|
110
|
+
|
111
|
+
def [](key)
|
112
|
+
attributes[key]
|
113
|
+
end
|
114
|
+
|
115
|
+
def identifier
|
116
|
+
attributes['identifier']
|
117
|
+
end
|
118
|
+
|
119
|
+
alias id identifier
|
120
|
+
|
121
|
+
def snapshot_create(args = {})
|
122
|
+
args_out = args.dup
|
123
|
+
args_out[:subscription_id] = attributes['identifier']
|
124
|
+
snapshot_id = client.subscription_snapshot_create(args_out)
|
125
|
+
snapshots[snapshot_id] ||= Snapshot.new(self, snapshot_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
def resubscribe
|
129
|
+
client.subscription_resubscribe(subscription_id: id)
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
def self.get_or_create(client, definition, snapshots = { })
|
134
|
+
watch_folder_path = definition['path']
|
135
|
+
scan_period = definition['scan_period']
|
136
|
+
expire_in = definition['expire_in']
|
137
|
+
|
138
|
+
subscriptions = nil
|
139
|
+
subscriptions_for_path = client.subscription_find_for_path(path: watch_folder_path, subscriptions: subscriptions)
|
140
|
+
subscriptions_for_path.delete_if { |s| s['scan_period']['sec'] != scan_period } if scan_period
|
141
|
+
|
142
|
+
if subscriptions_for_path.empty?
|
143
|
+
args_out = { watch_folder_path: watch_folder_path }
|
144
|
+
args_out[:scan_period] = scan_period if scan_period
|
145
|
+
args_out[:expire_in] = expire_in if expire_in
|
146
|
+
# Create Subscription
|
147
|
+
subscription = client.subscription_create(args_out)
|
148
|
+
else
|
149
|
+
subscription = subscriptions_for_path.first
|
150
|
+
end
|
151
|
+
|
152
|
+
new(subscription, client, definition, snapshots)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Subscription
|
156
|
+
end
|
157
|
+
|
158
|
+
attr_accessor :definition, :state, :client, :watcher,
|
159
|
+
:poll_interval, :previous_poll_time, :last_poll_time
|
160
|
+
|
161
|
+
def initialize(definition, state = nil, client = nil)
|
162
|
+
@definition = definition
|
163
|
+
@state = state || State.new
|
164
|
+
@client = @watcher = client || Client.new
|
165
|
+
@poll_interval = definition.fetch('poll_interval', 15)
|
166
|
+
process_definition(definition)
|
167
|
+
end
|
168
|
+
|
169
|
+
def logger
|
170
|
+
client.logger
|
171
|
+
end
|
172
|
+
|
173
|
+
def path
|
174
|
+
definition['path']
|
175
|
+
end
|
176
|
+
|
177
|
+
def process_definition(watch_folder_def)
|
178
|
+
state.subscription = subscription_get_or_create(watch_folder_def)
|
179
|
+
end
|
180
|
+
|
181
|
+
def subscription_get_or_create(watch_folder_def)
|
182
|
+
Subscription.get_or_create(client, watch_folder_def)
|
183
|
+
end
|
184
|
+
|
185
|
+
def process_stable_entry(entry)
|
186
|
+
logger.debug { "Stable Entry Detected: #{entry.stat}" }
|
187
|
+
end
|
188
|
+
|
189
|
+
def events(from, to)
|
190
|
+
state.subscription.client.subscription_snapshot_differential(subscription_id: state.subscription['identifier'],
|
191
|
+
from: from,
|
192
|
+
to: to)
|
193
|
+
end
|
194
|
+
|
195
|
+
def poll
|
196
|
+
subscription = state.subscription
|
197
|
+
subscription.resubscribe
|
198
|
+
previous_snapshot_version = subscription.snapshots.keys.last
|
199
|
+
|
200
|
+
# Delete other snapshots
|
201
|
+
subscription.snapshots.delete_if { |p, e| p != previous_snapshot_version } if previous_snapshot_version
|
202
|
+
|
203
|
+
snapshot = subscription.snapshot_create
|
204
|
+
@previous_poll_time = @last_poll_time
|
205
|
+
@last_poll_time = Time.now
|
206
|
+
|
207
|
+
no_change = snapshot.version == previous_snapshot_version
|
208
|
+
if !no_change
|
209
|
+
events = snapshot.differential(previous_snapshot_version)
|
210
|
+
logger.debug { "Events: #{events}" }
|
211
|
+
end
|
212
|
+
|
213
|
+
_known_path_map = state.known_path_map.dup
|
214
|
+
|
215
|
+
logger.debug { "Subscription ID: #{subscription['identifier']}" }
|
216
|
+
logger.debug { "Previous Snapshot: #{previous_snapshot_version}" }
|
217
|
+
logger.debug { "Current Snapshot: #{snapshot.version}" }
|
218
|
+
logger.debug { "Known Paths: #{_known_path_map.keys}" }
|
219
|
+
|
220
|
+
current_path_map = snapshot.entries_by_path
|
221
|
+
current_paths = current_path_map.keys
|
222
|
+
|
223
|
+
new_path_map = current_path_map.dup
|
224
|
+
|
225
|
+
deleted_path_map = {}
|
226
|
+
stable_path_map = {}
|
227
|
+
unstable_path_map = {}
|
228
|
+
_known_path_map.delete_if do |p, e|
|
229
|
+
deleted = !no_change && !current_paths.include?(p)
|
230
|
+
if deleted
|
231
|
+
logger.debug { "DELETED '#{p}'" }
|
232
|
+
deleted_path_map[p] = e
|
233
|
+
else
|
234
|
+
new_path_map.delete(p) # The file is not new, so remove it from the new list
|
235
|
+
|
236
|
+
new_entry = current_path_map[p]
|
237
|
+
previous_stat = e.stat.values_at('mtime', 'size')
|
238
|
+
new_stat = new_entry.stat.values_at('mtime', 'size')
|
239
|
+
|
240
|
+
if no_change || new_stat === previous_stat
|
241
|
+
logger.debug { "UNCHANGED '#{p}' '#{new_stat}' == '#{previous_stat}'" }
|
242
|
+
stable_poll_count = e[:stable_poll_count] || 0
|
243
|
+
stable_poll_count += 1
|
244
|
+
e[:stable_poll_count] = stable_poll_count
|
245
|
+
stable_path_map[p] = e
|
246
|
+
else
|
247
|
+
logger.debug { "MODIFIED '#{p}' '#{new_stat}' != '#{previous_stat}'" }
|
248
|
+
e[:stable_poll_count] = 0
|
249
|
+
unstable_path_map[p] = new_entry
|
250
|
+
end
|
251
|
+
end
|
252
|
+
deleted
|
253
|
+
end
|
254
|
+
|
255
|
+
new_path_map.each do |p, e|
|
256
|
+
logger.debug { "CREATED '#{p}' #{e.stat.values_at('mtime', 'size')}" }
|
257
|
+
e[:stable_poll_count] = 0
|
258
|
+
unstable_path_map[p] = e.dup
|
259
|
+
end
|
260
|
+
|
261
|
+
_known_path_map.merge! stable_path_map
|
262
|
+
_known_path_map.merge! unstable_path_map unless no_change
|
263
|
+
state.known_path_map = _known_path_map
|
264
|
+
state.details = {
|
265
|
+
previous_snapshot_version: previous_snapshot_version,
|
266
|
+
maps: {
|
267
|
+
new: new_path_map,
|
268
|
+
deleted: deleted_path_map,
|
269
|
+
stable: stable_path_map,
|
270
|
+
unstable: unstable_path_map
|
271
|
+
}
|
272
|
+
}
|
273
|
+
|
274
|
+
logger.debug { "Known Paths: #{_known_path_map.keys}" }
|
275
|
+
logger.debug { "New Paths: #{new_path_map.map { |p, e| "#{p} #{e.stat}" }}" }
|
276
|
+
|
277
|
+
logger.debug { "Deleted Paths: #{deleted_path_map.keys}" }
|
278
|
+
logger.debug { "Unstable Paths: #{unstable_path_map.keys}" }
|
279
|
+
logger.debug { "Stable Paths: #{stable_path_map.map { |p, e| [p, e[:stable_poll_count]] }}" }
|
280
|
+
|
281
|
+
yield self if block_given?
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.process_watch_folder_def(watch_folder_def)
|
286
|
+
new(watch_folder_def)
|
287
|
+
end
|
288
|
+
|
289
|
+
def self.process_watch_folder_defs(watch_folder_defs)
|
290
|
+
|
291
|
+
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.process_watch_folder(watch_folder, &block)
|
295
|
+
watch_folder.poll(&block) if (Time.now - watch_folder.last_poll_time) >= watch_folder.poll_interval
|
296
|
+
end
|
297
|
+
|
298
|
+
def self.process_watch_folders(watch_folders, &block)
|
299
|
+
watch_folders.each { |watch_folder| process_watch_folder(watch_folder, &block) }
|
300
|
+
end
|
301
|
+
|
302
|
+
def self.run_once(watch_folders, &block)
|
303
|
+
process_watch_folders(watch_folders, &block)
|
304
|
+
end
|
305
|
+
|
306
|
+
def self.run(watch_folders, poll_interval = 15, &block)
|
307
|
+
puts 'Starting...'
|
308
|
+
loop do
|
309
|
+
begin
|
310
|
+
run_once(watch_folders, &block)
|
311
|
+
sleep 1
|
312
|
+
rescue SystemExit, Interrupt
|
313
|
+
break
|
314
|
+
end
|
315
|
+
end
|
316
|
+
puts 'Exiting...'
|
317
|
+
end
|
318
|
+
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
data/lib/envoi/mam/agent.rb
CHANGED
@@ -12,7 +12,7 @@ module Envoi
|
|
12
12
|
|
13
13
|
def initialize(args = { })
|
14
14
|
@initial_args = args.clone
|
15
|
-
@config = args[:config]
|
15
|
+
@config = args[:config] || { }
|
16
16
|
@notifiers = args[:notifiers] || [ ]
|
17
17
|
|
18
18
|
@dry_run = args.fetch(:dry_run, false)
|
@@ -36,7 +36,13 @@ module Envoi
|
|
36
36
|
return if @notifiers.empty?
|
37
37
|
args[:level] ||= :info
|
38
38
|
args[:message] ||= message
|
39
|
-
@notifiers.each
|
39
|
+
@notifiers.each do |notifier|
|
40
|
+
begin
|
41
|
+
notifier.notify(args) rescue nil
|
42
|
+
rescue => e
|
43
|
+
logger.warn { "Notification Failed: #{e.message} "}
|
44
|
+
end
|
45
|
+
end
|
40
46
|
end
|
41
47
|
|
42
48
|
# @param [Hash] args {}
|
@@ -48,13 +54,17 @@ module Envoi
|
|
48
54
|
end
|
49
55
|
end
|
50
56
|
|
57
|
+
# @param [String] command
|
58
|
+
# @param [Boolean] dry_run
|
59
|
+
# @return [Hash]
|
51
60
|
def shell_execute(command, dry_run = @dry_run)
|
52
61
|
if dry_run
|
53
62
|
logger.debug { "Skipping Execution of Command: '#{command}' " }
|
54
|
-
return
|
63
|
+
return { }
|
55
64
|
end
|
56
65
|
logger.debug { "Executing Command: '#{command}'" }
|
57
66
|
|
67
|
+
success = false
|
58
68
|
Open3.popen3(command) do |stdin, stdout, stderr, thread|
|
59
69
|
# stdin.sync = true
|
60
70
|
# stdout.sync = true
|
@@ -65,36 +75,46 @@ module Envoi
|
|
65
75
|
output << stdout.read #rescue nil
|
66
76
|
output << stderr.read # rescue nil
|
67
77
|
unless output.empty?
|
68
|
-
|
78
|
+
logger.debug { output }
|
69
79
|
output.clear
|
70
80
|
end
|
71
81
|
end
|
82
|
+
|
83
|
+
success = thread.value == 0 ? true : false
|
72
84
|
end
|
85
|
+
|
86
|
+
{ success: success }
|
73
87
|
end
|
74
88
|
|
75
|
-
def self.
|
76
|
-
|
77
|
-
config_file_path =
|
78
|
-
|
89
|
+
def self.load_config_from_file(args)
|
90
|
+
config_file_paths = args[:config_file_path]
|
91
|
+
config_file_path = [*config_file_paths].find { |v| File.exists?(File.expand_path(v)) }
|
92
|
+
raise "Configuration file not found. Searched '#{config_file_paths}'" unless config_file_path && !config_file_path.empty? && File.exists?(config_file_path)
|
79
93
|
|
80
94
|
begin
|
81
95
|
config = JSON.parse(File.read(config_file_path))
|
82
96
|
rescue => e
|
83
|
-
|
97
|
+
raise "Failed to Load Config File. '#{config_file_path}' '#{e.message}'"
|
84
98
|
end
|
85
|
-
|
86
|
-
|
87
|
-
self.new(args.merge({ :config => config }))
|
99
|
+
config
|
88
100
|
end
|
89
101
|
|
90
|
-
def self.
|
102
|
+
def self.load_config_from_service(args)
|
91
103
|
args_out = { }
|
92
104
|
args_out[:app_id] = args[:config_service_app_id]
|
93
105
|
args_out[:token] = args[:config_service_app_token]
|
94
106
|
args_out[:api_url] = args[:config_service_app_url]
|
95
107
|
config = Envoi::Mam::Agent::ConfigServiceClient.config_get(args_out)
|
96
|
-
|
108
|
+
config
|
109
|
+
end
|
97
110
|
|
111
|
+
def self.load_from_config_file(args)
|
112
|
+
config = load_config_from_file(args)
|
113
|
+
self.new(args.merge({ :config => config }))
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.load_from_config_service(args)
|
117
|
+
config = load_config_from_service(args)
|
98
118
|
self.new(args.merge({ :config => config }))
|
99
119
|
end
|
100
120
|
|
data/lib/envoi/mam/agent/cli.rb
CHANGED
@@ -6,11 +6,11 @@ module Envoi
|
|
6
6
|
module Mam
|
7
7
|
class Agent
|
8
8
|
|
9
|
-
class
|
9
|
+
class CLI
|
10
10
|
|
11
11
|
CONFIG_FILE_PATHS = [
|
12
|
-
|
13
|
-
|
12
|
+
'./envoi-mam-agent-config.json',
|
13
|
+
'~/envoi-mam-agent-config.json',
|
14
14
|
]
|
15
15
|
CONFIG_FILE_PATHS.map! { |v| File.expand_path(v) }
|
16
16
|
|
@@ -23,33 +23,43 @@
|
|
23
23
|
# # Envoi
|
24
24
|
# end
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
@commands_dir = File.join(File.dirname(__FILE__), 'commands')
|
27
|
+
@command_ext = '.rb'
|
28
|
+
|
29
|
+
@command_list_str = ''
|
30
|
+
@commands = [ ]
|
31
|
+
@command_aliases = { }
|
32
|
+
|
33
|
+
def build_command_aliases(commands_dir = @commands_dir)
|
34
|
+
Dir.glob(File.join(commands_dir, "*.rb")).each do |fn|
|
35
|
+
command_file_name = File.basename(fn)
|
36
|
+
command_name = File.basename(command_file_name, '.*')
|
37
|
+
command_alias = command_name.tr('-_', '')
|
38
|
+
@commands << command_name
|
39
|
+
@command_aliases[command_alias] = command_name
|
40
|
+
@command_list_str += "\n\t - #{command_name}"
|
30
41
|
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def usage
|
31
45
|
|
32
46
|
<<-EOT
|
33
47
|
Usage: #{File.basename($0)} COMMAND [ARGS]
|
34
48
|
|
35
49
|
Available Commands:
|
36
|
-
#{command_list_str}
|
50
|
+
#{@command_list_str}
|
37
51
|
|
38
52
|
All commands can be run with -h (or --help) for more information.
|
39
53
|
EOT
|
40
54
|
end
|
41
55
|
|
42
|
-
@commands_dir = File.join(File.dirname(__FILE__), 'commands')
|
43
|
-
@command_ext = '.rb'
|
44
|
-
|
45
|
-
command_aliases = {
|
46
|
-
}
|
47
56
|
|
57
|
+
build_command_aliases
|
48
58
|
if ARGV.empty?
|
49
59
|
should_show_usage = true
|
50
60
|
else
|
51
|
-
command_name = ARGV
|
52
|
-
command_name = command_aliases[command_name] || command_name
|
61
|
+
command_name = ARGV.first.dup
|
62
|
+
command_name = @command_aliases[command_name] || command_name
|
53
63
|
should_show_usage = %w(-h --help).include?(command_name.downcase)
|
54
64
|
end
|
55
65
|
if should_show_usage
|
@@ -70,14 +80,14 @@ end
|
|
70
80
|
|
71
81
|
|
72
82
|
# Execute the command if it is in object form, otherwise it's just a script and executed when we required it
|
73
|
-
if Envoi::Mam::Agent::
|
83
|
+
if Envoi::Mam::Agent::CLI.const_defined?('Commands')
|
74
84
|
command_class_name = command_name.to_s.capitalize
|
75
|
-
if Envoi::Mam::Agent::
|
76
|
-
command_object = Envoi::Mam::Agent::
|
85
|
+
if Envoi::Mam::Agent::CLI::Commands.const_defined?(command_class_name)
|
86
|
+
command_object = Envoi::Mam::Agent::CLI::Commands.const_get(command_class_name.capitalize ) rescue nil
|
77
87
|
end
|
78
88
|
command_object ||= begin
|
79
89
|
command_class_name = command_class_name.upcase
|
80
|
-
Envoi::Mam::Agent::
|
90
|
+
Envoi::Mam::Agent::CLI::Commands.const_defined?(command_class_name) ? Envoi::Mam::Agent::CLI::Commands.const_get(command_class_name ) : false
|
81
91
|
end
|
82
92
|
if command_object
|
83
93
|
command_instance = command_object.new if command_object.respond_to?(:new)
|