packages feed

language-puppet-0.10.0: progs/PuppetResources.hs

{-# LANGUAGE LambdaCase #-}
{-|

Horrible and hackish ... but damn useful. This can be used as a standalone
executable or a ghci script for interactive usage. It comes with a sample Puppet
site (that I hope will gets more realistic later).

When given a single argument, it will try to parse the given file, and will
print the parsed values :

> $ puppetresources samplesite/manifests/site.pp
> node test.nod {
>     apt::builddep { Interpolable [Literal "glusterfs-server"]:
>     ;
>     }
>     ...

With two arguments, it will try to compute the catalog for a given node. The
first argument must be the path to the Puppet directory and the second the
name of the node. Note that regexp node names do not work yet.

> $ puppetresources samplesite test.nod
> The defined() function is not implemented for resource references. Returning true at "samplesite/modules/apt/manifests/ppa.pp" (line 20, column 3)
> The defined() function is not implemented for resource references. Returning true at "samplesite/modules/apt/manifests/key.pp" (line 38, column 7)
> The defined() function is not implemented for resource references. Returning true at "samplesite/modules/apt/manifests/key.pp" (line 42, column 7)
> anchor {
>     "apt::builddep::glusterfs-server": #"samplesite/modules/apt/manifests/builddep.pp" (line 12, column 12)
>         name => "apt::builddep::glusterfs-server";
>     "apt::key/Add key: 55BE302B from Apt::Source debian_unstable": #"samplesite/modules/apt/manifests/key.pp" (line 32, column 16)
>         name => "apt::key/Add key: 55BE302B from Apt::Source debian_unstable";
> ...

When adding a file name as the third argument to the previous invocation, it
will display the value of the /content/ attribute of the named /file/ resource.

> $ puppetresources samplesite test.nod karmic.pref
> The defined() function is not implemented for resource references. Returning true at "samplesite/modules/apt/manifests/ppa.pp" (line 20, column 3)
> The defined() function is not implemented for resource references. Returning true at "samplesite/modules/apt/manifests/key.pp" (line 38, column 7)
> The defined() function is not implemented for resource references. Returning true at "samplesite/modules/apt/manifests/key.pp" (line 42, column 7)
> # karmic
> Package: *
> Pin: release a=karmic
> Pin-Priority: 700

You can also just use a resource name :

> $ puppetresources samplesite test.nod 'exec[apt_update]'
> exec {
>   "apt_update": #"samplesite/modules/apt/manifests/update.pp" (line 4, column 10)
>       command     => "/usr/bin/apt-get update",
>       logoutput   => "false",
>       refreshonly => "true",
>       returns     => 0,
>       timeout     => 300,
>       tries       => 1,
>       try_sleep   => 0;
> }

With GHCI there are tons of things you can do. First initialize it

>>> queryfunc <- initializedaemon "./samplesite/"

You can now compute catalogs for various nodes.

>>> c1 <- queryfunc "test.nod"
The defined() function is not implemented for resource references. Returning true at "./samplesite//modules/apt/manifests/ppa.pp" (line 20, column 3)
The defined() function is not implemented for resource references. Returning true at "./samplesite//modules/apt/manifests/key.pp" (line 38, column 7)
The defined() function is not implemented for resource references. Returning true at "./samplesite//modules/apt/manifests/key.pp" (line 42, column 7)
>>> c2 <- queryfunc "test2.nod"
The defined() function is not implemented for resource references. Returning true at "./samplesite//modules/apt/manifests/ppa.pp" (line 20, column 3)
The defined() function is not implemented for resource references. Returning true at "./samplesite//modules/apt/manifests/key.pp" (line 38, column 7)
The defined() function is not implemented for resource references. Returning true at "./samplesite//modules/apt/manifests/key.pp" (line 42, column 7)

And you can check what the difference is between catalogs.

>>> diff c1 c2
file[karmic-updates.pref] {
# content
    + Pin-Priority: 750
    - Pin-Priority: 700
}

You also can manipulate catalogs.

>>> Map.size c1
25
>>> mapM_ print $ Map.toList $ Map.map (length . lines . (\x -> case x of (PString n) -> n) .fromJust . Map.lookup "content" . rrattributes) $ Map.filter (Map.member "content" . rrattributes) c1
(("file","debian_unstable.list"),3)
(("file","debian_unstable.pref"),4)
(("file","karmic-security.pref"),4)
(("file","karmic-updates.pref"),4)
(("file","karmic.pref"),4)

A typical usage of this tool is to compute a reference catalog, and then check the differences as you alter it. This can be done this way :

>>> reference <- queryfunc "test.nod"

And then run the following command every time you need to verify your changes are correct :

>>> queryfunc "test.nod" >>= diff reference

-}
module Main where

import System.IO
import qualified Data.HashMap.Strict as HM
import qualified System.Log.Logger as LOG
import qualified Data.ByteString.Lazy.Char8 as BSL
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Monoid hiding (First)
import qualified Text.Parsec as P
import qualified Data.Vector as V
import qualified Data.Either.Strict as S
import Options.Applicative as O
import Control.Monad
import Text.Regex.PCRE.String
import Data.Text.Strict.Lens
-- import Data.Aeson

import Facter

import Puppet.PP hiding ((<$>))
import Puppet.Preferences
import Puppet.Daemon
import Puppet.Interpreter.Types
import Puppet.Parser.Types
import Puppet.Parser
import Puppet.Parser.PrettyPrinter()
import Puppet.Interpreter.PrettyPrinter()
import PuppetDB.Remote
import PuppetDB.Dummy
import PuppetDB.TestDB
import PuppetDB.Common
import Puppet.Testing hiding ((<$>))

tshow :: Show a => a -> T.Text
tshow = T.pack . show

{-| Does all the work of initializing a daemon for querying.
Returns the final catalog when given a node name. Note that this is pretty
hackish as it will generate facts from the local computer !
-}

initializedaemonWithPuppet :: LOG.Priority -> PuppetDBAPI -> FilePath -> IO (T.Text -> IO (FinalCatalog, EdgeMap, FinalCatalog))
initializedaemonWithPuppet prio pdbapi puppetdir = do
    LOG.updateGlobalLogger "Puppet.Daemon" (LOG.setLevel prio)
    q <- fmap (prefPDB .~ pdbapi) (genPreferences puppetdir) >>= initDaemon
    let f ndename = puppetDBFacts ndename pdbapi
            >>= _dGetCatalog q ndename
            >>= \case
                    S.Left rr -> putDoc rr >> putStrLn "" >> error "error!"
                    S.Right x -> return x
    return f

parseFile :: FilePath -> IO (Either P.ParseError (V.Vector Statement))
parseFile fp = T.readFile fp >>= P.runParserT puppetParser () fp

printContent :: T.Text -> FinalCatalog -> IO ()
printContent filename catalog =
        case HM.lookup (RIdentifier "file" filename) catalog of
            Nothing -> error "File not found"
            Just r  -> case HM.lookup "content" (_rattributes r) of
                           Nothing -> error "This file has no content"
                           Just (PString c)  -> T.putStrLn c
                           Just x -> print x

data CommandLine = CommandLine { _pdb          :: Maybe String
                               , _showjson     :: Bool
                               , _showContent  :: Bool
                               , _resourceType :: Maybe T.Text
                               , _resourceName :: Maybe T.Text
                               , _puppetdir    :: FilePath
                               , _nodename     :: Maybe String
                               , _pdbfile      :: Maybe FilePath
                               , _loglevel     :: LOG.Priority
                               } deriving Show

cmdlineParser :: Parser CommandLine
cmdlineParser = CommandLine <$> optional remotepdb <*> sj <*> sc <*> optional (T.pack <$> rt) <*> optional (T.pack <$> rn) <*> pdir <*> optional nn <*> optional pdbfile <*> priority
    where
        sc = switch (  long "showcontent"
                    <> short 'c'
                    <> help "When specifying a file resource, only output its content (useful for testing templates)")
        sj = switch (  long "JSON"
                    <> short 'j'
                    <> help "Shows the output as a JSON document (useful for full catalog views)")
        remotepdb = strOption (  long "pdburl"
                              <> help "URL of the puppetdb (ie. http://localhost:8080/).")
        rt = strOption (  long "type"
                       <> short 't'
                       <> help "Filter the output by resource type (accepts a regular expression, ie '^file$')")
        rn = strOption (  long "name"
                       <> short 'n'
                       <> help "Filter the output by resource name (accepts a regular expression)")
        pdir = strOption (  long "puppetdir"
                         <> short 'p'
                         <> help "Puppet directory")
        nn   = strOption (  long "node"
                         <> short 'o'
                         <> help "Node name")
        pdbfile = strOption (  long "pdbfile"
                            <> help "Path to the testing PuppetDB file.")
        priority = option (  long "loglevel"
                          <> short 'v'
                          <> help "Values are : DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY"
                          <> value LOG.WARNING
                          )
run :: CommandLine -> IO ()
run (CommandLine _ _ _ _ _ f Nothing _ _) = parseFile f >>= \case
            Left rr -> error ("parse error:" ++ show rr)
            Right s -> putDoc (vcat (map pretty (V.toList s)))
run (CommandLine puppeturl showjson showcontent mrt mrn puppetdir (Just ndename) mpdbf prio) = do
    let checkError r (S.Left rr) = error (show (red r <> ":" <+> rr))
        checkError _ (S.Right x) = return x
        tnodename = T.pack ndename
    pdbapi <- case (puppeturl, mpdbf) of
                  (Nothing, Nothing) -> return dummyPuppetDB
                  (Just _, Just _)   -> error "You must choose between a testing PuppetDB and a remote one"
                  (Just url, _)      -> pdbConnect (T.pack url) >>= checkError "Error when connecting to the remote PuppetDB"
                  (_, Just file)     -> loadTestDB file >>= checkError "Error when initializing the PuppetDB API"
    queryfunc <- initializedaemonWithPuppet prio pdbapi puppetdir
    printFunc <- hIsTerminalDevice stdout >>= \isterm -> return $ \x ->
        if isterm
            then putDoc x >> putStrLn ""
            else displayIO stdout (renderCompact x) >> putStrLn ""
    (rawcatalog,m,rawexported) <- queryfunc tnodename
    void $ replaceCatalog pdbapi (generateWireCatalog tnodename (rawcatalog <> rawexported) m)
    void $ commitDB pdbapi
    let cmpMatch Nothing _ curcat = return curcat
        cmpMatch (Just rg) lns curcat = compile compBlank execBlank (T.unpack rg) >>= \case
            Left rr -> error ("Error compiling regexp 're': "  ++ show rr)
            Right rec -> fmap HM.fromList $ filterM (filterResource lns rec) (HM.toList curcat)
        filterResource lns rec v = execute rec (v ^. lns) >>= \case
                                        Left rr -> error ("Error when applying regexp: " ++ show rr)
                                        Right Nothing -> return False
                                        _ -> return True
        filterCatalog = cmpMatch mrt (_1 . itype . unpacked) >=> cmpMatch mrn (_1 . iname . unpacked)
    catalog  <- filterCatalog rawcatalog
    exported <- filterCatalog rawexported
    case (showcontent, showjson)  of
        (_, True) -> BSL.putStrLn ("TODO JSON MODE")
        (True, _) -> do
            unless (mrt == Just "file" || mrt == Nothing) (error $ "Show content only works for file, not for " ++ show mrt)
            case mrn of
                Just f -> printContent f catalog
                Nothing -> error "You should supply a resource name when using showcontent"
        _ -> do
            void $ testCatalog tnodename puppetdir rawcatalog basicTest
            printFunc (pretty (HM.elems catalog))
            unless (HM.null exported) $ do
                printFunc (mempty <+> dullyellow "Exported:" <+> mempty)
                printFunc (pretty (HM.elems exported))

main :: IO ()
main = execParser pinfo >>= run
    where
        pinfo :: ParserInfo CommandLine
        pinfo = ParserInfo (helper <*> cmdlineParser) True "A useful program for parsing puppet files, generating and inspecting catalogs" "puppetresources - a useful utility for dealing with Puppet" "" 3