File: //usr/lib/python2.7/site-packages/salt/modules/inspectlib/query.py
# -*- coding: utf-8 -*-
#
# Copyright 2015 SUSE LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Import Python Libs
from __future__ import absolute_import
import os
import time
import logging
# Import Salt Libs
import salt.utils.files
import salt.utils.fsutils
import salt.utils.network
from salt.modules.inspectlib.exceptions import (InspectorQueryException, SIException)
from salt.modules.inspectlib import EnvLoader
from salt.modules.inspectlib.entities import (Package, PackageCfgFile, PayloadFile)
log = logging.getLogger(__name__)
class SysInfo(object):
'''
System information.
'''
def __init__(self, systype):
if systype.lower() == "solaris":
raise SIException("Platform {0} not (yet) supported.".format(systype))
def _grain(self, grain):
'''
An alias for grains getter.
'''
return __grains__.get(grain, 'N/A')
def _get_disk_size(self, device):
'''
Get a size of a disk.
'''
out = __salt__['cmd.run_all']("df {0}".format(device))
if out['retcode']:
msg = "Disk size info error: {0}".format(out['stderr'])
log.error(msg)
raise SIException(msg)
devpath, blocks, used, available, used_p, mountpoint = [elm for elm in
out['stdout'].split(os.linesep)[-1].split(" ") if elm]
return {
'device': devpath, 'blocks': blocks, 'used': used,
'available': available, 'used (%)': used_p, 'mounted': mountpoint,
}
def _get_fs(self):
'''
Get available file systems and their types.
'''
data = dict()
for dev, dev_data in salt.utils.fsutils._blkid().items():
dev = self._get_disk_size(dev)
device = dev.pop('device')
dev['type'] = dev_data['type']
data[device] = dev
return data
def _get_mounts(self):
'''
Get mounted FS on the system.
'''
return salt.utils.fsutils._get_mounts()
def _get_cpu(self):
'''
Get available CPU information.
'''
# CPU data in grains is OK-ish, but lscpu is still better in this case
out = __salt__['cmd.run_all']("lscpu")
salt.utils.fsutils._verify_run(out)
data = dict()
for descr, value in [elm.split(":", 1) for elm in out['stdout'].split(os.linesep)]:
data[descr.strip()] = value.strip()
return data
def _get_mem(self):
'''
Get memory.
'''
out = __salt__['cmd.run_all']("vmstat -s")
if out['retcode']:
raise SIException("Memory info error: {0}".format(out['stderr']))
ret = dict()
for line in out['stdout'].split(os.linesep):
line = line.strip()
if not line:
continue
size, descr = line.split(" ", 1)
if descr.startswith("K "):
descr = descr[2:]
size = size + "K"
ret[descr] = size
return ret
def _get_network(self):
'''
Get network configuration.
'''
data = dict()
data['interfaces'] = salt.utils.network.interfaces()
data['subnets'] = salt.utils.network.subnets()
return data
def _get_os(self):
'''
Get operating system summary
'''
return {
'name': self._grain('os'),
'family': self._grain('os_family'),
'arch': self._grain('osarch'),
'release': self._grain('osrelease'),
}
class Query(EnvLoader):
'''
Query the system.
This class is actually puts all Salt features together,
so there would be no need to pick it from various places.
'''
# Configuration: config files
# Identity: users/groups
# Software: packages, patterns, repositories
# Services
# System: distro, RAM etc
# Changes: all files that are managed and were changed from the original
# all: include all scopes (scary!)
# payload: files that are not managed
SCOPES = ["changes", "configuration", "identity", "system", "software", "services", "payload", "all"]
def __init__(self, scope, cachedir=None):
'''
Constructor.
:param scope:
:return:
'''
if scope and scope not in self.SCOPES:
raise InspectorQueryException(
"Unknown scope: {0}. Must be one of: {1}".format(repr(scope), ", ".join(self.SCOPES))
)
elif not scope:
raise InspectorQueryException(
"Scope cannot be empty. Must be one of: {0}".format(", ".join(self.SCOPES))
)
EnvLoader.__init__(self, cachedir=cachedir)
self.scope = '_' + scope
self.local_identity = dict()
def __call__(self, *args, **kwargs):
'''
Call the query with the defined scope.
:param args:
:param kwargs:
:return:
'''
return getattr(self, self.scope)(*args, **kwargs)
def _changes(self, *args, **kwargs):
'''
Returns all diffs to the configuration files.
'''
raise Exception("Not yet implemented")
def _configuration(self, *args, **kwargs):
'''
Return configuration files.
'''
data = dict()
self.db.open()
for pkg in self.db.get(Package):
configs = list()
for pkg_cfg in self.db.get(PackageCfgFile, eq={'pkgid': pkg.id}):
configs.append(pkg_cfg.path)
data[pkg.name] = configs
if not data:
raise InspectorQueryException("No inspected configuration yet available.")
return data
def _get_local_users(self, disabled=None):
'''
Return all known local accounts to the system.
'''
users = dict()
path = '/etc/passwd'
with salt.utils.files.fopen(path, 'r') as fp_:
for line in fp_:
line = line.strip()
if ':' not in line:
continue
name, password, uid, gid, gecos, directory, shell = line.split(':')
active = not (password == '*' or password.startswith('!'))
if (disabled is False and active) or (disabled is True and not active) or disabled is None:
users[name] = {
'uid': uid,
'git': gid,
'info': gecos,
'home': directory,
'shell': shell,
'disabled': not active
}
return users
def _get_local_groups(self):
'''
Return all known local groups to the system.
'''
groups = dict()
path = '/etc/group'
with salt.utils.files.fopen(path, 'r') as fp_:
for line in fp_:
line = line.strip()
if ':' not in line:
continue
name, password, gid, users = line.split(':')
groups[name] = {
'gid': gid,
}
if users:
groups[name]['users'] = users.split(',')
return groups
def _get_external_accounts(self, locals):
'''
Return all known accounts, excluding local accounts.
'''
users = dict()
out = __salt__['cmd.run_all']("passwd -S -a")
if out['retcode']:
# System does not supports all accounts descriptions, just skipping.
return users
status = {'L': 'Locked', 'NP': 'No password', 'P': 'Usable password', 'LK': 'Locked'}
for data in [elm.strip().split(" ") for elm in out['stdout'].split(os.linesep) if elm.strip()]:
if len(data) < 2:
continue
name, login = data[:2]
if name not in locals:
users[name] = {
'login': login,
'status': status.get(login, 'N/A')
}
return users
def _identity(self, *args, **kwargs):
'''
Local users and groups.
accounts
Can be either 'local', 'remote' or 'all' (equal to "local,remote").
Remote accounts cannot be resolved on all systems, but only
those, which supports 'passwd -S -a'.
disabled
True (or False, default) to return only disabled accounts.
'''
LOCAL = 'local accounts'
EXT = 'external accounts'
data = dict()
data[LOCAL] = self._get_local_users(disabled=kwargs.get('disabled'))
data[EXT] = self._get_external_accounts(data[LOCAL].keys()) or 'N/A'
data['local groups'] = self._get_local_groups()
return data
def _system(self, *args, **kwargs):
'''
This basically calls grains items and picks out only
necessary information in a certain structure.
:param args:
:param kwargs:
:return:
'''
sysinfo = SysInfo(__grains__.get("kernel"))
data = dict()
data['cpu'] = sysinfo._get_cpu()
data['disks'] = sysinfo._get_fs()
data['mounts'] = sysinfo._get_mounts()
data['memory'] = sysinfo._get_mem()
data['network'] = sysinfo._get_network()
data['os'] = sysinfo._get_os()
return data
def _software(self, *args, **kwargs):
'''
Return installed software.
'''
data = dict()
if 'exclude' in kwargs:
excludes = kwargs['exclude'].split(",")
else:
excludes = list()
os_family = __grains__.get("os_family").lower()
# Get locks
if os_family == 'suse':
LOCKS = "pkg.list_locks"
if 'products' not in excludes:
products = __salt__['pkg.list_products']()
if products:
data['products'] = products
elif os_family == 'redhat':
LOCKS = "pkg.get_locked_packages"
else:
LOCKS = None
if LOCKS and 'locks' not in excludes:
locks = __salt__[LOCKS]()
if locks:
data['locks'] = locks
# Get patterns
if os_family == 'suse':
PATTERNS = 'pkg.list_installed_patterns'
elif os_family == 'redhat':
PATTERNS = 'pkg.group_list'
else:
PATTERNS = None
if PATTERNS and 'patterns' not in excludes:
patterns = __salt__[PATTERNS]()
if patterns:
data['patterns'] = patterns
# Get packages
if 'packages' not in excludes:
data['packages'] = __salt__['pkg.list_pkgs']()
# Get repositories
if 'repositories' not in excludes:
repos = __salt__['pkg.list_repos']()
if repos:
data['repositories'] = repos
return data
def _services(self, *args, **kwargs):
'''
Get list of enabled and disabled services on the particular system.
'''
return {
'enabled': __salt__['service.get_enabled'](),
'disabled': __salt__['service.get_disabled'](),
}
def _id_resolv(self, iid, named=True, uid=True):
'''
Resolve local users and groups.
:param iid:
:param named:
:param uid:
:return:
'''
if not self.local_identity:
self.local_identity['users'] = self._get_local_users()
self.local_identity['groups'] = self._get_local_groups()
if not named:
return iid
for name, meta in self.local_identity[uid and 'users' or 'groups'].items():
if (uid and int(meta.get('uid', -1)) == iid) or (not uid and int(meta.get('gid', -1)) == iid):
return name
return iid
def _payload(self, *args, **kwargs):
'''
Find all unmanaged files. Returns maximum 1000 values.
Parameters:
* **filter**: Include only results which path starts from the filter string.
* **time**: Display time in Unix ticks or format according to the configured TZ (default)
Values: ticks, tz (default)
* **size**: Format size. Values: B, KB, MB, GB
* **owners**: Resolve UID/GID to an actual names or leave them numeric (default).
Values: name (default), id
* **type**: Comma-separated type of included payload: dir (or directory), link and/or file.
* **brief**: Return just a list of matches, if True. Default: False
* **offset**: Offset of the files
* **max**: Maximum returned values. Default 1000.
Options:
* **total**: Return a total amount of found payload files
'''
def _size_format(size, fmt):
if fmt is None:
return size
fmt = fmt.lower()
if fmt == "b":
return "{0} Bytes".format(size)
elif fmt == "kb":
return "{0} Kb".format(round((float(size) / 0x400), 2))
elif fmt == "mb":
return "{0} Mb".format(round((float(size) / 0x400 / 0x400), 2))
elif fmt == "gb":
return "{0} Gb".format(round((float(size) / 0x400 / 0x400 / 0x400), 2))
filter = kwargs.get('filter')
offset = kwargs.get('offset', 0)
timeformat = kwargs.get("time", "tz")
if timeformat not in ["ticks", "tz"]:
raise InspectorQueryException('Unknown "{0}" value for parameter "time"'.format(timeformat))
tfmt = lambda param: timeformat == "tz" and time.strftime("%b %d %Y %H:%M:%S", time.gmtime(param)) or int(param)
size_fmt = kwargs.get("size")
if size_fmt is not None and size_fmt.lower() not in ["b", "kb", "mb", "gb"]:
raise InspectorQueryException('Unknown "{0}" value for parameter "size". '
'Should be either B, Kb, Mb or Gb'.format(timeformat))
owners = kwargs.get("owners", "id")
if owners not in ["name", "id"]:
raise InspectorQueryException('Unknown "{0}" value for parameter "owners". '
'Should be either name or id (default)'.format(owners))
incl_type = [prm for prm in kwargs.get("type", "").lower().split(",") if prm]
if not incl_type:
incl_type.append("file")
for i_type in incl_type:
if i_type not in ["directory", "dir", "d", "file", "f", "link", "l"]:
raise InspectorQueryException('Unknown "{0}" values for parameter "type". '
'Should be comma separated one or more of '
'dir, file and/or link.'.format(", ".join(incl_type)))
self.db.open()
if "total" in args:
return {'total': len(self.db.get(PayloadFile))}
brief = kwargs.get("brief")
pld_files = list() if brief else dict()
for pld_data in self.db.get(PayloadFile)[offset:offset + kwargs.get('max', 1000)]:
if brief:
pld_files.append(pld_data.path)
else:
pld_files[pld_data.path] = {
'uid': self._id_resolv(pld_data.uid, named=(owners == "id")),
'gid': self._id_resolv(pld_data.gid, named=(owners == "id"), uid=False),
'size': _size_format(pld_data.p_size, fmt=size_fmt),
'mode': oct(pld_data.mode),
'accessed': tfmt(pld_data.atime),
'modified': tfmt(pld_data.mtime),
'created': tfmt(pld_data.ctime),
}
return pld_files
def _all(self, *args, **kwargs):
'''
Return all the summary of the particular system.
'''
data = dict()
data['software'] = self._software(**kwargs)
data['system'] = self._system(**kwargs)
data['services'] = self._services(**kwargs)
try:
data['configuration'] = self._configuration(**kwargs)
except InspectorQueryException as ex:
data['configuration'] = 'N/A'
log.error(ex)
data['payload'] = self._payload(**kwargs) or 'N/A'
return data