Merge pull request #66 from loles/keystone_resources
Keystone related resources
This commit is contained in:
commit
f161638d7f
29
example.py
29
example.py
@ -41,6 +41,11 @@ def deploy():
|
||||
haproxy_config = resource.create('haproxy_config', 'resources/haproxy', {'ip': '', 'ssh_key': '', 'ssh_user': '', 'configs_names':[], 'configs_ports':[], 'listen_ports':[], 'configs':[], 'config_dir': ''})
|
||||
haproxy_service = resource.create('haproxy_service', 'resources/docker_container/', {'image': 'tutum/haproxy', 'ports': [], 'host_binds': [], 'volume_binds':[], 'ip': '', 'ssh_key': '', 'ssh_user': ''})
|
||||
|
||||
admin_tenant = resource.create('admin_tenant', 'resources/keystone_tenant', {'keystone_host': '', 'keystone_port':'', 'login_user': 'admin', 'admin_token':'', 'tenant_name' : 'admin', 'ip': '', 'ssh_user': '', 'ssh_key': ''})
|
||||
admin_user = resource.create('admin_user', 'resources/keystone_user', {'keystone_host': '', 'keystone_port':'', 'login_user': 'admin', 'admin_token':'', 'tenant_name' : '', 'user_name': 'admin', 'user_password':'admin', 'ip': '', 'ssh_user': '', 'ssh_key': ''})
|
||||
admin_role = resource.create('admin_role', 'resources/keystone_role', {'keystone_host': '', 'keystone_port':'', 'login_user': 'admin', 'admin_token':'', 'tenant_name' : '', 'user_name': '', 'role_name': 'admin', 'ip': '', 'ssh_user': '', 'ssh_key': ''})
|
||||
keystone_service_endpoint = resource.create('keystone_service_endpoint', 'resources/keystone_service_endpoint/', {'ip':'', 'ssh_key' : '', 'ssh_user':'', 'admin_port':'', 'admin_token':'', 'adminurl':'http://{{ip}}:{{admin_port}}/v2.0', 'internalurl':'http://{{ip}}:{{port}}/v2.0', 'publicurl':'http://{{ip}}:{{port}}/v2.0', 'description':'OpenStack Identity Service', 'keystone_host':'', 'keystone_port':'', 'name':'keystone', 'port':'', 'type':'identity'})
|
||||
|
||||
|
||||
####
|
||||
# connections
|
||||
@ -81,6 +86,15 @@ def deploy():
|
||||
signals.connect(node2, haproxy_service)
|
||||
signals.connect(haproxy_config, haproxy_service, {'listen_ports': 'ports', 'config_dir': 'host_binds'})
|
||||
|
||||
#keystone configuration
|
||||
signals.connect(keystone_config1, admin_tenant)
|
||||
signals.connect(keystone_service1, admin_tenant, {'admin_port': 'keystone_port', 'ip': 'keystone_host'})
|
||||
signals.connect(admin_tenant, admin_user)
|
||||
signals.connect(admin_user, admin_role)
|
||||
signals.connect(keystone_config1, keystone_service_endpoint)
|
||||
signals.connect(keystone_service1, keystone_service_endpoint, {'ip': 'keystone_host','admin_port':'admin_port', 'port':'port'})
|
||||
signals.connect(keystone_service1, keystone_service_endpoint, {'admin_port': 'keystone_port'})
|
||||
|
||||
|
||||
has_errors = False
|
||||
for r in [node1,
|
||||
@ -94,7 +108,11 @@ def deploy():
|
||||
keystone_service2,
|
||||
haproxy_keystone_config,
|
||||
haproxy_config,
|
||||
haproxy_service]:
|
||||
haproxy_service,
|
||||
admin_tenant,
|
||||
admin_user,
|
||||
admin_role,
|
||||
keystone_service_endpoint]:
|
||||
errors = validation.validate_resource(r)
|
||||
if errors:
|
||||
has_errors = True
|
||||
@ -115,7 +133,12 @@ def deploy():
|
||||
actions.resource_action(keystone_service2, 'run')
|
||||
actions.resource_action(haproxy_config, 'run')
|
||||
actions.resource_action(haproxy_service, 'run')
|
||||
time.sleep(10) #TODO fix haproxy to wait until it's ready
|
||||
|
||||
actions.resource_action(admin_tenant, 'run')
|
||||
actions.resource_action(admin_user, 'run')
|
||||
actions.resource_action(admin_role, 'run')
|
||||
actions.resource_action(keystone_service_endpoint, 'run')
|
||||
|
||||
# test working configuration
|
||||
requests.get('http://%s:%s' % (keystone_service1.args['ip'].value, keystone_service1.args['port'].value))
|
||||
@ -132,6 +155,10 @@ def undeploy():
|
||||
resources = map(resource.wrap_resource, db.get_list('resource'))
|
||||
resources = {r.name: r for r in resources}
|
||||
|
||||
actions.resource_action(resources['keystone_service_endpoint'], 'remove')
|
||||
actions.resource_action(resources['admin_role'], 'remove')
|
||||
actions.resource_action(resources['admin_user'], 'remove')
|
||||
actions.resource_action(resources['admin_tenant'], 'remove')
|
||||
actions.resource_action(resources['haproxy_service'], 'remove')
|
||||
actions.resource_action(resources['haproxy_config'], 'remove')
|
||||
actions.resource_action(resources['keystone_service2'], 'remove')
|
||||
|
311
library/keystone_service.py
Normal file
311
library/keystone_service.py
Normal file
@ -0,0 +1,311 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copied from: https://github.com/openstack-ansible/openstack-ansible-modules/blob/master/keystone_service
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: keystone_service
|
||||
short_description: Manage OpenStack Identity (keystone) service endpoints
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- name of service (e.g., keystone)
|
||||
required: yes
|
||||
type:
|
||||
description:
|
||||
- type of service (e.g., identity)
|
||||
required: yes
|
||||
description:
|
||||
description:
|
||||
- description of service (e.g., Identity Service)
|
||||
required: yes
|
||||
public_url:
|
||||
description:
|
||||
- public url of service.
|
||||
- 'Alias: I(url)'
|
||||
- 'Alias: I(publicurl)'
|
||||
required: yes
|
||||
internal_url:
|
||||
description:
|
||||
- internal url of service.
|
||||
- 'Alias: I(internalurl)'
|
||||
required: no
|
||||
default: value of public_url
|
||||
admin_url:
|
||||
description:
|
||||
- admin url of service.
|
||||
- 'Alias: I(adminurl)'
|
||||
required: no
|
||||
default: value of public_url
|
||||
insecure:
|
||||
description:
|
||||
- allow use of self-signed SSL certificates
|
||||
required: no
|
||||
choices: [ "yes", "no" ]
|
||||
region:
|
||||
description:
|
||||
- region of service
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the resource
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
|
||||
|
||||
|
||||
requirements: [ python-keystoneclient ]
|
||||
author: Lorin Hochstein
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
examples:
|
||||
keystone_service: >
|
||||
name=keystone
|
||||
type=identity
|
||||
description="Keystone Identity Service"
|
||||
publicurl=http://192.168.206.130:5000/v2.0
|
||||
internalurl=http://192.168.206.130:5000/v2.0
|
||||
adminurl=http://192.168.206.130:35357/v2.0
|
||||
|
||||
keystone_service: >
|
||||
name=glance
|
||||
type=image
|
||||
description="Glance Identity Service"
|
||||
url=http://192.168.206.130:9292
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
from keystoneclient.v2_0 import client
|
||||
except ImportError:
|
||||
keystoneclient_found = False
|
||||
else:
|
||||
keystoneclient_found = True
|
||||
|
||||
import traceback
|
||||
|
||||
|
||||
def authenticate(endpoint, token, login_user, login_password, tenant_name,
|
||||
insecure):
|
||||
"""Return a keystone client object"""
|
||||
|
||||
if token:
|
||||
return client.Client(endpoint=endpoint, token=token, insecure=insecure)
|
||||
else:
|
||||
return client.Client(auth_url=endpoint, username=login_user,
|
||||
password=login_password, tenant_name=tenant_name,
|
||||
insecure=insecure)
|
||||
|
||||
def get_service(keystone, name):
|
||||
""" Retrieve a service by name """
|
||||
services = [x for x in keystone.services.list() if x.name == name]
|
||||
count = len(services)
|
||||
if count == 0:
|
||||
raise KeyError("No keystone services with name %s" % name)
|
||||
elif count > 1:
|
||||
raise ValueError("%d services with name %s" % (count, name))
|
||||
else:
|
||||
return services[0]
|
||||
|
||||
|
||||
def get_endpoint(keystone, name):
|
||||
""" Retrieve a service endpoint by name """
|
||||
service = get_service(keystone, name)
|
||||
endpoints = [x for x in keystone.endpoints.list()
|
||||
if x.service_id == service.id]
|
||||
count = len(endpoints)
|
||||
if count == 0:
|
||||
raise KeyError("No keystone endpoints with service name %s" % name)
|
||||
elif count > 1:
|
||||
raise ValueError("%d endpoints with service name %s" % (count, name))
|
||||
else:
|
||||
return endpoints[0]
|
||||
|
||||
|
||||
def ensure_service_present(keystone, name, service_type, description,
|
||||
check_mode):
|
||||
""" Ensure the service is present and has the right values
|
||||
|
||||
Returns a pair, where the first element is a boolean that indicates
|
||||
a state change, and the second element is the service uuid, or None
|
||||
if running in check mode"""
|
||||
service = None
|
||||
try:
|
||||
service = get_service(keystone, name)
|
||||
except:
|
||||
# Service doesn't exist yet, we'll need to create one
|
||||
pass
|
||||
else:
|
||||
# See if it matches exactly
|
||||
if service.name == name and \
|
||||
service.type == service_type and \
|
||||
service.description == description:
|
||||
|
||||
# Same, no changes needed
|
||||
return (False, service.id)
|
||||
|
||||
# At this point, we know we will need to make a change
|
||||
if check_mode:
|
||||
return (True, None)
|
||||
|
||||
if service is None:
|
||||
service = keystone.services.create(name=name,
|
||||
service_type=service_type,
|
||||
description=description)
|
||||
return (True, service.id)
|
||||
else:
|
||||
msg = "keystone v2 API doesn't support updating services"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def ensure_endpoint_present(keystone, name, public_url, internal_url,
|
||||
admin_url, region, check_mode):
|
||||
""" Ensure the service endpoint is present and have the right values
|
||||
|
||||
Assumes the service object has already been created at this point"""
|
||||
|
||||
service = get_service(keystone, name)
|
||||
endpoint = None
|
||||
try:
|
||||
endpoint = get_endpoint(keystone, name)
|
||||
except:
|
||||
# Endpoint doesn't exist yet, we'll need to create one
|
||||
pass
|
||||
else:
|
||||
# See if it matches
|
||||
if endpoint.publicurl == public_url and \
|
||||
endpoint.adminurl == admin_url and \
|
||||
endpoint.internalurl == internal_url and \
|
||||
endpoint.region == region:
|
||||
|
||||
# Same, no changes needed
|
||||
return (False, endpoint.id)
|
||||
|
||||
# At this point, we know we will need to make a change
|
||||
if check_mode:
|
||||
return (True, None)
|
||||
|
||||
if endpoint is None:
|
||||
endpoint = keystone.endpoints.create(region=region,
|
||||
service_id=service.id,
|
||||
publicurl=public_url,
|
||||
adminurl=admin_url,
|
||||
internalurl=internal_url)
|
||||
return (True, endpoint.id)
|
||||
else:
|
||||
msg = "keystone v2 API doesn't support updating endpoints"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def ensure_service_absent(keystone, name, check_mode):
|
||||
""" Ensure the service is absent"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def ensure_endpoint_absent(keystone, name, check_mode):
|
||||
""" Ensure the service endpoint """
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def dispatch(keystone, name, service_type, description, public_url,
|
||||
internal_url, admin_url, region, state, check_mode):
|
||||
|
||||
if state == 'present':
|
||||
(service_changed, service_id) = ensure_service_present(keystone,
|
||||
name,
|
||||
service_type,
|
||||
description,
|
||||
check_mode)
|
||||
|
||||
(endpoint_changed, endpoint_id) = ensure_endpoint_present(
|
||||
keystone,
|
||||
name,
|
||||
public_url,
|
||||
internal_url,
|
||||
admin_url,
|
||||
region,
|
||||
check_mode)
|
||||
return dict(changed=service_changed or endpoint_changed,
|
||||
service_id=service_id,
|
||||
endpoint_id=endpoint_id)
|
||||
elif state == 'absent':
|
||||
endpoint_changed = ensure_endpoint_absent(keystone, name, check_mode)
|
||||
service_changed = ensure_service_absent(keystone, name, check_mode)
|
||||
return dict(changed=service_changed or endpoint_changed)
|
||||
else:
|
||||
raise ValueError("Code should never reach here")
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
name=dict(required=True),
|
||||
type=dict(required=True),
|
||||
description=dict(required=False),
|
||||
public_url=dict(required=True, aliases=['url', 'publicurl']),
|
||||
internal_url=dict(required=False, aliases=['internalurl']),
|
||||
admin_url=dict(required=False, aliases=['adminurl']),
|
||||
region=dict(required=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
endpoint=dict(required=False,
|
||||
default="http://127.0.0.1:35357/v2.0",
|
||||
aliases=['auth_url']),
|
||||
token=dict(required=False),
|
||||
insecure=dict(required=False, default=False, choices=BOOLEANS),
|
||||
|
||||
login_user=dict(required=False),
|
||||
login_password=dict(required=False),
|
||||
tenant_name=dict(required=False, aliases=['tenant'])
|
||||
),
|
||||
supports_check_mode=True,
|
||||
mutually_exclusive=[['token', 'login_user'],
|
||||
['token', 'login_password'],
|
||||
['token', 'tenant_name']]
|
||||
)
|
||||
|
||||
endpoint = module.params['endpoint']
|
||||
token = module.params['token']
|
||||
login_user = module.params['login_user']
|
||||
login_password = module.params['login_password']
|
||||
tenant_name = module.params['tenant_name']
|
||||
insecure = module.boolean(module.params['insecure'])
|
||||
name = module.params['name']
|
||||
service_type = module.params['type']
|
||||
description = module.params['description']
|
||||
public_url = module.params['public_url']
|
||||
internal_url = module.params['internal_url']
|
||||
if internal_url is None:
|
||||
internal_url = public_url
|
||||
admin_url = module.params['admin_url']
|
||||
if admin_url is None:
|
||||
admin_url = public_url
|
||||
region = module.params['region']
|
||||
state = module.params['state']
|
||||
|
||||
keystone = authenticate(endpoint, token, login_user, login_password,
|
||||
tenant_name, insecure)
|
||||
check_mode = module.check_mode
|
||||
|
||||
try:
|
||||
d = dispatch(keystone, name, service_type, description,
|
||||
public_url, internal_url, admin_url, region, state,
|
||||
check_mode)
|
||||
except Exception:
|
||||
if check_mode:
|
||||
# If we have a failure in check mode
|
||||
module.exit_json(changed=True,
|
||||
msg="exception: %s" % traceback.format_exc())
|
||||
else:
|
||||
module.fail_json(msg=traceback.format_exc())
|
||||
else:
|
||||
module.exit_json(**d)
|
||||
|
||||
|
||||
# this is magic, see lib/ansible/module_common.py
|
||||
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||
if __name__ == '__main__':
|
||||
main()
|
5
resources/keystone_role/actions/remove.yml
Normal file
5
resources/keystone_role/actions/remove.yml
Normal file
@ -0,0 +1,5 @@
|
||||
- hosts: [{{ ip }}]
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: keystone role
|
||||
keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} user={{user_name}} tenant={{tenant_name}} role={{role_name}} state=absent
|
5
resources/keystone_role/actions/run.yml
Normal file
5
resources/keystone_role/actions/run.yml
Normal file
@ -0,0 +1,5 @@
|
||||
- hosts: [{{ ip }}]
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: keystone role
|
||||
keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} user={{user_name}} tenant={{tenant_name}} role={{role_name}} state=present
|
33
resources/keystone_role/meta.yaml
Normal file
33
resources/keystone_role/meta.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
id: keystone_user
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
keystone_host:
|
||||
schema: str!
|
||||
value:
|
||||
keystone_port:
|
||||
schema: int!
|
||||
value:
|
||||
admin_token:
|
||||
schema: str!
|
||||
value:
|
||||
user_name:
|
||||
schema: str!
|
||||
value:
|
||||
tenant_name:
|
||||
schema: str!
|
||||
value:
|
||||
role_name:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
||||
tags: [resource/keystone_user, resources/keystone]
|
@ -4,7 +4,7 @@ version: 1.0.0
|
||||
input:
|
||||
image:
|
||||
schema: str!
|
||||
value: kollaglue/centos-rdo-keystone
|
||||
value: kollaglue/centos-rdo-j-keystone
|
||||
config_dir:
|
||||
schema: str!
|
||||
value: /etc/solar/keystone
|
||||
|
1
resources/keystone_service_endpoint/actions/remove.yaml
Normal file
1
resources/keystone_service_endpoint/actions/remove.yaml
Normal file
@ -0,0 +1 @@
|
||||
#todo
|
19
resources/keystone_service_endpoint/actions/run.yaml
Normal file
19
resources/keystone_service_endpoint/actions/run.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
- hosts: [{{ ip }}]
|
||||
sudo: yes
|
||||
vars:
|
||||
ip: {{ip}}
|
||||
port: {{port}}
|
||||
admin_port: {{admin_port}}
|
||||
tasks:
|
||||
- name: keystone service and endpoint
|
||||
keystone_service:
|
||||
token: {{admin_token}}
|
||||
name: {{name}}
|
||||
type: {{type}}
|
||||
description: {{description}}
|
||||
publicurl: {{publicurl}}
|
||||
internalurl: {{internalurl}}
|
||||
adminurl: {{adminurl}}
|
||||
region: "RegionOne"
|
||||
state: present
|
||||
endpoint: http://{{keystone_host}}:{{keystone_port}}/v2.0/
|
49
resources/keystone_service_endpoint/meta.yaml
Normal file
49
resources/keystone_service_endpoint/meta.yaml
Normal file
@ -0,0 +1,49 @@
|
||||
id: keystone_user
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
keystone_host:
|
||||
schema: str!
|
||||
value:
|
||||
keystone_port:
|
||||
schema: int!
|
||||
value:
|
||||
admin_token:
|
||||
schema: str!
|
||||
value:
|
||||
port:
|
||||
schema: int!
|
||||
value:
|
||||
admin_port:
|
||||
schema: int!
|
||||
value:
|
||||
name:
|
||||
schema: str!
|
||||
value:
|
||||
type:
|
||||
schema: str!
|
||||
value:
|
||||
description:
|
||||
schema: str!
|
||||
value:
|
||||
publicurl:
|
||||
schema: str!
|
||||
value:
|
||||
internalurl:
|
||||
schema: str!
|
||||
value:
|
||||
adminurl:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
||||
tags: [resource/keystone_tenant, resources/keystone]
|
||||
|
5
resources/keystone_tenant/actions/remove.yml
Normal file
5
resources/keystone_tenant/actions/remove.yml
Normal file
@ -0,0 +1,5 @@
|
||||
- hosts: [{{ ip }}]
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: keystone tenant
|
||||
keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} tenant={{tenant_name}} state=absent
|
5
resources/keystone_tenant/actions/run.yml
Normal file
5
resources/keystone_tenant/actions/run.yml
Normal file
@ -0,0 +1,5 @@
|
||||
- hosts: [{{ ip }}]
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: keystone tenant
|
||||
keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} tenant={{tenant_name}} state=present
|
27
resources/keystone_tenant/meta.yaml
Normal file
27
resources/keystone_tenant/meta.yaml
Normal file
@ -0,0 +1,27 @@
|
||||
id: keystone_user
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
keystone_host:
|
||||
schema: str!
|
||||
value:
|
||||
keystone_port:
|
||||
schema: int!
|
||||
value:
|
||||
admin_token:
|
||||
schema: str!
|
||||
value:
|
||||
tenant_name:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
||||
tags: [resource/keystone_tenant, resources/keystone]
|
@ -2,5 +2,4 @@
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: keystone user
|
||||
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ user={{user_name}} tenant={{tenant_name}} state=absent
|
||||
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ tenant={{tenant_name}} state=absent
|
||||
keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} user={{user_name}} password={{user_password}} tenant={{tenant_name}} state=absent
|
||||
|
@ -2,5 +2,4 @@
|
||||
sudo: yes
|
||||
tasks:
|
||||
- name: keystone user
|
||||
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ tenant={{tenant_name}} state=present
|
||||
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ user={{user_name}} password={{user_password}} tenant={{tenant_name}} state=present
|
||||
keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} user={{user_name}} password={{user_password}} tenant={{tenant_name}} state=present
|
||||
|
@ -8,10 +8,7 @@ input:
|
||||
keystone_port:
|
||||
schema: int!
|
||||
value:
|
||||
login_user:
|
||||
schema: str!
|
||||
value:
|
||||
login_token:
|
||||
admin_token:
|
||||
schema: str!
|
||||
value:
|
||||
user_name:
|
||||
|
Loading…
Reference in New Issue
Block a user