Module: CloudFlock::App::Common

Includes:
CloudFlock::App, Rackspace, Remote, ConsoleGlitter
Included in:
ServerMigrate, ServerProfile
Defined in:
lib/cloudflock/app/common/servers.rb,
lib/cloudflock/error.rb,
lib/cloudflock/errstr.rb,
lib/cloudflock/app/common/cleanup.rb,
lib/cloudflock/app/common/exclusions.rb,
lib/cloudflock/app/common/cleanup/unix.rb,
lib/cloudflock/app/common/exclusions/unix.rb,
lib/cloudflock/app/common/platform_action.rb,
lib/cloudflock/app/common/exclusions/unix/centos.rb,
lib/cloudflock/app/common/exclusions/unix/redhat.rb

Overview

Public: The Common module provides common methods for CLI interaction pertaining to interaction with remote (Unix) servers and the Rackspace API.

Defined Under Namespace

Modules: Errstr Classes: Cleanup, Exclusions, NoRsyncAvailable, PlatformAction

Constant Summary collapse

DATA_DIR =

Path to the base directory in which any CloudFlock files will be stored.

'/root/.cloudflock'
EXCLUSIONS =

Path to the file in which paths excluded from being migrated are stored.

"#{DATA_DIR}/migration_exclusions"
PRIVATE_KEY =

Path to the private key to be generated for migration.

"#{DATA_DIR}/migration_id_rsa"
PUBLIC_KEY =

Path to the public key corresponding to PRIVATE_KEY.

"#{PRIVATE_KEY}.pub"
MOUNT_POINT =

Path to the default path for root partition of the destination host to be mounted.

'/mnt/migration_target'
SSH_ARGUMENTS =

Commonly used arguments to ssh.

CloudFlock::Remote::SSH::SSH_ARGUMENTS

Instance Method Summary collapse

Methods included from CloudFlock::App

#check_option, #check_option_fs, #check_option_pw, #check_option_yn, #parse_options

Methods included from Rackspace

#define_rackspace_api, #define_rackspace_cloudservers_region, #define_rackspace_files_region, #define_rackspace_region, #define_rackspace_service_region

Instance Method Details

#check_hostkey(shell, ip) ⇒ Object

Public: Determine the ssh hostkey visible to a given host from an IP.

shell - SSH object logged in to a host. ip - String containing an ip (or hostname) for which to get a key.

Returns a String containing the key given by the host, or false if none given.



498
499
500
501
502
503
504
505
# File 'lib/cloudflock/app/common/servers.rb', line 498

def check_hostkey(shell, ip)
  ssh_cmd  = 'ssh -o UserKnownHostsFile=/dev/null ' \
             '-o NumberOfPasswordPrompts=0'

  hostkey = shell.query("#{ssh_cmd} #{ip}", 15, true)
  key = hostkey.lines.select { |line| /fingerprint is/.match(line) }
  key.first.to_s.strip.gsub(/.*fingerprint is /, '').gsub(/\.$/, '')
end

#cleanup_destination(shell, profile) ⇒ Object

Public: Perform post-migration cleanup tasks.

shell - SSH object logged in to the target host. profile - CPE describing the platform in question.

Returns nothing.



513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
# File 'lib/cloudflock/app/common/servers.rb', line 513

def cleanup_destination(shell, profile)
  UI.spinner('Performing post-migration cleanup') do
    cleanup        = Cleanup.new(profile)
    chroot_command = "chroot #{MOUNT_POINT} /bin/sh -C " \
                     "#{DATA_DIR}/chroot.sh"

    restore_rackspace_users(cleanup)

    shell.as_root("mkdir #{DATA_DIR}")
    shell.as_root("cat <<EOF> #{DATA_DIR}/pre.sh\n#{cleanup.pre_s}\nEOF")
    shell.as_root("cat <<EOF> #{DATA_DIR}/post.sh\n#{cleanup.post_s}\nEOF")

    chroot = "cat <<EOF> #{MOUNT_POINT}#{DATA_DIR}/chroot.sh\n" \
             "#{cleanup.chroot_s}\nEOF"
    shell.as_root("mkdir -p #{MOUNT_POINT}#{DATA_DIR}")
    shell.as_root(chroot)

    shell.as_root("/bin/sh #{DATA_DIR}/pre.sh", 0)
    shell.as_root(chroot_command, 0)
    shell.as_root("/bin/sh #{DATA_DIR}/post.sh", 0)

    cleanup_rackspace_server(shell)
  end
