guignol 0.1.2.1 → 0.3.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 (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