guignol 0.1.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +2 -0
  2. data/.rspec +2 -1
  3. data/Gemfile.lock +29 -18
  4. data/LICENCE +26 -0
  5. data/README.md +67 -38
  6. data/Rakefile +0 -26
  7. data/bin/guignol +3 -33
  8. data/guignol.gemspec +4 -30
  9. data/lib/core_ext/array/collect_key.rb +6 -0
  10. data/lib/core_ext/hash/map_to_hash.rb +31 -0
  11. data/lib/guignol.rb +15 -35
  12. data/lib/guignol/commands/base.rb +53 -66
  13. data/lib/guignol/commands/clone.rb +49 -0
  14. data/lib/guignol/commands/create.rb +14 -33
  15. data/lib/guignol/commands/execute.rb +69 -0
  16. data/lib/guignol/commands/fix_dns.rb +12 -33
  17. data/lib/guignol/commands/kill.rb +18 -36
  18. data/lib/guignol/commands/list.rb +19 -45
  19. data/lib/guignol/commands/start.rb +14 -33
  20. data/lib/guignol/commands/stop.rb +18 -37
  21. data/lib/guignol/commands/uuid.rb +19 -32
  22. data/lib/guignol/configuration.rb +43 -0
  23. data/lib/guignol/connection.rb +33 -0
  24. data/lib/guignol/env.rb +19 -0
  25. data/lib/guignol/logger.rb +29 -0
  26. data/lib/guignol/models/base.rb +125 -0
  27. data/lib/guignol/models/instance.rb +244 -0
  28. data/lib/guignol/models/volume.rb +91 -0
  29. data/lib/guignol/shell.rb +27 -42
  30. data/lib/guignol/tty_spinner.rb +6 -29
  31. data/lib/guignol/version.rb +1 -27
  32. data/spec/guignol/configuration_spec.rb +72 -0
  33. data/spec/guignol/instance_spec.rb +48 -8
  34. data/spec/guignol/volume_spec.rb +17 -0
  35. data/spec/spec_helper.rb +12 -0
  36. data/tmp/.keepme +0 -0
  37. metadata +79 -52
  38. data/lib/guignol/array/collect_key.rb +0 -32
  39. data/lib/guignol/commands.rb +0 -48
  40. data/lib/guignol/commands/help.rb +0 -77
  41. data/lib/guignol/instance.rb +0 -270
  42. data/lib/guignol/shared.rb +0 -80
  43. data/lib/guignol/volume.rb +0 -124
@@ -1,38 +1,18 @@
1
- # Copyright (c) 2012, HouseTrip SA.
2
- # All rights reserved.
3
- #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are met:
6
- #
7
- # 1. Redistributions of source code must retain the above copyright notice, this
8
- # list of conditions and the following disclaimer.
9
- # 2. Redistributions in binary form must reproduce the above copyright notice,
10
- # this list of conditions and the following disclaimer in the documentation
11
- # and/or other materials provided with the distribution.
12
- #
13
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
- #
24
- # The views and conclusions contained in the software and documentation are those
25
- # of the authors and should not be interpreted as representing official policies,
26
- # either expressed or implied, of the authors.
1
+
2
+ require 'core_ext/array/collect_key'
3
+ require 'core_ext/hash/map_to_hash'
4
+ require 'guignol/logger'
5
+ require 'guignol/configuration'
6
+ require 'guignol/env'
27
7
 
28
8
  module Guignol
29
- DefaultConnectionOptions = {
30
- :provider => :aws,
31
- :region => 'eu-west-1'
32
- }
33
- DefaultServerOptions = {
34
- :flavor_id => 't1.micro',
35
- :volumes => []
36
- }
37
- DefaultVolumeOptions = {}
9
+ DefaultConnectionOptions = {
10
+ :provider => :aws,
11
+ :region => 'eu-west-1'
12
+ }
13
+ DefaultServerOptions = {
14
+ :flavor_id => 't1.micro',
15
+ :volumes => []
16
+ }
17
+ DefaultVolumeOptions = {}
38
18
  end
@@ -1,88 +1,75 @@
1
- # Copyright (c) 2012, HouseTrip SA.
2
- # All rights reserved.
3
- #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are met:
6
- #
7
- # 1. Redistributions of source code must retain the above copyright notice, this
8
- # list of conditions and the following disclaimer.
9
- # 2. Redistributions in binary form must reproduce the above copyright notice,
10
- # this list of conditions and the following disclaimer in the documentation
11
- # and/or other materials provided with the distribution.
12
- #
13
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
- #
24
- # The views and conclusions contained in the software and documentation are those
25
- # of the authors and should not be interpreted as representing official policies,
26
- # either expressed or implied, of the authors.
27
-
28
-
1
+ require 'thor'
29
2
  require 'pathname'
