do_snapshot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7053ccac28ae155fac055356c0e2ef2e95373ffb
4
+ data.tar.gz: d315d1e4faa88f2b6562b64ab79d8480e01e25c5
5
+ SHA512:
6
+ metadata.gz: 77ca8773db485352055ca4118661451bcf1c55a538e248de7e7ed3eb44e6e8e5d6314ebb66645ed54f57f4fb31c76a21993000b1f97cc0f01223195cb3daeb80
7
+ data.tar.gz: 7d91bb1e124c979af67c26aed36c31e4d3e1d0f958b1ac01ca1f8e122fe8dba8e8e9e2585c1cd4d4973855e572470df7c7ea1a047b5b8e54c2fa754a3e80612a
data/.gitignore ADDED
@@ -0,0 +1,25 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ .idea
24
+ .irb_history
25
+
data/.rubocop.yml ADDED
@@ -0,0 +1,11 @@
1
+ LineLength:
2
+ Max: 200
3
+
4
+ HashSyntax:
5
+ EnforcedStyle: ruby19
6
+
7
+ SingleSpaceBeforeFirstArg:
8
+ Enabled: false
9
+
10
+ TrailingComma:
11
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in do_snapshot.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Alexander Merkulov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # DoSnapshot
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'do_snapshot'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install do_snapshot
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it ( https://github.com/[my-github-username]/do_snapshot/fork )
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/bin/do_snapshot ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # resolve bin path, ignoring symlinks
4
+ require 'pathname'
5
+ bin_file = Pathname.new(__FILE__).realpath
6
+
7
+ # add self to libpath
8
+ $:.unshift File.expand_path('../../lib', bin_file)
9
+
10
+ require 'do_snapshot/cli'
11
+
12
+ DoSnapshot::CLI.start(ARGV)
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'do_snapshot/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'do_snapshot'
8
+ spec.version = DoSnapshot::VERSION
9
+ spec.authors = ['Alexander Merkulov']
10
+ spec.email = ['sasha@merqlove.ru']
11
+ spec.summary = %q{Snapshot runner for Digital Ocean droplets. Use it with Cron or other tools.}
12
+ spec.description = %q{Snapshot runner for Digital Ocean droplets. Use it with Cron or other tools.}
13
+ spec.homepage = 'http://github.com/merqlove/do_snapshot'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_dependency 'digitalocean', '~> 1.2'
22
+ spec.add_dependency 'thor'
23
+ spec.add_dependency 'pony'
24
+
25
+ spec.add_development_dependency 'bundler', '~> 1.6'
26
+ spec.add_development_dependency 'rake'
27
+ end
@@ -0,0 +1,46 @@
1
+ require 'do_snapshot/version'
2
+ require 'logger'
3
+
4
+
5
+ # Used primary for creating snapshot's as backups for DigitalOcean
6
+ #
7
+ module DoSnapshot
8
+ # Shared logger
9
+ #
10
+ class Log
11
+ class << self
12
+ attr_accessor :logger
13
+ attr_accessor :thor_log
14
+
15
+ def log(type, message)
16
+ logger.send(type, message) if logger
17
+ end
18
+
19
+ def info(message)
20
+ log :info, message
21
+ say message, :green
22
+ end
23
+
24
+ def warning(message)
25
+ log :warn, message
26
+ say message, :yellow
27
+ end
28
+
29
+ def error(message)
30
+ log :error, message
31
+ say message, :red
32
+ end
33
+
34
+ def debug(message)
35
+ log :debug, message
36
+ say message, :white if logger && logger.level == Logger::DEBUG
37
+ end
38
+
39
+ protected
40
+
41
+ def say(message, color)
42
+ thor_log.say message, color if thor_log
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,73 @@
1
+ require 'do_snapshot'
2
+ require 'do_snapshot/command'
3
+ require 'thor'
4
+
5
+ module DoSnapshot
6
+ # CLI is here
7
+ #
8
+ class CLI < Thor
9
+ default_task :create
10
+
11
+ def initialize(*args)
12
+ super
13
+ if options.include? 'log'
14
+ Log.logger = Logger.new(options['log'])
15
+ Log.thor_log = Thor::Shell::Color.new
16
+ Log.logger.level = options.include?('trace') ? Logger::DEBUG : Logger::INFO
17
+ end
18
+ try_keys_first
19
+ end
20
+
21
+ desc 'create', 'create and cleanup snapshot\'s'
22
+ long_desc <<-LONGDESC
23
+ `do_snapshot create` will create and cleanup snapshots on your droplets.
24
+
25
+ You can optionally specify parameters to select or exclude some droplets.
26
+
27
+ Advanced options example for MAIL feature:
28
+
29
+ --mail to:mail@somehost.com from:from@host.com --smtp address:smtp.gmail.com user_name:someuser password:somepassword
30
+
31
+ For more details look here: https://github.com/benprew/pony
32
+
33
+ Example:
34
+
35
+ > $ do_snapshot --keep 5
36
+
37
+ > $ do_snapshot --only 123456 1234567 --store 3
38
+
39
+ > $ do_snapshot --exclude 123456 123457
40
+
41
+ > $ do_snapshot --keep 10 --stop true --mail to:yourmail@example.com
42
+ LONGDESC
43
+ option :only, type: :array, default: [], aliases: ['-o'], banner: '123456 123456 123456', desc: 'Use only selected droplets.'
44
+ option :exclude, type: :array, default: [], aliases: ['-e'], banner: '123456 123456 123456', desc: 'Except some droplets.'
45
+ option :keep, type: :numeric, default: 10, aliases: ['-k'], banner: '5', desc: 'How much snapshots you want to keep?'
46
+ option :stop, type: :boolean, aliases: ['-s'], banner: 'true', desc: 'Stop creating snapshots if maximum is reached.'
47
+ option :mail, type: :hash, default: {}, aliases: ['-m'], banner: 'to:yourmail@example.com', desc: 'Receive mail if maximum is reached.'
48
+ option :smtp, type: :hash, default: {}, aliases: ['-t'], banner: 'user_name:yourmail@example.com password:password', desc: 'SMTP options.'
49
+ option :log, type: :string, aliases: ['-l'], banner: '/Users/someone/.do_snapshot/main.log', desc: 'Log file path. By default logging is disabled.'
50
+ option :trace, type: :boolean, aliases: ['-d'], desc: 'Debug mode. Not implented now. Switched only log mode.'
51
+ def create
52
+ DoSnapshot::Command.execute options, %w( log trace )
53
+ rescue => e
54
+ Log.error e.message
55
+ backtrace(e) if options.include? 'trace'
56
+ end
57
+
58
+ protected
59
+
60
+ def backtrace(e)
61
+ e.backtrace.each do |t|
62
+ Log.error t
63
+ end
64
+ end
65
+
66
+ # Check DigitalOcean API keys
67
+ def try_keys_first
68
+ Log.debug 'Checking DigitalOcean Id\'s.'
69
+ fail Thor::Error, 'You must have DIGITAL_OCEAN_CLIENT_ID in environment.' if !ENV['DIGITAL_OCEAN_CLIENT_ID'] || ENV['DIGITAL_OCEAN_CLIENT_ID'].empty?
70
+ fail Thor::Error, 'You must have DIGITAL_OCEAN_API_KEY in environment.' if !ENV['DIGITAL_OCEAN_API_KEY'] || ENV['DIGITAL_OCEAN_API_KEY'].empty?
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,166 @@
1
+ require 'digitalocean'
2
+ require 'date'
3
+ require 'pony'
4
+ require 'core_ext/hash'
5
+
6
+ module DoSnapshot
7
+ # Our commands live here :)
8
+ #
9
+ class Command
10
+ class << self
11
+ def execute(options, skip)
12
+ return unless options
13
+
14
+ options.each_pair do |key, option|
15
+ send("#{key}=", option) unless skip.include? key
16
+ end
17
+
18
+ self.notify = false
19
+
20
+ work_droplets
21
+ email_message if notify && mail
22
+
23
+ Log.info 'All snapshots requested'
24
+ end
25
+
26
+ protected
27
+
28
+ attr_accessor :droplets
29
+ attr_accessor :exclude
30
+ attr_accessor :only
31
+ attr_accessor :keep
32
+ attr_accessor :mail
33
+ attr_accessor :smtp
34
+ attr_accessor :stop
35
+ attr_accessor :notify
36
+
37
+ # Getting droplets list from API.
38
+ # And store into object.
39
+ #
40
+ def load_droplets
41
+ set_id
42
+ Log.debug 'Loading list of DigitalOcean droplets'
43
+ droplets = Digitalocean::Droplet.all
44
+ fail droplets.message unless droplets.status == 'OK'
45
+ self.droplets = droplets.droplets
46
+ end
47
+
48
+ # Working with received list of droplets.
49
+ #
50
+ def work_droplets
51
+ load_droplets
52
+ Log.debug 'Working with list of DigitalOcean droplets'
53
+ droplets.each do |droplet|
54
+ id = droplet.id.to_s
55
+ next if exclude.include? id
56
+ next if !only.empty? && !only.include?(id)
57
+
58
+ instance = Digitalocean::Droplet.find(id)
59
+ fail instance.message unless instance.status == 'OK'
60
+
61
+ prepare_instance instance.droplet
62
+ end
63
+ end
64
+
65
+ # Preparing instance to take snapshot.
66
+ # Instance must be powered off first!
67
+ #
68
+ def prepare_instance(instance)
69
+ return unless instance
70
+ Log.debug "Preparing droplet id: #{instance.id} name: #{instance.name} to snapshot."
71
+
72
+ warning_size = "For droplet with id: #{instance.id} and name: #{instance.name} the maximum #{keep} is reached."
73
+
74
+ if instance.snapshots.size >= keep && stop
75
+ Log.warning warning_size
76
+ self.notify = true
77
+ return
78
+ end
79
+
80
+ # Stopping instance.
81
+ Log.debug 'Shutting down droplet.'
82
+ unless instance.status.include? 'off'
83
+ event = Digitalocean::Droplet.power_off(instance.id)
84
+ if event.status.include? 'OK'
85
+ sleep 1.3 until get_event_status(event.event_id)
86
+ end
87
+ end
88
+
89
+ # Create snapshot.
90
+ create_snapshot instance, warning_size
91
+ end
92
+
93
+ # Trying to create a snapshot.
94
+ #
95
+ def create_snapshot(instance, warning_size)
96
+ Log.debug "Start creating snapshot for droplet id: #{instance.id} name: #{instance.name}."
97
+
98
+ today = DateTime.now
99
+ name = "#{instance.name}_#{today.strftime('%Y_%m_%d')}"
100
+ event = Digitalocean::Droplet.snapshot(instance.id, name: name)
101
+ snapshot_size = instance.snapshots.size
102
+
103
+ if !event
104
+ fail 'Something wrong with DigitalOcean or with your connection :)'
105
+ elsif event && !event.status.include?('OK')
106
+ fail event.message
107
+ end
108
+
109
+ Log.debug 'Wait until snapshot will be created.'
110
+
111
+ sleep 10 until get_event_status(event.event_id)
112
+
113
+ snapshot_size += 1
114
+
115
+ Log.info "Snapshot name: #{name} created successfully."
116
+ Log.info "Droplet id: #{instance.id} name: #{instance.name} snapshots: #{snapshot_size}."
117
+
118
+ if snapshot_size > keep
119
+ Log.warning warning_size if snapshot_size > keep
120
+ self.notify = true
121
+ end
122
+ end
123
+
124
+ # Looking for event status.
125
+ #
126
+ # Before snapshot we to know that machine has powered off.
127
+ #
128
+ def get_event_status(id)
129
+ event = Digitalocean::Event.find(id)
130
+ fail event.message unless event.status.include?('OK')
131
+ event.event.percentage && event.event.percentage.include?('100') ? true : false
132
+ end
133
+
134
+ # Sending message via Hash params.
135
+ #
136
+ # Options:: -m to:mail@somehost.com from:from@host.com -t address:smtp.gmail.com user_name:someuser password:somepassword
137
+ #
138
+ def email_message
139
+ mail.symbolize_keys!
140
+ smtp.symbolize_keys!
141
+
142
+ mail[:subject] = 'DigitalOcean Snapshot maximum is reached.' unless mail[:subject]
143
+ mail[:body] = "Please cleanup your Digital Ocean account. \nSnapshot maximum is reached." unless mail[:body]
144
+ mail[:from] = 'noreply@someonelse.com' unless mail[:from]
145
+ mail[:via] = :smtp unless mail[:via]
146
+
147
+ smtp[:domain] = 'localhost.localdomain' unless smtp[:domain]
148
+ smtp[:port] = '25' unless smtp[:port]
149
+
150
+ mail[:via_options] = smtp
151
+
152
+ Log.debug 'Sending e-mail notification.'
153
+ # Look into your inbox :)
154
+ Pony.mail(mail)
155
+ end
156
+
157
+ # Set id's of Digital Ocean API.
158
+ #
159
+ def set_id
160
+ Log.debug 'Setting DigitalOcean Id\'s.'
161
+ Digitalocean.client_id = ENV['DIGITAL_OCEAN_CLIENT_ID']
162
+ Digitalocean.api_key = ENV['DIGITAL_OCEAN_API_KEY']
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,36 @@
1
+ # Rails symbolize keys methods for Hash
2
+ #
3
+ class Hash
4
+ # Returns a new hash with all keys converted using the block operation.
5
+ #
6
+ # hash = { name: 'Rob', age: '28' }
7
+ #
8
+ # hash.transform_keys{ |key| key.to_s.upcase }
9
+ # # => {"NAME"=>"Rob", "AGE"=>"28"}
10
+ def transform_keys
11
+ result = {}
12
+ each_key do |key|
13
+ result[yield(key)] = self[key]
14
+ end
15
+ result
16
+ end
17
+
18
+ # Destructively convert all keys using the block operations.
19
+ # Same as transform_keys but modifies +self+.
20
+ def transform_keys!
21
+ keys.each do |key|
22
+ self[yield(key)] = delete(key)
23
+ end
24
+ self
25
+ end
26
+
27
+ def symbolize_keys
28
+ transform_keys { |key| key.to_sym rescue key }
29
+ end
30
+
31
+ # Destructively convert all keys to symbols, as long as they respond
32
+ # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
33
+ def symbolize_keys!
34
+ transform_keys! { |key| key.to_sym rescue key }
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # Current version
2
+ #
3
+ module DoSnapshot
4
+ VERSION = '0.0.1'
5
+ end
data/test/.keep ADDED
File without changes
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: do_snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Merkulov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-07-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: digitalocean
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pony
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.6'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Snapshot runner for Digital Ocean droplets. Use it with Cron or other
84
+ tools.
85
+ email:
86
+ - sasha@merqlove.ru
87
+ executables:
88
+ - do_snapshot
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".gitignore"
93
+ - ".rubocop.yml"
94
+ - ".ruby-version"
95
+ - Gemfile
96
+ - LICENSE.txt
97
+ - README.md
98
+ - Rakefile
99
+ - bin/do_snapshot
100
+ - do_snapshot.gemspec
101
+ - lib/do_snapshot.rb
102
+ - lib/do_snapshot/cli.rb
103
+ - lib/do_snapshot/command.rb
104
+ - lib/do_snapshot/core_ext/hash.rb
105
+ - lib/do_snapshot/version.rb
106
+ - test/.keep
107
+ homepage: http://github.com/merqlove/do_snapshot
108
+ licenses:
109
+ - MIT
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.2.2
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Snapshot runner for Digital Ocean droplets. Use it with Cron or other tools.
131
+ test_files:
132
+ - test/.keep