caterer 0.0.1 → 0.1.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 (59) hide show
  1. data/.gitignore +1 -0
  2. data/Berksfile +3 -0
  3. data/Berksfile.lock +0 -0
  4. data/README.md +138 -2
  5. data/Vagrantfile +22 -0
  6. data/bin/cater +9 -0
  7. data/caterer.gemspec +5 -0
  8. data/cookbooks/users/recipes/default.rb +18 -0
  9. data/example/Caterfile +28 -10
  10. data/example/bootstrap.sh +3 -0
  11. data/knife.rb +1 -0
  12. data/lib/caterer/action/base.rb +10 -0
  13. data/lib/caterer/action/config/validate/image.rb +23 -0
  14. data/lib/caterer/action/config/validate/provisioner.rb +29 -0
  15. data/lib/caterer/action/config/validate.rb +10 -0
  16. data/lib/caterer/action/config.rb +7 -0
  17. data/lib/caterer/action/provisioner/base.rb +16 -0
  18. data/lib/caterer/action/provisioner/bootstrap.rb +14 -0
  19. data/lib/caterer/action/provisioner/cleanup.rb +14 -0
  20. data/lib/caterer/action/provisioner/prepare.rb +14 -0
  21. data/lib/caterer/action/provisioner/provision.rb +14 -0
  22. data/lib/caterer/action/provisioner.rb +11 -0
  23. data/lib/caterer/action/server/validate/ssh.rb +22 -0
  24. data/lib/caterer/action/server/validate.rb +9 -0
  25. data/lib/caterer/action/server.rb +7 -0
  26. data/lib/caterer/action.rb +9 -0
  27. data/lib/caterer/actions.rb +48 -0
  28. data/lib/caterer/command/base.rb +100 -0
  29. data/lib/caterer/command/bootstrap.rb +28 -0
  30. data/lib/caterer/command/provision.rb +24 -0
  31. data/lib/caterer/command/reboot.rb +22 -0
  32. data/lib/caterer/command/test.rb +9 -2
  33. data/lib/caterer/command/up.rb +28 -0
  34. data/lib/caterer/command.rb +6 -1
  35. data/lib/caterer/commands.rb +6 -1
  36. data/lib/caterer/communication/rsync.rb +14 -0
  37. data/lib/caterer/communication/ssh.rb +185 -0
  38. data/lib/caterer/communication.rb +6 -0
  39. data/lib/caterer/config/base.rb +24 -11
  40. data/lib/caterer/config/group.rb +24 -0
  41. data/lib/caterer/config/image.rb +20 -0
  42. data/lib/caterer/config/member.rb +14 -0
  43. data/lib/caterer/config/provision/chef_solo.rb +36 -12
  44. data/lib/caterer/config/provision.rb +5 -3
  45. data/lib/caterer/config.rb +5 -1
  46. data/lib/caterer/environment.rb +38 -12
  47. data/lib/caterer/provisioner/base.rb +19 -0
  48. data/lib/caterer/provisioner/chef_solo.rb +150 -0
  49. data/lib/caterer/provisioner.rb +6 -0
  50. data/lib/caterer/server.rb +102 -0
  51. data/lib/caterer/util/ansi_escape_code_remover.rb +34 -0
  52. data/lib/caterer/util/retryable.rb +25 -0
  53. data/lib/caterer/util.rb +6 -0
  54. data/lib/caterer/version.rb +1 -1
  55. data/lib/caterer.rb +15 -5
  56. data/lib/templates/provisioner/chef_solo/bootstrap.sh +87 -0
  57. data/lib/templates/provisioner/chef_solo/solo.erb +3 -0
  58. metadata +124 -3
  59. data/lib/caterer/config/role.rb +0 -21
