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.
@@ -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
- Log.quiet = options['quiet']
17
- # Use Thor shell
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
- ENV['DIGITAL_OCEAN_CLIENT_ID'] = options['digital_ocean_client_id'] if options.include? 'digital_ocean_client_id'
28
- ENV['DIGITAL_OCEAN_API_KEY'] = options['digital_ocean_api_key'] if options.include? 'digital_ocean_api_key'
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
- method_option :only, type: :array, default: [], aliases: %w( -o ), banner: '123456 123456 123456', desc: 'Select some droplets.'
71
- method_option :exclude, type: :array, default: [], aliases: %w( -e ), banner: '123456 123456 123456', desc: 'Except some droplets.'
72
- method_option :keep, type: :numeric, default: 10, aliases: %w( -k ), banner: '5', desc: 'How much snapshots you want to keep?'
73
- method_option :delay, type: :numeric, default: 10, aliases: %w( -d ), banner: '5', desc: 'Delay between snapshot operation status requests.'
74
- method_option :mail, type: :hash, aliases: %w( -m ), banner: 'to:yourmail@example.com', desc: 'Receive mail if fail or maximum is reached.'
75
- method_option :smtp, type: :hash, aliases: %w( -t ), banner: 'user_name:yourmail@example.com password:password', desc: 'SMTP options.'
76
- method_option :log, type: :string, aliases: %w( -l ), banner: '/Users/someone/.do_snapshot/main.log', desc: 'Log file path. By default logging is disabled.'
77
- method_option :clean, type: :boolean, aliases: %w( -c ), desc: 'Cleanup snapshots after create. If you have more images than you want to `keep`, older will be deleted.'
78
- method_option :stop, type: :boolean, aliases: %w( -s), desc: 'Stop creating snapshots if maximum is reached.'
79
- method_option :trace, type: :boolean, aliases: %w( -v ), desc: 'Verbose mode.'
80
- method_option :quiet, type: :boolean, aliases: %w( -q ), desc: 'Quiet mode. If don\'t need any messages and in console.'
81
-
82
- method_option :digital_ocean_client_id, type: :string, banner: 'YOURLONGAPICLIENTID', desc: 'DIGITAL_OCEAN_CLIENT_ID. if you can\'t use environment.'
83
- method_option :digital_ocean_api_key, type: :string, banner: 'YOURLONGAPIKEY', desc: 'DIGITAL_OCEAN_API_KEY. if you can\'t use environment.'
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.execute options, %w( log trace digital_ocean_client_id digital_ocean_api_key )
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 Log.mail
93
- Log.mail[:subject] = 'Digital Ocean: Error.'
94
- Log.mail[:body] = 'Please check your droplets.'
95
- Log.notify
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
- protected
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
- def logger
107
- Log.logger = Logger.new(options['log'])
108
- Log.logger.level = Log.verbose ? Logger::DEBUG : Logger::INFO
109
- end
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
- def backtrace(e)
112
- e.backtrace.each do |t|
113
- Log.error t
176
+ def backtrace(e)
177
+ e.backtrace.each do |t|
178
+ Log.error t
179
+ end
114
180
  end
115
- end
116
181
 
117
- # Check for DigitalOcean API keys
118
- def try_keys_first
119
- Log.debug 'Checking DigitalOcean Id\'s.'
120
- fail Thor::Error, 'You must have DIGITAL_OCEAN_CLIENT_ID in environment or set via options.' if !ENV['DIGITAL_OCEAN_CLIENT_ID'] || ENV['DIGITAL_OCEAN_CLIENT_ID'].empty?
121
- fail Thor::Error, 'You must have DIGITAL_OCEAN_API_KEY in environment or set via options.' if !ENV['DIGITAL_OCEAN_API_KEY'] || ENV['DIGITAL_OCEAN_API_KEY'].empty?
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
@@ -1,12 +1,13 @@
1
- require 'digitalocean'
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 execute(options, skip)
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
- work_droplets
18
+ work_with_droplets
18
19
  Log.info 'All operations has been finished.'
19
20
 
20
- Log.notify if notify && !quiet
21
+ Mail.notify if notify && !quiet
21
22
  end
22
23
 
23
- def fail_power_on(id)
24
- return unless id
25
-
26
- set_id
27
- instance = Digitalocean::Droplet.find(id)
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 = Digitalocean::Droplet.all
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
- # Working with received list of droplets.
67
+ # Dispatch received droplets, each by each.
64
68
  #
65
- def work_droplets
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 = Digitalocean::Droplet.find(id)
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
- # Threads review
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
- warning_size = "For droplet with id: #{instance.id} and name: #{instance.name} the maximum number #{keep} of snapshots is reached."
95
-
96
- if instance.snapshots.size >= keep && stop
97
- Log.warning warning_size
98
- self.notify = true
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
- # Stopping instance.
103
- Log.debug 'Shutting down droplet.'
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, warning_size)
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
- event = Digitalocean::Droplet.snapshot(instance.id, name: name)
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
- sleep delay until get_event_status(event.event_id)
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
- if snapshot_size > keep
147
- Log.warning warning_size if snapshot_size > keep
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
- Log.debug "Cleaning up snapshots for droplet id: #{instance.id} name: #{instance.name}."
153
+ return unless size > keep
166
154
 
167
- (0..size).each do |i|
168
- snapshot = instance.snapshots[i]
169
- event = Digitalocean::Image.destroy(snapshot.id)
155
+ warning_size(instance.id, instance.name, size)
170
156
 
171
- if !event
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
- Log.info "Snapshot name: #{snapshot.name} delete requested."
178
- end
159
+ api.cleanup_snapshots(instance, size - keep - 1)
179
160
  rescue => e
180
- raise SnapshotCleanupError.new(instance.id), e.message, e.backtrace
161
+ raise SnapshotCleanupError, e.message, e.backtrace
181
162
  end
182
163
 
183
- # Looking for event status.
184
- #
185
- # Before snapshot we to know that machine has powered off.
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