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,60 +1,34 @@
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 'guignol/commands/base'
29
- require 'guignol/instance'
30
- require 'term/ansicolor'
2
+
3
+ Guignol::Shell.class_eval do
4
+ desc 'list [PATTERNS]', 'List the status of all known instances'
5
+ def list(*patterns)
6
+ patterns.push('.*') if patterns.empty?
7
+ Guignol::Commands::List.new(patterns).run
8
+ end
9
+ end
31
10
 
32
11
  module Guignol::Commands
33
12
  class List < Base
34
- def initialize(*argv)
35
- argv = ['.*'] if argv.empty?
36
- super(*argv)
37
- end
13
+ private
38
14
 
39
- def run_on_server(config)
40
- instance = Guignol::Instance.new(config)
41
- puts "%s:\t%s" % [instance.name, colorize(instance.state)]
15
+ def run_on_server(instance, options = {})
16
+ synchronize do
17
+ shell.say instance.name.ljust(@max_width + 1)
18
+ shell.say instance.state, colorize(instance.state)
19
+ end
42
20
  end
43
21
 
44
- def self.short_usage
45
- ["[regexp]", "List known instances (matching the regexp) and their status."]
22
+ def before_run(configs)
23
+ @max_width = configs.keys.map(&:size).max
46
24
  end
47
25
 
48
- private
49
-
50
26
  def colorize(state)
51
27
  case state
52
- when 'running' then Term::ANSIColor.green(state)
53
- when 'stopped' then Term::ANSIColor.yellow(state)
54
- when 'nonexistent' then Term::ANSIColor.red(state)
55
- else state
28
+ when 'running' then :green
29
+ when /starting|stopping/ then :yellow
30
+ when 'nonexistent' then :red
56
31
  end
57
32
  end
58
33
  end
59
34
  end
60
-
@@ -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 Start < Base
33
- def run_on_server(config)
34
- Guignol::Instance.new(config).start
5
+ Guignol::Shell.class_eval do
6
+ desc 'start PATTERNS', 'Start all instances matching PATTERNS, attach their volumes, and setup DNS records'
7
+ def start(*patterns)
8
+ if patterns.empty?
9
+ raise Thor::Error.new('You must specify at least one PATTERN.')
35
10
  end
11
+ Guignol::Commands::Start.new(patterns).run
12
+ end
13
+ end
36
14
 
37
- def self.short_usage
38
- ["<regexps>", "Start instances (unless they're running) and setup DNS"]
15
+
16
+ module Guignol::Commands
17
+ class Start < Base
18
+ def run_on_server(instance, options = {})
19
+ instance.start
39
20
  end
40
21
  end
41
22
  end
@@ -1,47 +1,28 @@
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 Stop < 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 stop servers #{names}"
5
+ Guignol::Shell.class_eval do
6
+ desc 'stop PATTERNS', 'Stop all instances matching PATTERNS, and remove DNS records'
7
+ def stop(*patterns)
8
+ if patterns.empty?
9
+ raise Thor::Error.new('You must specify at least one PATTERN.')
37
10
  end
11
+ Guignol::Commands::Stop.new(patterns).run
12
+ end
13
+ end
38
14
 
39
- def run_on_server(config)
40
- Guignol::Instance.new(config).stop
15
+
16
+ module Guignol::Commands
17
+ class Stop < Base
18
+ def before_run(configs)
19
+ return true if configs.empty?
20
+ names = configs.keys.join(", ")
21
+ shell.yes? "Are you sure you want to stop servers #{names}? [y/N]", :cyan
41
22
  end
42
23
 
43
- def self.short_usage
44
- ["<regexps>", "Stop instances (if they're running) and remove DNS"]
24
+ def run_on_server(instance, options = {})
25
+ instance.stop
45
26
  end
46
27
  end
47
28
  end
@@ -1,41 +1,28 @@
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
3
  require 'uuidtools'
30
4
 
31
- module Guignol::Commands
32
- class UUID
33
- def run
34
- puts UUIDTools::UUID.random_create.to_s.upcase
5
+ Guignol::Shell.class_eval do
6
+ desc 'uuid [COUNT]', 'Print random UUIDs'
7
+ method_option :count,
8
+ :aliases => %w(-c),
9
+ :type => :numeric, :default => 1,
10
+ :desc => 'Number of UUIDs to print'
11
+ def uuid
12
+ unless options[:count].kind_of?(Fixnum) && options[:count] > 0
13
+ raise Thor::Error.new('Count should be a positive integer')
35
14
  end
