cloudflock 0.4.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 (33) hide show
  1. data/bin/flock +32 -0
  2. data/bin/flock-profile +17 -0
  3. data/bin/flock-servers +24 -0
  4. data/bin/flock.default +24 -0
  5. data/lib/cloudflock/error.rb +26 -0
  6. data/lib/cloudflock/interface/cli/app/common/servers.rb +128 -0
  7. data/lib/cloudflock/interface/cli/app/servers/migrate.rb +357 -0
  8. data/lib/cloudflock/interface/cli/app/servers/profile.rb +88 -0
  9. data/lib/cloudflock/interface/cli/app/servers.rb +2 -0
  10. data/lib/cloudflock/interface/cli/console.rb +213 -0
  11. data/lib/cloudflock/interface/cli/opts/servers.rb +20 -0
  12. data/lib/cloudflock/interface/cli/opts.rb +87 -0
  13. data/lib/cloudflock/interface/cli.rb +15 -0
  14. data/lib/cloudflock/patch/fog.rb +113 -0
  15. data/lib/cloudflock/remote/ssh.rb +311 -0
  16. data/lib/cloudflock/target/servers/data/exceptions/base.txt +44 -0
  17. data/lib/cloudflock/target/servers/data/exceptions/platform/amazon.txt +10 -0
  18. data/lib/cloudflock/target/servers/data/exceptions/platform/debian.txt +0 -0
  19. data/lib/cloudflock/target/servers/data/exceptions/platform/redhat.txt +5 -0
  20. data/lib/cloudflock/target/servers/data/exceptions/platform/suse.txt +1 -0
  21. data/lib/cloudflock/target/servers/data/post-migration/chroot/base.txt +1 -0
  22. data/lib/cloudflock/target/servers/data/post-migration/chroot/platform/amazon.txt +19 -0
  23. data/lib/cloudflock/target/servers/data/post-migration/pre/base.txt +3 -0
  24. data/lib/cloudflock/target/servers/data/post-migration/pre/platform/amazon.txt +4 -0
  25. data/lib/cloudflock/target/servers/migrate.rb +466 -0
  26. data/lib/cloudflock/target/servers/platform/v1.rb +97 -0
  27. data/lib/cloudflock/target/servers/platform/v2.rb +93 -0
  28. data/lib/cloudflock/target/servers/platform.rb +133 -0
  29. data/lib/cloudflock/target/servers/profile.rb +394 -0
  30. data/lib/cloudflock/target/servers.rb +5 -0
  31. data/lib/cloudflock/version.rb +3 -0
  32. data/lib/cloudflock.rb +10 -0
  33. metadata +128 -0
