Workload Status
This commit is contained in:
parent
0d6ad4872a
commit
783d5ccc85
@ -18,7 +18,7 @@ import os
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from charmhelpers.fetch import apt_install
|
from charmhelpers.fetch import apt_install, apt_update
|
||||||
from charmhelpers.core.hookenv import (
|
from charmhelpers.core.hookenv import (
|
||||||
log,
|
log,
|
||||||
ERROR,
|
ERROR,
|
||||||
@ -29,6 +29,7 @@ from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
|
|||||||
try:
|
try:
|
||||||
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
|
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
apt_update(fatal=True)
|
||||||
apt_install('python-jinja2', fatal=True)
|
apt_install('python-jinja2', fatal=True)
|
||||||
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
|
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import sys
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
import traceback
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from charmhelpers.contrib.network import ip
|
from charmhelpers.contrib.network import ip
|
||||||
@ -34,6 +35,8 @@ from charmhelpers.core import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.core.hookenv import (
|
from charmhelpers.core.hookenv import (
|
||||||
|
action_fail,
|
||||||
|
action_set,
|
||||||
config,
|
config,
|
||||||
log as juju_log,
|
log as juju_log,
|
||||||
charm_dir,
|
charm_dir,
|
||||||
@ -51,7 +54,8 @@ from charmhelpers.contrib.storage.linux.lvm import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.network.ip import (
|
from charmhelpers.contrib.network.ip import (
|
||||||
get_ipv6_addr
|
get_ipv6_addr,
|
||||||
|
is_ipv6,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.python.packages import (
|
from charmhelpers.contrib.python.packages import (
|
||||||
@ -516,6 +520,12 @@ def sync_db_with_multi_ipv6_addresses(database, database_user,
|
|||||||
relation_prefix=None):
|
relation_prefix=None):
|
||||||
hosts = get_ipv6_addr(dynamic_only=False)
|
hosts = get_ipv6_addr(dynamic_only=False)
|
||||||
|
|
||||||
|
if config('vip'):
|
||||||
|
vips = config('vip').split()
|
||||||
|
for vip in vips:
|
||||||
|
if vip and is_ipv6(vip):
|
||||||
|
hosts.append(vip)
|
||||||
|
|
||||||
kwargs = {'database': database,
|
kwargs = {'database': database,
|
||||||
'username': database_user,
|
'username': database_user,
|
||||||
'hostname': json.dumps(hosts)}
|
'hostname': json.dumps(hosts)}
|
||||||
@ -921,3 +931,47 @@ def incomplete_relation_data(configs, required_interfaces):
|
|||||||
for i in incomplete_relations:
|
for i in incomplete_relations:
|
||||||
incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
|
incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
|
||||||
return incomplete_context_data
|
return incomplete_context_data
|
||||||
|
|
||||||
|
|
||||||
|
def do_action_openstack_upgrade(package, upgrade_callback, configs):
|
||||||
|
"""Perform action-managed OpenStack upgrade.
|
||||||
|
|
||||||
|
Upgrades packages to the configured openstack-origin version and sets
|
||||||
|
the corresponding action status as a result.
|
||||||
|
|
||||||
|
If the charm was installed from source we cannot upgrade it.
|
||||||
|
For backwards compatibility a config flag (action-managed-upgrade) must
|
||||||
|
be set for this code to run, otherwise a full service level upgrade will
|
||||||
|
fire on config-changed.
|
||||||
|
|
||||||
|
@param package: package name for determining if upgrade available
|
||||||
|
@param upgrade_callback: function callback to charm's upgrade function
|
||||||
|
@param configs: templating object derived from OSConfigRenderer class
|
||||||
|
|
||||||
|
@return: True if upgrade successful; False if upgrade failed or skipped
|
||||||
|
"""
|
||||||
|
ret = False
|
||||||
|
|
||||||
|
if git_install_requested():
|
||||||
|
action_set({'outcome': 'installed from source, skipped upgrade.'})
|
||||||
|
else:
|
||||||
|
if openstack_upgrade_available(package):
|
||||||
|
if config('action-managed-upgrade'):
|
||||||
|
juju_log('Upgrading OpenStack release')
|
||||||
|
|
||||||
|
try:
|
||||||
|
upgrade_callback(configs=configs)
|
||||||
|
action_set({'outcome': 'success, upgrade completed.'})
|
||||||
|
ret = True
|
||||||
|
except:
|
||||||
|
action_set({'outcome': 'upgrade failed, see traceback.'})
|
||||||
|
action_set({'traceback': traceback.format_exc()})
|
||||||
|
action_fail('do_openstack_upgrade resulted in an '
|
||||||
|
'unexpected error')
|
||||||
|
else:
|
||||||
|
action_set({'outcome': 'action-managed-upgrade config is '
|
||||||
|
'False, skipped upgrade.'})
|
||||||
|
else:
|
||||||
|
action_set({'outcome': 'no upgrade available.'})
|
||||||
|
|
||||||
|
return ret
|
||||||
|
@ -13,6 +13,7 @@ from charmhelpers.core.hookenv import (
|
|||||||
service_name,
|
service_name,
|
||||||
unit_get,
|
unit_get,
|
||||||
UnregisteredHookError,
|
UnregisteredHookError,
|
||||||
|
status_set,
|
||||||
)
|
)
|
||||||
from charmhelpers.core.host import (
|
from charmhelpers.core.host import (
|
||||||
restart_on_change,
|
restart_on_change,
|
||||||
@ -32,6 +33,7 @@ from charmhelpers.contrib.openstack.utils import (
|
|||||||
git_install_requested,
|
git_install_requested,
|
||||||
openstack_upgrade_available,
|
openstack_upgrade_available,
|
||||||
os_requires_version,
|
os_requires_version,
|
||||||
|
os_workload_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.storage.linux.ceph import (
|
from charmhelpers.contrib.storage.linux.ceph import (
|
||||||
@ -66,6 +68,8 @@ from nova_compute_utils import (
|
|||||||
assert_charm_supports_ipv6,
|
assert_charm_supports_ipv6,
|
||||||
manage_ovs,
|
manage_ovs,
|
||||||
install_hugepages,
|
install_hugepages,
|
||||||
|
REQUIRED_INTERFACES,
|
||||||
|
check_optional_relations,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.network.ip import (
|
from charmhelpers.contrib.network.ip import (
|
||||||
@ -86,28 +90,38 @@ CONFIGS = register_configs()
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('install.real')
|
@hooks.hook('install.real')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
def install():
|
def install():
|
||||||
|
status_set('maintenance', 'Executing pre-install')
|
||||||
execd_preinstall()
|
execd_preinstall()
|
||||||
configure_installation_source(config('openstack-origin'))
|
configure_installation_source(config('openstack-origin'))
|
||||||
|
|
||||||
|
status_set('maintenance', 'Installing apt packages')
|
||||||
apt_update()
|
apt_update()
|
||||||
apt_install(determine_packages(), fatal=True)
|
apt_install(determine_packages(), fatal=True)
|
||||||
|
|
||||||
|
status_set('maintenance', 'Git install')
|
||||||
git_install(config('openstack-origin-git'))
|
git_install(config('openstack-origin-git'))
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook('config-changed')
|
@hooks.hook('config-changed')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
def config_changed():
|
def config_changed():
|
||||||
if config('prefer-ipv6'):
|
if config('prefer-ipv6'):
|
||||||
|
status_set('maintenance', 'configuring ipv6')
|
||||||
assert_charm_supports_ipv6()
|
assert_charm_supports_ipv6()
|
||||||
|
|
||||||
global CONFIGS
|
global CONFIGS
|
||||||
if git_install_requested():
|
if git_install_requested():
|
||||||
if config_value_changed('openstack-origin-git'):
|
if config_value_changed('openstack-origin-git'):
|
||||||
|
status_set('maintenance', 'Running Git install')
|
||||||
git_install(config('openstack-origin-git'))
|
git_install(config('openstack-origin-git'))
|
||||||
else:
|
else:
|
||||||
if openstack_upgrade_available('nova-common'):
|
if openstack_upgrade_available('nova-common'):
|
||||||
|
status_set('maintenance', 'Running openstack upgrade')
|
||||||
CONFIGS = do_openstack_upgrade()
|
CONFIGS = do_openstack_upgrade()
|
||||||
|
|
||||||
sysctl_dict = config('sysctl')
|
sysctl_dict = config('sysctl')
|
||||||
@ -117,11 +131,13 @@ def config_changed():
|
|||||||
if migration_enabled() and config('migration-auth-type') == 'ssh':
|
if migration_enabled() and config('migration-auth-type') == 'ssh':
|
||||||
# Check-in with nova-c-c and register new ssh key, if it has just been
|
# Check-in with nova-c-c and register new ssh key, if it has just been
|
||||||
# generated.
|
# generated.
|
||||||
|
status_set('maintenance', 'SSH key exchange')
|
||||||
initialize_ssh_keys()
|
initialize_ssh_keys()
|
||||||
import_authorized_keys()
|
import_authorized_keys()
|
||||||
|
|
||||||
if config('enable-resize') is True:
|
if config('enable-resize') is True:
|
||||||
enable_shell(user='nova')
|
enable_shell(user='nova')
|
||||||
|
status_set('maintenance', 'SSH key exchange')
|
||||||
initialize_ssh_keys(user='nova')
|
initialize_ssh_keys(user='nova')
|
||||||
import_authorized_keys(user='nova', prefix='nova')
|
import_authorized_keys(user='nova', prefix='nova')
|
||||||
else:
|
else:
|
||||||
@ -132,6 +148,7 @@ def config_changed():
|
|||||||
fix_path_ownership(fp, user='nova')
|
fix_path_ownership(fp, user='nova')
|
||||||
|
|
||||||
if config('virt-type').lower() == 'lxd':
|
if config('virt-type').lower() == 'lxd':
|
||||||
|
status_set('maintenance', 'Configure LXD')
|
||||||
configure_lxd(user='nova')
|
configure_lxd(user='nova')
|
||||||
|
|
||||||
[compute_joined(rid) for rid in relation_ids('cloud-compute')]
|
[compute_joined(rid) for rid in relation_ids('cloud-compute')]
|
||||||
@ -148,6 +165,8 @@ def config_changed():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('amqp-relation-joined')
|
@hooks.hook('amqp-relation-joined')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
def amqp_joined(relation_id=None):
|
def amqp_joined(relation_id=None):
|
||||||
relation_set(relation_id=relation_id,
|
relation_set(relation_id=relation_id,
|
||||||
username=config('rabbit-user'),
|
username=config('rabbit-user'),
|
||||||
@ -156,6 +175,8 @@ def amqp_joined(relation_id=None):
|
|||||||
|
|
||||||
@hooks.hook('amqp-relation-changed')
|
@hooks.hook('amqp-relation-changed')
|
||||||
@hooks.hook('amqp-relation-departed')
|
@hooks.hook('amqp-relation-departed')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
def amqp_changed():
|
def amqp_changed():
|
||||||
if 'amqp' not in CONFIGS.complete_contexts():
|
if 'amqp' not in CONFIGS.complete_contexts():
|
||||||
@ -171,6 +192,8 @@ def amqp_changed():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('shared-db-relation-joined')
|
@hooks.hook('shared-db-relation-joined')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
def db_joined(rid=None):
|
def db_joined(rid=None):
|
||||||
if is_relation_made('pgsql-db'):
|
if is_relation_made('pgsql-db'):
|
||||||
# error, postgresql is used
|
# error, postgresql is used
|
||||||
@ -186,6 +209,8 @@ def db_joined(rid=None):
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('pgsql-db-relation-joined')
|
@hooks.hook('pgsql-db-relation-joined')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
def pgsql_db_joined():
|
def pgsql_db_joined():
|
||||||
if is_relation_made('shared-db'):
|
if is_relation_made('shared-db'):
|
||||||
# raise error
|
# raise error
|
||||||
@ -198,6 +223,8 @@ def pgsql_db_joined():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('shared-db-relation-changed')
|
@hooks.hook('shared-db-relation-changed')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
def db_changed():
|
def db_changed():
|
||||||
if 'shared-db' not in CONFIGS.complete_contexts():
|
if 'shared-db' not in CONFIGS.complete_contexts():
|
||||||
@ -207,6 +234,8 @@ def db_changed():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('pgsql-db-relation-changed')
|
@hooks.hook('pgsql-db-relation-changed')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
def postgresql_db_changed():
|
def postgresql_db_changed():
|
||||||
if 'pgsql-db' not in CONFIGS.complete_contexts():
|
if 'pgsql-db' not in CONFIGS.complete_contexts():
|
||||||
@ -216,6 +245,8 @@ def postgresql_db_changed():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('image-service-relation-changed')
|
@hooks.hook('image-service-relation-changed')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
def image_service_changed():
|
def image_service_changed():
|
||||||
if 'image-service' not in CONFIGS.complete_contexts():
|
if 'image-service' not in CONFIGS.complete_contexts():
|
||||||
@ -258,7 +289,10 @@ def compute_changed():
|
|||||||
|
|
||||||
@hooks.hook('ceph-relation-joined')
|
@hooks.hook('ceph-relation-joined')
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
def ceph_joined():
|
def ceph_joined():
|
||||||
|
status_set('maintenance', 'Installing apt packages')
|
||||||
apt_install(filter_installed_packages(['ceph-common']), fatal=True)
|
apt_install(filter_installed_packages(['ceph-common']), fatal=True)
|
||||||
# Bug 1427660
|
# Bug 1427660
|
||||||
service_restart('libvirt-bin')
|
service_restart('libvirt-bin')
|
||||||
@ -273,6 +307,8 @@ def get_ceph_request():
|
|||||||
|
|
||||||
@hooks.hook('ceph-relation-changed')
|
@hooks.hook('ceph-relation-changed')
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
def ceph_changed():
|
def ceph_changed():
|
||||||
if 'ceph' not in CONFIGS.complete_contexts():
|
if 'ceph' not in CONFIGS.complete_contexts():
|
||||||
log('ceph relation incomplete. Peer not ready?')
|
log('ceph relation incomplete. Peer not ready?')
|
||||||
@ -306,6 +342,8 @@ def ceph_changed():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('ceph-relation-broken')
|
@hooks.hook('ceph-relation-broken')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
def ceph_broken():
|
def ceph_broken():
|
||||||
service = service_name()
|
service = service_name()
|
||||||
delete_keyring(service=service)
|
delete_keyring(service=service)
|
||||||
@ -316,6 +354,8 @@ def ceph_broken():
|
|||||||
'image-service-relation-broken',
|
'image-service-relation-broken',
|
||||||
'shared-db-relation-broken',
|
'shared-db-relation-broken',
|
||||||
'pgsql-db-relation-broken')
|
'pgsql-db-relation-broken')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
def relation_broken():
|
def relation_broken():
|
||||||
CONFIGS.write_all()
|
CONFIGS.write_all()
|
||||||
@ -324,6 +364,7 @@ def relation_broken():
|
|||||||
@hooks.hook('upgrade-charm')
|
@hooks.hook('upgrade-charm')
|
||||||
def upgrade_charm():
|
def upgrade_charm():
|
||||||
# NOTE: ensure psutil install for hugepages configuration
|
# NOTE: ensure psutil install for hugepages configuration
|
||||||
|
status_set('maintenance', 'Installing apt packages')
|
||||||
apt_install(filter_installed_packages(['python-psutil']))
|
apt_install(filter_installed_packages(['python-psutil']))
|
||||||
for r_id in relation_ids('amqp'):
|
for r_id in relation_ids('amqp'):
|
||||||
amqp_joined(relation_id=r_id)
|
amqp_joined(relation_id=r_id)
|
||||||
@ -339,6 +380,8 @@ def nova_ceilometer_relation_changed():
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('zeromq-configuration-relation-joined')
|
@hooks.hook('zeromq-configuration-relation-joined')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@os_requires_version('kilo', 'nova-common')
|
@os_requires_version('kilo', 'nova-common')
|
||||||
def zeromq_configuration_relation_joined(relid=None):
|
def zeromq_configuration_relation_joined(relid=None):
|
||||||
relation_set(relation_id=relid,
|
relation_set(relation_id=relid,
|
||||||
@ -347,6 +390,8 @@ def zeromq_configuration_relation_joined(relid=None):
|
|||||||
|
|
||||||
|
|
||||||
@hooks.hook('zeromq-configuration-relation-changed')
|
@hooks.hook('zeromq-configuration-relation-changed')
|
||||||
|
@os_workload_status(CONFIGS, REQUIRED_INTERFACES,
|
||||||
|
charm_func=check_optional_relations)
|
||||||
@restart_on_change(restart_map())
|
@restart_on_change(restart_map())
|
||||||
def zeromq_configuration_relation_changed():
|
def zeromq_configuration_relation_changed():
|
||||||
CONFIGS.write(NOVA_CONF)
|
CONFIGS.write(NOVA_CONF)
|
||||||
|
@ -32,6 +32,7 @@ from charmhelpers.core.hookenv import (
|
|||||||
relation_get,
|
relation_get,
|
||||||
DEBUG,
|
DEBUG,
|
||||||
INFO,
|
INFO,
|
||||||
|
status_get,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.core.templating import render
|
from charmhelpers.core.templating import render
|
||||||
@ -48,7 +49,8 @@ from charmhelpers.contrib.openstack.utils import (
|
|||||||
git_src_dir,
|
git_src_dir,
|
||||||
git_pip_venv_dir,
|
git_pip_venv_dir,
|
||||||
git_yaml_value,
|
git_yaml_value,
|
||||||
os_release
|
os_release,
|
||||||
|
set_os_workload_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.python.packages import (
|
from charmhelpers.contrib.python.packages import (
|
||||||
@ -248,6 +250,14 @@ LIBVIRT_URIS = {
|
|||||||
'lxc': 'lxc:///',
|
'lxc': 'lxc:///',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The interface is said to be satisfied if anyone of the interfaces in the
|
||||||
|
# list has a complete context.
|
||||||
|
REQUIRED_INTERFACES = {
|
||||||
|
'database': ['shared-db', 'pgsql-db'],
|
||||||
|
'message': ['amqp', 'zeromq-configuration'],
|
||||||
|
'image': ['image-service'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def resource_map():
|
def resource_map():
|
||||||
'''
|
'''
|
||||||
@ -829,3 +839,12 @@ def install_hugepages():
|
|||||||
)
|
)
|
||||||
subprocess.check_call('/etc/init.d/qemu-hugefsdir')
|
subprocess.check_call('/etc/init.d/qemu-hugefsdir')
|
||||||
subprocess.check_call(['update-rc.d', 'qemu-hugefsdir', 'defaults'])
|
subprocess.check_call(['update-rc.d', 'qemu-hugefsdir', 'defaults'])
|
||||||
|
|
||||||
|
|
||||||
|
def check_optional_relations(configs):
|
||||||
|
if relation_ids('ceph'):
|
||||||
|
required_interfaces = {'image-backend': 'ceph'}
|
||||||
|
set_os_workload_status(configs, required_interfaces)
|
||||||
|
return status_get()
|
||||||
|
else:
|
||||||
|
return 'unknown', 'No optional relations'
|
||||||
|
@ -6,6 +6,9 @@ import yaml
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from mock import patch, MagicMock
|
from mock import patch, MagicMock
|
||||||
|
|
||||||
|
patch('charmhelpers.contrib.openstack.utils.set_os_workload_status').start()
|
||||||
|
patch('charmhelpers.core.hookenv.status_set').start()
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
'''
|
'''
|
||||||
|
Loading…
Reference in New Issue
Block a user