#!/usr/bin/env ruby

#
# = Synopsis
#
# Generate a reference for all Puppet types.  Largely meant for internal Reductive
# Labs use.
#
# = Usage
#
#   puppetdoc [-a|--all] [-h|--help] [-m|--mode <text|pdf|trac> [-s|--section <[type]|configuration|report|function>]
#
# = Description
#
# This command generates a restructured-text document describing all installed
# Puppet types or all allowable arguments to puppet executables.  It is largely
# meant for internal use and is used to generate the reference document
# available on the Reductive Labs web site.
#
# = Options
#
# all::
#   Output the docs for all of the reference types.
#
# help::
#   Print this help message
#
# mode::
#   Determine the output mode.  Valid modes are 'text', 'trac', and 'pdf'.  Note that 'trac' mode only works on Reductive Labs servers.  The default mode is 'text'.
#
# section::
#   Handle a particular section.  Available sections are 'type', 'configuration', 'report', and 'function'.  The default section is 'type'.
#
# = Example
#
#   $ puppetdoc > /tmp/type_reference.rst
#
# = Author
#
# Luke Kanies
#
# = Copyright
#
# Copyright (c) 2005-2007 Reductive Labs, LLC
# Licensed under the GNU Public License

require 'puppet'

class PuppetDoc
    include Puppet::Util::Docs
    @@sections = {}

    attr_accessor :page, :depth, :header, :title

    def self.[](name)
        @@sections[name]
    end

    def self.each
        @@sections.sort { |a, b| a[0].to_s <=> b[0].to_s }.each { |name, instance| yield instance }
    end

    def self.footer
        "\n\n----------------\n\n*This page autogenerated on %s*\n" % Time.now
    end

    def self.page(*sections)
        depth = 4
        # Use the minimum depth
        sections.each do |name|
            section = @@sections[name] or raise "Could not find section %s" % name
            depth = section.depth if section.depth < depth
        end
        text = ".. contents:: :depth: 2\n\n"
    end

    def self.pdf(text)
        puts "creating pdf"
        File.open("/tmp/puppetdoc.txt", "w") do |f|
            f.puts text
        end
        rst2latex = %x{which rst2latex}
        if $? != 0 or rst2latex =~ /no /
            rst2latex = %x{which rst2latex.py}
        end
        if $? != 0 or rst2latex =~ /no /
            raise "Could not find rst2latex"
        end
        rst2latex.chomp!
        cmd = %{#{rst2latex} /tmp/puppetdoc.txt > /tmp/puppetdoc.tex}
        output = %x{#{cmd}}
        unless $? == 0
            $stderr.puts "rst2latex failed"
            $stderr.puts output
            exit(1)
        end
        $stderr.puts output

        # Now convert to pdf
        puts "handling pdf"
        Dir.chdir("/tmp") do
            %x{texi2pdf puppetdoc.tex >/dev/null 2>/dev/null}
        end

        #if FileTest.exists?("/tmp/puppetdoc.pdf")
        #    FileUtils.mv("/tmp/puppetdoc.pdf", "/export/apache/docroots/reductivelabs.com/htdocs/downloads/puppet/reference.pdf")
        #end
    end

    def self.sections
        @@sections.keys.sort { |a,b| a.to_s <=> b.to_s }
    end

    HEADER_LEVELS = [nil, "=", "-", "+", "'", "~"]

    def h(name, level)
        return "%s\n%s\n" % [name, HEADER_LEVELS[level] * name.to_s.length]
    end

    def initialize(name, options = {}, &block)
        @name = name
        options.each do |option, value|
            send(option.to_s + "=", value)
        end

        meta_def(:generate, &block)

        @@sections[name] = self

        # Now handle the defaults
        @title ||= "%s Reference" % @name.to_s.capitalize
        @page ||= @title.gsub(/\s+/, '')
        @depth ||= 2
        @header ||= ""
    end

    # Indent every line in the chunk except those which begin with '..'.
    def indent(text, tab)
        return text.gsub(/(^|\A)/, tab).gsub(/^ +\.\./, "..")
    end

    def paramwrap(name, text, options = {})
        options[:level] ||= 5
        #str = "%s : " % name
        str = h(name, options[:level])
        if options[:namevar]
            str += "- **namevar**\n\n"
        end
        str += text
        #str += text.gsub(/\n/, "\n    ")

        str += "\n\n"
        return str
    end

    def output(withcontents = true)
        # First the header
        text = h(@title, 1)
        text += "\n\n*This page is autogenerated; any changes will get overwritten*\n\n"
        if withcontents
        text +=  ".. contents:: :depth: %s\n\n" % @depth
        end

        text += @header

        text += generate()

        if withcontents
            text += self.class.footer
        end

        return text
    end

    def text
        puts output
    end

    def trac
        File.open("/tmp/puppetdoc.txt", "w") do |f|

            f.puts "{{{
#!rst\n
#{self.output}
    }}}"
        end

        puts "Writing %s reference to trac as %s" % [@name, @page]
        cmd = %{sudo trac-admin /export/svn/trac/puppet wiki import %s /tmp/puppetdoc.txt} % self.page
        output = %x{#{cmd}}
        unless $? == 0
            $stderr.puts "trac-admin failed"
            $stderr.puts output
            exit(1)
        end
        unless output =~ /^\s+/
            $stderr.puts output
        end
    end

end

require 'puppet'
require 'puppet/network/handler'
require 'getoptlong'

result = GetoptLong.new(
	[ "--all",	"-a",			GetoptLong::NO_ARGUMENT ],
	[ "--mode",	"-m",			GetoptLong::REQUIRED_ARGUMENT ],
	[ "--section",	"-s",			GetoptLong::REQUIRED_ARGUMENT ],
	[ "--help",		"-h",			GetoptLong::NO_ARGUMENT ]
)

debug = false

$tab = "    "
options = {:sections => [], :mode => :text}

begin
    result.each { |opt,arg|
        case opt
            when "--all"
                options[:all] = true
            when "--mode"
                case arg
                when "text", "pdf", "trac":
                    options[:mode] = arg.intern
                else
                    raise "Invalid output mode %s" % arg
                end
            when "--section"
                options[:sections] << arg.intern
            when "--help"
                if Puppet.features.usage?
                    RDoc::usage && exit
                else
                    puts "No help available unless you have RDoc::usage installed"
                    exit
                end
        end
    }
rescue GetoptLong::InvalidOption => detail
    $stderr.puts "Try '#{$0} --help'"
    exit(1)
end

if options[:sections].empty?
    options[:sections] << :type
end

config = PuppetDoc.new :configuration, :depth => 1 do
    docs = {}
    Puppet.config.each do |name, object|
        docs[name] = object
    end

    str = ""
    docs.sort { |a, b|
        a[0].to_s <=> b[0].to_s
    }.each do |name, object|
        # Make each name an anchor
        header = name.to_s
        str += h(header, 3)

        # Print the doc string itself
        begin
            str += object.desc.gsub(/\n/, " ")
        rescue => detail
            puts detail.backtrace
            puts detail
        end
        str += "\n\n"

        # Now print the data about the item.
        str += ""
        val = object.default
        if name.to_s == "vardir"
            val = "/var/puppet"
        elsif name.to_s == "confdir"
            val = "/etc/puppet"
        end
        str += "- **Section**: %s\n" % object.section
        unless val == ""
            str += "- **Default**: %s\n" % val
        end
        str += "\n"
    end

    return str
end

config.header = "
Specifying Configuration Parameters
-----------------------------------

Every Puppet executable (with the exception of ``puppetdoc``) accepts all of
the arguments below, but not all of the arguments make sense for every executable.
Each argument has a section listed with it in parentheses; often, that section
will map to an executable (e.g., ``puppetd``), in which case it probably only
makes sense for that one executable.  If ``puppet`` is listed as the section,
it is most likely an option that is valid for everyone.

This will not always be the case.  I have tried to be as thorough as possible
in the descriptions of the arguments, so it should be obvious whether an
argument is appropriate or not.

These arguments can be supplied to the executables either as command-line 
arugments or in the configuration file for the appropriate executable.  For 
instance, the command-line invocation below would set the configuration directory
to ``/private/puppet``::
  
    $ puppetd --confdir=/private/puppet
  
Note that boolean options are turned on and off with a slightly different syntax
on the command line::

    $ puppetd --storeconfigs
      
    $ puppetd --no-storeconfigs

The invocations above will enable and disable, respectively, the storage of 
the client configuration.

As mentioned above, the configuration parameters can also be stored in a 
configuration file located in the configuration directory (`/etc/puppet` 
by default).  The file is named for the executable it is intended for, for
example `/etc/puppetd.conf` is the configuration file for `puppetd`.

The file, which follows INI-style formatting, should contain a bracketed
heading named for the executable, followed by pairs of parameters with their
values.  Here is an example of a very simple ``puppetd.conf`` file::

    [puppetd]
        confdir = /private/puppet
        storeconfigs = true
    
Note that boolean parameters must be explicitly specified as `true` or
`false` as seen above.

If you're starting out with a fresh configuration, you may wish to let
the executable generate a template configuration file for you by invoking
the executable in question with the `--genconfig` command.  The executable
will print a template configuration to standard output, which can be
redirected to a file like so::

    $ puppetd --genconfig > /etc/puppet/puppetd.conf
  
Note that this invocation will \"clobber\" (throw away) the contents of any
pre-existing `puppetd.conf` file, so make a backup of your present config
if it contains valuable information.
  
Like the `--genconfig` argument, the executables also accept a `--genmanifest`
argument, which will generate a manifest that can be used to manage all of 
Puppet's directories and files and prints it to standard output.  This can
likewise be redirected to a file::

    $ puppetd --genmanifest > /etc/puppet/manifests/site.pp

Puppet can also create user and group accounts for itself (one `puppet` group
and one `puppet` user) if it is invoked as `root` with the `--mkusers` argument::

    $ puppetd --mkusers
  
Signals
-------
The ``puppetd`` and ``puppetmasterd`` executables catch some signals for special 
handling.  Both daemons catch (``SIGHUP``), which forces the server to restart 
tself.  Predictably, interrupt and terminate (``SIGINT`` and ``SIGHUP``) will shut 
down the server, whether it be an instance of ``puppetd`` or ``puppetmasterd``.

Sending the ``SIGUSR1`` signal to an instance of ``puppetd`` will cause it to 
immediately begin a new configuration transaction with the server.  This 
signal has no effect on ``puppetmasterd``.

Configuration Parameter Reference
---------------------------------
Below is a list of all documented parameters.  Not all of them are valid with all
Puppet executables, but the executables will ignore any inappropriate values.

"

report = PuppetDoc.new :report do
    Puppet::Network::Handler.report.reportdocs
end
report.header = "
Puppet clients can report back to the server after each transaction.  This
transaction report is sent as a YAML dump of the
``Puppet::Transaction::Report`` class and includes every log message that was
generated during the transaction along with as many metrics as Puppet knows how
to collect.  See `ReportsAndReporting Reports and Reporting`:trac:
for more information on how to use reports.

Currently, clients default to not sending in reports; you can enable reporting
by setting the ``report`` parameter to true.

To use a report, set the ``reports`` parameter on the server; multiple
reports must be comma-separated.  You can also specify ``none`` to disable
reports entirely.

Puppet provides multiple report handlers that will process client reports:
"

function = PuppetDoc.new :function do
    Puppet::Parser::Functions.functiondocs
end
function.header = "
There are two types of functions in Puppet: Statements and rvalues.
Statements stand on their own and do not return arguments; they are used for
performing stand-alone work like importing.  Rvalues return values and can
only be used in a statement requiring a value, such as an assignment or a case
statement. 

Here are the functions available in Puppet:
"

type = PuppetDoc.new :type do
    types = {}
    Puppet::Type.loadall

    Puppet::Type.eachtype { |type|
        next if type.name == :puppet
        next if type.name == :component
        types[type.name] = type
    }

    str = %{
Metaparameters
--------------
Metaparameters are parameters that work with any element; they are part of the
Puppet framework itself rather than being part of the implementation of any
given instance.  Thus, any defined metaparameter can be used with any instance
in your manifest, including defined components.

Available Metaparameters
++++++++++++++++++++++++
}
    begin
        params = []
        Puppet::Type.eachmetaparam { |param|
            params << param
        }

        params.sort { |a,b|
            a.to_s <=> b.to_s
        }.each { |param|
            str += paramwrap(param.to_s, scrub(Puppet::Type.metaparamdoc(param)), :level => 4)
            #puts "<dt>" + param.to_s + "</dt>"
            #puts tab(1) + Puppet::Type.metaparamdoc(param).scrub.indent($tab)gsub(/\n\s*/,' ')
            #puts "<dd>"
            #puts indent(scrub(Puppet::Type.metaparamdoc(param)), $tab)
            #puts scrub(Puppet::Type.metaparamdoc(param))
            #puts "</dd>"

            #puts ""
        }
    rescue => detail
        puts detail.backtrace
        puts "incorrect metaparams: %s" % detail
        exit(1)
    end

    str += %{

Resource Types
--------------

- The *namevar* is the parameter used to uniquely identify a type instance.
  This is the parameter that gets assigned when a string is provided before
  the colon in a type declaration.  In general, only developers will need to
  worry about which parameter is the ``namevar``.
  
  In the following code::

      file { "/etc/passwd":
          owner => root,
          group => root,
          mode => 644
      }

  ``/etc/passwd`` is considered the title of the file object (used for things like
  dependency handling), and because ``path`` is the namevar for ``file``, that
  string is assigned to the ``path`` parameter.

- *Features* are abilities that some providers might not support.  You can use the list
  of supported features to determine how a given provider can be used.

- *Parameters* determine the specific configuration of the instance.  They either
  directly modify the system (internally, these are called properties) or they affect
  how the instance behaves (e.g., adding a search path for ``exec`` instances
  or determining recursion on ``file`` instances).

- *Providers* provide low-level functionality for a given resource type.  This is
  usually in the form of calling out to external commands.

  When required binaries are specified for providers, fully qualifed paths
  indicate that the binary must exist at that specific path and unqualified
  binaries indicate that Puppet will search for the binary using the shell
  path.

  Resource types define features they can use, and providers can be tested to see
  which features they provide.

    }

    types.sort { |a,b|
        a.to_s <=> b.to_s
    }.each { |name,type|

        str += "

----------------

"

        str += h(name, 3)
        str += scrub(type.doc) + "\n\n"

        # Handle the feature docs.
        if featuredocs = type.featuredocs
            str += h("Features", 4)
            str += featuredocs
        end

        docs = {}
        type.validproperties.sort { |a,b|
            a.to_s <=> b.to_s
        }.reject { |sname|
            property = type.propertybyname(sname)
            property.nodoc
        }.each { |sname|
            property = type.propertybyname(sname)

            unless property
                raise "Could not retrieve property %s on type %s" % [sname, type.name]
            end

            doc = nil
            unless doc = property.doc
                $stderr.puts "No docs for %s[%s]" % [type, sname]
                next
            end
            doc = doc.dup
            tmp = doc
            tmp = scrub(tmp)

            docs[sname]  = tmp
        }

        str += h("Parameters", 4) + "\n"
        type.parameters.sort { |a,b|
            a.to_s <=> b.to_s
        }.each { |name,param|
            #docs[name] = indent(scrub(type.paramdoc(name)), $tab)
            docs[name] = scrub(type.paramdoc(name))
        }

        docs.sort { |a, b|
            a[0].to_s <=> b[0].to_s
        }.each { |name, doc|
            namevar = type.namevar == name and name != :name
            str += paramwrap(name, doc, :namevar => namevar)
        }
        str += "\n"
    }

    str
end

if options[:all]
    options[:sections] = PuppetDoc.sections
end

case options[:mode]
when :trac
    options[:sections].each do |name|
        section = PuppetDoc[name] or raise "Could not find section %s" % name
        unless options[:mode] == :pdf
            section.trac
        end
    end
else
    text = ".. contents:: :depth: 1\n\n"
    options[:sections].sort { |a,b| a.to_s <=> b.to_s }.each do |name|
        section = PuppetDoc[name]

        # Add the per-section text, but with no ToC
        text += section.output(false)
    end

    text += PuppetDoc.footer

    # Replace the trac links, since they're invalid everywhere else
    text.gsub!(/`\w+\s+([^`]+)`:trac:/) { |m| $1 }

    if options[:mode] == :pdf
        PuppetDoc.pdf(text)
    else
        puts text
    end
end


# $Id: puppetdoc 2412 2007-04-24 20:47:08Z luke $
