cloudflock 0.7.3 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/cloudflock.rb +1 -1
- data/lib/cloudflock/app.rb +15 -5
- data/lib/cloudflock/app/common/servers.rb +128 -9
- data/lib/cloudflock/app/common/watchdogs.rb +89 -0
- data/lib/cloudflock/app/server-migrate.rb +87 -33
- data/lib/cloudflock/app/server-profile.rb +56 -6
- data/lib/cloudflock/error.rb +1 -0
- data/lib/cloudflock/remote/ssh.rb +4 -0
- data/lib/cloudflock/remote/ssh/watchdog.rb +129 -0
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 893e1b1d92258582ec865910719ebc50e94c0699
|
4
|
+
data.tar.gz: e9882bb34a21c8b4a863718b10fdaae2056c01e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 763047aa8249b7bff6a305b790a2f66b93377f9acaa7b7756fbf61f89498728f7ab72b5dc4221bd24db831ba7bbf769e184eb3d3162ac115d167335d3d7bb965
|
7
|
+
data.tar.gz: d1ed3d12fcefdf9dcb6be6f0c2bf5eeefa83919dab823433a8f4a7592bed97dae0150082bc9166f6c0d814a0c071c94f83e263fd8f192c73c6768212d301ab2a
|
data/lib/cloudflock.rb
CHANGED
data/lib/cloudflock/app.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'optparse'
|
2
2
|
require 'cloudflock'
|
3
3
|
require 'console-glitter'
|
4
|
+
require 'yaml'
|
4
5
|
|
5
6
|
module CloudFlock
|
6
7
|
# Public: The App module provides any functionality that is expected to be
|
@@ -79,6 +80,16 @@ module CloudFlock
|
|
79
80
|
options[name] = UI.prompt_yn(prompt, prompt_options)
|
80
81
|
end
|
81
82
|
|
83
|
+
def load_config_if_present(options)
|
84
|
+
if File.file?(options[:config_file].to_s)
|
85
|
+
YAML.load_file(options[:config_file]).merge(options)
|
86
|
+
else
|
87
|
+
options
|
88
|
+
end
|
89
|
+
rescue Psych::SyntaxError, NoMethodError
|
90
|
+
options
|
91
|
+
end
|
92
|
+
|
82
93
|
# Public: Parse options and expose global options which are expected to be
|
83
94
|
# useful in any CLI application.
|
84
95
|
#
|
@@ -95,10 +106,9 @@ module CloudFlock
|
|
95
106
|
opts.separator ''
|
96
107
|
opts.separator 'Global Options:'
|
97
108
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
# end
|
109
|
+
opts.on('-c', '--config FILE', 'Specify configuration file') do |file|
|
110
|
+
options[:config_file] = File.expand_path(file)
|
111
|
+
end
|
102
112
|
|
103
113
|
opts.on_tail('--version', 'Show Version Information') do
|
104
114
|
puts "CloudFlock v#{CloudFlock::VERSION}"
|
@@ -112,7 +122,7 @@ module CloudFlock
|
|
112
122
|
|
113
123
|
opts.parse!(ARGV)
|
114
124
|
|
115
|
-
options
|
125
|
+
load_config_if_present(options)
|
116
126
|
rescue OptionParser::MissingArgument, OptionParser::InvalidOption => error
|
117
127
|
puts error.message.capitalize
|
118
128
|
puts
|
@@ -4,6 +4,7 @@ require 'cloudflock/app'
|
|
4
4
|
require 'cloudflock/remote/ssh'
|
5
5
|
require 'cloudflock/app/common/rackspace'
|
6
6
|
require 'cloudflock/app/common/exclusions'
|
7
|
+
require 'cloudflock/app/common/watchdogs'
|
7
8
|
require 'cloudflock/app/common/cleanup'
|
8
9
|
|
9
10
|
module CloudFlock; module App
|
@@ -142,6 +143,9 @@ module CloudFlock; module App
|
|
142
143
|
options = {username: nil, password: nil}
|
143
144
|
host = self.send(define_method, (host.merge(options)))
|
144
145
|
retry
|
146
|
+
rescue Errno::ECONNREFUSED
|
147
|
+
retry_exit("Connection refused from #{host[:hostname]}")
|
148
|
+
retry
|
145
149
|
end
|
146
150
|
|
147
151
|
# Public: Have the user select from a list of available images to provision
|
@@ -396,13 +400,15 @@ module CloudFlock; module App
|
|
396
400
|
rescue Net::SSH::Disconnect
|
397
401
|
retry_exit('Unable to establish a connection.')
|
398
402
|
retry
|
403
|
+
rescue Errno::ECONNREFUSED
|
404
|
+
retry_exit("Connection refused from #{host[:hostname]}")
|
405
|
+
retry
|
399
406
|
rescue ArgumentError
|
400
407
|
retry_exit('Incorrect passphrase provided for ssh key.')
|
401
408
|
|
402
409
|
host.delete(:passphrase)
|
403
410
|
check_option_pw(host, :passphrase, "Key passphrase", default_answer: '',
|
404
411
|
allow_empty: true)
|
405
|
-
retry
|
406
412
|
end
|
407
413
|
|
408
414
|
# Public: Get details for a Fog::Compute instance.
|
@@ -432,9 +438,11 @@ module CloudFlock; module App
|
|
432
438
|
rsync = prepare_source_rsync(source_shell, dest_shell)
|
433
439
|
dest_address = prepare_source_servicenet(source_shell, dest_shell)
|
434
440
|
|
441
|
+
watchdogs = create_watchdogs(source_shell, dest_shell)
|
435
442
|
rsync = "#{rsync} -azP -e 'ssh #{SSH_ARGUMENTS} -i #{PRIVATE_KEY}' " +
|
436
443
|
"--exclude-from='#{EXCLUSIONS}' / #{dest_address}:#{MOUNT_POINT}"
|
437
|
-
rsync_migrate(source_shell, rsync)
|
444
|
+
rsync_migrate(watchdogs, source_shell, rsync)
|
445
|
+
stop_watchdogs(watchdogs)
|
438
446
|
end
|
439
447
|
|
440
448
|
# Public: Generate a new ssh keypair to be used for the migration.
|
@@ -499,24 +507,46 @@ module CloudFlock; module App
|
|
499
507
|
retry
|
500
508
|
end
|
501
509
|
|
510
|
+
def rsync_migrate(watchdogs, shell, rsync)
|
511
|
+
UI.spinner('Waiting for all hosts to appear to be in a healthy state') do
|
512
|
+
ensure_no_watchdog_alerts(watchdogs)
|
513
|
+
end
|
514
|
+
UI.spinner('Performing rsync migration') do
|
515
|
+
worker = Thread.new do
|
516
|
+
rsync_migrate_thread(shell, rsync)
|
517
|
+
Thread.current[:complete] = true
|
518
|
+
end
|
519
|
+
set_watchdog_alerts(watchdogs, worker)
|
520
|
+
worker.join
|
521
|
+
raise WatchdogAlert unless worker[:complete]
|
522
|
+
end
|
523
|
+
rescue WatchdogAlert
|
524
|
+
retry
|
525
|
+
end
|
526
|
+
|
502
527
|
# Public: Wrap performing an rsync migration.
|
503
528
|
#
|
504
529
|
# shell - SSH object logged in to the source host.
|
505
530
|
# rsync - Command to be run on the source host.
|
506
531
|
#
|
507
532
|
# Returns nothing.
|
508
|
-
def
|
509
|
-
|
510
|
-
|
511
|
-
rsync_migrate_commands(shell, rsync)
|
512
|
-
end
|
513
|
-
shell.logout!
|
533
|
+
def rsync_migrate_thread(shell, rsync)
|
534
|
+
2.times do
|
535
|
+
rsync_migrate_commands(shell, rsync)
|
514
536
|
end
|
537
|
+
shell.logout!
|
515
538
|
rescue Timeout::Error
|
516
539
|
retry if retry_prompt('Server sync is taking a very long time')
|
517
540
|
exit
|
518
541
|
end
|
519
542
|
|
543
|
+
def ensure_no_watchdog_alerts(watchdogs)
|
544
|
+
raise WatchdogAlert if watchdogs.map(&:triggered_alarms).flatten.any?
|
545
|
+
rescue WatchdogAlert
|
546
|
+
sleep 30
|
547
|
+
retry
|
548
|
+
end
|
549
|
+
|
520
550
|
# Public: Issue an rsync command, keeping track of how many times a timeout
|
521
551
|
# has occurred, raising an error past a threshhold of 3 timeouts.
|
522
552
|
#
|
@@ -542,8 +572,9 @@ module CloudFlock; module App
|
|
542
572
|
#
|
543
573
|
# Returns a String containing the host's new ssh public key.
|
544
574
|
def generate_keypair(shell)
|
575
|
+
keygen_command = "ssh-keygen -b 4096 -q -t rsa -f #{PRIVATE_KEY} -P ''"
|
545
576
|
shell.as_root("mkdir #{DATA_DIR}")
|
546
|
-
shell.as_root(
|
577
|
+
shell.as_root(keygen_command, 3600)
|
547
578
|
shell.as_root("cat #{PUBLIC_KEY}")
|
548
579
|
end
|
549
580
|
|
@@ -670,6 +701,94 @@ module CloudFlock; module App
|
|
670
701
|
retry
|
671
702
|
end
|
672
703
|
|
704
|
+
# Public: For each watchdog in a collection, stop the watchdog.
|
705
|
+
#
|
706
|
+
# watchdogs - Hash containing name => Watchdog mappings.
|
707
|
+
#
|
708
|
+
# Returns nothing.
|
709
|
+
def stop_watchdogs(watchdogs)
|
710
|
+
watchdogs.each { |watchdog| stop_watchdog(watchdog) }
|
711
|
+
end
|
712
|
+
|
713
|
+
# Public: Stop a given watchdog, reporting on the status.
|
714
|
+
#
|
715
|
+
# watchdog - Watchdog object to be stopped.
|
716
|
+
#
|
717
|
+
# Returns nothing.
|
718
|
+
def stop_watchdog(watchdog)
|
719
|
+
UI.spinner("Stopping watchdog: #{watchdog.name}") { watchdog.stop }
|
720
|
+
rescue Timeout::Error
|
721
|
+
end
|
722
|
+
|
723
|
+
# Public: Start all watchdogs for a migration.
|
724
|
+
#
|
725
|
+
# source_shell - SSH object logged in to the source host.
|
726
|
+
# dest_shell - SSH object logged in to the destination host.
|
727
|
+
#
|
728
|
+
# Returns a Hash containing name => Watchdog mappings. Watchdogs will have
|
729
|
+
# no alarms set.
|
730
|
+
def create_watchdogs(source_shell, dest_shell)
|
731
|
+
source_watchdogs(source_shell) + dest_watchdogs(dest_shell)
|
732
|
+
end
|
733
|
+
|
734
|
+
# Public: Start all watchdogs to monitor a source host.
|
735
|
+
#
|
736
|
+
# source - SSH object logged in to the source host.
|
737
|
+
#
|
738
|
+
# Returns a Hash containing name => Watchdog mappings. Watchdogs will have
|
739
|
+
# no alarms set.
|
740
|
+
def source_watchdogs(shell)
|
741
|
+
[:system_load,:utilized_memory].map do |e|
|
742
|
+
start_watchdog(:source, e, shell)
|
743
|
+
end
|
744
|
+
end
|
745
|
+
|
746
|
+
# Public: Start all watchdogs to monitor a destination host.
|
747
|
+
#
|
748
|
+
# source - SSH object logged in to the destination host.
|
749
|
+
#
|
750
|
+
# Returns a Hash containing name => Watchdog mappings. Watchdogs will have
|
751
|
+
# no alarms set.
|
752
|
+
def dest_watchdogs(shell)
|
753
|
+
[:system_load,:utilized_memory, :used_space].map do |e|
|
754
|
+
start_watchdog(:destination, e, shell)
|
755
|
+
end
|
756
|
+
end
|
757
|
+
|
758
|
+
# Public: Start a watchdog on a given host.
|
759
|
+
#
|
760
|
+
# location - Symbol or String containing the name of the location where the
|
761
|
+
# watshdog should be run.
|
762
|
+
# name - Symbol or String describing the watchdog in question.
|
763
|
+
# source - SSH object logged in to the host which the watchdog should
|
764
|
+
# monitor.
|
765
|
+
#
|
766
|
+
# Returns a Hash containing name => Watchdog mappings. Watchdogs will have
|
767
|
+
# no alarms set.
|
768
|
+
def start_watchdog(location, name, shell)
|
769
|
+
display = "#{location} #{name}".capitalize
|
770
|
+
UI.spinner("Starting watchdog: #{display}") do
|
771
|
+
Watchdogs.send(name, shell, display)
|
772
|
+
end
|
773
|
+
rescue Timeout::Error
|
774
|
+
failed = name.to_s.gsub(/_/, ' ').capitalize
|
775
|
+
retry_exit("Timed out starting the #{failed} watchdog.")
|
776
|
+
retry
|
777
|
+
end
|
778
|
+
|
779
|
+
# Public: Set watchdogs up with default alarms.
|
780
|
+
#
|
781
|
+
# watchdogs - Array containing all default watchdogs.
|
782
|
+
# worker - Thread containing the migration worker.
|
783
|
+
#
|
784
|
+
# Returns nothing.
|
785
|
+
def set_watchdog_alerts(watchdogs, worker)
|
786
|
+
watchdogs.each do |watchdog|
|
787
|
+
method = watchdog.name.split(/ /).last
|
788
|
+
Watchdogs.send("set_alarm_#{method}", watchdog, worker)
|
789
|
+
end
|
790
|
+
end
|
791
|
+
|
673
792
|
# Public: Determine what address should be used when connecting from source
|
674
793
|
# to destination for the purpose of a migration. Prefer RFC1918 networks.
|
675
794
|
#
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'console-glitter'
|
2
|
+
require 'cloudflock/app'
|
3
|
+
require 'cloudflock/remote/ssh'
|
4
|
+
require 'cloudflock/remote/ssh/watchdog'
|
5
|
+
|
6
|
+
module CloudFlock; module App
|
7
|
+
# Public: The Watchdogs module provides commonly used watchdogs.
|
8
|
+
module Watchdogs extend self
|
9
|
+
# Public: Create a Watchdog which monitors the used disk space on a given
|
10
|
+
# host.
|
11
|
+
#
|
12
|
+
# ssh - SSH session which the Watchdog should monitor.
|
13
|
+
# name - String describing the Watchdog.
|
14
|
+
#
|
15
|
+
# Returns a Watchdog.
|
16
|
+
def used_space(ssh, name)
|
17
|
+
CloudFlock::Remote::SSH::Watchdog.new(name, ssh, 'df', 60) do |df|
|
18
|
+
lines = df.lines.select { |line| /^[^ ]*(?:\s+\d+){2,}/.match line }
|
19
|
+
total = lines.map { |line| line.split(/\s+/)[1].to_i }.reduce(&:+)
|
20
|
+
used = lines.map { |line| line.split(/\s+/)[2].to_i }.reduce(&:+)
|
21
|
+
used.to_f / total
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Public: Create a Watchdog which monitors the system load average on a
|
26
|
+
# given host.
|
27
|
+
#
|
28
|
+
# ssh - SSH session which the Watchdog should monitor.
|
29
|
+
# name - String describing the Watchdog.
|
30
|
+
#
|
31
|
+
# Returns a Watchdog.
|
32
|
+
def system_load(ssh, name)
|
33
|
+
CloudFlock::Remote::SSH::Watchdog.new(name, ssh, 'uptime', 15) do |uptime|
|
34
|
+
uptime.split(/\s+/)[-3].to_f
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Public: Create a Watchdog which monitors the memory in use on a given
|
39
|
+
# host.
|
40
|
+
#
|
41
|
+
# ssh - SSH session which the Watchdog should monitor.
|
42
|
+
# name - String describing the Watchdog.
|
43
|
+
#
|
44
|
+
# Returns a Watchdog.
|
45
|
+
def utilized_memory(ssh, name)
|
46
|
+
CloudFlock::Remote::SSH::Watchdog.new(name, ssh, 'free', 15) do |free|
|
47
|
+
lines = free.lines.select { |line| /Swap/.match line }
|
48
|
+
total,used = lines.empty? ? [0,0] : lines.map(&:to_f)[1..2]
|
49
|
+
total > 0 ? free / total : 0.0
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Public: Set up a default alert for if free space on the host falls below
|
54
|
+
# 5%, killing a given thread if it reaches that threshhold.
|
55
|
+
#
|
56
|
+
# watchdog - Watchdog to which the alarm should be added.
|
57
|
+
# thread - Thread to kill if the alarm fires.
|
58
|
+
#
|
59
|
+
# Returns nothing.
|
60
|
+
def set_alarm_used_space(watchdog, thread)
|
61
|
+
watchdog.create_alarm('out_of_space') { |space| space > 0.95 }
|
62
|
+
watchdog.on_alarm('out_of_space') { |space| thread.kill }
|
63
|
+
end
|
64
|
+
|
65
|
+
# Public: Set up a default alert for if the system load is >10, killing a
|
66
|
+
# given thread if it reaches that threshhold.
|
67
|
+
#
|
68
|
+
# watchdog - Watchdog to which the alarm should be added.
|
69
|
+
# thread - Thread to kill if the alarm fires.
|
70
|
+
#
|
71
|
+
# Returns nothing.
|
72
|
+
def set_alarm_system_load(watchdog, thread)
|
73
|
+
watchdog.create_alarm('load_too_high') { |waitq| waitq > 10 }
|
74
|
+
watchdog.on_alarm('load_too_high') { |waitq| thread.kill }
|
75
|
+
end
|
76
|
+
|
77
|
+
# Public: Set up a default alert for when swap used is > 25%, killing a
|
78
|
+
# given thread if it reaches that threshhold.
|
79
|
+
#
|
80
|
+
# watchdog - Watchdog to which the alarm should be added.
|
81
|
+
# thread - Thread to kill if the alarm fires.
|
82
|
+
#
|
83
|
+
# Returns nothing.
|
84
|
+
def set_alarm_utilized_memory(watchdog, thread)
|
85
|
+
watchdog.create_alarm('swapping') { |swap| swap > 0.25 }
|
86
|
+
watchdog.on_alarm('swapping') { |swap| thread.kill }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end; end
|
@@ -11,31 +11,83 @@ module CloudFlock; module App
|
|
11
11
|
include CloudFlock::App::Common
|
12
12
|
include CloudFlock::Remote
|
13
13
|
|
14
|
-
# Public:
|
15
|
-
#
|
14
|
+
# Public: Obtain information needed to migrate one or more Unix hosts, and
|
15
|
+
# perform the migrations.
|
16
16
|
def initialize
|
17
|
-
options
|
17
|
+
options = parse_options
|
18
|
+
servers = options[:servers]
|
19
|
+
servers ||= [options]
|
20
|
+
|
21
|
+
sources = servers.map(&method(:define_source))
|
22
|
+
profiles = sources.map do |host|
|
23
|
+
source_host = ssh_connect(host)
|
24
|
+
fetch_profile(source_host)
|
25
|
+
end
|
18
26
|
|
19
|
-
|
20
|
-
profile = fetch_profile(source_host)
|
27
|
+
api,managed = get_api_and_service_level unless options[:resume]
|
21
28
|
|
22
|
-
|
29
|
+
destinations = profiles.zip(sources).map do |profile, host|
|
30
|
+
destination_info(host, profile, options[:resume], managed, api)
|
31
|
+
end
|
23
32
|
|
24
|
-
|
25
|
-
|
26
|
-
|
33
|
+
exclusions = profiles.
|
34
|
+
zip(destinations).
|
35
|
+
zip(sources).
|
36
|
+
map(&:flatten).map do |profile, dest, host|
|
37
|
+
puts UI.green { "#{host[:hostname]} -> #{dest[:hostname]}" }
|
38
|
+
build_exclusions(profile.cpe)
|
39
|
+
end
|
27
40
|
|
28
|
-
|
29
|
-
|
41
|
+
results = sources.
|
42
|
+
zip(destinations).
|
43
|
+
zip(exclusions).
|
44
|
+
zip(profiles).
|
45
|
+
map(&:flatten).
|
46
|
+
map { |params| do_migration(*params) }
|
30
47
|
|
31
|
-
puts
|
32
|
-
rescue
|
33
|
-
puts UI.red { 'An unhandled error was encountered. Details follow:' }
|
34
|
-
raise
|
48
|
+
puts results.join("\n")
|
35
49
|
end
|
36
50
|
|
37
51
|
private
|
38
52
|
|
53
|
+
# Internal: Perform the steps necessary to migrate one Unix host to another.
|
54
|
+
#
|
55
|
+
# source_host - Information necessary to log in to the source host.
|
56
|
+
# dest_host - Information necessary to log in to the destination host.
|
57
|
+
# exclusions - String containing paths to exclude from the migration.
|
58
|
+
# profile - ServerProfile for the source host.
|
59
|
+
#
|
60
|
+
# Returns a String containing information regarding the success or failure
|
61
|
+
# of the migration.
|
62
|
+
def do_migration(source_host, dest_host, exclusions, profile)
|
63
|
+
source_ssh = ssh_connect(source_host)
|
64
|
+
dest_ssh = ssh_connect(dest_host)
|
65
|
+
|
66
|
+
migrate_server(source_ssh, dest_ssh, exclusions)
|
67
|
+
cleanup_destination(dest_ssh, profile.cpe)
|
68
|
+
configure_ips(dest_ssh, profile)
|
69
|
+
|
70
|
+
UI.bold { UI.blue { "Migration complete to #{dest_host[:hostname]}"} }
|
71
|
+
rescue => e
|
72
|
+
UI.red { 'An unhandled error was encountered. Details follow:' } +
|
73
|
+
UI.red { e.display + e.backtrace }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Internal: Obtain information relevant to a Rackspace account.
|
77
|
+
#
|
78
|
+
# Returns an Array containing a Fog::Itendity object and a boolean
|
79
|
+
# determining whether the account is managed.
|
80
|
+
def get_api_and_service_level
|
81
|
+
api = define_rackspace_api
|
82
|
+
Fog::Identity.new(api)
|
83
|
+
managed = UI.prompt_yn('Managed Account? (Y/N)', default_answer: 'N')
|
84
|
+
|
85
|
+
[api, managed]
|
86
|
+
rescue Excon::Errors::Unauthorized
|
87
|
+
retry if UI.prompt_yn('Login failed. Retry? (Y/N)', default_answer: 'Y')
|
88
|
+
exit
|
89
|
+
end
|
90
|
+
|
39
91
|
# Internal: Profile a server in order to make accurate recommendations.
|
40
92
|
#
|
41
93
|
# source_ssh - SSH object connected to a Unix host.
|
@@ -47,15 +99,28 @@ module CloudFlock; module App
|
|
47
99
|
end
|
48
100
|
end
|
49
101
|
|
50
|
-
# Internal:
|
51
|
-
#
|
102
|
+
# Internal: Display a recommendation to the user, then obtain information
|
103
|
+
# needed to log into a target host, creating a new cloud server if
|
104
|
+
# necessary.
|
52
105
|
#
|
53
|
-
#
|
106
|
+
# host - Hash containing information regarding the destination host,
|
107
|
+
# if given.
|
108
|
+
# profile - ServerProfile for the source host.
|
109
|
+
# resume - Boolean value denoting whether a migration will be resumed.
|
110
|
+
# managed - Boolean value denoting whether the account in question is
|
111
|
+
# managed.
|
112
|
+
# api - Fog::Identity object used to make API calls.
|
54
113
|
#
|
55
|
-
# Returns
|
56
|
-
|
57
|
-
|
58
|
-
|
114
|
+
# Returns a Hash containing information needed to log in to the destination
|
115
|
+
# host.
|
116
|
+
def destination_info(host, profile, resume, managed, api)
|
117
|
+
puts generate_recommendation(profile)
|
118
|
+
|
119
|
+
if resume
|
120
|
+
define_destination(host)
|
121
|
+
else
|
122
|
+
create_cloud_instance(api, profile, managed)
|
123
|
+
end
|
59
124
|
end
|
60
125
|
|
61
126
|
# Internal: Collect information needed to either connect to an existing
|
@@ -67,18 +132,7 @@ module CloudFlock; module App
|
|
67
132
|
#
|
68
133
|
# Returns an SSH object connected to the target host.
|
69
134
|
def destination_connect(options, profile)
|
70
|
-
if options[:resume]
|
71
|
-
dest_host = define_destination(options)
|
72
|
-
else
|
73
|
-
api = define_rackspace_api
|
74
|
-
managed = UI.prompt_yn('Managed account? (Y/N)', default_answer: 'N')
|
75
|
-
dest_host = create_cloud_instance(api, profile, managed)
|
76
|
-
end
|
77
|
-
|
78
135
|
ssh_connect(dest_host)
|
79
|
-
rescue Excon::Errors::Unauthorized
|
80
|
-
retry if UI.prompt_yn('Login failed. Retry? (Y/N)', default_answer: 'Y')
|
81
|
-
exit
|
82
136
|
end
|
83
137
|
|
84
138
|
# Internal: Provision a new instance on the Rackspace cloud and return
|
@@ -13,21 +13,37 @@ module CloudFlock; module App
|
|
13
13
|
# information.
|
14
14
|
def initialize
|
15
15
|
options = parse_options
|
16
|
-
|
16
|
+
servers = options[:servers]
|
17
|
+
save_option = true unless servers
|
18
|
+
servers ||= [options]
|
19
|
+
|
20
|
+
results = servers.map { |server| profile_host(server.dup, save_option) }
|
21
|
+
printable = results.map do |hash|
|
22
|
+
name = hash.keys.first
|
23
|
+
profile = hash[name]
|
24
|
+
UI.bold { UI.green { "#{name}\n" } } +
|
25
|
+
generate_report(profile) +
|
26
|
+
(options[:verbose] ? profile.process_list.to_s : "")
|
27
|
+
end
|
28
|
+
|
29
|
+
puts printable.join("\n\n")
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def profile_host(source_host, save_option)
|
35
|
+
source_host = define_source(source_host)
|
36
|
+
save_config(source_host) if save_option && save_config?
|
17
37
|
|
18
|
-
source_host = define_source(options)
|
19
38
|
source_ssh = connect_source(source_host)
|
20
39
|
|
21
40
|
profile = UI.spinner("Checking source host") do
|
22
41
|
CloudFlock::Task::ServerProfile.new(source_ssh)
|
23
42
|
end
|
24
43
|
|
25
|
-
|
26
|
-
puts profile.process_list if options[:verbose]
|
44
|
+
{source_host[:hostname] => profile}
|
27
45
|
end
|
28
46
|
|
29
|
-
private
|
30
|
-
|
31
47
|
# Internal: Generate a "title" String (bold, 15 characters wide).
|
32
48
|
#
|
33
49
|
# tag - String to be turned into a title.
|
@@ -81,6 +97,40 @@ module CloudFlock; module App
|
|
81
97
|
warnings
|
82
98
|
end
|
83
99
|
|
100
|
+
def save_config?
|
101
|
+
UI.prompt_yn('Save to a config file? (Y/N)', default_answer: 'Y')
|
102
|
+
end
|
103
|
+
|
104
|
+
# Internal: Save a configuration file based on the user's earlier answers.
|
105
|
+
#
|
106
|
+
# source_host - Hash containing parameters to use to log in to a server.
|
107
|
+
#
|
108
|
+
# Returns nothing.
|
109
|
+
def save_config(source_host)
|
110
|
+
config_location = determine_config_location(source_host[:hostname])
|
111
|
+
if File.exists?(config_location)
|
112
|
+
clobber = UI.prompt_yn('Overwrite? (Y/N)', default_answer: 'Y')
|
113
|
+
old_config = YAML.load_file(config_location) unless clobber
|
114
|
+
end
|
115
|
+
old_config ||= {}
|
116
|
+
|
117
|
+
File.open(config_location, 'w') do |file|
|
118
|
+
new_servers = old_config[:servers].to_a + [source_host]
|
119
|
+
file.write(YAML.dump(old_config.merge({servers: new_servers})))
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Internal: Prompt the user for a location to save a configuration file.
|
124
|
+
#
|
125
|
+
# hostname - String containing the hostname of the host.
|
126
|
+
#
|
127
|
+
# Returns a String containing a filesystem path.
|
128
|
+
def determine_config_location(hostname)
|
129
|
+
location = File.join(Dir.home, 'cloudflock_' + hostname + '.yaml')
|
130
|
+
UI.prompt_filesystem('Configuration file Location',
|
131
|
+
default_answer: location)
|
132
|
+
end
|
133
|
+
|
84
134
|
# Internal: Set up an OptionParser object to recognize options specific to
|
85
135
|
# profiling a remote host.
|
86
136
|
#
|
data/lib/cloudflock/error.rb
CHANGED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'cloudflock'
|
2
|
+
require 'net/ssh'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module CloudFlock; module Remote; class SSH
|
6
|
+
# The Watchdog Class allows for the creation of custom watchdogs to allow the
|
7
|
+
# status of an ongoing migration as well as the health of the hosts involved
|
8
|
+
# to be monitored.
|
9
|
+
#
|
10
|
+
# Examples
|
11
|
+
#
|
12
|
+
# # Create a Watchdog to monitor system load, the state will be tracked as
|
13
|
+
# a float and updated every 15 seconds (roughly 3 refreshes by default.)
|
14
|
+
# # The state of the Watchdog can be accessed via the Watchdog#state method.
|
15
|
+
# system_load = Watchdog.new(ssh, 'uptime', 15) do |wait|
|
16
|
+
# wait.gsub(/^.*(\d+\.\d+).*$/, '\\1').to_f
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # Alerts can be created, so that action can be taken automatically.
|
20
|
+
# system_load.create_alarm('high_load') { |wait| wait > 10 }
|
21
|
+
# system_load.on_alarm('high_load') { |wait| puts "Load is #{wait}!"; exit }
|
22
|
+
class Watchdog
|
23
|
+
attr_reader :state
|
24
|
+
attr_reader :name
|
25
|
+
|
26
|
+
# Public: Create a new Watchdog to keep track of some aspect of a given
|
27
|
+
# host's state.
|
28
|
+
#
|
29
|
+
# name - String containing the watchdog's name.
|
30
|
+
# ssh - SSH session which the Watchdog should monitor.
|
31
|
+
# command - String to run periodically on the target SSH session to
|
32
|
+
# determine the host's state.
|
33
|
+
# interval - Number of seconds to wait between command invocations.
|
34
|
+
# (default: 30)
|
35
|
+
# block - Optional block to be passed the results of the command to
|
36
|
+
# transform the data and make it more easily consumable.
|
37
|
+
# (default: identity function)
|
38
|
+
def initialize(name, ssh, command, interval = 30, &block)
|
39
|
+
@name = name
|
40
|
+
@ssh = ssh
|
41
|
+
@command = command
|
42
|
+
@interval = interval
|
43
|
+
@transform = block
|
44
|
+
@thread = start_thread
|
45
|
+
@alarms = {}
|
46
|
+
@actions = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Public: Stop the Watchdog from running.
|
50
|
+
#
|
51
|
+
# Returns nothing.
|
52
|
+
def stop
|
53
|
+
thread.kill
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Create a new named alarm, providing a predicate to indicate that
|
57
|
+
# the alarm should be considered active.
|
58
|
+
#
|
59
|
+
# name - Name for the alarm.
|
60
|
+
# block - Block to be evaluated in order to determine if the alarm is
|
61
|
+
# active. The block should accept one argument (the current state
|
62
|
+
# of the Watchdog).
|
63
|
+
#
|
64
|
+
# Returns nothing.
|
65
|
+
def create_alarm(name, &block)
|
66
|
+
alarms[name] = block
|
67
|
+
end
|
68
|
+
|
69
|
+
# Public: Define the action which should be taken when an alarm is
|
70
|
+
# triggered.
|
71
|
+
#
|
72
|
+
# name - Name of the alarm.
|
73
|
+
# block - Block to be executed when an alarm is determined to be triggered.
|
74
|
+
# The block should accept one argument (the current state of the
|
75
|
+
# Watchdog).
|
76
|
+
#
|
77
|
+
# Returns nothing.
|
78
|
+
def on_alarm(name, &block)
|
79
|
+
actions[name] = block
|
80
|
+
end
|
81
|
+
|
82
|
+
# Public: Determine whether a given alarm is presently active.
|
83
|
+
#
|
84
|
+
# name - Name of the alarm.
|
85
|
+
#
|
86
|
+
# Returns false if the alarm is not defined, or the result of the alarm
|
87
|
+
# predicate otherwise.
|
88
|
+
def alarm_active?(name)
|
89
|
+
triggered = alarms[name].nil? ? false : alarms[name]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Public: Return the state of all active alarms.
|
93
|
+
#
|
94
|
+
# Returns an Array of active alarms.
|
95
|
+
def triggered_alarms
|
96
|
+
alarms.select { |k,v| v[state] }.map(&:first)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
attr_reader :ssh, :command, :interval, :thread, :transform, :alarms
|
102
|
+
attr_reader :actions
|
103
|
+
attr_writer :state
|
104
|
+
|
105
|
+
# Internal: Create a thread to periodically poll the server and determine
|
106
|
+
# if any alerts should be considered active.
|
107
|
+
#
|
108
|
+
# Returns the newly created thread.
|
109
|
+
def start_thread
|
110
|
+
Thread.new do
|
111
|
+
loop do
|
112
|
+
result = ssh.query(command)
|
113
|
+
state = transform.nil? ? result : transform[result]
|
114
|
+
|
115
|
+
respond_to_alarms
|
116
|
+
sleep interval
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Internal: For each alert for which a triggered behavior exists, determine
|
122
|
+
# if the alarm is considered fired.
|
123
|
+
#
|
124
|
+
# Returns nothing.
|
125
|
+
def respond_to_alarms
|
126
|
+
triggered_alarms.each { |key| actions[key].call if actions[key] }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end; end; end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cloudflock
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Chris Wuest
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-11-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: fog-json
|
@@ -85,7 +85,7 @@ dependencies:
|
|
85
85
|
- !ruby/object:Gem::Version
|
86
86
|
version: 0.2.1
|
87
87
|
description: CloudFlock is a library and toolchain focused on migration
|
88
|
-
email:
|
88
|
+
email: chris@chriswuest.com
|
89
89
|
executables:
|
90
90
|
- cloudflock
|
91
91
|
- cloudflock-files
|
@@ -111,6 +111,7 @@ files:
|
|
111
111
|
- lib/cloudflock/app/common/platform_action.rb
|
112
112
|
- lib/cloudflock/app/common/rackspace.rb
|
113
113
|
- lib/cloudflock/app/common/servers.rb
|
114
|
+
- lib/cloudflock/app/common/watchdogs.rb
|
114
115
|
- lib/cloudflock/app/files-migrate.rb
|
115
116
|
- lib/cloudflock/app/server-migrate.rb
|
116
117
|
- lib/cloudflock/app/server-profile.rb
|
@@ -118,6 +119,7 @@ files:
|
|
118
119
|
- lib/cloudflock/errstr.rb
|
119
120
|
- lib/cloudflock/remote/files.rb
|
120
121
|
- lib/cloudflock/remote/ssh.rb
|
122
|
+
- lib/cloudflock/remote/ssh/watchdog.rb
|
121
123
|
- lib/cloudflock/target/servers/platform.rb
|
122
124
|
- lib/cloudflock/target/servers/profile.rb
|
123
125
|
- lib/cloudflock/task/server-profile.rb
|