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