30
3
  require 'parallel'
31
- require 'guignol/instance'
32
- require 'guignol/array/collect_key'
4
+ require 'guignol'
5
+ require 'guignol/configuration'
6
+ require 'guignol/models/instance'
7
+ require 'core_ext/array/collect_key'
33
8
 
34
9
  module Guignol::Commands
35
10
  class Base
36
11
 
37
- def initialize(*argv)
38
- @all_configs = load_config_files
39
- check_config_consistency
40
- @configs = argv.map { |pattern|
41
- @all_configs.select { |config|
42
- config[:name] =~ /#{pattern}/
43
- }
44
- }.flatten.uniq
12
+ def initialize(patterns, options = {})
13
+ @configs = select_configs(patterns)
14
+ @options = options
45
15
  end
46
16
 
17
+ # Run the block for each server in +configs+ (in parallel).
47
18
  def run
48
- before_run or return if respond_to?(:before_run)
19
+ before_run(@configs) or return
20
+ results = {}
49
21
 
50
- Parallel.each(@configs) do |config|
51
- run_on_server(config)
22
+ Parallel.each(@configs, parallel_options) do |name,config|
23
+ instance = Guignol::Models::Instance.new(name, config)
24
+ results[name] = run_on_server(instance, @options)
52
25
  end
26
+
27
+ after_run(results)
53
28
  end
54
29
 
55
- protected
56
30
 
57
- def confirm(message)
58
- $stdout.print "#{message}? [y/N] "
59
- $stdout.flush
60
- answer = $stdin.gets
61
- return answer.strip =~ /y/i
31
+ protected
32
+
33
+ # Override in subclasses
34
+ def before_run(configs) ; true ; end
35
+
36
+ # Override in subclasses
37
+ def after_run(data) ; true ; end
38
+
39
+
40
+ def shell
41
+ Guignol::Shell.shared_shell
62
42
  end
63
43
 
64
- private
65
-
66
- # Read & return the first available config.
67
- def load_config_files
68
- [
69
- Pathname.new(ENV['GUIGNOL_YML'] || '/var/nonexistent'),
70
- Pathname.new('guignol.yml'),
71
- Pathname.new('config/guignol.yml'),
72
- Pathname.new(ENV['HOME']).join('.guignol.yml')
73
- ].each do |pathname|
74
- next unless pathname.exist?
75
- return YAML.load(pathname.read)
44
+
45
+ def synchronize
46
+ (@mutex ||= Mutex.new).synchronize do
47
+ yield
76
48
  end
77
- return {}
78
49
  end
79
50
 
80
- def check_config_consistency
81
- errors = []
82
- errors << "Instance config lacks :name" unless @all_configs.collect_key(:name).all?
83
- errors << "Instance config lacks :uuid" unless @all_configs.collect_key(:uuid).all?
84
- errors << "Volume config lacks :uuid" unless @all_configs.collect_key(:volumes).collect_key(:uuid)
85
- raise errors.join(', ') if errors.any?
51
+
52
+ private
53
+
54
+
55
+ def parallel_options
56
+ if RUBY_VERSION >= '1.9.3'
57
+ # 1.9.3 has bugs with Excon / SSL connections
58
+ { :in_threads => 0 }
59
+ else
60
+ { :in_threads => @configs.size }
61
+ end
86
62
  end
63
+
64
+ # Put all the servers matching one of the +names+ in +configs+.
65
+ def select_configs(patterns)
66
+ patterns = patterns.map { |pattern|
67
+ pattern.kind_of?(String) ? Regexp.new(pattern) : pattern
68
+ }
69
+ Guignol.configuration.delete_if { |name,config|
70
+ patterns.none? { |pattern| name.to_s =~ pattern }
71
+ }
72
+ end
73
+
87
74
  end
88
75
  end