end

#cleanup_rackspace_server(shell) ⇒ Object

Public: Restore any users added by Rackspace automation in order to maintain access on hosts which are meant to be managed by Rackspace.

shell - SSH object logged in to the target host.

Returns nothing.



557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
# File 'lib/cloudflock/app/common/servers.rb', line 557

def cleanup_rackspace_server(shell)
  if restore_user(shell, 'rackconnect')
    sudoers         = "rackconnect ALL=(ALL) NOPASSWD: ALL\n" \
                      "Defaults:rackconnect !requiretty"
    sudoers_command = "cat <<EOF >> #{MOUNT_POINT}/etc/sudoers\n\n" \
                      "#{sudoers}\nEOF"

    shell.as_root(sudoers_command)
  end

  if restore_user(shell, 'rack')
    sudoers         = "rack ALL=(ALL) NOPASSWD: ALL"
    sudoers_command = "cat <<EOF >> #{MOUNT_POINT}/etc/sudoers\n\n" \
                      "#{sudoers}\nEOF"
    shell.as_root(sudoers_command)
  end
end

#configure_ips(shell, profile) ⇒ Object

Public: For each IP detected on the source host, perform IP remediation on the destination host post-migration. Allow the list of IPs and the list of directories to target to be overridden by the user.

shell - SSH object logged in to the target host. profile - Profile containing IPs gathered from the source host.

Returns nothing.



612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
# File 'lib/cloudflock/app/common/servers.rb', line 612

def configure_ips(shell, profile)
  destination_profile = CloudFlock::Task::ServerProfile.new(shell)
  source_ips          = profile.select_entries(/IP Usage/, /./)
  destination_ips     = destination_profile.select_entries(/IP Usage/, /./)
  target_directories  = ['/etc']

  puts "Detected IPs on the source: #{source_ips.join(', ')} "
  if UI.prompt_yn('Edit IP list? (Y/N)', default_answer: 'N')
    source_ips = edit_ip_list(source_ips)
  end

  puts 'By default only config files under /etc will be remediated.  '
  if UI.prompt_yn('Edit remediation targets? (Y/N)', default_answer: 'N')
    target_directories = edit_directory_list(target_directories)
  end

  puts "Detected IPs on the destination: #{destination_ips.join(', ')}"
  source_ips.each do |ip|
    appropriate = destination_ips.select do |dest_ip|
      Addrinfo.ip(ip).ipv4_private? == Addrinfo.ip(dest_ip).ipv4_private?
    end
    suggested = appropriate.first || destination_ips.first
    remediate_ip(shell, ip, suggested, target_directories)
  end
end

#define_compute_flavor(api, profile, constrained = true) ⇒ Object

Public: Have the user select from a list of available flavors to provision a new host.

api - Authenticated Fog API instance. profile - Profile of the source host. constrained - Whether the list should be constrained to flavors which

appear to be appropriate. (default: true)

Returns a String.



158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/cloudflock/app/common/servers.rb', line 158

def define_compute_flavor(api, profile, constrained = true)
  flavor_list = filter_compute_flavors(api, profile, constrained)

  puts "Suggested flavor: #{UI.blue { flavor_list.first[:name] }}"
  if UI.prompt_yn('Use this flavor? (Y/N)', default_answer: 'Y')
    return flavor_list.first[:id]
  end

  puts generate_selection_table(flavor_list, constrained)
  flavor = UI.prompt('Flavor to provision', valid_answers: [/^\d+$/, 'A'])
  return flavor_list[flavor.to_i][:id] unless /A/.match(flavor)

  define_compute_flavor(api, profile, false)
