|
|
@@ -0,0 +1,557 @@ |
|
|
''' |
|
|
Manage information about files on the minion, set/read user, group, and mode |
|
|
data |
|
|
''' |
|
|
|
|
|
# TODO: We should add the capability to do u+r type operations here |
|
|
# some time in the future |
|
|
|
|
|
import os |
|
|
import time |
|
|
import hashlib |
|
|
import win32api |
|
|
import win32con |
|
|
import win32security |
|
|
|
|
|
#import salt.utils.find |
|
|
from salt.exceptions import SaltInvocationError |
|
|
|
|
|
def __virtual__(): |
|
|
''' |
|
|
Only works on Windows systems |
|
|
''' |
|
|
if __grains__['os'] == 'Windows': |
|
|
return 'file' |
|
|
return False |
|
|
|
|
|
__outputter__ = { |
|
|
'touch': 'txt', |
|
|
'append': 'txt', |
|
|
} |
|
|
|
|
|
|
|
|
def gid_to_group(gid): |
|
|
''' |
|
|
Convert the group id to the group name on this system |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.gid_to_group 0 |
|
|
''' |
|
|
sid = win32security.GetBinarySid(gid) |
|
|
name, domain, type = win32security.LookupAccountSid (None, sid) |
|
|
return name |
|
|
|
|
|
|
|
|
def group_to_gid(group): |
|
|
''' |
|
|
Convert the group to the gid on this system |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.group_to_gid root |
|
|
''' |
|
|
sid, domain, type = win32security.LookupAccountName (None, group) |
|
|
return win32security.ConvertSidToStringSid(sid) |
|
|
|
|
|
|
|
|
def get_gid(path): |
|
|
''' |
|
|
Return the id of the group that owns a given file |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.get_gid /etc/passwd |
|
|
''' |
|
|
if not os.path.exists(path): |
|
|
return -1 |
|
|
secdesc = win32security.GetFileSecurity (path, win32security.OWNER_SECURITY_INFORMATION) |
|
|
owner_sid = secdesc.GetSecurityDescriptorOwner() |
|
|
return win32security.ConvertSidToStringSid(owner_sid) |
|
|
|
|
|
|
|
|
def get_group(path): |
|
|
''' |
|
|
Return the group that owns a given file |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.get_group /etc/passwd |
|
|
''' |
|
|
#groupname = win32api.GetGroupNameEx(win32con.NameSamCompatible).split('\\') |
|
|
#if groupname[1]: |
|
|
#return groupname[1] |
|
|
#return False |
|
|
return 'agroup' |
|
|
|
|
|
|
|
|
def uid_to_user(uid): |
|
|
''' |
|
|
Convert a uid to a user name |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.uid_to_user 0 |
|
|
''' |
|
|
try: |
|
|
return pwd.getpwuid(uid).pw_name |
|
|
except KeyError: |
|
|
return '' |
|
|
|
|
|
|
|
|
def user_to_uid(user): |
|
|
''' |
|
|
Convert user name to a uid |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.user_to_uid root |
|
|
''' |
|
|
try: |
|
|
return pwd.getpwnam(user).pw_uid |
|
|
except KeyError: |
|
|
return '' |
|
|
|
|
|
|
|
|
def get_uid(path): |
|
|
''' |
|
|
Return the id of the user that owns a given file |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.get_uid /etc/passwd |
|
|
''' |
|
|
if not os.path.exists(path): |
|
|
return False |
|
|
return os.stat(path).st_uid |
|
|
|
|
|
|
|
|
def get_user(path): |
|
|
''' |
|
|
Return the user that owns a given file |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.get_user /etc/passwd |
|
|
''' |
|
|
username = win32api.GetUserNameEx(win32con.NameSamCompatible).split('\\') |
|
|
if username[1]: |
|
|
return username[1] |
|
|
return False |
|
|
|
|
|
|
|
|
def get_mode(path): |
|
|
''' |
|
|
Return the mode of a file |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.get_mode /etc/passwd |
|
|
''' |
|
|
if not os.path.exists(path): |
|
|
return -1 |
|
|
mode = str(oct(os.stat(path).st_mode)[-4:]) |
|
|
if mode.startswith('0'): |
|
|
return mode[1:] |
|
|
return mode |
|
|
|
|
|
|
|
|
def set_mode(path, mode): |
|
|
''' |
|
|
Set the mode of a file |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.set_mode /etc/passwd 0644 |
|
|
''' |
|
|
mode = str(mode) |
|
|
if not os.path.exists(path): |
|
|
return 'File not found' |
|
|
try: |
|
|
os.chmod(path, int(mode, 8)) |
|
|
# FIXME: don't use a catch-all, be more specific... |
|
|
except: |
|
|
return 'Invalid Mode ' + mode |
|
|
return get_mode(path) |
|
|
|
|
|
|
|
|
def chown(path, user, group): |
|
|
''' |
|
|
Chown a file, pass the file the desired user and group |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.chown /etc/passwd root root |
|
|
''' |
|
|
uid = user_to_uid(user) |
|
|
gid = group_to_gid(group) |
|
|
err = '' |
|
|
if uid == '': |
|
|
err += 'User does not exist\n' |
|
|
if gid == '': |
|
|
err += 'Group does not exist\n' |
|
|
if not os.path.exists(path): |
|
|
err += 'File not found' |
|
|
if err: |
|
|
return err |
|
|
return os.chown(path, uid, gid) |
|
|
|
|
|
|
|
|
def chgrp(path, group): |
|
|
''' |
|
|
Change the group of a file |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.chgrp /etc/passwd root |
|
|
''' |
|
|
gid = group_to_gid(group) |
|
|
err = '' |
|
|
if gid == '': |
|
|
err += 'Group does not exist\n' |
|
|
if not os.path.exists(path): |
|
|
err += 'File not found' |
|
|
if err: |
|
|
return err |
|
|
user = get_user(path) |
|
|
return chown(path, user, group) |
|
|
|
|
|
|
|
|
def get_sum(path, form='md5'): |
|
|
''' |
|
|
Return the sum for the given file, default is md5, sha1, sha224, sha256, |
|
|
sha384, sha512 are supported |
|
|
|
|
|
CLI Example:: |
|
|
|
|
|
salt '*' file.get_sum /etc/passwd sha512 |
|
|
''' |
|
|
if not os.path.isfile(path): |
|
|
return 'File not found' |
|
|
try: |
|
|
return getattr(hashlib, form)(open(path, 'rb').read()).hexdigest() |
|
|
except (IOError, OSError), e: |
|
|
return 'File Error: %s' % (str(e)) |
|
|
except AttributeError, e: |
|
|
return 'Hash ' + form + ' not supported' |
|
|
except NameError, e: |
|
|
return 'Hashlib unavailable - please fix your python install' |
|
|
except Exception, e: |
|
|
return str(e) |
|
|
|
|
|
|
|
|
def find(path, *opts): |
|
|
''' |
|
|
Approximate the Unix find(1) command and return a list of paths that |
|
|
meet the specified critera. |
|
|
|
|
|
The options include match criteria:: |
|
|
|
|
|
name = path-glob # case sensitive |
|
|
iname = path-glob # case insensitive |
|
|
regex = path-regex # case sensitive |
|
|
iregex = path-regex # case insensitive |
|
|
type = file-types # match any listed type |
|
|
user = users # match any listed user |
|
|
group = groups # match any listed group |
|
|
size = [+-]number[size-unit] # default unit = byte |
|
|
mtime = interval # modified since date |
|
|
grep = regex # search file contents |
|
|
|
|
|
and/or actions:: |
|
|
|
|
|
delete [= file-types] # default type = 'f' |
|
|
exec = command [arg ...] # where {} is replaced by pathname |
|
|
print [= print-opts] |
|
|
|
|
|
The default action is 'print=path'. |
|
|
|
|
|
file-glob:: |
|
|
|
|
|
* = match zero or more chars |
|
|
? = match any char |
|
|
[abc] = match a, b, or c |
|
|
[!abc] or [^abc] = match anything except a, b, and c |
|
|
[x-y] = match chars x through y |
|
|
[!x-y] or [^x-y] = match anything except chars x through y |
|
|
{a,b,c} = match a or b or c |
|
|
|
|
|
path-regex: a Python re (regular expression) pattern to match pathnames |
|
|
|
|
|
file-types: a string of one or more of the following:: |
|
|
|
|
|
a: all file types |
|
|
b: block device |
|
|
c: character device |
|
|
d: directory |
|
|
p: FIFO (named pipe) |
|
|
f: plain file |
|
|
l: symlink |
|
|
s: socket |
|
|
|
|
|
users: a space and/or comma separated list of user names and/or uids |
|
|
|
|
|
groups: a space and/or comma separated list of group names and/or gids |
|
|
|
|
|
size-unit:: |
|
|
|
|
|
b: bytes |
|
|
k: kilobytes |
|
|
m: megabytes |
|
|
g: gigabytes |
|
|
t: terabytes |
|
|
|
|
|
interval:: |
|
|
|
|
|
[<num>w] [<num>[d]] [<num>h] [<num>m] [<num>s] |
|
|
|
|
|
where: |
|
|
w: week |
|
|
d: day |
|
|
h: hour |
|
|
m: minute |
|
|
s: second |
|
|
|
|
|
print-opts: a comma and/or space separated list of one or more of the |
|
|
following:: |
|
|
|
|
|
group: group name |
|
|
md5: MD5 digest of file contents |
|
|
mode: file permissions (as integer) |
|
|
mtime: last modification time (as time_t) |
|
|
name: file basename |
|
|
path: file absolute path |
|
|
size: file size in bytes |
|
|
type: file type |
|
|
user: user name |
|
|
|
|
|
CLI Examples:: |
|
|
|
|
|
salt '*' file.find / type=f name=\*.bak size=+10m |
|
|
salt '*' file.find /var mtime=+30d size=+10m print=path,size,mtime |
|
|
salt '*' file.find /var/log name=\*.[0-9] mtime=+30d size=+10m delete |
|
|
''' |
|
|
opts_dict = {} |
|
|
for opt in opts: |
|
|
key, value = opt.split('=', 1) |
|
|
opts_dict[key] = value |
|
|
try: |
|
|
f = salt.utils.find.Finder(opts_dict) |
|
|
except ValueError, ex: |
|
|
return 'error: {0}'.format(ex) |
|
|
|
|
|
ret = [p for p in f.find(path)] |
|
|
ret.sort() |
|
|
return ret |
|
|
|
|
|
def _sed_esc(s): |
|
|
''' |
|
|
Escape single quotes and forward slashes |
|
|
''' |
|
|
return '{0}'.format(s).replace("'", "'\"'\"'").replace("/", "\/") |
|
|
|
|
|
def sed(path, before, after, limit='', backup='.bak', options='-r -e', |
|
|
flags='g'): |
|
|
''' |
|
|
Make a simple edit to a file |
|
|
|
|
|
Equivalent to:: |
|
|
|
|
|
sed <backup> <options> "/<limit>/ s/<before>/<after>/<flags> <file>" |
|
|
|
|
|
path |
|
|
The full path to the file to be edited |
|
|
before |
|
|
A pattern to find in order to replace with ``after`` |
|
|
after |
|
|
Text that will replace ``before`` |
|
|
limit : ``''`` |
|
|
An initial pattern to search for before searching for ``before`` |
|
|
backup : ``.bak`` |
|
|
The file will be backed up before edit with this file extension; |
|
|
**WARNING:** each time ``sed``/``comment``/``uncomment`` is called will |
|
|
overwrite this backup |
|
|
options : ``-r -e`` |
|
|
Options to pass to sed |
|
|
flags : ``g`` |
|
|
Flags to modify the sed search; e.g., ``i`` for case-insensitve pattern |
|
|
matching |
|
|
|
|
|
Forward slashes and single quotes will be escaped automatically in the |
|
|
``before`` and ``after`` patterns. |
|
|
|
|
|
Usage:: |
|
|
|
|
|
salt '*' file.sed /etc/httpd/httpd.conf 'LogLevel warn' 'LogLevel info' |
|
|
|
|
|
.. versionadded:: 0.9.5 |
|
|
''' |
|
|
# Largely inspired by Fabric's contrib.files.sed() |
|
|
|
|
|
before = _sed_esc(before) |
|
|
after = _sed_esc(after) |
|
|
|
|
|
cmd = r"sed {backup}{options} '{limit}s/{before}/{after}/{flags}' {path}".format( |
|
|
backup = '-i{0} '.format(backup) if backup else '', |
|
|
options = options, |
|
|
limit = '/{0}/ '.format(limit) if limit else '', |
|
|
before = before, |
|
|
after = after, |
|
|
flags = flags, |
|
|
path = path) |
|
|
|
|
|
return __salt__['cmd.run'](cmd) |
|
|
|
|
|
def uncomment(path, regex, char='#', backup='.bak'): |
|
|
''' |
|
|
Uncomment specified commented lines in a file |
|
|
|
|
|
path |
|
|
The full path to the file to be edited |
|
|
regex |
|
|
A regular expression used to find the lines that are to be uncommented. |
|
|
This regex should not include the comment character. A leading ``^`` |
|
|
character will be stripped for convenience (for easily switching |
|
|
between comment() and uncomment()). |
|
|
char : ``#`` |
|
|
The character to remove in order to uncomment a line; if a single |
|
|
whitespace character follows the comment it will also be removed |
|
|
backup : ``.bak`` |
|
|
The file will be backed up before edit with this file extension; |
|
|
**WARNING:** each time ``sed``/``comment``/``uncomment`` is called will |
|
|
overwrite this backup |
|
|
|
|
|
Usage:: |
|
|
|
|
|
salt '*' file.uncomment /etc/hosts.deny 'ALL: PARANOID' |
|
|
|
|
|
.. versionadded:: 0.9.5 |
|
|
''' |
|
|
# Largely inspired by Fabric's contrib.files.uncomment() |
|
|
|
|
|
return __salt__['file.sed'](path, |
|
|
before=r'^([[:space:]]*){0}[[:space:]]?'.format(char), |
|
|
after=r'\1', |
|
|
limit=regex.lstrip('^'), |
|
|
backup=backup) |
|
|
|
|
|
def comment(path, regex, char='#', backup='.bak'): |
|
|
''' |
|
|
Comment out specified lines in a file |
|
|
|
|
|
path |
|
|
The full path to the file to be edited |
|
|
regex |
|
|
A regular expression used to find the lines that are to be commented; |
|
|
this pattern will be wrapped in parenthesis and will move any |
|
|
preceding/trailing ``^`` or ``$`` characters outside the parenthesis |
|
|
(e.g., the pattern ``^foo$`` will be rewritten as ``^(foo)$``) |
|
|
char : ``#`` |
|
|
The character to be inserted at the beginning of a line in order to |
|
|
comment it out |
|
|
backup : ``.bak`` |
|
|
The file will be backed up before edit with this file extension |
|
|
|
|
|
.. warning:: |
|
|
|
|
|
This backup will be overwritten each time ``sed`` / ``comment`` / |
|
|
``uncomment`` is called. Meaning the backup will only be useful |
|
|
after the first invocation. |
|
|
|
|
|
Usage:: |
|
|
|
|
|
salt '*' file.comment /etc/modules pcspkr |
|
|
|
|
|
.. versionadded:: 0.9.5 |
|
|
''' |
|
|
# Largely inspired by Fabric's contrib.files.comment() |
|
|
|
|
|
regex = "{0}({1}){2}".format( |
|
|
'^' if regex.startswith('^') else '', |
|
|
regex.lstrip('^').rstrip('$'), |
|
|
'$' if regex.endswith('$') else '') |
|
|
|
|
|
return __salt__['file.sed']( |
|
|
path, |
|
|
before=regex, |
|
|
after=r'{0}\1'.format(char), |
|
|
backup=backup) |
|
|
|
|
|
def contains(path, text, limit=''): |
|
|
''' |
|
|
Return True if the file at ``path`` contains ``text`` |
|
|
|
|
|
Usage:: |
|
|
|
|
|
salt '*' file.contains /etc/crontab 'mymaintenance.sh' |
|
|
|
|
|
.. versionadded:: 0.9.5 |
|
|
''' |
|
|
# Largely inspired by Fabric's contrib.files.contains() |
|
|
|
|
|
if not os.path.exists(path): |
|
|
return False |
|
|
|
|
|
result = __salt__['file.sed'](path, text, '&', limit=limit, backup='', |
|
|
options='-n -r -e', flags='gp') |
|
|
|
|
|
return bool(result) |
|
|
|
|
|
def append(path, *args): |
|
|
''' |
|
|
Append text to the end of a file |
|
|
|
|
|
Usage:: |
|
|
|
|
|
salt '*' file.append /etc/motd \\ |
|
|
"With all thine offerings thou shalt offer salt."\\ |
|
|
"Salt is what makes things taste bad when it isn't in them." |
|
|
|
|
|
.. versionadded:: 0.9.5 |
|
|
''' |
|
|
# Largely inspired by Fabric's contrib.files.append() |
|
|
|
|
|
with open(path, "a") as f: |
|
|
for line in args: |
|
|
f.write('{0}\n'.format(line)) |
|
|
|
|
|
return "Wrote {0} lines to '{1}'".format(len(args), path) |
|
|
|
|
|
def touch(name, atime=None, mtime=None): |
|
|
''' |
|
|
Just like 'nix's "touch" command, create a file if it |
|
|
doesn't exist or simply update the atime and mtime if |
|
|
it already does. |
|
|
|
|
|
atime: |
|
|
Access time in Unix epoch time |
|
|
mtime: |
|
|
Last modification in Unix epoch time |
|
|
|
|
|
Usage:: |
|
|
salt '*' file.touch /var/log/emptyfile |
|
|
|
|
|
.. versionadded:: 0.9.5 |
|
|
''' |
|
|
if atime and atime.isdigit(): |
|
|
atime = int(atime) |
|
|
if mtime and mtime.isdigit(): |
|
|
mtime = int(mtime) |
|
|
try: |
|
|
with open(name, "a"): |
|
|
if not atime and not mtime: |
|
|
times = None |
|
|
elif not mtime and atime: |
|
|
times = (atime, time.time()) |
|
|
elif not atime and mtime: |
|
|
times = (time.time(), mtime) |
|
|
else: |
|
|
times = (atime, mtime) |
|
|
os.utime(name, times) |
|
|
except TypeError as exc: |
|
|
msg = "atime and mtime must be integers" |
|
|
raise SaltInvocationError(msg) |
|
|
except (IOError, OSError) as exc: |
|
|
return False |
|
|
|
|
|
return os.path.exists(name) |