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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 77bad0c9dba5e0acc40d97db07f2275c580ccd83
4
- data.tar.gz: ea1c657a195ab4a6643e6c8b9267d6db074f7339
3
+ metadata.gz: 893e1b1d92258582ec865910719ebc50e94c0699
4
+ data.tar.gz: e9882bb34a21c8b4a863718b10fdaae2056c01e8
5
5
  SHA512:
6
- metadata.gz: 3a827d7f5a0882d97e6d8d3be223b24e66580d89e59b91123f19e0b1f5bafb15f0ce076876806e6e3b501859a1b2c59d4f24883ccf05605b90218b2307f4fa0e
7
- data.tar.gz: 6bc57df99c5cb032eba75095634eb974cea9050f7f063d4195978739391772532acfdefda92e4d711c3fe2fb580cacefcdc842b2f7a04faa14145201844decd5
6
+ metadata.gz: 763047aa8249b7bff6a305b790a2f66b93377f9acaa7b7756fbf61f89498728f7ab72b5dc4221bd24db831ba7bbf769e184eb3d3162ac115d167335d3d7bb965
7
+ data.tar.gz: d1ed3d12fcefdf9dcb6be6f0c2bf5eeefa83919dab823433a8f4a7592bed97dae0150082bc9166f6c0d814a0c071c94f83e263fd8f192c73c6768212d301ab2a
data/lib/cloudflock.rb CHANGED
@@ -4,5 +4,5 @@ require 'cloudflock/errstr'
4
4
  # Public: Encapsulate all functionality related to the CloudFlock API and any
5
5
  # apps built with such.
6
6
  module CloudFlock
7
- VERSION = '0.7.3'
7
+ VERSION = '0.8.0'
8
8
  end
@@ -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
- # TODO: Add config file support.
99
- # opts.on('-c', '--config FILE', 'Specify configuration file') do |file|
100
- # options[:config_file] = File.expand_path(file)
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 rsync_migrate(shell, rsync)
509
- UI.spinner('Performing rsync migration') do
510
- 2.times do
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("ssh-keygen -b 4096 -q -t rsa -f #{PRIVATE_KEY} -P ''")
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: Perform the steps necessary to migrate a Unix host to a standing
15
- # host or to a newly provisioned Rackspace Cloud server.
14
+ # Public: Obtain information needed to migrate one or more Unix hosts, and
15
+ # perform the migrations.
16
16
  def initialize
17
- options = parse_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
- source_host = source_connect(options)
20
- profile = fetch_profile(source_host)
27
+ api,managed = get_api_and_service_level unless options[:resume]
21
28
 
22
- puts generate_recommendation(profile)
29
+ destinations = profiles.zip(sources).map do |profile, host|
30
+ destination_info(host, profile, options[:resume], managed, api)
31
+ end
23
32
 
24
- dest_host = destination_connect(options, profile)
25
- exclusions = build_exclusions(profile.cpe)
26
- migrate_server(source_host, dest_host, exclusions)
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
- cleanup_destination(dest_host, profile.cpe)
29
- configure_ips(dest_host, profile)
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 UI.bold { UI.blue { "Migration complete to #{dest_host.hostname}"} }
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: Collect information needed to connect to the source host for a
51
- # migration. Connect to the target host.
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
- # options - Hash containing information to connect to an existing host.
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 an SSH object connected to the source host.
56
- def source_connect(options)
57
- source_host = define_source(options)
58
- ssh_connect(source_host)
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
- source_host = options.dup
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
- puts generate_report(profile)
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
  #
@@ -2,6 +2,7 @@ module CloudFlock
2
2
  module App
3
3
  module Common
4
4
  class NoRsyncAvailable < StandardError; end
5
+ class WatchdogAlert < StandardError; end
5
6
  end
6
7
  end
7
8
 
@@ -109,6 +109,10 @@ module CloudFlock; module Remote
109
109
  rescue EOFError
110
110
  start_session
111
111
  retry
112
+ rescue IO::EAGAINWaitReadable
113
+ sleep 10
114
+ start_session
115
+ retry
112
116
  end
113
117
 
114
118
  # Public: Call query on a list of commands, allowing optional timeout and
@@ -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.7.3
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
- - Tina Wuest
7
+ - Chris Wuest
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-10 00:00:00.000000000 Z
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: tina@wuest.me
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