end

#define_compute_image(api, profile, constrained = true) ⇒ Object

Public: Have the user select from a list of available images to provision a new host.

api - Authenticated Fog API instance. profile - Profile of the source host. constrained - Whether the list should be constrained to flavors which

appear to be appropriate. (default: true)

Returns a String.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/cloudflock/app/common/servers.rb', line 105

def define_compute_image(api, profile, constrained = true)
  image_list = filter_compute_images(api, profile, constrained)
  if image_list.length == 1
    puts "Suggested image: #{UI.blue { image_list.first[:name] }}"
    if UI.prompt_yn('Use this image? (Y/N)', default_answer: 'Y')
      return image_list.first[:id]
    end
  elsif image_list.length > 1
    puts generate_selection_table(image_list, constrained)

    image = UI.prompt('Image to provision', valid_answers: [/^\d+$/, 'A'])
    return image_list[image.to_i][:id] unless /A/.match(image)
  end

  define_compute_image(api, profile, false)
end

#define_compute_name(profile) ⇒ Object

Public: Prompt user for the name of a new host to be created, presenting the hostname of the source host as a default option.

profile - Profile of the source host.

Returns a String.



202
203
204
205
206
207
# File 'lib/cloudflock/app/common/servers.rb', line 202

def define_compute_name(profile)
  name = profile.select_entries(/System/, 'Hostname').join

  new_name = UI.prompt("Name", default_answer: name, allow_empty: false)
  new_name.gsub(/[^a-zA-Z0-9_-]/, '-')
end

#define_destination(host) ⇒ Object

Public: Collect information about the destination server to which data will be migrated.

host - Hash containing any options which may pertain to the host.

Returns a Hash containing information pertinent to logging in to a host.



52
53
54
55
56
57
58
59
60
# File 'lib/cloudflock/app/common/servers.rb', line 52

def define_destination(host)
  host.select! { |key| /dest_/.match(key) }
  host = host.reduce({}) do |c, e|
    key = e[0].to_s
    c[key.gsub(/dest_/, '').to_sym] = e[1]
    c
  end
  define_host(host, 'Destination')
end

#define_host(host, name) ⇒ Object

Public: Collect information about a named server to be migrated.

opts - Hash containing any applicable options mappings for the server in

question.

name - String containing the name/description for the host.

Returns a Hash containing information pertinent to logging in to a host.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/cloudflock/app/common/servers.rb', line 69

def define_host(host, name)
  host = host.dup
  check_option(host, :hostname, "#{name} host")
  check_option(host, :port, "#{name} SSH port", default_answer: '22')
  check_option(host, :username, "#{name} username", default_answer: 'root')
  check_option_pw(host, :password, "#{name} password",
                  default_answer: '', allow_empty: true)

  key_path = File.join(Dir.home, '.ssh', 'id_rsa')
  key_path = '' unless File.exists?(key_path)
  check_option_fs(host, :ssh_key, "#{name} SSH Key",
                  default_answer: key_path, allow_empty: true)

  # Using sudo is only applicable if the user isn't root
  host[:sudo] = false if host[:username] == 'root'
  check_option(host, :sudo, 'Use sudo? (Y/N)', default_answer: 'Y')

  # If non-root and using su, the root password is needed
  if host[:username] == 'root' || host[:sudo]
    host[:root_password] = host[:password]
  else
    check_option_pw(host, :root_password, 'Password for root')
  end

  host
end

#define_source(host) ⇒ Object

Public: Collect information about the source server to be migrated.

host - Hash containing any options which may pertain to the host.

Returns a Hash containing information pertinent to logging in to a host.



42
43
44
# File 'lib/cloudflock/app/common/servers.rb', line 42

def define_source(host)
  define_host(host, 'Source')
end

#determine_target_address(source_shell, dest_shell) ⇒ Object

Public: Determine what address should be used when connecting from source to destination for the purpose of a migration. Prefer RFC1918 networks.

source_shell - SSH object logged in to the source host. dest_shell - SSH object logged in to the destination host.

Returns a String containing the appropriate address.



