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 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