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
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZjljNmNiZDVlYmJkYmMzYjE2ZDY4NWQ3ZTYyMzAwZWRiODdmZTEwMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YmVmZGFjMzIwNjZmZDY1NjA3M2QzMDI0OGFmZDJkYTk3NzM0NmE2ZA==
|
5
7
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NGMzMjdiZTg5ODlmNzZiZWE2YzVjY2YwMDI4Y2U0YjE2ODQzYWQwZTI2N2U2
|
10
|
+
Mzc0MTUxZWUzODFkYjBkMGIyNGIwOWU3MGY2Y2EzNzJmNDgwNzQ1M2E2NWYx
|
11
|
+
MjBkYWY2MTIxNTNmMWZmN2U1MDMzNjBkNGIxODQwOWNiZGI5MmQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
MTY2YmI2NWQ1ZWYxZWYwMGNjMjYzMzk1NjU5M2Q2YjM0NGNjNjU4ZGVjOTc2
|
14
|
+
YWZjZWEzMDYzZDE0Yjk4Njg3ZWEyNDQwZDVmODEzN2UwNjk1OTYyZmI4NGJj
|
15
|
+
MGQ4NGQzMGZhNjJhYTZiMzcyOGZhNjNlNWZmNTJjNjNiNDQwMTg=
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# DoSnapshot
|
2
2
|
|
3
|
+
[](http://badge.fury.io/rb/do_snapshot)
|
4
|
+
[](https://travis-ci.org/merqlove/do_snapshot)
|
5
|
+
[](https://gemnasium.com/merqlove/do_snapshot)
|
6
|
+
[](https://coveralls.io/r/merqlove/do_snapshot?branch=master)
|
7
|
+
[](http://inch-ci.org/github/merqlove/do_snapshot)
|
8
|
+
[](https://codeclimate.com/github/merqlove/do_snapshot)
|
9
|
+
|
3
10
|
You can use this gem to backup's DigitalOcean droplet's via snapshot method.
|
4
11
|
|
5
12
|
Here some features:
|
@@ -10,6 +17,7 @@ Here some features:
|
|
10
17
|
- Mail notifications when fail or maximum of snapshots is reached for one or multiple droplets.
|
11
18
|
- Custom mail settings (You can set [Pony](https://github.com/benprew/pony) mail settings).
|
12
19
|
- Stop mode (when you don't want to create new snapshots when maximum is reached).
|
20
|
+
- Timeout for bad requests & uncaught loops.
|
13
21
|
- Logging into selected directory.
|
14
22
|
- Verbose mode for research.
|
15
23
|
- Quiet mode for silence.
|
@@ -20,6 +28,10 @@ There not so much of dependencies:
|
|
20
28
|
- `Thor` for CLI.
|
21
29
|
- `Pony` for mail notifications.
|
22
30
|
|
31
|
+
## Compatibility
|
32
|
+
|
33
|
+
Ruby versions: 1.9.3 and higher.
|
34
|
+
|
23
35
|
## Installation
|
24
36
|
|
25
37
|
Add this line to your application's Gemfile:
|
@@ -104,6 +116,10 @@ For working mailer you need to set e-mail settings via run options.
|
|
104
116
|
-e, [--exclude=123456 123456 123456] # Except some droplets.
|
105
117
|
-k, [--keep=5] # How much snapshots you want to keep?
|
106
118
|
# Default: 10
|
119
|
+
-d, [--delay=5] # Delay between snapshot operation status requests.
|
120
|
+
# Default: 10
|
121
|
+
[--timeout=250] # Timeout in sec's for events like Power Off or Create Snapshot.
|
122
|
+
# Default: 180
|
107
123
|
-m, [--mail=to:yourmail@example.com] # Receive mail if fail or maximum is reached.
|
108
124
|
-t, [--smtp=user_name:yourmail@example.com password:password] # SMTP options.
|
109
125
|
-l, [--log=/Users/someone/.do_snapshot/main.log] # Log file path. By default logging is disabled.
|
data/Rakefile
CHANGED
data/bin/do_snapshot
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# -*- encoding : utf-8 -*-
|
3
|
+
|
4
|
+
Signal.trap('INT') { exit 1 }
|
2
5
|
|
3
6
|
# resolve bin path, ignoring symlinks
|
4
7
|
require 'pathname'
|
5
8
|
bin_file = Pathname.new(__FILE__).realpath
|
6
9
|
|
7
10
|
# add self to libpath
|
8
|
-
|
11
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', bin_file)
|
9
12
|
|
10
13
|
require 'do_snapshot/cli'
|
11
14
|
|
data/do_snapshot.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = DoSnapshot::VERSION
|
9
9
|
spec.authors = ['Alexander Merkulov']
|
10
10
|
spec.email = ['sasha@merqlove.ru']
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
11
|
+
spec.summary = 'Snapshot creator for Digital Ocean droplets. Multi-threading. Auto-cleanup. Cron optimized.'
|
12
|
+
spec.description = 'Snapshot creator for Digital Ocean droplets. Multi-threading inside. Auto-cleanup feature. No matter how much droplets you have. Cron optimized.'
|
13
13
|
spec.homepage = 'http://github.com/merqlove/do_snapshot'
|
14
14
|
spec.license = 'MIT'
|
15
15
|
|
@@ -19,9 +19,15 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
21
|
spec.add_dependency 'digitalocean', '~> 1.2'
|
22
|
-
spec.add_dependency 'thor'
|
23
|
-
spec.add_dependency 'pony'
|
22
|
+
spec.add_dependency 'thor', '~> 0.19.1'
|
23
|
+
spec.add_dependency 'pony', '~> 1.1.0'
|
24
24
|
|
25
25
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
26
26
|
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'rubocop'
|
28
|
+
spec.add_development_dependency 'rspec-core', '~> 3.0.2'
|
29
|
+
spec.add_development_dependency 'rspec-expectations', '~> 3.0.2'
|
30
|
+
spec.add_development_dependency 'rspec-mocks', '~> 3.0.2'
|
31
|
+
spec.add_development_dependency 'webmock', '~> 1.18.0'
|
32
|
+
spec.add_development_dependency 'coveralls', '~> 0.7.0'
|
27
33
|
end
|
data/lib/do_snapshot.rb
CHANGED
@@ -1,121 +1,64 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
1
2
|
require 'do_snapshot/version'
|
2
|
-
require 'thor'
|
3
|
-
require 'logger'
|
4
|
-
require 'date'
|
5
|
-
require 'pony'
|
6
|
-
require 'do_snapshot/core_ext/hash'
|
7
3
|
|
8
4
|
# Used primary for creating snapshot's as backups for DigitalOcean
|
9
5
|
#
|
10
6
|
module DoSnapshot
|
11
|
-
#
|
7
|
+
# Standard Request Exception. When we don't need droplet instance id.
|
12
8
|
#
|
13
|
-
class
|
9
|
+
class RequestError < StandardError; end
|
10
|
+
|
11
|
+
# Base Exception for cases when we need id for log and/or something actions.
|
12
|
+
#
|
13
|
+
class RequestActionError < RequestError
|
14
14
|
attr_reader :id
|
15
|
-
|
16
|
-
def initialize(
|
17
|
-
@id =
|
15
|
+
|
16
|
+
def initialize(*args)
|
17
|
+
@id = args[0]
|
18
18
|
end
|
19
19
|
end
|
20
|
-
class DropletShutdownError < DigitalOceanError; end
|
21
|
-
class SnapshotCreateError < DigitalOceanError; end
|
22
|
-
class SnapshotCleanupError < DigitalOceanError; end
|
23
20
|
|
24
|
-
#
|
21
|
+
# Droplet must be powered off before snapshot operation!
|
25
22
|
#
|
26
|
-
class
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
attr_accessor :verbose
|
33
|
-
attr_writer :smtp
|
34
|
-
|
35
|
-
def smtp
|
36
|
-
@smtp ||= {}
|
37
|
-
end
|
38
|
-
|
39
|
-
def log(type, message)
|
40
|
-
buffer << message
|
41
|
-
logger.send(type, message) if logger
|
42
|
-
|
43
|
-
say message, color(type) unless type == :debug && !debug?
|
44
|
-
end
|
45
|
-
|
46
|
-
def color(type)
|
47
|
-
case type
|
48
|
-
when :debug
|
49
|
-
:white
|
50
|
-
when :error
|
51
|
-
:red
|
52
|
-
when :warn
|
53
|
-
:yellow
|
54
|
-
else
|
55
|
-
:green
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def info(message)
|
60
|
-
log :info, message
|
61
|
-
end
|
62
|
-
|
63
|
-
def warning(message)
|
64
|
-
log :warn, message
|
65
|
-
end
|
66
|
-
|
67
|
-
def error(message)
|
68
|
-
log :error, message
|
69
|
-
end
|
70
|
-
|
71
|
-
def debug(message)
|
72
|
-
log :debug, message
|
73
|
-
end
|
74
|
-
|
75
|
-
def say(message, color)
|
76
|
-
shell.say message, color if shell
|
77
|
-
end
|
78
|
-
|
79
|
-
def debug?
|
80
|
-
logger && logger.level == Logger::DEBUG
|
81
|
-
end
|
82
|
-
|
83
|
-
# Sending message via Hash params.
|
84
|
-
#
|
85
|
-
# Options:: --mail to:mail@somehost.com from:from@host.com --smtp address:smtp.gmail.com user_name:someuser password:somepassword
|
86
|
-
#
|
87
|
-
def notify
|
88
|
-
return unless mail
|
89
|
-
|
90
|
-
mail.symbolize_keys!
|
91
|
-
smtp.symbolize_keys!
|
92
|
-
|
93
|
-
notify_init
|
94
|
-
|
95
|
-
Log.debug 'Sending e-mail notification.'
|
96
|
-
# Look into your inbox :)
|
97
|
-
Pony.mail(mail)
|
98
|
-
end
|
99
|
-
|
100
|
-
protected
|
23
|
+
class DropletShutdownError < RequestActionError
|
24
|
+
def initialize(*args)
|
25
|
+
Log.error "Droplet id: #{args[0]} is Failed to Power Off."
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
101
29
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
30
|
+
# When snapshot create operation is failed.
|
31
|
+
# It can be because of something wrong with droplet or Digital Ocean API.
|
32
|
+
#
|
33
|
+
class SnapshotCreateError < RequestActionError
|
34
|
+
def initialize(*args)
|
35
|
+
Log.error "Droplet id: #{args[0]} is Failed to Snapshot."
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
111
39
|
|
112
|
-
|
113
|
-
|
40
|
+
# When Digital Ocean API say us that not found droplet by id.
|
41
|
+
# Or something wrong happened.
|
42
|
+
#
|
43
|
+
class DropletFindError < RequestError
|
44
|
+
def initialize(*args)
|
45
|
+
Log.error 'Droplet Not Found'
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
114
49
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
50
|
+
# When Digital Ocean API cannot retrieve list of droplets.
|
51
|
+
# Sometimes it connection problem or DigitalOcean API maintenance.
|
52
|
+
#
|
53
|
+
class DropletListError < RequestError
|
54
|
+
def initialize(*args)
|
55
|
+
Log.error 'Droplet Listing is failed to retrieve'
|
56
|
+
super
|
119
57
|
end
|
120
58
|
end
|
59
|
+
|
60
|
+
# When Digital Ocean API cannot remove old images.
|
61
|
+
# Sometimes it connection problem or DigitalOcean API maintenance.
|
62
|
+
#
|
63
|
+
class SnapshotCleanupError < RequestError; end
|
121
64
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
require 'digitalocean'
|
3
|
+
|
4
|
+
module DoSnapshot
|
5
|
+
# API for CLI commands
|
6
|
+
# Operating with Digital Ocean.
|
7
|
+
#
|
8
|
+
class API
|
9
|
+
attr_accessor :delay
|
10
|
+
attr_accessor :timeout
|
11
|
+
|
12
|
+
def initialize(options)
|
13
|
+
set_id
|
14
|
+
options.each_pair do |key, option|
|
15
|
+
send("#{key}=", option)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get single droplet from DigitalOcean
|
20
|
+
#
|
21
|
+
def droplet(id)
|
22
|
+
# noinspection RubyResolve
|
23
|
+
instance = Digitalocean::Droplet.find(id)
|
24
|
+
fail DropletFindError, instance.message unless instance.status.include? 'OK'
|
25
|
+
instance
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get droplets list from DigitalOcean
|
29
|
+
#
|
30
|
+
def droplets
|
31
|
+
# noinspection RubyResolve
|
32
|
+
droplets = Digitalocean::Droplet.all
|
33
|
+
fail DropletListError, droplets.message unless droplets.status.include? 'OK'
|
34
|
+
droplets
|
35
|
+
end
|
36
|
+
|
37
|
+
# Power On request for Droplet
|
38
|
+
#
|
39
|
+
def start_droplet(id)
|
40
|
+
# noinspection RubyResolve
|
41
|
+
instance = Digitalocean::Droplet.find(id)
|
42
|
+
|
43
|
+
fail unless instance.status.include? 'OK'
|
44
|
+
|
45
|
+
if instance.droplet.status.include? 'active'
|
46
|
+
Log.error 'Droplet is still running.'
|
47
|
+
else
|
48
|
+
power_on id
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Power Off request for Droplet
|
53
|
+
#
|
54
|
+
def stop_droplet(id)
|
55
|
+
# noinspection RubyResolve,RubyResolve
|
56
|
+
event = Digitalocean::Droplet.power_off(id)
|
57
|
+
|
58
|
+
fail event.message unless event.status.include? 'OK'
|
59
|
+
|
60
|
+
# noinspection RubyResolve
|
61
|
+
wait_event(event.event_id)
|
62
|
+
rescue => e
|
63
|
+
raise DropletShutdownError.new(id), e.message, e.backtrace
|
64
|
+
end
|
65
|
+
|
66
|
+
# Sending event to create snapshot via DigitalOcean API and wait for success
|
67
|
+
#
|
68
|
+
def create_snapshot(id, name)
|
69
|
+
# noinspection RubyResolve,RubyResolve
|
70
|
+
event = Digitalocean::Droplet.snapshot(id, name: name)
|
71
|
+
|
72
|
+
if !event
|
73
|
+
fail 'Something wrong with DigitalOcean or with your connection :)'
|
74
|
+
elsif event && !event.status.include?('OK')
|
75
|
+
fail event.message
|
76
|
+
end
|
77
|
+
|
78
|
+
# noinspection RubyResolve
|
79
|
+
wait_event(event.event_id)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Cleanup our snapshots.
|
83
|
+
#
|
84
|
+
def cleanup_snapshots(instance, size) # rubocop:disable MethodLength
|
85
|
+
(0..size).each do |i|
|
86
|
+
# noinspection RubyResolve
|
87
|
+
snapshot = instance.snapshots[i]
|
88
|
+
event = Digitalocean::Image.destroy(snapshot.id)
|
89
|
+
|
90
|
+
if !event
|
91
|
+
Log.error "Destroy of snapshot #{snapshot.name} for droplet id: #{instance.id} name: #{instance.name} is failed."
|
92
|
+
elsif event && !event.status.include?('OK')
|
93
|
+
Log.error event.message
|
94
|
+
else
|
95
|
+
Log.debug "Snapshot name: #{snapshot.name} delete requested."
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
# Set id's of Digital Ocean API.
|
103
|
+
#
|
104
|
+
def set_id
|
105
|
+
Log.debug 'Setting DigitalOcean Id\'s.'
|
106
|
+
Digitalocean.client_id = ENV['DIGITAL_OCEAN_CLIENT_ID']
|
107
|
+
Digitalocean.api_key = ENV['DIGITAL_OCEAN_API_KEY']
|
108
|
+
end
|
109
|
+
|
110
|
+
# Waiting for event exit
|
111
|
+
def wait_event(id)
|
112
|
+
time = Time.now.to_f
|
113
|
+
sleep delay until get_event_status(id, time)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Looking for event status.
|
117
|
+
# Before snapshot we to know that machine has powered off.
|
118
|
+
#
|
119
|
+
def get_event_status(id, time)
|
120
|
+
return true if (Time.now.to_f - time) > timeout
|
121
|
+
event = Digitalocean::Event.find(id)
|
122
|
+
fail event.message unless event.status.include?('OK')
|
123
|
+
# noinspection RubyResolve,RubyResolve
|
124
|
+
event.event.percentage && event.event.percentage.include?('100') ? true : false
|
125
|
+
end
|
126
|
+
|
127
|
+
# Request Power On for droplet
|
128
|
+
#
|
129
|
+
def power_on(id)
|
130
|
+
# noinspection RubyResolve
|
131
|
+
event = Digitalocean::Droplet.power_on(id)
|
132
|
+
case event && event.status
|
133
|
+
when 'OK'
|
134
|
+
Log.info 'Power On has been requested.'
|
135
|
+
else
|
136
|
+
Log.error 'Power On failed to request.'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|