Add converted version of block and object storage
This patch extends the previous 'Don't use merge.py for overcloud' commit with the cinder-storage.yaml and swift-storage.yaml templates. Requirements for this to deploy: 1. Block and object storage images have to be built (overcloud-cinder-volume and overcloud-swift-storage) 2. The images have to be loaded by devtest_overcloud.sh OVERCLOUD_CINDER_ID=$(load-image -d $TRIPLEO_ROOT/overcloud-cinder-volume.qcow2) OVERCLOUD_SWIFT_ID=$(load-image -d $TRIPLEO_ROOT/overcloud-swift-storage.qcow2) Change-Id: I45f9d9f051970a83e26c0fd924d7c98276958113
This commit is contained in:

committed by
Tomas Sedovic

parent
bcdcc28cb6
commit
24f40d5312
195
cinder-storage.yaml
Normal file
195
cinder-storage.yaml
Normal file
@@ -0,0 +1,195 @@
|
||||
heat_template_version: 2014-10-16
|
||||
description: 'Common Block Storage Configuration'
|
||||
parameters:
|
||||
AdminPassword:
|
||||
default: ''
|
||||
type: string
|
||||
Image:
|
||||
default: overcloud-cinder-volume
|
||||
type: string
|
||||
CinderISCSIHelper:
|
||||
default: tgtadm
|
||||
description: The iSCSI helper to use with cinder.
|
||||
type: string
|
||||
CinderLVMLoopDeviceSize:
|
||||
default: 5000
|
||||
description: The size of the loopback file used by the cinder LVM driver.
|
||||
type: number
|
||||
CinderPassword:
|
||||
default: unset
|
||||
description: The password for the cinder service account, used by cinder-api.
|
||||
hidden: true
|
||||
type: string
|
||||
ControllerIP:
|
||||
default: ''
|
||||
type: string
|
||||
ExtraConfig:
|
||||
default: {}
|
||||
description: |
|
||||
Additional configuration to inject into the cluster. The JSON should have
|
||||
the following structure:
|
||||
{"FILEKEY":
|
||||
{"config":
|
||||
[{"section": "SECTIONNAME",
|
||||
"values":
|
||||
[{"option": "OPTIONNAME",
|
||||
"value": "VALUENAME"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
For instance:
|
||||
{"nova":
|
||||
{"config":
|
||||
[{"section": "default",
|
||||
"values":
|
||||
[{"option": "force_config_drive",
|
||||
"value": "always"
|
||||
}
|
||||
]
|
||||
},
|
||||
{"section": "cells",
|
||||
"values":
|
||||
[{"option": "driver",
|
||||
"value": "nova.cells.rpc_driver.CellsRPCDriver"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
type: json
|
||||
Flavor:
|
||||
default: baremetal
|
||||
description: Flavor for block storage nodes to request when deploying.
|
||||
type: string
|
||||
GlancePort:
|
||||
default: "9292"
|
||||
description: Glance port.
|
||||
type: string
|
||||
KeyName:
|
||||
default: default
|
||||
description: Name of an existing EC2 KeyPair to enable SSH access to the instances
|
||||
type: string
|
||||
NeutronEnableTunnelling:
|
||||
default: "True"
|
||||
type: string
|
||||
NeutronNetworkType:
|
||||
default: gre
|
||||
type: string
|
||||
NeutronPassword:
|
||||
default: ''
|
||||
type: string
|
||||
NeutronPublicInterface:
|
||||
default: eth0
|
||||
type: string
|
||||
RabbitPassword:
|
||||
default: ''
|
||||
type: string
|
||||
RabbitUserName:
|
||||
default: ''
|
||||
type: string
|
||||
SnmpdReadonlyUserName:
|
||||
default: ro_snmp_user
|
||||
description: The user name for SNMPd with readonly rights running on all Overcloud nodes
|
||||
type: string
|
||||
SnmpdReadonlyUserPassword:
|
||||
default: unset
|
||||
description: The user password for SNMPd with readonly rights running on all Overcloud nodes
|
||||
type: string
|
||||
hidden: true
|
||||
|
||||
resources:
|
||||
BlockStorage:
|
||||
type: OS::Nova::Server
|
||||
properties:
|
||||
image:
|
||||
{get_param: Image}
|
||||
flavor: {get_param: Flavor}
|
||||
key_name: {get_param: KeyName}
|
||||
user_data_format: SOFTWARE_CONFIG
|
||||
networks:
|
||||
- network: ctlplane
|
||||
BlockStorageDeployment:
|
||||
type: OS::Heat::StructuredDeployment
|
||||
properties:
|
||||
server: {get_resource: BlockStorage}
|
||||
config: {get_resource: BlockStorageConfig}
|
||||
input_values:
|
||||
controller_host: {get_param: ControllerIP}
|
||||
cinder_dsn: {list_join: ['', ['mysql://cinder:unset@', {get_param: ControllerIP} , '/cinder']]}
|
||||
neutron_local_ip: {get_attr: [BlockStorage , networks, ctlplane, 0]}
|
||||
snmpd_readonly_user_name: {get_param: SnmpdReadonlyUserName}
|
||||
snmpd_readonly_user_password: {get_param: SnmpdReadonlyUserPassword}
|
||||
signal_transport: NO_SIGNAL
|
||||
BlockStorageConfig:
|
||||
type: OS::Heat::StructuredConfig
|
||||
properties:
|
||||
group: os-apply-config
|
||||
config:
|
||||
admin-password: {get_param: AdminPassword}
|
||||
keystone:
|
||||
host: {get_input: controller_host}
|
||||
cinder:
|
||||
db: {get_input: cinder_dsn}
|
||||
volume_size_mb:
|
||||
get_param: CinderLVMLoopDeviceSize
|
||||
service-password:
|
||||
get_param: CinderPassword
|
||||
iscsi-helper:
|
||||
get_param: CinderISCSIHelper
|
||||
snmpd:
|
||||
export_MIB: UCD-SNMP-MIB
|
||||
readonly_user_name: {get_input: snmpd_readonly_user_name}
|
||||
readonly_user_password: {get_input: snmpd_readonly_user_password}
|
||||
rabbit:
|
||||
host: {get_input: controller_host}
|
||||
username: {get_param: RabbitUserName}
|
||||
password: {get_param: RabbitPassword}
|
||||
glance:
|
||||
host: {get_input: controller_host}
|
||||
port: {get_param: GlancePort}
|
||||
interfaces:
|
||||
control: {get_param: NeutronPublicInterface}
|
||||
neutron:
|
||||
ovs:
|
||||
local_ip: {get_input: neutron_local_ip}
|
||||
tenant_network_type: {get_param: NeutronNetworkType}
|
||||
enable_tunneling: {get_param: NeutronEnableTunnelling}
|
||||
service-password:
|
||||
get_param: NeutronPassword
|
||||
config:
|
||||
keystone:
|
||||
host: {get_input: controller_host}
|
||||
cinder:
|
||||
db: {get_input: cinder_dsn}
|
||||
volume_size_mb:
|
||||
get_param: CinderLVMLoopDeviceSize
|
||||
service-password:
|
||||
get_param: CinderPassword
|
||||
iscsi-helper:
|
||||
get_param: CinderISCSIHelper
|
||||
admin-password: {get_param: AdminPassword}
|
||||
rabbit:
|
||||
host: {get_input: controller_host}
|
||||
username: {get_param: RabbitUserName}
|
||||
password: {get_param: RabbitPassword}
|
||||
interfaces:
|
||||
control: {get_param: NeutronPublicInterface}
|
||||
neutron:
|
||||
ovs:
|
||||
local_ip: { get_input: neutron_local_ip }
|
||||
tenant_network_type: {get_param: NeutronNetworkType}
|
||||
enable_tunneling: {get_param: NeutronEnableTunnelling}
|
||||
service-password:
|
||||
get_param: NeutronPassword
|
||||
outputs:
|
||||
hosts_entry:
|
||||
value:
|
||||
str_replace:
|
||||
template: "IP HOST HOST.novalocal"
|
||||
params:
|
||||
IP: {get_attr: [BlockStorage, networks, ctlplane, 0]}
|
||||
HOST: {get_attr: [BlockStorage, name]}
|
@@ -163,7 +163,7 @@ parameters:
|
||||
description: An OVS bridge to create for accessing external networks.
|
||||
type: string
|
||||
NeutronPublicInterface:
|
||||
default: ''
|
||||
default: eth0
|
||||
description: A port to add to the NeutronPhysicalBridge.
|
||||
type: string
|
||||
NeutronTunnelTypes:
|
||||
|
@@ -291,6 +291,26 @@ parameters:
|
||||
description: If set, the contents of an SSL certificate .key file for encrypting SSL endpoints.
|
||||
type: string
|
||||
hidden: true
|
||||
SwiftHashSuffix:
|
||||
default: unset
|
||||
description: A random string to be used as a salt when hashing to determine mappings
|
||||
in the ring.
|
||||
hidden: true
|
||||
type: string
|
||||
SwiftPartPower:
|
||||
default: 10
|
||||
description: Partition Power to use when building Swift rings
|
||||
type: number
|
||||
SwiftPassword:
|
||||
default: unset
|
||||
description: The password for the swift service account, used by the swift proxy
|
||||
services.
|
||||
hidden: true
|
||||
type: string
|
||||
SwiftReplicas:
|
||||
type: number
|
||||
default: 1
|
||||
description: How many replicas to use in the swift rings.
|
||||
VirtualIP:
|
||||
type: string
|
||||
default: '' # Has to be here because of the ignored empty value bug
|
||||
@@ -671,6 +691,28 @@ resources:
|
||||
input_values:
|
||||
passthrough_config_specific: {get_param: ControllerExtraConfig}
|
||||
|
||||
SwiftConfig:
|
||||
type: OS::Heat::StructuredConfig
|
||||
properties:
|
||||
group: os-apply-config
|
||||
config:
|
||||
swift:
|
||||
hash: { get_input: swift_hash_suffix }
|
||||
part-power: { get_input: swift_part_power }
|
||||
replicas: {get_input: swift_replicas }
|
||||
service-password: { get_input: swift_password }
|
||||
|
||||
SwiftStorageDeploy:
|
||||
type: OS::Heat::StructuredDeployment
|
||||
properties:
|
||||
server: {get_resource: Controller}
|
||||
config: {get_resource: SwiftConfig}
|
||||
signal_transport: NO_SIGNAL
|
||||
input_values:
|
||||
swift_hash_suffix: {get_param: SwiftHashSuffix}
|
||||
swift_password: {get_param: SwiftPassword}
|
||||
swift_part_power: {get_param: SwiftPartPower}
|
||||
swift_replicas: { get_param: SwiftReplicas}
|
||||
|
||||
outputs:
|
||||
ip_address:
|
||||
|
@@ -1,3 +1,5 @@
|
||||
resource_registry:
|
||||
OS::TripleO::BlockStorage: cinder-storage.yaml
|
||||
OS::TripleO::Compute: compute.yaml
|
||||
OS::TripleO::Controller: controller.yaml
|
||||
OS::TripleO::ObjectStorage: swift-storage.yaml
|
||||
|
@@ -398,6 +398,29 @@ parameters:
|
||||
type: string
|
||||
default: baremetal
|
||||
|
||||
# Block storage specific parameters
|
||||
BlockStorageCount:
|
||||
type: number
|
||||
default: 1
|
||||
BlockStorageImage:
|
||||
default: overcloud-cinder-volume
|
||||
type: string
|
||||
OvercloudBlockStorageFlavor:
|
||||
default: baremetal
|
||||
description: Flavor for block storage nodes to request when deploying.
|
||||
type: string
|
||||
|
||||
# Object storage specific parameters
|
||||
ObjectStorageCount:
|
||||
type: number
|
||||
default: 0
|
||||
OvercloudSwiftStorageFlavor:
|
||||
default: baremetal
|
||||
description: Flavor for Swift storage nodes to request when deploying.
|
||||
type: string
|
||||
SwiftStorageImage:
|
||||
default: overcloud-swift-storage
|
||||
type: string
|
||||
|
||||
resources:
|
||||
|
||||
@@ -456,6 +479,10 @@ resources:
|
||||
SSLCertificate: {get_param: SSLCertificate}
|
||||
SSLKey: {get_param: SSLKey}
|
||||
SSLCACertificate: {get_param: SSLCACertificate}
|
||||
SwiftHashSuffix: {get_param: SwiftHashSuffix}
|
||||
SwiftPartPower: {get_param: SwiftPartPower}
|
||||
SwiftPassword: {get_param: SwiftPassword}
|
||||
SwiftReplicas: { get_param: SwiftReplicas}
|
||||
VirtualIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
|
||||
PublicVirtualIP: {get_attr: [PublicVirtualIP, fixed_ips, 0, ip_address]}
|
||||
|
||||
@@ -525,6 +552,47 @@ resources:
|
||||
- *compute_database_host
|
||||
- /ovs_neutron
|
||||
|
||||
BlockStorage:
|
||||
type: OS::Heat::ResourceGroup
|
||||
properties:
|
||||
count: {get_param: BlockStorageCount}
|
||||
resource_def:
|
||||
type: OS::TripleO::BlockStorage
|
||||
properties:
|
||||
AdminPassword: {get_param: AdminPassword}
|
||||
Image: {get_param: BlockStorageImage}
|
||||
CinderISCSIHelper: {get_param: CinderISCSIHelper}
|
||||
CinderLVMLoopDeviceSize: {get_param: CinderLVMLoopDeviceSize}
|
||||
CinderPassword: {get_param: CinderPassword}
|
||||
ControllerIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
|
||||
KeyName: {get_param: KeyName}
|
||||
NeutronEnableTunnelling: {get_param: NeutronEnableTunnelling}
|
||||
NeutronNetworkType: {get_param: NeutronNetworkType}
|
||||
NeutronPassword: {get_param: NeutronPassword}
|
||||
NeutronPublicInterface: {get_param: NeutronPublicInterface}
|
||||
Flavor: {get_param: OvercloudBlockStorageFlavor}
|
||||
RabbitPassword: {get_param: RabbitPassword}
|
||||
RabbitUserName: {get_param: RabbitUserName}
|
||||
|
||||
ObjectStorage:
|
||||
type: OS::Heat::ResourceGroup
|
||||
properties:
|
||||
count: {get_param: ObjectStorageCount}
|
||||
resource_def:
|
||||
type: OS::TripleO::ObjectStorage
|
||||
properties:
|
||||
ControllerIP: {get_attr: [ControlVirtualIP, fixed_ips, 0, ip_address]}
|
||||
KeyName: {get_param: KeyName}
|
||||
NeutronEnableTunnelling: {get_param: NeutronEnableTunnelling}
|
||||
NeutronNetworkType: {get_param: NeutronNetworkType}
|
||||
Flavor: {get_param: OvercloudSwiftStorageFlavor}
|
||||
HashSuffix: {get_param: SwiftHashSuffix}
|
||||
PartPower: {get_param: SwiftPartPower}
|
||||
Password: {get_param: SwiftPassword}
|
||||
Image: {get_param: SwiftStorageImage}
|
||||
Replicas: { get_param: SwiftReplicas}
|
||||
|
||||
|
||||
allNodesConfig:
|
||||
type: OS::Heat::StructuredConfig
|
||||
properties:
|
||||
@@ -539,8 +607,12 @@ resources:
|
||||
- list_join:
|
||||
- "\n"
|
||||
- {get_attr: [Controller, hosts_entry]}
|
||||
# TODO: ADD BLOCK STORAGE ENTRY HERE
|
||||
# TODO: ADD SWIFT STORAGE ENTRY HERE
|
||||
- list_join:
|
||||
- "\n"
|
||||
- {get_attr: [BlockStorage, hosts_entry]}
|
||||
- list_join:
|
||||
- "\n"
|
||||
- {get_attr: [ObjectStorage, hosts_entry]}
|
||||
rabbit:
|
||||
nodes:
|
||||
list_join:
|
||||
@@ -595,16 +667,18 @@ resources:
|
||||
ControllerSwiftDeployment:
|
||||
type: OS::Heat::StructuredDeployments
|
||||
properties:
|
||||
config: {get_resource: ControllerSwiftConfig}
|
||||
config: {get_resource: SwiftDevicesAndProxyConfig}
|
||||
servers: {get_attr: [Controller, attributes, nova_server_resource]}
|
||||
signal_transport: NO_SIGNAL
|
||||
input_values:
|
||||
swift_hash_suffix: {get_param: SwiftHashSuffix}
|
||||
swift_password: {get_param: SwiftPassword}
|
||||
swift_part_power: {get_param: SwiftPartPower}
|
||||
swift_replicas: { get_param: SwiftReplicas}
|
||||
|
||||
ControllerSwiftConfig:
|
||||
ObjectStorageSwiftDeployment:
|
||||
type: OS::Heat::StructuredDeployments
|
||||
properties:
|
||||
config: {get_resource: SwiftDevicesAndProxyConfig}
|
||||
servers: {get_attr: [ObjectStorage, attributes, nova_server_resource]}
|
||||
signal_transport: NO_SIGNAL
|
||||
|
||||
SwiftDevicesAndProxyConfig:
|
||||
type: OS::Heat::StructuredConfig
|
||||
properties:
|
||||
group: os-apply-config
|
||||
@@ -618,18 +692,11 @@ resources:
|
||||
- {get_attr: [Controller, swift_device]}
|
||||
- list_join:
|
||||
- ", "
|
||||
# TODO: replace the empty list with this:
|
||||
# - {get_attr: [ObjectStorage, swift_device]}
|
||||
# Once we have the swift/object-storage role
|
||||
- []
|
||||
hash: { get_input: swift_hash_suffix }
|
||||
part-power: { get_input: swift_part_power }
|
||||
- {get_attr: [ObjectStorage, swift_device]}
|
||||
proxy-memcache:
|
||||
list_join:
|
||||
- ","
|
||||
- {get_attr: [Controller, swift_proxy_memcache]}
|
||||
replicas: {get_input: swift_replicas }
|
||||
service-password: { get_input: swift_password }
|
||||
|
||||
ControllerClusterConfig:
|
||||
type: OS::Heat::StructuredConfig
|
||||
|
168
swift-storage.yaml
Normal file
168
swift-storage.yaml
Normal file
@@ -0,0 +1,168 @@
|
||||
heat_template_version: 2014-10-16
|
||||
description: 'Common Swift Storage Configuration'
|
||||
parameters:
|
||||
ControllerIP:
|
||||
default: ''
|
||||
type: string
|
||||
ExtraConfig:
|
||||
default: {}
|
||||
description: |
|
||||
Additional configuration to inject into the cluster. The JSON should have
|
||||
the following structure:
|
||||
{"FILEKEY":
|
||||
{"config":
|
||||
[{"section": "SECTIONNAME",
|
||||
"values":
|
||||
[{"option": "OPTIONNAME",
|
||||
"value": "VALUENAME"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
For instance:
|
||||
{"nova":
|
||||
{"config":
|
||||
[{"section": "default",
|
||||
"values":
|
||||
[{"option": "force_config_drive",
|
||||
"value": "always"
|
||||
}
|
||||
]
|
||||
},
|
||||
{"section": "cells",
|
||||
"values":
|
||||
[{"option": "driver",
|
||||
"value": "nova.cells.rpc_driver.CellsRPCDriver"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
type: json
|
||||
Flavor:
|
||||
default: baremetal
|
||||
description: Flavor for Swift storage nodes to request when deploying.
|
||||
type: string
|
||||
HashSuffix:
|
||||
default: unset
|
||||
description: A random string to be used as a salt when hashing to determine mappings
|
||||
in the ring.
|
||||
hidden: true
|
||||
type: string
|
||||
Image:
|
||||
default: overcloud-swift-storage
|
||||
type: string
|
||||
KeyName:
|
||||
default: default
|
||||
description: Name of an existing EC2 KeyPair to enable SSH access to the instances
|
||||
type: string
|
||||
NeutronEnableTunnelling:
|
||||
default: "True"
|
||||
type: string
|
||||
NeutronNetworkType:
|
||||
default: gre
|
||||
type: string
|
||||
PartPower:
|
||||
default: 10
|
||||
description: Partition Power to use when building Swift rings
|
||||
type: number
|
||||
Password:
|
||||
default: unset
|
||||
description: The password for the swift service account, used by the swift proxy
|
||||
services.
|
||||
hidden: true
|
||||
type: string
|
||||
Replicas:
|
||||
type: number
|
||||
default: 1
|
||||
description: How many replicas to use in the swift rings.
|
||||
SnmpdReadonlyUserName:
|
||||
default: ro_snmp_user
|
||||
description: The user name for SNMPd with readonly rights running on all Overcloud nodes
|
||||
type: string
|
||||
SnmpdReadonlyUserPassword:
|
||||
default: unset
|
||||
description: The user password for SNMPd with readonly rights running on all Overcloud nodes
|
||||
type: string
|
||||
hidden: true
|
||||
|
||||
resources:
|
||||
SwiftConfig:
|
||||
type: OS::Heat::StructuredConfig
|
||||
properties:
|
||||
group: os-apply-config
|
||||
config:
|
||||
snmpd:
|
||||
export_MIB: UCD-SNMP-MIB
|
||||
readonly_user_name: {get_input: snmpd_readonly_user_name}
|
||||
readonly_user_password: {get_input: snmpd_readonly_user_password}
|
||||
swift:
|
||||
hash: { get_input: swift_hash_suffix }
|
||||
part-power: { get_input: swift_part_power }
|
||||
replicas: {get_input: swift_replicas }
|
||||
service-password: { get_input: swift_password }
|
||||
neutron:
|
||||
enable_tunnelling: {get_param: NeutronEnableTunnelling}
|
||||
tenant_network_type: {get_param: NeutronNetworkType}
|
||||
ovs:
|
||||
local_ip: { get_input: neutron_local_ip }
|
||||
SwiftStorage:
|
||||
type: OS::Nova::Server
|
||||
properties:
|
||||
image: {get_param: Image}
|
||||
flavor: {get_param: Flavor}
|
||||
key_name: {get_param: KeyName}
|
||||
user_data_format: SOFTWARE_CONFIG
|
||||
networks:
|
||||
- network: ctlplane
|
||||
SwiftKeystoneConfig:
|
||||
type: OS::Heat::StructuredConfig
|
||||
properties:
|
||||
config:
|
||||
keystone:
|
||||
host: {get_input: keystone_host}
|
||||
SwiftStorageKeystone:
|
||||
type: OS::Heat::StructuredDeployment
|
||||
properties:
|
||||
server: {get_resource: SwiftStorage}
|
||||
config: {get_resource: SwiftKeystoneConfig}
|
||||
signal_transport: NO_SIGNAL
|
||||
input_values:
|
||||
keystone_host: {get_param: ControllerIP}
|
||||
SwiftStorageDeploy:
|
||||
type: OS::Heat::StructuredDeployment
|
||||
properties:
|
||||
server: {get_resource: SwiftStorage}
|
||||
config: {get_resource: SwiftConfig}
|
||||
signal_transport: NO_SIGNAL
|
||||
input_values:
|
||||
neutron_local_ip: {get_attr: [SwiftStorage, networks, ctlplane, 0]}
|
||||
snmpd_readonly_user_name: {get_param: SnmpdReadonlyUserName}
|
||||
snmpd_readonly_user_password: {get_param: SnmpdReadonlyUserPassword}
|
||||
swift_hash_suffix: {get_param: HashSuffix}
|
||||
swift_password: {get_param: Password}
|
||||
swift_part_power: {get_param: PartPower}
|
||||
swift_replicas: { get_param: Replicas}
|
||||
|
||||
outputs:
|
||||
hosts_entry:
|
||||
value:
|
||||
str_replace:
|
||||
template: "IP HOST HOST.novalocal"
|
||||
params:
|
||||
IP: {get_attr: [SwiftStorage, networks, ctlplane, 0]}
|
||||
HOST: {get_attr: [SwiftStorage, name]}
|
||||
nova_server_resource:
|
||||
description: Heat resource handle for the swift storage server
|
||||
value:
|
||||
{get_resource: SwiftStorage}
|
||||
swift_device:
|
||||
description: Swift device formatted for swift-ring-builder
|
||||
value:
|
||||
str_replace:
|
||||
template: 'r1z1-IP:%PORT%/d1'
|
||||
params:
|
||||
IP: {get_attr: [SwiftStorage, networks, ctlplane, 0]}
|
Reference in New Issue
Block a user