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