|
#!/usr/bin/env ruby |
|
# |
|
# CFengine command-line test utility. |
|
# |
|
# Diego Zamboni, December 27th, 2011 |
|
# |
|
# Run as "cf-cmd help" to see usage information. |
|
|
|
require 'readline' |
|
require 'tmpdir' |
|
|
|
###################################################################### |
|
# ATTENTION: customize this if necessary |
|
|
|
# Where can I find a copy of cfengine_stdlib.cf |
|
# $stdlib_loc = "#{ENV['HOME']}/CFEngine/src/copbl/cfengine_stdlib.cf"; |
|
$stdlib_loc = "/var/cfengine/inputs/cfengine_stdlib.cf"; |
|
|
|
###################################################################### |
|
|
|
class CfCmd |
|
|
|
# Constructor |
|
def initialize(stdlib, initialstate = 'reports:') |
|
@version = "1.0"; |
|
# Initialize base parameters |
|
@stdlib_loc = stdlib |
|
@initialstate = initialstate |
|
@mode = @initialstate |
|
|
|
# Valid promise types and commands |
|
@modes = %w(vars: classes: commands: databases: environments: files: interfaces: |
|
methods: outputs: packages: processes: services: storage: reports:) |
|
|
|
@commands = self.methods(/^cmd_/).grep(/^cmd_/).map { |c| c.to_s.gsub(/^cmd_/, "") } |
|
|
|
# Initialize data structures |
|
@lines = emptypolicy |
|
# By default, initialize the reports: section with a cfengine:: class, so that things are always printed. |
|
# Issue a "clear" command to eliminate it. |
|
@lines['reports:'] = "cfengine::\n" |
|
end |
|
|
|
#---------------------------------------------------------------------- |
|
# Utility functions |
|
|
|
# Policy template to use |
|
def policytemplate(bundlebody) |
|
<<EOF |
|
body common control |
|
{ |
|
inputs => { "#{ @stdlib_loc }" }; |
|
bundlesequence => { "test" }; |
|
} |
|
|
|
bundle agent test |
|
{ |
|
#{bundlebody}} |
|
EOF |
|
end |
|
|
|
# Return current policy as a string |
|
def buildpolicy |
|
bundlebody = "" |
|
@lines.keys.each { |k| |
|
bundlebody += "#{k}\n#{@lines[k]}" unless @lines[k].empty? |
|
} |
|
return policytemplate(bundlebody) |
|
end |
|
|
|
# Empty all policy sections |
|
def emptypolicy |
|
res = {} |
|
@modes.each { |m| |
|
res[m] = "" |
|
} |
|
return res |
|
end |
|
|
|
# Find the first command (if any) that start with the given string |
|
def findcmd(cmd) |
|
cmds = @commands.grep(/^#{Regexp.quote(cmd)}/) |
|
return cmds.empty? ? nil : cmds[0] |
|
end |
|
|
|
# Word wrap, with an optional prefix string. Based on http://snipplr.com/view.php?codeview&id=1081 |
|
def wrap_text(txt, col = 80, prefix="") |
|
ncol = col - prefix.length |
|
txt.gsub(/(.{1,#{ncol}})( +|$\n?)|(.{1,#{ncol}})/, "#{prefix}\\1\\3\n") |
|
end |
|
|
|
# Interpret and process a line |
|
def interpret(line) |
|
(cmd, args) = line.split(nil, 2) |
|
if @modes.include?(cmd) |
|
$stderr.puts "-> Switching to #{cmd} promise type." |
|
@mode = cmd |
|
elsif cmdname = findcmd(cmd) |
|
eval "cmd_#{cmdname}(args)" |
|
else |
|
@lines[@mode] = @lines[@mode] + " #{line}\n" |
|
end |
|
end |
|
|
|
#---------------------------------------------------------------------- |
|
# Command definitions |
|
|
|
# List current policy |
|
def cmd_list(args=nil) |
|
puts "#{buildpolicy}"; |
|
end |
|
|
|
# Clear current policy |
|
def cmd_clear(args=nil) |
|
@lines = emptypolicy |
|
end |
|
|
|
# Execute current policy |
|
def cmd_go(args=nil) |
|
fname = "./test.cf" |
|
Dir.mktmpdir('cf-') { |dir| |
|
Dir.chdir(dir) |
|
File.open(fname, "w") { |f| f.puts(buildpolicy) } |
|
cmd = "cf-agent #{args.to_s} -KI -f #{fname}" |
|
$stderr.puts "-> Running policy with '#{cmd}'" |
|
system(cmd) |
|
} |
|
end |
|
|
|
# Synonymous for go |
|
def cmd_run(args=nil) |
|
cmd_go(args) |
|
end |
|
|
|
# Save to a file |
|
def cmd_save(args=nil) |
|
if args.nil? |
|
$stderr.puts "-> Error: need a filename to which to save the script." |
|
else |
|
File.open(args, "w") { |f| f.puts(buildpolicy) } |
|
$stderr.puts "-> Saved script to #{args}." |
|
end |
|
end |
|
|
|
# Print help |
|
def cmd_help(args=nil) |
|
cmdname=File.basename($0) |
|
puts <<EOM |
|
#{cmdname} v#{@version} - Diego Zamboni <diego@zzamboni.org> |
|
|
|
#{cmdname} is a tool that allows you to run small CFEngine snippets quickly, |
|
by automatically wrapping them around a standard "test" bundle. |
|
|
|
The CFEngine Standard Library is automatically included. |
|
|
|
The following inputs are understood by this tool: |
|
|
|
help Print this message |
|
list Print current policy |
|
clear Clear current policy |
|
go|run Execute current policy using cf-agent |
|
type: Switch to the given promise type |
|
#{wrap_text("(" + @modes.sort.join(', ') +")", 80, ' ').chomp} |
|
The current promise type is shown in the prompt. |
|
|
|
All other lines are added literally to the current promise type. |
|
|
|
Commands can be abbreviated to any part of their name (for example, |
|
"r" or "ru" for "run"). |
|
|
|
You can add lines to any of the standard promise types inside the test |
|
bundle by switching to the appropriate promise type first. |
|
|
|
The default promise type is "reports:", to make it easier to quickly print |
|
the value of expressions. |
|
|
|
You can give the inputs also on the command line, they are interpreted |
|
in exactly the same way (make sure to quote things correctly). |
|
|
|
Examples: |
|
#{cmdname} '"Flavor: $(sys.flavor)";' list run |
|
#{cmdname} '"var1 = $(var1)";' vars: '"var1" string => "test";' l r |
|
#{cmdname} h |
|
EOM |
|
end |
|
|
|
#---------------------------------------------------------------------- |
|
# Interactive subroutine |
|
|
|
def run |
|
# Trap Ctrl-C and make it do a clean exit |
|
stty_save = `stty -g`.chomp |
|
trap('INT') { system('stty', stty_save); exit } |
|
|
|
comp = proc { |s| (@commands + @modes).grep( /^#{Regexp.escape(s)}/ ) } |
|
|
|
Readline.completion_append_character = " " |
|
Readline.completion_proc = comp |
|
|
|
while line = Readline.readline("#{@mode} > ", true) |
|
interpret(line) |
|
end |
|
end |
|
|
|
end # class CfCmd |
|
|
|
|
|
# Do something if invoked directly (instead of included as a module) |
|
if __FILE__ == $0 |
|
cfcmd = CfCmd.new($stdlib_loc) |
|
if !ARGV.empty? |
|
# If any command-line arguments are given, interpret them as commands |
|
ARGV.each { |cmd| |
|
cfcmd.interpret(cmd) |
|
} |
|
else |
|
# Otherwise start interactive mode |
|
cfcmd.run |
|
end |
|
end |