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