#!/usr/local/bin/ruby
# == Synopsis 
#
# Retrieve the client configuration from the central puppet server and apply
# it to the local host.
#
# Currently must be run out periodically, using cron or something similar.
#
# = Usage
#
#   puppetd  [-D|--daemonize|--no-daemonize] [-d|--debug] [--disable] [--enable]
#       [-h|--help] [--fqdn <host name>] [-l|--logdest syslog|<file>|console]
#       [-o|--onetime] [--serve <handler>] [-t|--test]
#       [-V|--version] [-v|--verbose] [-w|--waitforcert <seconds>]
#
# = Description
#
# This is the main puppet client.  Its job is to retrieve the local machine's
# configuration from a remote server and apply it.  In order to successfully
# communicate with the remote server, the client must have a certificate signed
# by a certificate authority that the server trusts; the recommended method
# for this, at the moment, is to run a certificate authority as part of the
# puppet server (which is the default).  The client will connect and request
# a signed certificate, and will continue connecting until it receives one.
#
# Once the client has a signed certificate, it will retrieve its configuration
# and apply it.
#
# = Usage Notes
#
# +puppetd+ does its best to find a compromise between interactive use and
# daemon use.  Run with no arguments and no configuration, it will go into the
# backgroun, attempt to get a signed certificate, and retrieve and apply its
# configuration every 30 minutes.
#
# Some flags are meant specifically for interactive use -- in particular,
# +test+ and +tags+ are useful.  +test+ enables verbose logging, causes
# the daemon to stay in the foreground, exits if the server's configuration is
# invalid (this happens if, for instance, you've left a syntax error on the
# server), and exits after running the configuration once (rather than hanging
# around as a long-running process).
#
# +tags+ allows you to specify what portions of a configuration you want to apply.
# Puppet elements are tagged with all of the class or definition names that
# contain them, and you can use the +tags+ flag to specify one of these names,
# causing only configuration elements contained within that class or definition
# to be applied.  This is very useful when you are testing new configurations --
# for instance, if you are just starting to manage +ntpd+, you would put all of
# the new elements into an +ntpd+ class, and call puppet with +--tags ntpd+,
# which would only apply that small portion of the configuration during your
# testing, rather than applying the whole thing.
#
# = Options
#
# Note that any configuration parameter that's valid in the configuration file
# is also a valid long argument.  For example, 'server' is a valid configuration
# parameter, so you can specify '--server <servername>' as an argument.
#
# See the configuration file documentation at
# http://reductivelabs.com/projects/puppet/reference/configref.html for
# the full list of acceptable parameters. A commented list of all
# configuration options can also be generated by running puppetd with
# '--genconfig'.
#
# daemonize::
#   Send the process into the background.  This is the default.
#
# no-daemonize::
#   Do not send the process into the background.
#
# debug::
#   Enable full debugging.
#
# disable::
#   Disable working on the local system.  This puts a lock file in place,
#   causing +puppetd+ not to work on the system until the lock file is removed.
#   This is useful if you are testing a configuration and do not want the central
#   configuration to override the local state until everything is tested and
#   committed.
#
#   +puppetd+ uses the same lock file while it is running, so no more than one
#   +puppetd+ process is working at a time.
#
#   +puppetd+ exits after executing this.
#
# enable::
#   Enable working on the local system.  This removes any lock file, causing
#   +puppetd+ to start managing the local system again (although it will continue
#   to use its normal scheduling, so it might not start for another half hour).
#
#   +puppetd+ exits after executing this.
#
# fqdn::
#   Set the fully-qualified domain name of the client.  This is only used for
#   certificate purposes, but can be used to override the discovered hostname.
#   If you need to use this flag, it is generally an indication of a setup problem.
#
# help::
#   Print this help message
#
# logdest::
#   Where to send messages.  Choose between syslog, the console, and a log file.
#   Defaults to sending messages to syslog, or the console if debugging or
#   verbosity is enabled.
#
# no-client::
#   Do not create a config client.  This will cause the daemon to run
#   without ever checking for its configuration automatically, and only
#   makes sense when used in conjunction with --listen.
#
# onetime::
#   Run the configuration once, rather than as a long-running daemon.  This is
#   useful for interactively running puppetd.
#
# serve::
#   Start another type of server.  By default, +puppetd+ will start
#   a service handler that allows authenticated and authorized remote nodes to
#   trigger the configuration to be pulled down and applied.  You can specify
#   any handler here that does not require configuration, e.g., filebucket, ca,
#   or resource.  The handlers are in +lib/puppet/network/handler+, and the names
#   must match exactly, both in the call to +serve+ and in +namespaceauth.conf+.
#
# test::
#   Enable the most common options used for testing.  These are +onetime+,
#   +verbose+, +ignorecache, and +no-usecacheonfailure+.
#
# verbose::
#   Turn on verbose reporting.
#
# version::
#   Print the puppet version number and exit.
#
# waitforcert::
#   This option only matters for daemons that do not yet have certificates
#   and it is enabled by default, with a value of 120 (seconds).  This causes
#   +puppetd+ to connect to the server every 2 minutes and ask it to sign a
#   certificate request.  This is useful for the initial setup of a puppet
#   client.  You can turn off waiting for certificates by specifying a time
#   of 0.
#
# = Example
#
#   puppetd --server puppet.domain.com
#
# = Author
#
# Luke Kanies
#
# = Copyright
#
# Copyright (c) 2005, 2006 Reductive Labs, LLC
# Licensed under the GNU Public License