475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# File 'lib/cloudflock/app/common/servers.rb', line 475

def determine_target_address(source_shell, dest_shell)
  hostname = dest_shell.hostname
  host_key = check_hostkey(dest_shell, '127.0.0.1')

  ips = dest_shell.query('ifconfig')
  ips = ips.lines.select { |line| /inet[^6]/.match(line) }

  ips.map! { |line| line.strip.split(/\s+/)[1] }
  ips.map! { |ip| ip.gsub(/[^0-9\.]/, '') }

  ips.select! { |ip| check_hostkey(source_shell, ip) == host_key }
  return ips.last if ips.any?

  hostname
end

#filter_compute_flavors(api, profile, constrained) ⇒ Object

Public: Filter available flavors to those expected to be appropriate for a given amount of resource usage.

api - Authenticated Fog API instance. profile - Profile of the source host. constrained - Whether the list should be constrained to flavors which

appear to be appropriate.

Returns an Array of Hashes mapping :name to the flavor name and :id to the flavor’s internal id.



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/cloudflock/app/common/servers.rb', line 183

def filter_compute_flavors(api, profile, constrained)
  flavor_list = api.flavors.to_a
  if constrained
    hdd = profile.select_entries(/Storage/, /Usage/)
    ram = profile.select_entries(/Memory/, /Used/)
    hdd = hdd.first.to_i
    ram = ram.first.to_i

    flavor_list.select! { |flavor| flavor.disk > hdd && flavor.ram > ram }
  end
  flavor_list.map! { |flavor| { name: flavor.name, id: flavor.id } }
end

#filter_compute_images(api, profile, constrained) ⇒ Object

Public: Filter available images to those expected to be appropriate for a given amount of resource usage.

api - Authenticated Fog API instance. profile - Profile of the source host. constrained - Whether the list should be constrained to images which

appear to be appropriate.

Returns an Array of Hashes mapping :name to the image name and :id to the image’s internal id.



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/cloudflock/app/common/servers.rb', line 132

def filter_compute_images(api, profile, constrained)
  image_list = api.images.to_a
  if constrained
    cpe = profile.cpe
    search = [cpe.vendor, cpe.version]
    search.map! { |s| Regexp.new(s, Regexp::IGNORECASE) }

    image_list.select! do |image|
      search.reduce(true) { |c,e| e.match(image.name) && c }
    end
  end
  image_list.map! { |image| { name: image.name, id: image.id } }
rescue Excon::Errors::Timeout
  retry if retry_prompt('Unable to fetch a list of available images.')
  raise
end

#generate_keypair(shell) ⇒ Object

Public: Create a temporary ssh key to be used for passwordless access to the destination host.

shell - SSH object logged in to the source host.

Returns a String containing the host’s new ssh public key.



388
389
390
391
392
# File 'lib/cloudflock/app/common/servers.rb', line 388

def generate_keypair(shell)
  shell.as_root("mkdir #{DATA_DIR}")
  shell.as_root("ssh-keygen -b 4096 -q -t rsa -f #{PRIVATE_KEY} -P ''")
  shell.as_root("cat #{PUBLIC_KEY}")
end

#generate_selection_table(options, constrained) ⇒ Object

Public: Create a printable table with options to be presented to a user.

options - Array of Hashes containing columns to be desplayed, with

the following keys:
:selection_id - ID for the user to select the option.
:name         - String containing the option's name.

constrained - Whether the table is constrained (and a “View All” option

is appropriate).

Returns a String.



219
220
221
222
223
224
225
226
# File 'lib/cloudflock/app/common/servers.rb', line 219

def generate_selection_table(options, constrained)
  options = options.each_with_index.map do |option, index|
    { selection_id: index.to_s, name: option[:name] }
  end
  options << { selection_id: 'A', name: 'View All' } if constrained
  labels = { selection_id: 'ID', name: 'Name' }
  UI.build_grid(options, labels)
end

#get_host_details(host) ⇒ Object

Public: Get details for a Fog::Compute instance.

host - Fog::Compute instance.

