cloudflock 0.7.3 → 0.8.0
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.
- 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
|