@@ -0,0 +1,28 @@
1
+ module Caterer
2
+ module Command
3
+ class Bootstrap < Base
4
+
5
+ def execute
6
+ options = {}
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Usage: cater bootstrap HOST [options]"
9
+ opts.separator ""
10
+ opts.on("-s SCRIPT", "--script SCRIPT", 'optional bootstrap script') do |s|
11
+ options[:script] = s
12
+ end
13
+ end
14
+
15
+ # Parse the options
16
+ argv = parse_options(opts, options, true)
17
+ return if not argv
18
+
19
+ with_target_servers(argv, options) do |server|
20
+ server.bootstrap({:script => options[:script]})
21
+ end
22
+
23
+ 0
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module Caterer
2
+ module Command
3
+ class Provision < Base
4
+
5
+ def execute
6
+ options = {}
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Usage: cater provision HOST [options]"
9
+ end
10
+
11
+ # Parse the options
12
+ argv = parse_options(opts, options, true)
13
+ return if not argv
14
+
15
+ with_target_servers(argv, options) do |server|
16
+ server.provision
17
+ end
18
+
19
+ 0
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Caterer
2
+ module Command
3
+ class Reboot < Base
4
+
5
+ def execute
6
+ options = {}
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Usage: cater provision HOST [options]"
9
+ end
10
+
11
+ # Parse the options
12
+ argv = parse_options(opts, options, true)
13
+ return if not argv
14
+
15
+ @env.ui.info options
16
+ @env.ui.info argv
17
+ 0
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -1,9 +1,16 @@
1
1
  module Caterer
2
2
  module Command
3
- class Test < Vli::Command::Base
3
+ class Test < Base
4
4
 
5
5
  def execute
6
- puts "testy testy!"
6
+ options = {}
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Usage: cater test"
9
+ end
10
+
11
+ # Parse the options
12
+ argv = parse_options(opts, options, false)
13
+
7
14
  0
8
15
  end
9
16
 
@@ -0,0 +1,28 @@
1
+ module Caterer
2
+ module Command
3
+ class Up < Base
4
+
5
+ def execute
6
+ options = {}
7
+ opts = OptionParser.new do |opts|
8
+ opts.banner = "Usage: cater provision HOST [options]"
9
+ opts.separator ""
10
+ opts.on("-s SCRIPT", "--script SCRIPT", 'optional bootstrap script') do |s|
11
+ options[:script] = s
12
+ end
13
+ end
14
+
15
+ # Parse the options
16
+ argv = parse_options(opts, options, true)
17
+ return if not argv
18
+
19
+ with_target_servers(argv, options) do |server|
20
+ server.up({:script => options[:script]})
21
+ end
22
+
23
+ 0
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,10 @@
1
1
  module Caterer
2
2
  module Command
3
- autoload :Test, 'caterer/command/test'
3
+ autoload :Base, 'caterer/command/base'
4
+ autoload :Bootstrap, 'caterer/command/bootstrap'
5
+ autoload :Provision, 'caterer/command/provision'
6
+ autoload :Reboot, 'caterer/command/reboot'
7
+ autoload :Test, 'caterer/command/test'
8
+ autoload :Up, 'caterer/command/up'
4
9
  end
5
10
  end
