cloudflock 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +15 -0
  2. data/bin/cloudflock +7 -1
  3. data/bin/cloudflock-files +2 -14
  4. data/bin/cloudflock-profile +3 -15
  5. data/bin/cloudflock-servers +3 -22
  6. data/bin/cloudflock.default +3 -22
  7. data/lib/cloudflock/app/common/cleanup/unix.rb +23 -0
  8. data/lib/cloudflock/app/common/cleanup.rb +107 -0
  9. data/lib/cloudflock/app/common/exclusions/unix/centos.rb +18 -0
  10. data/lib/cloudflock/app/common/exclusions/unix/redhat.rb +18 -0
  11. data/lib/cloudflock/app/common/exclusions/unix.rb +58 -0
  12. data/lib/cloudflock/app/common/exclusions.rb +57 -0
  13. data/lib/cloudflock/app/common/platform_action.rb +59 -0
  14. data/lib/cloudflock/app/common/rackspace.rb +63 -0
  15. data/lib/cloudflock/app/common/servers.rb +673 -0
  16. data/lib/cloudflock/app/files-migrate.rb +246 -0
  17. data/lib/cloudflock/app/server-migrate.rb +327 -0
  18. data/lib/cloudflock/app/server-profile.rb +130 -0
  19. data/lib/cloudflock/app.rb +87 -0
  20. data/lib/cloudflock/error.rb +6 -19
  21. data/lib/cloudflock/errstr.rb +31 -0
  22. data/lib/cloudflock/remote/files.rb +82 -22
  23. data/lib/cloudflock/remote/ssh.rb +234 -278
  24. data/lib/cloudflock/target/servers/platform.rb +92 -115
  25. data/lib/cloudflock/target/servers/profile.rb +331 -340
  26. data/lib/cloudflock/task/server-profile.rb +651 -0
  27. data/lib/cloudflock.rb +6 -8
  28. metadata +49 -68
  29. data/lib/cloudflock/interface/cli/app/common/servers.rb +0 -128
  30. data/lib/cloudflock/interface/cli/app/files.rb +0 -179
  31. data/lib/cloudflock/interface/cli/app/servers/migrate.rb +0 -491
  32. data/lib/cloudflock/interface/cli/app/servers/profile.rb +0 -88
  33. data/lib/cloudflock/interface/cli/app/servers.rb +0 -2
  34. data/lib/cloudflock/interface/cli/console.rb +0 -213
  35. data/lib/cloudflock/interface/cli/opts/servers.rb +0 -20
  36. data/lib/cloudflock/interface/cli/opts.rb +0 -87
  37. data/lib/cloudflock/interface/cli.rb +0 -15
  38. data/lib/cloudflock/target/servers/data/exceptions/base.txt +0 -44
  39. data/lib/cloudflock/target/servers/data/exceptions/platform/amazon.txt +0 -10
  40. data/lib/cloudflock/target/servers/data/exceptions/platform/centos.txt +0 -7
  41. data/lib/cloudflock/target/servers/data/exceptions/platform/debian.txt +0 -0
  42. data/lib/cloudflock/target/servers/data/exceptions/platform/redhat.txt +0 -7
  43. data/lib/cloudflock/target/servers/data/exceptions/platform/suse.txt +0 -1
  44. data/lib/cloudflock/target/servers/data/post-migration/chroot/base.txt +0 -1
  45. data/lib/cloudflock/target/servers/data/post-migration/chroot/platform/amazon.txt +0 -19
  46. data/lib/cloudflock/target/servers/data/post-migration/pre/base.txt +0 -3
  47. data/lib/cloudflock/target/servers/data/post-migration/pre/platform/amazon.txt +0 -4
  48. data/lib/cloudflock/target/servers/migrate.rb +0 -466
  49. data/lib/cloudflock/target/servers/platform/v1.rb +0 -97
  50. data/lib/cloudflock/target/servers/platform/v2.rb +0 -93
  51. data/lib/cloudflock/target/servers.rb +0 -5
  52. 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