Implement test workflow steps
Change-Id: I34026836ac42dd6f2a5e5dfc05203c460cc949fc
This commit is contained in:
parent
dd5362b779
commit
31f6026ab9
22
bindep.txt
22
bindep.txt
|
@ -1,28 +1,38 @@
|
||||||
# This file contains runtime (non-python) dependencies
|
# This file contains runtime (non-python) dependencies
|
||||||
# More info at: https://docs.openstack.org/infra/bindep/readme.html
|
# More info at: https://docs.openstack.org/infra/bindep/readme.html
|
||||||
|
|
||||||
# All OSs
|
|
||||||
gcc []
|
|
||||||
python3 []
|
|
||||||
|
|
||||||
# Red Hat
|
# Red Hat
|
||||||
|
gcc [platform:redhat]
|
||||||
|
make [platform:redhat]
|
||||||
|
python3 [platform:redhat]
|
||||||
|
bzip2-devel [platform:redhat]
|
||||||
git [platform:redhat]
|
git [platform:redhat]
|
||||||
iproute [platform:redhat]
|
iproute [platform:redhat]
|
||||||
libffi-devel [platform:redhat]
|
libffi-devel [platform:redhat]
|
||||||
openssl-devel [platform:redhat]
|
openssl-devel [platform:redhat]
|
||||||
python3-devel [platform:redhat]
|
python3-devel [platform:redhat]
|
||||||
|
python3-setuptools [platform:redhat]
|
||||||
|
readline-devel [platform:redhat]
|
||||||
|
sqlite-devel [platform:redhat]
|
||||||
|
zlib-devel [platform:redhat]
|
||||||
|
|
||||||
# RHEL/CentOS 7
|
# RHEL/CentOS 7
|
||||||
libselinux-python [platform:rhel-7]
|
libselinux-python [platform:rhel-7]
|
||||||
libselinux-python [platform:centos-7]
|
libselinux-python [platform:centos-7]
|
||||||
python2-devel [platform:rhel-7]
|
python-devel [platform:rhel-7]
|
||||||
python2-devel [platform:centos-7]
|
python-devel [platform:centos-7]
|
||||||
|
python-virtualenv [platform:centos-7]
|
||||||
|
|
||||||
# RHEL/CentOS 8
|
# RHEL/CentOS 8
|
||||||
python3-libselinux [platform:rhel-8]
|
python3-libselinux [platform:rhel-8]
|
||||||
python3-libselinux [platform:centos-8]
|
python3-libselinux [platform:centos-8]
|
||||||
|
python3-virtualenv [platform:rhel-8]
|
||||||
|
python3-virtualenv [platform:centos-8]
|
||||||
|
|
||||||
# Ubuntu
|
# Ubuntu
|
||||||
|
gcc [platform:ubuntu]
|
||||||
|
make [platform:ubuntu]
|
||||||
|
python3 [platform:ubuntu]
|
||||||
git [platform:ubuntu]
|
git [platform:ubuntu]
|
||||||
libffi-dev [platform:ubuntu]
|
libffi-dev [platform:ubuntu]
|
||||||
libssl-dev [platform:ubuntu]
|
libssl-dev [platform:ubuntu]
|
||||||
|
|
|
@ -89,8 +89,12 @@ subparsers:
|
||||||
help: enable/disable verbose log entries in tests results log file
|
help: enable/disable verbose log entries in tests results log file
|
||||||
ansible_variable: test_log_debug
|
ansible_variable: test_log_debug
|
||||||
|
|
||||||
- title: Run tox stage
|
- title: Run stage
|
||||||
options:
|
options:
|
||||||
|
workflow:
|
||||||
|
type: Value
|
||||||
|
help: name of workflow to execute
|
||||||
|
ansible_variable: test_workflow
|
||||||
tox-dir:
|
tox-dir:
|
||||||
type: Value
|
type: Value
|
||||||
help: directory from where run tox (typically test_dir)
|
help: directory from where run tox (typically test_dir)
|
||||||
|
|
|
@ -8,11 +8,11 @@ VAGRANTFILE_API_VERSION = "2"
|
||||||
CPUS = 4
|
CPUS = 4
|
||||||
|
|
||||||
# Customize the amount of memory on the VM
|
# Customize the amount of memory on the VM
|
||||||
MEMORY = 512
|
MEMORY = ENV.fetch("VM_SIZE", "512").to_i
|
||||||
|
|
||||||
# Every Vagrant development environment requires a box. You can search for
|
# Every Vagrant development environment requires a box. You can search for
|
||||||
# boxes at https://vagrantcloud.com/search.
|
# boxes at https://vagrantcloud.com/search.
|
||||||
BOX = "generic/centos8"
|
BOX = ENV.fetch("VM_BOX", "generic/centos8")
|
||||||
|
|
||||||
TOX_INI_DIR = '../..'
|
TOX_INI_DIR = '../..'
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
bindep_command: bindep
|
||||||
|
bindep_min_version: 2.8
|
||||||
|
bindep_file: bindep.txt
|
||||||
|
bindep_profile: test
|
|
@ -1,5 +1,4 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- role: tobiko-common
|
|
||||||
- role: tobiko-ensure-python3
|
- role: tobiko-ensure-python3
|
|
@ -8,29 +8,31 @@
|
||||||
|
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
- name: "get '{{ bindep_executable }}' full path"
|
- name: "get '{{ bindep_command }}' full path"
|
||||||
command: >
|
shell: |
|
||||||
which '{{ bindep_executable }}'
|
export PATH={{ python_exe_path }}
|
||||||
|
which '{{ bindep_command }}'
|
||||||
register: get_bindep_path
|
register: get_bindep_path
|
||||||
|
|
||||||
- name: "update bindep_executable fact"
|
- name: "set bindep_executable fact"
|
||||||
set_fact:
|
set_fact:
|
||||||
bindep_executable: '{{ get_bindep_path.stdout_lines | first }}'
|
bindep_executable: '{{ get_bindep_path.stdout_lines | first }}'
|
||||||
|
|
||||||
rescue:
|
rescue:
|
||||||
- name: "update bindep_executable fact"
|
- name: "set bindep_executable fact"
|
||||||
set_fact:
|
set_fact:
|
||||||
bindep_executable: '{{ ansible_user_dir }}/.local/bin/bindep'
|
bindep_executable: '{{ ansible_user_dir }}/.local/bin/bindep'
|
||||||
|
|
||||||
|
|
||||||
- name: "get installed Bindep version"
|
- name: "get '{{ bindep_executable }}' version"
|
||||||
command: "'{{ bindep_executable }}' --version"
|
shell: "'{{ bindep_executable }}' --version 2>&1"
|
||||||
register: get_bindep_version
|
register: get_bindep_version
|
||||||
|
failed_when: '(get_bindep_version.stdout_lines | length) == 0'
|
||||||
|
|
||||||
|
|
||||||
- name: "update bindep_version fact"
|
- name: "update bindep_version fact"
|
||||||
set_fact:
|
set_fact:
|
||||||
bindep_version: '{{ get_bindep_version.stdout }}'
|
bindep_version: '{{ get_bindep_version.stdout_lines | first }}'
|
||||||
|
|
||||||
|
|
||||||
- name: "show Bindep facts"
|
- name: "show Bindep facts"
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
- name: "ensure '{{ bindep_command }}' command is available"
|
||||||
|
include_tasks: ensure-bindep.yaml
|
||||||
|
when:
|
||||||
|
- bindep_executable is not defined
|
||||||
|
|
||||||
|
|
||||||
|
- name: "check '{{ bindep_file }}' file exists"
|
||||||
|
stat:
|
||||||
|
path: '{{ bindep_file | realpath }}'
|
||||||
|
register: check_bindep_file
|
||||||
|
|
||||||
|
|
||||||
|
- block:
|
||||||
|
- name: "run '{{ bindep_command }}' with profile '{{ bindep_profile }}'"
|
||||||
|
command: >
|
||||||
|
'{{ bindep_executable }}' -b -f '{{ bindep_file }}' {{ bindep_profile }}
|
||||||
|
when:
|
||||||
|
- check_bindep_file.stat.readable
|
||||||
|
register: run_bindep
|
||||||
|
changed_when: no
|
||||||
|
|
||||||
|
rescue:
|
||||||
|
- debug: var=run_bindep
|
||||||
|
when: '(run_bindep.stdout_lines | length) == 0'
|
||||||
|
failed_when: yes
|
||||||
|
|
||||||
|
- name: 'install missing packages'
|
||||||
|
become: yes
|
||||||
|
package:
|
||||||
|
name: '{{ run_bindep.stdout_lines }}'
|
||||||
|
register: install_packages
|
||||||
|
|
||||||
|
- name: 'show installed packages'
|
||||||
|
debug: var=install_packages
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- name: "ensures local directory '{{ collect_dir }}' exists"
|
- name: "ensure local directory '{{ collect_dir }}' exists"
|
||||||
file:
|
file:
|
||||||
path: '{{ collect_dir }}'
|
path: '{{ collect_dir }}'
|
||||||
state: directory
|
state: directory
|
||||||
|
|
|
@ -4,4 +4,3 @@ dependencies:
|
||||||
- role: tobiko-common
|
- role: tobiko-common
|
||||||
- role: tobiko-ensure-rsync
|
- role: tobiko-ensure-rsync
|
||||||
- role: tobiko-ensure-git
|
- role: tobiko-ensure-git
|
||||||
- role: tobiko-ensure-bindep
|
|
||||||
|
|
|
@ -63,27 +63,3 @@
|
||||||
|
|
||||||
- name: "show last change details"
|
- name: "show last change details"
|
||||||
debug: var=get_git_log.stdout_lines
|
debug: var=get_git_log.stdout_lines
|
||||||
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: "check missing binary dependencies from '{{ bindep_file }}'"
|
|
||||||
command:
|
|
||||||
cmd: >
|
|
||||||
'{{ bindep_executable }}' -b -f '{{ bindep_file }}' {{ bindep_profiles }}
|
|
||||||
chdir: '{{ dest_dir }}'
|
|
||||||
register: get_bindeps
|
|
||||||
changed_when: no
|
|
||||||
|
|
||||||
rescue:
|
|
||||||
- name: "install missing dependencies"
|
|
||||||
become: yes
|
|
||||||
package:
|
|
||||||
name: '{{ get_bindeps.stdout_lines }}'
|
|
||||||
|
|
||||||
- name: "check missing binary dependencies from '{{ bindep_file }}'"
|
|
||||||
command:
|
|
||||||
cmd: >
|
|
||||||
'{{ bindep_executable }}' -b -f '{{ bindep_file }}' {{ bindep_profiles }}
|
|
||||||
chdir: '{{ dest_dir }}'
|
|
||||||
register: get_bindeps
|
|
||||||
changed_when: no
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
bindep_executable: bindep
|
|
||||||
bindep_min_version: 2.8
|
|
|
@ -1,6 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
- name: "ensures '{{ bindep_executable }}' command is available"
|
|
||||||
include_tasks: bindep.yaml
|
|
||||||
when:
|
|
||||||
- bindep_version is not defined
|
|
|
@ -1,4 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- role: tobiko-common
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- name: "ensures 'git' command is available"
|
- name: "ensure '{{ git_executable }}' command is available"
|
||||||
include_tasks: git.yaml
|
include_tasks: git.yaml
|
||||||
when:
|
when:
|
||||||
- git_version is not defined
|
- git_version is not defined
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
python_executable: '/usr/bin/python'
|
python_exe_dirs:
|
||||||
|
- ~/.local/bin
|
||||||
|
- ~/bin
|
||||||
|
- /usr/local/bin
|
||||||
|
- /usr/bin
|
||||||
|
- /usr/local/sbin
|
||||||
|
- /usr/sbin
|
||||||
|
|
||||||
|
python_exe_path:
|
||||||
|
"{{ python_exe_dirs | join(':') }}"
|
||||||
|
|
||||||
python_version: '3'
|
python_version: '3'
|
||||||
python3_executable: '/usr/bin/python{{ python_version }}'
|
python_command: 'python'
|
||||||
|
python3_command: 'python{{ python_version }}'
|
||||||
|
|
||||||
|
python_packages: {}
|
||||||
|
python_info: {}
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
- name: "install '{{ python_command }}' packages"
|
||||||
|
become: true
|
||||||
|
package:
|
||||||
|
name: "{{ python_packages[python_command] | flatten }}"
|
||||||
|
when: 'python_command in python_packages'
|
||||||
|
register: install_python_packages
|
||||||
|
|
||||||
|
|
||||||
|
- name: "show installed packages"
|
||||||
|
debug: var=install_python_packages.changes
|
||||||
|
when: install_python_packages is changed
|
||||||
|
|
||||||
|
|
||||||
|
- name: "get '{{ python_command }}' full path"
|
||||||
|
shell: |
|
||||||
|
export PATH={{ python_exe_path }}
|
||||||
|
which "{{ python_command }}"
|
||||||
|
register: get_python_exe
|
||||||
|
|
||||||
|
|
||||||
|
- name: "get '{{ get_python_exe.stdout_lines | first }}' info"
|
||||||
|
script:
|
||||||
|
cmd: get_python_info.py --base --quiet
|
||||||
|
executable: '{{ get_python_exe.stdout_lines | first }}'
|
||||||
|
register: get_python_info
|
||||||
|
|
||||||
|
|
||||||
|
- name: "set python_info['{{ python_command }}'] fact"
|
||||||
|
set_fact:
|
||||||
|
python_info: >
|
||||||
|
{{ python_info | combine({python_command:
|
||||||
|
(get_python_info.stdout | from_json)}) }}
|
||||||
|
|
||||||
|
|
||||||
|
- name: "upgrade Pip command to latest version"
|
||||||
|
command: >
|
||||||
|
'{{ python_info[python_command].executable }}' -m pip install pip --upgrade --user
|
||||||
|
|
||||||
|
|
||||||
|
- name: "show python_info facts"
|
||||||
|
debug:
|
||||||
|
msg:
|
||||||
|
python_info: '{{ python_info }}'
|
|
@ -1,31 +1,5 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- name: "show ansible distro variables"
|
- name: "ensure '{{ python3_command }}' command is available"
|
||||||
debug:
|
include_tasks: python3.yaml
|
||||||
msg:
|
when: 'python3_command not in python_info'
|
||||||
ansible_architecture: '{{ ansible_architecture }}'
|
|
||||||
ansible_distribution: '{{ ansible_distribution }}'
|
|
||||||
ansible_distribution_major_version: '{{ ansible_distribution_major_version }}'
|
|
||||||
ansible_os_family: '{{ ansible_os_family }}'
|
|
||||||
|
|
||||||
|
|
||||||
- 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: "ensures '{{ python3_executable }}' command is available"
|
|
||||||
include_tasks: python3.yaml
|
|
||||||
when:
|
|
||||||
- python3_info is not defined
|
|
||||||
|
|
||||||
- name: "ensures '{{ python_executable }}' command is available"
|
|
||||||
include_tasks: python.yaml
|
|
||||||
when:
|
|
||||||
- python_info is not defined
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
- name: "show ansible distro variables"
|
||||||
|
debug:
|
||||||
|
msg:
|
||||||
|
ansible_architecture: '{{ ansible_architecture }}'
|
||||||
|
ansible_distribution: '{{ ansible_distribution }}'
|
||||||
|
ansible_distribution_major_version: '{{ ansible_distribution_major_version }}'
|
||||||
|
ansible_os_family: '{{ ansible_os_family }}'
|
||||||
|
|
||||||
|
|
||||||
|
- name: "include platform 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"
|
||||||
|
register: include_platform_vars
|
||||||
|
|
||||||
|
|
||||||
|
- name: "set Python platform facts"
|
||||||
|
set_fact:
|
||||||
|
python_platform:
|
||||||
|
'{{ (include_platform_vars.results | first).item | basename | splitext | first }}'
|
||||||
|
|
||||||
|
|
||||||
|
- name: 'show Python platform facts'
|
||||||
|
debug:
|
||||||
|
msg: '{{ (include_platform_vars.results | first).ansible_facts }}'
|
|
@ -1,41 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
- block:
|
|
||||||
- name: "get Python info for '{{ python_executable }}'"
|
|
||||||
script:
|
|
||||||
cmd: get_python_info.py --base --quiet
|
|
||||||
executable: '{{ python_executable }}'
|
|
||||||
register: get_python_info
|
|
||||||
|
|
||||||
rescue:
|
|
||||||
- block:
|
|
||||||
- name: "set '{{ python_alternative }}' as default alternative for python"
|
|
||||||
become: true
|
|
||||||
command: "alternatives --set python '{{ python_alternative }}'"
|
|
||||||
|
|
||||||
- name: "get Python info for '{{ python_executable }}'"
|
|
||||||
script:
|
|
||||||
cmd: get_python_info.py --base --quiet
|
|
||||||
executable: '{{ python_executable }}'
|
|
||||||
register: get_python_info
|
|
||||||
|
|
||||||
when:
|
|
||||||
- python_alternative is defined
|
|
||||||
|
|
||||||
- name: "report '{{ python_executable }}' failure"
|
|
||||||
fail:
|
|
||||||
msg: get_python_info
|
|
||||||
when: get_python_info is failed
|
|
||||||
|
|
||||||
|
|
||||||
- name: "set python_info fact"
|
|
||||||
set_fact:
|
|
||||||
python_executable: '{{ (get_python_info.stdout | from_json).executables | first }}'
|
|
||||||
python_info: '{{ get_python_info.stdout | from_json }}'
|
|
||||||
|
|
||||||
|
|
||||||
- name: "show '{{ python_executable }}' executables facts"
|
|
||||||
debug:
|
|
||||||
msg:
|
|
||||||
python_executable: '{{ python_executable }}'
|
|
||||||
python_info: '{{ python_info }}'
|
|
|
@ -1,6 +1,12 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- name: "validate python_version value: {{ python_version }}"
|
- name: "include platform variables"
|
||||||
|
include_tasks: platform.yaml
|
||||||
|
when:
|
||||||
|
- python_platform is not defined
|
||||||
|
|
||||||
|
|
||||||
|
- name: "validate Python version: {{ python_version }}"
|
||||||
assert:
|
assert:
|
||||||
that:
|
that:
|
||||||
- (python_version | string).split(".") | length >= 1
|
- (python_version | string).split(".") | length >= 1
|
||||||
|
@ -8,43 +14,33 @@
|
||||||
- (python_version | string).split(".")[0] == '3'
|
- (python_version | string).split(".")[0] == '3'
|
||||||
|
|
||||||
|
|
||||||
|
- include_tasks: install.yaml
|
||||||
|
vars:
|
||||||
|
python_command: '{{ python3_command }}'
|
||||||
|
|
||||||
|
|
||||||
|
- name: "set python3_executable fact"
|
||||||
|
set_fact:
|
||||||
|
python3_executable: '{{ python_info[python3_command].executable }}'
|
||||||
|
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
- name: "get Python info for '{{ python3_executable }}'"
|
- include_tasks: install.yaml
|
||||||
script:
|
when: python_command not in python_info
|
||||||
cmd: get_python_info.py --base --quiet
|
|
||||||
executable: '{{ python3_executable }}'
|
- name: "set python_executable fact"
|
||||||
register: get_python_info
|
set_fact:
|
||||||
|
python_executable: '{{ python_info[python_command].executable }}'
|
||||||
|
|
||||||
rescue:
|
rescue:
|
||||||
- block:
|
- name: "try setting python alternative to '{{ python_alternative }}'"
|
||||||
- name: "install Python '{{ python3_executable }}' packages"
|
become: true
|
||||||
become: true
|
command: "alternatives --set python '{{ python_alternative }}'"
|
||||||
package:
|
ignore_errors: yes
|
||||||
name: "{{ python3_packages }}"
|
when: 'python_alternative is defined'
|
||||||
|
|
||||||
- name: "get Python info for '{{ python3_executable }}'"
|
- include_tasks: install.yaml
|
||||||
script:
|
|
||||||
cmd: get_python_info.py --base --quiet
|
|
||||||
executable: '{{ python3_executable }}'
|
|
||||||
register: get_python_info
|
|
||||||
|
|
||||||
when:
|
- name: "set python_executable fact"
|
||||||
- python3_packages is defined
|
set_fact:
|
||||||
|
python_executable: '{{ python_info[python_command].executable }}'
|
||||||
- name: "report '{{ python3_executable }}' failure"
|
|
||||||
fail:
|
|
||||||
msg: get_python_info
|
|
||||||
when: get_python_info is failed
|
|
||||||
|
|
||||||
|
|
||||||
- name: "set python3_info fact"
|
|
||||||
set_fact:
|
|
||||||
python3_executable: '{{ (get_python_info.stdout | from_json).executables | first }}'
|
|
||||||
python3_info: '{{ get_python_info.stdout | from_json }}'
|
|
||||||
|
|
||||||
|
|
||||||
- name: "show '{{ python3_executable }}' facts"
|
|
||||||
debug:
|
|
||||||
msg:
|
|
||||||
python3_executable: '{{ python3_executable }}'
|
|
||||||
python3_info: '{{ python3_info }}'
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
python_packages:
|
||||||
|
python:
|
||||||
|
- bzip2-devel
|
||||||
|
- gcc
|
||||||
|
- make
|
||||||
|
- openssl-devel
|
||||||
|
- python2-devel
|
||||||
|
- python2-pip
|
||||||
|
- readline-devel
|
||||||
|
- sqlite-devel
|
||||||
|
- zlib-devel
|
||||||
|
python3:
|
||||||
|
- python3
|
||||||
|
python3.6:
|
||||||
|
- python36
|
|
@ -1,3 +1,14 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
python3_executable: python3
|
python_exe_dirs:
|
||||||
|
- ~/Library/Python/3.8/bin
|
||||||
|
- ~/Library/Python/3.7/bin
|
||||||
|
- ~/Library/Python/3.6/bin
|
||||||
|
- /Library/Frameworks/Python.framework/Versions/3.8/bin
|
||||||
|
- /Library/Frameworks/Python.framework/Versions/3.7/bin
|
||||||
|
- /Library/Frameworks/Python.framework/Versions/3.6/bin
|
||||||
|
- /usr/local/bin
|
||||||
|
- /usr/bin
|
||||||
|
- /bin
|
||||||
|
- /usr/sbin
|
||||||
|
- /sbin
|
||||||
|
|
|
@ -2,17 +2,8 @@
|
||||||
|
|
||||||
python_alternative: '/usr/bin/python3'
|
python_alternative: '/usr/bin/python3'
|
||||||
|
|
||||||
python3_package_name: "python{{ python_version | regex_replace('\\.', '') }}"
|
python_packages:
|
||||||
|
python3:
|
||||||
python3_packages:
|
- python3
|
||||||
- bzip2-devel
|
python3.6:
|
||||||
- gcc
|
- python36
|
||||||
- make
|
|
||||||
- openssl-devel
|
|
||||||
- readline-devel
|
|
||||||
- sqlite-devel
|
|
||||||
- zlib-devel
|
|
||||||
- '{{ python3_package_name }}'
|
|
||||||
- '{{ python3_package_name }}-devel'
|
|
||||||
- '{{ python3_package_name }}-setuptools'
|
|
||||||
- '{{ python3_package_name }}-virtualenv'
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
rsync_executable: rsync
|
|
@ -1,4 +0,0 @@
|
||||||
---
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- role: tobiko-common
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- name: "ensures 'rsync' command is available"
|
- name: "ensure '{{ rsync_executable }}' command is available"
|
||||||
include_tasks: rsync.yaml
|
include_tasks: rsync.yaml
|
||||||
when:
|
when:
|
||||||
- test_stage in ['all', 'pre-run', 'rsync']
|
- rsync_version is not defined
|
||||||
|
|
|
@ -1,23 +1,42 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
- name: "get installed rsync version" # noqa 303
|
- name: "get '{{ rsync_executable }}' full path"
|
||||||
shell: >
|
shell: >
|
||||||
rsync --version
|
which '{{ rsync_executable }}'
|
||||||
register: get_rsync_gersion
|
register: get_rsync_path
|
||||||
|
|
||||||
rescue:
|
rescue:
|
||||||
- name: "install rsync packages"
|
- name: "install Rsync packages"
|
||||||
become: true
|
become: true
|
||||||
package:
|
package:
|
||||||
name: rsync
|
name: rsync
|
||||||
|
|
||||||
- name: "get installed rsync version" # noqa 303
|
- name: "get '{{ rsync_executable }}' full path"
|
||||||
shell: >
|
shell: >
|
||||||
rsync --version
|
which '{{ rsync_executable }}'
|
||||||
register: get_rsync_gersion
|
register: get_rsync_path
|
||||||
|
|
||||||
|
|
||||||
- name: "show installed rsync version"
|
- name: "update rsync_executable fact"
|
||||||
|
set_fact:
|
||||||
|
rsync_executable: '{{ get_rsync_path.stdout_lines | first }}'
|
||||||
|
|
||||||
|
|
||||||
|
- name: "get Rsync version"
|
||||||
|
command: >
|
||||||
|
'{{ rsync_executable }}' --version
|
||||||
|
register:
|
||||||
|
get_rsync_version
|
||||||
|
|
||||||
|
|
||||||
|
- name: update rsync_version fact
|
||||||
|
set_fact:
|
||||||
|
rsync_version: '{{ get_rsync_version.stdout_lines | first }}'
|
||||||
|
|
||||||
|
|
||||||
|
- name: "show Rsync facts"
|
||||||
debug:
|
debug:
|
||||||
var: get_rsync_gersion.stdout_lines
|
msg:
|
||||||
|
rsync_executable: '{{ rsync_executable }}'
|
||||||
|
rsync_version: '{{ rsync_version }}'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
tox_executable: tox
|
tox_command: tox
|
||||||
tox_min_version: 3.4
|
tox_min_version: 3.4
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- role: tobiko-common
|
|
||||||
- role: tobiko-ensure-python3
|
- role: tobiko-ensure-python3
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
- name: "ensures '{{ tox_executable }}' command is available"
|
- name: "ensure '{{ tox_command }}' command is available"
|
||||||
include_tasks: tox.yaml
|
include_tasks: tox.yaml
|
||||||
when:
|
when:
|
||||||
- tox_version is not defined
|
- tox_executable is not defined
|
||||||
|
|
|
@ -3,22 +3,23 @@
|
||||||
- name: "ensure Tox is installed"
|
- name: "ensure Tox is installed"
|
||||||
command: |
|
command: |
|
||||||
'{{ python_executable }}' -m pip install --user 'tox>={{ tox_min_version }}'
|
'{{ python_executable }}' -m pip install --user 'tox>={{ tox_min_version }}'
|
||||||
register: result
|
register: install_tox
|
||||||
changed_when: "'Successfully installed' in result.stdout"
|
changed_when: "'Successfully installed' in install_tox.stdout"
|
||||||
|
|
||||||
|
|
||||||
- block:
|
- block:
|
||||||
- name: "get '{{ tox_executable }}' full path"
|
- name: "get '{{ tox_command }}' full path"
|
||||||
command: >
|
shell: |
|
||||||
which '{{ tox_executable }}'
|
export PATH={{ python_exe_path }}
|
||||||
|
which '{{ tox_command }}'
|
||||||
register: get_tox_path
|
register: get_tox_path
|
||||||
|
|
||||||
- name: "update tox_executable fact"
|
- name: "set tox_executable fact"
|
||||||
set_fact:
|
set_fact:
|
||||||
tox_executable: '{{ get_tox_path.stdout_lines | first }}'
|
tox_executable: '{{ get_tox_path.stdout_lines | first }}'
|
||||||
|
|
||||||
rescue:
|
rescue:
|
||||||
- name: "update tox_executable fact"
|
- name: "set tox_executable fact"
|
||||||
set_fact:
|
set_fact:
|
||||||
tox_executable: '{{ ansible_user_dir }}/.local/bin/tox'
|
tox_executable: '{{ ansible_user_dir }}/.local/bin/tox'
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
|
|
||||||
- name: "update tox_version fact"
|
- name: "update tox_version fact"
|
||||||
set_fact:
|
set_fact:
|
||||||
tox_version: '{{ get_tox_version.stdout }}'
|
tox_version: '{{ get_tox_version.stdout_lines | first }}'
|
||||||
|
|
||||||
|
|
||||||
- name: "show Tox facts"
|
- name: "show Tox facts"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
test_workflow: default
|
||||||
|
tox_step_name:
|
||||||
|
tox_step_index: 0
|
|
@ -1,4 +1,4 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- role: tobiko-tox
|
- role: tobiko-common
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
- name: "include test workflow steps for name '{{ test_workflow }}'"
|
||||||
|
include_vars: "{{ test_workflow }}.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
- name: "show test workflow steps"
|
||||||
|
debug: var=test_workflow_steps
|
||||||
|
|
||||||
|
|
||||||
|
- name: "run '{{ test_workflow }}' test steps"
|
||||||
|
include_tasks: run.yaml
|
||||||
|
loop: '{{ test_workflow_steps }}'
|
||||||
|
loop_control:
|
||||||
|
label: '{{ test_step.tox_description }}'
|
||||||
|
loop_var: test_step
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
- name: 'set step variables'
|
||||||
|
set_fact:
|
||||||
|
'{{ item.0 }}': '{{ item.1 }}'
|
||||||
|
loop: '{{ test_step | dictsort }}'
|
||||||
|
loop_control:
|
||||||
|
label: '{{ item.0 }} = {{ item.1 }}'
|
||||||
|
|
||||||
|
|
||||||
|
- name: 'set test step index'
|
||||||
|
set_fact:
|
||||||
|
tox_step_index: "{{ (tox_step_index | int) + 1 }}"
|
||||||
|
when:
|
||||||
|
- (test_workflow_steps | length) > 1
|
||||||
|
|
||||||
|
|
||||||
|
- name: '{{ test_step.tox_description }}'
|
||||||
|
include_role:
|
||||||
|
name: tobiko-tox
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
test_workflow_steps:
|
||||||
|
- tox_description: 'run test cases'
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
test_workflow_steps:
|
||||||
|
- tox_description: 'create workload resources'
|
||||||
|
tox_envlist: scenario
|
||||||
|
tox_step_name: create_resources
|
||||||
|
tox_environment:
|
||||||
|
TOBIKO_PREVENT_CREATE: no
|
||||||
|
|
||||||
|
- tox_description: 'run disruptive test cases'
|
||||||
|
tox_envlist: faults
|
||||||
|
tox_step_name: faults
|
||||||
|
|
||||||
|
- tox_description: 'verify workload resources'
|
||||||
|
tox_envlist: scenario
|
||||||
|
tox_step_name: verify_resources'
|
||||||
|
tox_environment:
|
||||||
|
TOBIKO_PREVENT_CREATE: yes
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
|
||||||
|
test_workflow_steps:
|
||||||
|
- tox_description: 'run functional test cases'
|
||||||
|
tox_envlist: functional
|
|
@ -0,0 +1,15 @@
|
||||||
|
test_workflow_steps:
|
||||||
|
- tox_description: 'run unit tests using Python 3.6'
|
||||||
|
tox_envlist: py36
|
||||||
|
python_version: '3.6'
|
||||||
|
bindep_profile: test py36
|
||||||
|
|
||||||
|
- tox_description: 'run unit tests using Python 3.7'
|
||||||
|
tox_envlist: py37
|
||||||
|
python_version: '3.7'
|
||||||
|
bindep_profile: test py37
|
||||||
|
|
||||||
|
- tox_description: 'run unit tests using Python 3.8'
|
||||||
|
tox_envlist: py38
|
||||||
|
python_version: '3.8'
|
||||||
|
bindep_profile: test py38
|
|
@ -1,22 +1,21 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
# Directory where test cases has been deployed to
|
# Directory where test cases has been deployed to
|
||||||
|
tox_description: 'run test cases'
|
||||||
tox_dir: '{{ test_dir | realpath }}'
|
tox_dir: '{{ test_dir | realpath }}'
|
||||||
tox_command: '{{ tox_executable | default(tox) }}'
|
tox_command: '{{ tox_executable | default(tox) }}'
|
||||||
tox_environment: {}
|
tox_environment: {}
|
||||||
tox_envlist:
|
tox_envlist:
|
||||||
tox_extra_args: ''
|
tox_extra_args: ''
|
||||||
tox_command_line: >
|
|
||||||
{{ tox_command }}
|
|
||||||
{% if tox_envlist %} -e {{ tox_envlist | quote }} {% endif %}
|
|
||||||
{{ tox_extra_args }}
|
|
||||||
|
|
||||||
tox_python: '{{ python3_executable }}'
|
tox_python: '{{ python3_executable }}'
|
||||||
tox_report_dir: '{{ test_report_dir | realpath }}'
|
tox_report_dir: '{{ test_report_dir | realpath }}'
|
||||||
tox_report_name: '{{ test_report_name }}{% if tox_envlist %}_{{ tox_envlist }}{% endif %}'
|
tox_step_name:
|
||||||
|
tox_step_index: 0
|
||||||
|
tox_report_name:
|
||||||
|
"{{ test_report_name }}{% if tox_step_index %}_{{ '{:02d}'.format(tox_step_index | int) }}{% endif %}{% if tox_step_name %}_{{ tox_step_name }}{% endif %}{% if tox_envlist %}_{{ tox_envlist }}{% endif %}"
|
||||||
tox_report_env:
|
tox_report_env:
|
||||||
TOBIKO_TEST_REPORT_DIR: '{{ tox_report_dir }}'
|
TOBIKO_TEST_REPORT_DIR: '{{ tox_report_dir }}'
|
||||||
TOBIKO_TEST_REPORT_NAME: '{{ tox_report_name }}'
|
TOBIKO_TEST_REPORT_NAME: '{{ tox_report_name }}'
|
||||||
|
|
||||||
tox_constrain_env:
|
tox_constrain_env: {}
|
||||||
PYTHON: '{{ tox_python }}'
|
|
||||||
|
|
|
@ -2,5 +2,7 @@
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
- role: tobiko-common
|
- role: tobiko-common
|
||||||
- role: tobiko-ensure-python3
|
- role: tobiko-bindep
|
||||||
|
vars:
|
||||||
|
bindep_file: '{{ tox_dir }}/bindep.txt'
|
||||||
- role: tobiko-ensure-tox
|
- role: tobiko-ensure-tox
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
---
|
---
|
||||||
|
|
||||||
|
- name: "set Tox command line fact"
|
||||||
|
set_fact:
|
||||||
|
tox_command_line: >
|
||||||
|
{{ tox_command }}
|
||||||
|
{% if tox_envlist %} -e {{ tox_envlist | quote }} {% endif %}
|
||||||
|
{{ tox_extra_args }}
|
||||||
|
|
||||||
|
|
||||||
- name: "normalize white spaces from Tox command line"
|
- name: "normalize white spaces from Tox command line"
|
||||||
set_fact:
|
set_fact:
|
||||||
tox_command_line: '{{ tox_command_line.split() | join(" ") }}'
|
tox_command_line: '{{ tox_command_line.split() | join(" ") }}'
|
||||||
|
|
||||||
|
|
||||||
- name: "run Tox on direcory '{{ tox_dir }}': '{{ tox_command_line }}'"
|
- name: "show tox variables"
|
||||||
|
debug:
|
||||||
|
msg:
|
||||||
|
tox_command_line: '{{ tox_command_line }}'
|
||||||
|
tox_description: '{{ tox_description }}'
|
||||||
|
tox_dir: '{{ tox_dir }}'
|
||||||
|
tox_environment: '{{ tox_environment | combine(tox_constrain_env) }}'
|
||||||
|
tox_report_env: '{{ tox_report_env | combine(tox_constrain_env) }}'
|
||||||
|
|
||||||
|
|
||||||
|
- name: "{{ tox_description }}"
|
||||||
command:
|
command:
|
||||||
chdir: '{{ tox_dir }}'
|
chdir: '{{ tox_dir }}'
|
||||||
cmd: '{{ tox_command_line }}'
|
cmd: '{{ tox_command_line }}'
|
||||||
|
|
|
@ -339,9 +339,9 @@ def get_bool_env(name):
|
||||||
value = get_env(name)
|
value = get_env(name)
|
||||||
if value:
|
if value:
|
||||||
value = str(value).lower()
|
value = str(value).lower()
|
||||||
if value in ["true", "1"]:
|
if value in ['true', '1', 'yes']:
|
||||||
return True
|
return True
|
||||||
elif value in ["false", '0']:
|
elif value in ['false', '0', 'no']:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
LOG.exception("Environment variable %r is not a boolean: %r",
|
LOG.exception("Environment variable %r is not a boolean: %r",
|
||||||
|
|
|
@ -25,8 +25,8 @@ LOG = logging.getLogger(__name__)
|
||||||
def main():
|
def main():
|
||||||
setup_logging()
|
setup_logging()
|
||||||
add_tobiko_plugin()
|
add_tobiko_plugin()
|
||||||
import_workspace()
|
ensure_workspace()
|
||||||
copy_ansible_inventory()
|
copy_inventory()
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(level=logging.DEBUG):
|
def setup_logging(level=logging.DEBUG):
|
||||||
|
@ -36,62 +36,82 @@ def setup_logging(level=logging.DEBUG):
|
||||||
format='%(name)-s: %(levelname)-7s %(asctime)-15s | %(message)s')
|
format='%(name)-s: %(levelname)-7s %(asctime)-15s | %(message)s')
|
||||||
|
|
||||||
|
|
||||||
def add_tobiko_plugin():
|
def add_tobiko_plugin(path=None):
|
||||||
add_plugin('tobiko', os.environ.get('IR_TOBIKO_PLUGIN'))
|
path = path or os.environ.get('IR_TOBIKO_PLUGIN')
|
||||||
|
if path:
|
||||||
|
add_plugin('tobiko', path)
|
||||||
|
|
||||||
|
|
||||||
def add_plugin(name, path):
|
def add_plugin(name, path):
|
||||||
if path:
|
path = normalize_path(path)
|
||||||
path = normalize_path(path)
|
if not os.path.isdir(path):
|
||||||
if os.path.isdir(path):
|
message = ("invalid plug-in '{}' directory: '{}'").format(name, path)
|
||||||
remove_plugin(name)
|
raise RuntimeError(message)
|
||||||
execute('ir plugin add "{}"', path)
|
|
||||||
else:
|
|
||||||
LOG.debug("Plug-in '%s' directory not found: '%s'", name, path)
|
|
||||||
|
|
||||||
else:
|
remove_plugin(name)
|
||||||
LOG.debug("Plug-in '%s' path not specified", name)
|
execute('ir plugin add "{}"', path)
|
||||||
|
LOG.info("plug-in '%s' added from path '%s'", name, path)
|
||||||
|
|
||||||
|
|
||||||
def remove_plugin(name):
|
def remove_plugin(name):
|
||||||
try:
|
try:
|
||||||
execute('ir plugin remove "{}"', name)
|
execute('ir plugin remove "{}"', name)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError as ex:
|
||||||
|
LOG.debug("plug-in '%s' not removed: %s", name, ex)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
LOG.info("plug-in '%s' removed", name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def import_workspace(filename=None):
|
def ensure_workspace(filename=None):
|
||||||
filename = filename or os.environ.get('IR_WORKSPACE_FILE')
|
filename = (filename or
|
||||||
if filename:
|
os.environ.get('IR_WORKSPACE_FILE') or
|
||||||
filename = normalize_path(filename)
|
'workspace.tgz')
|
||||||
if os.path.isfile(filename):
|
filename = normalize_path(filename)
|
||||||
try:
|
workspace = name_from_path(filename)
|
||||||
execute('ir workspace import "{}"', filename)
|
if os.path.isfile(filename):
|
||||||
except subprocess.CalledProcessError:
|
try:
|
||||||
# If file was already imported before we checkout to its
|
execute('ir workspace import "{}"', filename)
|
||||||
# workspace
|
except subprocess.CalledProcessError as ex:
|
||||||
workspace = name_from_path(filename)
|
LOG.debug("workspace file '%s' not imported: %s", filename, ex)
|
||||||
execute('ir workspace checkout "{}"', workspace)
|
|
||||||
else:
|
else:
|
||||||
LOG.debug("No such workspace file: '%s'", filename)
|
LOG.info("workspace imported from file '%s'", filename)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
LOG.debug('Workspace file not specified')
|
LOG.debug("workspace file not found: '%s'", filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
execute('ir workspace checkout "{}"', workspace)
|
||||||
|
except subprocess.CalledProcessError as ex:
|
||||||
|
LOG.debug("workspace '%s' not checked out: %s", workspace, ex)
|
||||||
|
else:
|
||||||
|
LOG.info("workspace '%s' checked out", workspace)
|
||||||
|
return
|
||||||
|
|
||||||
|
execute('infrared workspace checkout --create "{}"', workspace)
|
||||||
|
LOG.info("workspace '%s' created", workspace)
|
||||||
|
|
||||||
|
|
||||||
def copy_ansible_inventory(filename=None):
|
def copy_inventory(filename=None):
|
||||||
filename = filename or os.environ.get('ANSIBLE_INVENTORY')
|
filename = (filename or
|
||||||
if filename:
|
os.environ.get('ANSIBLE_INVENTORY') or
|
||||||
if os.path.isfile(filename):
|
'ansible_hosts')
|
||||||
destination = execute('ir workspace inventory')
|
if not os.path.isfile(filename):
|
||||||
if not os.path.exists(os.path.basename(destination)):
|
LOG.debug('inventary file not found: %r', filename)
|
||||||
os.makedirs(os.path.basename(destination))
|
return False
|
||||||
execute('cp {} {}', filename, destination)
|
|
||||||
else:
|
dest_file = execute('ir workspace inventory')
|
||||||
LOG.debug('Inventary file not found: %r', filename)
|
LOG.debug("got workspace inventory file: '%s'", dest_file)
|
||||||
else:
|
|
||||||
LOG.debug('Ansible inventory file not specified')
|
dest_dir = os.path.basename(dest_file)
|
||||||
|
if not os.path.exists(dest_dir):
|
||||||
|
os.makedirs(dest_dir)
|
||||||
|
LOG.info("directory created: '%s'", dest_dir)
|
||||||
|
|
||||||
|
execute('cp {} {}', filename, dest_file)
|
||||||
|
LOG.info("inventary file '%s' copied to '%s'", filename, dest_file)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def normalize_path(path):
|
def normalize_path(path):
|
||||||
|
@ -101,7 +121,7 @@ def normalize_path(path):
|
||||||
def execute(command, *args, **kwargs):
|
def execute(command, *args, **kwargs):
|
||||||
if args or kwargs:
|
if args or kwargs:
|
||||||
command = command.format(*args, **kwargs)
|
command = command.format(*args, **kwargs)
|
||||||
LOG.info('%s', command)
|
LOG.debug("execute command: '%s'", command)
|
||||||
return subprocess.check_output(command, shell=True,
|
return subprocess.check_output(command, shell=True,
|
||||||
universal_newlines=True)
|
universal_newlines=True)
|
||||||
|
|
||||||
|
|
8
tox.ini
8
tox.ini
|
@ -184,6 +184,8 @@ usedevelop = false
|
||||||
skipdist = true
|
skipdist = true
|
||||||
skip_install = true
|
skip_install = true
|
||||||
sitepackages = true
|
sitepackages = true
|
||||||
|
whitelist_externals =
|
||||||
|
rm
|
||||||
|
|
||||||
deps =
|
deps =
|
||||||
-r infrared-requirements.txt
|
-r infrared-requirements.txt
|
||||||
|
@ -206,7 +208,11 @@ commands_pre =
|
||||||
{envpython} {toxinidir}/tools/setup_infrared.py
|
{envpython} {toxinidir}/tools/setup_infrared.py
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
ir tobiko --tobiko-src-dir {toxinidir} {posargs}
|
rm -fR '{toxinidir}/test_results'
|
||||||
|
ir tobiko \
|
||||||
|
--tobiko-src-dir {toxinidir} \
|
||||||
|
--collect-dir '{toxinidir}/test_results' \
|
||||||
|
{posargs}
|
||||||
|
|
||||||
|
|
||||||
# --- documentation environments ----------------------------------------------
|
# --- documentation environments ----------------------------------------------
|
||||||
|
|
Loading…
Reference in New Issue