Mini Shell
"""
This module provides the point of entry to SPM, the Salt Package Manager
.. versionadded:: 2015.8.0
"""
import hashlib
import logging
import os
import shutil
import sys
import tarfile
import salt.cache
import salt.client
import salt.config
import salt.loader
import salt.syspaths as syspaths
import salt.utils.files
import salt.utils.http as http
import salt.utils.path
import salt.utils.platform
import salt.utils.win_functions
import salt.utils.yaml
from salt.template import compile_template
try:
import grp
import pwd
except ImportError:
pass
log = logging.getLogger(__name__)
FILE_TYPES = ("c", "d", "g", "l", "r", "s", "m")
# c: config file
# d: documentation file
# g: ghost file (i.e. the file contents are not included in the package payload)
# l: license file
# r: readme file
# s: SLS file
# m: Salt module
class SPMException(Exception):
"""
Base class for SPMClient exceptions
"""
class SPMInvocationError(SPMException):
"""
Wrong number of arguments or other usage error
"""
class SPMPackageError(SPMException):
"""
Problem with package file or package installation
"""
class SPMDatabaseError(SPMException):
"""
SPM database not found, etc
"""
class SPMOperationCanceled(SPMException):
"""
SPM install or uninstall was canceled
"""
class SPMClient:
"""
Provide an SPM Client
"""
def __init__(self, ui, opts=None): # pylint: disable=W0231
self.ui = ui
if not opts:
opts = salt.config.spm_config(os.path.join(syspaths.CONFIG_DIR, "spm"))
self.opts = opts
self.db_prov = self.opts.get("spm_db_provider", "sqlite3")
self.files_prov = self.opts.get("spm_files_provider", "local")
self._prep_pkgdb()
self._prep_pkgfiles()
self.db_conn = None
self.files_conn = None
self._init()
def _prep_pkgdb(self):
self.pkgdb = salt.loader.pkgdb(self.opts)
def _prep_pkgfiles(self):
self.pkgfiles = salt.loader.pkgfiles(self.opts)
def _init(self):
if not self.db_conn:
self.db_conn = self._pkgdb_fun("init")
if not self.files_conn:
self.files_conn = self._pkgfiles_fun("init")
def _close(self):
if self.db_conn:
self.db_conn.close()
def run(self, args):
"""
Run the SPM command
"""
command = args[0]
try:
if command == "install":
self._install(args)
elif command == "local":
self._local(args)
elif command == "repo":
self._repo(args)
elif command == "remove":
self._remove(args)
elif command == "build":
self._build(args)
elif command == "update_repo":
self._download_repo_metadata(args)
elif command == "create_repo":
self._create_repo(args)
elif command == "files":
self._list_files(args)
elif command == "info":
self._info(args)
elif command == "list":
self._list(args)
elif command == "close":
self._close()
else:
raise SPMInvocationError(f"Invalid command '{command}'")
except SPMException as exc:
self.ui.error(str(exc))
def _pkgdb_fun(self, func, *args, **kwargs):
try:
return getattr(getattr(self.pkgdb, self.db_prov), func)(*args, **kwargs)
except AttributeError:
return self.pkgdb[f"{self.db_prov}.{func}"](*args, **kwargs)
def _pkgfiles_fun(self, func, *args, **kwargs):
try:
return getattr(getattr(self.pkgfiles, self.files_prov), func)(
*args, **kwargs
)
except AttributeError:
return self.pkgfiles[f"{self.files_prov}.{func}"](*args, **kwargs)
def _list(self, args):
"""
Process local commands
"""
args.pop(0)
command = args[0]
if command == "packages":
self._list_packages(args)
elif command == "files":
self._list_files(args)
elif command == "repos":
self._repo_list(args)
else:
raise SPMInvocationError(f"Invalid list command '{command}'")
def _local(self, args):
"""
Process local commands
"""
args.pop(0)
command = args[0]
if command == "install":
self._local_install(args)
elif command == "files":
self._local_list_files(args)
elif command == "info":
self._local_info(args)
else:
raise SPMInvocationError(f"Invalid local command '{command}'")
def _repo(self, args):
"""
Process repo commands
"""
args.pop(0)
command = args[0]
if command == "list":
self._repo_list(args)
elif command == "packages":
self._repo_packages(args)
elif command == "search":
self._repo_packages(args, search=True)
elif command == "update":
self._download_repo_metadata(args)
elif command == "create":
self._create_repo(args)
else:
raise SPMInvocationError(f"Invalid repo command '{command}'")
def _repo_packages(self, args, search=False):
"""
List packages for one or more configured repos
"""
packages = []
repo_metadata = self._get_repo_metadata()
for repo in repo_metadata:
for pkg in repo_metadata[repo]["packages"]:
if args[1] in pkg:
version = repo_metadata[repo]["packages"][pkg]["info"]["version"]
release = repo_metadata[repo]["packages"][pkg]["info"]["release"]
packages.append((pkg, version, release, repo))
for pkg in sorted(packages):
self.ui.status(f"{pkg[0]}\t{pkg[1]}-{pkg[2]}\t{pkg[3]}")
return packages
def _repo_list(self, args):
"""
List configured repos
This can be called either as a ``repo`` command or a ``list`` command
"""
repo_metadata = self._get_repo_metadata()
for repo in repo_metadata:
self.ui.status(repo)
def _install(self, args):
"""
Install a package from a repo
"""
if len(args) < 2:
raise SPMInvocationError("A package must be specified")
caller_opts = self.opts.copy()
caller_opts["file_client"] = "local"
self.caller = salt.client.Caller(mopts=caller_opts)
self.client = salt.client.get_local_client(self.opts["conf_file"])
cache = salt.cache.Cache(self.opts)
packages = args[1:]
file_map = {}
optional = []
recommended = []
to_install = []
for pkg in packages:
if pkg.endswith(".spm"):
if self._pkgfiles_fun("path_exists", pkg):
comps = pkg.split("-")
comps = os.path.split("-".join(comps[:-2]))
pkg_name = comps[-1]
formula_tar = tarfile.open(pkg, "r:bz2")
formula_ref = formula_tar.extractfile(f"{pkg_name}/FORMULA")
formula_def = salt.utils.yaml.safe_load(formula_ref)
file_map[pkg_name] = pkg
to_, op_, re_ = self._check_all_deps(
pkg_name=pkg_name, pkg_file=pkg, formula_def=formula_def
)
to_install.extend(to_)
optional.extend(op_)
recommended.extend(re_)
formula_tar.close()
else:
raise SPMInvocationError(f"Package file {pkg} not found")
else:
to_, op_, re_ = self._check_all_deps(pkg_name=pkg)
to_install.extend(to_)
optional.extend(op_)
recommended.extend(re_)
optional = set(filter(len, optional))
if optional:
self.ui.status(
"The following dependencies are optional:\n\t{}\n".format(
"\n\t".join(optional)
)
)
recommended = set(filter(len, recommended))
if recommended:
self.ui.status(
"The following dependencies are recommended:\n\t{}\n".format(
"\n\t".join(recommended)
)
)
to_install = set(filter(len, to_install))
msg = "Installing packages:\n\t{}\n".format("\n\t".join(to_install))
if not self.opts["assume_yes"]:
self.ui.confirm(msg)
repo_metadata = self._get_repo_metadata()
dl_list = {}
for package in to_install:
if package in file_map:
self._install_indv_pkg(package, file_map[package])
else:
for repo in repo_metadata:
repo_info = repo_metadata[repo]
if package in repo_info["packages"]:
dl_package = False
repo_ver = repo_info["packages"][package]["info"]["version"]
repo_rel = repo_info["packages"][package]["info"]["release"]
repo_url = repo_info["info"]["url"]
if package in dl_list:
# Check package version, replace if newer version
if repo_ver == dl_list[package]["version"]:
# Version is the same, check release
if repo_rel > dl_list[package]["release"]:
dl_package = True
elif repo_rel == dl_list[package]["release"]:
# Version and release are the same, give
# preference to local (file://) repos
if dl_list[package]["source"].startswith("file://"):
if not repo_url.startswith("file://"):
dl_package = True
elif repo_ver > dl_list[package]["version"]:
dl_package = True
else:
dl_package = True
if dl_package is True:
# Put together download directory
cache_path = os.path.join(self.opts["spm_cache_dir"], repo)
# Put together download paths
dl_url = "{}/{}".format(
repo_info["info"]["url"],
repo_info["packages"][package]["filename"],
)
out_file = os.path.join(
cache_path, repo_info["packages"][package]["filename"]
)
dl_list[package] = {
"version": repo_ver,
"release": repo_rel,
"source": dl_url,
"dest_dir": cache_path,
"dest_file": out_file,
}
for package in dl_list:
dl_url = dl_list[package]["source"]
cache_path = dl_list[package]["dest_dir"]
out_file = dl_list[package]["dest_file"]
# Make sure download directory exists
if not os.path.exists(cache_path):
os.makedirs(cache_path)
# Download the package
if dl_url.startswith("file://"):
dl_url = dl_url.replace("file://", "")
shutil.copyfile(dl_url, out_file)
else:
with salt.utils.files.fopen(out_file, "wb") as outf:
outf.write(
self._query_http(dl_url, repo_info["info"], decode_body=False)
)
# First we download everything, then we install
for package in dl_list:
out_file = dl_list[package]["dest_file"]
# Kick off the install
self._install_indv_pkg(package, out_file)
return
def _local_install(self, args, pkg_name=None):
"""
Install a package from a file
"""
if len(args) < 2:
raise SPMInvocationError("A package file must be specified")
self._install(args)
def _check_all_deps(self, pkg_name=None, pkg_file=None, formula_def=None):
"""
Starting with one package, check all packages for dependencies
"""
if pkg_file and not os.path.exists(pkg_file):
raise SPMInvocationError(f"Package file {pkg_file} not found")
self.repo_metadata = self._get_repo_metadata()
if not formula_def:
for repo in self.repo_metadata:
if not isinstance(self.repo_metadata[repo]["packages"], dict):
continue
if pkg_name in self.repo_metadata[repo]["packages"]:
formula_def = self.repo_metadata[repo]["packages"][pkg_name]["info"]
if not formula_def:
raise SPMInvocationError(f"Unable to read formula for {pkg_name}")
# Check to see if the package is already installed
pkg_info = self._pkgdb_fun("info", pkg_name, self.db_conn)
pkgs_to_install = []
if pkg_info is None or self.opts["force"]:
pkgs_to_install.append(pkg_name)
elif pkg_info is not None and not self.opts["force"]:
raise SPMPackageError(
"Package {} already installed, not installing again".format(
formula_def["name"]
)
)
optional_install = []
recommended_install = []
if (
"dependencies" in formula_def
or "optional" in formula_def
or "recommended" in formula_def
):
self.avail_pkgs = {}
for repo in self.repo_metadata:
if not isinstance(self.repo_metadata[repo]["packages"], dict):
continue
for pkg in self.repo_metadata[repo]["packages"]:
self.avail_pkgs[pkg] = repo
needs, unavail, optional, recommended = self._resolve_deps(formula_def)
if len(unavail) > 0:
raise SPMPackageError(
"Cannot install {}, the following dependencies are needed:\n\n{}".format(
formula_def["name"], "\n".join(unavail)
)
)
if optional:
optional_install.extend(optional)
for dep_pkg in optional:
pkg_info = self._pkgdb_fun("info", formula_def["name"])
msg = dep_pkg
if isinstance(pkg_info, dict):
msg = f"{dep_pkg} [Installed]"
optional_install.append(msg)
if recommended:
recommended_install.extend(recommended)
for dep_pkg in recommended:
pkg_info = self._pkgdb_fun("info", formula_def["name"])
msg = dep_pkg
if isinstance(pkg_info, dict):
msg = f"{dep_pkg} [Installed]"
recommended_install.append(msg)
if needs:
pkgs_to_install.extend(needs)
for dep_pkg in needs:
pkg_info = self._pkgdb_fun("info", formula_def["name"])
msg = dep_pkg
if isinstance(pkg_info, dict):
msg = f"{dep_pkg} [Installed]"
return pkgs_to_install, optional_install, recommended_install
def _install_indv_pkg(self, pkg_name, pkg_file):
"""
Install one individual package
"""
self.ui.status(f"... installing {pkg_name}")
formula_tar = tarfile.open(pkg_file, "r:bz2")
formula_ref = formula_tar.extractfile(f"{pkg_name}/FORMULA")
formula_def = salt.utils.yaml.safe_load(formula_ref)
for field in ("version", "release", "summary", "description"):
if field not in formula_def:
raise SPMPackageError(f"Invalid package: the {field} was not found")
pkg_files = formula_tar.getmembers()
# First pass: check for files that already exist
existing_files = self._pkgfiles_fun(
"check_existing", pkg_name, pkg_files, formula_def
)
if existing_files and not self.opts["force"]:
raise SPMPackageError(
"Not installing {} due to existing files:\n\n{}".format(
pkg_name, "\n".join(existing_files)
)
)
# We've decided to install
self._pkgdb_fun("register_pkg", pkg_name, formula_def, self.db_conn)
# Run the pre_local_state script, if present
if "pre_local_state" in formula_def:
high_data = self._render(formula_def["pre_local_state"], formula_def)
ret = self.caller.cmd("state.high", data=high_data)
if "pre_tgt_state" in formula_def:
log.debug("Executing pre_tgt_state script")
high_data = self._render(formula_def["pre_tgt_state"]["data"], formula_def)
tgt = formula_def["pre_tgt_state"]["tgt"]
ret = self.client.run_job(
tgt=formula_def["pre_tgt_state"]["tgt"],
fun="state.high",
tgt_type=formula_def["pre_tgt_state"].get("tgt_type", "glob"),
timout=self.opts["timeout"],
data=high_data,
)
# No defaults for this in config.py; default to the current running
# user and group
if salt.utils.platform.is_windows():
uname = gname = salt.utils.win_functions.get_current_user()
uname_sid = salt.utils.win_functions.get_sid_from_name(uname)
uid = self.opts.get("spm_uid", uname_sid)
gid = self.opts.get("spm_gid", uname_sid)
else:
uid = self.opts.get("spm_uid", os.getuid())
gid = self.opts.get("spm_gid", os.getgid())
uname = pwd.getpwuid(uid)[0]
gname = grp.getgrgid(gid)[0]
# Second pass: install the files
for member in pkg_files:
member.uid = uid
member.gid = gid
member.uname = uname
member.gname = gname
out_path = self._pkgfiles_fun(
"install_file",
pkg_name,
formula_tar,
member,
formula_def,
self.files_conn,
)
if out_path is not False:
if member.isdir():
digest = ""
else:
self._verbose(
f"Installing file {member.name} to {out_path}",
log.trace,
)
file_hash = hashlib.sha1()
digest = self._pkgfiles_fun(
"hash_file",
os.path.join(out_path, member.name),
file_hash,
self.files_conn,
)
self._pkgdb_fun(
"register_file", pkg_name, member, out_path, digest, self.db_conn
)
# Run the post_local_state script, if present
if "post_local_state" in formula_def:
log.debug("Executing post_local_state script")
high_data = self._render(formula_def["post_local_state"], formula_def)
self.caller.cmd("state.high", data=high_data)
if "post_tgt_state" in formula_def:
log.debug("Executing post_tgt_state script")
high_data = self._render(formula_def["post_tgt_state"]["data"], formula_def)
tgt = formula_def["post_tgt_state"]["tgt"]
ret = self.client.run_job(
tgt=formula_def["post_tgt_state"]["tgt"],
fun="state.high",
tgt_type=formula_def["post_tgt_state"].get("tgt_type", "glob"),
timout=self.opts["timeout"],
data=high_data,
)
formula_tar.close()
def _resolve_deps(self, formula_def):
"""
Return a list of packages which need to be installed, to resolve all
dependencies
"""
pkg_info = self.pkgdb[f"{self.db_prov}.info"](formula_def["name"])
if not isinstance(pkg_info, dict):
pkg_info = {}
can_has = {}
cant_has = []
if "dependencies" in formula_def and formula_def["dependencies"] is None:
formula_def["dependencies"] = ""
for dep in formula_def.get("dependencies", "").split(","):
dep = dep.strip()
if not dep:
continue
if self.pkgdb[f"{self.db_prov}.info"](dep):
continue
if dep in self.avail_pkgs:
can_has[dep] = self.avail_pkgs[dep]
else:
cant_has.append(dep)
optional = formula_def.get("optional", "").split(",")
recommended = formula_def.get("recommended", "").split(",")
inspected = []
to_inspect = can_has.copy()
while len(to_inspect) > 0:
dep = next(iter(to_inspect.keys()))
del to_inspect[dep]
# Don't try to resolve the same package more than once
if dep in inspected:
continue
inspected.append(dep)
repo_contents = self.repo_metadata.get(can_has[dep], {})
repo_packages = repo_contents.get("packages", {})
dep_formula = repo_packages.get(dep, {}).get("info", {})
also_can, also_cant, opt_dep, rec_dep = self._resolve_deps(dep_formula)
can_has.update(also_can)
cant_has = sorted(set(cant_has + also_cant))
optional = sorted(set(optional + opt_dep))
recommended = sorted(set(recommended + rec_dep))
return can_has, cant_has, optional, recommended
def _traverse_repos(self, callback, repo_name=None):
"""
Traverse through all repo files and apply the functionality provided in
the callback to them
"""
repo_files = []
if os.path.exists(self.opts["spm_repos_config"]):
repo_files.append(self.opts["spm_repos_config"])
for dirpath, dirnames, filenames in salt.utils.path.os_walk(
"{}.d".format(self.opts["spm_repos_config"])
):
for repo_file in filenames:
if not repo_file.endswith(".repo"):
continue
repo_files.append(repo_file)
for repo_file in repo_files:
repo_path = "{}.d/{}".format(self.opts["spm_repos_config"], repo_file)
with salt.utils.files.fopen(repo_path) as rph:
repo_data = salt.utils.yaml.safe_load(rph)
for repo in repo_data:
if repo_data[repo].get("enabled", True) is False:
continue
if repo_name is not None and repo != repo_name:
continue
callback(repo, repo_data[repo])
def _query_http(self, dl_path, repo_info, decode_body=True):
"""
Download files via http
"""
query = None
response = None
try:
if "username" in repo_info:
try:
if "password" in repo_info:
query = http.query(
dl_path,
text=True,
username=repo_info["username"],
password=repo_info["password"],
decode_body=decode_body,
)
else:
raise SPMException(
"Auth defined, but password is not set for username: '{}'".format(
repo_info["username"]
)
)
except SPMException as exc:
self.ui.error(str(exc))
else:
query = http.query(dl_path, text=True, decode_body=decode_body)
except SPMException as exc:
self.ui.error(str(exc))
try:
if query:
if "SPM-METADATA" in dl_path:
response = salt.utils.yaml.safe_load(query.get("text", "{}"))
else:
response = query.get("text")
else:
raise SPMException("Response is empty, please check for Errors above.")
except SPMException as exc:
self.ui.error(str(exc))
return response
def _download_repo_metadata(self, args):
"""
Connect to all repos and download metadata
"""
cache = salt.cache.Cache(self.opts, self.opts["spm_cache_dir"])
def _update_metadata(repo, repo_info):
dl_path = "{}/SPM-METADATA".format(repo_info["url"])
if dl_path.startswith("file://"):
dl_path = dl_path.replace("file://", "")
with salt.utils.files.fopen(dl_path, "r") as rpm:
metadata = salt.utils.yaml.safe_load(rpm)
else:
metadata = self._query_http(dl_path, repo_info)
cache.store(".", repo, metadata)
repo_name = args[1] if len(args) > 1 else None
self._traverse_repos(_update_metadata, repo_name)
def _get_repo_metadata(self):
"""
Return cached repo metadata
"""
cache = salt.cache.Cache(self.opts, self.opts["spm_cache_dir"])
metadata = {}
def _read_metadata(repo, repo_info):
if cache.updated(".", repo) is None:
log.warning("Updating repo metadata")
self._download_repo_metadata({})
metadata[repo] = {
"info": repo_info,
"packages": cache.fetch(".", repo),
}
self._traverse_repos(_read_metadata)
return metadata
def _create_repo(self, args):
"""
Scan a directory and create an SPM-METADATA file which describes
all of the SPM files in that directory.
"""
if len(args) < 2:
raise SPMInvocationError("A path to a directory must be specified")
if args[1] == ".":
repo_path = os.getcwd()
else:
repo_path = args[1]
old_files = []
repo_metadata = {}
for dirpath, dirnames, filenames in salt.utils.path.os_walk(repo_path):
for spm_file in filenames:
if not spm_file.endswith(".spm"):
continue
spm_path = f"{repo_path}/{spm_file}"
if not tarfile.is_tarfile(spm_path):
continue
comps = spm_file.split("-")
spm_name = "-".join(comps[:-2])
spm_fh = tarfile.open(spm_path, "r:bz2")
formula_handle = spm_fh.extractfile(f"{spm_name}/FORMULA")
formula_conf = salt.utils.yaml.safe_load(formula_handle.read())
use_formula = True
if spm_name in repo_metadata:
# This package is already in the repo; use the latest
cur_info = repo_metadata[spm_name]["info"]
new_info = formula_conf
if int(new_info["version"]) == int(cur_info["version"]):
# Version is the same, check release
if int(new_info["release"]) < int(cur_info["release"]):
# This is an old release; don't use it
use_formula = False
elif int(new_info["version"]) < int(cur_info["version"]):
# This is an old version; don't use it
use_formula = False
if use_formula is True:
# Ignore/archive/delete the old version
log.debug(
"%s %s-%s had been added, but %s-%s will replace it",
spm_name,
cur_info["version"],
cur_info["release"],
new_info["version"],
new_info["release"],
)
old_files.append(repo_metadata[spm_name]["filename"])
else:
# Ignore/archive/delete the new version
log.debug(
"%s %s-%s has been found, but is older than %s-%s",
spm_name,
new_info["version"],
new_info["release"],
cur_info["version"],
cur_info["release"],
)
old_files.append(spm_file)
if use_formula is True:
log.debug(
"adding %s-%s-%s to the repo",
formula_conf["name"],
formula_conf["version"],
formula_conf["release"],
)
repo_metadata[spm_name] = {
"info": formula_conf.copy(),
}
repo_metadata[spm_name]["filename"] = spm_file
metadata_filename = f"{repo_path}/SPM-METADATA"
with salt.utils.files.fopen(metadata_filename, "w") as mfh:
salt.utils.yaml.safe_dump(
repo_metadata,
mfh,
indent=4,
canonical=False,
default_flow_style=False,
)
log.debug("Wrote %s", metadata_filename)
for file_ in old_files:
if self.opts["spm_repo_dups"] == "ignore":
# ignore old packages, but still only add the latest
log.debug("%s will be left in the directory", file_)
elif self.opts["spm_repo_dups"] == "archive":
# spm_repo_archive_path is where old packages are moved
if not os.path.exists("./archive"):
try:
os.makedirs("./archive")
log.debug("%s has been archived", file_)
except OSError:
log.error("Unable to create archive directory")
try:
shutil.move(file_, "./archive")
except OSError:
log.error("Unable to archive %s", file_)
elif self.opts["spm_repo_dups"] == "delete":
# delete old packages from the repo
try:
os.remove(file_)
log.debug("%s has been deleted", file_)
except OSError:
log.error("Unable to delete %s", file_)
except OSError: # pylint: disable=duplicate-except
# The file has already been deleted
pass
def _remove(self, args):
"""
Remove a package
"""
if len(args) < 2:
raise SPMInvocationError("A package must be specified")
packages = args[1:]
msg = "Removing packages:\n\t{}".format("\n\t".join(packages))
if not self.opts["assume_yes"]:
self.ui.confirm(msg)
for package in packages:
self.ui.status(f"... removing {package}")
if not self._pkgdb_fun("db_exists", self.opts["spm_db"]):
raise SPMDatabaseError(
"No database at {}, cannot remove {}".format(
self.opts["spm_db"], package
)
)
# Look at local repo index
pkg_info = self._pkgdb_fun("info", package, self.db_conn)
if pkg_info is None:
raise SPMInvocationError(f"Package {package} not installed")
# Find files that have not changed and remove them
files = self._pkgdb_fun("list_files", package, self.db_conn)
dirs = []
for filerow in files:
if self._pkgfiles_fun("path_isdir", filerow[0]):
dirs.append(filerow[0])
continue
file_hash = hashlib.sha1()
digest = self._pkgfiles_fun(
"hash_file", filerow[0], file_hash, self.files_conn
)
if filerow[1] == digest:
self._verbose(f"Removing file {filerow[0]}", log.trace)
self._pkgfiles_fun("remove_file", filerow[0], self.files_conn)
else:
self._verbose(f"Not removing file {filerow[0]}", log.trace)
self._pkgdb_fun("unregister_file", filerow[0], package, self.db_conn)
# Clean up directories
for dir_ in sorted(dirs, reverse=True):
self._pkgdb_fun("unregister_file", dir_, package, self.db_conn)
try:
self._verbose(f"Removing directory {dir_}", log.trace)
os.rmdir(dir_)
except OSError:
# Leave directories in place that still have files in them
self._verbose(
f"Cannot remove directory {dir_}, probably not empty",
log.trace,
)
self._pkgdb_fun("unregister_pkg", package, self.db_conn)
def _verbose(self, msg, level=log.debug):
"""
Display verbose information
"""
if self.opts.get("verbose", False) is True:
self.ui.status(msg)
level(msg)
def _local_info(self, args):
"""
List info for a package file
"""
if len(args) < 2:
raise SPMInvocationError("A package filename must be specified")
pkg_file = args[1]
if not os.path.exists(pkg_file):
raise SPMInvocationError(f"Package file {pkg_file} not found")
comps = pkg_file.split("-")
comps = "-".join(comps[:-2]).split("/")
name = comps[-1]
formula_tar = tarfile.open(pkg_file, "r:bz2")
formula_ref = formula_tar.extractfile(f"{name}/FORMULA")
formula_def = salt.utils.yaml.safe_load(formula_ref)
self.ui.status(self._get_info(formula_def))
formula_tar.close()
def _info(self, args):
"""
List info for a package
"""
if len(args) < 2:
raise SPMInvocationError("A package must be specified")
package = args[1]
pkg_info = self._pkgdb_fun("info", package, self.db_conn)
if pkg_info is None:
raise SPMPackageError(f"package {package} not installed")
self.ui.status(self._get_info(pkg_info))
def _get_info(self, formula_def):
"""
Get package info
"""
fields = (
"name",
"os",
"os_family",
"release",
"version",
"dependencies",
"os_dependencies",
"os_family_dependencies",
"summary",
"description",
)
for item in fields:
if item not in formula_def:
formula_def[item] = "None"
if "installed" not in formula_def:
formula_def["installed"] = "Not installed"
return (
"Name: {name}\n"
"Version: {version}\n"
"Release: {release}\n"
"Install Date: {installed}\n"
"Supported OSes: {os}\n"
"Supported OS families: {os_family}\n"
"Dependencies: {dependencies}\n"
"OS Dependencies: {os_dependencies}\n"
"OS Family Dependencies: {os_family_dependencies}\n"
"Summary: {summary}\n"
"Description:\n"
"{description}".format(**formula_def)
)
def _local_list_files(self, args):
"""
List files for a package file
"""
if len(args) < 2:
raise SPMInvocationError("A package filename must be specified")
pkg_file = args[1]
if not os.path.exists(pkg_file):
raise SPMPackageError(f"Package file {pkg_file} not found")
formula_tar = tarfile.open(pkg_file, "r:bz2")
pkg_files = formula_tar.getmembers()
for member in pkg_files:
self.ui.status(member.name)
def _list_packages(self, args):
"""
List files for an installed package
"""
packages = self._pkgdb_fun("list_packages", self.db_conn)
for package in packages:
if self.opts["verbose"]:
status_msg = ",".join(package)
else:
status_msg = package[0]
self.ui.status(status_msg)
def _list_files(self, args):
"""
List files for an installed package
"""
if len(args) < 2:
raise SPMInvocationError("A package name must be specified")
package = args[-1]
files = self._pkgdb_fun("list_files", package, self.db_conn)
if files is None:
raise SPMPackageError(f"package {package} not installed")
else:
for file_ in files:
if self.opts["verbose"]:
status_msg = ",".join(file_)
else:
status_msg = file_[0]
self.ui.status(status_msg)
def _build(self, args):
"""
Build a package
"""
if len(args) < 2:
raise SPMInvocationError("A path to a formula must be specified")
self.abspath = args[1].rstrip("/")
comps = self.abspath.split("/")
self.relpath = comps[-1]
formula_path = f"{self.abspath}/FORMULA"
if not os.path.exists(formula_path):
raise SPMPackageError(f"Formula file {formula_path} not found")
with salt.utils.files.fopen(formula_path) as fp_:
formula_conf = salt.utils.yaml.safe_load(fp_)
for field in ("name", "version", "release", "summary", "description"):
if field not in formula_conf:
raise SPMPackageError(f"Invalid package: a {field} must be defined")
out_path = "{}/{}-{}-{}.spm".format(
self.opts["spm_build_dir"],
formula_conf["name"],
formula_conf["version"],
formula_conf["release"],
)
if not os.path.exists(self.opts["spm_build_dir"]):
os.mkdir(self.opts["spm_build_dir"])
self.formula_conf = formula_conf
formula_tar = tarfile.open(out_path, "w:bz2")
if "files" in formula_conf:
# This allows files to be added to the SPM file in a specific order.
# It also allows for files to be tagged as a certain type, as with
# RPM files. This tag is ignored here, but is used when installing
# the SPM file.
if isinstance(formula_conf["files"], list):
formula_dir = tarfile.TarInfo(formula_conf["name"])
formula_dir.type = tarfile.DIRTYPE
formula_tar.addfile(formula_dir)
for file_ in formula_conf["files"]:
for ftype in FILE_TYPES:
if file_.startswith(f"{ftype}|"):
file_ = file_.lstrip(f"{ftype}|")
formula_tar.add(
os.path.join(os.getcwd(), file_),
os.path.join(formula_conf["name"], file_),
)
else:
# If no files are specified, then the whole directory will be added.
try:
formula_tar.add(
formula_path, formula_conf["name"], filter=self._exclude
)
formula_tar.add(
self.abspath, formula_conf["name"], filter=self._exclude
)
except TypeError:
formula_tar.add(
formula_path, formula_conf["name"], exclude=self._exclude
)
formula_tar.add(
self.abspath, formula_conf["name"], exclude=self._exclude
)
formula_tar.close()
self.ui.status(f"Built package {out_path}")
def _exclude(self, member):
"""
Exclude based on opts
"""
if isinstance(member, str):
return None
for item in self.opts["spm_build_exclude"]:
if member.name.startswith("{}/{}".format(self.formula_conf["name"], item)):
return None
elif member.name.startswith(f"{self.abspath}/{item}"):
return None
return member
def _render(self, data, formula_def):
"""
Render a [pre|post]_local_state or [pre|post]_tgt_state script
"""
# FORMULA can contain a renderer option
renderer = formula_def.get("renderer", self.opts.get("renderer", "jinja|yaml"))
rend = salt.loader.render(self.opts, {})
blacklist = self.opts.get("renderer_blacklist")
whitelist = self.opts.get("renderer_whitelist")
template_vars = formula_def.copy()
template_vars["opts"] = self.opts.copy()
return compile_template(
":string:",
rend,
renderer,
blacklist,
whitelist,
input_data=data,
**template_vars,
)
class SPMUserInterface:
"""
Handle user interaction with an SPMClient object
"""
def status(self, msg):
"""
Report an SPMClient status message
"""
raise NotImplementedError()
def error(self, msg):
"""
Report an SPM error message
"""
raise NotImplementedError()
def confirm(self, action):
"""
Get confirmation from the user before performing an SPMClient action.
Return if the action is confirmed, or raise SPMOperationCanceled(<msg>)
if canceled.
"""
raise NotImplementedError()
class SPMCmdlineInterface(SPMUserInterface):
"""
Command-line interface to SPMClient
"""
def status(self, msg):
print(msg)
def error(self, msg):
print(msg, file=sys.stderr)
def confirm(self, action):
print(action)
res = input("Proceed? [N/y] ")
if not res.lower().startswith("y"):
raise SPMOperationCanceled("canceled")
Zerion Mini Shell 1.0