Skip to content

Instantly share code, notes, and snippets.

@mdub
Created October 29, 2010 01:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save mdub/652683 to your computer and use it in GitHub Desktop.
Save mdub/652683 to your computer and use it in GitHub Desktop.
bootstrap a remote machine as a Chef server/client
#!/usr/bin/env ruby
require "rubygems"
require "erb"
require "fileutils"
require "json"
require "net/ssh"
require "net/scp"
require "optparse"
RUBYGEMS_VERSION = "1.3.7"
BOOTSTRAP_TEMPLATE = <<BASH
announce() {
echo ""
echo "*** $1 ***"
}
set -e
announce "Setting hostname"
echo <%= target_host_shortname %> > /etc/hostname
hostname <%= target_host_shortname %>
cat > /etc/hosts <<EOF
127.0.0.1 <%= target_host_fqdn %> <%= target_host_shortname %> localhost
EOF
hostname -f
announce "Installing debian packages"
apt-get install -y ruby ruby-dev irb build-essential wget ssl-cert libopenssl-ruby
if [ ! -x /usr/bin/gem ]; then
announce "Installing Rubygems"
wget -O - http://production.cf.rubygems.org/rubygems/rubygems-#{RUBYGEMS_VERSION}.tgz | tar xzv --directory /tmp
ruby /tmp/rubygems-#{RUBYGEMS_VERSION}/setup.rb --no-format-executable
fi
if [ ! -x /usr/bin/chef-solo ]; then
announce "Installing chef"
gem install --no-ri --no-rdoc chef
fi
announce "Configuring chef-solo"
mkdir -p /etc/chef
mkdir -p /tmp/chef-solo
cat > /etc/chef/solo.rb <<EOF
file_cache_path "/tmp/chef-solo"
cookbook_path "/tmp/chef-solo/cookbooks"
EOF
<% if client? %>
(
cat <<'EOP'
<%= IO.read(validation_key_file) %>
EOP
) | awk NF > /etc/chef/validation.pem
<% end %>
tee /tmp/chef-bootstrap.json <<EOF
<%= JSON.pretty_generate(json_attributes) %>
EOF
announce "Bootstrapping with chef-solo"
chef-solo \\
--node-name <%= target_host_fqdn %> \\
--json-attributes /tmp/chef-bootstrap.json \\
--recipe-url "<%= recipe_url %>"
<% if client? %>
announce "Running chef-client"
sv stop chef-client
chef-client
announce "Starting chef-client service"
sv start chef-client
<% end %>
<% if server? %>
test -f /etc/chef/admin.pem || {
announce "Generating admin keypair"
knife client create admin -a -n -u chef-webui -k /etc/chef/webui.pem -f /etc/chef/admin.pem
}
<% end %>
announce "DONE"
BASH
KNIFE_RB_TEMPLATE = <<RUBY
chef_server_url "http://<%= target_host_fqdn %>:4000"
node_name "admin"
client_key File.expand_path("../admin.pem", __FILE__)
validation_client_name "chef-validator"
validation_key File.expand_path("../validation.pem", __FILE__)
RUBY
class ChefBootstrap
DEFAULT_RECIPE_URL = "http://s3.amazonaws.com/chef-solo/bootstrap-0.9.8.tar.gz"
DEFAULT_CHEF_DIR = ".chef"
attr_accessor :target_host_fqdn
attr_accessor :validation_key_file
attr_accessor :ssh_identity_file
attr_accessor :ssh_password
attr_accessor :recipe_url
attr_accessor :chef_dir
attr_reader :json_attributes
def self.attr_boolean_accessor(*names)
names.each do |name|
attr_accessor name
alias_method "#{name}?", name
end
end
attr_boolean_accessor :server, :client, :do_ssh
def initialize
self.do_ssh = true
self.recipe_url = DEFAULT_RECIPE_URL
self.chef_dir = DEFAULT_CHEF_DIR
@json_attributes = {
"chef" => {},
"run_list" => []
}
end
def server_url
json_attributes["chef"]["server_url"]
end
def server_url=(url)
json_attributes["chef"]["server_url"] = url
end
def target_host_shortname
target_host_fqdn.sub(/\..*/, '')
end
def command_line_parser
@command_line_parser ||= OptionParser.new do |opts|
opts.banner = "usage: chef-bootstrap [options] HOST_FQDN"
opts.separator "\n Options:\n\n"
opts.on("-i", "--identity-file FILE", %[the SSH private-key file]) do |file|
self.ssh_identity_file = file
end
opts.on("-P", "--ssh-password PASSWORD", %[the SSH password]) do |password|
self.ssh_password = password
end
opts.on("--no-ssh", %[just print the bootstrapping script]) do
self.do_ssh = false
end
opts.on("--run-list X,Y,Z", Array, %[comma-separated recipies to run]) do |run_list|
attributes["run_list"] = run_list
end
opts.on("--client", %[bootstrap a Chef client]) do
self.client = true
json_attributes["run_list"] = ["recipe[chef::bootstrap_client]"]
json_attributes["chef"]["client_interval"] ||= 120
json_attributes["chef"]["client_splay"] ||= 5
end
opts.on("--server", %[bootstrap a Chef server]) do
self.server = true
json_attributes["run_list"] = ["recipe[chef::bootstrap_server]"]
json_attributes["chef"]["server_url"] ||= "http://localhost:4000"
json_attributes["chef"]["webui_enabled"] ||= true
self.validation_key_file ||= "validation.pem"
end
opts.on("-d", "--chef-dir DIR", %[Chef config directory], %[ (default: "#{DEFAULT_CHEF_DIR}")]) do |dir|
self.chef_dir = dir
end
opts.on("-s", "--server-address ADDRESS", %[Chef server hostname or IP]) do |address|
address += ":4000" unless address =~ /:/
json_attributes["chef"]["server_url"] = "http://#{address}"
end
opts.on("-V", "--validation-key FILE", %[the Chef server validation.pem file], %[ (default: "#{DEFAULT_CHEF_DIR}/validation.pem")]) do |file|
self.validation_key_file = file
end
opts.on("--recipe-url URL", %[alternate source of recipes], " (should be a gzipped tarball)") do |url|
self.recipe_url = url
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
end
end
class UsageError < StandardError; end
def validate_arguments
unless target_host_fqdn
raise UsageError, "no hostname provided"
end
unless target_host_fqdn =~ /^[a-z][a-z0-9_-]+\./
raise UsageError, "a fully-qualified hostname is required"
end
if client?
raise(UsageError, "--server-address required") unless server_url
self.validation_key_file ||= "#{chef_dir}/validation.pem" if chef_dir
raise(UsageError, "--validation-key required") unless validation_key_file
raise(UsageError, "Cannot read validation key from #{validation_key_file.inspect}") unless File.readable?(validation_key_file)
end
end
def parse_command_line(argv)
argv = argv.dup
command_line_parser.parse!(argv)
self.target_host_fqdn = argv.shift
begin
validate_arguments
rescue UsageError => e
$stderr.puts "ERROR: #{e}"
$stderr.puts ""
$stderr.puts command_line_parser.to_s
exit 1
end
end
def bootstrap_script
script_template = ERB.new(BOOTSTRAP_TEMPLATE, nil, "<>")
script = script_template.result(binding)
end
def using_ssh
ssh_opts = {}
ssh_opts[:keys] = File.expand_path(ssh_identity_file) if ssh_identity_file
ssh_opts[:password] = ssh_password if ssh_password
Net::SSH.start(target_host_fqdn, 'root', ssh_opts) do |ssh|
yield ssh
end
end
def execute_command(ssh, command)
ssh.open_channel do |channel|
channel.exec(command) do |ch, success|
raise "could not execute command: #{command.inspect}" unless success
channel.on_data do |_ch, data|
$stdout.print(data)
end
channel.on_extended_data do |_ch, _type, data|
$stderr.print(data)
end
channel.on_request("exit-status") do |ch, data|
exit_code = data.read_long
unless exit_code.zero?
$stderr.puts "WARNING: remote command exited with status #{exit_code}"
exit(exit_code)
end
end
channel.on_request("exit-signal") do |ch, data|
$stderr.puts "WARNING: remote command terminated with signal"
exit 1
end
end
end.wait
end
def generate_chef_dir(ssh)
puts "\n*** Writing config to #{chef_dir}/knife.rb"
FileUtils.mkpath(chef_dir)
ssh.scp.download!("/etc/chef/admin.pem", "#{chef_dir}/admin.pem")
ssh.scp.download!("/etc/chef/validation.pem", "#{chef_dir}/validation.pem")
knife_rb_template = ERB.new(KNIFE_RB_TEMPLATE)
File.open("#{chef_dir}/knife.rb", "w") do |io|
io.print(knife_rb_template.result(binding))
end
end
def execute
script = bootstrap_script
if do_ssh?
using_ssh do |ssh|
execute_command(ssh, "bash -c '#{script}'")
if server?
generate_chef_dir(ssh)
end
end
else
puts script
end
end
def self.run(argv)
b = new
b.parse_command_line(argv)
b.execute
end
end
ChefBootstrap.run(ARGV)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment