do_snapshot 0.0.15 → 0.2.2
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/lib/do_snapshot/adapter/abstract.rb +8 -7
- data/lib/do_snapshot/adapter/digitalocean.rb +14 -11
- data/lib/do_snapshot/adapter/digitalocean_v2.rb +11 -11
- data/lib/do_snapshot/cli.rb +23 -13
- data/lib/do_snapshot/command.rb +139 -127
- data/lib/do_snapshot/log.rb +20 -19
- data/lib/do_snapshot/mail.rb +55 -25
- data/lib/do_snapshot/version.rb +1 -1
- data/lib/do_snapshot.rb +4 -4
- data/spec/do_snapshot/cli_spec.rb +11 -12
- data/spec/do_snapshot/command_spec.rb +1 -2
- data/spec/do_snapshot/mail_spec.rb +15 -0
- data/spec/shared/environment.rb +7 -2
- data/spec/spec_helper.rb +0 -1
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25e673a7db69593b0007d27f55f873b12b2efad6
|
|
4
|
+
data.tar.gz: 159359c4f15b5a85163b660b84e994c742f3cccd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb017632d9bdcff73d39310c7d5754fd0579f3975d23f6e16824797e3cf2afc7dfeeb041d645db05df370e1dd45b37842b4150d0f02d5e9158a8577d8e51c566
|
|
7
|
+
data.tar.gz: 1ef277c08b234bc7925d9e797c85c1e3179d475512ee8debe4159bc4ec818ac274e596e30643f7c2551e92f46451ff5f9d748f86a3113e1ada5766084af401c0
|
|
@@ -6,10 +6,11 @@ module DoSnapshot
|
|
|
6
6
|
# Operating with Digital Ocean.
|
|
7
7
|
#
|
|
8
8
|
class Abstract
|
|
9
|
-
|
|
10
|
-
attr_accessor :timeout
|
|
9
|
+
include DoSnapshot::Log
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
attr_accessor :delay, :timeout
|
|
12
|
+
|
|
13
|
+
def initialize(options = {})
|
|
13
14
|
check_keys
|
|
14
15
|
set_id
|
|
15
16
|
options.each_pair do |key, option|
|
|
@@ -25,18 +26,18 @@ module DoSnapshot
|
|
|
25
26
|
|
|
26
27
|
# Waiting for event exit
|
|
27
28
|
def wait_event(id)
|
|
28
|
-
|
|
29
|
+
log.debug "Event Id: #{id}"
|
|
29
30
|
time = Time.now
|
|
30
31
|
sleep(delay) until get_event_status(id, time)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def after_cleanup(droplet_id, droplet_name, snapshot, event)
|
|
34
35
|
if !event
|
|
35
|
-
|
|
36
|
+
log.error "Destroy of snapshot #{snapshot.name} for droplet id: #{droplet_id} name: #{droplet_name} is failed."
|
|
36
37
|
elsif event && !event.status.include?('OK')
|
|
37
|
-
|
|
38
|
+
log.error event.message
|
|
38
39
|
else
|
|
39
|
-
|
|
40
|
+
log.debug "Snapshot name: #{snapshot.name} delete requested."
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
end
|
|
@@ -36,7 +36,7 @@ module DoSnapshot
|
|
|
36
36
|
instance = droplet(id)
|
|
37
37
|
|
|
38
38
|
if instance.status.include? 'active'
|
|
39
|
-
|
|
39
|
+
log.error 'Droplet is still running.'
|
|
40
40
|
else
|
|
41
41
|
power_on id
|
|
42
42
|
end
|
|
@@ -74,7 +74,7 @@ module DoSnapshot
|
|
|
74
74
|
|
|
75
75
|
# Cleanup our snapshots.
|
|
76
76
|
#
|
|
77
|
-
def cleanup_snapshots(instance, size)
|
|
77
|
+
def cleanup_snapshots(instance, size)
|
|
78
78
|
(0..size).each do |i|
|
|
79
79
|
# noinspection RubyResolve
|
|
80
80
|
snapshot = instance.snapshots[i]
|
|
@@ -85,9 +85,9 @@ module DoSnapshot
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def check_keys
|
|
88
|
-
|
|
88
|
+
log.debug 'Checking DigitalOcean Id\'s.'
|
|
89
89
|
%w( DIGITAL_OCEAN_CLIENT_ID DIGITAL_OCEAN_API_KEY ).each do |key|
|
|
90
|
-
|
|
90
|
+
log.error "You must have #{key} in environment or set it via options." if ENV[key].blank?
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
|
|
@@ -96,7 +96,7 @@ module DoSnapshot
|
|
|
96
96
|
# Set id's of Digital Ocean API.
|
|
97
97
|
#
|
|
98
98
|
def set_id
|
|
99
|
-
|
|
99
|
+
log.debug 'Setting DigitalOcean Id\'s.'
|
|
100
100
|
::DigitaloceanC.client_id = ENV['DIGITAL_OCEAN_CLIENT_ID']
|
|
101
101
|
::DigitaloceanC.api_key = ENV['DIGITAL_OCEAN_API_KEY']
|
|
102
102
|
end
|
|
@@ -105,10 +105,7 @@ module DoSnapshot
|
|
|
105
105
|
# Before snapshot we to know that machine has powered off.
|
|
106
106
|
#
|
|
107
107
|
def get_event_status(id, time)
|
|
108
|
-
if (
|
|
109
|
-
Log.debug "Event #{id} finished by timeout #{time}"
|
|
110
|
-
return true
|
|
111
|
-
end
|
|
108
|
+
return true if timeout?(id, time)
|
|
112
109
|
|
|
113
110
|
event = ::DigitaloceanC::Event.find(id)
|
|
114
111
|
fail event.message unless event.status.include?('OK')
|
|
@@ -116,6 +113,12 @@ module DoSnapshot
|
|
|
116
113
|
event.event.percentage && event.event.percentage.include?('100') ? true : false
|
|
117
114
|
end
|
|
118
115
|
|
|
116
|
+
def timeout?(id, time)
|
|
117
|
+
return false unless (Time.now - time) > @timeout
|
|
118
|
+
log.debug "Event #{id} finished by timeout #{time}"
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
119
122
|
# Request Power On for droplet
|
|
120
123
|
#
|
|
121
124
|
def power_on(id)
|
|
@@ -123,9 +126,9 @@ module DoSnapshot
|
|
|
123
126
|
event = ::DigitaloceanC::Droplet.power_on(id)
|
|
124
127
|
case event && event.status
|
|
125
128
|
when 'OK'
|
|
126
|
-
|
|
129
|
+
log.info 'Power On has been requested.'
|
|
127
130
|
else
|
|
128
|
-
|
|
131
|
+
log.error 'Power On failed to request.'
|
|
129
132
|
end
|
|
130
133
|
end
|
|
131
134
|
end
|
|
@@ -42,7 +42,7 @@ module DoSnapshot
|
|
|
42
42
|
instance = droplet(id)
|
|
43
43
|
|
|
44
44
|
if instance.status && instance.status.include?('active')
|
|
45
|
-
|
|
45
|
+
log.error 'Droplet is still running.'
|
|
46
46
|
else
|
|
47
47
|
power_on id
|
|
48
48
|
end
|
|
@@ -74,14 +74,14 @@ module DoSnapshot
|
|
|
74
74
|
|
|
75
75
|
# Cleanup our snapshots.
|
|
76
76
|
#
|
|
77
|
-
def cleanup_snapshots(instance, size)
|
|
77
|
+
def cleanup_snapshots(instance, size)
|
|
78
78
|
(0..size).each do |i|
|
|
79
79
|
# noinspection RubyResolve
|
|
80
80
|
snapshot = instance.snapshot_ids[i]
|
|
81
81
|
event = client.images.delete(id: snapshot)
|
|
82
82
|
|
|
83
83
|
unless event.is_a?(TrueClass)
|
|
84
|
-
|
|
84
|
+
log.debug event
|
|
85
85
|
event = false
|
|
86
86
|
end
|
|
87
87
|
|
|
@@ -90,16 +90,16 @@ module DoSnapshot
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def check_keys
|
|
93
|
-
|
|
93
|
+
log.debug 'Checking DigitalOcean Access Token.'
|
|
94
94
|
%w( DIGITAL_OCEAN_ACCESS_TOKEN ).each do |key|
|
|
95
|
-
|
|
95
|
+
log.error "You must have #{key} in environment or set it via options." if ENV[key].blank?
|
|
96
96
|
end
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
# Set id's of Digital Ocean API.
|
|
100
100
|
#
|
|
101
101
|
def set_id
|
|
102
|
-
|
|
102
|
+
log.debug 'Setting DigitalOcean Access Token.'
|
|
103
103
|
@client = ::DropletKit::Client.new(access_token: ENV['DIGITAL_OCEAN_ACCESS_TOKEN'])
|
|
104
104
|
end
|
|
105
105
|
|
|
@@ -107,9 +107,9 @@ module DoSnapshot
|
|
|
107
107
|
|
|
108
108
|
def after_cleanup(droplet_id, droplet_name, snapshot, event)
|
|
109
109
|
if !event
|
|
110
|
-
|
|
110
|
+
log.error "Destroy of snapshot #{snapshot} for droplet id: #{droplet_id} name: #{droplet_name} is failed."
|
|
111
111
|
else
|
|
112
|
-
|
|
112
|
+
log.debug "Snapshot: #{snapshot} delete requested."
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
@@ -118,7 +118,7 @@ module DoSnapshot
|
|
|
118
118
|
#
|
|
119
119
|
def get_event_status(id, time)
|
|
120
120
|
if (Time.now - time) > @timeout
|
|
121
|
-
|
|
121
|
+
log.debug "Event #{id} finished by timeout #{time}"
|
|
122
122
|
return true
|
|
123
123
|
end
|
|
124
124
|
|
|
@@ -135,9 +135,9 @@ module DoSnapshot
|
|
|
135
135
|
# noinspection RubyResolve
|
|
136
136
|
event = client.droplet_actions.power_on(droplet_id: id)
|
|
137
137
|
if event.status.include?('in-progress')
|
|
138
|
-
|
|
138
|
+
log.info 'Power On has been requested.'
|
|
139
139
|
else
|
|
140
|
-
|
|
140
|
+
log.error 'Power On failed to request.'
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
end
|
data/lib/do_snapshot/cli.rb
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
|
2
2
|
require 'thor'
|
|
3
3
|
require 'do_snapshot'
|
|
4
|
-
require_relative 'command'
|
|
5
|
-
require_relative 'mail'
|
|
6
4
|
require_relative 'log'
|
|
5
|
+
require_relative 'mail'
|
|
6
|
+
require_relative 'command'
|
|
7
7
|
|
|
8
8
|
module DoSnapshot
|
|
9
9
|
# CLI is here
|
|
10
10
|
#
|
|
11
11
|
class CLI < Thor # rubocop:disable ClassLength
|
|
12
|
+
include DoSnapshot::Log
|
|
13
|
+
include DoSnapshot::Mail
|
|
14
|
+
|
|
12
15
|
default_task :snap
|
|
13
16
|
|
|
14
17
|
map %w( c s create ) => :snap
|
|
@@ -154,11 +157,10 @@ module DoSnapshot
|
|
|
154
157
|
desc: 'DIGITAL_OCEAN_API_KEY. if you can\'t use environment.'
|
|
155
158
|
|
|
156
159
|
def snap
|
|
157
|
-
|
|
158
|
-
Command.snap
|
|
160
|
+
command.snap
|
|
159
161
|
rescue => e
|
|
160
|
-
|
|
161
|
-
|
|
162
|
+
command.fail_power_off(e) if [SnapshotCreateError, DropletShutdownError].include?(e.class)
|
|
163
|
+
log.error e.message
|
|
162
164
|
backtrace(e) if options.include? 'trace'
|
|
163
165
|
send_error
|
|
164
166
|
end
|
|
@@ -169,22 +171,30 @@ module DoSnapshot
|
|
|
169
171
|
end
|
|
170
172
|
|
|
171
173
|
no_commands do
|
|
174
|
+
def command
|
|
175
|
+
@command ||= Command.new(options,
|
|
176
|
+
%w( log smtp mail trace digital_ocean_client_id digital_ocean_api_key digital_ocean_access_token ))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def update_command
|
|
180
|
+
command.load_options(options,
|
|
181
|
+
%w( log smtp mail trace digital_ocean_client_id digital_ocean_api_key digital_ocean_access_token ))
|
|
182
|
+
end
|
|
183
|
+
|
|
172
184
|
def set_mailer
|
|
173
|
-
Mail.opts
|
|
174
|
-
Mail.smtp = options['smtp']
|
|
185
|
+
Mail.load_options(opts: options['mail'], smtp: options['smtp'])
|
|
175
186
|
end
|
|
176
187
|
|
|
177
188
|
def send_error
|
|
178
|
-
return unless
|
|
189
|
+
return unless mailer.opts
|
|
179
190
|
|
|
180
191
|
Mail.opts[:subject] = 'Digital Ocean: Error.'
|
|
181
192
|
Mail.opts[:body] = 'Please check your droplets.'
|
|
182
|
-
|
|
193
|
+
mailer.notify
|
|
183
194
|
end
|
|
184
195
|
|
|
185
196
|
def set_logger
|
|
186
|
-
Log.quiet
|
|
187
|
-
Log.verbose = options['trace']
|
|
197
|
+
Log.load_options(quiet: options['quiet'], verbose: options['trace'])
|
|
188
198
|
# Use Thor shell
|
|
189
199
|
Log.shell = shell unless options['quiet']
|
|
190
200
|
init_logger if options.include?('log')
|
|
@@ -197,7 +207,7 @@ module DoSnapshot
|
|
|
197
207
|
|
|
198
208
|
def backtrace(e)
|
|
199
209
|
e.backtrace.each do |t|
|
|
200
|
-
|
|
210
|
+
log.error t
|
|
201
211
|
end
|
|
202
212
|
end
|
|
203
213
|
end
|
data/lib/do_snapshot/command.rb
CHANGED
|
@@ -5,167 +5,179 @@ module DoSnapshot
|
|
|
5
5
|
# Our commands live here :)
|
|
6
6
|
#
|
|
7
7
|
class Command # rubocop:disable ClassLength
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Log.info 'Start performing operations'
|
|
11
|
-
work_with_droplets
|
|
12
|
-
Log.info 'All operations has been finished.'
|
|
8
|
+
include DoSnapshot::Log
|
|
9
|
+
include DoSnapshot::Mail
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
def initialize(*args)
|
|
12
|
+
load_options(*args)
|
|
13
|
+
end
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
end
|
|
15
|
+
def snap
|
|
16
|
+
log.info 'Start performing operations'
|
|
17
|
+
work_with_droplets
|
|
18
|
+
log.info 'All operations has been finished.'
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
mailer.notify if notify && !quiet
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fail_power_off(e)
|
|
24
|
+
return unless e && e.id
|
|
25
|
+
api.start_droplet(e.id)
|
|
26
|
+
rescue
|
|
27
|
+
raise DropletFindError, e.message, e.backtrace
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def load_options(options = {}, skip = %w())
|
|
31
|
+
reset_options
|
|
32
|
+
options.each_pair do |key, option|
|
|
33
|
+
send("#{key}=", option) unless skip.include?(key)
|
|
34
|
+
end if options
|
|
35
|
+
end
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
def reset_options
|
|
38
|
+
%i(droplets exclude only keep quiet stop clean timeout delay protocol threads api).each do |key|
|
|
39
|
+
send("#{key}=", nil)
|
|
33
40
|
end
|
|
41
|
+
end
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
def stop_droplet(droplet)
|
|
44
|
+
log.debug 'Shutting down droplet.'
|
|
45
|
+
api.stop_droplet(droplet.id) unless droplet.status.include? 'off'
|
|
46
|
+
end
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
# Trying to create a snapshot.
|
|
49
|
+
#
|
|
50
|
+
def create_snapshot(droplet) # rubocop:disable MethodLength,Metrics/AbcSize
|
|
51
|
+
log.info "Start creating snapshot for droplet id: #{droplet.id} name: #{droplet.name}."
|
|
44
52
|
|
|
45
|
-
|
|
53
|
+
today = DateTime.now
|
|
54
|
+
name = "#{droplet.name}_#{today.strftime('%Y_%m_%d')}"
|
|
55
|
+
# noinspection RubyResolve
|
|
56
|
+
snapshot_size = api.snapshots(droplet).size
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
log.debug 'Wait until snapshot will be created.'
|
|
48
59
|
|
|
49
|
-
|
|
60
|
+
api.create_snapshot droplet.id, name
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
Log.info "Droplet id: #{droplet.id} name: #{droplet.name} snapshots: #{snapshot_size}."
|
|
62
|
+
snapshot_size += 1
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
rescue => e
|
|
57
|
-
case e.class.to_s
|
|
58
|
-
when 'DoSnapshot::SnapshotCleanupError'
|
|
59
|
-
raise e.class, e.message, e.backtrace
|
|
60
|
-
else
|
|
61
|
-
raise SnapshotCreateError.new(droplet.id), e.message, e.backtrace
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
+
log.info "Snapshot name: #{name} created successfully."
|
|
65
|
+
log.info "Droplet id: #{droplet.id} name: #{droplet.name} snapshots: #{snapshot_size}."
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
# Cleanup snapshots.
|
|
68
|
+
cleanup_snapshots droplet, snapshot_size if clean
|
|
69
|
+
rescue => e
|
|
70
|
+
case e.class.to_s
|
|
71
|
+
when 'DoSnapshot::SnapshotCleanupError'
|
|
72
|
+
raise e.class, e.message, e.backtrace
|
|
73
|
+
else
|
|
74
|
+
raise SnapshotCreateError.new(droplet.id), e.message, e.backtrace
|
|
67
75
|
end
|
|
76
|
+
end
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
def api
|
|
79
|
+
@api ||= DoSnapshot::Adapter.api(protocol, delay: delay, timeout: timeout)
|
|
80
|
+
end
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
attr_accessor :keep, :quiet, :stop, :clean, :timeout, :delay, :protocol
|
|
82
|
+
protected
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
attr_accessor :droplets, :exclude, :only
|
|
85
|
+
attr_accessor :keep, :quiet, :stop, :clean, :timeout, :delay, :protocol
|
|
75
86
|
|
|
76
|
-
|
|
77
|
-
@notify ||= false
|
|
78
|
-
end
|
|
87
|
+
attr_writer :threads, :api
|
|
79
88
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
def notify
|
|
90
|
+
@notify ||= false
|
|
91
|
+
end
|
|
83
92
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
load_droplets
|
|
88
|
-
dispatch_droplets
|
|
89
|
-
Log.debug 'Working with list of DigitalOcean droplets'
|
|
90
|
-
thread_chain
|
|
91
|
-
end
|
|
93
|
+
def threads
|
|
94
|
+
@threads ||= []
|
|
95
|
+
end
|
|
92
96
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
# Working with list of droplets.
|
|
98
|
+
#
|
|
99
|
+
def work_with_droplets
|
|
100
|
+
load_droplets
|
|
101
|
+
dispatch_droplets
|
|
102
|
+
log.debug 'Working with list of DigitalOcean droplets'
|
|
103
|
+
thread_chain
|
|
104
|
+
end
|
|
100
105
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
# Getting droplets list from API.
|
|
107
|
+
# And store into object.
|
|
108
|
+
#
|
|
109
|
+
def load_droplets
|
|
110
|
+
log.debug 'Loading list of DigitalOcean droplets'
|
|
111
|
+
self.droplets = api.droplets
|
|
112
|
+
end
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
# Dispatch received droplets, each by each.
|
|
115
|
+
#
|
|
116
|
+
def dispatch_droplets
|
|
117
|
+
droplets.each do |droplet|
|
|
118
|
+
id = droplet.id.to_s
|
|
119
|
+
next if exclude.include? id
|
|
120
|
+
next unless only.empty? || only.include?(id)
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
#
|
|
115
|
-
def thread_chain
|
|
116
|
-
threads.each(&:join)
|
|
122
|
+
prepare_droplet id, droplet.name
|
|
117
123
|
end
|
|
124
|
+
end
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
create_snapshot droplet
|
|
125
|
-
end
|
|
126
|
-
end
|
|
126
|
+
# Join threads
|
|
127
|
+
#
|
|
128
|
+
def thread_chain
|
|
129
|
+
threads.each(&:join)
|
|
130
|
+
end
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return unless droplet
|
|
136
|
-
Log.info "Preparing droplet id: #{droplet.id} name: #{droplet.name} to take snapshot."
|
|
137
|
-
return if too_much_snapshots(droplet)
|
|
138
|
-
thread_runner(droplet)
|
|
132
|
+
# Run threads
|
|
133
|
+
#
|
|
134
|
+
def thread_runner(droplet)
|
|
135
|
+
threads << Thread.new do
|
|
136
|
+
stop_droplet droplet
|
|
137
|
+
create_snapshot droplet
|
|
139
138
|
end
|
|
139
|
+
end
|
|
140
140
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
# Preparing droplet to take a snapshot.
|
|
142
|
+
# Droplet instance must be powered off first!
|
|
143
|
+
#
|
|
144
|
+
def prepare_droplet(id, name)
|
|
145
|
+
log.debug "Droplet id: #{id} name: #{name} "
|
|
146
|
+
droplet = api.droplet id
|
|
147
|
+
|
|
148
|
+
return unless droplet
|
|
149
|
+
log.info "Preparing droplet id: #{droplet.id} name: #{droplet.name} to take snapshot."
|
|
150
|
+
return if too_much_snapshots(droplet)
|
|
151
|
+
thread_runner(droplet)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def too_much_snapshots(instance)
|
|
155
|
+
# noinspection RubyResolve
|
|
156
|
+
return false unless api.snapshots(instance).size >= keep
|
|
157
|
+
warning_size(instance.id, instance.name, keep)
|
|
158
|
+
stop ? true : false
|
|
159
|
+
end
|
|
147
160
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
161
|
+
# Cleanup our snapshots.
|
|
162
|
+
#
|
|
163
|
+
def cleanup_snapshots(droplet, size) # rubocop:disable Metrics/AbcSize
|
|
164
|
+
return unless size > keep
|
|
152
165
|
|
|
153
|
-
|
|
166
|
+
warning_size(droplet.id, droplet.name, size)
|
|
154
167
|
|
|
155
|
-
|
|
168
|
+
log.debug "Cleaning up snapshots for droplet id: #{droplet.id} name: #{droplet.name}."
|
|
156
169
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
170
|
+
api.cleanup_snapshots(droplet, size - keep - 1)
|
|
171
|
+
rescue => e
|
|
172
|
+
raise SnapshotCleanupError, e.message, e.backtrace
|
|
173
|
+
end
|
|
161
174
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
end
|
|
175
|
+
# Helpers
|
|
176
|
+
#
|
|
177
|
+
def warning_size(id, name, keep)
|
|
178
|
+
message = "For droplet with id: #{id} and name: #{name} the maximum number #{keep} of snapshots is reached."
|
|
179
|
+
log.warn message
|
|
180
|
+
@notify = true
|
|
169
181
|
end
|
|
170
182
|
end
|
|
171
183
|
end
|
data/lib/do_snapshot/log.rb
CHANGED
|
@@ -4,33 +4,32 @@ require 'logger'
|
|
|
4
4
|
module DoSnapshot
|
|
5
5
|
# Shared logger
|
|
6
6
|
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def buffer
|
|
13
|
-
@buffer ||= %w()
|
|
14
|
-
end
|
|
7
|
+
module Log
|
|
8
|
+
def log
|
|
9
|
+
UniversalLogger
|
|
10
|
+
end
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
# UniversalLogger is module to deal with singleton methods.
|
|
13
|
+
# Used to give classes access only for selected methods
|
|
14
|
+
#
|
|
15
|
+
module UniversalLogger
|
|
16
|
+
%i(info warn error debug).each do |type|
|
|
17
|
+
define_singleton_method(type) { |message| Log.log type, message }
|
|
18
18
|
end
|
|
19
|
+
end
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
class << self
|
|
22
|
+
attr_accessor :logger, :shell, :quiet, :verbose
|
|
23
|
+
attr_writer :buffer
|
|
23
24
|
|
|
24
|
-
def
|
|
25
|
-
|
|
25
|
+
def load_options(options = {})
|
|
26
|
+
options.each { |key, option| send("#{key}=", option) }
|
|
26
27
|
end
|
|
27
28
|
|
|
28
|
-
def
|
|
29
|
-
|
|
29
|
+
def buffer
|
|
30
|
+
@buffer ||= %w()
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
protected
|
|
33
|
-
|
|
34
33
|
def log(type, message)
|
|
35
34
|
buffer << message
|
|
36
35
|
logger.send(type, message) if logger
|
|
@@ -38,6 +37,8 @@ module DoSnapshot
|
|
|
38
37
|
say message, color(type) unless print?(type)
|
|
39
38
|
end
|
|
40
39
|
|
|
40
|
+
protected
|
|
41
|
+
|
|
41
42
|
def print?(type)
|
|
42
43
|
(type == :debug && !verbose) || quiet
|
|
43
44
|
end
|
data/lib/do_snapshot/mail.rb
CHANGED
|
@@ -2,17 +2,63 @@
|
|
|
2
2
|
require 'date'
|
|
3
3
|
require 'pony'
|
|
4
4
|
require_relative 'core_ext/hash'
|
|
5
|
+
require_relative 'log'
|
|
5
6
|
|
|
6
7
|
module DoSnapshot
|
|
7
8
|
# Shared mailer.
|
|
8
9
|
#
|
|
9
|
-
|
|
10
|
+
module Mail
|
|
11
|
+
def mailer
|
|
12
|
+
UniversalMailer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# UniversalMailer is module to deal with singleton methods.
|
|
16
|
+
# Used to give classes access only for selected methods
|
|
17
|
+
#
|
|
18
|
+
module UniversalMailer
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def notify
|
|
22
|
+
Mail.notify
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
10
26
|
class << self
|
|
11
|
-
|
|
12
|
-
|
|
27
|
+
include DoSnapshot::Log
|
|
28
|
+
|
|
29
|
+
attr_writer :mailer, :opts_default, :smtp_default
|
|
30
|
+
|
|
31
|
+
def load_options(options = {})
|
|
32
|
+
options.each { |key, option| send("#{key}=", option) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reset_options
|
|
36
|
+
@opts = opts_default
|
|
37
|
+
@smtp = smtp_default
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mailer
|
|
41
|
+
@mailer ||= Pony.method(:mail)
|
|
42
|
+
end
|
|
13
43
|
|
|
14
44
|
def smtp
|
|
15
|
-
@smtp ||=
|
|
45
|
+
@smtp ||= smtp_default.dup
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def opts
|
|
49
|
+
@opts ||= opts_default.dup
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def smtp=(options)
|
|
53
|
+
options.each_pair do |key, value|
|
|
54
|
+
smtp[key.to_sym] = value
|
|
55
|
+
end if options
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def opts=(options)
|
|
59
|
+
options.each_pair do |key, value|
|
|
60
|
+
opts[key.to_sym] = value
|
|
61
|
+
end if options
|
|
16
62
|
end
|
|
17
63
|
|
|
18
64
|
# Sending message via Hash params.
|
|
@@ -20,17 +66,10 @@ module DoSnapshot
|
|
|
20
66
|
# Options:: --mail to:mail@somehost.com from:from@host.com --smtp address:smtp.gmail.com user_name:someuser password:somepassword
|
|
21
67
|
#
|
|
22
68
|
def notify
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
opts.symbolize_keys!
|
|
26
|
-
smtp.symbolize_keys!
|
|
27
|
-
|
|
28
|
-
opts_setup
|
|
29
|
-
smtp_setup
|
|
30
|
-
|
|
31
|
-
Log.debug 'Sending e-mail notification.'
|
|
69
|
+
setup_notify
|
|
70
|
+
log.debug 'Sending e-mail notification.'
|
|
32
71
|
# Look into your inbox :)
|
|
33
|
-
|
|
72
|
+
mailer.call(opts)
|
|
34
73
|
end
|
|
35
74
|
|
|
36
75
|
protected
|
|
@@ -52,17 +91,8 @@ module DoSnapshot
|
|
|
52
91
|
}
|
|
53
92
|
end
|
|
54
93
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
opts[key] = value unless opts.include? key
|
|
58
|
-
end
|
|
59
|
-
opts[:body] = "#{opts[:body]}\n\nTrace: #{DateTime.now}\n#{Log.buffer.join("\n")}"
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def smtp_setup
|
|
63
|
-
smtp_default.each_pair do |key, value|
|
|
64
|
-
smtp[key] = value unless smtp.include? key
|
|
65
|
-
end
|
|
94
|
+
def setup_notify
|
|
95
|
+
opts[:body] = "#{opts[:body]}\n\nTrace: #{DateTime.now}\n#{Log.buffer.join("\n")}"
|
|
66
96
|
opts[:via_options] = smtp
|
|
67
97
|
end
|
|
68
98
|
end
|
data/lib/do_snapshot/version.rb
CHANGED
data/lib/do_snapshot.rb
CHANGED
|
@@ -22,7 +22,7 @@ module DoSnapshot
|
|
|
22
22
|
#
|
|
23
23
|
class DropletShutdownError < RequestActionError
|
|
24
24
|
def initialize(*args)
|
|
25
|
-
Log.error "Droplet id: #{args[0]} is Failed to Power Off."
|
|
25
|
+
Log.log :error, "Droplet id: #{args[0]} is Failed to Power Off."
|
|
26
26
|
super
|
|
27
27
|
end
|
|
28
28
|
end
|
|
@@ -32,7 +32,7 @@ module DoSnapshot
|
|
|
32
32
|
#
|
|
33
33
|
class SnapshotCreateError < RequestActionError
|
|
34
34
|
def initialize(*args)
|
|
35
|
-
Log.error "Droplet id: #{args[0]} is Failed to Snapshot."
|
|
35
|
+
Log.log :error, "Droplet id: #{args[0]} is Failed to Snapshot."
|
|
36
36
|
super
|
|
37
37
|
end
|
|
38
38
|
end
|
|
@@ -42,7 +42,7 @@ module DoSnapshot
|
|
|
42
42
|
#
|
|
43
43
|
class DropletFindError < RequestError
|
|
44
44
|
def initialize(*args)
|
|
45
|
-
Log.error 'Droplet Not Found'
|
|
45
|
+
Log.log :error, 'Droplet Not Found'
|
|
46
46
|
super
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -52,7 +52,7 @@ module DoSnapshot
|
|
|
52
52
|
#
|
|
53
53
|
class DropletListError < RequestError
|
|
54
54
|
def initialize(*args)
|
|
55
|
-
Log.error 'Droplet Listing is failed to retrieve'
|
|
55
|
+
Log.log :error, 'Droplet Listing is failed to retrieve'
|
|
56
56
|
super
|
|
57
57
|
end
|
|
58
58
|
end
|
|
@@ -6,7 +6,6 @@ describe DoSnapshot::CLI do
|
|
|
6
6
|
include_context 'api_v1_helpers'
|
|
7
7
|
|
|
8
8
|
subject(:cli) { described_class }
|
|
9
|
-
subject(:command) { DoSnapshot::Command }
|
|
10
9
|
subject(:api) { DoSnapshot::Adapter::Digitalocean }
|
|
11
10
|
|
|
12
11
|
describe '.initialize' do
|
|
@@ -28,7 +27,7 @@ describe DoSnapshot::CLI do
|
|
|
28
27
|
stub_all_api(%w(100825 100823))
|
|
29
28
|
hash_attribute_eq_no_stub(exclude: excluded_droplets, only: %w())
|
|
30
29
|
|
|
31
|
-
expect(command.send('exclude'))
|
|
30
|
+
expect(@cli.command.send('exclude'))
|
|
32
31
|
.to eq excluded_droplets
|
|
33
32
|
end
|
|
34
33
|
|
|
@@ -37,7 +36,7 @@ describe DoSnapshot::CLI do
|
|
|
37
36
|
stub_all_api(selected_droplets)
|
|
38
37
|
hash_attribute_eq_no_stub(only: selected_droplets)
|
|
39
38
|
|
|
40
|
-
expect(command.send('only'))
|
|
39
|
+
expect(@cli.command.send('only'))
|
|
41
40
|
.to eq selected_droplets
|
|
42
41
|
end
|
|
43
42
|
|
|
@@ -93,19 +92,19 @@ describe DoSnapshot::CLI do
|
|
|
93
92
|
end
|
|
94
93
|
|
|
95
94
|
it 'with mail' do
|
|
96
|
-
hash_attribute_eq(mail_options)
|
|
95
|
+
hash_attribute_eq(mail: mail_options)
|
|
97
96
|
end
|
|
98
97
|
|
|
99
98
|
it 'with no mail' do
|
|
100
|
-
without_hash_attribute_eq(mail_options)
|
|
99
|
+
without_hash_attribute_eq(mail: mail_options)
|
|
101
100
|
end
|
|
102
101
|
|
|
103
102
|
it 'with smtp' do
|
|
104
|
-
hash_attribute_eq(smtp_options)
|
|
103
|
+
hash_attribute_eq(smtp: smtp_options)
|
|
105
104
|
end
|
|
106
105
|
|
|
107
106
|
it 'with no smtp' do
|
|
108
|
-
without_hash_attribute_eq(smtp_options)
|
|
107
|
+
without_hash_attribute_eq(smtp: smtp_options)
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
it 'with log' do
|
|
@@ -153,9 +152,9 @@ describe DoSnapshot::CLI do
|
|
|
153
152
|
stub_all_api
|
|
154
153
|
options = default_options.merge!(:"#{name}" => value)
|
|
155
154
|
@cli.options = @cli.options.merge(options)
|
|
156
|
-
@cli.
|
|
155
|
+
@cli.update_command
|
|
157
156
|
|
|
158
|
-
expect(command.send(name))
|
|
157
|
+
expect(@cli.command.send(name))
|
|
159
158
|
.to eq value
|
|
160
159
|
end
|
|
161
160
|
|
|
@@ -163,7 +162,7 @@ describe DoSnapshot::CLI do
|
|
|
163
162
|
stub_all_api
|
|
164
163
|
options = default_options.merge!(hash)
|
|
165
164
|
@cli.options = @cli.options.merge(options)
|
|
166
|
-
@cli.
|
|
165
|
+
@cli.update_command
|
|
167
166
|
end
|
|
168
167
|
|
|
169
168
|
def with_hash_attribute_eq(hash)
|
|
@@ -181,11 +180,11 @@ describe DoSnapshot::CLI do
|
|
|
181
180
|
def hash_attribute_eq_no_stub(hash)
|
|
182
181
|
options = default_options.merge!(hash)
|
|
183
182
|
@cli.options = @cli.options.merge(options)
|
|
184
|
-
@cli.
|
|
183
|
+
@cli.update_command
|
|
185
184
|
end
|
|
186
185
|
|
|
187
186
|
def set_api_attribute(options = { delay: delay, timeout: timeout }) # rubocop:disable Style/AccessorMethodName
|
|
188
|
-
command.send('api=', api.new(options))
|
|
187
|
+
@cli.command.send('api=', api.new(options))
|
|
189
188
|
end
|
|
190
189
|
|
|
191
190
|
before(:each) do
|
|
@@ -6,7 +6,7 @@ describe DoSnapshot::Command do
|
|
|
6
6
|
include_context 'uri_helpers'
|
|
7
7
|
include_context 'api_v1_helpers'
|
|
8
8
|
|
|
9
|
-
subject(:cmd) { DoSnapshot::Command }
|
|
9
|
+
subject(:cmd) { DoSnapshot::Command.new }
|
|
10
10
|
subject(:log) { DoSnapshot::Log }
|
|
11
11
|
|
|
12
12
|
describe '.snap' do
|
|
@@ -144,7 +144,6 @@ describe DoSnapshot::Command do
|
|
|
144
144
|
|
|
145
145
|
def load_options(options = nil)
|
|
146
146
|
options ||= default_options
|
|
147
|
-
cmd.send('api=', nil)
|
|
148
147
|
cmd.load_options(options, [:log, :mail, :smtp, :trace, :digital_ocean_client_id, :digital_ocean_api_key])
|
|
149
148
|
end
|
|
150
149
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
|
|
4
|
+
describe DoSnapshot::Mail do
|
|
5
|
+
include_context 'spec'
|
|
6
|
+
|
|
7
|
+
describe 'will send mail with options' do
|
|
8
|
+
it '#notify' do
|
|
9
|
+
DoSnapshot::Mail.reset_options
|
|
10
|
+
DoSnapshot::Mail.load_options(opts: mail_options, smtp: smtp_options)
|
|
11
|
+
expect { DoSnapshot::Mail.notify }.not_to raise_error
|
|
12
|
+
expect(DoSnapshot::Mail.smtp[:address]).to eq(smtp_options[:address])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/spec/shared/environment.rb
CHANGED
|
@@ -4,6 +4,10 @@ require 'spec_helper'
|
|
|
4
4
|
shared_context 'spec' do
|
|
5
5
|
include_context 'api_helpers'
|
|
6
6
|
|
|
7
|
+
def do_not_send_email
|
|
8
|
+
allow(Pony).to receive(:deliver)
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
let(:client_key) { 'foo' }
|
|
8
12
|
let(:api_key) { 'bar' }
|
|
9
13
|
let(:access_token) { 'sometoken' }
|
|
@@ -11,7 +15,7 @@ shared_context 'spec' do
|
|
|
11
15
|
let(:droplet_id) { '100823' }
|
|
12
16
|
let(:image_id) { '5019770' }
|
|
13
17
|
let(:image_id2) { '5019903' }
|
|
14
|
-
let(:cli_keys) { Thor::CoreExt::HashWithIndifferentAccess.new(digital_ocean_client_id: 'NOTFOO',
|
|
18
|
+
let(:cli_keys) { Thor::CoreExt::HashWithIndifferentAccess.new(digital_ocean_client_id: 'NOTFOO', digital_ocean_api_key: 'NOTBAR', digital_ocean_access_token: 'NOTTOK') }
|
|
15
19
|
let(:snapshot_name) { "foo_#{DateTime.now.strftime('%Y_%m_%d')}" }
|
|
16
20
|
let(:default_options) { Hash[protocol: 1, only: %w( 100823 ), exclude: %w(), keep: 3, stop: false, trace: true, clean: true, delay: 0, timeout: 600, droplets: nil, threads: []] }
|
|
17
21
|
let(:no_exclude) { [] }
|
|
@@ -30,7 +34,7 @@ shared_context 'spec' do
|
|
|
30
34
|
let(:smtp_options) { Thor::CoreExt::HashWithIndifferentAccess.new(address: 'smtp.gmail.com', port: '25', user_name: 'someuser', password: 'somepassword') }
|
|
31
35
|
let(:log) { Thor::CoreExt::HashWithIndifferentAccess.new(log: "#{project_path}/log/test.log") }
|
|
32
36
|
|
|
33
|
-
def stub_all_api(droplets = nil, active = false)
|
|
37
|
+
def stub_all_api(droplets = nil, active = false)
|
|
34
38
|
drops = []
|
|
35
39
|
droplets ||= [droplet_id]
|
|
36
40
|
droplets.each do |droplet|
|
|
@@ -90,6 +94,7 @@ shared_context 'spec' do
|
|
|
90
94
|
end
|
|
91
95
|
|
|
92
96
|
before(:each) do
|
|
97
|
+
do_not_send_email
|
|
93
98
|
set_api_keys
|
|
94
99
|
end
|
|
95
100
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: do_snapshot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Merkulov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2015-
|
|
11
|
+
date: 2015-07-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 4.0.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 4.0.0
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: digitalocean_c
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -95,6 +109,7 @@ files:
|
|
|
95
109
|
- spec/do_snapshot/adapter/digitalocean_v2_spec.rb
|
|
96
110
|
- spec/do_snapshot/cli_spec.rb
|
|
97
111
|
- spec/do_snapshot/command_spec.rb
|
|
112
|
+
- spec/do_snapshot/mail_spec.rb
|
|
98
113
|
- spec/do_snapshots_spec.rb
|
|
99
114
|
- spec/fixtures/digitalocean/v1/error_message.json
|
|
100
115
|
- spec/fixtures/digitalocean/v1/response_event.json
|
|
@@ -155,6 +170,7 @@ test_files:
|
|
|
155
170
|
- spec/do_snapshot/adapter/digitalocean_v2_spec.rb
|
|
156
171
|
- spec/do_snapshot/cli_spec.rb
|
|
157
172
|
- spec/do_snapshot/command_spec.rb
|
|
173
|
+
- spec/do_snapshot/mail_spec.rb
|
|
158
174
|
- spec/do_snapshots_spec.rb
|
|
159
175
|
- spec/fixtures/digitalocean/v1/error_message.json
|
|
160
176
|
- spec/fixtures/digitalocean/v1/response_event.json
|