Source code for watchmaker.workers.salt

# -*- coding: utf-8 -*-
"""Watchmaker salt worker."""
from __future__ import (
    absolute_import,
    division,
    print_function,
    unicode_literals,
    with_statement,
)

import ast
import codecs
import glob
import json
import os
import re
import shutil

import distro
import importlib_resources as resources
import yaml

import watchmaker.utils
from watchmaker import static
from watchmaker.exceptions import (
    InvalidComputerNameError,
    InvalidValueError,
    OuPathRequiredError,
    WatchmakerError,
)
from watchmaker.managers.platform_manager import (
    LinuxPlatformManager,
    PlatformManagerBase,
    WindowsPlatformManager,
)
from watchmaker.workers.base import WorkerBase


[docs] class SaltBase(WorkerBase, PlatformManagerBase): r""" Cross-platform worker for running salt. Args: salt_debug_log: (:obj:`list`) Filesystem path to a file where the salt debug output should be saved. When unset, the salt debug log is saved to the Watchmaker log directory. (*Default*: ``''``) salt_content: (:obj:`str`) URL to a salt content archive (zip file) that will be uncompressed in the watchmaker salt "srv" directory. This typically is used to create a top.sls file and to populate salt's file_roots. (*Default*: ``''``) - *Linux*: ``/srv/watchmaker/salt`` - *Windows*: ``C:\Watchmaker\Salt\srv`` salt_content_path: (:obj:`str`) Used in conjunction with the "salt_content" arg. Glob pattern for the location of salt content files inside the provided salt_content archive. To be used when salt content files are located within a sub-path of the archive, rather than at its top-level. Multiple paths matching the given pattern will result in error. E.g. ``salt_content_path='*/'`` (*Default*: ``''``) salt_states: (:obj:`str`) Comma-separated string of salt states to execute. When "highstate" is included with additional states, "highstate" runs first, then the other states. Accepts two special keywords (case-insensitive): (*Default*: ``'highstate'``) - ``none``: Do not apply any salt states. - ``highstate``: Apply the salt "highstate". exclude_states: (:obj:`str`) Comma-separated string of states to exclude from execution. (*Default*: ``''``) user_formulas: (:obj:`dict`) Map of formula names and URLs to zip archives of salt formulas. These formulas will be downloaded, extracted, and added to the salt file roots. The zip archive must contain a top-level directory that, itself, contains the actual salt formula. To "overwrite" bundled submodule formulas, make sure the formula name matches the submodule name. (*Default*: ``{}``) admin_groups: (:obj:`str`) Sets a salt grain that specifies the domain groups that should have root privileges on Linux or admin privileges on Windows. Value must be a colon-separated string. E.g. ``"group1:group2"`` (*Default*: ``''``) admin_users: (:obj:`str`) Sets a salt grain that specifies the domain users that should have root privileges on Linux or admin privileges on Windows. Value must be a colon-separated string. E.g. ``"user1:user2"`` (*Default*: ``''``) environment: (:obj:`str`) Sets a salt grain that specifies the environment in which the system is being built. E.g. ``dev``, ``test``, ``prod``, etc. (*Default*: ``''``) ou_path: (:obj:`str`) Sets a salt grain that specifies the full DN of the OU where the computer account will be created when joining a domain. E.g. ``"OU=SuperCoolApp,DC=example,DC=com"`` (*Default*: ``''``) pip_install: (:obj:`list`) Python packages to be installed prior to applying the high state. (*Default*: ``[]``) pip_args: (:obj:`list`) Options to pass to pip when installing packages. (*Default*: ``[]``) pip_index: (:obj:`str`) URL used for an index by pip. (*Default*: ``https://pypi.org/simple``) """ def __init__(self, *args, **kwargs): # Init inherited classes super(SaltBase, self).__init__(*args, **kwargs) # Pop arguments used by SaltBase self.user_formulas = kwargs.pop('user_formulas', None) or {} self.computer_name = kwargs.pop('computer_name', None) or '' self.computer_name_pattern = \ kwargs.pop('computer_name_pattern', None) or '' self.ent_env = kwargs.pop('environment', None) or '' self.valid_envs = kwargs.pop('valid_environments', []) or [] self.salt_debug_log = kwargs.pop('salt_debug_log', None) or '' self.salt_content = kwargs.pop('salt_content', None) or '' self.salt_content_path = kwargs.pop('salt_content_path', None) or '' self.ou_path_required = kwargs.pop('ou_path_required', None) or False self.ou_path = kwargs.pop('ou_path', None) or '' self.admin_groups = kwargs.pop('admin_groups', None) or '' self.admin_users = kwargs.pop('admin_users', None) or '' self.pip_install = kwargs.pop('pip_install', None) or [] self.pip_args = kwargs.pop('pip_args', None) or [] self.pip_index = kwargs.pop('pip_index', None) or \ 'https://pypi.org/simple' self.salt_version = kwargs.pop('salt_version', None) or '' self.salt_states = kwargs.pop('salt_states', 'highstate') or '' self.exclude_states = kwargs.pop('exclude_states', None) or '' self.computer_name = watchmaker.utils.config_none_deprecate( self.computer_name, self.log) self.computer_name_pattern = watchmaker.utils.config_none_deprecate( self.computer_name_pattern, self.log) self.salt_debug_log = watchmaker.utils.config_none_deprecate( self.salt_debug_log, self.log) self.salt_content = watchmaker.utils.config_none_deprecate( self.salt_content, self.log) self.salt_content_path = watchmaker.utils.config_none_deprecate( self.salt_content_path, self.log) self.ou_path = watchmaker.utils.config_none_deprecate( self.ou_path, self.log) self.admin_groups = watchmaker.utils.config_none_deprecate( self.admin_groups, self.log) self.admin_users = watchmaker.utils.config_none_deprecate( self.admin_users, self.log) self.salt_states = watchmaker.utils.config_none_deprecate( self.salt_states, self.log) # Init attributes used by SaltBase, overridden by inheriting classes self.salt_working_dir = None self.salt_working_dir_prefix = None self.salt_log_dir = None self.salt_conf_path = None self.salt_conf = None self.salt_call = None self.salt_base_env = None self.salt_formula_root = None self.salt_file_roots = None self.salt_state_args = None self.salt_debug_logfile = None
[docs] def before_install(self): """Validate configuration before starting install.""" # Convert environment to lowercase env = str(self.ent_env).lower() # Convert all valid environment to lowercase valid_envs = [str(x).lower() for x in self.valid_envs] if valid_envs and env not in valid_envs: msg = ( 'Selected environment ({0}) is not one of the valid' ' environment types: {1}'.format(env, valid_envs) ) self.log.critical(msg) raise InvalidValueError(msg) if self.ou_path_required and not self.ou_path: raise OuPathRequiredError( "The 'ou_path' option is required, but not provided." ) if self.computer_name and self.computer_name_pattern: if not re.match(self.computer_name_pattern + r"\Z", self.computer_name): raise InvalidComputerNameError( "Computer name: %s does not match pattern %s" % (self.computer_name, self.computer_name_pattern) )
[docs] def install(self): """Install Salt.""" pass
@staticmethod def _get_salt_dirs(srv): salt_base_env = os.sep.join((srv, 'states')) salt_formula_root = os.sep.join((srv, 'formulas')) salt_pillar_root = os.sep.join((srv, 'pillar')) return ( salt_base_env, salt_formula_root, salt_pillar_root ) def _prepare_for_install(self): self.working_dir = self.create_working_dir( self.salt_working_dir, self.salt_working_dir_prefix ) if self.salt_debug_log: self.salt_debug_logfile = self.salt_debug_log else: self.salt_debug_logfile = os.sep.join( (self.salt_log_dir, 'salt_call.debug.log') ) self.salt_state_args = [ '--log-file', self.salt_debug_logfile, '--log-file-level', 'debug', '--log-level', 'error', '--out', 'quiet', '--return', 'local' ] for salt_dir in [ self.salt_formula_root, self.salt_conf_path ]: try: os.makedirs(salt_dir) except OSError: if not os.path.isdir(salt_dir): msg = 'Unable to create directory - {0}'.format(salt_dir) self.log.error(msg) raise SystemError(msg) with codecs.open( os.path.join(self.salt_conf_path, 'minion'), 'w', encoding="utf-8" ) as fh_: yaml.safe_dump(self.salt_conf, fh_, default_flow_style=False) def _check_salt_version(self): current_salt_version = self.call_process([self.salt_call, '--version']) if ( self.salt_version and self.salt_version in current_salt_version["stdout"].decode() ): self.log.info('Salt version %s is already installed.', self.salt_version) return True self.log.info('Salt version was not specified or does not match.') return False def _get_formulas_conf(self): # Append Salt formulas bundled with Watchmaker package. with resources.as_file( resources.files(static) / 'salt' / 'formulas' ) as formulas_handle: formulas_path = str(formulas_handle) for formula in os.listdir(formulas_path): formula_path = os.sep.join((self.salt_formula_root, formula)) watchmaker.utils.copytree( os.sep.join((formulas_path, formula)), formula_path, force=True ) # Obtain & extract any Salt formulas specified in user_formulas. for formula_name, formula_url in self.user_formulas.items(): filename = os.path.basename(formula_url) file_loc = os.sep.join((self.working_dir, filename)) # Download the formula self.retrieve_file(formula_url, file_loc) # Extract the formula formula_working_dir = self.create_working_dir( self.working_dir, '{0}-'.format(filename) ) self.extract_contents( filepath=file_loc, to_directory=formula_working_dir ) # Get the first directory within the extracted directory formula_inner_dir = os.path.join( formula_working_dir, next(os.walk(formula_working_dir))[1][0] ) # Move the formula to the formula root formula_loc = os.sep.join((self.salt_formula_root, formula_name)) self.log.debug( 'Placing user formula in salt file roots. formula_url=%s, ' 'formula_loc=%s', formula_url, formula_loc ) if os.path.exists(formula_loc): shutil.rmtree(formula_loc) shutil.move(formula_inner_dir, formula_loc) return [ os.path.join(self.salt_formula_root, x) for x in next(os.walk( self.salt_formula_root))[1] ] def _build_salt_formula(self, extract_dir): if self.salt_content: salt_content_filename = watchmaker.utils.basename_from_uri( self.salt_content ) salt_content_file = os.sep.join(( self.working_dir, salt_content_filename )) self.retrieve_file(self.salt_content, salt_content_file) if not self.salt_content_path: self.extract_contents( filepath=salt_content_file, to_directory=extract_dir ) else: self.log.debug( 'Using salt content path: %s', self.salt_content_path ) temp_extract_dir = os.sep.join(( self.working_dir, 'salt-archive')) self.extract_contents( filepath=salt_content_file, to_directory=temp_extract_dir ) salt_content_src = os.sep.join(( temp_extract_dir, self.salt_content_path)) salt_content_glob = glob.glob(salt_content_src) self.log.debug('salt_content_glob: %s', salt_content_glob) if len(salt_content_glob) > 1: msg = 'Found multiple paths matching' \ ' \'{0}\' in {1}'.format( self.salt_content_path, self.salt_content) self.log.critical(msg) raise WatchmakerError(msg) try: salt_files_dir = salt_content_glob[0] except IndexError: msg = 'Salt content glob path \'{0}\' not' \ ' found in {1}'.format( self.salt_content_path, self.salt_content) self.log.critical(msg) raise WatchmakerError(msg) watchmaker.utils.copy_subdirectories( salt_files_dir, extract_dir, self.log) with resources.as_file( resources.files(static) / 'salt' / 'content' ) as bundled_content: watchmaker.utils.copy_subdirectories( bundled_content, extract_dir, self.log) with codecs.open( os.path.join(self.salt_conf_path, 'minion'), 'r+', encoding="utf-8" ) as fh_: salt_conf = yaml.safe_load(fh_) salt_conf.update(self.salt_file_roots) fh_.seek(0) yaml.safe_dump(salt_conf, fh_, default_flow_style=False) def _set_grain(self, grain, value): cmd = [ 'grains.setval', grain, str(json.dumps(value)) ] self.run_salt(cmd) def _get_grain(self, grain): grain_full = self.run_salt(['grains.get', grain]) return grain_full['stdout'].decode().split('\n')[1].strip() def _get_failed_states(self, state_ret): failed_states = {} try: # parse state return salt_id_delim = '_|-' salt_id_pos = 1 for state, data in state_ret['return'].items(): if data['result'] is False: state_id = state.split(salt_id_delim)[salt_id_pos] failed_states[state_id] = data except AttributeError: # some error other than a failed state, msg is in the 'return' key self.log.debug('Salt return (AttributeError): %s', state_ret) failed_states = state_ret['return'] except KeyError: # not sure what failed, just return everything self.log.debug('Salt return (KeyError): %s', state_ret) failed_states = state_ret return failed_states def _install_pip_packages(self): self.log.info( 'Pip installing module(s): `%s`', " ".join(self.pip_install)) pip_cmd = [ 'pip.install', ] pip_cmd.extend([','.join(self.pip_install)]) pip_cmd.extend(['index_url={0}'.format(self.pip_index)]) pip_cmd.extend(self.pip_args) self.run_salt(pip_cmd)
[docs] def run_salt(self, command, **kwargs): """ Execute salt command. Args: command: (:obj:`str` or :obj:`list`) Salt options and a salt module to be executed by salt-call. Watchmaker will always begin the command with the options ``--local``, ``--retcode-passthrough``, and ``--no-color``, so do not specify those options in the command. """ cmd = [ self.salt_call, '--local', '--retcode-passthrough', '--no-color', '--config-dir', self.salt_conf_path ] if isinstance(command, list): cmd.extend(command) else: cmd.append(command) return self.call_process(cmd, **kwargs)
[docs] def service_status(self, service): """ Get the service status using salt. Args: service: (obj:`str`) Name of the service to query. Returns: :obj:`tuple`: ``('running', 'enabled')`` First element is the service running status. Second element is the service enabled status. Each element is a :obj:`bool` representing whether the service is running or enabled. """ cmd_status = [ 'service.status', service, '--out', 'newline_values_only' ] cmd_enabled = [ 'service.enabled', service, '--out', 'newline_values_only' ] return ( self.run_salt(cmd_status)['stdout'].strip().lower() == b'true', self.run_salt(cmd_enabled)['stdout'].strip().lower() == b'true' )
[docs] def service_stop(self, service): """ Stop a service status using salt. Args: service: (:obj:`str`) Name of the service to stop. Returns: :obj:`bool`: ``True`` if the service was stopped. ``False`` if the service could not be stopped. """ cmd = [ 'service.stop', service, '--out', 'newline_values_only' ] return self.run_salt(cmd)['stdout'].strip().lower() == b'true'
[docs] def service_start(self, service): """ Start a service status using salt. Args: service: (:obj:`str`) Name of the service to start. Returns: :obj:`bool`: ``True`` if the service was started. ``False`` if the service could not be started. """ cmd = [ 'service.start', service, '--out', 'newline_values_only' ] return self.run_salt(cmd)['stdout'].strip().lower() == b'true'
[docs] def service_disable(self, service): """ Disable a service using salt. Args: service: (:obj:`str`) Name of the service to disable. Returns: :obj:`bool`: ``True`` if the service was disabled. ``False`` if the service could not be disabled. """ cmd = [ 'service.disable', service, '--out', 'newline_values_only' ] return self.run_salt(cmd)['stdout'].strip().lower() == b'true'
[docs] def service_enable(self, service): """ Enable a service using salt. Args: service: (:obj:`str`) Name of the service to enable. Returns: :obj:`bool`: ``True`` if the service was enabled. ``False`` if the service could not be enabled. """ cmd = [ 'service.enable', service, '--out', 'newline_values_only' ] return self.run_salt(cmd)['stdout'].strip().lower() == b'true'
[docs] def process_grains(self): """Set salt grains.""" ent_env = {'enterprise_environment': str(self.ent_env)} self._set_grain('systemprep', ent_env) self._set_grain('watchmaker', ent_env) grain = {} if self.ou_path: grain['oupath'] = self.ou_path if self.admin_groups: grain['admin_groups'] = self.admin_groups.split(':') if self.admin_users: grain['admin_users'] = self.admin_users.split(':') if grain: self._set_grain('join-domain', grain) grain = {} if self.computer_name: grain['computername'] = self.computer_name if self.computer_name_pattern: grain['pattern'] = self.computer_name_pattern if grain: self._set_grain('name-computer', grain) self.log.info('Syncing custom salt modules...') self.run_salt('saltutil.sync_all')
[docs] def process_states(self, states, exclude): """ Apply salt states but exclude certain states. Args: states: (:obj:`str`) Comma-separated string of salt states to execute. When "highstate" is included with additional states, "highstate" runs first, then the other states. Accepts two special keywords (case-insensitive): - ``none``: Do not apply any salt states. - ``highstate``: Apply the salt "highstate". exclude: (:obj:`str`) Comma-separated string of states to exclude from execution. """ if not states: self.log.info( 'No States were specified. Will not apply any salt states.' ) return cmds = [] states = states.split(',') salt_cmd = self.salt_state_args highstate = False for state in states: if state.lower() == 'highstate': highstate = True states.remove(state) if highstate: self.log.info('Applying the salt "highstate"') cmds.append(salt_cmd + ['state.highstate']) if states: self.log.info( 'Applying the user-defined list of states, states=%s', states ) states = ','.join(states) cmds.append(salt_cmd + ['state.sls', states]) for cmd in cmds: if exclude: cmd.extend(['exclude={0}'.format(exclude)]) ret = self.run_salt(cmd, log_pipe='stderr', raise_error=False) if ret['retcode'] != 0: failed_states = self._get_failed_states( ast.literal_eval(ret['stdout'].decode('utf-8'))) if failed_states: raise WatchmakerError( yaml.safe_dump( { 'Salt state execution failed': failed_states }, default_flow_style=False, indent=4 ) ) self.log.info('Salt states all applied successfully!')
[docs] class SaltLinux(SaltBase, LinuxPlatformManager): """ Run salt on Linux. Args: install_method: (:obj:`str`) **Required**. Method to use to install salt. (*Default*: ``yum``) - ``yum``: Install salt from an RPM using yum. - ``git``: Install salt from source, using the salt bootstrap. bootstrap_source: (:obj:`str`) URL to the salt bootstrap script. Required if ``install_method`` is ``git``. (*Default*: ``''``) git_repo: (:obj:`str`) URL to the salt git repo. Required if ``install_method`` is ``git``. (*Default*: ``''``) salt_version: (:obj:`str`) A git reference present in ``git_repo``, such as a commit or a tag. If not specified, the HEAD of the default branch is used. (*Default*: ``''``) """ def __init__(self, *args, **kwargs): # Init inherited classes super(SaltLinux, self).__init__(*args, **kwargs) # Pop arguments used by SaltLinux self.install_method = kwargs.pop('install_method', None) or 'yum' self.bootstrap_source = \ kwargs.pop('bootstrap_source', None) or '' self.git_repo = kwargs.pop('git_repo', None) or '' # Enforce lowercase and replace spaces with ^ in Linux if self.admin_groups: self.admin_groups = self.admin_groups.lower().replace(' ', '^') # Extra variables needed for SaltLinux. self.yum_pkgs = [ 'selinux-policy-targeted', 'salt-minion', ] # Add distro-specific policycoreutils RPM to package-list self.yum_pkgs.append(self._policy_rpm(distro.version()[0])) # Set up variables for paths to Salt directories and applications. self.salt_call = '/usr/bin/salt-call' self.salt_conf_path = '/opt/watchmaker/salt' self.salt_srv = '/srv/watchmaker/salt' self.salt_log_dir = self.system_params['logdir'] self.salt_working_dir = self.system_params['workingdir'] self.salt_working_dir_prefix = 'salt-' salt_dirs = self._get_salt_dirs(self.salt_srv) self.salt_base_env = salt_dirs[0] self.salt_formula_root = salt_dirs[1] self.salt_pillar_root = salt_dirs[2] # Set up the salt config self.salt_conf = { 'file_client': 'local', 'hash_type': 'sha512', 'pillar_roots': {'base': [str(self.salt_pillar_root)]}, 'pillar_merge_lists': True, 'conf_dir': self.salt_conf_path, 'log_granular_levels': { 'salt.template': 'info', 'salt.loaded.int.render.yaml': 'info' } } def _configuration_validation(self): if self.install_method.lower() == 'git': if not self.bootstrap_source: self.log.error( 'Detected `git` as the install method, but the required ' 'parameter `bootstrap_source` was not provided.' ) if not self.git_repo: self.log.error( 'Detected `git` as the install method, but the required ' 'parameter `git_repo` was not provided.' ) def _install_package(self): if os.path.exists(self.salt_call) and self._check_salt_version(): return self.log.info('Starting Salt installation') if self.install_method.lower() == 'yum': self._install_from_yum(self.yum_pkgs) elif self.install_method.lower() == 'git': salt_bootstrap_filename = os.sep.join(( self.working_dir, watchmaker.utils.basename_from_uri(self.bootstrap_source) )) self.retrieve_file(self.bootstrap_source, salt_bootstrap_filename) bootstrap_cmd = [ 'sh', salt_bootstrap_filename, '-g', self.git_repo ] if self.salt_version: bootstrap_cmd.append('git') bootstrap_cmd.append(self.salt_version) else: self.log.debug('No salt version defined in config.') self.call_process(bootstrap_cmd) def _build_salt_formula(self, extract_dir): formulas_conf = self._get_formulas_conf() file_roots = [str(self.salt_base_env)] file_roots += [str(x) for x in formulas_conf] self.salt_file_roots = {'file_roots': {'base': file_roots}} super(SaltLinux, self)._build_salt_formula(extract_dir) def _set_grain(self, grain, value): self.log.info('Setting grain `%s` ...', grain) super(SaltLinux, self)._set_grain(grain, value) def _selinux_status(self): selinux_getenforce = self.call_process(['getenforce']) return selinux_getenforce['stdout'].strip().lower() == b'enforcing' def _selinux_setenforce(self, state): return self.call_process(['setenforce', state])
[docs] def install(self): """Install salt and execute salt states.""" self._configuration_validation() self._prepare_for_install() salt_running = False salt_enabled = False salt_svc = 'salt-minion' if os.path.exists(self.salt_call): salt_running, salt_enabled = self.service_status(salt_svc) self._install_package() if self.pip_install: self._install_pip_packages() salt_stopped = self.service_stop(salt_svc) self._build_salt_formula(self.salt_srv) if salt_enabled: if not self.service_enable(salt_svc): self.log.error('Failed to enable %s service', salt_svc) else: if not self.service_disable(salt_svc): self.log.error('Failed to disable %s service', salt_svc) if salt_running and salt_stopped: if not self.service_start(salt_svc): self.log.error('Failed to restart %s service', salt_svc) self.process_grains() selinux_enforcing = self._selinux_status() if selinux_enforcing: self.log.info('Making selinux permisive for salt execution') self._selinux_setenforce('permissive') try: self.process_states(self.salt_states, self.exclude_states) finally: if selinux_enforcing: self.log.info('Setting selinux back to enforcing mode') self._selinux_setenforce('enforcing') if self.working_dir: self.cleanup()
def _policy_rpm(self, arg): """Return name of distro version-appropriate policycoreutils RPM.""" if arg < '8': result = 'policycoreutils-python' else: result = 'policycoreutils-python-utils' return result
[docs] class SaltWindows(SaltBase, WindowsPlatformManager): """ Run salt on Windows. Args: installer_url: (:obj:`str`) **Required**. URL to the salt installer for Windows. (*Default*: ``''``) ash_role: (:obj:`str`) Sets a salt grain that specifies the role used by the ash-windows salt formula. E.g. ``"MemberServer"``, ``"DomainController"``, or ``"Workstation"`` (*Default*: ``''``) """ def __init__(self, *args, **kwargs): # Pop arguments used by SaltWindows self.installer_url = kwargs.pop('installer_url', None) or '' self.ash_role = kwargs.pop('ash_role', None) or '' # Init inherited classes super(SaltWindows, self).__init__(*args, **kwargs) self.ash_role = watchmaker.utils.config_none_deprecate( self.ash_role, self.log) # Set up variables for paths to Salt directories and applications. self.salt_call = SaltWindows._get_salt_call() self.salt_wam_root = os.sep.join(( self.system_params['prepdir'], 'Salt')) self.salt_conf_path = os.sep.join((self.salt_wam_root, 'conf')) self.salt_srv = os.sep.join((self.salt_wam_root, 'srv')) self.salt_win_repo = os.sep.join((self.salt_srv, 'winrepo')) self.salt_log_dir = self.system_params['logdir'] self.salt_working_dir = self.system_params['workingdir'] self.salt_working_dir_prefix = 'Salt-' salt_dirs = self._get_salt_dirs(self.salt_srv) self.salt_base_env = salt_dirs[0] self.salt_formula_root = salt_dirs[1] self.salt_pillar_root = salt_dirs[2] # Set up the salt config self.salt_conf = { 'file_client': 'local', 'hash_type': 'sha512', 'pillar_roots': {'base': [str(self.salt_pillar_root)]}, 'pillar_merge_lists': True, 'conf_dir': self.salt_conf_path, 'log_granular_levels': { 'salt.template': 'info', 'salt.loaded.int.render.yaml': 'info' }, 'winrepo_source_dir': 'salt://winrepo', 'winrepo_dir': os.sep.join((self.salt_win_repo, 'winrepo')) } def _install_package(self): if os.path.exists(self.salt_call) and self._check_salt_version(): return installer_name = os.sep.join(( self.working_dir, watchmaker.utils.basename_from_uri(self.installer_url) )) self.retrieve_file(self.installer_url, installer_name) install_cmd = [installer_name, '/S'] self.call_process(install_cmd) def _prepare_for_install(self): if not self.installer_url: self.log.error( 'Parameter `installer_url` was not provided and is' ' needed for installation of Salt in Windows.' ) super(SaltWindows, self)._prepare_for_install() def _build_salt_formula(self, extract_dir): formulas_conf = self._get_formulas_conf() file_roots = [str(self.salt_base_env), str(self.salt_win_repo)] file_roots += [str(x) for x in formulas_conf] self.salt_file_roots = {'file_roots': {'base': file_roots}} super(SaltWindows, self)._build_salt_formula(extract_dir) def _set_grain(self, grain, value): self.log.info('Setting grain `%s` ...', grain) super(SaltWindows, self)._set_grain(grain, value) @staticmethod def _get_salt_call(): """Retrieve installation path for Salt if it exists.""" system_drive = os.environ['systemdrive'] program_files = os.environ['ProgramFiles'] old_salt_path = os.sep.join((system_drive, 'Salt', 'salt-call.bat')) new_salt_paths = [ os.sep.join( (program_files, 'Salt Project', 'Salt', 'salt-call.exe') ), os.sep.join( (program_files, 'Salt Project', 'Salt', 'salt-call.bat') ), ] for salt_path in new_salt_paths: if os.path.isfile(salt_path): return salt_path return old_salt_path
[docs] def install(self): """Install salt and execute salt states.""" self._prepare_for_install() salt_running = False salt_enabled = False salt_svc = 'salt-minion' if os.path.exists(self.salt_call): salt_running, salt_enabled = self.service_status(salt_svc) self._install_package() if self.pip_install: self._install_pip_packages() self.salt_call = SaltWindows._get_salt_call() salt_stopped = self.service_stop(salt_svc) self._build_salt_formula(self.salt_srv) if salt_enabled: if not self.service_enable(salt_svc): self.log.error('Failed to enable %s service', salt_svc) else: if not self.service_disable(salt_svc): self.log.error('Failed to disable %s service', salt_svc) if salt_running and salt_stopped: if not self.service_start(salt_svc): self.log.error('Failed to restart %s service', salt_svc) if self.ash_role: role = {'lookup': {'role': str(self.ash_role)}} self._set_grain('ash-windows', role) self.process_grains() self.log.info('Refreshing package database...') self.run_salt('pkg.refresh_db') self.process_states(self.salt_states, self.exclude_states) if self.working_dir: self.cleanup()