do_snapshot 0.0.6 → 0.0.7
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 +13 -5
- data/.gitignore +5 -2
- data/.rubocop.yml +3 -0
- data/.travis.yml +10 -0
- data/README.md +16 -0
- data/Rakefile +6 -0
- data/bin/do_snapshot +4 -1
- data/do_snapshot.gemspec +10 -4
- data/lib/do_snapshot.rb +47 -104
- data/lib/do_snapshot/api.rb +140 -0
- data/lib/do_snapshot/cli.rb +115 -48
- data/lib/do_snapshot/command.rb +71 -102
- data/lib/do_snapshot/core_ext/hash.rb +3 -2
- data/lib/do_snapshot/log.rb +59 -0
- data/lib/do_snapshot/mail.rb +70 -0
- data/lib/do_snapshot/version.rb +2 -1
- data/{test → log}/.keep +0 -0
- data/spec/.keep +0 -0
- data/spec/do_snapshot/api_spec.rb +218 -0
- data/spec/do_snapshot/cli_spec.rb +173 -0
- data/spec/do_snapshot/command_spec.rb +121 -0
- data/spec/do_snapshots_spec.rb +58 -0
- data/spec/fixtures/error_message.json +4 -0
- data/spec/fixtures/response_event.json +4 -0
- data/spec/fixtures/show_droplet.json +39 -0
- data/spec/fixtures/show_droplet_inactive.json +39 -0
- data/spec/fixtures/show_droplets.json +35 -0
- data/spec/fixtures/show_droplets_empty.json +4 -0
- data/spec/fixtures/show_event_done.json +10 -0
- data/spec/fixtures/show_event_start.json +10 -0
- data/spec/shared/api_helpers.rb +117 -0
- data/spec/shared/environment.rb +114 -0
- data/spec/shared/uri_helpers.rb +12 -0
- data/spec/spec_helper.rb +34 -0
- data/tmp/.keep +0 -0
- metadata +145 -23
data/lib/do_snapshot/cli.rb
CHANGED
@@ -1,31 +1,31 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'thor'
|
1
3
|
require 'do_snapshot'
|
2
4
|
require 'do_snapshot/command'
|
5
|
+
require 'do_snapshot/mail'
|
6
|
+
require 'do_snapshot/log'
|
3
7
|
|
4
8
|
module DoSnapshot
|
5
9
|
# CLI is here
|
6
10
|
#
|
7
|
-
class CLI < Thor
|
11
|
+
class CLI < Thor # rubocop:disable ClassLength
|
8
12
|
default_task :snap
|
9
13
|
|
10
14
|
map %w( c s create ) => :snap
|
11
15
|
map %w( -V ) => :version
|
12
16
|
|
17
|
+
# Overriding Thor method for custom initialization
|
18
|
+
#
|
13
19
|
def initialize(*args)
|
14
20
|
super
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
Log.shell = shell unless Log.quiet
|
19
|
-
Log.verbose = options['trace']
|
20
|
-
|
21
|
-
logger if options.include?('log')
|
22
|
-
|
23
|
-
Log.mail = options['mail']
|
24
|
-
Log.smtp = options['smtp']
|
22
|
+
set_logger
|
23
|
+
set_mailer
|
25
24
|
|
26
25
|
# Check for keys via options
|
27
|
-
|
28
|
-
|
26
|
+
%w( digital_ocean_client_id digital_ocean_api_key ).each do |key|
|
27
|
+
ENV[key.upcase] = options[key] if options.include? key
|
28
|
+
end
|
29
29
|
|
30
30
|
try_keys_first
|
31
31
|
end
|
@@ -66,33 +66,86 @@ module DoSnapshot
|
|
66
66
|
|
67
67
|
VERSION: #{DoSnapshot::VERSION}
|
68
68
|
LONGDESC
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
method_option :
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
69
|
+
method_option :only,
|
70
|
+
type: :array,
|
71
|
+
default: [],
|
72
|
+
aliases: %w( -o ),
|
73
|
+
banner: '123456 123456 123456',
|
74
|
+
desc: 'Select some droplets.'
|
75
|
+
method_option :exclude,
|
76
|
+
type: :array,
|
77
|
+
default: [],
|
78
|
+
aliases: %w( -e ),
|
79
|
+
banner: '123456 123456 123456',
|
80
|
+
desc: 'Except some droplets.'
|
81
|
+
method_option :keep,
|
82
|
+
type: :numeric,
|
83
|
+
default: 10,
|
84
|
+
aliases: %w( -k ),
|
85
|
+
banner: '5',
|
86
|
+
desc: 'How much snapshots you want to keep?'
|
87
|
+
method_option :delay,
|
88
|
+
type: :numeric,
|
89
|
+
default: 10,
|
90
|
+
aliases: %w( -d ),
|
91
|
+
banner: '5',
|
92
|
+
desc: 'Delay between snapshot operation status requests.'
|
93
|
+
method_option :timeout,
|
94
|
+
type: :numeric,
|
95
|
+
default: 180,
|
96
|
+
banner: '250',
|
97
|
+
desc: 'Timeout in sec\'s for events like Power Off or Create Snapshot.'
|
98
|
+
method_option :mail,
|
99
|
+
type: :hash,
|
100
|
+
aliases: %w( -m ),
|
101
|
+
banner: 'to:yourmail@example.com',
|
102
|
+
desc: 'Receive mail if fail or maximum is reached.'
|
103
|
+
method_option :smtp,
|
104
|
+
type: :hash,
|
105
|
+
aliases: %w( -t ),
|
106
|
+
banner: 'user_name:yourmail@example.com password:password',
|
107
|
+
desc: 'SMTP options.'
|
108
|
+
method_option :log,
|
109
|
+
type: :string,
|
110
|
+
aliases: %w( -l ),
|
111
|
+
banner: '/Users/someone/.do_snapshot/main.log',
|
112
|
+
desc: 'Log file path. By default logging is disabled.'
|
113
|
+
method_option :clean,
|
114
|
+
type: :boolean,
|
115
|
+
aliases: %w( -c ),
|
116
|
+
desc: 'Cleanup snapshots after create. If you have more images than you want to `keep`, older will be deleted.'
|
117
|
+
method_option :stop,
|
118
|
+
type: :boolean,
|
119
|
+
aliases: %w( -s),
|
120
|
+
desc: 'Stop creating snapshots if maximum is reached.'
|
121
|
+
method_option :trace,
|
122
|
+
type: :boolean,
|
123
|
+
aliases: %w( -v ),
|
124
|
+
desc: 'Verbose mode.'
|
125
|
+
method_option :quiet,
|
126
|
+
type: :boolean,
|
127
|
+
aliases: %w( -q ),
|
128
|
+
desc: 'Quiet mode. If don\'t need any messages and in console.'
|
129
|
+
|
130
|
+
method_option :digital_ocean_client_id,
|
131
|
+
type: :string,
|
132
|
+
banner: 'YOURLONGAPICLIENTID',
|
133
|
+
desc: 'DIGITAL_OCEAN_CLIENT_ID. if you can\'t use environment.'
|
134
|
+
method_option :digital_ocean_api_key,
|
135
|
+
type: :string,
|
136
|
+
banner: 'YOURLONGAPIKEY',
|
137
|
+
desc: 'DIGITAL_OCEAN_API_KEY. if you can\'t use environment.'
|
84
138
|
|
85
139
|
def snap
|
86
|
-
Command.
|
140
|
+
Command.snap options, %w( log trace digital_ocean_client_id digital_ocean_api_key )
|
87
141
|
rescue => e
|
88
|
-
|
89
|
-
Command.fail_power_on(e.id) if e && e.class == SnapshotCreateError && e.respond_to?('id')
|
142
|
+
Command.fail_power_off(e) if [SnapshotCreateError, DropletShutdownError].include?(e.class)
|
90
143
|
Log.error e.message
|
91
144
|
backtrace(e) if options.include? 'trace'
|
92
|
-
if
|
93
|
-
|
94
|
-
|
95
|
-
|
145
|
+
if Mail.opts
|
146
|
+
Mail.opts[:subject] = 'Digital Ocean: Error.'
|
147
|
+
Mail.opts[:body] = 'Please check your droplets.'
|
148
|
+
Mail.notify
|
96
149
|
end
|
97
150
|
end
|
98
151
|
|
@@ -101,24 +154,38 @@ module DoSnapshot
|
|
101
154
|
puts DoSnapshot::VERSION
|
102
155
|
end
|
103
156
|
|
104
|
-
|
157
|
+
no_commands do
|
158
|
+
def set_mailer
|
159
|
+
Mail.opts = options['mail']
|
160
|
+
Mail.smtp = options['smtp']
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_logger
|
164
|
+
Log.quiet = options['quiet']
|
165
|
+
Log.verbose = options['trace']
|
166
|
+
# Use Thor shell
|
167
|
+
Log.shell = shell unless options['quiet']
|
168
|
+
init_logger if options.include?('log')
|
169
|
+
end
|
105
170
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
171
|
+
def init_logger
|
172
|
+
Log.logger = Logger.new(options['log'])
|
173
|
+
Log.logger.level = Log.verbose ? Logger::DEBUG : Logger::INFO
|
174
|
+
end
|
110
175
|
|
111
|
-
|
112
|
-
|
113
|
-
|
176
|
+
def backtrace(e)
|
177
|
+
e.backtrace.each do |t|
|
178
|
+
Log.error t
|
179
|
+
end
|
114
180
|
end
|
115
|
-
end
|
116
181
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
182
|
+
# Check for DigitalOcean API keys
|
183
|
+
def try_keys_first
|
184
|
+
Log.debug 'Checking DigitalOcean Id\'s.'
|
185
|
+
%w( DIGITAL_OCEAN_CLIENT_ID DIGITAL_OCEAN_API_KEY ).each do |key|
|
186
|
+
Log.fail Thor::Error, "You must have #{key} in environment or set it via options." if !ENV[key] || ENV[key].empty?
|
187
|
+
end
|
188
|
+
end
|
122
189
|
end
|
123
190
|
end
|
124
191
|
end
|
data/lib/do_snapshot/command.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
|
2
|
-
require 'thread'
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
# require 'thread'
|
3
|
+
require 'do_snapshot/api'
|
3
4
|
|
4
5
|
module DoSnapshot
|
5
6
|
# Our commands live here :)
|
6
7
|
#
|
7
|
-
class Command
|
8
|
+
class Command # rubocop:disable ClassLength
|
8
9
|
class << self
|
9
|
-
def
|
10
|
+
def snap(options, skip)
|
10
11
|
return unless options
|
11
12
|
|
12
13
|
options.each_pair do |key, option|
|
@@ -14,32 +15,29 @@ module DoSnapshot
|
|
14
15
|
end
|
15
16
|
|
16
17
|
Log.info 'Start performing operations'
|
17
|
-
|
18
|
+
work_with_droplets
|
18
19
|
Log.info 'All operations has been finished.'
|
19
20
|
|
20
|
-
|
21
|
+
Mail.notify if notify && !quiet
|
21
22
|
end
|
22
23
|
|
23
|
-
def
|
24
|
-
return unless id
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
fail instance.message unless instance.status.include? 'OK'
|
29
|
-
if instance.droplet.status.include? 'active'
|
30
|
-
Log.info "Droplet id: #{id} failed to snapshot. But it still running."
|
31
|
-
else
|
32
|
-
Digitalocean::Droplet.power_on(id)
|
33
|
-
Log.info "Droplet id: #{id} failed to snapshot. POWER ON has been requested."
|
34
|
-
end
|
24
|
+
def fail_power_off(e)
|
25
|
+
return unless e && e.id
|
26
|
+
api.start_droplet(e.id)
|
27
|
+
rescue
|
28
|
+
raise DropletFindError, e.message, e.backtrace
|
35
29
|
end
|
36
30
|
|
37
31
|
protected
|
38
32
|
|
39
33
|
attr_accessor :droplets, :mail, :smtp, :exclude, :only
|
40
|
-
attr_accessor :delay, :keep, :quiet, :stop, :clean
|
34
|
+
attr_accessor :delay, :timeout, :keep, :quiet, :stop, :clean
|
41
35
|
|
42
|
-
attr_writer :notify, :threads
|
36
|
+
attr_writer :notify, :threads, :api
|
37
|
+
|
38
|
+
def api
|
39
|
+
@api ||= API.new(delay: delay, timeout: timeout)
|
40
|
+
end
|
43
41
|
|
44
42
|
def notify
|
45
43
|
@notify ||= false
|
@@ -49,111 +47,101 @@ module DoSnapshot
|
|
49
47
|
@threads ||= []
|
50
48
|
end
|
51
49
|
|
50
|
+
# Working with list of droplets.
|
51
|
+
#
|
52
|
+
def work_with_droplets
|
53
|
+
load_droplets
|
54
|
+
dispatch_droplets
|
55
|
+
Log.debug 'Working with list of DigitalOcean droplets'
|
56
|
+
thread_chain
|
57
|
+
end
|
58
|
+
|
52
59
|
# Getting droplets list from API.
|
53
60
|
# And store into object.
|
54
61
|
#
|
55
62
|
def load_droplets
|
56
|
-
set_id
|
57
63
|
Log.debug 'Loading list of DigitalOcean droplets'
|
58
|
-
droplets =
|
59
|
-
fail droplets.message unless droplets.status.include? 'OK'
|
60
|
-
self.droplets = droplets.droplets
|
64
|
+
self.droplets = api.droplets.droplets
|
61
65
|
end
|
62
66
|
|
63
|
-
#
|
67
|
+
# Dispatch received droplets, each by each.
|
64
68
|
#
|
65
|
-
def
|
66
|
-
load_droplets
|
67
|
-
Log.debug 'Working with list of DigitalOcean droplets'
|
69
|
+
def dispatch_droplets
|
68
70
|
droplets.each do |droplet|
|
69
71
|
id = droplet.id.to_s
|
70
72
|
next if exclude.include? id
|
71
73
|
next if !only.empty? && !only.include?(id)
|
72
74
|
|
73
|
-
instance =
|
74
|
-
fail instance.message unless instance.status.include? 'OK'
|
75
|
+
instance = api.droplet id
|
75
76
|
|
76
77
|
prepare_instance instance.droplet
|
77
78
|
end
|
78
|
-
thread_chain
|
79
79
|
end
|
80
80
|
|
81
|
-
#
|
81
|
+
# Join threads
|
82
82
|
#
|
83
83
|
def thread_chain
|
84
84
|
threads.each { |t| t.join }
|
85
85
|
end
|
86
86
|
|
87
|
+
# Run threads
|
88
|
+
#
|
89
|
+
def thread_runner(instance)
|
90
|
+
threads << Thread.new do
|
91
|
+
Log.debug 'Shutting down droplet.'
|
92
|
+
stop_droplet instance
|
93
|
+
create_snapshot instance
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
87
97
|
# Preparing instance to take snapshot.
|
88
98
|
# Instance must be powered off first!
|
89
99
|
#
|
90
100
|
def prepare_instance(instance)
|
91
101
|
return unless instance
|
92
102
|
Log.info "Preparing droplet id: #{instance.id} name: #{instance.name} to take snapshot."
|
103
|
+
return if too_much_snapshots(instance)
|
104
|
+
thread_runner(instance)
|
105
|
+
end
|
93
106
|
|
94
|
-
|
95
|
-
|
96
|
-
if instance.snapshots.size >= keep
|
97
|
-
|
98
|
-
|
99
|
-
return
|
107
|
+
def too_much_snapshots(instance)
|
108
|
+
# noinspection RubyResolve
|
109
|
+
if instance.snapshots.size >= keep
|
110
|
+
warning_size(instance.id, instance.name, keep)
|
111
|
+
return true if stop
|
100
112
|
end
|
113
|
+
false
|
114
|
+
end
|
101
115
|
|
102
|
-
|
103
|
-
|
104
|
-
threads << Thread.new do
|
105
|
-
begin
|
106
|
-
unless instance.status.include? 'off'
|
107
|
-
event = Digitalocean::Droplet.power_off(instance.id)
|
108
|
-
if event.status.include? 'OK'
|
109
|
-
sleep delay until get_event_status(event.event_id)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
rescue => e
|
113
|
-
raise DropletShutdownError.new(instance.id), e.message, e.backtrace
|
114
|
-
end
|
115
|
-
|
116
|
-
# Create snapshot.
|
117
|
-
create_snapshot instance, warning_size
|
118
|
-
end
|
116
|
+
def stop_droplet(instance)
|
117
|
+
api.stop_droplet(instance.id) unless instance.status.include? 'off'
|
119
118
|
end
|
120
119
|
|
121
120
|
# Trying to create a snapshot.
|
122
121
|
#
|
123
|
-
def create_snapshot(instance
|
122
|
+
def create_snapshot(instance) # rubocop:disable MethodLength
|
124
123
|
Log.info "Start creating snapshot for droplet id: #{instance.id} name: #{instance.name}."
|
125
124
|
|
126
125
|
today = DateTime.now
|
127
126
|
name = "#{instance.name}_#{today.strftime('%Y_%m_%d')}"
|
128
|
-
|
127
|
+
# noinspection RubyResolve
|
129
128
|
snapshot_size = instance.snapshots.size
|
130
129
|
|
131
|
-
if !event
|
132
|
-
fail 'Something wrong with DigitalOcean or with your connection :)'
|
133
|
-
elsif event && !event.status.include?('OK')
|
134
|
-
fail event.message
|
135
|
-
end
|
136
|
-
|
137
130
|
Log.debug 'Wait until snapshot will be created.'
|
138
131
|
|
139
|
-
|
132
|
+
api.create_snapshot instance.id, name
|
140
133
|
|
141
134
|
snapshot_size += 1
|
142
135
|
|
143
136
|
Log.info "Snapshot name: #{name} created successfully."
|
144
137
|
Log.info "Droplet id: #{instance.id} name: #{instance.name} snapshots: #{snapshot_size}."
|
145
138
|
|
146
|
-
|
147
|
-
|
148
|
-
self.notify = true
|
149
|
-
|
150
|
-
# Cleanup snapshots.
|
151
|
-
cleanup_snapshots instance, (snapshot_size - keep - 1) if clean
|
152
|
-
end
|
139
|
+
# Cleanup snapshots.
|
140
|
+
cleanup_snapshots instance, snapshot_size if clean
|
153
141
|
rescue => e
|
154
|
-
case e.class
|
155
|
-
when SnapshotCleanupError
|
156
|
-
raise
|
142
|
+
case e.class.to_s
|
143
|
+
when 'DoSnapshot::SnapshotCleanupError'
|
144
|
+
raise e.class, e.message, e.backtrace
|
157
145
|
else
|
158
146
|
raise SnapshotCreateError.new(instance.id), e.message, e.backtrace
|
159
147
|
end
|
@@ -162,40 +150,21 @@ module DoSnapshot
|
|
162
150
|
# Cleanup our snapshots.
|
163
151
|
#
|
164
152
|
def cleanup_snapshots(instance, size)
|
165
|
-
|
153
|
+
return unless size > keep
|
166
154
|
|
167
|
-
(
|
168
|
-
snapshot = instance.snapshots[i]
|
169
|
-
event = Digitalocean::Image.destroy(snapshot.id)
|
155
|
+
warning_size(instance.id, instance.name, size)
|
170
156
|
|
171
|
-
|
172
|
-
fail 'Something wrong with DigitalOcean or with your connection :)'
|
173
|
-
elsif event && !event.status.include?('OK')
|
174
|
-
fail event.message
|
175
|
-
end
|
157
|
+
Log.debug "Cleaning up snapshots for droplet id: #{instance.id} name: #{instance.name}."
|
176
158
|
|
177
|
-
|
178
|
-
end
|
159
|
+
api.cleanup_snapshots(instance, size - keep - 1)
|
179
160
|
rescue => e
|
180
|
-
raise SnapshotCleanupError
|
161
|
+
raise SnapshotCleanupError, e.message, e.backtrace
|
181
162
|
end
|
182
163
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
def get_event_status(id)
|
188
|
-
event = Digitalocean::Event.find(id)
|
189
|
-
fail event.message unless event.status.include?('OK')
|
190
|
-
event.event.percentage && event.event.percentage.include?('100') ? true : false
|
191
|
-
end
|
192
|
-
|
193
|
-
# Set id's of Digital Ocean API.
|
194
|
-
#
|
195
|
-
def set_id
|
196
|
-
Log.debug 'Setting DigitalOcean Id\'s.'
|
197
|
-
Digitalocean.client_id = ENV['DIGITAL_OCEAN_CLIENT_ID']
|
198
|
-
Digitalocean.api_key = ENV['DIGITAL_OCEAN_API_KEY']
|
164
|
+
def warning_size(id, name, keep)
|
165
|
+
message = "For droplet with id: #{id} and name: #{name} the maximum number #{keep} of snapshots is reached."
|
166
|
+
Log.warning message
|
167
|
+
self.notify = true
|
199
168
|
end
|
200
169
|
end
|
201
170
|
end
|