Returns a Hash containing the host’s address and root password.



287
288
289
290
291
# File 'lib/cloudflock/app/common/servers.rb', line 287

def get_host_details(host)
  { hostname: host.ipv4_address,
    password: host.password,
    root_password: host.password }
end

#managed_wait(host) ⇒ Object

Public: Wait for a Rackspace Cloud instance with Managed service level to finish post-provisioning automation.

host - Fog::Compute instance.

Returns nothing.



272
273
274
275
276
277
278
279
280
# File 'lib/cloudflock/app/common/servers.rb', line 272

def managed_wait(host)
  finished = '/tmp/rs_managed_cloud_automation_complete'
  connect = { username: 'root', port: '22' }.merge(get_host_details(host))

  ssh = ssh_connect(connect)
  UI.spinner('Waiting for managed cloud automation to complete') do
    ssh.as_root("while [ ! -f #{finished} ]; do sleep 5; done", 3600)
  end
end

#migrate_server(source_shell, dest_shell, exclusions) ⇒ Object

Public: Perform the final preperatory steps necessary as well as the migration.

source_shell - SSH object logged in to the source host. dest_shell - SSH object logged in to the destination host. exclusions - String containing the exclusions list for the migration.

Returns a String containing the host’s new ssh public key.



348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/cloudflock/app/common/servers.rb', line 348

def migrate_server(source_shell, dest_shell, exclusions)
  pubkey = UI.spinner('Generating a keypair for the source environment') do
    generate_keypair(source_shell)
  end

  UI.spinner('Preparing the destination environment') do
    setup_destination(dest_shell, pubkey)
  end

  rsync = UI.spinner('Preparing the source environment') do
    location = setup_source(source_shell, exclusions)
    if location.empty?
      location = transfer_rsync(source_shell, dest_shell)
    end

    location
  end

  dest_address = UI.spinner('Checking for ServiceNet') do
    determine_target_address(source_shell, dest_shell)
  end

  rsync = "#{rsync} -azP -e 'ssh #{SSH_ARGUMENTS} -i #{PRIVATE_KEY}' " +
          "--exclude-from='#{EXCLUSIONS}' / #{dest_address}:#{MOUNT_POINT}"

  UI.spinner('Performing rsync migration') do
    2.times do
      # TODO: this dies in exceptional cases
      source_shell.as_root(rsync, 7200)
      source_shell.as_root("sed -i 's/\/var\/log//g' #{EXCLUSIONS}")
    end
  end
end

#provision_compute(api, managed, compute_spec) ⇒ Object

Public: Create a new compute instance via API.

api - Authenticated Fog API instance. managed - Whether the instance is expected to be managed (if

Rackspace public cloud).

compute_spec - Hash containing parameters to pass via the API call.

Returns a Hash with information necessary to log in to the new host.



236
237
238
239
240
241
242
243
244
245
246
# File 'lib/cloudflock/app/common/servers.rb', line 236

def provision_compute(api, managed, compute_spec)
  host = api.servers.create(compute_spec)
  provision_wait(host, compute_spec[:name])
  managed_wait(host) if managed
  rescue_compute(host)

  { username: 'root', port: '22' }.merge(get_host_details(host))
rescue Fog::Errors::TimeoutError, Excon::Errors::Timeout
  retry if retry_prompt('Provisioning failed.')
  raise
end

#provision_wait(host, name) ⇒ Object

Public: Wait for a Rackspace Cloud instance to be provisioned.

host - Fog::Compute instance. name - String containing the name of the server.

Returns nothing.



254
255
256
257
258
259
260
261
262
263
264
# File 'lib/cloudflock/app/common/servers.rb', line 254

def provision_wait(host, name)
  UI.spinner("Waiting for #{name} to provision") do
    host.wait_for { ready? }
  end
rescue Fog::Errors::TimeoutError, Excon::Errors::Timeout
  error = UI.red { 'Provisioning is taking an unusually long time.' }

  retry if UI.prompt_yn("#{error}  Continue waiting? (Y/N)",
                        default_answer: 'Y')
  raise
