%PDF- %PDF-
Direktori : /proc/227033/root/lib/python2.7/site-packages/salt/spm/ |
Current File : //proc/227033/root/lib/python2.7/site-packages/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 len(unavail) > 0: 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 len(to_inspect) > 0: 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')