# Do an initial trap, so that cancels don't get a stack trace.
trap(:INT) do
    $stderr.puts "Cancelling startup"
    exit(0)
end

require 'puppet'
require 'puppet/network/client'
require 'getoptlong'

options = [
	[ "--centrallogging",			GetoptLong::NO_ARGUMENT ],
	[ "--disable",	    			GetoptLong::NO_ARGUMENT ],
	[ "--debug",	"-d",			GetoptLong::NO_ARGUMENT ],
	[ "--enable",	    			GetoptLong::NO_ARGUMENT ],
	[ "--fqdn",     "-f",			GetoptLong::REQUIRED_ARGUMENT ],
	[ "--help",		"-h",			GetoptLong::NO_ARGUMENT ],
	[ "--logdest",	"-l",			GetoptLong::REQUIRED_ARGUMENT ],
	[ "--onetime",  "-o",			GetoptLong::NO_ARGUMENT ],
	[ "--test",	    "-t",			GetoptLong::NO_ARGUMENT ],
	[ "--serve",    "-s",			GetoptLong::REQUIRED_ARGUMENT ],
	[ "--no-client",       			GetoptLong::NO_ARGUMENT ],
	[ "--verbose",	"-v",			GetoptLong::NO_ARGUMENT ],
	[ "--version",	"-V",			GetoptLong::NO_ARGUMENT ],
	[ "--waitforcert",	"-w",		GetoptLong::REQUIRED_ARGUMENT ]
]

# Add all of the config parameters as valid options.
Puppet.settings.addargs(options)

result = GetoptLong.new(*options)

args = {}

options = {
    :waitforcert => 120,  # Default to checking for certs every 5 minutes
    :onetime => false,
    :verbose => false,
    :debug => false,
    :centrallogs => false,
    :setdest => false,
    :enable => false,
    :disable => false,
    :client => true,
    :fqdn => nil,
    :serve => {}
}

begin
    explicit_waitforcert = false
    result.each { |opt,arg|
        case opt
            # First check to see if the argument is a valid configuration parameter;
            # if so, set it. NOTE: there is a catch-all at the bottom for defaults.rb
            when "--disable"
                options[:disable] = true
            when "--serve"
                if Puppet::Network::Handler.handler(arg)
                    options[:serve][arg.to_sym] = {}
                else
                    raise "Could not find handler for %s" % arg
                end
            when "--enable"
                options[:enable] = true
            when "--test"
                options[:test] = true
            when "--centrallogging"
                options[:centrallogs] = true
            when "--help"
                if Puppet.features.usage?
                    RDoc::usage && exit
                else
                    puts "No help available unless you have RDoc::usage installed"
                    exit
                end
            when "--version"
                puts "%s" % Puppet.version
                exit
            when "--verbose"
                options[:verbose] = true
            when "--debug"
                options[:debug] = true
            when "--fqdn"
                options[:fqdn] = arg
            when "--no-client"
                options[:client] = false
            when "--onetime"
                options[:onetime] = true
                options[:waitforcert] = 0 unless explicit_waitforcert
            when "--port"
                args[:Port] = arg
            when "--logdest"
                begin
                    Puppet::Util::Log.newdestination(arg)
                    options[:setdest] = true
                rescue => detail
                    if Puppet[:debug]
                        puts detail.backtrace
                    end
                    $stderr.puts detail.to_s
                end
            when "--waitforcert"
                options[:waitforcert] = arg.to_i
                explicit_waitforcert = true
            else
                Puppet.settings.handlearg(opt, arg)
        end
    }
rescue GetoptLong::InvalidOption => detail
    $stderr.puts detail
    $stderr.puts "Try '#{$0} --help'"
    exit(1)
end

# Now parse the config
Puppet.parse_config

