Skip to content
This repository has been archived by the owner on Nov 28, 2018. It is now read-only.

Prior to this, the filemgr did not allow listing of an entire directory. #49

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions agent/filemgr/agent/filemgr.ddl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Copy link
Contributor

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:

display :always

like in the status action

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
138 changes: 87 additions & 51 deletions agent/filemgr/agent/filemgr.rb
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
Expand All @@ -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.
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

The 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

72 changes: 68 additions & 4 deletions agent/filemgr/application/filemgr.rb
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"

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand All @@ -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

Expand Down