15
+ Guignol::Commands::UUID.new.run(options[:count])
16
+ end
17
+ end
36
18
 
37
- def self.short_usage
38
- ["", "Return a brand new UUID"]
19
+
20
+ module Guignol::Commands
21
+ class UUID
22
+ def run(count = 1)
23
+ count.times do
24
+ puts UUIDTools::UUID.random_create.to_s.upcase
25
+ end
39
26
  end
40
27
  end
41
28
  end
@@ -0,0 +1,43 @@
1
+
2
+ require 'active_support'
3
+ require 'active_support/core_ext/enumerable'
4
+ require 'guignol'
5
+
6
+ module Guignol::Configuration
7
+
8
+ def configuration
9
+ @configuration ||= load_config_file
10
+ end
11
+
12
+ private
13
+
14
+ def config_file_path
15
+ @config_file_path ||= [
16
+ Pathname.new(ENV['GUIGNOL_YML'] || '/var/nonexistent'),
17
+ Pathname.new('guignol.yml'),
18
+ Pathname.new('config/guignol.yml'),
19
+ Pathname.new(ENV['HOME']).join('.guignol.yml')
20
+ ].find(&:exist?)
21
+ end
22
+
23
+ # Load the config hash for the file, converting old (v0.2.0) Yaml config files.
24
+ def load_config_file
25
+ return {} if config_file_path.nil?
26
+ data = YAML.load(config_file_path.read)
27
+ return data unless data.kind_of?(Array)
28
+
29
+ # Convert the toplevel array to a hash. Same for arrays of volumes.
30
+ Guignol.logger.warn "Configuration file '#{config_file_path}' uses the old array format. Trying to load it."
31
+ raise "Instance config lacks :name" unless data.collect_key(:name).all?
32
+ result = data.index_by { |item| item.delete(:name) }
33
+ result.each_pair do |name, config|
34
+ next unless config[:volumes]
35
+ raise "Volume config lacks :name" unless config[:volumes].collect_key(:name).all?
36
+ config[:volumes] = config[:volumes].index_by { |item| item.delete(:name) }
37
+ end
38
+
39
+ return result
40
+ end
41
+
42
+ Guignol.extend(self)
43
+ end
@@ -0,0 +1,33 @@
1
+
2
+ require 'fog'
3
+ require 'active_support/core_ext/hash/slice'
4
+ require 'active_support/core_ext/hash/reverse_merge'
5
+ require 'guignol/configuration'
6
+
7
+ module Guignol
8
+ # Pool Fog connections to minimize latency
9
+ module Connection
10
+ def self.get(options)
11
+ @connections ||= {}
12
+ @connections[options] ||= Fog::Compute.new(options)
13
+ end
14
+
15
+
16
+ private
17
+
18
+
19
+ # Find and return credentials
20
+ def credentials
21
+ if ENV['AWS_SECRET_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY']
22
+ {
23
+ :aws_access_key_id => ENV['AWS_SECRET_KEY_ID'],
24
+ :aws_secret_access_key => ENV['AWS_SECRET_ACCESS_KEY']
25
+ }
26
+ else
27
+ Guignol.configuration.slice(:aws_access_key_id, :aws_secret_access_key)
28
+ end
29
+ end
30
+
31
+
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ module Guignol
2
+ def env
3
+ @env ||= Env.new
4
+ end
5
+
6
+ private
7
+
8
+ class Env
9
+ def initialize
10
+ @env = ENV['GUIGNOL_ENV'] || 'development'
11
+ end
12
+
13
+ def test?
14
+ @env == 'test'
15
+ end
16
+ end
17
+
18
+ Guignol.extend(self)
19
+ end
@@ -0,0 +1,29 @@
1
+ require 'logger'
2
+
3
+ module Guignol::Logger
4
+ def logger
5
+ @logger ||= ::Logger.new(logger_file).tap do |logger|
6
+ logger.progname = 'guignol'
7
+ logger.formatter = Formatter.new
8
+ end
9
+ end
10
+
11
+
12
+ private
13
+
14
+ class Formatter < ::Logger::Formatter
15
+ Format = "[%s] %s: %s\n"
16
+
17
+ def call(severity, time, progname, msg)
18
+ Format % [time.strftime('%F %T'), severity, msg2str(msg)]
19
+ end
20
+ end
21
+
22
+
23
+ def logger_file
24
+ return File.open(ENV['GUIGNOL_LOG'] ,'a') if ENV['GUIGNOL_LOG']
25
+ $stdout.tty? ? $stdout : File.open('/dev/null','w')
26
+ end
27
+
28
+ Guignol.extend(self)
29
+ end
@@ -0,0 +1,125 @@
1
+
2
+ require 'guignol/connection'
3
+ require 'guignol/tty_spinner'
4
+
5
+ module Guignol::Models
6
+ class Base
7
+ # The wrapped instance, volume, etc we're manipulating
8
+ attr :subject
9
+ attr :options
10
+ attr :connection
11
+ attr :name
12
+
13
+ def initialize(name, options)
14
+ @name = name
15
+ @options = default_options.merge(options)
16
+ require_name!
17
+ require_options! :uuid
18
+ connection_options = Guignol::DefaultConnectionOptions.merge @options.slice(:region)
19
+
20
+ @connection = Guignol::Connection.get(connection_options)
21
+ @subject = find_subject
22
+ end
23
+
24
+
25
+ def exist?
26
+ !!@subject
27
+ end
28
+ alias_method :exists?, :exist?
29
+
30
+
31
+ def uuid
32
+ @options[:uuid]
33
+ end
34
+
35
+
36
+ def state
37
+ @subject and @subject.state or 'nonexistent'
38
+ end
39
+
40
+
41
+ protected
42
+
43
+
44
+ def set_subject(subject)
45
+ @subject = subject
46
+ end
47
+
48
+
49
+ def reload
50
+ @subject = find_subject
51
+ end
52
+
53
+
54
+ def find_subject
55
+ raise 'Define me in a subclass'
56
+ end
57
+
58
+
59
+ Interval = 200e-3
60
+ Timeout = 300
61
+
62
+
63
+ def default_options
64
+ {}
65
+ end
66
+
67
+ def log(message, options={})
68
+ Guignol.logger.info("#{name}: #{message}")
69
+ if e = options[:error]
70
+ Guignol.logger.info e.class.name
71
+ Guignol.logger.info e.message
72
+ e.backtrace.each { |line| Guignol.logger.debug line }
73
+ end
74
+ Thread.pass
75
+ true
76
+ end
77
+
78
+
79
+ def subject_name
80
+ self.class.name.gsub(/.*:/,'').downcase
81
+ end
82
+
83
+
84
+ # wait until the subject is in one of +states+
85
+ def wait_for_state(*states)
86
+ exist? or raise "#{subject_name} doesn't exist"
87
+ original_state = state
88
+ return if states.include?(original_state)
89
+ log "waiting for #{subject_name} to become #{states.join(' or ')}..."
90
+ Fog.wait_for(Timeout,Interval) do
91
+ Guignol::TtySpinner.spin!
92
+ reload
93
+ if state != original_state
94
+ log "#{subject_name} now #{state}"
95
+ original_state = state
96
+ end
97
+ states.include?(state)
98
+ end
99
+ end
100
+
101
+
102
+ def wait_for(&block)
103
+ return unless @subject
104
+ @subject.wait_for(Timeout,Interval) { Guignol::TtySpinner.spin! ; block.call }
105
+ end
106
+
107
+
108
+ def confirm(message)
109
+ puts "#{message} [y/n]"
110
+ $stdin.gets =~ /^y$/i
111
+ end
112
+
113
+ def require_name!
114
+ return unless @name.nil? || @name =~ /^\s*$/
115
+ raise "Name cannot be empty or blank"
116
+ end
117
+
118
+ def require_options!(*required_options)
119
+ required_options.each do |required_option|
120
+ next if @options.include?(required_option)
121
+ raise "option '#{required_option}' is mandatory for each #{subject_name}"
122
+ end
123
+ end
124
+ end
125
+ end