#!/usr/bin/ruby
# vim: softtabstop=4 shiftwidth=4 expandtab
#
# = Synopsis
#
# Use the Puppet RAL to directly interact with the system.
#
# = Usage
#
#   ralsh [-h|--help] [-d|--debug] [-v|--verbose] [-e|--edit] [-H|--host <host>]
#           [-p|--param <param>] [-t|--types] type <name>
#
# = Description
#
# This command provides simple facilities for converting current system state
# into Puppet code, along with some ability to use Puppet to affect the current
# state.
#
# By default, you must at least provide a type to list, which case ralsh
# will tell you everything it knows about all instances of that type.  You can
# optionally specify an instance name, and ralsh will only describe that single
# instance.
#
# You can also add +--edit+ as an argument, and ralsh will write its output
# to a file, open that file in an editor, and then apply the file as a Puppet
# transaction.  You can easily use this to use Puppet to make simple changes to
# a system.
#
# = Options
#
# Note that any configuration parameter that's valid in the configuration file
# is also a valid long argument.  For example, 'ssldir' is a valid configuration
# parameter, so you can specify '--ssldir <directory>' 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 puppet with
# '--genconfig'.
#
# debug::
#   Enable full debugging.
#
# edit:
#   Write the results of the query to a file, open the file in an editor,
#   and read the file back in as an executable Puppet manifest.
#
# host:
#   When specified, connect to the resource server on the named host
#   and retrieve the list of resouces of the type specified.
#
# help:
#   Print this help message.
#
# param:
#   Add more parameters to be outputted from queries.
#
# types:
#   List all available types.
#
# verbose::
#   Print extra information.
#
# = Example
#
#   $ ralsh user luke
#   user { 'luke':
#       home => '/home/luke',
#       uid => '100',
#       ensure => 'present',
#       comment => 'Luke Kanies,,,',
#       gid => '1000',
#       shell => '/bin/bash',
#       groups => ['sysadmin','audio','video','puppet']
#   }
#   $
#
# = Author
#
# Luke Kanies
#
# = Copyright
#
# Copyright (c) 2005-2007 Reductive Labs, LLC
# Licensed under the GNU Public License


require 'getoptlong'
require 'puppet'

options = [
    [ "--debug",	"-d",			GetoptLong::NO_ARGUMENT ],
    [ "--verbose",  "-v",			GetoptLong::NO_ARGUMENT ],
    [ "--types",    "-t",                   GetoptLong::NO_ARGUMENT ],
    [ "--param",    "-p",                   GetoptLong::REQUIRED_ARGUMENT ],
    [ "--host",     "-H",                   GetoptLong::REQUIRED_ARGUMENT ],
    [ "--edit",             "-e",                   GetoptLong::NO_ARGUMENT ],
    [ "--help",             "-h",                   GetoptLong::NO_ARGUMENT ]
]

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

result = GetoptLong.new(*options)

debug = false
verbose = false
edit = false
extra_params = []
host = nil

result.each { |opt,arg|
    case opt
    when "--host"
        host = arg
    when "--types"
        types = []
        Puppet::Type.loadall
        Puppet::Type.eachtype do |t|
            next if t.name == :component
            types << t.name.to_s
        end
        puts types.sort
        exit
    when "--param"
        extra_params << arg.to_sym
    when "--edit"
        edit = true
    when "--help"
        if Puppet.features.usage?
            RDoc::usage
        else
            puts "install RDoc:usage for help"
        end
        exit
    when "--verbose"
        verbose = true
    when "--debug"
        debug = true
    else
        # Anything else is handled by the config stuff
        Puppet.config.handlearg(opt, arg)
    end
}

Puppet::Util::Log.newdestination(:console)

# Now parse the config
Puppet.parse_config

if debug
    Puppet::Util::Log.level = :debug
elsif verbose
    Puppet::Util::Log.level = :info
end


if ARGV.length > 0
    type = ARGV.shift
else
    raise "You must specify the type to display"
end

name = nil
params = {}
if ARGV.length > 0
    name = ARGV.shift
end

if ARGV.length > 0
    ARGV.each do |setting|
        if setting =~ /^(\w+)=(.+)$/
            params[$1] = $2
        else
            raise "Invalid parameter setting %s" % setting
        end
    end
end

if edit and host
    raise "You cannot edit a remote host"
end

typeobj = nil

unless typeobj = Puppet::Type.type(type)
    raise "Could not find type %s" % type
end

properties = typeobj.properties.collect { |s| s.name }

format = proc {|trans|
    trans.dup.collect do |param, value|
        if value == "" or value == []
            trans.delete(param)
        end

        unless properties.include?(param) or extra_params.include?(param)
            trans.delete(param)
        end
    end
    trans.to_manifest
}

text = if host
    client = Puppet::Network::Client.resource.new(:Server => host, :Port => Puppet[:puppetport])
    unless client.read_cert
        raise "client.read_cert failed"
    end
    begin
        # They asked for a single resource.
        if name
            transbucket = [client.describe(type, name)]
        else
            # Else, list the whole thing out.
            transbucket = client.instances(type)
        end
    rescue Puppet::Network::XMLRPCClientError => exc
        raise "client.list(#{type}) failed: #{exc.message}"
    end
    transbucket.sort { |a,b| a.name <=> b.name }.collect(&format)
else
    if name
        obj = typeobj.create(:name => name, :check => properties)
        vals = obj.retrieve

        unless params.empty?
            params.each do |param, value|
                obj[param] = value
            end
            comp = Puppet::Type.type(:component).create(:name => "ralsh")
            comp.push(obj)
            transaction = comp.evaluate
            begin
                transaction.evaluate
            rescue => detail
                if Puppet[:trace]
                    puts detail.backtrace
                end
            end

        end
        [format.call(obj.to_trans(true))]
    else
        typeobj.instances.collect do |obj|
            next if ARGV.length > 0 and ! ARGV.include? obj.name
            trans = obj.to_trans(true)
            format.call(trans)
        end
    end
end.compact.join("\n")

if edit
    file = "/tmp/x2puppet-#{Process.pid}.pp"
    begin
        File.open(file, "w") do |f|
            f.puts text
        end
        ENV["EDITOR"] ||= "vi"
        system(ENV["EDITOR"], file)
        system("puppet -v " + file)
    ensure
        #if FileTest.exists? file
        #    File.unlink(file)
        #end
    end
else
    puts text
end

# $Id: ralsh 2573 2007-06-13 22:31:52Z luke $


syntax highlighted by Code2HTML, v. 0.9.1