Mini Shell
"""
Aptly Debian repository manager.
.. versionadded:: 2018.3.0
"""
import logging
import os
import re
import salt.utils.json
import salt.utils.path
import salt.utils.stringutils
from salt.exceptions import SaltInvocationError
_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(f"-{setting}={settings[setting]}")
if cached_uploaders_path:
ret.append(f"-uploaders-file={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 = f"Unable to get configuration file: {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", f"-config={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", f"-config={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 = str(bool(with_packages)).lower()
ret = dict()
cmd = [
"repo",
"show",
f"-config={config_path}",
f"-with-packages={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", f"-config={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", f"-config={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 = str(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",
f"-config={config_path}",
f"-force={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", f"-config={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", f"-config={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", f"-config={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 = str(bool(dry_run)).lower()
ret = {"deleted_keys": list(), "deleted_files": list()}
cmd = [
"db",
"cleanup",
f"-config={config_path}",
f"-dry-run={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 = f"deleted_{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
Zerion Mini Shell 1.0