This commit is contained in:
parent
71cad95079
commit
641faf30bf
@ -16,3 +16,10 @@ pipeline:
|
|||||||
commands:
|
commands:
|
||||||
- pip install tox==3.14.6
|
- pip install tox==3.14.6
|
||||||
- tox -e ${TOXENV}
|
- tox -e ${TOXENV}
|
||||||
|
---
|
||||||
|
pipeline:
|
||||||
|
integration:
|
||||||
|
image: python:${IMAGE}
|
||||||
|
commands:
|
||||||
|
- pip install -e .
|
||||||
|
- cd integration-test-role && molecule test
|
||||||
|
8
.envrc.sample
Normal file
8
.envrc.sample
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# The Hetzner Cloud API token for managing our instances
|
||||||
|
export HCLOUD_TOKEN=$(pass show logins/hetzner/cicd/api_key)
|
||||||
|
|
||||||
|
# So molecule will show credentials in the logs
|
||||||
|
export MOLECULE_NO_LOG=False
|
||||||
|
|
||||||
|
# For unique integration testing VPS names
|
||||||
|
export INSTANCE_UUID=$RANDOM
|
15
Makefile
Normal file
15
Makefile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
DIRECTORY := integration-test-role
|
||||||
|
DRIVER := hetznercloud
|
||||||
|
|
||||||
|
default: test
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf $(DIRECTORY)
|
||||||
|
|
||||||
|
init: clean
|
||||||
|
@molecule init role -d $(DRIVER) $(DIRECTORY)
|
||||||
|
|
||||||
|
test:
|
||||||
|
@cd $(DIRECTORY) && molecule test
|
||||||
|
|
||||||
|
.PHONY: clean init test
|
21
README.md
21
README.md
@ -26,3 +26,24 @@ TODO.
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
The [LGPL](https://www.gnu.org/licenses/lgpl-3.0.en.html) license.
|
The [LGPL](https://www.gnu.org/licenses/lgpl-3.0.en.html) license.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Unit tests and such.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ pip install tox
|
||||||
|
$ tox -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests.
|
||||||
|
|
||||||
|
Only doable by [Autonomic Cooperative](https://autonomic.zone/) members.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo apt install -y direnv
|
||||||
|
$ cp .envrc.sample .envrc
|
||||||
|
$ direnv allow
|
||||||
|
$ pip install -e .
|
||||||
|
$ cd integration-test-role && molecule test
|
||||||
|
```
|
||||||
|
29
integration-test-role/.travis.yml
Normal file
29
integration-test-role/.travis.yml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
language: python
|
||||||
|
python: "2.7"
|
||||||
|
|
||||||
|
# Use the new container infrastructure
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
# Install ansible
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- python-pip
|
||||||
|
|
||||||
|
install:
|
||||||
|
# Install ansible
|
||||||
|
- pip install ansible
|
||||||
|
|
||||||
|
# Check ansible version
|
||||||
|
- ansible --version
|
||||||
|
|
||||||
|
# Create ansible.cfg with correct roles_path
|
||||||
|
- printf '[defaults]\nroles_path=../' >ansible.cfg
|
||||||
|
|
||||||
|
script:
|
||||||
|
# Basic role syntax check
|
||||||
|
- ansible-playbook tests/test.yml -i tests/inventory --syntax-check
|
||||||
|
|
||||||
|
notifications:
|
||||||
|
webhooks: https://galaxy.ansible.com/api/v1/notifications/
|
33
integration-test-role/.yamllint
Normal file
33
integration-test-role/.yamllint
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
# Based on ansible-lint config
|
||||||
|
extends: default
|
||||||
|
|
||||||
|
rules:
|
||||||
|
braces:
|
||||||
|
max-spaces-inside: 1
|
||||||
|
level: error
|
||||||
|
brackets:
|
||||||
|
max-spaces-inside: 1
|
||||||
|
level: error
|
||||||
|
colons:
|
||||||
|
max-spaces-after: -1
|
||||||
|
level: error
|
||||||
|
commas:
|
||||||
|
max-spaces-after: -1
|
||||||
|
level: error
|
||||||
|
comments: disable
|
||||||
|
comments-indentation: disable
|
||||||
|
document-start: disable
|
||||||
|
empty-lines:
|
||||||
|
max: 3
|
||||||
|
level: error
|
||||||
|
hyphens:
|
||||||
|
level: error
|
||||||
|
indentation: disable
|
||||||
|
key-duplicates: enable
|
||||||
|
line-length: disable
|
||||||
|
new-line-at-end-of-file: disable
|
||||||
|
new-lines:
|
||||||
|
type: unix
|
||||||
|
trailing-spaces: disable
|
||||||
|
truthy: disable
|
38
integration-test-role/README.md
Normal file
38
integration-test-role/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
Role Name
|
||||||
|
=========
|
||||||
|
|
||||||
|
A brief description of the role goes here.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
|
||||||
|
|
||||||
|
Role Variables
|
||||||
|
--------------
|
||||||
|
|
||||||
|
A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
|
||||||
|
|
||||||
|
Dependencies
|
||||||
|
------------
|
||||||
|
|
||||||
|
A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
|
||||||
|
|
||||||
|
Example Playbook
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
|
||||||
|
|
||||||
|
- hosts: servers
|
||||||
|
roles:
|
||||||
|
- { role: username.rolename, x: 42 }
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
BSD
|
||||||
|
|
||||||
|
Author Information
|
||||||
|
------------------
|
||||||
|
|
||||||
|
An optional section for the role authors to include contact information, or a website (HTML is not allowed).
|
2
integration-test-role/defaults/main.yml
Normal file
2
integration-test-role/defaults/main.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
# defaults file for integration-test-role
|
2
integration-test-role/handlers/main.yml
Normal file
2
integration-test-role/handlers/main.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
# handlers file for integration-test-role
|
53
integration-test-role/meta/main.yml
Normal file
53
integration-test-role/meta/main.yml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
galaxy_info:
|
||||||
|
author: your name
|
||||||
|
description: your role description
|
||||||
|
company: your company (optional)
|
||||||
|
|
||||||
|
# If the issue tracker for your role is not on github, uncomment the
|
||||||
|
# next line and provide a value
|
||||||
|
# issue_tracker_url: http://example.com/issue/tracker
|
||||||
|
|
||||||
|
# Choose a valid license ID from https://spdx.org - some suggested licenses:
|
||||||
|
# - BSD-3-Clause (default)
|
||||||
|
# - MIT
|
||||||
|
# - GPL-2.0-or-later
|
||||||
|
# - GPL-3.0-only
|
||||||
|
# - Apache-2.0
|
||||||
|
# - CC-BY-4.0
|
||||||
|
license: license (GPL-2.0-or-later, MIT, etc)
|
||||||
|
|
||||||
|
min_ansible_version: 2.9
|
||||||
|
|
||||||
|
# If this a Container Enabled role, provide the minimum Ansible Container version.
|
||||||
|
# min_ansible_container_version:
|
||||||
|
|
||||||
|
#
|
||||||
|
# Provide a list of supported platforms, and for each platform a list of versions.
|
||||||
|
# If you don't wish to enumerate all versions for a particular platform, use 'all'.
|
||||||
|
# To view available platforms and versions (or releases), visit:
|
||||||
|
# https://galaxy.ansible.com/api/v1/platforms/
|
||||||
|
#
|
||||||
|
# platforms:
|
||||||
|
# - name: Fedora
|
||||||
|
# versions:
|
||||||
|
# - all
|
||||||
|
# - 25
|
||||||
|
# - name: SomePlatform
|
||||||
|
# versions:
|
||||||
|
# - all
|
||||||
|
# - 1.0
|
||||||
|
# - 7
|
||||||
|
# - 99.99
|
||||||
|
|
||||||
|
galaxy_tags: []
|
||||||
|
# List tags for your role here, one per line. A tag is a keyword that describes
|
||||||
|
# and categorizes the role. Users find roles by searching for tags. Be sure to
|
||||||
|
# remove the '[]' above, if you add tags to this list.
|
||||||
|
#
|
||||||
|
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
|
||||||
|
# Maximum 20 tags per role.
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
|
# List your role dependencies here, one per line. Be sure to remove the '[]' above,
|
||||||
|
# if you add dependencies to this list.
|
||||||
|
|
16
integration-test-role/molecule/default/INSTALL.rst
Normal file
16
integration-test-role/molecule/default/INSTALL.rst
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
***************************************
|
||||||
|
Hetzner Cloud plugin installation guide
|
||||||
|
***************************************
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
============
|
||||||
|
|
||||||
|
* Ansible >= 2.9
|
||||||
|
* ``HCLOUD_TOKEN`` exposed in your environment
|
||||||
|
|
||||||
|
Install
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install molecule-hetznercloud
|
7
integration-test-role/molecule/default/converge.yml
Normal file
7
integration-test-role/molecule/default/converge.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
- name: Converge
|
||||||
|
hosts: all
|
||||||
|
tasks:
|
||||||
|
- name: "Include integration-test-role"
|
||||||
|
include_role:
|
||||||
|
name: "integration-test-role"
|
85
integration-test-role/molecule/default/create.yml
Normal file
85
integration-test-role/molecule/default/create.yml
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
- name: Create
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
no_log: "{{ molecule_no_log }}"
|
||||||
|
vars:
|
||||||
|
ssh_port: 22
|
||||||
|
ssh_user: root
|
||||||
|
ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
|
||||||
|
tasks:
|
||||||
|
- name: Create SSH key
|
||||||
|
openssh_keypair:
|
||||||
|
path: "{{ ssh_path }}"
|
||||||
|
force: true
|
||||||
|
register: generated_ssh_key
|
||||||
|
|
||||||
|
- name: Register the SSH key name
|
||||||
|
set_fact:
|
||||||
|
ssh_key_name: "molecule-generated-{{ 12345 | random | to_uuid }}"
|
||||||
|
|
||||||
|
- name: Register SSH key for test instance(s)
|
||||||
|
hcloud_ssh_key:
|
||||||
|
name: "{{ ssh_key_name }}"
|
||||||
|
public_key: "{{ generated_ssh_key.public_key }}"
|
||||||
|
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create molecule instance(s)
|
||||||
|
hcloud_server:
|
||||||
|
name: "{{ item.name }}"
|
||||||
|
server_type: "{{ item.server_type }}"
|
||||||
|
ssh_keys:
|
||||||
|
- "{{ ssh_key_name }}"
|
||||||
|
volumes: "{{ item.volumes | default(omit) }}"
|
||||||
|
image: "{{ item.image }}"
|
||||||
|
location: "{{ item.location | default(omit) }}"
|
||||||
|
datacenter: "{{ item.datacenter | default(omit) }}"
|
||||||
|
user_data: "{{ item.user_data | default(omit) }}"
|
||||||
|
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||||
|
state: present
|
||||||
|
register: server
|
||||||
|
with_items: "{{ molecule_yml.platforms }}"
|
||||||
|
async: 7200
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: Wait for instance(s) creation to complete
|
||||||
|
async_status:
|
||||||
|
jid: "{{ item.ansible_job_id }}"
|
||||||
|
register: hetzner_jobs
|
||||||
|
until: hetzner_jobs.finished
|
||||||
|
retries: 300
|
||||||
|
with_items: "{{ server.results }}"
|
||||||
|
|
||||||
|
- name: Populate instance config dict
|
||||||
|
set_fact:
|
||||||
|
instance_conf_dict: {
|
||||||
|
'instance': "{{ item.hcloud_server.name }}",
|
||||||
|
'ssh_key_name': "{{ ssh_key_name }}",
|
||||||
|
'address': "{{ item.hcloud_server.ipv4_address }}",
|
||||||
|
'user': "{{ ssh_user }}",
|
||||||
|
'port': "{{ ssh_port }}",
|
||||||
|
'identity_file': "{{ ssh_path }}", }
|
||||||
|
with_items: "{{ hetzner_jobs.results }}"
|
||||||
|
register: instance_config_dict
|
||||||
|
when: server.changed | bool
|
||||||
|
|
||||||
|
- name: Convert instance config dict to a list
|
||||||
|
set_fact:
|
||||||
|
instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"
|
||||||
|
when: server.changed | bool
|
||||||
|
|
||||||
|
- name: Dump instance config
|
||||||
|
copy:
|
||||||
|
content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}"
|
||||||
|
dest: "{{ molecule_instance_config }}"
|
||||||
|
when: server.changed | bool
|
||||||
|
|
||||||
|
- name: Wait for SSH
|
||||||
|
wait_for:
|
||||||
|
port: "{{ ssh_port }}"
|
||||||
|
host: "{{ item.address }}"
|
||||||
|
search_regex: SSH
|
||||||
|
delay: 10
|
||||||
|
with_items: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
|
56
integration-test-role/molecule/default/destroy.yml
Normal file
56
integration-test-role/molecule/default/destroy.yml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
- name: Destroy
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
no_log: "{{ molecule_no_log }}"
|
||||||
|
tasks:
|
||||||
|
- name: Populate the instance config
|
||||||
|
block:
|
||||||
|
- name: Populate instance config from file
|
||||||
|
set_fact:
|
||||||
|
instance_conf: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"
|
||||||
|
skip_instances: false
|
||||||
|
rescue:
|
||||||
|
- name: Populate instance config when file missing
|
||||||
|
set_fact:
|
||||||
|
instance_conf: {}
|
||||||
|
skip_instances: true
|
||||||
|
|
||||||
|
- name: Destroy molecule instance(s)
|
||||||
|
hcloud_server:
|
||||||
|
name: "{{ item.instance }}"
|
||||||
|
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||||
|
state: absent
|
||||||
|
register: server
|
||||||
|
with_items: "{{ instance_conf }}"
|
||||||
|
when: not skip_instances
|
||||||
|
async: 7200
|
||||||
|
poll: 0
|
||||||
|
|
||||||
|
- name: Wait for instance(s) deletion to complete
|
||||||
|
async_status:
|
||||||
|
jid: "{{ item.ansible_job_id }}"
|
||||||
|
register: hetzner_jobs
|
||||||
|
until: hetzner_jobs.finished
|
||||||
|
retries: 300
|
||||||
|
with_items: "{{ server.results }}"
|
||||||
|
|
||||||
|
- name: Remove registered SSH key
|
||||||
|
hcloud_ssh_key:
|
||||||
|
name: "{{ instance_conf[0].ssh_key_name }}"
|
||||||
|
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||||
|
state: absent
|
||||||
|
when:
|
||||||
|
- not skip_instances
|
||||||
|
- instance_conf | length # must contain at least one instance
|
||||||
|
|
||||||
|
- name: Populate instance config
|
||||||
|
set_fact:
|
||||||
|
instance_conf: {}
|
||||||
|
|
||||||
|
- name: Dump instance config
|
||||||
|
copy:
|
||||||
|
content: "{{ instance_conf | molecule_to_yaml | molecule_header }}"
|
||||||
|
dest: "{{ molecule_instance_config }}"
|
||||||
|
when: server.changed | bool
|
13
integration-test-role/molecule/default/molecule.yml
Normal file
13
integration-test-role/molecule/default/molecule.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
dependency:
|
||||||
|
name: galaxy
|
||||||
|
driver:
|
||||||
|
name: hetznercloud
|
||||||
|
platforms:
|
||||||
|
- name: "molecule-hetznercloud-integration-test-${INSTANCE_UUID}"
|
||||||
|
server_type: cx11
|
||||||
|
image: debian-10
|
||||||
|
provisioner:
|
||||||
|
name: ansible
|
||||||
|
verifier:
|
||||||
|
name: ansible
|
9
integration-test-role/molecule/default/prepare.yml
Normal file
9
integration-test-role/molecule/default/prepare.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
- name: Prepare
|
||||||
|
hosts: all
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Install python for Ansible
|
||||||
|
raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal python-zipstream)
|
||||||
|
become: true
|
||||||
|
changed_when: false
|
9
integration-test-role/molecule/default/verify.yml
Normal file
9
integration-test-role/molecule/default/verify.yml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
# This is an example playbook to execute Ansible tests.
|
||||||
|
|
||||||
|
- name: Verify
|
||||||
|
hosts: all
|
||||||
|
tasks:
|
||||||
|
- name: Example assertion
|
||||||
|
assert:
|
||||||
|
that: true
|
2
integration-test-role/tasks/main.yml
Normal file
2
integration-test-role/tasks/main.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
# tasks file for integration-test-role
|
2
integration-test-role/tests/inventory
Normal file
2
integration-test-role/tests/inventory
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
localhost
|
||||||
|
|
5
integration-test-role/tests/test.yml
Normal file
5
integration-test-role/tests/test.yml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- hosts: localhost
|
||||||
|
remote_user: root
|
||||||
|
roles:
|
||||||
|
- integration-test-role
|
2
integration-test-role/vars/main.yml
Normal file
2
integration-test-role/vars/main.yml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
# vars file for integration-test-role
|
@ -11,10 +11,8 @@
|
|||||||
ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
|
ssh_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Create SSH key
|
- name: Create SSH key
|
||||||
user:
|
openssh_keypair:
|
||||||
name: "{{ lookup('env', 'USER') }}"
|
path: "{{ ssh_path }}"
|
||||||
generate_ssh_key: true
|
|
||||||
ssh_key_file: "{{ ssh_path }}"
|
|
||||||
force: true
|
force: true
|
||||||
register: generated_ssh_key
|
register: generated_ssh_key
|
||||||
|
|
||||||
@ -25,7 +23,8 @@
|
|||||||
- name: Register SSH key for test instance(s)
|
- name: Register SSH key for test instance(s)
|
||||||
hcloud_ssh_key:
|
hcloud_ssh_key:
|
||||||
name: "{{ ssh_key_name }}"
|
name: "{{ ssh_key_name }}"
|
||||||
public_key: "{{ generated_ssh_key.ssh_public_key }}"
|
public_key: "{{ generated_ssh_key.public_key }}"
|
||||||
|
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
- name: Create molecule instance(s)
|
- name: Create molecule instance(s)
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
- name: Remove registered SSH key
|
- name: Remove registered SSH key
|
||||||
hcloud_ssh_key:
|
hcloud_ssh_key:
|
||||||
name: "{{ instance_conf[0].ssh_key_name }}"
|
name: "{{ instance_conf[0].ssh_key_name }}"
|
||||||
|
api_token: "{{ lookup('env', 'HCLOUD_TOKEN') }}"
|
||||||
state: absent
|
state: absent
|
||||||
when:
|
when:
|
||||||
- not skip_instances
|
- not skip_instances
|
||||||
|
@ -42,11 +42,11 @@ classifiers =
|
|||||||
Topic :: Utilities
|
Topic :: Utilities
|
||||||
keywords =
|
keywords =
|
||||||
ansible
|
ansible
|
||||||
roles
|
hetznercloud
|
||||||
testing
|
|
||||||
molecule
|
molecule
|
||||||
plugin
|
plugin
|
||||||
hetznercloud
|
roles
|
||||||
|
testing
|
||||||
verifier
|
verifier
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
@ -59,6 +59,7 @@ setup_requires =
|
|||||||
setuptools_scm >= 3.5.0
|
setuptools_scm >= 3.5.0
|
||||||
setuptools_scm_git_archive >= 1.1
|
setuptools_scm_git_archive >= 1.1
|
||||||
install_requires =
|
install_requires =
|
||||||
|
hcloud >= 1.6.3, < 2
|
||||||
molecule >= 3.0.3, <= 3.1
|
molecule >= 3.0.3, <= 3.1
|
||||||
pyyaml >= 5.3.1, < 6
|
pyyaml >= 5.3.1, < 6
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user