@@ -1 +1,6 @@
1
- Caterer.commands.register(:test) { Caterer::Command::Test }
1
+ # commands
2
+ Caterer.commands.register(:test) { Caterer::Command::Test }
3
+ Caterer.commands.register(:bootstrap) { Caterer::Command::Bootstrap }
4
+ Caterer.commands.register(:provision) { Caterer::Command::Provision }
5
+ Caterer.commands.register(:up) { Caterer::Command::Up }
6
+ Caterer.commands.register(:reboot) { Caterer::Command::Reboot }
@@ -0,0 +1,14 @@
1
+ module Caterer
2
+ module Communication
3
+ class Rsync
4
+
5
+ def initialize(server)
6
+ @server = server
7
+ @logger = Log4r::Logger.new("caterer::communication::ssh")
8
+ end
9
+
10
+
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,185 @@
1
+ require 'timeout'
2
+ require 'net/ssh'
3
+ require 'net/scp'
4
+ require 'log4r'
5
+
6
+ module Caterer
7
+ module Communication
8
+ class SSH
9
+ include Util::ANSIEscapeCodeRemover
10
+ include Util::Retryable
11
+
12
+ def initialize(server)
13
+ @server = server
14
+ @connection = nil
15
+ @logger = Log4r::Logger.new("caterer::communication::ssh")
16
+ end
17
+
18
+ def ready?
19
+ @logger.debug("Checking whether SSH is ready...")
20
+
21
+ Timeout.timeout(30) do
22
+ connect
23
+ end
24
+
25
+ # If we reached this point then we successfully connected
26
+ @logger.info("SSH is ready!")
27
+ true
28
+ rescue => e
29
+ # The above errors represent various reasons that SSH may not be
30
+ # ready yet. Return false.
31
+ @logger.info("SSH not up: #{e.inspect}")
32
+ return false
33
+ end
34
+
35
+ def execute(command, opts={}, &block)
36
+ connect do |connection|
37
+ shell_execute(connection, command, opts, &block)
38
+ end
39
+ end
40
+
41
+ def sudo(command, opts={}, &block)
42
+ sudo = (@server.username == 'root') ? false : true
43
+ execute(command, opts.merge({:sudo => sudo}), &block)
44
+ end
45
+
46
+ def upload(from, to)
47
+ @logger.debug("Uploading: #{from} to #{to}")
48
+
49
+ # Do an SCP-based upload...
50
+ connect do |connection|
51
+ opts = {}
52
+ opts[:recursive] = true if from.is_a?(String) and File.directory?(from)
53
+ scp = Net::SCP.new(connection)
54
+ scp.upload!(from, to, opts)
55
+ end
56
+ rescue Net::SCP::Error => e
57
+ # If we get the exit code of 127, then this means SCP is unavailable.
58
+ raise "scp unavailable" if e.message =~ /\(127\)/
59
+
60
+ # Otherwise, just raise the error up
61
+ raise
62
+ end
63
+
64
+ protected
65
+
66
+ # Opens an SSH connection and yields it to a block.
67
+ def connect
68
+ if @connection && !@connection.closed?
69
+ # There is a chance that the socket is closed despite us checking
70
+ # 'closed?' above. To test this we need to send data through the
71
+ # socket.
72
+ begin
73
+ @connection.exec!("")
74
+ rescue IOError
75
+ @logger.info("Connection has been closed. Not re-using.")
76
+ @connection = nil
77
+ end
78
+
79
+ # If the @connection is still around, then it is valid,
80
+ # and we use it.
81
+ if @connection
82
+ @logger.debug("Re-using SSH connection.")
83
+ return yield @connection if block_given?
84
+ return
85
+ end
86
+ end
87
+
88
+ # Connect to SSH, giving it a few tries
89
+ connection = nil
90
+ # These are the exceptions that we retry because they represent
91
+ # errors that are generally fixed from a retry and don't
92
+ # necessarily represent immediate failure cases.
93
+ exceptions = [
94
+ Errno::ECONNREFUSED,
95
+ Errno::EHOSTUNREACH,
96
+ Net::SSH::Disconnect,
97
+ Timeout::Error
98
+ ]
99
+
100
+ @logger.info("Connecting to SSH: (#{@server.host}:#{@server.port}")
101
+ @server.ui.info "Connecting..."
102
+ connection = retryable(:tries => 10, :on => exceptions) do
103
+ Net::SSH.start(@server.host, @server.username, @server.ssh_opts)
104
+ end
105
+
106
+ @connection = connection
107
+
108
+ # This is hacky but actually helps with some issues where
109
+ # Net::SSH is simply not robust enough to handle... see
110
+ # issue #391, #455, etc.
111
+ # sleep 4
112
+
113
+ # Yield the connection that is ready to be used and
114
+ # return the value of the block
115
+ return yield connection if block_given?
116
+ end
117
+
118
+ # Executes the command on an SSH connection within a login shell.
119
+ def shell_execute(connection, command, opts={})
120
+
121
+ opts[:sudo] ||= false
122
+ opts[:stream] ||= false
123
+
124
+ @logger.info("Execute: #{command} (opts=#{opts.inspect})")
125
+ exit_status = nil
126
+
127
+ # Determine the shell to execute. If we are using `sudo` then we
128
+ # need to wrap the shell in a `sudo` call.
129
+ shell = "bash -l"
130
+ shell = "sudo -H #{shell}" if opts[:sudo]
131
+
132
+ # Open the channel so we can execute or command
133
+ channel = connection.open_channel do |ch|
134
+ ch.exec(shell) do |ch2, _|
135
+ # Setup the channel callbacks so we can get data and exit status
136
+ ch2.on_data do |ch3, data|
137
+ @logger.debug("stdout: #{data}")
138
+ if block_given?
139
+ # Filter out the clear screen command
140
+ data = remove_ansi_escape_codes(data)
141
+ yield :stdout, data
142
+ end
143
+ if opts[:stream]
144
+ @server.ui.info data, {:prefix => false, :new_line => false}
145
+ end
146
+ end
147
+
148
+ ch2.on_extended_data do |ch3, type, data|
149
+ @logger.debug("stderr: #{data}")
150
+ if block_given?
151
+ # Filter out the clear screen command
152
+ data = remove_ansi_escape_codes(data)
153
+ yield :stderr, data
154
+ end
155
+ if opts[:stream]
156
+ @server.ui.info data, {:prefix => false, :new_line => false}
157
+ end
158
+ end
159
+
160
+ ch2.on_request("exit-status") do |ch3, data|
161
+ exit_status = data.read_long
162
+ @logger.debug("Exit status: #{exit_status}")
163
+ end
164
+
165
+ # Set the terminal
166
+ ch2.send_data "export TERM=vt100\n"
167
+
168
+ # Output the command
169
+ ch2.send_data "#{command}\n"
170
+
171
+ # Remember to exit or this channel will hang open
172
+ ch2.send_data "exit\n"
173
+ end
174
+ end
175
+
176
+ # Wait for the channel to complete
177
+ channel.wait
178
+
179
+ # Return the final exit status
180
+ return exit_status
181
+ end
182
+
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,6 @@
1
+ module Caterer
2
+ module Communication
3
+ autoload :Rsync, 'caterer/communication/rsync'
4
+ autoload :SSH, 'caterer/communication/ssh'
5
+ end
6
+ end
@@ -1,17 +1,30 @@
1
- module Caterer::Config
1
+ module Caterer
2
+ module Config
3
+ class Base
2
4
 
