def _ansible(self, *args, utility=None, check=True, pipe_stdout=False, **run_kwargs): """ Call Ansible. Args: args: Ansible positional arguments. run_kwargs: subprocess check (bool): If True, Check return code for error. pipe_stdout (bool): If True, redirect stdout into a pipe, this allow to hide outputs from sys.stdout and permit to retrieve stdout as "result.stdout". Returns: subprocess.CompletedProcess: Ansible call result. """ return call([ executable, f"{self._executable()}-{utility}" if utility else self._executable ] + list(arg for arg in args if arg), cwd=self._config_dir, check=check, pipe_stdout=pipe_stdout, **run_kwargs)
def yaml_lint(files, **kwargs): """ Lint YAML files. Args: files (list of str): Paths to YAML files. """ from accelpy._common import call return call(['yamllint', '-s'] + files, **kwargs)
def _get_executable(cls): """ Get utility executable path after installing or updating it if required. """ if not cls._executable: # Get utility release information from HashiCorp checkpoint API last_release = cls._get_last_version() # Check if executable is already installed and up-to-date cls._executable = exec_file = join(cls._install_dir(), last_release['executable_name']) if isfile(exec_file): line = call((exec_file, 'version'), pipe_stdout=True).stdout.splitlines()[0] exec_version = line.split(' ')[1].strip().lstrip('v') # If file is installed and up-to-date, returns its path current_version = last_release['current_version'] if exec_version == current_version: # pragma: no branch return exec_file # Download executables checksum file and associated signature checksum_raw = cls._download(last_release['checksum_url']).content checksum_sig_raw = cls._download( last_release['signature_url']).content # Verify checksum file signature against HashiCorp GPG key cls._gpg_verify(checksum_raw, checksum_sig_raw) # Download the latest compressed executable compressed = cls._download(last_release['archive_url']).content # Verify executable checksum cls._checksum_verify(checksum_raw, compressed, last_release['archive_name']) # Ensure directories exists makedirs(cls._install_dir(), exist_ok=True) # Lazy import package that are required only on install from io import BytesIO from zipfile import ZipFile # Extract executable and returns its path compressed_file_obj = BytesIO(compressed) compressed_file_obj.seek(0) with ZipFile(compressed_file_obj) as compressed_file: cls._executable = compressed_file.extract( compressed_file.namelist()[0], path=cls._install_dir()) # Ensure the file is executable chmod(cls._executable, stat(cls._executable).st_mode | 0o111) return cls._executable
def _gpg_verify(cls, data, signature): """ Verify GPG signature with HashiCorp public key. Args: data (bytes): Data to verify. signature (bytes): Signature against verify data. Raises: accelpy.exceptions.RuntimeException: Invalid signature. References: https://www.hashicorp.com/security.html """ # Import HashiCorp GPG public key call( ('gpg', '--import', join(dirname(__file__), 'gpg_public_key.asc')), pipe_stdout=True) # Lazy import package that are required only on install from tempfile import TemporaryDirectory # Verify signature with TemporaryDirectory() as tmp: data_path = join(tmp, 'data') with open(data_path, 'wb') as data_file: data_file.write(data) signature_path = join(tmp, 'data.sig') with open(signature_path, 'wb') as signature_file: signature_file.write(signature) gpg_valid = call(('gpg', '--verify', signature_path, data_path), pipe_stdout=True, check=False) if gpg_valid.returncode: raise RuntimeException( f'Unable to update {cls._name()}: Invalid signature')
def test_call(): """ Test call """ from subprocess import CompletedProcess, PIPE import accelpy._common as common from accelpy.exceptions import RuntimeException from accelpy._common import call # Mock subprocess retries = {} returncode = 0 def run(*args, **kwargs): """Mocker run""" if retries: # Simulate failure to retry result = CompletedProcess(args, 1) retries.clear() else: result = CompletedProcess(args, returncode) result.kwargs = kwargs return result common_run = common._run common._run = run # Tests try: # Test: pipe_stdout assert 'stdout' not in call([]).kwargs assert call([], pipe_stdout=True).kwargs['stdout'] == PIPE # Test: Passing keyword arguments assert call([], testing=True).kwargs['testing'] is True # Test: retries retries[0] = 0 assert call([], retries=3).returncode == returncode assert not retries # Test: check returncode = 1 assert call([], check=False).returncode == returncode with pytest.raises(RuntimeException): call([], check=True) # Restore mocked functions finally: common._run = common_run
def cli(*args): """ Call cli Args: *args: CLI arguments. Returns: subprocess.CompletedProcess: Utility call result. """ from sys import executable from accelpy._common import call from accelpy.__main__ import __file__ as cli_exec return call([executable, cli_exec] + [str(arg) for arg in args], pipe_stdout=True, check=False)
def _exec(self, *args, check=True, pipe_stdout=False, **run_kwargs): """ Call utility. Args: args: Utility positional arguments. run_kwargs: subprocess check (bool): If True, Check return code for error. pipe_stdout (bool): If True, redirect stdout into a pipe, this allow to hide outputs from sys.stdout and permit to retrieve stdout as "result.stdout". Returns: subprocess.CompletedProcess: Utility call result. """ return call([self._get_executable()] + [arg for arg in args if arg], cwd=self._config_dir, check=check, pipe_stdout=pipe_stdout, **run_kwargs)
def cli(*args, **env): """ Call cli Args: *args: CLI arguments. **env: Environment variables Returns: subprocess.CompletedProcess: Utility call result. """ from os import environ from sys import executable from accelpy._common import call from accelpy.__main__ import __file__ as cli_exec call_env = environ.copy() call_env.update(env) return call([executable, cli_exec] + [str(arg) for arg in args], pipe_stdout=True, check=False, env=call_env)