cloudflock 0.6.1 → 0.7.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 +15 -0
- data/bin/cloudflock +7 -1
- data/bin/cloudflock-files +2 -14
- data/bin/cloudflock-profile +3 -15
- data/bin/cloudflock-servers +3 -22
- data/bin/cloudflock.default +3 -22
- data/lib/cloudflock/app/common/cleanup/unix.rb +23 -0
- data/lib/cloudflock/app/common/cleanup.rb +107 -0
- data/lib/cloudflock/app/common/exclusions/unix/centos.rb +18 -0
- data/lib/cloudflock/app/common/exclusions/unix/redhat.rb +18 -0
- data/lib/cloudflock/app/common/exclusions/unix.rb +58 -0
- data/lib/cloudflock/app/common/exclusions.rb +57 -0
- data/lib/cloudflock/app/common/platform_action.rb +59 -0
- data/lib/cloudflock/app/common/rackspace.rb +63 -0
- data/lib/cloudflock/app/common/servers.rb +673 -0
- data/lib/cloudflock/app/files-migrate.rb +246 -0
- data/lib/cloudflock/app/server-migrate.rb +327 -0
- data/lib/cloudflock/app/server-profile.rb +130 -0
- data/lib/cloudflock/app.rb +87 -0
- data/lib/cloudflock/error.rb +6 -19
- data/lib/cloudflock/errstr.rb +31 -0
- data/lib/cloudflock/remote/files.rb +82 -22
- data/lib/cloudflock/remote/ssh.rb +234 -278
- data/lib/cloudflock/target/servers/platform.rb +92 -115
- data/lib/cloudflock/target/servers/profile.rb +331 -340
- data/lib/cloudflock/task/server-profile.rb +651 -0
- data/lib/cloudflock.rb +6 -8
- metadata +49 -68
- data/lib/cloudflock/interface/cli/app/common/servers.rb +0 -128
- data/lib/cloudflock/interface/cli/app/files.rb +0 -179
- data/lib/cloudflock/interface/cli/app/servers/migrate.rb +0 -491
- data/lib/cloudflock/interface/cli/app/servers/profile.rb +0 -88
- data/lib/cloudflock/interface/cli/app/servers.rb +0 -2
- data/lib/cloudflock/interface/cli/console.rb +0 -213
- data/lib/cloudflock/interface/cli/opts/servers.rb +0 -20
- data/lib/cloudflock/interface/cli/opts.rb +0 -87
- data/lib/cloudflock/interface/cli.rb +0 -15
- data/lib/cloudflock/target/servers/data/exceptions/base.txt +0 -44
- data/lib/cloudflock/target/servers/data/exceptions/platform/amazon.txt +0 -10
- data/lib/cloudflock/target/servers/data/exceptions/platform/centos.txt +0 -7
- data/lib/cloudflock/target/servers/data/exceptions/platform/debian.txt +0 -0
- data/lib/cloudflock/target/servers/data/exceptions/platform/redhat.txt +0 -7
- data/lib/cloudflock/target/servers/data/exceptions/platform/suse.txt +0 -1
- data/lib/cloudflock/target/servers/data/post-migration/chroot/base.txt +0 -1
- data/lib/cloudflock/target/servers/data/post-migration/chroot/platform/amazon.txt +0 -19
- data/lib/cloudflock/target/servers/data/post-migration/pre/base.txt +0 -3
- data/lib/cloudflock/target/servers/data/post-migration/pre/platform/amazon.txt +0 -4
- data/lib/cloudflock/target/servers/migrate.rb +0 -466
- data/lib/cloudflock/target/servers/platform/v1.rb +0 -97
- data/lib/cloudflock/target/servers/platform/v2.rb +0 -93
- data/lib/cloudflock/target/servers.rb +0 -5
- data/lib/cloudflock/version.rb +0 -3
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'cloudflock/remote/files'
|
4
|
+
require 'cloudflock/app/common/rackspace'
|
5
|
+
require 'cloudflock/app'
|
6
|
+
|
7
|
+
module CloudFlock; module App
|
8
|
+
# Public: The FilesMigrate class provides the interface to perform migrations
|
9
|
+
# to and from Cloud Files containers, S3 buckets, and local file stores.
|
10
|
+
class FilesMigrate
|
11
|
+
include CloudFlock::App::Rackspace
|
12
|
+
include CloudFlock::Remote
|
13
|
+
|
14
|
+
# Default number of threads to be used to upload staged files.
|
15
|
+
UPLOAD_THREADS = 20
|
16
|
+
|
17
|
+
# Default number of threads to be used to download files to staging area.
|
18
|
+
DOWNLOAD_THREADS = 20
|
19
|
+
|
20
|
+
# Public: Perform the steps necessary to migrate files from one file store
|
21
|
+
# to another.
|
22
|
+
def initialize
|
23
|
+
options = parse_options
|
24
|
+
source_store = define_source
|
25
|
+
dest_store = define_destination
|
26
|
+
|
27
|
+
UI.spinner('Migrating files') do
|
28
|
+
files_migrate(source_store, dest_store, options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# Internal: Gather information for and connect to the source store.
|
35
|
+
#
|
36
|
+
# Returns a CloudFlock::Remote::Files object.
|
37
|
+
def define_source
|
38
|
+
define_api('Source')
|
39
|
+
end
|
40
|
+
|
41
|
+
# Internal: Gather information for and connect to the destination store.
|
42
|
+
#
|
43
|
+
# Returns a CloudFlock::Remote::Files object.
|
44
|
+
def define_destination
|
45
|
+
define_api('Destination', true)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Internal: Obtain information needed to connect to a data store.
|
49
|
+
#
|
50
|
+
# desc - Description of the data store for display purposes.
|
51
|
+
# create - Whether to create non-existing locations. (default: false)
|
52
|
+
#
|
53
|
+
# Returns a CloudFlock::Remote::Files object.
|
54
|
+
def define_api(desc, create = false)
|
55
|
+
store = {}
|
56
|
+
query = "#{desc} provider (rackspace, aws, local)"
|
57
|
+
answers = [/^(?:rackspace|aws|local)$/i]
|
58
|
+
|
59
|
+
provider = UI.prompt(query, valid_answers: answers)
|
60
|
+
|
61
|
+
case provider
|
62
|
+
when /rackspace/i
|
63
|
+
store[:provider] = 'Rackspace'
|
64
|
+
store[:rackspace_username] = UI.prompt('Rackspace username')
|
65
|
+
store[:rackspace_api_key] = UI.prompt('Rackspace API key')
|
66
|
+
when /aws/i
|
67
|
+
store[:provider] = 'AWS'
|
68
|
+
store[:aws_access_key_id] = UI.prompt('AWS Access Key ID')
|
69
|
+
store[:aws_secret_access_key] = UI.prompt('AWS secret access key')
|
70
|
+
when /local/i
|
71
|
+
store[:provider] = 'local'
|
72
|
+
store[:local_root] = UI.prompt("#{desc} location")
|
73
|
+
return CloudFlock::Remote::Files.new(store)
|
74
|
+
end
|
75
|
+
|
76
|
+
api = CloudFlock::Remote::Files.new(store)
|
77
|
+
|
78
|
+
options = api.directories.map do |dir|
|
79
|
+
{ name: dir.key, files: dir.count.to_s }
|
80
|
+
end
|
81
|
+
valid = options.reduce([]) { |c,e| c << e[:name] }
|
82
|
+
|
83
|
+
puts UI.build_grid(options, name: "Directory name", files: "File count")
|
84
|
+
if create
|
85
|
+
selected = UI.prompt("#{desc} directory")
|
86
|
+
unless api.directories.select { |dir| dir.key == selected }.any?
|
87
|
+
api.directories.create(key: selected)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
selected = UI.prompt("#{desc} directory", valid_answers: valid)
|
91
|
+
end
|
92
|
+
api.directory = selected
|
93
|
+
|
94
|
+
api
|
95
|
+
end
|
96
|
+
|
97
|
+
# Internal: Set up queue and Mutexes, create threads to manage the transfer
|
98
|
+
# of files from source to destination.
|
99
|
+
#
|
100
|
+
# source_store - CloudFlock::Remote::Files object set up to pull files from
|
101
|
+
# a source directory.
|
102
|
+
# dest_store - CloudFlock::Remote::Files object set up to create files as
|
103
|
+
# they are uploaded.
|
104
|
+
# options - Hash optionally containing overrides for the number of
|
105
|
+
# upload and download threads to use for transfer
|
106
|
+
# concurrency. (default: {})
|
107
|
+
# :upload_threads - Number of upload threads to use.
|
108
|
+
# Overrides UPLOAD_THREADS constant.
|
109
|
+
# :download_threads - Number of download threads to use.
|
110
|
+
# Overrides DOWNLOAD_THREADS constant.
|
111
|
+
#
|
112
|
+
# Returns nothing.
|
113
|
+
def files_migrate(source_store, dest_store, options = {})
|
114
|
+
file_queue = []
|
115
|
+
mutexes = { queue: Mutex.new, finished: Mutex.new }
|
116
|
+
up_threads = options[:upload_threads] || UPLOAD_THREADS
|
117
|
+
down_threads = options[:download_threads] || DOWNLOAD_THREADS
|
118
|
+
|
119
|
+
source = Thread.new do
|
120
|
+
manage_source(source_store, file_queue, mutexes, up_threads)
|
121
|
+
end
|
122
|
+
|
123
|
+
destination = Thread.new do
|
124
|
+
manage_destination(dest_store, file_queue, mutexes, down_threads)
|
125
|
+
end
|
126
|
+
|
127
|
+
[source, destination].each(&:join)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Internal: Create and observe threads which download files from a
|
131
|
+
# non-local file store. If the files exist locally, simply generate a list
|
132
|
+
# of the files in queue.
|
133
|
+
#
|
134
|
+
# source_store - CloudFlock::Remote::Files object set up to pull files from
|
135
|
+
# a source directory.
|
136
|
+
# file_queue - Array in which details regarding files to be transferred
|
137
|
+
# will be stored.
|
138
|
+
# mutexes - Hash containing two Mutexes:
|
139
|
+
# :queue - Coordinates access to file_queue.
|
140
|
+
# :finished - Locked immediately, only unlocked once all
|
141
|
+
# files are queued for transfer.
|
142
|
+
# thread_count - Hash optionally containing overrides for the number of
|
143
|
+
# upload and download threads to use for transfer
|
144
|
+
# concurrency. (default: {})
|
145
|
+
# :upload_threads - Number of upload threads to use.
|
146
|
+
# Overrides UPLOAD_THREADS constant.
|
147
|
+
# :download_threads - Number of download threads to use.
|
148
|
+
# Overrides DOWNLOAD_THREADS constant.
|
149
|
+
#
|
150
|
+
# Returns nothing.
|
151
|
+
def manage_source(source_store, file_queue, mutexes, thread_count)
|
152
|
+
mutexes[:finished].lock
|
153
|
+
|
154
|
+
if source_store.local?
|
155
|
+
source_store.each_file do |file|
|
156
|
+
mutexes[:queue].synchronize do
|
157
|
+
file_queue << { path: "#{source_store.prefix}/#{file.key}",
|
158
|
+
name: file.key, temp: false }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
else
|
162
|
+
source_threads = []
|
163
|
+
file_list_mutex = Mutex.new
|
164
|
+
file_list = source_store.file_list
|
165
|
+
|
166
|
+
thread_count.times do
|
167
|
+
source_threads << Thread.new do
|
168
|
+
while file = file_list_mutex.synchronize { file_list.pop } do
|
169
|
+
temp = Tempfile.new(file.gsub(/\//, ''))
|
170
|
+
temp.write(source_store.get_file(file))
|
171
|
+
temp.close
|
172
|
+
mutexes[:queue].synchronize do
|
173
|
+
file_queue << { path: temp.path, name: file, temp: true }
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
source_threads.each(&:join)
|
180
|
+
end
|
181
|
+
|
182
|
+
mutexes[:finished].unlock
|
183
|
+
end
|
184
|
+
|
185
|
+
# Internal: Create and observe threads which download files from a
|
186
|
+
# non-local file store. If the files exist locally, simply generate a list
|
187
|
+
# of the files in queue.
|
188
|
+
#
|
189
|
+
# dest_store - CloudFlock::Remote::Files object set up to upload files to
|
190
|
+
# a destination directory.
|
191
|
+
# file_queue - Array from which to retrieve details regarding files to be
|
192
|
+
# transferred.
|
193
|
+
# mutexes - Hash containing two Mutexes:
|
194
|
+
# :queue - Coordinates access to file_queue.
|
195
|
+
# :finished - Locked immediately, only unlocked once all
|
196
|
+
# files are queued for transfer.
|
197
|
+
# thread_count - Hash optionally containing overrides for the number of
|
198
|
+
# upload and download threads to use for transfer
|
199
|
+
# concurrency. (default: {})
|
200
|
+
# :upload_threads - Number of upload threads to use.
|
201
|
+
# Overrides UPLOAD_THREADS constant.
|
202
|
+
# :download_threads - Number of download threads to use.
|
203
|
+
# Overrides DOWNLOAD_THREADS constant.
|
204
|
+
#
|
205
|
+
# Returns nothing.
|
206
|
+
def manage_destination(dest_store, file_queue, mutexes, thread_count)
|
207
|
+
dest_threads = []
|
208
|
+
|
209
|
+
thread_count.times do
|
210
|
+
dest_threads << Thread.new do
|
211
|
+
while mutexes[:finished].locked?
|
212
|
+
while file = mutexes[:queue].synchronize { file_queue.pop }
|
213
|
+
content = File.read(file[:path])
|
214
|
+
dest_store.create(key: file[:name], body: content)
|
215
|
+
File.unlink(file[:path]) if file[:temp]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
dest_threads.each(&:join)
|
222
|
+
end
|
223
|
+
|
224
|
+
# Internal: Set up an OptionParser object to recognize options specific to
|
225
|
+
# profiling a remote host.
|
226
|
+
#
|
227
|
+
# Returns nothing.
|
228
|
+
def parse_options
|
229
|
+
options = {}
|
230
|
+
|
231
|
+
CloudFlock::App.parse_options(options) do |opts|
|
232
|
+
opts.separator 'Migrate files between file stores'
|
233
|
+
opts.separator ''
|
234
|
+
|
235
|
+
opts.on('-u', '--upload-threads THREADS',
|
236
|
+
'Number of upload threads to use (default 20)') do |threads|
|
237
|
+
options[:upload_threads] = threads.to_i if threads.to_i > 0
|
238
|
+
end
|
239
|
+
opts.on('-d', '--download-threads THREADS',
|
240
|
+
'Number of download threads to use (default 20)') do |threads|
|
241
|
+
options[:download_threads] = threads.to_i if threads.to_i > 0
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end; end
|
@@ -0,0 +1,327 @@
|
|
1
|
+
require 'cloudflock/app/common/servers'
|
2
|
+
require 'cloudflock/task/server-profile'
|
3
|
+
require 'cloudflock/app'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'fog'
|
6
|
+
|
7
|
+
module CloudFlock; module App
|
8
|
+
# Public: The ServerMigrate class provides the interface to perform one-shot
|
9
|
+
# migrations as a CLI application.
|
10
|
+
class ServerMigrate
|
11
|
+
include CloudFlock::App::Common
|
12
|
+
include CloudFlock::Remote
|
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.
|
16
|
+
def initialize
|
17
|
+
options = parse_options
|
18
|
+
|
19
|
+
source_host = source_connect(options)
|
20
|
+
profile = fetch_profile(source_host)
|
21
|
+
|
22
|
+
puts generate_recommendation(profile)
|
23
|
+
|
24
|
+
dest_host = destination_connect(options, profile)
|
25
|
+
exclusions = build_exclusions(profile.cpe)
|
26
|
+
migrate_server(source_host, dest_host, exclusions)
|
27
|
+
|
28
|
+
source_host.logout!
|
29
|
+
cleanup_destination(dest_host, profile.cpe)
|
30
|
+
configure_ips(dest_host, profile)
|
31
|
+
|
32
|
+
puts UI.bold { UI.blue { "Migration complete to #{dest_host.hostname}"} }
|
33
|
+
rescue
|
34
|
+
puts UI.red { 'An unhandled error was encountered. Details follow:' }
|
35
|
+
raise
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# Internal: Profile a server in order to make accurate recommendations.
|
41
|
+
#
|
42
|
+
# source_ssh - SSH object connected to a Unix host.
|
43
|
+
#
|
44
|
+
# Returns a ServerProfile object.
|
45
|
+
def fetch_profile(source_ssh)
|
46
|
+
UI.spinner("Checking source host") do
|
47
|
+
CloudFlock::Task::ServerProfile.new(source_ssh)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Internal: Collect information needed to connect to the source host for a
|
52
|
+
# migration. Connect to the target host.
|
53
|
+
#
|
54
|
+
# options - Hash containing information to connect to an existing host.
|
55
|
+
#
|
56
|
+
# Returns an SSH object connected to the source host.
|
57
|
+
def source_connect(options)
|
58
|
+
source_host = define_source(options)
|
59
|
+
ssh_connect(source_host)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Internal: Collect information needed to either connect to an existing
|
63
|
+
# host or provision a new one on Rackspace Cloud to be used as a target
|
64
|
+
# for migration. Connect to the target host.
|
65
|
+
#
|
66
|
+
# options - Hash containing information to connect to an existing host.
|
67
|
+
# profile - ServerProfile for the source host.
|
68
|
+
#
|
69
|
+
# Returns an SSH object connected to the target host.
|
70
|
+
def destination_connect(options, profile)
|
71
|
+
if options[:resume]
|
72
|
+
dest_host = define_destination(options)
|
73
|
+
else
|
74
|
+
api = define_rackspace_api
|
75
|
+
managed = UI.prompt_yn('Managed account? (Y/N)', default_answer: 'N')
|
76
|
+
dest_host = create_cloud_instance(api, profile, managed)
|
77
|
+
end
|
78
|
+
|
79
|
+
ssh_connect(dest_host)
|
80
|
+
rescue Excon::Errors::Unauthorized
|
81
|
+
retry if UI.prompt_yn('Login failed. Retry? (Y/N)', default_answer: 'Y')
|
82
|
+
exit
|
83
|
+
end
|
84
|
+
|
85
|
+
# Internal: Provision a new instance on the Rackspace cloud and return
|
86
|
+
# credentials once finished.
|
87
|
+
#
|
88
|
+
# api - Hash containing credentials to interact with the Rackspace
|
89
|
+
# Cloud API.
|
90
|
+
# profile - ServerProfile for the source host.
|
91
|
+
# managed - Whether the account is a Managed Cloud account (needed to know.
|
92
|
+
# whether to wait for post-provisioning automation to finish)
|
93
|
+
#
|
94
|
+
# Returns a Hash containing credentials suitable for logging in via SSH.
|
95
|
+
def create_cloud_instance(api, profile, managed)
|
96
|
+
api = define_rackspace_cloudservers_region(api)
|
97
|
+
compute = Fog::Compute.new(api)
|
98
|
+
image = define_compute_image(compute, profile)
|
99
|
+
flavor = define_compute_flavor(compute, profile)
|
100
|
+
name = define_compute_name(profile)
|
101
|
+
|
102
|
+
compute_spec = { image_id: image, flavor_id: flavor, name: name }
|
103
|
+
provision_compute(compute, managed, compute_spec)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Internal: Generate a recommendation based on the results of profiling a
|
107
|
+
# host.
|
108
|
+
#
|
109
|
+
# profile - ServerProfile for the source host.
|
110
|
+
#
|
111
|
+
# Returns a String.
|
112
|
+
def generate_recommendation(profile)
|
113
|
+
os = profile_os_string(profile)
|
114
|
+
ram = profile_ram_string(profile)
|
115
|
+
hdd = profile_hdd_string(profile)
|
116
|
+
|
117
|
+
"OS: " + UI.bold { os } + "\n" +
|
118
|
+
"RAM: " + UI.bold { ram } + "\n" +
|
119
|
+
"HDD: " + UI.bold { hdd } + "\n" +
|
120
|
+
UI.red { UI.bold { profile.warnings.join("\n") } }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Internal: Build exclusions list based on a host's CPE.
|
124
|
+
#
|
125
|
+
# cpe - CPE object describing a given host.
|
126
|
+
#
|
127
|
+
# Returns a String
|
128
|
+
def build_exclusions(cpe)
|
129
|
+
exclusions = Exclusions.new(cpe)
|
130
|
+
edit = UI.prompt_yn('Edit exclusions list? (Y/N)', default_answer: 'N')
|
131
|
+
exclusions = edit_exclusions(exclusions) if edit
|
132
|
+
|
133
|
+
exclusions.to_s
|
134
|
+
end
|
135
|
+
|
136
|
+
# Internal: Allow editing of the default exclusions for a given platform.
|
137
|
+
#
|
138
|
+
# exclusions - String containing exclusions.
|
139
|
+
#
|
140
|
+
# Returns a String.
|
141
|
+
def edit_exclusions(exclusions)
|
142
|
+
temp_file('exclusions', exclusions)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Internal: Allow editing of a list of IPs.
|
146
|
+
#
|
147
|
+
# ips - Array containing Strings of IPs.
|
148
|
+
#
|
149
|
+
# Returns an Array containing Strings of IPs.
|
150
|
+
def edit_ip_list(ips)
|
151
|
+
temp_file('ips', ips.join("\n")).split(/\s+/)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Internal: Allow editing of a list of target directories.
|
155
|
+
#
|
156
|
+
# dirs - Array containing Strings of paths.
|
157
|
+
#
|
158
|
+
# Returns an Array containing Strings of paths.
|
159
|
+
def edit_directory_list(dirs)
|
160
|
+
temp_file('directories', dirs.join("\n")).split(/\s+/)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Internal: Generate a String describing a host's operating system.
|
164
|
+
#
|
165
|
+
# profile - ServerProfile for the source host.
|
166
|
+
#
|
167
|
+
# Returns a String.
|
168
|
+
def profile_os_string(profile)
|
169
|
+
os = profile.select_entries(/System/, 'OS')
|
170
|
+
os += profile.select_entries(/System/, 'Arch')
|
171
|
+
os.map(&:capitalize).join(' ')
|
172
|
+
end
|
173
|
+
|
174
|
+
# Internal: Generate a String describing a host's memory usage.
|
175
|
+
#
|
176
|
+
# profile - ServerProfile for the source host.
|
177
|
+
#
|
178
|
+
# Returns a String.
|
179
|
+
def profile_ram_string(profile)
|
180
|
+
profile.select_entries(/Memory/, /Used RAM/).join.strip
|
181
|
+
end
|
182
|
+
|
183
|
+
# Internal: Generate a String describing a host's disk usage.
|
184
|
+
#
|
185
|
+
# profile - ServerProfile for the source host.
|
186
|
+
#
|
187
|
+
# Returns a String.
|
188
|
+
def profile_hdd_string(profile)
|
189
|
+
profile.select_entries(/Storage/, /Usage/).join(' ').strip
|
190
|
+
end
|
191
|
+
|
192
|
+
# Internal: Set up a temporary file, open it for editing locally, and read
|
193
|
+
# it back in after finished.
|
194
|
+
#
|
195
|
+
# name - Name to append to the temporary file's path.
|
196
|
+
# content - Content with which the temporary file should be pre-populated.
|
197
|
+
#
|
198
|
+
# BUG: Works only on POSIX-compliant hosts; needs work to support Windows.
|
199
|
+
#
|
200
|
+
# Returns a String.
|
201
|
+
def temp_file(name, content)
|
202
|
+
editor = File.exists?('/usr/bin/editor') ? '/usr/bin/editor' : 'vi'
|
203
|
+
|
204
|
+
temp = Tempfile.new("cloudflock_#{name}")
|
205
|
+
temp.write(content)
|
206
|
+
temp.close
|
207
|
+
|
208
|
+
system("#{editor} #{temp.path}")
|
209
|
+
temp.open
|
210
|
+
result = temp.read
|
211
|
+
temp.close
|
212
|
+
temp.unlink
|
213
|
+
|
214
|
+
result
|
215
|
+
end
|
216
|
+
|
217
|
+
# Internal: Set up an OptionParser object to recognize options specific to
|
218
|
+
# profiling a remote host.
|
219
|
+
#
|
220
|
+
# Returns nothing.
|
221
|
+
def parse_options
|
222
|
+
options = {}
|
223
|
+
|
224
|
+
CloudFlock::App.parse_options(options) do |opts|
|
225
|
+
opts.separator 'Perform host-level migration'
|
226
|
+
opts.separator ''
|
227
|
+
opts.separator 'Options for source definition:'
|
228
|
+
|
229
|
+
begin # Source options
|
230
|
+
opts.on('-h', '--src-host HOST', 'Address for source host') do |host|
|
231
|
+
options[:hostname] = host
|
232
|
+
end
|
233
|
+
|
234
|
+
opts.on('-p', '--src-port PORT',
|
235
|
+
'Source SSH port for source host') do |port|
|
236
|
+
options[:port] = port
|
237
|
+
end
|
238
|
+
|
239
|
+
opts.on('-u', '--src-user USER',
|
240
|
+
'Username for source host') do |user|
|
241
|
+
options[:username] = user
|
242
|
+
end
|
243
|
+
|
244
|
+
opts.on('-a', '--src-password [PASSWORD]',
|
245
|
+
'Password for source host login') do |pass|
|
246
|
+
options[:password] = pass
|
247
|
+
end
|
248
|
+
|
249
|
+
opts.on('-s', '--src-sudo', 'Use sudo to gain root on source host') do
|
250
|
+
options[:sudo] = true
|
251
|
+
end
|
252
|
+
|
253
|
+
opts.on('-n', '--src-no-sudo', 'Use su to gain root on source host') do
|
254
|
+
options[:sudo] = false
|
255
|
+
end
|
256
|
+
|
257
|
+
opts.on('-r', '--src-root-pass PASS',
|
258
|
+
'Password for root user on the source host') do |root|
|
259
|
+
options[:root_password] = root
|
260
|
+
end
|
261
|
+
|
262
|
+
opts.on('-i', '--src-identity IDENTITY',
|
263
|
+
'SSH identity to use for the source host') do |key|
|
264
|
+
options[:ssh_key] = key
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
opts.separator ''
|
269
|
+
opts.separator 'Options for destination (if not using automation):'
|
270
|
+
|
271
|
+
begin # Destination options
|
272
|
+
opts.on('-H', '--dest-host HOST',
|
273
|
+
'Address for destination host') do |host|
|
274
|
+
options[:dest_hostname] = host
|
275
|
+
end
|
276
|
+
|
277
|
+
opts.on('-P', '--dest-port PORT',
|
278
|
+
'Source SSH port for destination host') do |port|
|
279
|
+
options[:dest_port] = port
|
280
|
+
end
|
281
|
+
|
282
|
+
opts.on('-U', '--dest-user USER',
|
283
|
+
'Username for destination host') do |user|
|
284
|
+
options[:dest_username] = user
|
285
|
+
end
|
286
|
+
|
287
|
+
opts.on('-A', '--dest-password [PASSWORD]',
|
288
|
+
'Password for destination host login') do |pass|
|
289
|
+
options[:dest_password] = pass
|
290
|
+
end
|
291
|
+
|
292
|
+
opts.on('-S', '--dest-sudo', 'Use sudo to gain root on destination host') do
|
293
|
+
options[:dest_sudo] = true
|
294
|
+
end
|
295
|
+
|
296
|
+
opts.on('-N', '--dest-no-sudo', 'Use su to gain root on destination host') do
|
297
|
+
options[:dest_sudo] = false
|
298
|
+
end
|
299
|
+
|
300
|
+
opts.on('-R', '--dest-root-pass PASS',
|
301
|
+
'Password for root user on the destination host') do |root|
|
302
|
+
options[:dest_root_password] = root
|
303
|
+
end
|
304
|
+
|
305
|
+
opts.on('-I', '--dest-identity IDENTITY',
|
306
|
+
'SSH identity to use for the destination host') do |key|
|
307
|
+
options[:dest_ssh_key] = key
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
opts.separator ''
|
312
|
+
opts.separator 'Operation options:'
|
313
|
+
|
314
|
+
begin # Operation options
|
315
|
+
opts.on('--resume', '--pre-provisioned',
|
316
|
+
'Migrate over standing host ("resume" mode)') do
|
317
|
+
options[:resume] = true
|
318
|
+
end
|
319
|
+
opts.on('--echo-passwords',
|
320
|
+
'Echo entered passwords to the console') do
|
321
|
+
options[:password_echo] = true
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end; end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'cloudflock/app/common/servers'
|
2
|
+
require 'cloudflock/task/server-profile'
|
3
|
+
require 'cloudflock/app'
|
4
|
+
|
5
|
+
module CloudFlock; module App
|
6
|
+
# Public: The ServerProfile class provides the interface to produce profiles
|
7
|
+
# describing hosts running Unix-like operating systems as a CLI application.
|
8
|
+
class ServerProfile
|
9
|
+
include CloudFlock::App::Common
|
10
|
+
include CloudFlock::Remote
|
11
|
+
|
12
|
+
# Public: Connect to and profile a remote host, then display the gathered
|
13
|
+
# information.
|
14
|
+
def initialize
|
15
|
+
options = parse_options
|
16
|
+
|
17
|
+
source_host = define_source(options)
|
18
|
+
source_ssh = UI.spinner("Logging in to #{source_host[:hostname]}") do
|
19
|
+
SSH.new(source_host)
|
20
|
+
end
|
21
|
+
|
22
|
+
profile = UI.spinner("Checking source host") do
|
23
|
+
CloudFlock::Task::ServerProfile.new(source_ssh)
|
24
|
+
end
|
25
|
+
|
26
|
+
puts generate_report(profile)
|
27
|
+
puts profile.process_list if options[:verbose]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Internal: Generate a "title" String (bold, 15 characters wide).
|
33
|
+
#
|
34
|
+
# tag - String to be turned into a title.
|
35
|
+
#
|
36
|
+
# Returns a String.
|
37
|
+
def title(tag)
|
38
|
+
UI.bold { "%15s" % tag }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Internal: Generate a report containing informational aspects of a host's
|
42
|
+
# profile as well as any warnings profiling the host in question generated.
|
43
|
+
#
|
44
|
+
# profile - Profile object.
|
45
|
+
#
|
46
|
+
# Returns a String.
|
47
|
+
def generate_report(profile)
|
48
|
+
profile_hash = profile.to_hash
|
49
|
+
host_info(profile_hash[:info]) + host_warnings(profile_hash[:warnings])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Internal: Generate a string containing informational aspects of a host's
|
53
|
+
# profile.
|
54
|
+
#
|
55
|
+
# profile - Profile object.
|
56
|
+
#
|
57
|
+
# Returns a String.
|
58
|
+
def host_info(profile)
|
59
|
+
profile.map do |section|
|
60
|
+
"#{UI.blue { UI.bold { section.title } } }\n" +
|
61
|
+
section.entries.reject do |entry|
|
62
|
+
entry.values.to_s.empty?
|
63
|
+
end.
|
64
|
+
map { |entry| title(entry.name) + " #{entry.values}" }.join("\n")
|
65
|
+
end.join("\n\n")
|
66
|
+
end
|
67
|
+
|
68
|
+
# Internal: Generate a string containing each warning produced by profiling
|
69
|
+
# a host.
|
70
|
+
#
|
71
|
+
# warnings - Array containing Strings.
|
72
|
+
#
|
73
|
+
# Returns a String.
|
74
|
+
def host_warnings(warnings)
|
75
|
+
warnings = warnings.map do |entry|
|
76
|
+
"* #{entry}"
|
77
|
+
end.join("\n")
|
78
|
+
|
79
|
+
unless warnings.empty?
|
80
|
+
warnings = UI.red { UI.bold { "\n\nWarnings:\n#{warnings}" } }
|
81
|
+
end
|
82
|
+
warnings
|
83
|
+
end
|
84
|
+
|
85
|
+
# Internal: Set up an OptionParser object to recognize options specific to
|
86
|
+
# profiling a remote host.
|
87
|
+
#
|
88
|
+
# Returns nothing.
|
89
|
+
def parse_options
|
90
|
+
options = {}
|
91
|
+
|
92
|
+
CloudFlock::App.parse_options(options) do |opts|
|
93
|
+
opts.separator 'Generate a report for a host'
|
94
|
+
opts.separator ''
|
95
|
+
|
96
|
+
opts.on('-h', '--host HOST', 'Target host to profile') do |host|
|
97
|
+
options[:hostname] = host
|
98
|
+
end
|
99
|
+
|
100
|
+
opts.on('-p', '--port PORT', 'Port SSH is listening on') do |port|
|
101
|
+
options[:port] = port
|
102
|
+
end
|
103
|
+
|
104
|
+
opts.on('-u', '--user USER', 'Username to log in') do |user|
|
105
|
+
options[:username] = user
|
106
|
+
end
|
107
|
+
|
108
|
+
opts.on('-a', '--password [PASSWORD]', 'Password to log in') do |pass|
|
109
|
+
options[:password] = pass
|
110
|
+
end
|
111
|
+
|
112
|
+
opts.on('-s', '--sudo', 'Use sudo to gain root') do
|
113
|
+
options[:sudo] = true
|
114
|
+
end
|
115
|
+
|
116
|
+
opts.on('-n', '--no-sudo', 'Use su to gain root') do
|
117
|
+
options[:sudo] = false
|
118
|
+
end
|
119
|
+
|
120
|
+
opts.on('-r', '--root-pass PASS', 'Password for root user') do |root|
|
121
|
+
options[:root_password] = root
|
122
|
+
end
|
123
|
+
|
124
|
+
opts.on('-i', '--identity IDENTITY', 'SSH identity to use') do |key|
|
125
|
+
options[:ssh_key] = key
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end; end
|