end

#remediate_ip(shell, source_ip, default_ip, target_directories) ⇒ Object

Public: Perform post-migration IP remediation in configuration files for a given IP.

shell - SSH object logged in to the target host. source_ip - String containing the IP to replace. default_ip - String containing an IP to suggest as the default

replacement.

target_directories - Array containing Strings of directories to target

for IP remediation.

Returns nothing.



649
650
651
652
653
654
655
656
657
658
659
660
# File 'lib/cloudflock/app/common/servers.rb', line 649

def remediate_ip(shell, source_ip, default_ip, target_directories)
  replace = UI.prompt("Replacement for #{source_ip}",
                      allow_empty: true, default_answer: default_ip).strip
  return if replace.empty? || target_directories.empty?

  sed = "sed -i 's/#{source_ip}/#{replace}/g' {} \\;"
  UI.spinner("Remediating IP: #{source_ip}") do
    target_directories.each do |dir|
      shell.as_root("find #{MOUNT_POINT}#{dir} -type f -exec #{sed}", 7200)
    end
  end
end

#rescue_compute(host) ⇒ Object

Public: Bring a host into Rescue mode.

host - Fog::Compute instance.

Returns nothing.



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/cloudflock/app/common/servers.rb', line 298

def rescue_compute(host)
  host.rescue
  begin
    UI.spinner("Waiting for Rescue Mode (password: #{host.password})") do
      host.wait_for { state == 'RESCUE' }
    end
  rescue Fog::Errors::TimeoutError
    retry if retry_prompt('Timeout exceeded waiting for the host.')

    host.destroy
    raise
  end
rescue Excon::Errors::Timeout
  retry if retry_prompt('API timed out waiting for server status update.')
end

#restore_rackspace_users(cleanup) ⇒ Object

Public: Restore any users added by Rackspace automation in order to maintain access on hosts which are meant to be managed by Rackspace.

cleanup - Cleanup object to which to add chroot tasks.

Returns nothing.



544
545
546
547
548
549
# File 'lib/cloudflock/app/common/servers.rb', line 544

def restore_rackspace_users(cleanup)
  ['rack', 'rackconnect'].each do |user|
    check_user = "grep '^#{user}:' /etc/passwd.migration"
    cleanup.chroot_step("#{check_user} \&\& useradd #{user}")
  end
end

#restore_user(shell, user) ⇒ Object

Public: If a given user had previously existed, create that user in the current environment and copy password hash from a backup copy of /etc/shadow.

shell - SSH instance logged in to the target host. user - String containing the username to restore.

Returns true if the user was successfully restored, false otherwise.



583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/cloudflock/app/common/servers.rb', line 583

def restore_user(shell, user)
  passwd_path = "#{MOUNT_POINT}/etc/passwd"
  shadow_path = "#{MOUNT_POINT}/etc/shadow"

  present = ['passwd', 'shadow'].map do |file|
    path = "#{MOUNT_POINT}/etc/#{file}.migration"
    shell.as_root("grep '^#{user}:' #{path}")
  end
  return false if present.include?('')

  passwd, shadow = present
  uid, gid       = passwd.split(/:/)[2..3]

  steps = ["chown -R #{uid}.#{gid} #{MOUNT_POINT}/home/#{user}",
           "sed -i '/^#{user}:.*$/d' #{shadow_path}",
           "printf '#{shadow}\\n' >> #{shadow_path}"]
  steps.each { |step| shell.as_root(step) }

  true
end

#retry_prompt(message) ⇒ Object

Public: Display a failure message to the user and prompt whether to retry.

message - String containing a failure message.

Returns true or false indicating whether the user wishes to retry.



668
669
670
671
# File 'lib/cloudflock/app/common/servers.rb', line 668

def retry_prompt(message)
  error = UI.red { "#{message}  Try again? (Y/N)" }
  UI.prompt_yn(error, default_answer: 'Y')
end

#setup_destination(shell, pubkey) ⇒ Object