@@ -0,0 +1,49 @@
1
+
2
+ require 'guignol/configuration'
3
+ require 'guignol/commands/base'
4
+ require 'uuidtools'
5
+ require 'yaml'
6
+
7
+
8
+ Guignol::Shell.class_eval do
9
+ desc 'clone SOURCE', 'Print a new config similar to the server named SOURCE'
10
+ method_option :name,
11
+ :aliases => %w(-n),
12
+ :type => :string, :default => 'new-server',
13
+ :desc => 'Name to use for the new server'
14
+ def clone(source)
15
+ Guignol::Commands::Clone.new(source, options[:name]).run
16
+ end
17
+ end
18
+
19
+
20
+ module Guignol::Commands
21
+ class Clone
22
+ def initialize(source_name, target_name)
23
+ @source_name = source_name
24
+ @target_name = target_name
25
+
26
+ @source_config = Guignol.configuration[source_name]
27
+ unless @source_config
28
+ raise Thor::Error.new "machine '#{source_name}' is unknown"
29
+ end
30
+ end
31
+
32
+
33
+ def run
34
+ new_config = @source_config.map_to_hash(:deep => true) do |key,value|
35
+ value = value.gsub(/#{@source_name}/, @target_name) if value.kind_of?(String)
36
+ key = key.gsub( /#{@source_name}/, @target_name) if key.kind_of?(String)
37
+
38
+ case key
39
+ when :uuid
40
+ [key, UUIDTools::UUID.random_create.to_s.upcase]
41
+ else
42
+ [key, value]
43
+ end
44
+ end
45
+
46
+ $stdout.puts({@target_name => new_config}.to_yaml)
47
+ end
48
+ end
49
+ end
@@ -1,41 +1,22 @@
1
- # Copyright (c) 2012, HouseTrip SA.
2
- # All rights reserved.
3
- #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are met:
6
- #
7
- # 1. Redistributions of source code must retain the above copyright notice, this
8
- # list of conditions and the following disclaimer.
9
- # 2. Redistributions in binary form must reproduce the above copyright notice,
10
- # this list of conditions and the following disclaimer in the documentation
11
- # and/or other materials provided with the distribution.
12
- #
13
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
- #
24
- # The views and conclusions contained in the software and documentation are those
25
- # of the authors and should not be interpreted as representing official policies,
26
- # either expressed or implied, of the authors.
27
1
 
28
2
  require 'guignol/commands/base'
29
- require 'guignol/instance'
3
+ require 'guignol/models/instance'
30
4
 
31
- module Guignol::Commands
32
- class Create < Base
33
- def run_on_server(config)
34
- Guignol::Instance.new(config).create
5
+ Guignol::Shell.class_eval do
6
+ desc 'create PATTERNS', 'Create and start all instances matching PATTERNS and their volumes'
7
+ def create(*patterns)
8
+ if patterns.empty?
9
+ raise Thor::Error.new('You must specify at least one PATTERN.')
35
10
  end
11
+ Guignol::Commands::Create.new(patterns).run
12
+ end
13
+ end
36
14
 
37
- def self.short_usage
38
- ["<regexps>", "Create instances and volumes (unless they exist) then run start"]
15
+
16
+ module Guignol::Commands
17
+ class Create < Base
18
+ def run_on_server(instance, options = {})
19
+ instance.create
39
20
  end
40
21
  end
41
22
  end
@@ -0,0 +1,69 @@
1
+ require 'guignol/commands/base'
2
+
3
+ Guignol::Shell.class_eval do
4
+ desc 'execute COMMAND', 'Execute a command over SSH on instances'
5
+ method_option :on,
6
+ :banner => 'PATTERNS',
7
+ :type => :array, :default => ['.*'],
8
+ :desc => 'A list of regexps matching servers on which to run'
9
+ method_option :verbose,
10
+ :aliases => %w(-v), :type => :boolean, :default => false,
11
+ :desc => 'Output the command output to the local terminal'
12
+ method_option :aws_key,
13
+ :aliases => %w(-k), :type => :string,
14
+ :desc => 'Path the the SSH key file to use to connect'
15
+ method_option :user,
16
+ :aliases => %w(-u), :type => :string,
17
+ :desc => 'Username used to connect (takes precedence over the name defined in the config file)'
18
+ long_desc %Q{
19
+ Connect to each server matching one of the PATTERNS (defaults to all
20
+ known servers) over SSH, and run the COMMAND on them, in parallel.
21
+ }
22
+ def execute(*words)
23
+ words.shift if words.first =~ /^-+$/
24
+ command = words.join(' ')
25
+ Guignol::Commands::Execute.
26
+ new(options[:on], options.merge(:command => command)).
27
+ run
28
+ end
29
+ end
30
+
31
+ module Guignol::Commands
32
+ class Execute < Base
33
+ private
34
+ def run_on_server(instance, options = {})
35
+ if instance.state != 'running'
36
+ print_output(instance.name, "Instance not running", :red)
37
+ return
38
+ end
39
+
40
+ instance.subject.username = options[:user] if options[:user]
41
+ results = instance.subject.ssh(options[:command], :keys => options[:aws_key])
42
+
43
+ return unless options[:verbose]
44
+ results.each do |result|
45
+ print_output(instance.name, result.stderr, :yellow)
46
+ print_output(instance.name, result.stdout, :green)
47
+ end
48
+
49
+ rescue Net::SSH::AuthenticationFailed => e
50
+ print_output(instance.name, "#{e.message} (#{e.class.name})", :red)
51
+ end
52
+
53
+ def before_run(configs)
54
+ @max_width = configs.keys.map(&:size).max
55
+ end
56
+
57
+
58
+ def print_output(name, data, color)
59
+ synchronize do
60
+ name_string = name.ljust(@max_width+1)
61
+ data.split(/\r?\n/).each do |line|
62
+ shell.say name_string, color
63
+ shell.say line, :clear, true
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
@@ -1,41 +1,20 @@
1
- # Copyright (c) 2012, HouseTrip SA.
2
- # All rights reserved.
3
- #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are met:
6
- #
7
- # 1. Redistributions of source code must retain the above copyright notice, this
8
- # list of conditions and the following disclaimer.
9
- # 2. Redistributions in binary form must reproduce the above copyright notice,
10
- # this list of conditions and the following disclaimer in the documentation
11
- # and/or other materials provided with the distribution.
12
- #
13
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
- #
24
- # The views and conclusions contained in the software and documentation are those
25
- # of the authors and should not be interpreted as representing official policies,
26
- # either expressed or implied, of the authors.
27
1
 
28
2
  require 'guignol/commands/base'
29
- require 'guignol/instance'
3
+ require 'guignol/models/instance'
4
+
5
+ Guignol::Shell.class_eval do
6
+ desc 'fixdns [PATTERNS]', 'Make sure the DNS mappings are correct for servers matching PATTERNS'
7
+ def fixdns(*patterns)
8
+ patterns.push('.*') if patterns.empty?
9
+ Guignol::Commands::FixDNS.new(patterns).run
10
+ end
11
+ end
12
+
30
13
 
31
14
  module Guignol::Commands
32
15
  class FixDNS < Base
33
- def run_on_server(config)
34
- Guignol::Instance.new(config).update_dns
35
- end
36
-
37
- def self.short_usage
38
- ["<regexps>", "Make sure the DNS mappings are correct."]
16
+ def run_on_server(instance, options = {})
17
+ instance.update_dns
39
18
  end
40
19
  end
41
20
  end
@@ -1,47 +1,29 @@
1
- # Copyright (c) 2012, HouseTrip SA.
2
- # All rights reserved.
3
- #
4
- # Redistribution and use in source and binary forms, with or without
5
- # modification, are permitted provided that the following conditions are met:
6
- #
7
- # 1. Redistributions of source code must retain the above copyright notice, this
8
- # list of conditions and the following disclaimer.
9
- # 2. Redistributions in binary form must reproduce the above copyright notice,
10
- # this list of conditions and the following disclaimer in the documentation
11
- # and/or other materials provided with the distribution.
12
- #
13
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
- # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
- # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
- # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
- # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
- # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
- # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
- # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
- # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
- # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23
- #
24
- # The views and conclusions contained in the software and documentation are those
25
- # of the authors and should not be interpreted as representing official policies,
26
- # either expressed or implied, of the authors.
27
1
 
28
2
  require 'guignol/commands/base'
29
- require 'guignol/instance'
3
+ require 'guignol/models/instance'
4
+
5
+ Guignol::Shell.class_eval do
6
+ desc 'kill PATTERNS', 'Terminate all instances matching PATTERNS'
7
+ def kill(*patterns)
8
+ if patterns.empty?
9
+ raise Thor::Error.new('You must specify at least one PATTERN.')
10
+ end
11
+ Guignol::Commands::Kill.new(patterns).run
12
+ end
13
+ end
14
+
30
15
 
31
16
  module Guignol::Commands
32
17
  class Kill < Base
33
- def before_run
34
- return true if @configs.empty?
35
- names = @configs.map { |config| config[:name] }.join(", ")
36
- confirm "Are you sure you want to destroy servers #{names}"
37
- end
38
18
 
39
- def run_on_server(config)
40
- Guignol::Instance.new(config).destroy
19
+ def before_run(configs)
20
+ return true if configs.empty?
21
+ names = configs.keys.join(", ")
22
+ shell.yes? "Are you sure you want to destroy servers #{names}? [y/N]", :cyan
41
23
  end
42
24
 
43
- def self.short_usage
44
- ["<regexps>", "Destroy instances (if they exist)"]
25
+ def run_on_server(instance, options = {})
26
+ instance.destroy
45
27
  end
46
28
  end
47
29
  end