3
- class Base
4
- attr_accessor :roles
5
+ attr_reader :images, :groups
5
6
 
6
- def initialize
7
- @roles = []
8
- end
7
+ def initialize
8
+ @images = {}
9
+ @groups = {}
10
+ end
11
+
12
+ def image(name)
13
+ @images[name] ||= Image.new(name)
14
+ yield @images[name] if block_given?
15
+ end
16
+
17
+ def group(name)
18
+ @groups[name] ||= Group.new(name)
19
+ yield @groups[name] if block_given?
20
+ end
21
+
22
+ def member(name, &block)
23
+ group(:default) do |d|
24
+ d.member(name, &block)
25
+ end
26
+ end
9
27
 
10
- def role(name)
11
- role = Caterer::Config::Role.new(name)
12
- yield role if block_given?
13
- @roles << role
14
28
  end
15
29
  end
16
-
17
30
  end
@@ -0,0 +1,24 @@
1
+ module Caterer
2
+ module Config
3
+ class Group
4
+
5
+ attr_reader :name
6
+ attr_accessor :images, :members, :user, :password
7
+
8
+ def initialize(name=nil)
9
+ @name = name
10
+ @images = []
11
+ @members = {}
12
+ end
13
+
14
+ def add_image(image)
15
+ @images << image
16
+ end
17
+
18
+ def member(name)
19
+ @members[name] ||= Member.new(name)
20
+ yield @members[name] if block_given?
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Caterer
4
+ module Config
5
+ class Image
6
+
7
+ attr_reader :name, :provisioner
8
+
9
+ def initialize(name)
10
+ @name = name
11
+ end
12
+
13
+ def provision(type)
14
+ @provisioner = "Caterer::Config::Provision::#{type.to_s.classify}".constantize.new(type)
15
+ yield @provisioner if block_given?
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ module Caterer
2
+ module Config
3
+ class Member
4
+
5
+ attr_reader :name
6
+ attr_accessor :host, :port, :user, :password, :images
7
+
8
+ def initialize(name=nil)
9
+ @name = name
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -1,18 +1,42 @@
1
- module Caterer::Config::Provision
1
+ module Caterer
2
+ module Config
3
+ module Provision
2
4
 
3
- class ChefSolo
4
-
5
- attr_accessor :recipes, :json
5
+ class ChefSolo
6
+
7
+ attr_reader :name, :run_list
8
+ attr_accessor :json, :cookbooks_path, :roles_path, :data_bags_path, :bootstrap_script
6
9
 
7
- def initialize
8
- @recipes = []
9
- @json = {}
10
- end
10
+ def initialize(name)
11
+ @name = name
12
+ @run_list = []
13
+ @json = {}
14
+ @cookbooks_path = ['cookbooks']
15
+ @roles_path = ['roles']
16
+ @data_bags_path = ['data_bags']
17
+ end
11
18
 
12
- def add_recipe(recipe)
13
- @recipes << recipe
14
- end
19
+ def add_recipe(recipe)
20
+ @run_list << "recipe[#{recipe}]"
21
+ end
15
22
 
16
- end
23
+ def add_role(role)
24
+ @run_list << "role[#{role}]"
25
+ end
26
+
27
+ def errors
28
+ errors = {}
17
29
 