if options[:test]
    # Enable all of the most common test options.
    Puppet.settings.handlearg("--ignorecache")
    Puppet.settings.handlearg("--no-usecacheonfailure")
    Puppet.settings.handlearg("--no-splay")
    Puppet.settings.handlearg("--show_diff")
    Puppet.settings.handlearg("--no-daemonize")
    options[:verbose] = true
    options[:onetime] = true
    options[:waitforcert] = 0
end

# Handle the logging settings.
if options[:debug] or options[:verbose]
    Puppet::Util::Log.newdestination(:console)
    if options[:debug]
        Puppet::Util::Log.level = :debug
    else
        Puppet::Util::Log.level = :info
    end
end

unless options[:setdest]
    Puppet::Util::Log.newdestination(:syslog)
end

Puppet.genconfig
Puppet.genmanifest

# If noop is set, then also enable diffs
if Puppet[:noop]
    Puppet[:show_diff] = true
end

args[:Server] = Puppet[:server]
if options[:fqdn]
    args[:FQDN] = options[:fqdn]
    Puppet[:certname] = options[:fqdn] 
end

if options[:centrallogs]
    logdest = args[:Server]

    if args.include?(:Port)
        logdest += ":" + args[:Port]
    end
    Puppet::Util::Log.newdestination(logdest)
end

# We need tomake the client either way, we just don't start it
# if --no-client is set.
client = Puppet::Network::Client.master.new(args)
if options[:enable]
    client.enable
elsif options[:disable]
    client.disable
end

if options[:enable] or options[:disable]
    exit(0)
end

server = nil

# It'd be nice to daemonize later, but we have to daemonize before the
# waitforcert happens.
if Puppet[:daemonize]
    client.daemonize
end

unless Puppet::Network::HttpPool.read_cert
    # If we don't already have the certificate, then create a client to
    # request one.  Use the special ca stuff, don't use the normal server and port.
    caclient = Puppet::Network::Client.ca.new()
    if options[:waitforcert] > 0
        begin
            while ! caclient.request_cert do
                Puppet.notice "Did not receive certificate"
                sleep options[:waitforcert]
            end
        rescue => detail
            Puppet.err "Could not request certificate: %s" % detail.to_s
            exit(23)
        end
    else
        unless caclient.request_cert
            Puppet.notice "No certificates; exiting"
            exit(1)
        end
    end

    # Now read the new cert in.
    if Puppet::Network::HttpPool.read_cert
        # If we read it in, then get rid of our existing http connection.
        client.recycle_connection
        Puppet.notice "Got signed certificate"
    else
        Puppet.err "Could not read certificates after retrieving them"
        exit(34)
    end
end

objects = []

# This has to go after the certs are dealt with.
if Puppet[:listen] and ! options[:onetime]
    unless FileTest.exists?(Puppet[:authconfig])
        Puppet.err "Will not start without authorization file %s" %
            Puppet[:authconfig]
        exit(14)
    end

    # FIXME: we should really figure out how to distribute the CRL
    # to clients. In the meantime, we just disable CRL checking if
    # the CRL file doesn't exist
    unless File::exist?(Puppet[:cacrl])
        Puppet[:cacrl] = 'false' 
    end

    handlers = nil

    if options[:serve].empty?
        handlers = {:Runner => {}}
    else
        handlers = options[:serve]
    end

    handlers.each do |name, hash|
        Puppet.info "Starting handler for %s" % name
    end

    args[:Handlers] = handlers
    args[:Port] = Puppet[:puppetport]

    require 'puppet/network/http_server/webrick'

    begin
        server = Puppet::Network::HTTPServer::WEBrick.new(args)
    rescue => detail
        $stderr.puts detail
        puts detail.backtrace
        exit(1)
    end

    objects << server
elsif options[:onetime] and Puppet[:listen]
    Puppet.notice "Ignoring --listen on onetime run"
end

if options[:client]
    objects << client
end

# Set traps for INT and TERM
Puppet.settraps

# If --onetime is specified, we don't run 'start', which means we don't
# create a pidfile.
if options[:onetime]
    unless options[:client]
        $stderr.puts "onetime is specified but there is no client"
        exit(43)
    end

    # Add the service, so the traps work correctly.
    Puppet.newservice(client)

    begin
        client.run
    rescue => detail
        if Puppet[:trace]
            puts detail.backtrace
        end
        Puppet.err detail.to_s
    end
    exit(0)
else
    if server
        Puppet.newservice(server)
    end

    if options[:client]
        Puppet.notice "Starting Puppet client version %s" % [Puppet.version]
        Puppet.newservice(client)
    end

    Puppet.settraps

    Puppet.start
end

