summaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2012-07-03 08:56:47 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2012-07-03 08:56:47 -0400
commit09e934512dc053a96bd7b16c2c95563e055720f7 (patch)
treee1351268921fb0fc3b64df8d565044df25196930 /src/lib
parent9fe65b2fe9323da6583625cde1b2494352207d51 (diff)
downloadbcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.gz
bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.tar.bz2
bcfg2-09e934512dc053a96bd7b16c2c95563e055720f7.zip
added selinux support
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/Bcfg2/Client/Frame.py76
-rw-r--r--src/lib/Bcfg2/Client/Tools/POSIX.py798
-rw-r--r--src/lib/Bcfg2/Client/Tools/SELinux.py716
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py23
-rw-r--r--src/lib/Bcfg2/Options.py40
-rw-r--r--src/lib/Bcfg2/Server/Admin/Compare.py3
-rw-r--r--src/lib/Bcfg2/Server/Lint/RequiredAttrs.py134
-rw-r--r--src/lib/Bcfg2/Server/Plugin.py12
-rw-r--r--src/lib/Bcfg2/Server/Plugins/SEModules.py46
9 files changed, 1401 insertions, 447 deletions
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py
index a8bcb69bf..51bc4aec7 100644
--- a/src/lib/Bcfg2/Client/Frame.py
+++ b/src/lib/Bcfg2/Client/Frame.py
@@ -124,33 +124,47 @@ class Frame:
self.logger.info([tool.name for tool in self.tools])
# find entries not handled by any tools
- problems = [entry for struct in config for \
- entry in struct if entry not in self.handled]
+ problems = [entry for struct in config
+ for entry in struct
+ if entry not in self.handled]
if problems:
self.logger.error("The following entries are not handled by any tool:")
- self.logger.error(["%s:%s:%s" % (entry.tag, entry.get('type'), \
- entry.get('name')) for entry in problems])
+ for entry in problems:
+ self.logger.error("%s:%s:%s" % (entry.tag, entry.get('type'),
+ entry.get('name')))
self.logger.error("")
- entries = [(entry.tag, entry.get('name'))
- for struct in config for entry in struct]
+
+ self.find_dups(config)
+
pkgs = [(entry.get('name'), entry.get('origin'))
- for struct in config for entry in struct if entry.tag == 'Package']
- multi = []
- for entry in entries[:]:
- if entries.count(entry) > 1:
- multi.append(entry)
- entries.remove(entry)
- if multi:
- self.logger.debug("The following entries are included multiple times:")
- self.logger.debug(["%s:%s" % entry for entry in multi])
- self.logger.debug("")
+ for struct in config
+ for entry in struct
+ if entry.tag == 'Package']
if pkgs:
self.logger.debug("The following packages are specified in bcfg2:")
self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == None])
self.logger.debug("The following packages are prereqs added by Packages:")
self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == 'Packages'])
+ def find_dups(self, config):
+ entries = dict()
+ for struct in config:
+ for entry in struct:
+ for tool in self.tools:
+ if tool.handlesEntry(entry):
+ pkey = tool.primarykey(entry)
+ if pkey in entries:
+ entries[pkey] += 1
+ else:
+ entries[pkey] = 1
+ multi = [e for e, c in entries.items() if c > 1]
+ if multi:
+ self.logger.debug("The following entries are included multiple times:")
+ for entry in multi:
+ self.logger.debug(entry)
+ self.logger.debug("")
+
def __getattr__(self, name):
if name in ['extra', 'handled', 'modified', '__important__']:
ret = []
@@ -399,16 +413,32 @@ class Frame:
def CondDisplayState(self, phase):
"""Conditionally print tracing information."""
self.logger.info('\nPhase: %s' % phase)
- self.logger.info('Correct entries:\t%d' % list(self.states.values()).count(True))
- self.logger.info('Incorrect entries:\t%d' % list(self.states.values()).count(False))
+ self.logger.info('Correct entries:\t%d' %
+ list(self.states.values()).count(True))
+ self.logger.info('Incorrect entries:\t%d' %
+ list(self.states.values()).count(False))
if phase == 'final' and list(self.states.values()).count(False):
- self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) for \
- entry in self.states if not self.states[entry]])
- self.logger.info('Total managed entries:\t%d' % len(list(self.states.values())))
+ for entry in self.states.keys():
+ if not self.states[entry]:
+ etype = entry.get('type')
+ if etype:
+ self.logger.info( "%s:%s:%s" % (entry.tag, etype,
+ entry.get('name')))
+ else:
+ self.logger.info(" %s:%s" % (entry.tag,
+ entry.get('name')))
+ self.logger.info('Total managed entries:\t%d' %
+ len(list(self.states.values())))
self.logger.info('Unmanaged entries:\t%d' % len(self.extra))
if phase == 'final' and self.setup['extra']:
- self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) \
- for entry in self.extra])
+ for entry in self.extra:
+ etype = entry.get('type')
+ if etype:
+ self.logger.info( "%s:%s:%s" % (entry.tag, etype,
+ entry.get('name')))
+ else:
+ self.logger.info(" %s:%s" % (entry.tag,
+ entry.get('name')))
self.logger.info("")
diff --git a/src/lib/Bcfg2/Client/Tools/POSIX.py b/src/lib/Bcfg2/Client/Tools/POSIX.py
index 995d82356..859d4dd81 100644
--- a/src/lib/Bcfg2/Client/Tools/POSIX.py
+++ b/src/lib/Bcfg2/Client/Tools/POSIX.py
@@ -20,7 +20,13 @@ import Bcfg2.Client.Tools
import Bcfg2.Options
from Bcfg2.Client import XML
-log = logging.getLogger('POSIX')
+log = logging.getLogger(__name__)
+
+try:
+ import selinux
+ has_selinux = True
+except ImportError:
+ has_selinux = False
# map between dev_type attribute and stat constants
device_map = {'block': stat.S_IFBLK,
@@ -28,24 +34,7 @@ device_map = {'block': stat.S_IFBLK,
'fifo': stat.S_IFIFO}
-def calcPerms(initial, perms):
- """This compares ondisk permissions with specified ones."""
- pdisp = [{1:stat.S_ISVTX, 2:stat.S_ISGID, 4:stat.S_ISUID},
- {1:stat.S_IXUSR, 2:stat.S_IWUSR, 4:stat.S_IRUSR},
- {1:stat.S_IXGRP, 2:stat.S_IWGRP, 4:stat.S_IRGRP},
- {1:stat.S_IXOTH, 2:stat.S_IWOTH, 4:stat.S_IROTH}]
- tempperms = initial
- if len(perms) == 3:
- perms = '0%s' % (perms)
- pdigits = [int(perms[digit]) for digit in range(4)]
- for index in range(4):
- for (num, perm) in list(pdisp[index].items()):
- if pdigits[index] & num:
- tempperms |= perm
- return tempperms
-
-
-def normGid(entry, logger=None):
+def normGid(entry):
"""
This takes a group name or gid and
returns the corresponding gid or False.
@@ -96,6 +85,115 @@ def isString(strng, encoding):
return False
+def secontextMatches(entry):
+ """ determine if the SELinux context of the file on disk matches
+ the desired context """
+ if not has_selinux:
+ # no selinux libraries
+ return True
+
+ path = entry.get("path")
+ context = entry.get("secontext")
+ if context is None:
+ # no context listed
+ return True
+
+ if context == '__default__':
+ if selinux.getfilecon(entry.get('name'))[1] == \
+ selinux.matchpathcon(entry.get('name'), 0)[1]:
+ return True
+ else:
+ return False
+ elif selinux.getfilecon(entry.get('name'))[1] == context:
+ return True
+ else:
+ return False
+
+
+def setSEContext(entry, path=None, recursive=False):
+ """ set the SELinux context of the file on disk according to the
+ config"""
+ if not has_selinux:
+ return True
+
+ if path is None:
+ path = entry.get("path")
+ context = entry.get("secontext")
+ if context is None:
+ # no context listed
+ return True
+
+ rv = True
+ if context == '__default__':
+ try:
+ selinux.restorecon(path, recursive=recursive)
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s" %
+ (path, err))
+ rv = False
+ else:
+ try:
+ rv &= selinux.lsetfilecon(path, context) == 0
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s" %
+ (path, err))
+ rv = False
+
+ if recursive:
+ for root, dirs, files in os.walk(path):
+ for p in dirs + files:
+ try:
+ rv &= selinux.lsetfilecon(p, context) == 0
+ except:
+ err = sys.exc_info()[1]
+ log.error("Failed to restore SELinux context for %s: %s"
+ % (path, err))
+ rv = False
+ return rv
+
+
+def setPerms(entry, path=None):
+ if path is None:
+ path = entry.get("name")
+
+ if (entry.get('perms') == None or
+ entry.get('owner') == None or
+ entry.get('group') == None):
+ self.logger.error('Entry %s not completely specified. '
+ 'Try running bcfg2-lint.' % entry.get('name'))
+ return False
+
+ rv = True
+ # split this into multiple try...except blocks so that even if a
+ # chown fails, the chmod can succeed -- get as close to the
+ # desired state as we can
+ try:
+ os.chown(path, normUid(entry), normGid(entry))
+ except KeyError:
+ logger.error('Failed to change ownership of %s' % path)
+ rv = False
+ os.chown(path, 0, 0)
+ except OSError:
+ logger.error('Failed to change ownership of %s' % path)
+ rv = False
+
+ configPerms = int(entry.get('perms'), 8)
+ if entry.get('dev_type'):
+ configPerms |= device_map[entry.get('dev_type')]
+ try:
+ os.chmod(path, configPerms)
+ except (OSError, KeyError):
+ logger.error('Failed to change permissions mode of %s' % path)
+ rv = False
+
+ if has_selinux:
+ rv &= setSEContext(entry, path=path)
+
+ return rv
+
+
class POSIX(Bcfg2.Client.Tools.Tool):
"""POSIX File support code."""
name = 'POSIX'
@@ -106,7 +204,14 @@ class POSIX(Bcfg2.Client.Tools.Tool):
('Path', 'nonexistent'),
('Path', 'permissions'),
('Path', 'symlink')]
- __req__ = {'Path': ['name', 'type']}
+ __req__ = dict(Path=dict(
+ device=['name', 'dev_type', 'perms', 'owner', 'group'],
+ directory=['name', 'perms', 'owner', 'group'],
+ file=['name', 'perms', 'owner', 'group'],
+ hardlink=['name', 'to'],
+ nonexistent=['name'],
+ permissions=['name', 'perms', 'owner', 'group'],
+ symlink=['name', 'to']))
# grab paranoid options from /etc/bcfg2.conf
opts = {'ppath': Bcfg2.Options.PARANOID_PATH,
@@ -119,13 +224,9 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def canInstall(self, entry):
"""Check if entry is complete for installation."""
if Bcfg2.Client.Tools.Tool.canInstall(self, entry):
- if (entry.tag,
- entry.get('type'),
- entry.text,
- entry.get('empty', 'false')) == ('Path',
- 'file',
- None,
- 'false'):
+ if (entry.get('type') == 'file' and
+ entry.text is None and
+ entry.get('empty', 'false') == 'false'):
return False
return True
else:
@@ -145,69 +246,60 @@ class POSIX(Bcfg2.Client.Tools.Tool):
entry.set('current_group', str(ondisk[stat.ST_GID]))
except (OSError, KeyError):
pass
+
+ if has_selinux:
+ try:
+ entry.set('current_secontext',
+ selinux.getfilecon(entry.get('name'))[1])
+ except (OSError, KeyError):
+ pass
entry.set('perms', str(oct(ondisk[stat.ST_MODE])[-4:]))
def Verifydevice(self, entry, _):
"""Verify device entry."""
- if entry.get('dev_type') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
if entry.get('dev_type') in ['block', 'char']:
# check if major/minor are properly specified
- if entry.get('major') == None or \
- entry.get('minor') == None:
+ if (entry.get('major') == None or
+ entry.get('minor') == None):
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
+ 'Try running bcfg2-lint.' %
+ (entry.get('name')))
return False
+
try:
- # check for file existence
- filestat = os.stat(entry.get('name'))
+ ondisk = os.stat(path)
except OSError:
entry.set('current_exists', 'false')
self.logger.debug("%s %s does not exist" %
- (entry.tag, entry.get('name')))
+ (entry.tag, path))
return False
- try:
- # attempt to verify device properties as specified in config
- dev_type = entry.get('dev_type')
- mode = calcPerms(device_map[dev_type],
- entry.get('mode', '0600'))
- owner = normUid(entry, logger=self.logger)
- group = normGid(entry, logger=self.logger)
- if dev_type in ['block', 'char']:
- # check for incompletely specified entries
- if entry.get('major') == None or \
- entry.get('minor') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
- major = int(entry.get('major'))
- minor = int(entry.get('minor'))
- if major == os.major(filestat.st_rdev) and \
- minor == os.minor(filestat.st_rdev) and \
- mode == filestat.st_mode and \
- owner == filestat.st_uid and \
- group == filestat.st_gid:
- return True
- else:
- return False
- elif dev_type == 'fifo' and \
- mode == filestat.st_mode and \
- owner == filestat.st_uid and \
- group == filestat.st_gid:
- return True
- else:
- self.logger.info('Device properties for %s incorrect' % \
- entry.get('name'))
- return False
- except OSError:
- self.logger.debug("%s %s failed to verify" %
- (entry.tag, entry.get('name')))
- return False
+ rv = self._verify_metadata(entry)
+
+ # attempt to verify device properties as specified in config
+ dev_type = entry.get('dev_type')
+ if dev_type in ['block', 'char']:
+ major = int(entry.get('major'))
+ minor = int(entry.get('minor'))
+ if major != os.major(ondisk.st_rdev):
+ entry.set('current_mtime', mtime)
+ msg = ("Major number for device %s is incorrect. "
+ "Current major is %s but should be %s" %
+ (path, os.major(ondisk.st_rdev), major))
+ self.logger.debug(msg)
+ entry.set('qtext', entry.get('qtext') + "\n" + msg)
+ rv = False
+
+ if minor != os.minor(ondisk.st_rdev):
+ entry.set('current_mtime', mtime)
+ msg = ("Minor number for device %s is incorrect. "
+ "Current minor is %s but should be %s" %
+ (path, os.minor(ondisk.st_rdev), minor))
+ self.logger.debug(msg)
+ entry.set('qtext', entry.get('qtext') + "\n" + msg)
+ rv = False
+
+ return rv
def Installdevice(self, entry):
"""Install device entries."""
@@ -218,7 +310,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
os.unlink(entry.get('name'))
exists = False
except OSError:
- self.logger.info('Failed to unlink %s' % \
+ self.logger.info('Failed to unlink %s' %
entry.get('name'))
return False
except OSError:
@@ -227,14 +319,14 @@ class POSIX(Bcfg2.Client.Tools.Tool):
if not exists:
try:
dev_type = entry.get('dev_type')
- mode = calcPerms(device_map[dev_type],
- entry.get('mode', '0600'))
+ mode = device_map[dev_type] | int(entry.get('mode', '0600'), 8)
if dev_type in ['block', 'char']:
# check if major/minor are properly specified
- if entry.get('major') == None or \
- entry.get('minor') == None:
+ if (entry.get('major') == None or
+ entry.get('minor') == None):
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
+ 'Try running bcfg2-lint.' %
+ entry.get('name'))
return False
major = int(entry.get('major'))
minor = int(entry.get('minor'))
@@ -242,17 +334,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
os.mknod(entry.get('name'), mode, device)
else:
os.mknod(entry.get('name'), mode)
- """
- Python uses the OS mknod(2) implementation which modifies the
- mode based on the umask of the running process. Therefore, the
- following chmod(2) call is needed to make sure the permissions
- are set as specified by the user.
- """
- os.chmod(entry.get('name'), mode)
- os.chown(entry.get('name'),
- normUid(entry, logger=self.logger),
- normGid(entry, logger=self.logger))
- return True
+ return setPerms(entry)
except KeyError:
self.logger.error('Failed to install %s' % entry.get('name'))
except OSError:
@@ -261,47 +343,13 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifydirectory(self, entry, modlist):
"""Verify Path type='directory' entry."""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error("POSIX: Entry %s not completely specified. "
- "Try running bcfg2-lint." % (entry.get('name')))
- return False
- while len(entry.get('perms', '')) < 4:
- entry.set('perms', '0' + entry.get('perms', ''))
- try:
- ondisk = os.stat(entry.get('name'))
- except OSError:
- entry.set('current_exists', 'false')
- self.logger.info("POSIX: %s %s does not exist" %
- (entry.tag, entry.get('name')))
- return False
- try:
- owner = str(ondisk[stat.ST_UID])
- group = str(ondisk[stat.ST_GID])
- except (OSError, KeyError):
- self.logger.info("POSIX: User/Group resolution failed "
- "for path %s" % entry.get('name'))
- owner = 'root'
- group = '0'
- finfo = os.stat(entry.get('name'))
- perms = oct(finfo[stat.ST_MODE])[-4:]
- if entry.get('mtime', '-1') != '-1':
- mtime = str(finfo[stat.ST_MTIME])
- else:
- mtime = '-1'
- pTrue = ((owner == str(normUid(entry, logger=self.logger))) and
- (group == str(normGid(entry, logger=self.logger))) and
- (perms == entry.get('perms')) and
- (mtime == entry.get('mtime', '-1')))
-
pruneTrue = True
ex_ents = []
- if entry.get('prune', 'false') == 'true' \
- and (entry.tag == 'Path' and entry.get('type') == 'directory'):
+ if (entry.get('prune', 'false') == 'true'
+ and (entry.tag == 'Path' and entry.get('type') == 'directory')):
# check for any extra entries when prune='true' attribute is set
try:
- entries = ['/'.join([entry.get('name'), ent]) \
+ entries = ['/'.join([entry.get('name'), ent])
for ent in os.listdir(entry.get('name'))]
ex_ents = [e for e in entries if e not in modlist]
if ex_ents:
@@ -313,99 +361,48 @@ class POSIX(Bcfg2.Client.Tools.Tool):
nqtext += "Directory %s contains extra entries: " % \
entry.get('name')
nqtext += ":".join(ex_ents)
- entry.set('qtest', nqtext)
- [entry.append(XML.Element('Prune', path=x)) \
+ entry.set('qtext', nqtext)
+ [entry.append(XML.Element('Prune', path=x))
for x in ex_ents]
except OSError:
ex_ents = []
pruneTrue = True
- if not pTrue:
- if owner != str(normUid(entry, logger=self.logger)):
- entry.set('current_owner', owner)
- self.logger.debug("%s %s ownership wrong" % \
- (entry.tag, entry.get('name')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s owner wrong. is %s should be %s" % \
- (entry.get('name'), owner, entry.get('owner'))
- entry.set('qtext', nqtext)
- if group != str(normGid(entry, logger=self.logger)):
- entry.set('current_group', group)
- self.logger.debug("%s %s group wrong" % \
- (entry.tag, entry.get('name')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s group is %s should be %s" % \
- (entry.get('name'), group, entry.get('group'))
- entry.set('qtext', nqtext)
- if perms != entry.get('perms'):
- entry.set('current_perms', perms)
- self.logger.debug("%s %s permissions are %s should be %s" %
- (entry.tag,
- entry.get('name'),
- perms,
- entry.get('perms')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s %s perms are %s should be %s" % \
- (entry.tag,
- entry.get('name'),
- perms,
- entry.get('perms'))
- entry.set('qtext', nqtext)
- if mtime != entry.get('mtime', '-1'):
- entry.set('current_mtime', mtime)
- self.logger.debug("%s %s mtime is %s should be %s" \
- % (entry.tag, entry.get('name'), mtime,
- entry.get('mtime')))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += "%s mtime is %s should be %s" % \
- (entry.get('name'), mtime, entry.get('mtime'))
- entry.set('qtext', nqtext)
- if entry.get('type') != 'file':
- nnqtext = entry.get('qtext')
- nnqtext += '\nInstall %s %s: (y/N) ' % (entry.get('type'),
- entry.get('name'))
- entry.set('qtext', nnqtext)
- return pTrue and pruneTrue
+ return pruneTrue and self._verify_metadata(entry)
def Installdirectory(self, entry):
"""Install Path type='directory' entry."""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
- self.logger.info("Installing directory %s" % (entry.get('name')))
+ self.logger.info("Installing directory %s" % entry.get('name'))
try:
fmode = os.lstat(entry.get('name'))
- if not stat.S_ISDIR(fmode[stat.ST_MODE]):
- self.logger.debug("Found a non-directory entry at %s" % \
- (entry.get('name')))
- try:
- os.unlink(entry.get('name'))
- exists = False
- except OSError:
- self.logger.info("Failed to unlink %s" % \
- (entry.get('name')))
- return False
- else:
- self.logger.debug("Found a pre-existing directory at %s" % \
- (entry.get('name')))
- exists = True
except OSError:
# stat failed
exists = False
+ if not stat.S_ISDIR(fmode[stat.ST_MODE]):
+ self.logger.debug("Found a non-directory entry at %s" %
+ entry.get('name'))
+ try:
+ os.unlink(entry.get('name'))
+ exists = False
+ except OSError:
+ self.logger.info("Failed to unlink %s" % entry.get('name'))
+ return False
+ else:
+ self.logger.debug("Found a pre-existing directory at %s" %
+ entry.get('name'))
+ exists = True
+
if not exists:
parent = "/".join(entry.get('name').split('/')[:-1])
if parent:
try:
os.stat(parent)
except:
- self.logger.debug('Creating parent path for directory %s' % (entry.get('name')))
+ self.logger.debug('Creating parent path for directory %s' %
+ entry.get('name'))
for idx in range(len(parent.split('/')[:-1])):
- current = '/'+'/'.join(parent.split('/')[1:2+idx])
+ current = '/' + '/'.join(parent.split('/')[1:2+idx])
try:
sloc = os.stat(current)
except OSError:
@@ -424,10 +421,10 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.mkdir(entry.get('name'))
except OSError:
- self.logger.error('Failed to create directory %s' % \
- (entry.get('name')))
+ self.logger.error('Failed to create directory %s' %
+ entry.get('name'))
return False
- if entry.get('prune', 'false') == 'true' and entry.get("qtest"):
+ if entry.get('prune', 'false') == 'true' and entry.get("qtext"):
for pent in entry.findall('Prune'):
pname = pent.get('path')
ulfailed = False
@@ -448,7 +445,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifyfile(self, entry, _):
"""Verify Path type='file' entry."""
# permissions check + content check
- permissionStatus = self.Verifydirectory(entry, _)
+ permissionStatus = self._verify_metadata(entry)
tbin = False
if entry.text == None and entry.get('empty', 'false') == 'false':
self.logger.error("Cannot verify incomplete Path type='%s' %s" %
@@ -466,7 +463,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
tempdata = tempdata.encode(self.setup['encoding'])
except UnicodeEncodeError:
e = sys.exc_info()[1]
- self.logger.error("Error encoding file %s:\n %s" % \
+ self.logger.error("Error encoding file %s:\n %s" %
(entry.get('name'), e))
different = False
@@ -535,8 +532,6 @@ class POSIX(Bcfg2.Client.Tools.Tool):
else:
prompt.append("Diff took too long to compute, no "
"printable diff")
- prompt.append("Install %s %s: (y/N): " % (entry.tag,
- entry.get('name')))
entry.set("qtext", "\n".join(prompt))
if entry.get('sensitive', 'false').lower() != 'true':
@@ -565,12 +560,6 @@ class POSIX(Bcfg2.Client.Tools.Tool):
binascii.b2a_base64("\n".join(diff)))
elif not tbin and isString(content, self.setup['encoding']):
entry.set('current_bfile', binascii.b2a_base64(content))
- elif permissionStatus == False and self.setup['interactive']:
- prompt = [entry.get('qtext', '')]
- prompt.append("Install %s %s: (y/N): " % (entry.tag,
- entry.get('name')))
- entry.set("qtext", "\n".join(prompt))
-
return permissionStatus and not different
@@ -583,8 +572,8 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.stat(parent)
except:
- self.logger.debug('Creating parent path for config file %s' % \
- (entry.get('name')))
+ self.logger.debug('Creating parent path for config file %s' %
+ entry.get('name'))
current = '/'
for next in parent.split('/')[1:]:
current += next + '/'
@@ -592,23 +581,24 @@ class POSIX(Bcfg2.Client.Tools.Tool):
sloc = os.stat(current)
try:
if not stat.S_ISDIR(sloc[stat.ST_MODE]):
- self.logger.debug('%s is not a directory; recreating' \
- % (current))
+ self.logger.debug('%s is not a directory; recreating'
+ % current)
os.unlink(current)
os.mkdir(current)
except OSError:
return False
except OSError:
try:
- self.logger.debug("Creating non-existent path %s" % current)
+ self.logger.debug("Creating non-existent path %s" %
+ current)
os.mkdir(current)
except OSError:
return False
# If we get here, then the parent directory should exist
- if (entry.get("paranoid", False) in ['true', 'True']) and \
- self.setup.get("paranoid", False) and not \
- (entry.get('current_exists', 'true') == 'false'):
+ if (entry.get("paranoid", 'false').lower() == 'true' and
+ self.setup.get("paranoid", False) and
+ entry.get('current_exists', 'true') != 'false'):
bkupnam = entry.get('name').replace('/', '_')
# current list of backups for this file
try:
@@ -627,7 +617,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
try:
os.remove("%s/%s" % (self.ppath, oldest))
except:
- self.logger.error("Failed to remove %s/%s" % \
+ self.logger.error("Failed to remove %s/%s" %
(self.ppath, oldest))
return False
try:
@@ -639,8 +629,8 @@ class POSIX(Bcfg2.Client.Tools.Tool):
(entry.get('name'), self.ppath))
except IOError:
e = sys.exc_info()[1]
- self.logger.error("Failed to create backup file for %s" % \
- (entry.get('name')))
+ self.logger.error("Failed to create backup file for %s" %
+ entry.get('name'))
self.logger.error(e)
return False
try:
@@ -656,82 +646,59 @@ class POSIX(Bcfg2.Client.Tools.Tool):
filedata = entry.text
newfile.write(filedata)
newfile.close()
- try:
- os.chown(newfile.name,
- normUid(entry, logger=self.logger),
- normGid(entry, logger=self.logger))
- except KeyError:
- self.logger.error("Failed to chown %s to %s:%s" %
- (newfile.name, entry.get('owner'),
- entry.get('group')))
- os.chown(newfile.name, 0, 0)
- except OSError:
- err = sys.exc_info()[1]
- self.logger.error("Could not chown %s: %s" % (newfile.name,
- err))
- os.chmod(newfile.name, calcPerms(stat.S_IFREG, entry.get('perms')))
+
+ rv = setPerms(entry, newfile.name)
os.rename(newfile.name, entry.get('name'))
- if entry.get('mtime', '-1') != '-1':
+ if entry.get('mtime'):
try:
os.utime(entry.get('name'), (int(entry.get('mtime')),
int(entry.get('mtime'))))
except:
- self.logger.error("File %s mtime fix failed" \
- % (entry.get('name')))
- return False
- return True
+ logger.error("Failed to set mtime of %s" % path)
+ rv = False
+ return rv
except (OSError, IOError):
err = sys.exc_info()[1]
if err.errno == errno.EACCES:
- self.logger.info("Failed to open %s for writing" % (entry.get('name')))
+ self.logger.info("Failed to open %s for writing" %
+ entry.get('name'))
else:
print(err)
return False
def Verifyhardlink(self, entry, _):
"""Verify HardLink entry."""
- if entry.get('to') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
+ rv = True
+
try:
- if os.path.samefile(entry.get('name'), entry.get('to')):
- return True
- self.logger.debug("Hardlink %s is incorrect" % \
- entry.get('name'))
- entry.set('qtext', "Link %s to %s? [y/N] " % \
- (entry.get('name'),
- entry.get('to')))
- return False
+ if not os.path.samefile(entry.get('name'), entry.get('to')):
+ msg = "Hardlink %s is incorrect." % entry.get('name')
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), msg]))
+ rv = False
except OSError:
entry.set('current_exists', 'false')
- entry.set('qtext', "Link %s to %s? [y/N] " % \
- (entry.get('name'),
- entry.get('to')))
return False
+ rv &= self._verify_secontext(entry)
+ return rv
+
def Installhardlink(self, entry):
"""Install HardLink entry."""
- if entry.get('to') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
- return False
- self.logger.info("Installing Hardlink %s" % (entry.get('name')))
+ self.logger.info("Installing Hardlink %s" % entry.get('name'))
if os.path.lexists(entry.get('name')):
try:
fmode = os.lstat(entry.get('name'))[stat.ST_MODE]
if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode):
self.logger.debug("Non-directory entry already exists at "
- "%s. Unlinking entry." % (entry.get('name')))
+ "%s. Unlinking entry." %
+ entry.get('name'))
os.unlink(entry.get('name'))
elif stat.S_ISDIR(fmode):
- self.logger.debug("Directory already exists at %s" % \
- (entry.get('name')))
- self.cmd.run("mv %s/ %s.bak" % \
- (entry.get('name'),
- entry.get('name')))
+ self.logger.debug("Directory already exists at %s" %
+ entry.get('name'))
+ self.cmd.run("mv %s/ %s.bak" % (entry.get('name'),
+ entry.get('name')))
else:
os.unlink(entry.get('name'))
except OSError:
@@ -739,7 +706,7 @@ class POSIX(Bcfg2.Client.Tools.Tool):
(entry.get('name')))
try:
os.link(entry.get('to'), entry.get('name'))
- return True
+ return setPerms(entry)
except OSError:
return False
@@ -789,135 +756,63 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def Verifypermissions(self, entry, _):
"""Verify Path type='permissions' entry"""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
- if entry.get('recursive') in ['True', 'true']:
+ rv = self._verify_metadata(entry)
+
+ if entry.get('recursive', 'false').lower() == 'true':
# verify ownership information recursively
- owner = normUid(entry, logger=self.logger)
- group = normGid(entry, logger=self.logger)
-
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
- path = os.path.join(root, p)
- pstat = os.stat(path)
- if owner != pstat.st_uid:
- # owner mismatch for path
- entry.set('current_owner', str(pstat.st_uid))
- self.logger.debug("%s %s ownership wrong" % \
- (entry.tag, path))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += ("Owner for path %s is incorrect. "
- "Current owner is %s but should be %s\n" % \
- (path, pstat.st_uid, entry.get('owner')))
- nqtext += ("\nInstall %s %s: (y/N): " %
- (entry.tag, entry.get('name')))
- entry.set('qtext', nqtext)
- return False
- if group != pstat.st_gid:
- # group mismatch for path
- entry.set('current_group', str(pstat.st_gid))
- self.logger.debug("%s %s group wrong" % \
- (entry.tag, path))
- nqtext = entry.get('qtext', '') + '\n'
- nqtext += ("Group for path %s is incorrect. "
- "Current group is %s but should be %s\n" % \
- (path, pstat.st_gid, entry.get('group')))
- nqtext += ("\nInstall %s %s: (y/N): " %
- (entry.tag, entry.get('name')))
- entry.set('qtext', nqtext)
- return False
- return self.Verifydirectory(entry, _)
-
- def _diff(self, content1, content2, difffunc, filename=None):
- rv = []
- start = time.time()
- longtime = False
- for diffline in difffunc(content1.split('\n'),
- content2.split('\n')):
- now = time.time()
- rv.append(diffline)
- if now - start > 5 and not longtime:
- if filename:
- self.logger.info("Diff of %s taking a long time" %
- filename)
- else:
- self.logger.info("Diff taking a long time")
- longtime = True
- elif now - start > 30:
- if filename:
- self.logger.error("Diff of %s took too long; giving up" %
- filename)
- else:
- self.logger.error("Diff took too long; giving up")
- return False
+ rv &= self._verify_metadata(entry,
+ path=os.path.join(root, p))
return rv
def Installpermissions(self, entry):
"""Install POSIX permissions"""
- if entry.get('perms') == None or \
- entry.get('owner') == None or \
- entry.get('group') == None:
- self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % (entry.get('name')))
- return False
plist = [entry.get('name')]
if entry.get('recursive') in ['True', 'true']:
# verify ownership information recursively
- owner = normUid(entry, logger=self.logger)
- group = normGid(entry, logger=self.logger)
-
for root, dirs, files in os.walk(entry.get('name')):
for p in dirs + files:
- path = os.path.join(root, p)
- pstat = os.stat(path)
- if owner != pstat.st_uid or group != pstat.st_gid:
- # owner mismatch for path
+ if not self._verify_metadata(entry,
+ path=os.path.join(root, p),
+ checkonly=True):
plist.append(path)
- try:
- for p in plist:
- os.chown(p,
- normUid(entry, logger=self.logger),
- normGid(entry, logger=self.logger))
- os.chmod(p, calcPerms(stat.S_IFDIR, entry.get('perms')))
- return True
- except (OSError, KeyError):
- self.logger.error('Permission fixup failed for %s' % \
- (entry.get('name')))
- return False
+ rv = True
+ for path in plist:
+ rv &= setPerms(entry, path)
+ return rv
def Verifysymlink(self, entry, _):
"""Verify Path type='symlink' entry."""
if entry.get('to') == None:
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
+ 'Try running bcfg2-lint.' %
(entry.get('name')))
return False
+
+ rv = True
+
try:
sloc = os.readlink(entry.get('name'))
- if sloc == entry.get('to'):
- return True
- self.logger.debug("Symlink %s points to %s, should be %s" % \
- (entry.get('name'), sloc, entry.get('to')))
- entry.set('current_to', sloc)
- entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'),
- entry.get('to')))
- return False
+ if sloc != entry.get('to'):
+ entry.set('current_to', sloc)
+ msg = ("Symlink %s points to %s, should be %s" %
+ (entry.get('name'), sloc, entry.get('to')))
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get('qtext', ''), msg]))
+ rv = False
except OSError:
entry.set('current_exists', 'false')
- entry.set('qtext', "Link %s to %s? [y/N] " % (entry.get('name'),
- entry.get('to')))
return False
+ rv &= self._verify_secontext(entry)
+ return rv
+
def Installsymlink(self, entry):
"""Install Path type='symlink' entry."""
if entry.get('to') == None:
self.logger.error('Entry %s not completely specified. '
- 'Try running bcfg2-lint.' % \
- (entry.get('name')))
+ 'Try running bcfg2-lint.' % entry.get('name'))
return False
self.logger.info("Installing symlink %s" % (entry.get('name')))
if os.path.lexists(entry.get('name')):
@@ -925,23 +820,22 @@ class POSIX(Bcfg2.Client.Tools.Tool):
fmode = os.lstat(entry.get('name'))[stat.ST_MODE]
if stat.S_ISREG(fmode) or stat.S_ISLNK(fmode):
self.logger.debug("Non-directory entry already exists at "
- "%s. Unlinking entry." % \
- (entry.get('name')))
+ "%s. Unlinking entry." %
+ entry.get('name'))
os.unlink(entry.get('name'))
elif stat.S_ISDIR(fmode):
- self.logger.debug("Directory already exists at %s" %\
- (entry.get('name')))
- self.cmd.run("mv %s/ %s.bak" % \
- (entry.get('name'),
- entry.get('name')))
+ self.logger.debug("Directory already exists at %s" %
+ entry.get('name'))
+ self.cmd.run("mv %s/ %s.bak" % (entry.get('name'),
+ entry.get('name')))
else:
os.unlink(entry.get('name'))
except OSError:
- self.logger.info("Symlink %s cleanup failed" %\
+ self.logger.info("Symlink %s cleanup failed" %
(entry.get('name')))
try:
os.symlink(entry.get('to'), entry.get('name'))
- return True
+ return setSEContext(entry)
except OSError:
return False
@@ -952,5 +846,139 @@ class POSIX(Bcfg2.Client.Tools.Tool):
def VerifyPath(self, entry, _):
"""Dispatch verify to the proper method according to type"""
- ret = getattr(self, 'Verify%s' % entry.get('type'))
- return ret(entry, _)
+ ret = getattr(self, 'Verify%s' % entry.get('type'))(entry, _)
+ if entry.get('qtext') and self.setup['interactive']:
+ entry.set('qtext',
+ '%s\nInstall %s %s: (y/N) ' %
+ (entry.get('qtext'),
+ entry.get('type'), entry.get('name')))
+ return ret
+
+ def _verify_metadata(self, entry, path=None, checkonly=False):
+ """ generic method to verify perms, owner, group, secontext,
+ and mtime """
+
+ # allow setting an alternate path for recursive permissions checking
+ if path is None:
+ path = entry.get('name')
+
+ while len(entry.get('perms', '')) < 4:
+ entry.set('perms', '0' + entry.get('perms', ''))
+
+ try:
+ ondisk = os.stat(path)
+ except OSError:
+ entry.set('current_exists', 'false')
+ self.logger.debug("POSIX: %s %s does not exist" %
+ (entry.tag, path))
+ return False
+
+ try:
+ owner = str(ondisk[stat.ST_UID])
+ group = str(ondisk[stat.ST_GID])
+ except (OSError, KeyError):
+ self.logger.error('POSIX: User/Group resolution failed for path %s'
+ % path)
+ owner = 'root'
+ group = '0'
+
+ perms = oct(ondisk[stat.ST_MODE])[-4:]
+ if entry.get('mtime', '-1') != '-1':
+ mtime = str(ondisk[stat.ST_MTIME])
+ else:
+ mtime = '-1'
+
+ configOwner = str(normUid(entry))
+ configGroup = str(normGid(entry))
+ configPerms = int(entry.get('perms'), 8)
+ if entry.get('dev_type'):
+ configPerms |= device_map[entry.get('dev_type')]
+ if has_selinux:
+ if entry.get("secontext") == "__default__":
+ configContext = selinux.matchpathcon(path, 0)[1]
+ else:
+ configContext = entry.get("secontext")
+
+ errors = []
+ if owner != configOwner:
+ if checkonly:
+ return False
+ entry.set('current_owner', owner)
+ errors.append("POSIX: Owner for path %s is incorrect. "
+ "Current owner is %s but should be %s" %
+ (path, ondisk.st_uid, entry.get('owner')))
+
+ if group != configGroup:
+ if checkonly:
+ return False
+ entry.set('current_group', group)
+ errors.append("POSIX: Group for path %s is incorrect. "
+ "Current group is %s but should be %s" %
+ (path, ondisk.st_gid, entry.get('group')))
+
+ if oct(int(perms, 8)) != oct(configPerms):
+ if checkonly:
+ return False
+ entry.set('current_perms', perms)
+ errors.append("POSIX: Permissions for path %s are incorrect. "
+ "Current permissions are %s but should be %s" %
+ (path, perms, entry.get('perms')))
+
+ if entry.get('mtime') and mtime != entry.get('mtime', '-1'):
+ if checkonly:
+ return False
+ entry.set('current_mtime', mtime)
+ errors.append("POSIX: mtime for path %s is incorrect. "
+ "Current mtime is %s but should be %s" %
+ (path, mtime, entry.get('mtime')))
+
+ seVerifies = self._verify_secontext(entry)
+
+ if errors:
+ for error in errors:
+ self.logger.debug(error)
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return False
+ else:
+ return seVerifies
+
+ def _verify_secontext(self, entry):
+ if not secontextMatches(entry):
+ path = entry.get("name")
+ if entry.get("secontext") == "__default__":
+ configContext = selinux.matchpathcon(path, 0)[1]
+ else:
+ configContext = entry.get("secontext")
+ pcontext = selinux.getfilecon(path)[1]
+ entry.set('current_secontext', pcontext)
+ msg = ("SELinux context for path %s is incorrect. "
+ "Current context is %s but should be %s" %
+ (path, pcontext, configContext))
+ self.logger.debug(msg)
+ entry.set('qtext', "\n".join([entry.get("qtext", ''), msg]))
+ return False
+ return True
+
+ def _diff(self, content1, content2, difffunc, filename=None):
+ rv = []
+ start = time.time()
+ longtime = False
+ for diffline in difffunc(content1.split('\n'),
+ content2.split('\n')):
+ now = time.time()
+ rv.append(diffline)
+ if now - start > 5 and not longtime:
+ if filename:
+ self.logger.info("Diff of %s taking a long time" %
+ filename)
+ else:
+ self.logger.info("Diff taking a long time")
+ longtime = True
+ elif now - start > 30:
+ if filename:
+ self.logger.error("Diff of %s took too long; giving up" %
+ filename)
+ else:
+ self.logger.error("Diff took too long; giving up")
+ return False
+ return rv
diff --git a/src/lib/Bcfg2/Client/Tools/SELinux.py b/src/lib/Bcfg2/Client/Tools/SELinux.py
new file mode 100644
index 000000000..1c0db904b
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/SELinux.py
@@ -0,0 +1,716 @@
+import os
+import re
+import sys
+import copy
+import glob
+import struct
+import socket
+import selinux
+import seobject
+import Bcfg2.Client.XML
+import Bcfg2.Client.Tools
+import Bcfg2.Client.Tools.POSIX
+
+def pack128(int_val):
+ """ pack a 128-bit integer in big-endian format """
+ max_int = 2 ** (128) - 1
+ max_word_size = 2 ** 32 - 1
+
+ if int_val <= max_word_size:
+ return struct.pack('>L', int_val)
+
+ words = []
+ for i in range(4):
+ word = int_val & max_word_size
+ words.append(int(word))
+ int_val >>= 32
+ words.reverse()
+ return struct.pack('>4I', *words)
+
+def netmask_itoa(netmask, proto="ipv4"):
+ """ convert an integer netmask (e.g., /16) to dotted-quad
+ notation (255.255.0.0) or IPv6 prefix notation (ffff::) """
+ if proto == "ipv4":
+ size = 32
+ family = socket.AF_INET
+ else: # ipv6
+ size = 128
+ family = socket.AF_INET6
+ try:
+ int(netmask)
+ except ValueError:
+ return netmask
+
+ if netmask > size:
+ raise ValueError("Netmask too large: %s" % netmask)
+
+ res = 0L
+ for n in range(netmask):
+ res |= 1 << (size - n - 1)
+ netmask = socket.inet_ntop(family, pack128(res))
+ return netmask
+
+
+class SELinux(Bcfg2.Client.Tools.Tool):
+ """ SELinux boolean and module support """
+ name = 'SELinux'
+ __handles__ = [('SELinux', 'boolean'),
+ ('SELinux', 'port'),
+ ('SELinux', 'fcontext'),
+ ('SELinux', 'node'),
+ ('SELinux', 'login'),
+ ('SELinux', 'user'),
+ ('SELinux', 'interface'),
+ ('SELinux', 'permissive'),
+ ('SELinux', 'module')]
+ __req__ = dict(SELinux=dict(boolean=['name', 'value'],
+ module=['name'],
+ port=['name', 'selinuxtype'],
+ fcontext=['name', 'selinuxtype'],
+ node=['name', 'selinuxtype', 'proto'],
+ login=['name', 'selinuxuser'],
+ user=['name', 'roles', 'prefix'],
+ interface=['name', 'selinuxtype'],
+ permissive=['name']))
+
+ def __init__(self, logger, setup, config):
+ Bcfg2.Client.Tools.Tool.__init__(self, logger, setup, config)
+ self.handlers = {}
+ for handles in self.__handles__:
+ etype = handles[1]
+ self.handlers[etype] = \
+ globals()["SELinux%sHandler" % etype.title()](self, logger,
+ setup, config)
+
+ def BundleUpdated(self, _, states):
+ for handler in self.handlers.values():
+ handler.BundleUpdated(states)
+
+ def FindExtra(self):
+ extra = []
+ for handler in self.handlers.values():
+ extra.extend(handler.FindExtra())
+ return extra
+
+ def canInstall(self, entry):
+ return (Bcfg2.Client.Tools.Tool.canInstall(self, entry) and
+ self.handlers[entry.get('type')].canInstall(entry))
+
+ def primarykey(self, entry):
+ """ return a string that should be unique amongst all entries
+ in the specification """
+ return self.handlers[entry.get('type')].primarykey(entry)
+
+ def Install(self, entries, states):
+ # start a transaction
+ sr = seobject.semanageRecords("")
+ if hasattr(sr, "start"):
+ self.logger.debug("Starting SELinux transaction")
+ sr.start()
+ else:
+ self.logger.debug("SELinux transactions not supported; this may "
+ "slow things down considerably")
+ Bcfg2.Client.Tools.Tool.Install(self, entries, states)
+ if hasattr(sr, "finish"):
+ self.logger.debug("Committing SELinux transaction")
+ sr.finish()
+
+ def InstallSELinux(self, entry):
+ """Dispatch install to the proper method according to type"""
+ return self.handlers[entry.get('type')].Install(entry)
+
+ def VerifySELinux(self, entry, _):
+ """Dispatch verify to the proper method according to type"""
+ rv = self.handlers[entry.get('type')].Verify(entry)
+ if entry.get('qtext') and self.setup['interactive']:
+ entry.set('qtext',
+ '%s\nInstall SELinux %s %s: (y/N) ' %
+ (entry.get('qtext'),
+ entry.get('type'),
+ self.handlers[entry.get('type')].tostring(entry)))
+ return rv
+
+ def Remove(self, entries):
+ """Dispatch verify to the proper removal method according to type"""
+ # sort by type
+ types = list()
+ for entry in entries:
+ if entry.get('type') not in types:
+ types.append(entry.get('type'))
+
+ for etype in types:
+ self.handlers[entry.get('type')].Remove([e for e in entries
+ if e.get('type') == etype])
+
+
+class SELinuxEntryHandler(object):
+ etype = None
+ key_format = ("name",)
+ value_format = ()
+ str_format = '%(name)s'
+ custom_re = re.compile(' (?P<name>\S+)$')
+ custom_format = None
+
+ def __init__(self, tool, logger, setup, config):
+ self.tool = tool
+ self.logger = logger
+ self._records = None
+ self._all = None
+ if not self.custom_format:
+ self.custom_format = self.key_format
+
+ @property
+ def records(self):
+ if self._records is None:
+ self._records = getattr(seobject, "%sRecords" % self.etype)("")
+ return self._records
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ self._all = self.records.get_all()
+ return self._all
+
+ @property
+ def custom_records(self):
+ if hasattr(self.records, "customized") and self.custom_re:
+ return dict([(k, self.all_records[k]) for k in self.custom_keys])
+ else:
+ # ValueError is really a pretty dumb exception to raise,
+ # but that's what the seobject customized() method raises
+ # if it's defined but not implemented. yeah, i know, wtf.
+ raise ValueError("custom_records")
+
+ @property
+ def custom_keys(self):
+ keys = []
+ for cmd in self.records.customized():
+ match = self.custom_re.search(cmd)
+ if match:
+ if (len(self.custom_format) == 1 and
+ self.custom_format[0] == "name"):
+ keys.append(match.group("name"))
+ else:
+ keys.append(tuple([match.group(k)
+ for k in self.custom_format]))
+ return keys
+
+ def tostring(self, entry):
+ return self.str_format % entry.attrib
+
+ def keytostring(self, key):
+ return self.str_format % self._key2attrs(key)
+
+ def _key(self, entry):
+ if len(self.key_format) == 1 and self.key_format[0] == "name":
+ return entry.get("name")
+ else:
+ rv = []
+ for key in self.key_format:
+ rv.append(entry.get(key))
+ return tuple(rv)
+
+ def _key2attrs(self, key):
+ if isinstance(key, tuple):
+ rv = dict((self.key_format[i], key[i])
+ for i in range(len(self.key_format))
+ if self.key_format[i])
+ else:
+ rv = dict(name=key)
+ if self.value_format:
+ vals = self.all_records[key]
+ rv.update(dict((self.value_format[i], vals[i])
+ for i in range(len(self.value_format))
+ if self.value_format[i]))
+ return rv
+
+ def key2entry(self, key):
+ attrs = self._key2attrs(key)
+ attrs["type"] = self.etype
+ return Bcfg2.Client.XML.Element("SELinux", **attrs)
+
+ def _args(self, entry, method):
+ if hasattr(self, "_%sargs" % method):
+ return getattr(self, "_%sargs" % method)(entry)
+ elif hasattr(self, "_defaultargs"):
+ # default args
+ return self._defaultargs(entry)
+ else:
+ raise NotImplementedError
+
+ def _deleteargs(self, entry):
+ return (self._key(entry))
+
+ def canInstall(self, entry):
+ return bool(self._key(entry))
+
+ def primarykey(self, entry):
+ return ":".join([entry.tag, entry.get("type"), entry.get("name")])
+
+ def exists(self, entry):
+ if self._key(entry) not in self.all_records:
+ self.logger.debug("SELinux %s %s does not exist" %
+ (self.etype, self.tostring(entry)))
+ return False
+ return True
+
+ def Verify(self, entry):
+ if not self.exists(entry):
+ entry.set('current_exists', 'false')
+ return False
+
+ errors = []
+ current_attrs = self._key2attrs(self._key(entry))
+ desired_attrs = entry.attrib
+ for attr in self.value_format:
+ if not attr:
+ continue
+ if current_attrs[attr] != desired_attrs[attr]:
+ entry.set('current_%s' % attr, current_attrs[attr])
+ errors.append("SELinux %s %s has wrong %s: %s, should be %s" %
+ (self.etype, self.tostring(entry), attr,
+ current_attrs[attr], desired_attrs[attr]))
+
+ if errors:
+ for error in errors:
+ self.logger.debug(error)
+ entry.set('qtext', "\n".join([entry.get('qtext', '')] + errors))
+ return False
+ else:
+ return True
+
+ def Install(self, entry, method=None):
+ if not method:
+ if self.exists(entry):
+ method = "modify"
+ else:
+ method = "add"
+ self.logger.debug("%s SELinux %s %s" %
+ (method.title(), self.etype, self.tostring(entry)))
+
+ try:
+ getattr(self.records, method)(*self._args(entry, method))
+ self._all = None
+ return True
+ except ValueError:
+ err = sys.exc_info()[1]
+ self.logger.debug("Failed to %s SELinux %s %s: %s" %
+ (method, self.etype, self.tostring(entry), err))
+ return False
+
+ def Remove(self, entries):
+ for entry in entries:
+ try:
+ self.records.delete(*self._args(entry, "delete"))
+ self._all = None
+ except ValueError:
+ err = sys.exc_info()[1]
+ self.logger.info("Failed to remove SELinux %s %s: %s" %
+ (self.etype, self.tostring(entry), err))
+
+ def FindExtra(self):
+ specified = [self._key(e)
+ for e in self.tool.getSupportedEntries()
+ if e.get("type") == self.etype]
+ try:
+ records = self.custom_records
+ except ValueError:
+ records = self.all_records
+ return [self.key2entry(key)
+ for key in records.keys()
+ if key not in specified]
+
+ def BundleUpdated(self, states):
+ pass
+
+
+class SELinuxBooleanHandler(SELinuxEntryHandler):
+ etype = "boolean"
+ value_format = ("value",)
+
+ @property
+ def all_records(self):
+ # older versions of selinux return a single 0/1 value for each
+ # bool, while newer versions return a list of three 0/1 values
+ # representing various states. we don't care about the latter
+ # two values, but it's easier to coerce the older format into
+ # the newer format as far as interoperation with the rest of
+ # SELinuxEntryHandler goes
+ rv = SELinuxEntryHandler.all_records.fget(self)
+ if rv.values()[0] in [0, 1]:
+ for key, val in rv.items():
+ rv[key] = [val, val, val]
+ return rv
+
+ def _key2attrs(self, key):
+ rv = SELinuxEntryHandler._key2attrs(self, key)
+ status = self.all_records[key][0]
+ if status:
+ rv['value'] = "on"
+ else:
+ rv['value'] = "off"
+ return rv
+
+ def _defaultargs(self, entry):
+ # the only values recognized by both new and old versions of
+ # selinux are the strings "0" and "1". old selinux accepts
+ # ints or bools as well, new selinux accepts "on"/"off"
+ if entry.get("value").lower() == "on":
+ value = "1"
+ else:
+ value = "0"
+ return (entry.get("name"), value)
+
+ def canInstall(self, entry):
+ if entry.get("value").lower() not in ["on", "off"]:
+ self.logger.debug("SELinux %s %s has a bad value: %s" %
+ (self.etype, self.tostring(entry),
+ entry.get("value")))
+ return False
+ return (self.exists(entry) and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+
+class SELinuxPortHandler(SELinuxEntryHandler):
+ etype = "port"
+ value_format = ('selinuxtype', None)
+ custom_re = re.compile(r'-p (?P<proto>tcp|udp).*? (?P<start>\d+)(?:-(?P<end>\d+))?$')
+
+ @property
+ def custom_keys(self):
+ keys = []
+ for cmd in self.records.customized():
+ match = self.custom_re.search(cmd)
+ if match:
+ if match.group('end'):
+ keys.append((int(match.group('start')),
+ int(match.group('end')),
+ match.group('proto')))
+ else:
+ keys.append((int(match.group('start')),
+ int(match.group('start')),
+ match.group('proto')))
+ return keys
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # older versions of selinux use (startport, endport) as
+ # they key for the ports.get_all() dict, and (type, proto,
+ # level) as the value; this is obviously broken, so newer
+ # versions use (startport, endport, proto) as the key, and
+ # (type, level) as the value. abstracting around this
+ # sucks.
+ ports = self.records.get_all()
+ if len(ports.keys()[0]) == 3:
+ self._all = ports
+ else:
+ # uglist list comprehension ever?
+ self._all = dict([((k[0], k[1], v[1]), (v[0], v[2]))
+ for k, v in ports.items()])
+ return self._all
+
+ def _key(self, entry):
+ try:
+ (port, proto) = entry.get("name").split("/")
+ except ValueError:
+ self.logger.error("Invalid SELinux node %s: no protocol specified" %
+ entry.get("name"))
+ return
+ if "-" in port:
+ start, end = port.split("-")
+ else:
+ start = port
+ end = port
+ return (int(start), int(end), proto)
+
+ def _key2attrs(self, key):
+ if key[0] == key[1]:
+ port = str(key[0])
+ else:
+ port = "%s-%s" % (key[0], key[1])
+ vals = self.all_records[key]
+ return dict(name="%s/%s" % (port, key[2]), selinuxtype=vals[0])
+
+ def _defaultargs(self, entry):
+ (port, proto) = entry.get("name").split("/")
+ return (port, proto, '', entry.get("selinuxtype"))
+
+ def _deleteargs(self, entry):
+ return tuple(entry.get("name").split("/"))
+
+
+class SELinuxFcontextHandler(SELinuxEntryHandler):
+ etype = "fcontext"
+ key_format = ("name", "filetype")
+ value_format = (None, None, "selinuxtype", None)
+ filetypeargs = dict(all="",
+ regular="--",
+ directory="-d",
+ symlink="-l",
+ pipe="-p",
+ socket="-s",
+ block="-b",
+ char="-c",
+ door="-D")
+ filetypenames = dict(all="all files",
+ regular="regular file",
+ directory="directory",
+ symlink="symbolic link",
+ pipe="named pipe",
+ socket="socket",
+ block="block device",
+ char="character device",
+ door="door")
+ filetypeattrs = dict([v, k] for k, v in filetypenames.iteritems())
+ custom_re = re.compile(r'-f \'(?P<filetype>[a-z ]+)\'.*? \'(?P<name>.*)\'')
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # on older selinux, fcontextRecords.get_all() returns a
+ # list of tuples of (filespec, filetype, seuser, serole,
+ # setype, level); on newer selinux, get_all() returns a
+ # dict of (filespec, filetype) => (seuser, serole, setype,
+ # level).
+ fcontexts = self.records.get_all()
+ if isinstance(fcontexts, dict):
+ self._all = fcontexts
+ else:
+ self._all = dict([(f[0:2], f[2:]) for f in fcontexts])
+ return self._all
+
+ def _key(self, entry):
+ ftype = entry.get("filetype", "all")
+ return (entry.get("name"),
+ self.filetypenames.get(ftype, ftype))
+
+ def _key2attrs(self, key):
+ rv = dict(name=key[0], filetype=self.filetypeattrs[key[1]])
+ vals = self.all_records[key]
+ # in older versions of selinux, an fcontext with no selinux
+ # type is the single value None; in newer versions, it's a
+ # tuple whose 0th (and only) value is None.
+ if vals and vals[0]:
+ rv["selinuxtype"] = vals[2]
+ else:
+ rv["selinuxtype"] = "<<none>>"
+ return rv
+
+ def canInstall(self, entry):
+ return (entry.get("filetype", "all") in self.filetypeargs and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), entry.get("selinuxtype"),
+ self.filetypeargs[entry.get("filetype", "all")],
+ '', '')
+
+ def primarykey(self, entry):
+ return ":".join([entry.tag, entry.get("type"), entry.get("name"),
+ entry.get("filetype", "all")])
+
+
+class SELinuxNodeHandler(SELinuxEntryHandler):
+ etype = "node"
+ value_format = (None, None, "selinuxtype", None)
+ str_format = '%(name)s (%(proto)s)'
+ custom_re = re.compile(r'-M (?P<netmask>\S+).*?-p (?P<proto>ipv\d).*? (?P<addr>\S+)$')
+ custom_format = ('addr', 'netmask', 'proto')
+
+ def _key(self, entry):
+ try:
+ (addr, netmask) = entry.get("name").split("/")
+ except ValueError:
+ self.logger.error("Invalid SELinux node %s: no netmask specified" %
+ entry.get("name"))
+ return
+ netmask = netmask_itoa(netmask, proto=entry.get("proto"))
+ return (addr, netmask, entry.get("proto"))
+
+ def _key2attrs(self, key):
+ vals = self.all_records[key]
+ return dict(name="%s/%s" % (key[0], key[1]), proto=key[2],
+ selinuxtype=vals[2])
+
+ def _defaultargs(self, entry):
+ (addr, netmask) = entry.get("name").split("/")
+ return (addr, netmask, entry.get("proto"), "", entry.get("selinuxtype"))
+
+
+class SELinuxLoginHandler(SELinuxEntryHandler):
+ etype = "login"
+ value_format = ("selinuxuser", None)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), entry.get("selinuxuser"), "")
+
+
+class SELinuxUserHandler(SELinuxEntryHandler):
+ etype = "user"
+ value_format = ("prefix", None, None, "roles")
+
+ def __init__(self, tool, logger, setup, config):
+ SELinuxEntryHandler.__init__(self, tool, logger, setup, config)
+ self.needs_prefix = False
+
+ @property
+ def records(self):
+ if self._records is None:
+ self._records = seobject.seluserRecords()
+ return self._records
+
+ def Install(self, entry):
+ # in older versions of selinux, modify() is broken if you
+ # provide a prefix _at all_, so we try to avoid giving the
+ # prefix. however, in newer versions, prefix is _required_,
+ # so we a) try without a prefix; b) catch TypeError, which
+ # indicates that we had the wrong number of args (ValueError
+ # is thrown by the bug in older versions of selinux); and c)
+ # try with prefix.
+ try:
+ SELinuxEntryHandler.Install(self, entry)
+ except TypeError:
+ self.needs_prefix = True
+ SELinuxEntryHandler.Install(self, entry)
+
+ def _defaultargs(self, entry):
+ # in older versions of selinux, modify() is broken if you
+ # provide a prefix _at all_, so we try to avoid giving the
+ # prefix. see the comment in Install() above for more
+ # details.
+ rv = [entry.get("name"),
+ entry.get("roles", "").replace(" ", ",").split(",")]
+ if self.needs_prefix:
+ rv.extend(['', '', entry.get("prefix")])
+ else:
+ key = self._key(entry)
+ if key in self.all_records:
+ attrs = self._key2attrs(key)
+ if attrs['prefix'] != entry.get("prefix"):
+ rv.extend(['', '', entry.get("prefix")])
+ return tuple(rv)
+
+
+class SELinuxInterfaceHandler(SELinuxEntryHandler):
+ etype = "interface"
+ value_format = (None, None, "selinuxtype", None)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"), '', entry.get("selinuxtype"))
+
+
+class SELinuxPermissiveHandler(SELinuxEntryHandler):
+ etype = "permissive"
+
+ @property
+ def records(self):
+ try:
+ return SELinuxEntryHandler.records.fget(self)
+ except AttributeError:
+ self.logger.info("Permissive domains not supported by this version "
+ "of SELinux")
+ self._records = False
+ return self._records
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ if self.records == False:
+ self._all = dict()
+ else:
+ # permissiveRecords.get_all() returns a list, so we just
+ # make it into a dict so that the rest of
+ # SELinuxEntryHandler works
+ self._all = dict([(d, d) for d in self.records.get_all()])
+ return self._all
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"),)
+
+
+class SELinuxModuleHandler(SELinuxEntryHandler):
+ etype = "module"
+ value_format = (None, "disabled")
+
+ def __init__(self, tool, logger, setup, config):
+ SELinuxEntryHandler.__init__(self, tool, logger, setup, config)
+ self.posixtool = Bcfg2.Client.Tools.POSIX.POSIX(logger, setup, config)
+ try:
+ self.setype = selinux.selinux_getpolicytype()[1]
+ except IndexError:
+ self.logger.error("Unable to determine SELinux policy type")
+ self.setype = None
+
+ @property
+ def all_records(self):
+ if self._all is None:
+ # we get a list of tuples back; coerce it into a dict
+ self._all = dict([(m[0], (m[1], m[2]))
+ for m in self.records.get_all()])
+ return self._all
+
+ def _key2attrs(self, key):
+ rv = SELinuxEntryHandler._key2attrs(self, key)
+ status = self.all_records[key][1]
+ if status:
+ rv['disabled'] = "false"
+ else:
+ rv['disabled'] = "true"
+ return rv
+
+ def _filepath(self, entry):
+ return os.path.join("/usr/share/selinux", self.setype,
+ "%s.pp" % entry.get("name"))
+
+ def _pathentry(self, entry):
+ pathentry = copy.deepcopy(entry)
+ pathentry.set("name", self._filepath(pathentry))
+ pathentry.set("perms", "0644")
+ pathentry.set("owner", "root")
+ pathentry.set("group", "root")
+ pathentry.set("secontext", "__default__")
+ return pathentry
+
+ def Verify(self, entry):
+ if not entry.get("disabled"):
+ entry.set("disabled", "false")
+ return (SELinuxEntryHandler.Verify(self, entry) and
+ self.posixtool.Verifyfile(self._pathentry(entry), None))
+
+ def canInstall(self, entry):
+ return (entry.text and self.setype and
+ SELinuxEntryHandler.canInstall(self, entry))
+
+ def Install(self, entry):
+ rv = self.posixtool.Installfile(self._pathentry(entry))
+ try:
+ rv = rv and SELinuxEntryHandler.Install(self, entry)
+ except NameError:
+ # some versions of selinux have a bug in seobject that
+ # makes modify() calls fail. add() seems to have the same
+ # effect as modify, but without the bug
+ if self.exists(entry):
+ rv = rv and SELinuxEntryHandler.Install(self, entry,
+ method="add")
+
+ if entry.get("disabled", "false").lower() == "true":
+ method = "disable"
+ else:
+ method = "enable"
+ return rv and SELinuxEntryHandler.Install(self, entry, method=method)
+
+ def _addargs(self, entry):
+ return (self._filepath(entry),)
+
+ def _defaultargs(self, entry):
+ return (entry.get("name"),)
+
+ def FindExtra(self):
+ specified = [self._key(e)
+ for e in self.tool.getSupportedEntries()
+ if e.get("type") == self.etype]
+ return [self.key2entry(os.path.basename(f)[:-3])
+ for f in glob.glob(os.path.join("/usr/share/selinux",
+ self.setype, "*.pp"))
+ if f not in specified]
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py
index d423b6380..e4a0ec220 100644
--- a/src/lib/Bcfg2/Client/Tools/__init__.py
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -137,6 +137,18 @@ class Tool:
"""Default implementation of the information gathering routines."""
pass
+ def missing_attrs(self, entry):
+ required = self.__req__[entry.tag]
+ if isinstance(required, dict):
+ required = ["type"]
+ try:
+ required.extend(self.__req__[entry.tag][entry.get("type")])
+ except KeyError:
+ pass
+
+ return [attr for attr in required
+ if attr not in entry.attrib or not entry.attrib[attr]]
+
def canVerify(self, entry):
"""Test if entry has enough information to be verified."""
if not self.handlesEntry(entry):
@@ -149,8 +161,7 @@ class Tool:
entry.get('failure')))
return False
- missing = [attr for attr in self.__req__[entry.tag] \
- if attr not in entry.attrib]
+ missing = self.missing_attrs(entry)
if missing:
self.logger.error("Incomplete information for entry %s:%s; cannot verify" \
% (entry.tag, entry.get('name')))
@@ -168,6 +179,11 @@ class Tool:
"""Return a list of extra entries."""
return []
+ def primarykey(self, entry):
+ """ return a string that should be unique amongst all entries
+ in the specification """
+ return "%s:%s" % (entry.tag, entry.get("name"))
+
def canInstall(self, entry):
"""Test if entry has enough information to be installed."""
if not self.handlesEntry(entry):
@@ -178,8 +194,7 @@ class Tool:
(entry.tag, entry.get('name')))
return False
- missing = [attr for attr in self.__ireq__[entry.tag] \
- if attr not in entry.attrib or not entry.attrib[attr]]
+ missing = self.missing_attrs(entry)
if missing:
self.logger.error("Incomplete information for entry %s:%s; cannot install" \
% (entry.tag, entry.get('name')))
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py
index bbbbec343..803b2755d 100644
--- a/src/lib/Bcfg2/Options.py
+++ b/src/lib/Bcfg2/Options.py
@@ -330,6 +330,11 @@ MDATA_PERMS = \
default='644',
odesc='octal permissions',
cf=('mdata', 'perms'))
+MDATA_SECONTEXT = \
+ Option('Default SELinux context',
+ default='__default__',
+ odesc='SELinux context',
+ cf=('mdata', 'secontext'))
MDATA_PARANOID = \
Option('Default Path paranoid setting',
default='true',
@@ -836,6 +841,41 @@ DRIVER_OPTIONS = \
yumng_verify_fail_action=CLIENT_YUMNG_VERIFY_FAIL_ACTION,
yumng_verify_flags=CLIENT_YUMNG_VERIFY_FLAGS)
+CLIENT_COMMON_OPTIONS = \
+ dict(extra=CLIENT_EXTRA_DISPLAY,
+ quick=CLIENT_QUICK,
+ lockfile=LOCKFILE,
+ drivers=CLIENT_DRIVERS,
+ dryrun=CLIENT_DRYRUN,
+ paranoid=CLIENT_PARANOID,
+ bundle=CLIENT_BUNDLE,
+ skipbundle=CLIENT_SKIPBUNDLE,
+ bundle_quick=CLIENT_BUNDLEQUICK,
+ indep=CLIENT_INDEP,
+ skipindep=CLIENT_SKIPINDEP,
+ file=CLIENT_FILE,
+ interactive=INTERACTIVE,
+ cache=CLIENT_CACHE,
+ profile=CLIENT_PROFILE,
+ remove=CLIENT_REMOVE,
+ server=SERVER_LOCATION,
+ user=CLIENT_USER,
+ password=SERVER_PASSWORD,
+ retries=CLIENT_RETRIES,
+ kevlar=CLIENT_KEVLAR,
+ omit_lock_check=OMIT_LOCK_CHECK,
+ decision=CLIENT_DLIST,
+ servicemode=CLIENT_SERVICE_MODE,
+ key=CLIENT_KEY,
+ certificate=CLIENT_CERT,
+ ca=CLIENT_CA,
+ serverCN=CLIENT_SCNS,
+ timeout=CLIENT_TIMEOUT,
+ decision_list=CLIENT_DECISION_LIST)
+CLIENT_COMMON_OPTIONS.update(DRIVER_OPTIONS)
+CLIENT_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS)
+
+
class OptionParser(OptionSet):
"""
OptionParser bootstraps option parsing,
diff --git a/src/lib/Bcfg2/Server/Admin/Compare.py b/src/lib/Bcfg2/Server/Admin/Compare.py
index 050dd69f8..78b30120a 100644
--- a/src/lib/Bcfg2/Server/Admin/Compare.py
+++ b/src/lib/Bcfg2/Server/Admin/Compare.py
@@ -18,7 +18,8 @@ class Compare(Bcfg2.Server.Admin.Mode):
'important', 'paranoid', 'sensitive',
'dev_type', 'major', 'minor', 'prune',
'encoding', 'empty', 'to', 'recursive',
- 'vcstype', 'sourceurl', 'revision'],
+ 'vcstype', 'sourceurl', 'revision',
+ 'secontext'],
'Package': ['name', 'type', 'version', 'simplefile',
'verify'],
'Service': ['name', 'type', 'status', 'mode',
diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
index 6f76cf2db..0a369c841 100644
--- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
+++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
@@ -1,50 +1,114 @@
-import os.path
+import os
+import re
import lxml.etree
import Bcfg2.Server.Lint
+import Bcfg2.Client.Tools.POSIX
+import Bcfg2.Client.Tools.VCS
from Bcfg2.Server.Plugins.Packages import Apt, Yum
+# format verifying functions
+def is_filename(val):
+ return val.startswith("/") and len(val) > 1
+
+def is_selinux_type(val):
+ return re.match(r'^[a-z_]+_t', val)
+
+def is_selinux_user(val):
+ return re.match(r'^[a-z_]+_u', val)
+
+def is_octal_mode(val):
+ return re.match(r'[0-7]{3,4}', val)
+
+def is_username(val):
+ return re.match(r'^([a-z]\w{0,30}|\d+)$', val)
+
+def is_device_mode(val):
+ try:
+ # checking upper bound seems like a good way to discover some
+ # obscure OS with >8-bit device numbers
+ return int(val) > 0
+ except:
+ return False
+
class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
""" verify attributes for configuration entries (as defined in
doc/server/configurationentries) """
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
- self.required_attrs = {
- 'Path': {
- 'device': ['name', 'owner', 'group', 'dev_type'],
- 'directory': ['name', 'owner', 'group', 'perms'],
- 'file': ['name', 'owner', 'group', 'perms', '__text__'],
- 'hardlink': ['name', 'to'],
- 'symlink': ['name', 'to'],
- 'ignore': ['name'],
- 'nonexistent': ['name'],
- 'permissions': ['name', 'owner', 'group', 'perms'],
- 'vcs': ['vcstype', 'revision', 'sourceurl']},
- 'Service': {
- 'chkconfig': ['name'],
- 'deb': ['name'],
- 'rc-update': ['name'],
- 'smf': ['name', 'FMRI'],
- 'upstart': ['name']},
- 'Action': ['name', 'timing', 'when', 'status', 'command'],
- 'Package': ['name']}
+ self.required_attrs = dict(
+ Path=dict(
+ device=dict(name=is_filename, owner=is_username,
+ group=is_username,
+ dev_type=lambda v: \
+ v in Bcfg2.Client.Tools.POSIX.device_map),
+ directory=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode),
+ file=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode,
+ __text__=None),
+ hardlink=dict(name=is_filename, to=is_filename),
+ symlink=dict(name=is_filename, to=is_filename),
+ ignore=dict(name=is_filename),
+ nonexistent=dict(name=is_filename),
+ permissions=dict(name=is_filename, owner=is_username,
+ group=is_username, perms=is_octal_mode),
+ vcs=dict(vcstype=lambda v: (v != 'Path' and
+ hasattr(Bcfg2.Client.Tools.VCS,
+ "Install%s" % v)),
+ revision=None, sourceurl=None)),
+ Service={
+ "chkconfig": dict(name=None),
+ "deb": dict(name=None),
+ "rc-update": dict(name=None),
+ "smf": dict(name=None, FMRI=None),
+ "upstart": dict(name=None)},
+ Action={None: dict(name=None,
+ timing=lambda v: v in ['pre', 'post', 'both'],
+ when=lambda v: v in ['modified', 'always'],
+ status=lambda v: v in ['ignore', 'check'],
+ command=None)},
+ Package={None: dict(name=None)},
+ SELinux=dict(
+ boolean=dict(name=None,
+ value=lambda v: v in ['on', 'off']),
+ module=dict(name=None, __text__=None),
+ port=dict(name=lambda v: re.match(r'^\d+(-\d+)?/(tcp|udp)', v),
+ selinuxtype=is_selinux_type),
+ fcontext=dict(name=None, selinuxtype=is_selinux_type),
+ node=dict(name=lambda v: "/" in v,
+ selinuxtype=is_selinux_type,
+ proto=lambda v: v in ['ipv6', 'ipv4']),
+ login=dict(name=is_username,
+ selinuxuser=is_selinux_user),
+ user=dict(name=is_selinux_user,
+ roles=lambda v: all(is_selinux_user(u)
+ for u in " ".split(v)),
+ prefix=None),
+ interface=dict(name=None, selinuxtype=is_selinux_type),
+ permissive=dict(name=is_selinux_type))
+ )
def Run(self):
+ print "checking packages\n"
self.check_packages()
if "Defaults" in self.core.plugins:
self.logger.info("Defaults plugin enabled; skipping required "
"attribute checks")
else:
+ print "checking rules\n"
self.check_rules()
+ print "checking bundles\n"
self.check_bundles()
+ print 'done running RequiredAttrs'
@classmethod
def Errors(cls):
return {"unknown-entry-type":"error",
"unknown-entry-tag":"error",
"required-attrs-missing":"error",
+ "required-attr-format":"error",
"extra-attrs":"warning"}
-
def check_packages(self):
""" check package sources for Source entries with missing attrs """
if 'Packages' in self.core.plugins:
@@ -85,6 +149,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
""" check bundles for BoundPath entries with missing attrs """
if 'Bundler' in self.core.plugins:
for bundle in self.core.plugins['Bundler'].entries.values():
+ print "checking bundle %s" % bundle.name
try:
xdata = lxml.etree.XML(bundle.data)
except (lxml.etree.XMLSyntaxError, AttributeError):
@@ -103,43 +168,52 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
if tag not in self.required_attrs:
self.LintError("unknown-entry-tag",
"Unknown entry tag '%s': %s" %
- (entry.tag, self.RenderXML(entry)))
+ (tag, self.RenderXML(entry)))
if isinstance(self.required_attrs[tag], dict):
etype = entry.get('type')
if etype in self.required_attrs[tag]:
- required_attrs = set(self.required_attrs[tag][etype] +
- ['type'])
+ required_attrs = self.required_attrs[tag][etype]
else:
self.LintError("unknown-entry-type",
"Unknown %s type %s: %s" %
(tag, etype, self.RenderXML(entry)))
return
else:
- required_attrs = set(self.required_attrs[tag])
+ required_attrs = self.required_attrs[tag]
attrs = set(entry.attrib.keys())
if 'dev_type' in required_attrs:
dev_type = entry.get('dev_type')
if dev_type in ['block', 'char']:
# check if major/minor are specified
- required_attrs |= set(['major', 'minor'])
+ required_attrs['major'] = is_device_mode
+ required_attrs['minor'] = is_device_mode
if '__text__' in required_attrs:
- required_attrs.remove('__text__')
+ del required_attrs['__text__']
if (not entry.text and
not entry.get('empty', 'false').lower() == 'true'):
self.LintError("required-attrs-missing",
"Text missing for %s %s in %s: %s" %
- (entry.tag, name, filename,
+ (tag, name, filename,
self.RenderXML(entry)))
- if not attrs.issuperset(required_attrs):
+ if not attrs.issuperset(required_attrs.keys()):
self.LintError("required-attrs-missing",
"The following required attribute(s) are "
"missing for %s %s in %s: %s\n%s" %
- (entry.tag, name, filename,
+ (tag, name, filename,
", ".join([attr
for attr in
required_attrs.difference(attrs)]),
self.RenderXML(entry)))
+
+ for attr, fmt in required_attrs.items():
+ if fmt and attr in attrs and not fmt(entry.attrib[attr]):
+ self.LintError("required-attr-format",
+ "The %s attribute of %s %s in %s is "
+ "malformed\n%s" %
+ (attr, tag, name, filename,
+ self.RenderXML(entry)))
+
diff --git a/src/lib/Bcfg2/Server/Plugin.py b/src/lib/Bcfg2/Server/Plugin.py
index 98e6e6f51..d035b83d4 100644
--- a/src/lib/Bcfg2/Server/Plugin.py
+++ b/src/lib/Bcfg2/Server/Plugin.py
@@ -32,8 +32,9 @@ encoding = encparse['encoding']
# grab default metadata info from bcfg2.conf
opts = {'owner': Bcfg2.Options.MDATA_OWNER,
'group': Bcfg2.Options.MDATA_GROUP,
- 'important': Bcfg2.Options.MDATA_IMPORTANT,
'perms': Bcfg2.Options.MDATA_PERMS,
+ 'secontext': Bcfg2.Options.MDATA_SECONTEXT,
+ 'important': Bcfg2.Options.MDATA_IMPORTANT,
'paranoid': Bcfg2.Options.MDATA_PARANOID,
'sensitive': Bcfg2.Options.MDATA_SENSITIVE}
mdata_setup = Bcfg2.Options.OptionParser(opts)
@@ -52,6 +53,7 @@ info_regex = re.compile( \
'owner:(\s)*(?P<owner>\S+)|' +
'paranoid:(\s)*(?P<paranoid>\S+)|' +
'perms:(\s)*(?P<perms>\w+)|' +
+ 'secontext:(\s)*(?P<secontext>\S+)|' +
'sensitive:(\s)*(?P<sensitive>\S+)|')
def bind_info(entry, metadata, infoxml=None, default=default_file_metadata):
@@ -1162,13 +1164,14 @@ class GroupSpool(Plugin, Generator):
filename_pattern = ""
es_child_cls = object
es_cls = EntrySet
+ entry_type = 'Path'
def __init__(self, core, datastore):
Plugin.__init__(self, core, datastore)
Generator.__init__(self)
if self.data[-1] == '/':
self.data = self.data[:-1]
- self.Entries['Path'] = {}
+ self.Entries[self.entry_type] = {}
self.entries = {}
self.handles = {}
self.AddDirectoryMonitor('')
@@ -1185,7 +1188,8 @@ class GroupSpool(Plugin, Generator):
dirpath,
self.es_child_cls,
self.encoding)
- self.Entries['Path'][ident] = self.entries[ident].bind_entry
+ self.Entries[self.entry_type][ident] = \
+ self.entries[ident].bind_entry
if not posixpath.isdir(epath):
# do not pass through directory events
self.entries[ident].handle_event(event)
@@ -1231,7 +1235,7 @@ class GroupSpool(Plugin, Generator):
if fbase in self.entries:
# a directory was deleted
del self.entries[fbase]
- del self.Entries['Path'][fbase]
+ del self.Entries[self.entry_type][fbase]
elif ident in self.entries:
self.entries[ident].handle_event(event)
elif ident not in self.entries:
diff --git a/src/lib/Bcfg2/Server/Plugins/SEModules.py b/src/lib/Bcfg2/Server/Plugins/SEModules.py
new file mode 100644
index 000000000..2059baf60
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Plugins/SEModules.py
@@ -0,0 +1,46 @@
+import os
+import logging
+import binascii
+import posixpath
+
+import Bcfg2.Server.Plugin
+logger = logging.getLogger(__name__)
+
+class SEModuleData(Bcfg2.Server.Plugin.SpecificData):
+ def bind_entry(self, entry, _):
+ entry.set('encoding', 'base64')
+ entry.text = binascii.b2a_base64(self.data)
+
+
+class SEModules(Bcfg2.Server.Plugin.GroupSpool):
+ """ Handle SELinux 'module' entries """
+ name = 'SEModules'
+ __author__ = 'chris.a.st.pierre@gmail.com'
+ es_cls = Bcfg2.Server.Plugin.EntrySet
+ es_child_cls = SEModuleData
+ entry_type = 'SELinux'
+ experimental = True
+
+ def _get_module_name(self, entry):
+ """ GroupSpool stores entries as /foo.pp, but we want people
+ to be able to specify module entries as name='foo' or
+ name='foo.pp', so we put this abstraction in between """
+ if entry.get("name").endswith(".pp"):
+ name = entry.get("name")
+ else:
+ name = entry.get("name") + ".pp"
+ return "/" + name
+
+ def HandlesEntry(self, entry, metadata):
+ if entry.tag in self.Entries and entry.get('type') == 'module':
+ return self._get_module_name(entry) in self.Entries[entry.tag]
+ return Bcfg2.Server.Plugin.GroupSpool.HandlesEntry(self, entry,
+ metadata)
+
+ def HandleEntry(self, entry, metadata):
+ entry.set("name", self._get_module_name(entry))
+ return self.Entries[entry.tag][name](entry, metadata)
+
+ def add_entry(self, event):
+ self.filename_pattern = os.path.basename(event.filename)
+ Bcfg2.Server.Plugin.GroupSpool.add_entry(self, event)