do_snapshot 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|