cloudflock 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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