From 10c1eda1fc0dd58f68e0e86b0118ede705c0066d Mon Sep 17 00:00:00 2001 From: Federico Ressi Date: Tue, 24 Mar 2020 12:00:47 +0100 Subject: [PATCH] Add role to ensure python3 is installed on target host Change-Id: I3b514f3a3cb1f50ac121ca9d865eb500cf49b975 --- roles/tobiko-ensure-python/defaults/main.yaml | 7 + .../files/get_python_info.py | 142 ++++++++++++++++++ roles/tobiko-ensure-python/meta/main.yaml | 4 + roles/tobiko-ensure-python/tasks/main.yaml | 102 +++++++++++++ roles/tobiko-ensure-python/vars/RedHat.yaml | 18 +++ roles/tobiko-tox/defaults/main.yaml | 4 +- roles/tobiko-tox/meta/main.yaml | 1 + 7 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 roles/tobiko-ensure-python/defaults/main.yaml create mode 100644 roles/tobiko-ensure-python/files/get_python_info.py create mode 100644 roles/tobiko-ensure-python/meta/main.yaml create mode 100644 roles/tobiko-ensure-python/tasks/main.yaml create mode 100644 roles/tobiko-ensure-python/vars/RedHat.yaml diff --git a/roles/tobiko-ensure-python/defaults/main.yaml b/roles/tobiko-ensure-python/defaults/main.yaml new file mode 100644 index 000000000..efd70dcf6 --- /dev/null +++ b/roles/tobiko-ensure-python/defaults/main.yaml @@ -0,0 +1,7 @@ +--- + +python_version: '3' +python_command: 'python{{ python_version }}' + +unversioned_python_command: '/usr/bin/python' +unversioned_python_alternative: '/usr/bin/python3' diff --git a/roles/tobiko-ensure-python/files/get_python_info.py b/roles/tobiko-ensure-python/files/get_python_info.py new file mode 100644 index 000000000..25610575d --- /dev/null +++ b/roles/tobiko-ensure-python/files/get_python_info.py @@ -0,0 +1,142 @@ +# Copyright 2018 Red Hat +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import + +import argparse +import json +import logging +import os +import subprocess +import sys + + +LOG = logging.getLogger(__name__) + +GET_PYTHON_VERSION_SCRIPT = """ +import sys + +version = '.'.join(str(i) for i in sys.version_info[:3]) +print(version) +""" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--quiet', '-q', + action="store_true", + help="mute logging messages from STDERR") + parser.add_argument('--base', '-b', + action="store_true", + help="print Python base prefix") + args = parser.parse_args() + + setup_logging(quiet=args.quiet) + version = get_python_version() + executables = [ + info['executable'] + for info in iter_python_executables_info(match_version=version, + base=args.base)] + info = {'version': version, 'executables': executables} + output = json.dumps(info, indent=4, sort_keys=True) + print(output) + + +def setup_logging(quiet=False): + if quiet: + level = logging.ERROR + else: + level = logging.DEBUG + logging.basicConfig( + level=level, + stream=sys.stderr, + format='%(name)-s: %(levelname)-7s %(asctime)-15s | %(message)s') + + +def get_python_version(): + return '.'.join(str(i) for i in sys.version_info[:3]) + + +def iter_python_executables_info(match_version=None, base=None): + last_error = None + for executable in iter_python_executables(base=base): + command = subprocess.list2cmdline( + [executable, '-c', GET_PYTHON_VERSION_SCRIPT]) + try: + version = execute(command).splitlines()[0] + except subprocess.CalledProcessError: + LOG.exception('Unable to get version from script') + else: + if not match_version or match_version == version: + yield {'executable': executable, 'version': version} + else: + if last_error: + raise last_error + + +def iter_python_executables(base=None): + iterated = set() + for executable in _iter_python_executables(base=base): + # Iterate every executable only once + if executable not in iterated: + iterated.add(executable) + yield executable + + +def _iter_python_executables(base): + if base: + base_prefix = getattr(sys, 'base_prefix', None) + if base_prefix: + for executable in iter_prefix_executables(base_prefix): + yield executable + + for executable in iter_prefix_executables(sys.prefix): + yield executable + + yield sys.executable + + +def iter_prefix_executables(prefix): + if os.path.isdir(prefix): + for python_name in iter_versioned_names(): + executable = os.path.join(prefix, 'bin', python_name) + if os.path.isfile(executable): + yield executable + + +def iter_versioned_names(unversioned=None): + unversioned = unversioned or 'python' + short_versioned = unversioned + str(sys.version_info[0]) + long_versioned = '.'.join([short_versioned, str(sys.version_info[1])]) + yield long_versioned + yield short_versioned + yield unversioned + + +def execute(command, *args, **kwargs): + if args or kwargs: + command = command.format(*args, **kwargs) + LOG.debug('%s', command) + env = kwargs.get('env', None) + return subprocess.check_output(command, shell=True, + universal_newlines=True, + env=env) + + +def name_from_path(path): + return os.path.splitext(os.path.basename(path))[0] + + +if __name__ == '__main__': + LOG = logging.getLogger(name_from_path(__file__)) + main() diff --git a/roles/tobiko-ensure-python/meta/main.yaml b/roles/tobiko-ensure-python/meta/main.yaml new file mode 100644 index 000000000..0d4361583 --- /dev/null +++ b/roles/tobiko-ensure-python/meta/main.yaml @@ -0,0 +1,4 @@ +--- + +dependencies: + - role: tobiko-common diff --git a/roles/tobiko-ensure-python/tasks/main.yaml b/roles/tobiko-ensure-python/tasks/main.yaml new file mode 100644 index 000000000..3b984b27c --- /dev/null +++ b/roles/tobiko-ensure-python/tasks/main.yaml @@ -0,0 +1,102 @@ +--- + +- name: "validate python_version value: {{ python_version }}" + assert: + that: + - (python_version|string).split(".") | length >= 1 + - (python_version|string).split(".") | length <= 2 + - (python_version|string).split(".")[0] == '3' + + +- name: "include OS-specific variables" + include_vars: "{{ item }}" + ignore_errors: yes + with_first_found: + - "{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yaml" + - "{{ ansible_distribution }}.{{ ansible_architecture }}.yaml" + - "{{ ansible_distribution }}.yaml" + - "{{ ansible_os_family }}.yaml" + + +- block: + - name: "get Python info for '{{ python_command }}'" + script: + cmd: get_python_info.py --base --quiet + executable: '{{ python_command }}' + register: get_python_info + + rescue: + - name: "install Python {{ python_version }} packages" + become: true + package: + name: "{{ python_packages }}" + + - name: "get Python info for '{{ python_command }}'" + script: + cmd: get_python_info.py --base --quiet + executable: '{{ python_command }}' + register: get_python_info + + +- name: "set python_info fact" + set_fact: + python_info: '{{ get_python_info.stdout | from_json }}' + + +- name: "update Python executable facts" + set_fact: + python_command: '{{ python_info.executables | first | basename }}' + python_executable: '{{ python_info.executables | first }}' + python_version: '{{ python_info.version }}' + + +- name: "show Python executables facts" + debug: + msg: + python_command: '{{ python_command }}' + python_executable: '{{ python_executable }}' + python_info: '{{ python_info }}' + python_version: '{{ python_version }}' + + +- block: + - name: "get Python info for '{{ unversioned_python_command }}'" + script: + cmd: get_python_info.py --base --quiet + executable: '{{ unversioned_python_command }}' + register: get_unversioned_python_info + + rescue: + - name: "set '{{ python_executable }}' as default alternative for python" + become: true + command: "alternatives --set python '{{ unversioned_python_alternative }}'" + + - name: "get Python info for '{{ unversioned_python_command }}'" + script: + cmd: get_python_info.py --base --quiet + executable: '{{ unversioned_python_command }}' + register: get_unversioned_python_info + + +- name: "set unversioned_python_info fact" + set_fact: + unversioned_python_info: '{{ get_unversioned_python_info.stdout | from_json }}' + + +- name: "update unversioned Python executable facts" + set_fact: + unversioned_python_command: + '{{ unversioned_python_info.executables | first | basename }}' + unversioned_python_executable: + '{{ unversioned_python_info.executables | first }}' + unversioned_python_version: + '{{ unversioned_python_info.version }}' + + +- name: "show unversioned Python executables facts" + debug: + msg: + unversioned_python_command: '{{ unversioned_python_command }}' + unversioned_python_executable: '{{ unversioned_python_executable }}' + unversioned_python_info: '{{ unversioned_python_info }}' + unversioned_python_version: '{{ unversioned_python_version }}' diff --git a/roles/tobiko-ensure-python/vars/RedHat.yaml b/roles/tobiko-ensure-python/vars/RedHat.yaml new file mode 100644 index 000000000..ffcaad16c --- /dev/null +++ b/roles/tobiko-ensure-python/vars/RedHat.yaml @@ -0,0 +1,18 @@ +--- + +python_command: '/usr/bin/python{{ python_version }}' + +python_package_name: "python{{ python_version | regex_replace('\\.', '') }}" + +python_packages: + - bzip2-devel + - gcc + - make + - openssl-devel + - readline-devel + - sqlite-devel + - zlib-devel + - '{{ python_package_name }}' + - '{{ python_package_name }}-devel' + - '{{ python_package_name }}-setuptools' + - '{{ python_package_name }}-virtualenv' diff --git a/roles/tobiko-tox/defaults/main.yaml b/roles/tobiko-tox/defaults/main.yaml index 731d9c383..01ae54537 100644 --- a/roles/tobiko-tox/defaults/main.yaml +++ b/roles/tobiko-tox/defaults/main.yaml @@ -11,9 +11,9 @@ tox_command_line: > {% if tox_envlist %} -e {{ tox_envlist | quote }} {% endif %} {{ tox_extra_args }} -tox_python: 'python3' +tox_python: '{{ python_executable }}' tox_report_dir: '{{ test_report_dir | realpath }}' -tox_report_name: '{{ test_report_name }}_{{ tox_envlist }}' +tox_report_name: '{{ test_report_name }}{% if tox_envlist %}_{{ tox_envlist }}{% endif %}' tox_report_env: TOBIKO_TEST_REPORT_DIR: '{{ tox_report_dir }}' TOBIKO_TEST_REPORT_NAME: '{{ tox_report_name }}' diff --git a/roles/tobiko-tox/meta/main.yaml b/roles/tobiko-tox/meta/main.yaml index 0d4361583..0c47ff77c 100644 --- a/roles/tobiko-tox/meta/main.yaml +++ b/roles/tobiko-tox/meta/main.yaml @@ -2,3 +2,4 @@ dependencies: - role: tobiko-common + - role: tobiko-ensure-python