File: //usr/lib/python2.7/site-packages/salt/modules/aptly.py
# -*- coding: utf-8 -*-
'''
Aptly Debian repository manager.
.. versionadded:: 2018.3.0
'''
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
import logging
import os
import re
# Import salt libs
from salt.ext import six
from salt.exceptions import SaltInvocationError
import salt.utils.json
import salt.utils.path
import salt.utils.stringutils
_DEFAULT_CONFIG_PATH = '/etc/aptly.conf'
log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = 'aptly'
def __virtual__():
'''
Only works on systems with the aptly binary in the system path.
'''
if salt.utils.path.which('aptly'):
return __virtualname__
return (False, 'The aptly binaries required cannot be found or are not installed.')
def _cmd_run(cmd):
'''
Run the aptly command.
:return: The string output of the command.
:rtype: str
'''
cmd.insert(0, 'aptly')
cmd_ret = __salt__['cmd.run_all'](cmd, ignore_retcode=True)
if cmd_ret['retcode'] != 0:
log.debug('Unable to execute command: %s\nError: %s', cmd,
cmd_ret['stderr'])
return cmd_ret['stdout']
def _format_repo_args(comment=None, component=None, distribution=None,
uploaders_file=None, saltenv='base'):
'''
Format the common arguments for creating or editing a repository.
:param str comment: The description of the repository.
:param str component: The default component to use when publishing.
:param str distribution: The default distribution to use when publishing.
:param str uploaders_file: The repository upload restrictions config.
:param str saltenv: The environment the file resides in.
:return: A list of the arguments formatted as aptly arguments.
:rtype: list
'''
ret = list()
cached_uploaders_path = None
settings = {'comment': comment, 'component': component,
'distribution': distribution}
if uploaders_file:
cached_uploaders_path = __salt__['cp.cache_file'](uploaders_file, saltenv)
if not cached_uploaders_path:
log.error('Unable to get cached copy of file: %s', uploaders_file)
return False
for setting in settings:
if settings[setting] is not None:
ret.append('-{}={}'.format(setting, settings[setting]))
if cached_uploaders_path:
ret.append('-uploaders-file={}'.format(cached_uploaders_path))
return ret
def _validate_config(config_path):
'''
Validate that the configuration file exists and is readable.
:param str config_path: The path to the configuration file for the aptly instance.
:return: None
:rtype: None
'''
log.debug('Checking configuration file: %s', config_path)
if not os.path.isfile(config_path):
message = 'Unable to get configuration file: {}'.format(config_path)
log.error(message)
raise SaltInvocationError(message)
def get_config(config_path=_DEFAULT_CONFIG_PATH):
'''
Get the configuration data.
:param str config_path: The path to the configuration file for the aptly instance.
:return: A dictionary containing the configuration data.
:rtype: dict
CLI Example:
.. code-block:: bash
salt '*' aptly.get_config
'''
_validate_config(config_path)
cmd = ['config', 'show', '-config={}'.format(config_path)]
cmd_ret = _cmd_run(cmd)
return salt.utils.json.loads(cmd_ret)
def list_repos(config_path=_DEFAULT_CONFIG_PATH, with_packages=False):
'''
List all of the repos.
:param str config_path: The path to the configuration file for the aptly instance.
:param bool with_packages: Return a list of packages in the repo.
:return: A dictionary of the repositories.
:rtype: dict
CLI Example:
.. code-block:: bash
salt '*' aptly.list_repos
'''
_validate_config(config_path)
ret = dict()
cmd = ['repo', 'list', '-config={}'.format(config_path), '-raw=true']
cmd_ret = _cmd_run(cmd)
repos = [line.strip() for line in cmd_ret.splitlines()]
log.debug('Found repositories: %s', len(repos))
for name in repos:
ret[name] = get_repo(name=name, config_path=config_path,
with_packages=with_packages)
return ret
def get_repo(name, config_path=_DEFAULT_CONFIG_PATH, with_packages=False):
'''
Get the details of the repository.
:param str name: The name of the repository.
:param str config_path: The path to the configuration file for the aptly instance.
:param bool with_packages: Return a list of packages in the repo.
:return: A dictionary containing information about the repository.
:rtype: dict
CLI Example:
.. code-block:: bash
salt '*' aptly.get_repo name="test-repo"
'''
_validate_config(config_path)
with_packages = six.text_type(bool(with_packages)).lower()
ret = dict()
cmd = ['repo', 'show', '-config={}'.format(config_path),
'-with-packages={}'.format(with_packages), name]
cmd_ret = _cmd_run(cmd)
for line in cmd_ret.splitlines():
try:
# Extract the settings and their values, and attempt to format
# them to match their equivalent setting names.
items = line.split(':')
key = items[0].lower().replace('default', '').strip()
key = ' '.join(key.split()).replace(' ', '_')
ret[key] = salt.utils.stringutils.to_none(
salt.utils.stringutils.to_num(items[1].strip()))
except (AttributeError, IndexError):
# If the line doesn't have the separator or is otherwise invalid, skip it.
log.debug('Skipping line: %s', line)
if ret:
log.debug('Found repository: %s', name)
else:
log.debug('Unable to find repository: %s', name)
return ret
def new_repo(name, config_path=_DEFAULT_CONFIG_PATH, comment=None, component=None,
distribution=None, uploaders_file=None, from_snapshot=None,
saltenv='base'):
'''
Create the new repository.
:param str name: The name of the repository.
:param str config_path: The path to the configuration file for the aptly instance.
:param str comment: The description of the repository.
:param str component: The default component to use when publishing.
:param str distribution: The default distribution to use when publishing.
:param str uploaders_file: The repository upload restrictions config.
:param str from_snapshot: The snapshot to initialize the repository contents from.
:param str saltenv: The environment the file resides in.
:return: A boolean representing whether all changes succeeded.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' aptly.new_repo name="test-repo" comment="Test main repo" component="main" distribution="trusty"
'''
_validate_config(config_path)
current_repo = __salt__['aptly.get_repo'](name=name, config_path=config_path)
if current_repo:
log.debug('Repository already exists: %s', name)
return True
cmd = ['repo', 'create', '-config={}'.format(config_path)]
repo_params = _format_repo_args(comment=comment, component=component,
distribution=distribution,
uploaders_file=uploaders_file, saltenv=saltenv)
cmd.extend(repo_params)
cmd.append(name)
if from_snapshot:
cmd.extend(['from', 'snapshot', from_snapshot])
_cmd_run(cmd)
repo = __salt__['aptly.get_repo'](name=name, config_path=config_path)
if repo:
log.debug('Created repo: %s', name)
return True
log.error('Unable to create repo: %s', name)
return False
def set_repo(name, config_path=_DEFAULT_CONFIG_PATH, comment=None, component=None,
distribution=None, uploaders_file=None, saltenv='base'):
'''
Configure the repository settings.
:param str name: The name of the repository.
:param str config_path: The path to the configuration file for the aptly instance.
:param str comment: The description of the repository.
:param str component: The default component to use when publishing.
:param str distribution: The default distribution to use when publishing.
:param str uploaders_file: The repository upload restrictions config.
:param str from_snapshot: The snapshot to initialize the repository contents from.
:param str saltenv: The environment the file resides in.
:return: A boolean representing whether all changes succeeded.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' aptly.set_repo name="test-repo" comment="Test universe repo" component="universe" distribution="xenial"
'''
_validate_config(config_path)
failed_settings = dict()
# Only check for settings that were passed in and skip the rest.
settings = {'comment': comment, 'component': component,
'distribution': distribution}
for setting in list(settings):
if settings[setting] is None:
settings.pop(setting, None)
current_settings = __salt__['aptly.get_repo'](name=name, config_path=config_path)
if not current_settings:
log.error('Unable to get repo: %s', name)
return False
# Discard any additional settings that get_repo gives
# us that are not present in the provided arguments.
for current_setting in list(current_settings):
if current_setting not in settings:
current_settings.pop(current_setting, None)
# Check the existing repo settings to see if they already have the desired values.
if settings == current_settings:
log.debug('Settings already have the desired values for repository: %s', name)
return True
cmd = ['repo', 'edit', '-config={}'.format(config_path)]
repo_params = _format_repo_args(comment=comment, component=component,
distribution=distribution,
uploaders_file=uploaders_file, saltenv=saltenv)
cmd.extend(repo_params)
cmd.append(name)
_cmd_run(cmd)
new_settings = __salt__['aptly.get_repo'](name=name, config_path=config_path)
# Check the new repo settings to see if they have the desired values.
for setting in settings:
if settings[setting] != new_settings[setting]:
failed_settings.update({setting: settings[setting]})
if failed_settings:
log.error('Unable to change settings for the repository: %s', name)
return False
log.debug('Settings successfully changed to the desired values for repository: %s', name)
return True
def delete_repo(name, config_path=_DEFAULT_CONFIG_PATH, force=False):
'''
Remove the repository.
:param str name: The name of the repository.
:param str config_path: The path to the configuration file for the aptly instance.
:param bool force: Whether to remove the repository even if it is used as the source
of an existing snapshot.
:return: A boolean representing whether all changes succeeded.
:rtype: bool
CLI Example:
.. code-block:: bash
salt '*' aptly.delete_repo name="test-repo"
'''
_validate_config(config_path)
force = six.text_type(bool(force)).lower()
current_repo = __salt__['aptly.get_repo'](name=name, config_path=config_path)
if not current_repo:
log.debug('Repository already absent: %s', name)
return True
cmd = ['repo', 'drop', '-config={}'.format(config_path),
'-force={}'.format(force), name]
_cmd_run(cmd)
repo = __salt__['aptly.get_repo'](name=name, config_path=config_path)
if repo:
log.error('Unable to remove repo: %s', name)
return False
log.debug('Removed repo: %s', name)
return True
def list_mirrors(config_path=_DEFAULT_CONFIG_PATH):
'''
Get a list of all the mirrors.
:param str config_path: The path to the configuration file for the aptly instance.
:return: A list of the mirror names.
:rtype: list
CLI Example:
.. code-block:: bash
salt '*' aptly.list_mirrors
'''
_validate_config(config_path)
cmd = ['mirror', 'list', '-config={}'.format(config_path), '-raw=true']
cmd_ret = _cmd_run(cmd)
ret = [line.strip() for line in cmd_ret.splitlines()]
log.debug('Found mirrors: %s', len(ret))
return ret
def list_published(config_path=_DEFAULT_CONFIG_PATH):
'''
Get a list of all the published repositories.
:param str config_path: The path to the configuration file for the aptly instance.
:return: A list of the published repository names.
:rtype: list
CLI Example:
.. code-block:: bash
salt '*' aptly.list_published
'''
_validate_config(config_path)
cmd = ['publish', 'list', '-config={}'.format(config_path), '-raw=true']
cmd_ret = _cmd_run(cmd)
ret = [line.strip() for line in cmd_ret.splitlines()]
log.debug('Found published repositories: %s', len(ret))
return ret
def list_snapshots(config_path=_DEFAULT_CONFIG_PATH, sort_by_time=False):
'''
Get a list of all the snapshots.
:param str config_path: The path to the configuration file for the aptly instance.
:param bool sort_by_time: Whether to sort by creation time instead of by name.
:return: A list of the snapshot names.
:rtype: list
CLI Example:
.. code-block:: bash
salt '*' aptly.list_snapshots
'''
_validate_config(config_path)
cmd = ['snapshot', 'list', '-config={}'.format(config_path), '-raw=true']
if sort_by_time:
cmd.append('-sort=time')
else:
cmd.append('-sort=name')
cmd_ret = _cmd_run(cmd)
ret = [line.strip() for line in cmd_ret.splitlines()]
log.debug('Found snapshots: %s', len(ret))
return ret
def cleanup_db(config_path=_DEFAULT_CONFIG_PATH, dry_run=False):
'''
Remove data regarding unreferenced packages and delete files in the package pool that
are no longer being used by packages.
:param bool dry_run: Report potential changes without making any changes.
:return: A dictionary of the package keys and files that were removed.
:rtype: dict
CLI Example:
.. code-block:: bash
salt '*' aptly.cleanup_db
'''
_validate_config(config_path)
dry_run = six.text_type(bool(dry_run)).lower()
ret = {'deleted_keys': list(),
'deleted_files': list()}
cmd = ['db', 'cleanup', '-config={}'.format(config_path),
'-dry-run={}'.format(dry_run), '-verbose=true']
cmd_ret = _cmd_run(cmd)
type_pattern = r'^List\s+[\w\s]+(?P<package_type>(file|key)s)[\w\s]+:$'
list_pattern = r'^\s+-\s+(?P<package>.*)$'
current_block = None
for line in cmd_ret.splitlines():
if current_block:
match = re.search(list_pattern, line)
if match:
package_type = 'deleted_{}'.format(current_block)
ret[package_type].append(match.group('package'))
else:
current_block = None
# Intentionally not using an else here, in case of a situation where
# the next list header might be bordered by the previous list.
if not current_block:
match = re.search(type_pattern, line)
if match:
current_block = match.group('package_type')
log.debug('Package keys identified for deletion: %s', len(ret['deleted_keys']))
log.debug('Package files identified for deletion: %s', len(ret['deleted_files']))
return ret