Public: Prepare the destination host for migration by verifying that rsync is present, mounting the primary disk to /mnt/migration_target, installing a temporary ssh public key for root, and backing up the original passwd, shadow and group files.

shell - SSH object logged in to the destination host. pubkey - String containing the text of the ssh public key to install for

root.

Returns nothing.



438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/cloudflock/app/common/servers.rb', line 438

def setup_destination(shell, pubkey)
  preserve_files = ['passwd', 'shadow', 'group']
  path = "#{MOUNT_POINT}/etc"

  # TODO: Dynamic mountpoint/block device support
  # Presently mount point and block device are hard-coded.  This will be
  # changed in a future release.
  shell.as_root("mkdir -p #{MOUNT_POINT}")
  shell.as_root("mount -o acl /dev/xvdb1 #{MOUNT_POINT}")

  preserve_files.each do |file|
    original = "#{path}/#{file}"
    backup   = "#{original}.migration"
    shell.as_root("[ -f #{backup} ] || /bin/cp -a #{original} #{backup}")
  end

  # TODO: Better distro support
  # Only Debian- and RedHat-based Unix hosts support automatic rsync
  # installation at this time.  This will be fixed in a future release.
  unless /rsync error/.match(shell.as_root('rsync'))
    package_manager = shell.as_root('which {yum,apt-get} 2>/dev/null')
    raise NoRsyncAvailable if package_manager.empty?
    shell.as_root("#{package_manager} install rsync -y", 300)
  end

  ssh_key = "mkdir $HOME/.ssh; chmod 0700 $HOME/.ssh; printf " +
            "'#{pubkey}\\n' >> $HOME/.ssh/authorized_keys"
  shell.as_root(ssh_key)
end

#setup_source(shell, exclusions) ⇒ Object

Public: Prepare the source host for migration by populating the exclusions list in the file located at EXCLUSIONS and determining the location of rsync on the system.

shell - SSH object logged in to the source host. exclusions - String containing the exclusions list for the source host.

Returns a String containing path to rsync on the host if present.



402
403
404
405
# File 'lib/cloudflock/app/common/servers.rb', line 402

def setup_source(shell, exclusions)
  shell.as_root("cat <<EOF> #{EXCLUSIONS}\n#{exclusions}\nEOF")
  shell.as_root('which rsync 2>/dev/null')
end

#ssh_connect(host, attempts = 5) ⇒ Object

Public: Connect to a host via SSH, automatically retrying a set number of times, and prompting whether to continue trying beyond that.

host - Hash containing information about the host. Defaults are

defined in the CloudFlock::Remote::SSH Class.

attempts - Number of times to retry connecting before alerting the user

to failures and asking whether to continue. (Default: 5)

Returns an SSH Object.



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/cloudflock/app/common/servers.rb', line 323

def ssh_connect(host, attempts = 5)
  attempt = 0

  UI.spinner("Logging in to #{host[:hostname]}") do
    begin
      SSH.new(host)
    rescue Net::SSH::Disconnect
      sleep 10
      attempt += 1
      retry if attempt < 5
    end
  end
rescue Net::SSH::Disconnect
  retry if retry_prompt('Unable to establish a connection.')
  raise
end

#transfer_rsync(source_shell, dest_shell) ⇒ Object

Public: Transfer rsync from the destination host to the source host to facilitate the migration.

source_shell - SSH object logged in to the source host. dest_shell - SSH object logged in to the source host.

Raises NoRsyncAvailable if rsync doesn’t exist on the destination host.

Returns a String.

Raises:



416
417
418
419
420
421
422
423
424
425
426
# File 'lib/cloudflock/app/common/servers.rb', line 416

def transfer_rsync(source_shell, dest_shell)
  host     = dest_shell.hostname
  location = dest_shell.as_root('which rsync')
  raise(NoRsyncAvailable, Errstr::NO_RSYNC) if location.empty?

  scp = "scp #{SSH_ARGUMENTS} -i #{PRIVATE_KEY} #{host}:#{location} " +
        "#{DATA_DIR}/"

  source_shell.as_root(scp)
  "#{DATA_DIR}/rsync"
end