Created
October 29, 2010 01:08
-
-
Save mdub/652683 to your computer and use it in GitHub Desktop.
bootstrap a remote machine as a Chef server/client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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