salt/spm/__init__.py
# -*- coding: utf-8 -*-
'''
This module provides the point of entry to SPM, the Salt Package Manager
.. versionadded:: 2015.8.0
'''
# Import Python libs
from __future__ import absolute_import, print_function, unicode_literals
import os
import tarfile
import shutil
import hashlib
import logging
import sys
try:
import pwd
import grp
except ImportError:
pass
# Import Salt libs
import salt.client
import salt.config
import salt.loader
import salt.cache
import salt.syspaths as syspaths
from salt.ext import six
from salt.ext.six import string_types
from salt.ext.six.moves import input
from salt.ext.six.moves import filter
from salt.template import compile_template
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
# Get logging started
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(object):
'''
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('Invalid command \'{0}\''.format(command))
except SPMException as exc:
self.ui.error(six.text_type(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['{0}.{1}'.format(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['{0}.{1}'.format(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('Invalid list command \'{0}\''.format(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('Invalid local command \'{0}\''.format(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('Invalid repo command \'{0}\''.format(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(
'{0}\t{1}-{2}\t{3}'.format(pkg[0], pkg[1], pkg[2], 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('{0}/FORMULA'.format(pkg_name))
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('Package file {0} not found'.format(pkg))
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{0}\n'.format(
'\n\t'.join(optional)
))
recommended = set(filter(len, recommended))
if recommended:
self.ui.status('The following dependencies are recommended:\n\t{0}\n'.format(
'\n\t'.join(recommended)
))
to_install = set(filter(len, to_install))
msg = 'Installing packages:\n\t{0}\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 = '{0}/{1}'.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, 'w') as outf:
outf.write(self._query_http(dl_url, repo_info['info']))
# 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('Package file {0} not found'.format(pkg_file))
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('Unable to read formula for {0}'.format(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 {0} 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 unavail:
raise SPMPackageError(
'Cannot install {0}, the following dependencies are needed:\n\n{1}'.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 = '{0} [Installed]'.format(dep_pkg)
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 = '{0} [Installed]'.format(dep_pkg)
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 = '{0} [Installed]'.format(dep_pkg)
return pkgs_to_install, optional_install, recommended_install
def _install_indv_pkg(self, pkg_name, pkg_file):
'''
Install one individual package
'''
self.ui.status('... installing {0}'.format(pkg_name))
formula_tar = tarfile.open(pkg_file, 'r:bz2')
formula_ref = formula_tar.extractfile('{0}/FORMULA'.format(pkg_name))
formula_def = salt.utils.yaml.safe_load(formula_ref)
for field in ('version', 'release', 'summary', 'description'):
if field not in formula_def:
raise SPMPackageError('Invalid package: the {0} was not found'.format(field))
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 {0} due to existing files:\n\n{1}'.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('Installing file {0} to {1}'.format(member.name, 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['{0}.info'.format(self.db_prov)](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['{0}.info'.format(self.db_prov)](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 to_inspect:
dep = next(six.iterkeys(to_inspect))
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('{0}.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 = '{0}.d/{1}'.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):
'''
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']
)
else:
raise SPMException('Auth defined, but password is not set for username: \'{0}\''
.format(repo_info['username']))
except SPMException as exc:
self.ui.error(six.text_type(exc))
else:
query = http.query(dl_path, text=True)
except SPMException as exc:
self.ui.error(six.text_type(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(six.text_type(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 = '{0}/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.getcwdu()
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 = '{0}/{1}'.format(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('{0}/FORMULA'.format(spm_name))
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 = '{0}/SPM-METADATA'.format(repo_path)
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 IOError:
log.error('Unable to create archive directory')
try:
shutil.move(file_, './archive')
except (IOError, 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 IOError:
log.error('Unable to delete %s', file_)
except OSError:
# 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{0}'.format('\n\t'.join(packages))
if not self.opts['assume_yes']:
self.ui.confirm(msg)
for package in packages:
self.ui.status('... removing {0}'.format(package))
if not self._pkgdb_fun('db_exists', self.opts['spm_db']):
raise SPMDatabaseError('No database at {0}, cannot remove {1}'.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('Package {0} not installed'.format(package))
# 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('Removing file {0}'.format(filerow[0]), log.trace)
self._pkgfiles_fun('remove_file', filerow[0], self.files_conn)
else:
self._verbose('Not removing file {0}'.format(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('Removing directory {0}'.format(dir_), log.trace)
os.rmdir(dir_)
except OSError:
# Leave directories in place that still have files in them
self._verbose('Cannot remove directory {0}, probably not empty'.format(dir_), 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('Package file {0} not found'.format(pkg_file))
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('{0}/FORMULA'.format(name))
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('package {0} not installed'.format(package))
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('Package file {0} not found'.format(pkg_file))
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('package {0} not installed'.format(package))
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 = '{0}/FORMULA'.format(self.abspath)
if not os.path.exists(formula_path):
raise SPMPackageError('Formula file {0} not found'.format(formula_path))
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('Invalid package: a {0} must be defined'.format(field))
out_path = '{0}/{1}-{2}-{3}.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('{0}|'.format(ftype)):
file_ = file_.lstrip('{0}|'.format(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('Built package {0}'.format(out_path))
def _exclude(self, member):
'''
Exclude based on opts
'''
if isinstance(member, string_types):
return None
for item in self.opts['spm_build_exclude']:
if member.name.startswith('{0}/{1}'.format(self.formula_conf['name'], item)):
return None
elif member.name.startswith('{0}/{1}'.format(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(object):
'''
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')