-
Notifications
You must be signed in to change notification settings - Fork 97
Prior to this, the filemgr did not allow listing of an entire directory. #49
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,18 +2,17 @@ metadata :name => "filemgr", | |
:description => "File Manager", | ||
:author => "Mike Pountney <[email protected]>", | ||
:license => "Apache 2", | ||
:version => "0.3", | ||
:version => "1.1", | ||
:url => "http://www.puppetlabs.com/mcollective", | ||
:timeout => 5 | ||
|
||
|
||
action "touch", :description => "Creates an empty file or touch it's timestamp" do | ||
input :file, | ||
:prompt => "File", | ||
:description => "File to touch", | ||
:type => :string, | ||
:validation => '^.+$', | ||
:optional => true, | ||
:optional => false, | ||
:maxlength => 256 | ||
end | ||
|
||
|
@@ -23,7 +22,7 @@ action "remove", :description => "Removes a file" do | |
:description => "File to remove", | ||
:type => :string, | ||
:validation => '^.+$', | ||
:optional => true, | ||
:optional => false, | ||
:maxlength => 256 | ||
end | ||
|
||
|
@@ -35,7 +34,7 @@ action "status", :description => "Basic information about a file" do | |
:description => "File to get information for", | ||
:type => :string, | ||
:validation => '^.+$', | ||
:optional => true, | ||
:optional => false, | ||
:maxlength => 256 | ||
|
||
output :name, | ||
|
@@ -47,7 +46,7 @@ action "status", :description => "Basic information about a file" do | |
:display_as => "Status" | ||
|
||
output :present, | ||
:description => "Indicates if the file exist using 0 or 1", | ||
:description => "Indicates if the file exists using 0 or 1", | ||
:display_as => "Present" | ||
|
||
output :size, | ||
|
@@ -94,7 +93,30 @@ action "status", :description => "Basic information about a file" do | |
:description => "File group", | ||
:display_as => "Group" | ||
|
||
output :uid_name, | ||
:description => "File owner user name", | ||
:display_as => "Owner name" | ||
|
||
output :gid_name, | ||
:description => "File group name", | ||
:display_as => "Group name" | ||
|
||
output :type, | ||
:description => "File type", | ||
:display_as => "Type" | ||
end | ||
|
||
action "list", :description => "Lists a directory's contents" do | ||
input :dir, | ||
:prompt => "Directory", | ||
:description => "Directory to list", | ||
:type => :string, | ||
:validation => '^.+$', | ||
:optional => false, | ||
:maxlength => 256 | ||
|
||
output :files, | ||
:description => "Hash of files in the directory with file stats", | ||
:display_as => "File details", | ||
:default => {} | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
require 'fileutils' | ||
require 'digest/md5' | ||
require 'etc' | ||
|
||
module MCollective | ||
module Agent | ||
|
@@ -14,10 +15,9 @@ class Filemgr<RPC::Agent | |
:description => "File Manager", | ||
:author => "Mike Pountney <[email protected]>", | ||
:license => "Apache 2", | ||
:version => "1.0", | ||
:version => "1.1", | ||
:url => "http://www.puppetlabs.com/mcollective", | ||
:timeout => 5 | ||
|
||
# Basic file touch action - create (empty) file if it doesn't exist, | ||
# update last mod time otherwise. | ||
# useful for checking if mcollective is operational, via NRPE or similar. | ||
|
@@ -34,62 +34,80 @@ class Filemgr<RPC::Agent | |
action "status" do | ||
status | ||
end | ||
|
||
# Basic directory listing with file status | ||
action "list" do | ||
list | ||
end | ||
|
||
private | ||
def get_filename | ||
request[:file] || config.pluginconf["filemgr.touch_file"] || "/var/run/mcollective.plugin.filemgr.touch" | ||
end | ||
|
||
def status | ||
file = get_filename | ||
reply[:name] = file | ||
reply[:output] = "not present" | ||
reply[:type] = "unknown" | ||
reply[:mode] = "0000" | ||
reply[:present] = 0 | ||
reply[:size] = 0 | ||
reply[:mtime] = 0 | ||
reply[:ctime] = 0 | ||
reply[:atime] = 0 | ||
reply[:mtime_seconds] = 0 | ||
reply[:ctime_seconds] = 0 | ||
reply[:atime_seconds] = 0 | ||
reply[:md5] = 0 | ||
reply[:uid] = 0 | ||
reply[:gid] = 0 | ||
|
||
|
||
if File.exists?(file) | ||
logger.debug("Asked for status of '#{file}' - it is present") | ||
reply[:output] = "present" | ||
reply[:present] = 1 | ||
|
||
if File.symlink?(file) | ||
stat = File.lstat(file) | ||
else | ||
stat = File.stat(file) | ||
end | ||
private | ||
def get_file_status(file = get_filename) | ||
results = {} | ||
results[:name] = file | ||
results[:output] = "not present" | ||
results[:type] = "unknown" | ||
results[:mode] = "0000" | ||
results[:present] = 0 | ||
results[:size] = 0 | ||
results[:mtime] = 0 | ||
results[:ctime] = 0 | ||
results[:atime] = 0 | ||
results[:mtime_seconds] = 0 | ||
results[:ctime_seconds] = 0 | ||
results[:atime_seconds] = 0 | ||
results[:md5] = 0 | ||
results[:uid] = 0 | ||
results[:gid] = 0 | ||
|
||
if !File.exists?(file) | ||
logger.debug("Asked for status of '#{file}' - it is not present") | ||
reply.fail!("#{file} does not exist") | ||
elsif !File.readable?(file) | ||
logger.debug("Asked for status of '#{file}' - permission denied") | ||
reply.fail!("#{file} - permission denied") | ||
end | ||
|
||
[:size, :mtime, :ctime, :atime, :uid, :gid].each do |item| | ||
reply[item] = stat.send(item) | ||
end | ||
logger.debug("Asked for status of '#{file}' - it is present") | ||
results[:output] = "present" | ||
results[:present] = 1 | ||
|
||
[:mtime, :ctime, :atime].each do |item| | ||
reply["#{item}_seconds".to_sym] = stat.send(item).to_i | ||
end | ||
if File.symlink?(file) | ||
stat = File.lstat(file) | ||
else | ||
stat = File.stat(file) | ||
end | ||
|
||
[:size, :mtime, :ctime, :atime, :uid, :gid].each do |item| | ||
results[item] = stat.send(item) | ||
end | ||
|
||
reply[:mode] = "%o" % [stat.mode] | ||
reply[:md5] = Digest::MD5.hexdigest(File.read(file)) if stat.file? | ||
[:mtime, :ctime, :atime].each do |item| | ||
results["#{item}_seconds".to_sym] = stat.send(item).to_i | ||
end | ||
|
||
reply[:type] = "directory" if stat.directory? | ||
reply[:type] = "file" if stat.file? | ||
reply[:type] = "symlink" if stat.symlink? | ||
reply[:type] = "socket" if stat.socket? | ||
reply[:type] = "chardev" if stat.chardev? | ||
reply[:type] = "blockdev" if stat.blockdev? | ||
else | ||
logger.debug("Asked for status of '#{file}' - it is not present") | ||
reply.fail! "#{file} does not exist" | ||
results[:uid_name] = Etc.getpwuid(stat.send(:uid)).name | ||
results[:gid_name] = Etc.getgrgid(stat.send(:gid)).name | ||
|
||
results[:mode] = "%o" % [stat.mode] | ||
results[:md5] = Digest::MD5.hexdigest(File.read(file)) if stat.file? | ||
results[:type] = "directory" if stat.directory? | ||
results[:type] = "file" if stat.file? | ||
results[:type] = "symlink" if stat.symlink? | ||
results[:type] = "socket" if stat.socket? | ||
results[:type] = "chardev" if stat.chardev? | ||
results[:type] = "blockdev" if stat.blockdev? | ||
|
||
return results | ||
end | ||
|
||
def status | ||
get_file_status.each do |key, val| | ||
reply[key.to_sym] = val | ||
end | ||
end | ||
|
||
|
@@ -106,7 +124,7 @@ def remove | |
reply.statusmsg = "OK" | ||
rescue | ||
logger.warn("Could not remove file '#{file}'") | ||
reply.fail! "Could not remove file '#{file}'" | ||
reply.fail!("Could not remove file '#{file}'") | ||
end | ||
end | ||
|
||
|
@@ -117,10 +135,28 @@ def touch | |
logger.debug("Touched file '#{file}'") | ||
rescue | ||
logger.warn("Could not touch file '#{file}'") | ||
reply.fail! "Could not touch file '#{file}'" | ||
reply.fail!("Could not touch file '#{file}'") | ||
end | ||
end | ||
|
||
def list | ||
dir = request[:dir] | ||
directory = {} | ||
if File.directory?(dir) | ||
begin | ||
Dir.foreach(dir) do |entry| | ||
directory[entry.to_sym] = get_file_status(File.join(dir, entry)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as this uses get_file_status() on each file and get_file_status fails the whole request if it cant get a file it means that trying to get the contents of any dir that is not readable fails. Usually this is fine cos mcollective runs as root but if selinux for example denies access to the process you will get no results rather than say partial results. % mco rpc filemgr list dir=/etc devco.net-0 Request Aborted Could not list directory '/etc': /etc/libaudit.conf - permission denied File details: {} Also not sure if converting the filename to a symbol is needed here, eventually mcollective will switch to JSON and symbols in data like this wont work - safest to just keep those as strings now in the case where its a deep nested structure |
||
end | ||
reply[:directory] = directory | ||
rescue => e | ||
logger.warn("Could not list directory '%s': %s" % [dir, e]) | ||
reply.fail!("Could not list directory '%s': %s" % [dir, e]) | ||
end | ||
else | ||
logger.debug("Asked to list directory '#{dir}', but it does not exist") | ||
reply.fail!("#{dir} does not exist") | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,56 @@ | ||
class MCollective::Application::Filemgr<MCollective::Application | ||
|
||
description "Generic File Manager Client" | ||
usage "Usage: mc-filemgr [--file FILE] [touch|remove|status]" | ||
|
||
usage "Usage: mc-filemgr [--dir DIR] list" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should change this to 'mco filemgr' i guess |
||
option :file, | ||
:description => "File to manage", | ||
:arguments => ["--file FILE", "-f FILE"], | ||
:required => true | ||
:arguments => ["--file FILE", "-f FILE"] | ||
|
||
option :details, | ||
:description => "Show full file details", | ||
:arguments => ["--details", "-d"], | ||
:type => :bool | ||
|
||
option :directory, | ||
:description => "Directory to list", | ||
:arguments => "--dir DIR" | ||
|
||
def post_option_parser(configuration) | ||
configuration[:command] = ARGV.shift if ARGV.size > 0 | ||
end | ||
|
||
def validate_configuration(configuration) | ||
configuration[:command] = "touch" unless configuration.include?(:command) | ||
if ['touch','remove','status'].include?(configuration[:command]) && !configuration[:file] | ||
raise "Action requires the file option" | ||
elsif configuration[:command] == "list" && (!configuration[:directory] && !configuration[:file]) | ||
raise "Action requires the directory option" | ||
end | ||
if configuration[:directory] && configuration[:file] | ||
raise "Option must be file or directory, not both." | ||
end | ||
end | ||
|
||
def size_to_human(size) | ||
factor = 1024 | ||
case | ||
when size >= factor**4 | ||
# TB | ||
return "%.1fT" % (size/(factor**4)) | ||
when size >= factor**3 | ||
# GB | ||
return "%.1fG" % (size/(factor**3)) | ||
when size >= factor**2 | ||
# MB | ||
return "%.1fM" % (size/(factor**2)) | ||
when size >= factor | ||
# KB | ||
return "%.1fK" % (size/factor) | ||
else | ||
return size | ||
end | ||
end | ||
|
||
def main | ||
|
@@ -39,9 +72,40 @@ def main | |
end | ||
end | ||
|
||
when "list" | ||
# Allow the use of the file flag in place of directory | ||
configuration[:directory] = configuration[:file] unless configuration[:directory] | ||
mc.list(:dir => configuration[:directory]).each do |resp| | ||
if resp[:statuscode] == 0 | ||
printf("%-40s:\n", resp[:sender]) | ||
if configuration[:details] | ||
files = resp[:data][:directory] | ||
uid_max = files.values.max { |a, b| a[:uid_name].length <=> b[:uid_name].length }[:uid_name].length | ||
gid_max = files.values.max { |a, b| a[:gid_name].length <=> b[:gid_name].length }[:gid_name].length | ||
files.each do |key, val| | ||
val[:size] = size_to_human(val[:size]) | ||
end | ||
size_max = files.values.max { |a, b| a[:size].size <=> b[:size].size}[:size].size | ||
files.sort_by { |key, val| key }.each do |key,val| | ||
uid = "%-#{uid_max}s" % val[:uid_name] | ||
gid = "%-#{gid_max}s" % val[:gid_name] | ||
size = "%-#{size_max}s" % val[:size] | ||
print "%5s %s %s %s %s %s\n" % ["", uid, gid, size, val[:mtime], key] | ||
end | ||
else | ||
files = resp[:data][:directory].keys.sort | ||
files.each do |key,val| | ||
printf("%5s%s\n", "", key) | ||
end | ||
end | ||
else | ||
printf("%-40s: Error %s\n", resp[:sender], resp[:statuscode]) | ||
end | ||
end | ||
|
||
else | ||
mc.disconnect | ||
puts "Valid commands are 'touch', 'status', and 'remove'" | ||
puts "Valid commands are 'touch', 'remove', 'status' and 'list'" | ||
exit 1 | ||
end | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think people will always want to see hte list when they use this agent from 'mco rpc filemgr...' so we should have here:
like in the status action