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 CHANGED
@@ -1,7 +1,15 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e50e39afd9fac811be4207a14b617657b693cd82
4
- data.tar.gz: 6f986b2879036aa093088593a553c2e87eb0f6d6
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZjljNmNiZDVlYmJkYmMzYjE2ZDY4NWQ3ZTYyMzAwZWRiODdmZTEwMg==
5
+ data.tar.gz: !binary |-
6
+ YmVmZGFjMzIwNjZmZDY1NjA3M2QzMDI0OGFmZDJkYTk3NzM0NmE2ZA==
5
7
  SHA512:
6
- metadata.gz: 30c103ccbeb7b93993a0f675cb084c367aabf5f1bd6a7e996a16cd089ce1ad93048d421b8abd3e1cf815afd960bf891bd3ed9ae42d7cd5c85fc7df99e6e186e6
7
- data.tar.gz: 70d05a6af6884d49e78aa232a62976e76a0b6743f677f4b7ea201d325bc06c0ab61e4cd0e27d536dacb8134e21c09c885c8703f22f5bdcde65ed9cb8d49c2e30
8
+ metadata.gz: !binary |-
9
+ NGMzMjdiZTg5ODlmNzZiZWE2YzVjY2YwMDI4Y2U0YjE2ODQzYWQwZTI2N2U2
10
+ Mzc0MTUxZWUzODFkYjBkMGIyNGIwOWU3MGY2Y2EzNzJmNDgwNzQ1M2E2NWYx
11
+ MjBkYWY2MTIxNTNmMWZmN2U1MDMzNjBkNGIxODQwOWNiZGI5MmQ=
12
+ data.tar.gz: !binary |-
13
+ MTY2YmI2NWQ1ZWYxZWYwMGNjMjYzMzk1NjU5M2Q2YjM0NGNjNjU4ZGVjOTc2
14
+ YWZjZWEzMDYzZDE0Yjk4Njg3ZWEyNDQwZDVmODEzN2UwNjk1OTYyZmI4NGJj
15
+ MGQ4NGQzMGZhNjJhYTZiMzcyOGZhNjNlNWZmNTJjNjNiNDQwMTg=
data/.gitignore CHANGED
@@ -14,7 +14,10 @@ rdoc
14
14
  spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
- tmp
17
+ log/*
18
+ !log/.keep
19
+ tmp/*
20
+ !tmp/.keep
18
21
  *.bundle
19
22
  *.so
20
23
  *.o
@@ -22,4 +25,4 @@ tmp
22
25
  mkmf.log
23
26
  .idea
24
27
  .irb_history
25
-
28
+ .ruby-version
data/.rubocop.yml CHANGED
@@ -9,3 +9,6 @@ SingleSpaceBeforeFirstArg:
9
9
 
10
10
  TrailingComma:
11
11
  Enabled: false
12
+ AllCops:
13
+ Exclude:
14
+ - '*.gemspec'
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
4
+ - 1.9.3
5
+ - jruby-19mode
6
+ notifications:
7
+ email:
8
+ - api@mrcr.ru
9
+ script:
10
+ - bundle exec rubocop
data/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # DoSnapshot
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/do_snapshot.svg)](http://badge.fury.io/rb/do_snapshot)
4
+ [![Build Status](https://travis-ci.org/merqlove/do_snapshot.svg?branch=master)](https://travis-ci.org/merqlove/do_snapshot)
5
+ [![Dependency Status](https://gemnasium.com/merqlove/do_snapshot.svg)](https://gemnasium.com/merqlove/do_snapshot)
6
+ [![Coverage Status](https://coveralls.io/repos/merqlove/do_snapshot/badge.png?branch=master)](https://coveralls.io/r/merqlove/do_snapshot?branch=master)
7
+ [![Inline docs](http://inch-ci.org/github/merqlove/do_snapshot.png?branch=master)](http://inch-ci.org/github/merqlove/do_snapshot)
8
+ [![Code Climate](https://codeclimate.com/github/merqlove/do_snapshot.png)](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
@@ -1 +1,7 @@
1
+ # -*- encoding : utf-8 -*-
1
2
  require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
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
- $:.unshift File.expand_path('../../lib', bin_file)
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 = %q{Snapshot creator for Digital Ocean droplets. Multi-threading. Auto-cleanup. Cron optimized.}
12
- spec.description = %q{Snapshot creator for Digital Ocean droplets. Multi-threading inside. Auto-cleanup feature. No matter how much droplets you have. Cron optimized.}
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
- # Set multiple Errors with `id` @param
7
+ # Standard Request Exception. When we don't need droplet instance id.
12
8
  #
13
- class DigitalOceanError < StandardError
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
- # @param [Object] id
16
- def initialize(id)
17
- @id = 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
- # Shared logger
21
+ # Droplet must be powered off before snapshot operation!
25
22
  #
26
- class Log
27
- class << self
28
- attr_accessor :logger
29
- attr_accessor :shell
30
- attr_accessor :mail
31
- attr_accessor :quiet
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
- def notify_init
103
- mail[:subject] = 'Digital Ocean: maximum snapshots is reached.' unless mail[:subject]
104
- mail[:body] = "Please cleanup your Digital Ocean account.\nSnapshot maximum is reached." unless mail[:body]
105
- mail[:from] = 'noreply@someonelse.com' unless mail[:from]
106
- mail[:to] = 'to@someonelse.com' unless mail[:to]
107
- mail[:via] = :smtp unless mail[:via]
108
- mail[:body] = "#{mail[:body]}\n\nTrace: #{DateTime.now}\n#{buffer.join("\n")}"
109
- smtp[:domain] = 'localhost.localdomain' unless smtp[:domain]
110
- smtp[:port] = '25' unless smtp[:port]
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
- mail[:via_options] = smtp
113
- end
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
- attr_writer :buffer
116
- def buffer
117
- @buffer ||= %w()
118
- end
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