File: //proc/self/root/usr/lib/python2.7/site-packages/salt/modules/freebsdports.py
# -*- coding: utf-8 -*-
'''
Install software from the FreeBSD ``ports(7)`` system
.. versionadded:: 2014.1.0
This module allows you to install ports using ``BATCH=yes`` to bypass
configuration prompts. It is recommended to use the :mod:`ports state
<salt.states.freebsdports>` to install ports, but it is also possible to use
this module exclusively from the command line.
.. code-block:: bash
salt minion-id ports.config security/nmap IPV6=off
salt minion-id ports.install security/nmap
'''
from __future__ import absolute_import, unicode_literals, print_function
# Import python libs
import fnmatch
import os
import re
import logging
# Import salt libs
import salt.utils.data
import salt.utils.files
import salt.utils.path
from salt.ext.six import string_types
from salt.exceptions import SaltInvocationError, CommandExecutionError
from salt.ext import six
log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = 'ports'
def __virtual__():
'''
Only runs on FreeBSD systems
'''
if __grains__['os'] == 'FreeBSD':
return __virtualname__
return (False, 'The freebsdports execution module cannot be loaded: '
'only available on FreeBSD systems.')
def _portsnap():
'''
Return 'portsnap --interactive' for FreeBSD 10, otherwise 'portsnap'
'''
ret = ['portsnap']
if float(__grains__['osrelease']) >= 10:
ret.append('--interactive')
return ret
def _check_portname(name):
'''
Check if portname is valid and whether or not the directory exists in the
ports tree.
'''
if not isinstance(name, string_types) or '/' not in name:
raise SaltInvocationError(
'Invalid port name \'{0}\' (category required)'.format(name)
)
path = os.path.join('/usr/ports', name)
if not os.path.isdir(path):
raise SaltInvocationError('Path \'{0}\' does not exist'.format(path))
return path
def _options_dir(name):
'''
Retrieve the path to the dir containing OPTIONS file for a given port
'''
_check_portname(name)
_root = '/var/db/ports'
# New path: /var/db/ports/category_portname
new_dir = os.path.join(_root, name.replace('/', '_'))
# Old path: /var/db/ports/portname
old_dir = os.path.join(_root, name.split('/')[-1])
if os.path.isdir(old_dir):
return old_dir
return new_dir
def _options_file_exists(name):
'''
Returns True/False based on whether or not the options file for the
specified port exists.
'''
return os.path.isfile(os.path.join(_options_dir(name), 'options'))
def _write_options(name, configuration):
'''
Writes a new OPTIONS file
'''
_check_portname(name)
pkg = next(iter(configuration))
conf_ptr = configuration[pkg]
dirname = _options_dir(name)
if not os.path.isdir(dirname):
try:
os.makedirs(dirname)
except OSError as exc:
raise CommandExecutionError(
'Unable to make {0}: {1}'.format(dirname, exc)
)
with salt.utils.files.fopen(os.path.join(dirname, 'options'), 'w') as fp_:
sorted_options = list(conf_ptr)
sorted_options.sort()
fp_.write(salt.utils.stringutils.to_str(
'# This file was auto-generated by Salt (http://saltstack.com)\n'
'# Options for {0}\n'
'_OPTIONS_READ={0}\n'
'_FILE_COMPLETE_OPTIONS_LIST={1}\n'
.format(pkg, ' '.join(sorted_options))
))
opt_tmpl = 'OPTIONS_FILE_{0}SET+={1}\n'
for opt in sorted_options:
fp_.write(salt.utils.stringutils.to_str(
opt_tmpl.format(
'' if conf_ptr[opt] == 'on' else 'UN',
opt
)
))
def _normalize(val):
'''
Fix Salt's yaml-ification of on/off, and otherwise normalize the on/off
values to be used in writing the options file
'''
if isinstance(val, bool):
return 'on' if val else 'off'
return six.text_type(val).lower()
def install(name, clean=True):
'''
Install a port from the ports tree. Installs using ``BATCH=yes`` for
non-interactive building. To set config options for a given port, use
:mod:`ports.config <salt.modules.freebsdports.config>`.
clean : True
If ``True``, cleans after installation. Equivalent to running ``make
install clean BATCH=yes``.
.. note::
It may be helpful to run this function using the ``-t`` option to set a
higher timeout, since compiling a port may cause the Salt command to
exceed the default timeout.
CLI Example:
.. code-block:: bash
salt -t 1200 '*' ports.install security/nmap
'''
portpath = _check_portname(name)
old = __salt__['pkg.list_pkgs']()
if old.get(name.rsplit('/')[-1]):
deinstall(name)
cmd = ['make', 'install']
if clean:
cmd.append('clean')
cmd.append('BATCH=yes')
result = __salt__['cmd.run_all'](
cmd,
cwd=portpath,
reset_system_locale=False,
python_shell=False
)
if result['retcode'] != 0:
__context__['ports.install_error'] = result['stderr']
__context__.pop('pkg.list_pkgs', None)
new = __salt__['pkg.list_pkgs']()
ret = salt.utils.data.compare_dicts(old, new)
if not ret and result['retcode'] == 0:
# No change in package list, but the make install was successful.
# Assume that the installation was a recompile with new options, and
# set return dict so that changes are detected by the ports.installed
# state.
ret = {name: {'old': old.get(name, ''),
'new': new.get(name, '')}}
return ret
def deinstall(name):
'''
De-install a port.
CLI Example:
.. code-block:: bash
salt '*' ports.deinstall security/nmap
'''
portpath = _check_portname(name)
old = __salt__['pkg.list_pkgs']()
result = __salt__['cmd.run_all'](
['make', 'deinstall', 'BATCH=yes'],
cwd=portpath,
python_shell=False
)
__context__.pop('pkg.list_pkgs', None)
new = __salt__['pkg.list_pkgs']()
return salt.utils.data.compare_dicts(old, new)
def rmconfig(name):
'''
Clear the cached options for the specified port; run a ``make rmconfig``
name
The name of the port to clear
CLI Example:
.. code-block:: bash
salt '*' ports.rmconfig security/nmap
'''
portpath = _check_portname(name)
return __salt__['cmd.run'](
['make', 'rmconfig'],
cwd=portpath,
python_shell=False
)
def showconfig(name, default=False, dict_return=False):
'''
Show the configuration options for a given port.
default : False
Show the default options for a port (not necessarily the same as the
current configuration)
dict_return : False
Instead of returning the output of ``make showconfig``, return the data
in an dictionary
CLI Example:
.. code-block:: bash
salt '*' ports.showconfig security/nmap
salt '*' ports.showconfig security/nmap default=True
'''
portpath = _check_portname(name)
if default and _options_file_exists(name):
saved_config = showconfig(name, default=False, dict_return=True)
rmconfig(name)
if _options_file_exists(name):
raise CommandExecutionError('Unable to get default configuration')
default_config = showconfig(name, default=False,
dict_return=dict_return)
_write_options(name, saved_config)
return default_config
try:
result = __salt__['cmd.run_all'](
['make', 'showconfig'],
cwd=portpath,
python_shell=False
)
output = result['stdout'].splitlines()
if result['retcode'] != 0:
error = result['stderr']
else:
error = ''
except TypeError:
error = result
if error:
msg = ('Error running \'make showconfig\' for {0}: {1}'
.format(name, error))
log.error(msg)
raise SaltInvocationError(msg)
if not dict_return:
return '\n'.join(output)
if (not output) or ('configuration options' not in output[0]):
return {}
try:
pkg = output[0].split()[-1].rstrip(':')
except (IndexError, AttributeError, TypeError) as exc:
log.error(
'Unable to get pkg-version string: {0}'.format(exc)
)
return {}
ret = {pkg: {}}
output = output[1:]
for line in output:
try:
opt, val, desc = re.match(
r'\s+([^=]+)=(off|on): (.+)', line
).groups()
except AttributeError:
continue
ret[pkg][opt] = val
if not ret[pkg]:
return {}
return ret
def config(name, reset=False, **kwargs):
'''
Modify configuration options for a given port. Multiple options can be
specified. To see the available options for a port, use
:mod:`ports.showconfig <salt.modules.freebsdports.showconfig>`.
name
The port name, in ``category/name`` format
reset : False
If ``True``, runs a ``make rmconfig`` for the port, clearing its
configuration before setting the desired options
CLI Examples:
.. code-block:: bash
salt '*' ports.config security/nmap IPV6=off
'''
portpath = _check_portname(name)
if reset:
rmconfig(name)
configuration = showconfig(name, dict_return=True)
if not configuration:
raise CommandExecutionError(
'Unable to get port configuration for \'{0}\''.format(name)
)
# Get top-level key for later reference
pkg = next(iter(configuration))
conf_ptr = configuration[pkg]
opts = dict(
(six.text_type(x), _normalize(kwargs[x]))
for x in kwargs
if not x.startswith('_')
)
bad_opts = [x for x in opts if x not in conf_ptr]
if bad_opts:
raise SaltInvocationError(
'The following opts are not valid for port {0}: {1}'
.format(name, ', '.join(bad_opts))
)
bad_vals = [
'{0}={1}'.format(x, y) for x, y in six.iteritems(opts)
if y not in ('on', 'off')
]
if bad_vals:
raise SaltInvocationError(
'The following key/value pairs are invalid: {0}'
.format(', '.join(bad_vals))
)
conf_ptr.update(opts)
_write_options(name, configuration)
new_config = showconfig(name, dict_return=True)
try:
new_config = new_config[next(iter(new_config))]
except (StopIteration, TypeError):
return False
return all(conf_ptr[x] == new_config.get(x) for x in conf_ptr)
def update(extract=False):
'''
Update the ports tree
extract : False
If ``True``, runs a ``portsnap extract`` after fetching, should be used
for first-time installation of the ports tree.
CLI Example:
.. code-block:: bash
salt '*' ports.update
'''
result = __salt__['cmd.run_all'](
_portsnap() + ['fetch'],
python_shell=False
)
if not result['retcode'] == 0:
raise CommandExecutionError(
'Unable to fetch ports snapshot: {0}'.format(result['stderr'])
)
ret = []
try:
patch_count = re.search(
r'Fetching (\d+) patches', result['stdout']
).group(1)
except AttributeError:
patch_count = 0
try:
new_port_count = re.search(
r'Fetching (\d+) new ports or files', result['stdout']
).group(1)
except AttributeError:
new_port_count = 0
ret.append('Applied {0} new patches'.format(patch_count))
ret.append('Fetched {0} new ports or files'.format(new_port_count))
if extract:
result = __salt__['cmd.run_all'](
_portsnap() + ['extract'],
python_shell=False
)
if not result['retcode'] == 0:
raise CommandExecutionError(
'Unable to extract ports snapshot {0}'.format(result['stderr'])
)
result = __salt__['cmd.run_all'](
_portsnap() + ['update'],
python_shell=False
)
if not result['retcode'] == 0:
raise CommandExecutionError(
'Unable to apply ports snapshot: {0}'.format(result['stderr'])
)
__context__.pop('ports.list_all', None)
return '\n'.join(ret)
def list_all():
'''
Lists all ports available.
CLI Example:
.. code-block:: bash
salt '*' ports.list_all
.. warning::
Takes a while to run, and returns a **LOT** of output
'''
if 'ports.list_all' not in __context__:
__context__['ports.list_all'] = []
for path, dirs, files in salt.utils.path.os_walk('/usr/ports'):
stripped = path[len('/usr/ports'):]
if stripped.count('/') != 2 or stripped.endswith('/CVS'):
continue
__context__['ports.list_all'].append(stripped[1:])
return __context__['ports.list_all']
def search(name):
'''
Search for matches in the ports tree. Globs are supported, and the category
is optional
CLI Examples:
.. code-block:: bash
salt '*' ports.search 'security/*'
salt '*' ports.search 'security/n*'
salt '*' ports.search nmap
.. warning::
Takes a while to run
'''
name = six.text_type(name)
all_ports = list_all()
if '/' in name:
if name.count('/') > 1:
raise SaltInvocationError(
'Invalid search string \'{0}\'. Port names cannot have more '
'than one slash'
)
else:
return fnmatch.filter(all_ports, name)
else:
ret = []
for port in all_ports:
if fnmatch.fnmatch(port.rsplit('/')[-1], name):
ret.append(port)
return ret