Source code for watchmaker.managers.platform_manager
# -*- coding: utf-8 -*-
"""Watchmaker base manager."""
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
with_statement,
)
import concurrent.futures
import logging
import os
import shutil
import subprocess
import tarfile
import tempfile
import zipfile
import watchmaker.utils
from watchmaker.exceptions import WatchmakerError
from watchmaker.utils import urllib_utils
[docs]
class PlatformManagerBase(object):
"""
Base class for operating system managers.
All child classes will have access to methods unless overridden by an
identically-named method in the child class.
Args:
system_params: (:obj:`dict`)
Attributes, mostly file-paths, specific to the system-type (Linux
or Windows). The dict keys are as follows:
prepdir:
Directory where Watchmaker will keep files on the system.
readyfile:
Path to a file that will be created upon successful
completion.
logdir:
Directory to store log files.
workingdir:
Directory to store temporary files. Deleted upon successful
completion.
restart:
Command to use to restart the system upon successful
completion.
shutdown_path:
(Windows-only) Path to the Windows ``shutdown.exe`` command.
"""
boto3 = None
boto_client = None
def __init__(self, system_params, *args, **kwargs):
self.log = logging.getLogger(
'{0}.{1}'.format(__name__, self.__class__.__name__)
)
self.system_params = system_params
self.working_dir = None
PlatformManagerBase.args = args
PlatformManagerBase.kwargs = kwargs
[docs]
def retrieve_file(self, url, filename):
"""
Retrieve a file from a provided URL.
Supports all :obj:`urllib.request` handlers, as well as S3 buckets.
Args:
url: (:obj:`str`)
URL to a file.
filename: (:obj:`str`)
Path where the file will be saved.
"""
# Convert a local path to a URI
url = watchmaker.utils.uri_from_filepath(url)
self.log.debug('Downloading: %s', url)
self.log.debug('Destination: %s', filename)
try:
self.log.debug('Establishing connection to the host, %s', url)
response = watchmaker.utils.urlopen_retry(url)
self.log.debug('Opening the file handle, %s', filename)
with open(filename, 'wb') as outfile:
self.log.debug('Saving file to local filesystem...')
shutil.copyfileobj(response, outfile)
except (ValueError, urllib_utils.error.URLError):
self.log.critical(
'Failed to retrieve the file. url = %s. filename = %s',
url, filename
)
raise
self.log.info(
'Retrieved the file successfully. url=%s. filename=%s',
url, filename
)
[docs]
def create_working_dir(self, basedir, prefix):
"""
Create a directory in ``basedir`` with a prefix of ``prefix``.
Args:
prefix: (:obj:`str`)
Prefix to prepend to the working directory.
basedir: (:obj:`str`)
The directory in which to create the working directory.
Returns:
:obj:`str`: Path to the working directory.
"""
self.log.info('Creating a working directory.')
original_umask = os.umask(0)
try:
working_dir = tempfile.mkdtemp(prefix=prefix, dir=basedir)
except Exception:
msg = 'Could not create a working dir in {0}'.format(basedir)
self.log.critical(msg)
raise
self.log.debug('Created working directory: %s', working_dir)
os.umask(original_umask)
return working_dir
@staticmethod
def _pipe_handler(pipe, logger=None, prefix_msg=''):
ret = b''
try:
for line in iter(pipe.readline, b''):
if logger:
logger('%s%s', prefix_msg, line.rstrip())
ret += line
finally:
pipe.close()
return ret
[docs]
def call_process(self, cmd, log_pipe='all', raise_error=True):
"""
Execute a shell command.
Args:
cmd: (:obj:`list`)
Command to execute.
log_pipe: (:obj:`str`)
Controls what to log from the command output. Supports three
values: ``stdout``, ``stderr``, ``all``.
(*Default*: ``all``)
raise_error: (:obj:`bool`)
Switch to control whether to raise if the command return code
is non-zero.
(*Default*: ``True``)
Returns:
:obj:`dict`:
Dictionary containing three keys: ``retcode`` (:obj:`int`),
``stdout`` (:obj:`bytes`), and ``stderr`` (:obj:`bytes`).
"""
ret = {
'retcode': 0,
'stdout': b'',
'stderr': b''
}
if not isinstance(cmd, list):
msg = 'Command is not a list: {0}'.format(cmd)
self.log.critical(msg)
raise WatchmakerError(msg)
self.log.debug('Command: %s', ' '.join(cmd))
# If running as a standalone, PyInstaller will have modified the
# LD_LIBRARY_PATH to point to standalone libraries. If there were a
# value at runtime, PyInstaller will create LD_LIBRARY_PATH_ORIG. In
# order for salt to run correctly, LD_LIBRARY_PATH has to be fixed.
kwargs = {}
env = dict(os.environ)
lib_path_key = 'LD_LIBRARY_PATH'
if env.get(lib_path_key) is not None:
lib_path_orig_value = env.get(lib_path_key + '_ORIG')
if lib_path_orig_value is None:
# you can have lib_path and no orig, if:
# 1. none was set and pyinstaller set one, or
# 2. one was set and we're not in standalone package
env.pop(lib_path_key, None)
else:
# put original lib_path back
env[lib_path_key] = lib_path_orig_value
kwargs['env'] = env
with subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs
) as process, concurrent.futures.ThreadPoolExecutor(
max_workers=2
) as executor:
stdout_future = executor.submit(
self._pipe_handler,
process.stdout,
self.log.debug if log_pipe in ['stdout', 'all'] else None,
'Command stdout: '
)
stderr_future = executor.submit(
self._pipe_handler,
process.stderr,
self.log.error if log_pipe in ['stderr', 'all'] else None,
'Command stderr: ')
ret['stdout'] = stdout_future.result()
ret['stderr'] = stderr_future.result()
ret['retcode'] = process.wait()
self.log.debug('Command retcode: %s', ret['retcode'])
if raise_error and ret['retcode'] != 0:
msg = 'Command failed! Exit code={0}, cmd={1}'.format(
ret['retcode'], ' '.join(cmd))
self.log.critical(msg)
raise WatchmakerError(msg)
return ret
[docs]
def cleanup(self):
"""Delete working directory."""
self.log.info('Cleanup Time...')
try:
self.log.debug('working_dir=%s', self.working_dir)
shutil.rmtree(self.working_dir)
self.log.info('Deleted working directory...')
except Exception:
msg = 'Cleanup Failed!'
self.log.critical(msg)
raise
self.log.info('Exiting cleanup routine...')
[docs]
def extract_contents(self, filepath, to_directory, create_dir=False):
"""
Extract a compressed archive to the specified directory.
Args:
filepath: (:obj:`str`)
Path to the compressed file. Supported file extensions:
- `.zip`
- `.tar.gz`
- `.tgz`
- `.tar.bz2`
- `.tbz`
to_directory: (:obj:`str`)
Path to the target directory
create_dir: (:obj:`bool`)
Switch to control the creation of a subdirectory within
``to_directory`` named for the filename of the compressed file.
(*Default*: ``False``)
"""
if filepath.endswith('.zip'):
self.log.debug('File Type: zip')
opener, mode = zipfile.ZipFile, 'r'
elif filepath.endswith('.tar.gz') or filepath.endswith('.tgz'):
self.log.debug('File Type: GZip Tar')
opener, mode = tarfile.open, 'r:gz'
elif filepath.endswith('.tar.bz2') or filepath.endswith('.tbz'):
self.log.debug('File Type: Bzip Tar')
opener, mode = tarfile.open, 'r:bz2'
else:
msg = (
'Could not extract "{0}" as no appropriate extractor is found.'
.format(filepath)
)
self.log.critical(msg)
raise WatchmakerError(msg)
if create_dir:
to_directory = os.sep.join((
to_directory,
'.'.join(filepath.split(os.sep)[-1].split('.')[:-1])
))
try:
os.makedirs(to_directory)
except OSError:
if not os.path.isdir(to_directory):
msg = 'Unable create directory - {0}'.format(to_directory)
self.log.critical(msg)
raise
cwd = os.getcwd()
os.chdir(to_directory)
try:
openfile = opener(filepath, mode)
try:
openfile.extractall()
finally:
openfile.close()
finally:
os.chdir(cwd)
self.log.info(
'Extracted file. source=%s, dest=%s',
filepath, to_directory
)
[docs]
class LinuxPlatformManager(PlatformManagerBase):
"""
Base class for Linux Platforms.
Serves as a foundational class to keep OS consistency.
"""
def _install_from_yum(self, packages):
yum_cmd = ['sudo', 'yum', '-y', 'install']
if isinstance(packages, list):
yum_cmd.extend(packages)
else:
yum_cmd.append(packages)
self.call_process(yum_cmd)
self.log.debug(packages)
[docs]
class WindowsPlatformManager(PlatformManagerBase):
"""
Base class for Windows Platform.
Serves as a foundational class to keep OS consistency.
"""