data/bin/flock ADDED
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ current_path = File.expand_path('../', __FILE__)
4
+ function = ARGV.shift.to_s
5
+ bin = File.basename($0, '.dev')
6
+
7
+ if function.match(/^[^-]/)
8
+ target = "#{current_path}/#{bin}-#{function}"
9
+ else
10
+ ARGV.unshift(function)
11
+ target = "#{current_path}/#{bin}.default"
12
+ end
13
+
14
+ if File.exists?(target)
15
+ load target
16
+ else
17
+ print "Function \"#{function}\" not found. " unless target =~ /\.default$/
18
+ puts "Available functions:"
19
+
20
+ functions = []
21
+ Dir.foreach(current_path) do |file|
22
+ next unless file.match(/^#{bin}-[^.]*$/)
23
+ functions << file.gsub(/^#{bin}-/, '')
24
+ end
25
+
26
+ until functions.empty?
27
+ a, b, c = functions.shift(3)
28
+ puts "%-15s %-15s %-15s" % [a, b, c].map { |i| i.to_s }
29
+ end
30
+
31
+ exit 1
32
+ end
data/bin/flock-profile ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cloudflock/interface/cli/app/servers/profile'
4
+ CLI = CloudFlock::Interface::CLI::Console
5
+ Opts = CloudFlock::Interface::CLI::Opts
6
+
7
+ # Trap C-c to kill the application in a friendly manner
8
+ trap 'INT' do
9
+ puts "\nCaught SIGINT, exiting..."
10
+ exit 1
11
+ end
12
+
13
+ options = Opts.parse
14
+ options[:function] = :profile
15
+ options[:config] = Opts.parse_config_file(options[:config_file])
16
+
17
+ CloudFlock::Interface::CLI::App::Servers::Profile.new(options)
data/bin/flock-servers ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cloudflock/interface/cli/app/servers/migrate'
4
+ CLI = CloudFlock::Interface::CLI::Console
5
+ Opts = CloudFlock::Interface::CLI::Opts
6
+
7
+ # Trap C-c to kill the application in a friendly manner
8
+ trap 'INT' do
9
+ puts "\nCaught SIGINT, exiting..."
10
+ exit 1
11
+ end
12
+
13
+ options = Opts.parse(:servers)
14
+ type_prompt = "Type of migration (servers, opencloud)?"
15
+
16
+ if options[:function].nil?
17
+ options[:function] = CLI.prompt(type_prompt, valid_answers:
18
+ ["servers", "opencloud"],
19
+ default_answer: "opencloud")
20
+ end
21
+ options[:function] = options[:function].to_sym
22
+ options[:config] = Opts.parse_config_file(options[:config_file])
23
+
24
+ CloudFlock::Interface::CLI::App::Servers::Migrate.new(options)
data/bin/flock.default ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'cloudflock/interface/cli/app/servers/migrate'
4
+ CLI = CloudFlock::Interface::CLI::Console
5
+ Opts = CloudFlock::Interface::CLI::Opts
6
+
7
+ # Trap C-c to kill the application in a friendly manner
8
+ trap 'INT' do
9
+ puts "\nCaught SIGINT, exiting..."
10
+ exit 1
11
+ end
12
+
13
+ options = Opts.parse(:servers)
14
+ type_prompt = "Type of migration (servers, opencloud)?"
15
+
16
+ if options[:function].nil?
17
+ options[:function] = CLI.prompt(type_prompt, valid_answers:
18
+ ["servers", "opencloud"],
19
+ default_answer: "opencloud")
20
+ end
21
+ options[:function] = options[:function].to_sym
22
+ options[:config] = Opts.parse_config_file(options[:config_file])
23
+
24
+ CloudFlock::Interface::CLI::App::Servers::Migrate.new(options)
@@ -0,0 +1,26 @@
1
+ module CloudFlock
2
+ module Remote
3
+ class SSH
4
+ class InvalidHostname < StandardError; end
5
+ class ConnectionFailed < StandardError; end
6
+ class LoginFailed < StandardError; end
7
+ class RootFailed < StandardError; end
8
+ end
9
+ end
10
+
11
+ module Target
12
+ module Servers
13
+ module Migrate
14
+ class LongRunFailed < StandardError; end
15
+ end
16
+
17
+ class Platform
18
+ class ValueError < StandardError; end
19
+ class MapUndefined < NameError; end
20
+ end
21
+
22
+ class Profile
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,128 @@
1
+ require 'cloudflock'
2
+ require 'cloudflock/interface/cli'
3
+ require 'cloudflock/remote/ssh'
4
+
5
+ # Public: The ServersCommon module provides common methods for CLI interaction
6
+ # pertaining to interaction with remote (Unix) servers.
7
+ module CloudFlock::Interface::CLI::App::Common::Servers
8
+ include CloudFlock::Target::Servers
9
+
10
+ SSH = CloudFlock::Remote::SSH
11
+ CLI = CloudFlock::Interface::CLI::Console
12
+
13
+ # Internal: Collect information about the source server to be migrated.
14
+ #
15
+ # opts - Hash containing any applicable options mappings for the server in
16
+ # question.
17
+ #
18
+ # Returns a Hash containing information pertinent to logging in to a host.
19
+ def define_source(opts)
20
+ host = {}
21
+
22
+ host[:host] = opts[:source_host] || CLI.prompt("Source host")
23
+ host[:port] = opts[:source_port] || CLI.prompt("Source SSH port",
24
+ default_answer: "22")
25
+ host[:username] = opts[:source_user] || CLI.prompt("Source username",
26
+ default_answer: "root")
27
+ host[:password] = opts[:source_pass] || CLI.prompt("Source password",
28
+ default_answer: "")
29
+
30
+ until host[:public_key].kind_of?(String)
31
+ key = opts[:public_key] || CLI.prompt("SSH Key", default_answer: "")
32
+ if File.file?(File.expand_path(key)) || key.empty?
33
+ host[:public_key] = key
34
+ end
35
+ end
36
+
37
+ # Only need to use sudo if the user isn't root
38
+ if host[:username] == "root"
39
+ host[:sudo] = false
40
+ elsif !opts[:source_sudo].nil?
41
+ host[:sudo] = opts[:source_sudo]
42
+ else
43
+ host[:sudo] = CLI.prompt_yn("Use sudo? (Y/N)", default_answer: "Y")
44
+ end
45
+
46
+ # We need the root pass if non-root and no sudo
47
+ if host[:username] == "root" || host[:sudo]
48
+ host[:root_pass] = host[:password]
49
+ else
50
+ host[:root_pass] = CLI.prompt("Password for root")
51
+ end
52
+
53
+ host
54
+ end
55
+
56
+ # Internal: Collect information about the destination server to target in a
57
+ # migration -- only used for resume functions.
58
+ #
59
+ # Returns a Hash containing information pertinent to logging in to a host.
60
+ def define_destination
61
+ host = Hash.new
62
+
63
+ host[:host] = CLI.prompt("Destination host")
64
+ host[:password] = CLI.prompt("Destination root password")
65
+ host[:pre_steps] = CLI.prompt_yn("Perform pre-migration steps? (Y/N)")
66
+ host[:username] = "root"
67
+
68
+ host
69
+ end
70
+
71
+ # Internal: Initiate a connection to a given host and obtain root privileges.
72
+ #
73
+ # host - Hash containing information for connecting to the host:
74
+ # :host - String containing the location to which to connect.
75
+ # :port - String or Fixnum containing the port on which ssh
76
+ # listens. (default: "22")
77
+ # :username - String containing the username to use when logging in.
78
+ # :password - String containing the password for the above user.
79
+ # :sudo - Boolean value defining whether to use sudo to obtain
80
+ # root. (default: false)
81
+ # :root_pass - String containing the password to use to obtain root,
82
+ # if the user isn't root and sudo isn't used.
83
+ # :verbose - Boolean value defining whether to flush output to
84
+ # STDOUT. (default: false)
85
+ #
86
+ # Returns an SSH object.
87
+ # Raises ArgumentError unless at least host and user are defined.
88
+ def host_login(host)
89
+ if host[:host].nil? || host[:username].nil?
90
+ raise ArgumentError, "Need at least host and username defined"
91
+ end
92
+
93
+ host[:flush_buffer] = host[:verbose] || false
94
+
95
+ ssh = SSH.new(host)
96
+ ssh.get_root(host[:root_pass], host[:sudo])
97
+
98
+ ssh
99
+ end
100
+
101
+ # Internal: Initiate a connection to a destination host.
102
+ #
103
+ # host - Hash containing information for connecting to the host:
104
+ # :host - String containing the location to which to connect.
105
+ # :password - String containing the password for the above user.
106
+ # :verbose - Boolean value defining whether to flush output to
107
+ # STDOUT. (default: false)
108
+ #
109
+ # Returns an SSH object.
110
+ # Raises ArgumentError unless at least host and user are defined.
111
+ def destination_login(host)
112
+ host[:username] = "root"
113
+ message = "Connecting to destination host (password: #{host[:password]})"
114
+ r = 0
115
+
116
+ destination_host = CLI.spinner(message) do
117
+ begin
118
+ host_login(host)
119
+ rescue Timeout::Error
120
+ if (r += 1) < 5
121
+ sleep 300
122
+ retry
123
+ end
124
+ raise
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,357 @@
1
+ require 'fog'
2
+ require 'cloudflock/target/servers'
3
+ require 'cloudflock/interface/cli/app/common/servers'
4
+
5
+ # Public: The Servers::Migrate app provides the interface to Servers migrations
6
+ # (primarily targeting Managed/Unmanaged Rackspace First-gen and Open Cloud,
7
+ # but other migrations are possible) on the CLI.
8
+ class CloudFlock::Interface::CLI::App::Servers::Migrate
9
+ include CloudFlock::Interface::CLI::App::Common::Servers
10
+ CLI = CloudFlock::Interface::CLI::Console
11
+
12
+ # Public: Begin Servers migration on the command line
13
+ #
14
+ # opts - Hash containing options mappings.
15
+ def initialize(opts)
16
+ opencloud = (opts[:function] == :opencloud)
17
+
18
+ resume = opts[:resume]
19
+ source_host_def = define_source(opts[:config])
20
+ source_host_ssh = CLI.spinner("Logging in to #{source_host_def[:host]}") do
21
+ host_login(source_host_def)
22
+ end
23
+
24
+ source_profile = CLI.spinner("Checking source host") do
25
+ profile = Profile.new(source_host_ssh)
26
+ profile.build
27
+ profile
28
+ end
29
+
30
+ if opencloud
31
+ target_platform = Platform::V2
32
+ else
33
+ target_platform = Platform::V1
34
+ end
35
+ platform = target_platform.new(source_profile[:cpe])
36
+ build_target = platform.build_recommendation(source_profile)
37
+ flavor_list = target_platform::FLAVOR_LIST
38
+ default_target = flavor_list[build_target[:flavor]]
39
+
40
+ # Generate and display a brief summary of the server Platform/Profile
41
+ os_tag = source_profile[:cpe].vendor == "Unknown" ? CLI.red : CLI.blue
42
+ ram_qty = default_target[:mem]
43
+ hdd_qty = default_target[:hdd]
44
+ decision_reason = "#{CLI.bold}#{build_target[:flavor_reason]}#{CLI.reset}"
45
+
46
+ puts "OS: #{CLI.bold}#{os_tag}#{platform}#{CLI.reset}"
47
+ puts "---"
48
+ puts "Recommended server:"
49
+ puts "RAM: #{CLI.bold} % 6d MiB#{CLI.reset}" % ram_qty
50
+ puts "HDD: #{CLI.bold} % 7d GB#{CLI.reset}" % hdd_qty
51
+ puts "The reason for this decision is: #{decision_reason}"
52
+ puts "---"
53
+ unless source_profile.warnings.empty?
54
+ print CLI.red + CLI.bold
55
+ source_profile.warnings.each { |warning| puts warning }
56
+ print CLI.reset
57
+ puts "---"
58
+ end
59
+
60
+ if resume
61
+ destination_host_def = define_destination
62
+ migration_exclusions = determine_exclusions(source_profile[:cpe])
63
+ platform.managed = CLI.prompt_yn("Managed service level? (Y/N)",
64
+ default_answer: "Y")
65
+ platform.rack_connect = CLI.prompt_yn("Rack Connected? (Y/N)",
66
+ default_answer: "N")
67
+ else
68
+ api = {}
69
+ api[:version] = opencloud ? :v2 : :v1
70
+
71
+ proceed = CLI.prompt_yn("Spin up this server? (Y/N)", default_answer: "Y")
72
+ if proceed
73
+ api[:flavor] = default_target[:id]
74
+ else
75
+ puts CLI.build_grid(flavor_list,
76
+ {id: "ID", mem: "RAM (MiB)", hdd: "HDD (GB)" })
77
+ api[:flavor] = CLI.prompt("Flavor ID",
78
+ default_answer: default_target[:id])
79
+ api[:flavor] = api[:flavor].to_i
80
+ end
81
+
82
+ migration_exclusions = determine_exclusions(source_profile[:cpe])
83
+ platform.managed = CLI.prompt_yn("Managed service level? (Y/N)",
84
+ default_answer: "Y")
85
+ platform.rack_connect = CLI.prompt_yn("Rack Connected? (Y/N)",
86
+ default_answer: "N")
87
+
88
+ # Warn against Rack Connect
89
+ if platform.rack_connect
90
+ puts "#{CLI.bold}#{CLI.red}*** Rack Connect servers might not " +
91
+ "provision properly when spun up from the API! Resume " +
92
+ "recommended!#{CLI.reset}"
93
+ sleep 5
94
+ end
95
+
96
+
97
+ # Check to make sure we have a valid flavor ID
98
+ exit 0 if api[:flavor] == 0 or flavor_list[api[:flavor]-1].nil?
99
+
100
+ # Build our API call
101
+ api[:hostname] = CLI.prompt("New Server Name",
102
+ default_answer: source_profile[:hostname])
103
+
104
+ # OpenCloud only supports US migrations presently
105
+ if opts[:function] == :opencloud
106
+ api[:region] = CLI.prompt("Region (dfw, ord)", default_answer: "dfw",
107
+ valid_answers: ["ord", "dfw"])
108
+ else
109
+ api[:region] = :dfw
110
+ end
111
+
112
+ api[:username] = CLI.prompt("RS Cloud Username")
113
+ api[:api_key] = CLI.prompt("RS Cloud API key")
114
+
115
+ # Name cannot have any special characters in it
116
+ api[:hostname].gsub!(/[^A-Za-z0-9.]/, '-')
117
+
118
+ rack_api = Fog::Compute.new(provider: 'rackspace',
119
+ rackspace_username: api[:username],
120
+ rackspace_api_key: api[:api_key],
121
+ rackspace_region: api[:region],
122
+ version: api[:version])
123
+
124
+ # Rescue patch has to be loaded after the connection is created
125
+ require 'cloudflock/patch/fog'
126
+
127
+ # Send API call
128
+ new_server = CLI.spinner("Spinning up new server: #{api[:hostname]}") do
129
+ rack_api.servers::create(name: api[:hostname],
130
+ image_id: platform.image,
131
+ flavor_id: api[:flavor])
132
+
133
+ end
134
+
135
+ # Set the destination host address
136
+ destination_host_def = {}
137
+ CLI.spinner("Obtaining information for new instance") do
138
+ # Obtain the administrative pass for the new host.
139
+ destination_host_def[:password] = new_server.password
140
+ server_id = new_server.id
141
+
142
+ until new_server.state == 'ACTIVE'
143
+ sleep 30
144
+ begin
145
+ new_server.update
146
+ rescue NoMethodError
147
+ new_server.reload
148
+ end
149
+ end
150
+
151
+ if opencloud
152
+ dest_host = new_server.addresses["public"].select do |e|
153
+ e["version"] == 4
154
+ end
155
+ destination_host_def[:host] = dest_host[0]["addr"]
156
+ else
157
+ destination_host_def[:host] = new_server.addresses["public"][0]
158
+ end
159
+ end
160
+
161
+ # If we're working within the Managed service level, ensure that Chef
162
+ # has finished successfully
163
+ if platform.managed
164
+ r = 0
165
+ destination_host_ssh = destination_login(destination_host_def)
166
+
167
+ begin
168
+ message =
169
+ finished = CLI.spinner("Waiting for Chef to finish") do
170
+ # Sleep 180 seconds before trying
171
+ sleep 180
172
+ Migrate.setup_managed(destination_host_ssh)
173
+ end
174
+ unless finished
175
+ panic = "#{CLI.bold}#{CLI.red}*** MGC Cloud Scripts appear to " +
176
+ "have failed to run in a reasonable amount of time." +
177
+ "#{CLI.reset}"
178
+ puts panic
179
+ exit unless CLI.prompt_yn("Continue? (Y/N)", default_answer: "Y")
180
+ end
181
+ destination_host_ssh.logout!
182
+ rescue
183
+ panic = "#{CLI.bold}#{CLI.red}*** Unable to communicate with the " +
184
+ "destination host. Bailing out.#{CLI.reset}"
185
+ puts panic
186
+ raise
187
+ end
188
+ end
189
+
190
+ if opts[:function] == :opencloud
191
+ host = destination_host_def[:host]
192
+ CLI.spinner("Putting #{host} into rescue mode") do
193
+ new_server.rescue
194
+ destination_host_def[:password] = new_server.password
195
+ new_server.update
196
+ end
197
+ else
198
+ pass_prompt = "Please put #{api[:hostname]} into rescue mode and " +
199
+ "give password"
200
+ destination_host_def[:password] = CLI.prompt(pass_prompt)
201
+ end
202
+
203
+ CLI.spinner "Letting rescue mode come up..." do
204
+ until new_server.state == 'RESCUE'
205
+ sleep 30
206
+ begin
207
+ new_server.update
208
+ rescue NoMethodError
209
+ sleep 60
210
+ new_server.reload
211
+ end
212
+ end
213
+ end
214
+
215
+ Thread.new do
216
+ continue = false
217
+ until continue
218
+ r = 0
219
+ message = "Checking for SSH on #{destination_host_def[:host]}"
220
+ ssh_command = "ssh #{SSH::SSH_ARGUMENTS} " +
221
+ "root@#{destination_host_def[:host]}"
222
+ continue = CLI.spinner(message) do
223
+ begin
224
+ sleep 20
225
+ ssh_expect = Expectr.new(ssh_command, flush_buffer: false)
226
+ ssh_expect.expect("password")
227
+ rescue
228
+ retry if (r+=1) < 10
229
+ raise
230
+ end
231
+ end
232
+ end
233
+ end.join
234
+ end
235
+
236
+ destination_host_ssh = destination_login(destination_host_def)
237
+
238
+ unless destination_host_def[:pre_steps] == false
239
+ # Attempt to set up the source host 5 times. If there is a failure,
240
+ # sleep for 60 seconds before retrying.
241
+ r = 0
242
+ begin
243
+ message = "Setting up source host (attempt #{r + 1}/5)"
244
+ pubkey = CLI.spinner(message) do
245
+ begin
246
+ message.gsub!(/\d\/5/, "#{r + 1}/5")
247
+ sleep 60 unless r == 0
248
+ Migrate.setup_source(source_host_ssh, migration_exclusions)
249
+ rescue
250
+ retry if (r += 1) < 5
251
+ raise
252
+ end
253
+ end
254
+ rescue
255
+ panic = "#{CLI.bold}#{CLI.red}*** Unable to communicate with the " +
256
+ "source host. Bailing out.#{CLI.reset}"
257
+ puts panic
258
+ raise
259
+ end
260
+
261
+ # Attempt to set up the destination host 5 times. If there is a
262
+ # failure, sleep for 60 seconds before retrying.
263
+ r = 0
264
+ begin
265
+ message = "Setting up destination host (attempt #{r + 1}/5)"
266
+ CLI.spinner(message) do
267
+ begin
268
+ message.gsub!(/\d\/5/, "#{r + 1}/5")
269
+ sleep 60 unless r == 0
270
+ Migrate.setup_destination(destination_host_ssh, pubkey)
271
+ rescue
272
+ retry if (r += 1) < 5
273
+ raise
274
+ end
275
+ end
276
+ rescue
277
+ panic = "#{CLI.bold}#{CLI.red}*** Unable to communicate with the " +
278
+ "destination host. Bailing out.#{CLI.reset}"
279
+ puts panic
280
+ raise
281
+ end
282
+ end
283
+
284
+ # Determine if Service Net can be used
285
+ begin
286
+ CLI.spinner "Checking for ServiceNet" do
287
+ target_addr = Migrate.check_servicenet(source_host_ssh,
288
+ destination_host_ssh)
289
+ raise if target_addr.nil?
290
+ destination_host_def[:target_addr] = target_addr
291
+ end
292
+ rescue
293
+ destination_host_def[:target_addr] = destination_host_def[:host]
294
+ end
295
+
296
+ # Define no timeout for the migration rsync
297
+ destination_host_def[:timeout] = -1
298
+
299
+ # Kick off the migration proper
300
+ if opts[:verbose]
301
+ Migrate.migrate_server(source_host_ssh, destination_host_def)
302
+ else
303
+ CLI.spinner "Performing migration" do
304
+ Migrate.migrate_server(source_host_ssh, destination_host_def)
305
+ end
306
+ end
307
+
308
+ CLI.spinner "Cleaning up destination host" do
309
+ Migrate.clean_destination(destination_host_ssh, source_profile[:cpe])
310
+ end
311
+
312
+ [destination_host_ssh, source_host_ssh].each do |host|
313
+ host.logout!
314
+ end
315
+
316
+ puts
317
+ puts "#{CLI.bold}#{CLI.blue}*** Migration complete#{CLI.reset}\a"
318
+ end
319
+
320
+ # Internal: Ask whether or not to edit the default exclusion list for a given
321
+ # platform, and facilitate the edit if so.
322
+ #
323
+ # cpe - CPE object for the host in question.
324
+ #
325
+ # Returns a String containing the exclusions.
326
+ # Raises ArgumentError if cpe isn't a CPE object.
327
+ def determine_exclusions(cpe)
328
+ raise ArgumentError unless cpe.kind_of?(CPE)
329
+
330
+ exclusion_string = Migrate.build_default_exclusions(cpe)
331
+ alter = CLI.prompt_yn("Edit default exclusions list? (Y/N)",
332
+ default_answer: "N")
333
+
334
+ if alter
335
+ require 'tempfile'
336
+
337
+ tmp_file = Tempfile.new("exclude")
338
+ tmp_file.write(exclusion_string)
339
+ tmp_file.close
340
+
341
+ # Allow for "other" editors
342
+ if File.exists?("/usr/bin/editor")
343
+ editor = "/usr/bin/editor"
344
+ else
345
+ editor = "vim"
346
+ end
347
+
348
+ system("#{editor} #{tmp_file.path}")
349
+ tmp_file.open
350
+ exclusion_string = tmp_file.read
351
+ tmp_file.close
352
+ tmp_file.unlink
353
+ end
354
+
355
+ exclusion_string
356
+ end
357
+ end