30
+ if not @run_list.length > 0
31
+ errors[:run_list] = "is empty"
32
+ end
33
+
34
+ if errors.length > 0
35
+ errors
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
18
42
  end
@@ -1,5 +1,7 @@
1
- module Caterer::Config
2
- module Provision
3
- autoload :ChefSolo, 'caterer/config/provision/chef_solo'
1
+ module Caterer
2
+ module Config
3
+ module Provision
4
+ autoload :ChefSolo, 'caterer/config/provision/chef_solo'
5
+ end
4
6
  end
5
7
  end
@@ -1,7 +1,11 @@
1
1
  module Caterer
2
2
  module Config
3
3
  autoload :Base, 'caterer/config/base'
4
- autoload :Role, 'caterer/config/role'
4
+ autoload :Cluster, 'caterer/config/cluster'
5
+ autoload :Group, 'caterer/config/group'
6
+ autoload :Member, 'caterer/config/member'
7
+ autoload :Node, 'caterer/config/node'
8
+ autoload :Image, 'caterer/config/image'
5
9
  autoload :Provision, 'caterer/config/provision'
6
10
  end
7
11
  end
@@ -9,42 +9,68 @@ module Caterer
9
9
  opts = {
10
10
  :cwd => nil,
11
11
  :caterfile_name => nil,
12
- :ui_class => nil
12
+ :ui_class => nil,
13
+ :custom_config => nil
13
14
  }.merge(opts)
14
15
 
15
16
  opts[:cwd] ||= ENV["CATERER_CWD"] if ENV.has_key?("CATERER_CWD")
16
17
  opts[:cwd] ||= Dir.pwd
17
- opts[:cwd] = Pathname.new(opts[:cwd])
18
18
 
19
19
  opts[:caterfile_name] ||= []
20
- opts[:caterfile_name] = [opts[:caterfile_name]] if !opts[:vagrantfile_name].is_a?(Array)
20
+ opts[:caterfile_name] = [opts[:caterfile_name]] if !opts[:caterfile_name].is_a?(Array)
21
21
  opts[:caterfile_name] += ["Caterfile"]
22
22
 
23
- @cwd = opts[:cwd]
23
+ @cwd = Pathname.new(opts[:cwd])
24
24
  @caterfile_name = opts[:caterfile_name]
25
+ @custom_config = opts[:custom_config]
25
26
 
26
27
  ui_class = opts[:ui_class] || Vli::UI::Silent
27
28
  @ui = ui_class.new("cater")
28
29
 
29
30
  end
30
31
 
31
- def load!
32
- load_default_config
33
- load_custom_config
32
+ def action_runner
33
+ @action_runner ||= Vli::Action::Runner.new(action_registry) do
34
+ {
35
+ :action_runner => action_runner,
36
+ :ui => @ui
37
+ }
38
+ end
39
+ end
40
+
41
+ def action_registry
42
+ Caterer.actions
43
+ end
44
+
45
+ def config
46
+ @config ||= begin
47
+ load_default_config
48
+ load_custom_config
49
+ Caterer.config
50
+ end
34
51
  end
52
+ alias :load! :config
35
53
 
36
54
  def load_default_config
37
- # doesn't work yet
38
- # require 'config/default'
55
+ require_relative '../../config/default'
39
56
  end
40
57
 
41
58
  def load_custom_config
42
- @caterfile_name.each do |config_file|
43
- file = "#{@cwd}/#{config_file}"
44
- load file if File.exists? file
59
+ if @custom_config
60
+ # if it's been explicitly defined, load it
61
+ load_config_file @custom_config
62
+ else
63
+ # lets try a few variations
64
+ @caterfile_name.each do |config_file|
65
+ load_config_file "#{@cwd}/#{config_file}"
66
+ end
45
67
  end
46
68
  end
47
69
 
70
+ def load_config_file(file)
71
+ load file if File.exists? file
72
+ end
73
+
48
74
  def cli(*args)
49
75
  Cli.new(args.flatten, self).execute
50
76
  end
@@ -0,0 +1,19 @@
1
+ module Caterer
2
+ module Provisioner
3
+ class Base
4
+
5
+ attr_reader :server
6
+
7
+ def initialize(server, config=nil)
8
+ @server = server
9
+ @config = config
10
+ end
11
+
12
+ def bootstrap(script=nil); end
13
+ def prepare; end
14
+ def provision; end
15
+ def cleanup; end
16
+
17
+ end
18
+ end
19
+ end