caterer 0.0.1 → 0.1.0

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