ECS - support for CapacityProviders (#4977)
This commit is contained in:
parent
60e48c101e
commit
1a5c18878c
@ -1995,21 +1995,21 @@
|
||||
|
||||
## ecs
|
||||
<details>
|
||||
<summary>73% implemented</summary>
|
||||
<summary>78% implemented</summary>
|
||||
|
||||
- [ ] create_capacity_provider
|
||||
- [X] create_capacity_provider
|
||||
- [X] create_cluster
|
||||
- [X] create_service
|
||||
- [X] create_task_set
|
||||
- [X] delete_account_setting
|
||||
- [X] delete_attributes
|
||||
- [ ] delete_capacity_provider
|
||||
- [X] delete_capacity_provider
|
||||
- [X] delete_cluster
|
||||
- [X] delete_service
|
||||
- [X] delete_task_set
|
||||
- [X] deregister_container_instance
|
||||
- [X] deregister_task_definition
|
||||
- [ ] describe_capacity_providers
|
||||
- [X] describe_capacity_providers
|
||||
- [X] describe_clusters
|
||||
- [X] describe_container_instances
|
||||
- [X] describe_services
|
||||
|
@ -27,13 +27,17 @@ ecs
|
||||
|
||||
|start-h3| Implemented features for this service |end-h3|
|
||||
|
||||
- [ ] create_capacity_provider
|
||||
- [X] create_capacity_provider
|
||||
- [X] create_cluster
|
||||
|
||||
The following parameters are not yet implemented: configuration, capacityProviders, defaultCapacityProviderStrategy
|
||||
|
||||
|
||||
- [X] create_service
|
||||
- [X] create_task_set
|
||||
- [X] delete_account_setting
|
||||
- [X] delete_attributes
|
||||
- [ ] delete_capacity_provider
|
||||
- [X] delete_capacity_provider
|
||||
- [X] delete_cluster
|
||||
- [X] delete_service
|
||||
- [X] delete_task_set
|
||||
@ -43,7 +47,7 @@ ecs
|
||||
|
||||
- [X] deregister_container_instance
|
||||
- [X] deregister_task_definition
|
||||
- [ ] describe_capacity_providers
|
||||
- [X] describe_capacity_providers
|
||||
- [X] describe_clusters
|
||||
|
||||
Only include=TAGS is currently supported.
|
||||
|
@ -61,7 +61,7 @@ class AccountSetting(BaseObject):
|
||||
|
||||
|
||||
class Cluster(BaseObject, CloudFormationModel):
|
||||
def __init__(self, cluster_name, region_name):
|
||||
def __init__(self, cluster_name, region_name, cluster_settings=None):
|
||||
self.active_services_count = 0
|
||||
self.arn = "arn:aws:ecs:{0}:{1}:cluster/{2}".format(
|
||||
region_name, ACCOUNT_ID, cluster_name
|
||||
@ -72,6 +72,7 @@ class Cluster(BaseObject, CloudFormationModel):
|
||||
self.running_tasks_count = 0
|
||||
self.status = "ACTIVE"
|
||||
self.region_name = region_name
|
||||
self.settings = cluster_settings
|
||||
|
||||
@property
|
||||
def physical_resource_id(self):
|
||||
@ -326,6 +327,31 @@ class Task(BaseObject):
|
||||
return response_object
|
||||
|
||||
|
||||
class CapacityProvider(BaseObject):
|
||||
def __init__(self, region_name, name, asg_details, tags):
|
||||
self._id = str(uuid.uuid4())
|
||||
self.capacity_provider_arn = f"arn:aws:ecs:{region_name}:{ACCOUNT_ID}:capacity_provider/{name}/{self._id}"
|
||||
self.name = name
|
||||
self.status = "ACTIVE"
|
||||
self.auto_scaling_group_provider = asg_details
|
||||
self.tags = tags
|
||||
|
||||
|
||||
class CapacityProviderFailure(BaseObject):
|
||||
def __init__(self, reason, name, region_name):
|
||||
self.reason = reason
|
||||
self.arn = "arn:aws:ecs:{0}:{1}:capacity_provider/{2}".format(
|
||||
region_name, ACCOUNT_ID, name
|
||||
)
|
||||
|
||||
@property
|
||||
def response_object(self):
|
||||
response_object = self.gen_response_object()
|
||||
response_object["reason"] = self.reason
|
||||
response_object["arn"] = self.arn
|
||||
return response_object
|
||||
|
||||
|
||||
class Service(BaseObject, CloudFormationModel):
|
||||
def __init__(
|
||||
self,
|
||||
@ -727,6 +753,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
||||
def __init__(self, region_name):
|
||||
super().__init__()
|
||||
self.account_settings = dict()
|
||||
self.capacity_providers = dict()
|
||||
self.clusters = {}
|
||||
self.task_definitions = {}
|
||||
self.tasks = {}
|
||||
@ -760,6 +787,13 @@ class EC2ContainerServiceBackend(BaseBackend):
|
||||
|
||||
return cluster
|
||||
|
||||
def create_capacity_provider(self, name, asg_details, tags):
|
||||
capacity_provider = CapacityProvider(self.region_name, name, asg_details, tags)
|
||||
self.capacity_providers[name] = capacity_provider
|
||||
if tags:
|
||||
self.tagger.tag_resource(capacity_provider.capacity_provider_arn, tags)
|
||||
return capacity_provider
|
||||
|
||||
def describe_task_definition(self, task_definition_str):
|
||||
task_definition_name = task_definition_str.split("/")[-1]
|
||||
if ":" in task_definition_name:
|
||||
@ -777,13 +811,42 @@ class EC2ContainerServiceBackend(BaseBackend):
|
||||
else:
|
||||
raise Exception("{0} is not a task_definition".format(task_definition_name))
|
||||
|
||||
def create_cluster(self, cluster_name, tags=None):
|
||||
cluster = Cluster(cluster_name, self.region_name)
|
||||
def create_cluster(self, cluster_name, tags=None, cluster_settings=None):
|
||||
"""
|
||||
The following parameters are not yet implemented: configuration, capacityProviders, defaultCapacityProviderStrategy
|
||||
"""
|
||||
cluster = Cluster(cluster_name, self.region_name, cluster_settings)
|
||||
self.clusters[cluster_name] = cluster
|
||||
if tags:
|
||||
self.tagger.tag_resource(cluster.arn, tags)
|
||||
return cluster
|
||||
|
||||
def _get_provider(self, name_or_arn):
|
||||
for provider in self.capacity_providers.values():
|
||||
if (
|
||||
provider.name == name_or_arn
|
||||
or provider.capacity_provider_arn == name_or_arn
|
||||
):
|
||||
return provider
|
||||
|
||||
def describe_capacity_providers(self, names):
|
||||
providers = []
|
||||
failures = []
|
||||
for name in names:
|
||||
provider = self._get_provider(name)
|
||||
if provider:
|
||||
providers.append(provider)
|
||||
else:
|
||||
failures.append(
|
||||
CapacityProviderFailure("MISSING", name, self.region_name)
|
||||
)
|
||||
return providers, failures
|
||||
|
||||
def delete_capacity_provider(self, name_or_arn):
|
||||
provider = self._get_provider(name_or_arn)
|
||||
self.capacity_providers.pop(provider.name)
|
||||
return provider
|
||||
|
||||
def list_clusters(self):
|
||||
"""
|
||||
maxSize and pagination not implemented
|
||||
@ -1165,6 +1228,15 @@ class EC2ContainerServiceBackend(BaseBackend):
|
||||
"Could not find task {} on cluster {}".format(task_str, cluster.name)
|
||||
)
|
||||
|
||||
def _get_service(self, cluster_str, service_str):
|
||||
cluster = self._get_cluster(cluster_str)
|
||||
for service in self.services.values():
|
||||
if service.cluster_name == cluster.name and (
|
||||
service.name == service_str or service.arn == service_str
|
||||
):
|
||||
return service
|
||||
raise ServiceNotFoundException
|
||||
|
||||
def create_service(
|
||||
self,
|
||||
cluster_str,
|
||||
@ -1223,31 +1295,19 @@ class EC2ContainerServiceBackend(BaseBackend):
|
||||
|
||||
def describe_services(self, cluster_str, service_names_or_arns):
|
||||
cluster = self._get_cluster(cluster_str)
|
||||
service_names = [name.split("/")[-1] for name in service_names_or_arns]
|
||||
|
||||
result = []
|
||||
failures = []
|
||||
for existing_service_name, existing_service_obj in sorted(
|
||||
self.services.items()
|
||||
):
|
||||
for requested_name_or_arn in service_names_or_arns:
|
||||
cluster_service_pair = "{0}:{1}".format(
|
||||
cluster.name, requested_name_or_arn
|
||||
for name in service_names:
|
||||
cluster_service_pair = "{0}:{1}".format(cluster.name, name)
|
||||
if cluster_service_pair in self.services:
|
||||
result.append(self.services[cluster_service_pair])
|
||||
else:
|
||||
missing_arn = (
|
||||
f"arn:aws:ecs:{self.region_name}:{ACCOUNT_ID}:service/{name}"
|
||||
)
|
||||
if (
|
||||
cluster_service_pair == existing_service_name
|
||||
or existing_service_obj.arn == requested_name_or_arn
|
||||
):
|
||||
result.append(existing_service_obj)
|
||||
else:
|
||||
service_name = requested_name_or_arn.split("/")[-1]
|
||||
failures.append(
|
||||
{
|
||||
"arn": "arn:aws:ecs:eu-central-1:{0}:service/{1}".format(
|
||||
ACCOUNT_ID, service_name
|
||||
),
|
||||
"reason": "MISSING",
|
||||
}
|
||||
)
|
||||
failures.append({"arn": missing_arn, "reason": "MISSING"})
|
||||
|
||||
return result, failures
|
||||
|
||||
@ -1272,18 +1332,17 @@ class EC2ContainerServiceBackend(BaseBackend):
|
||||
|
||||
def delete_service(self, cluster_name, service_name, force):
|
||||
cluster = self._get_cluster(cluster_name)
|
||||
cluster_service_pair = "{0}:{1}".format(cluster.name, service_name)
|
||||
service = self._get_service(cluster_name, service_name)
|
||||
|
||||
if cluster_service_pair in self.services:
|
||||
service = self.services[cluster_service_pair]
|
||||
if service.desired_count > 0 and not force:
|
||||
raise InvalidParameterException(
|
||||
"The service cannot be stopped while it is scaled above 0."
|
||||
)
|
||||
else:
|
||||
return self.services.pop(cluster_service_pair)
|
||||
cluster_service_pair = "{0}:{1}".format(cluster.name, service.name)
|
||||
|
||||
service = self.services[cluster_service_pair]
|
||||
if service.desired_count > 0 and not force:
|
||||
raise InvalidParameterException(
|
||||
"The service cannot be stopped while it is scaled above 0."
|
||||
)
|
||||
else:
|
||||
raise ServiceNotFoundException
|
||||
return self.services.pop(cluster_service_pair)
|
||||
|
||||
def register_container_instance(self, cluster_str, ec2_instance_id):
|
||||
cluster_name = cluster_str.split("/")[-1]
|
||||
|
@ -25,12 +25,20 @@ class EC2ContainerServiceResponse(BaseResponse):
|
||||
def _get_param(self, param_name, if_none=None):
|
||||
return self.request_params.get(param_name, if_none)
|
||||
|
||||
def create_capacity_provider(self):
|
||||
name = self._get_param("name")
|
||||
asg_provider = self._get_param("autoScalingGroupProvider")
|
||||
tags = self._get_param("tags")
|
||||
provider = self.ecs_backend.create_capacity_provider(name, asg_provider, tags)
|
||||
return json.dumps({"capacityProvider": provider.response_object})
|
||||
|
||||
def create_cluster(self):
|
||||
cluster_name = self._get_param("clusterName")
|
||||
tags = self._get_param("tags")
|
||||
settings = self._get_param("settings")
|
||||
if cluster_name is None:
|
||||
cluster_name = "default"
|
||||
cluster = self.ecs_backend.create_cluster(cluster_name, tags)
|
||||
cluster = self.ecs_backend.create_cluster(cluster_name, tags, settings)
|
||||
return json.dumps({"cluster": cluster.response_object})
|
||||
|
||||
def list_clusters(self):
|
||||
@ -42,6 +50,21 @@ class EC2ContainerServiceResponse(BaseResponse):
|
||||
}
|
||||
)
|
||||
|
||||
def delete_capacity_provider(self):
|
||||
name = self._get_param("capacityProvider")
|
||||
provider = self.ecs_backend.delete_capacity_provider(name)
|
||||
return json.dumps({"capacityProvider": provider.response_object})
|
||||
|
||||
def describe_capacity_providers(self):
|
||||
names = self._get_param("capacityProviders")
|
||||
providers, failures = self.ecs_backend.describe_capacity_providers(names)
|
||||
return json.dumps(
|
||||
{
|
||||
"capacityProviders": [p.response_object for p in providers],
|
||||
"failures": [p.response_object for p in failures],
|
||||
}
|
||||
)
|
||||
|
||||
def describe_clusters(self):
|
||||
names = self._get_param("clusters")
|
||||
include = self._get_param("include")
|
||||
|
@ -39,6 +39,20 @@ def test_create_cluster():
|
||||
response["cluster"]["activeServicesCount"].should.equal(0)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_create_cluster_with_setting():
|
||||
client = boto3.client("ecs", region_name="us-east-1")
|
||||
cluster = client.create_cluster(
|
||||
clusterName="test_ecs_cluster",
|
||||
settings=[{"name": "containerInsights", "value": "disabled"}],
|
||||
)["cluster"]
|
||||
cluster["clusterName"].should.equal("test_ecs_cluster")
|
||||
cluster["status"].should.equal("ACTIVE")
|
||||
cluster.should.have.key("settings").equals(
|
||||
[{"name": "containerInsights", "value": "disabled"}]
|
||||
)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_list_clusters():
|
||||
client = boto3.client("ecs", region_name="us-east-2")
|
||||
@ -112,7 +126,7 @@ def test_delete_cluster():
|
||||
response["cluster"]["activeServicesCount"].should.equal(0)
|
||||
|
||||
response = client.list_clusters()
|
||||
len(response["clusterArns"]).should.equal(0)
|
||||
response["clusterArns"].should.have.length_of(0)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
@ -685,7 +699,9 @@ def test_list_services():
|
||||
@mock_ecs
|
||||
def test_describe_services():
|
||||
client = boto3.client("ecs", region_name="us-east-1")
|
||||
_ = client.create_cluster(clusterName="test_ecs_cluster")
|
||||
cluster_arn = client.create_cluster(clusterName="test_ecs_cluster")["cluster"][
|
||||
"clusterArn"
|
||||
]
|
||||
_ = client.register_task_definition(
|
||||
family="test_ecs_task",
|
||||
containerDefinitions=[
|
||||
@ -721,6 +737,14 @@ def test_describe_services():
|
||||
taskDefinition="test_ecs_task",
|
||||
desiredCount=2,
|
||||
)
|
||||
|
||||
# Verify we can describe services using the cluster ARN
|
||||
response = client.describe_services(
|
||||
cluster=cluster_arn, services=["test_ecs_service1"]
|
||||
)
|
||||
response.should.have.key("services").length_of(1)
|
||||
|
||||
# Verify we can describe services using both names and ARN's
|
||||
response = client.describe_services(
|
||||
cluster="test_ecs_cluster",
|
||||
services=[
|
||||
@ -1072,6 +1096,43 @@ def test_delete_service():
|
||||
)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_delete_service__using_arns():
|
||||
client = boto3.client("ecs", region_name="us-east-1")
|
||||
cluster_arn = client.create_cluster(clusterName="test_ecs_cluster")["cluster"][
|
||||
"clusterArn"
|
||||
]
|
||||
_ = client.register_task_definition(
|
||||
family="test_ecs_task",
|
||||
containerDefinitions=[
|
||||
{
|
||||
"name": "hello_world",
|
||||
"image": "docker/hello-world:latest",
|
||||
"cpu": 1024,
|
||||
"memory": 400,
|
||||
"essential": True,
|
||||
"environment": [
|
||||
{"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"}
|
||||
],
|
||||
"logConfiguration": {"logDriver": "json-file"},
|
||||
}
|
||||
],
|
||||
)
|
||||
service_arn = client.create_service(
|
||||
cluster="test_ecs_cluster",
|
||||
serviceName="test_ecs_service",
|
||||
taskDefinition="test_ecs_task",
|
||||
desiredCount=2,
|
||||
)["service"]["serviceArn"]
|
||||
_ = client.update_service(
|
||||
cluster="test_ecs_cluster", service="test_ecs_service", desiredCount=0
|
||||
)
|
||||
response = client.delete_service(cluster=cluster_arn, service=service_arn)
|
||||
response["service"]["clusterArn"].should.equal(
|
||||
"arn:aws:ecs:us-east-1:{}:cluster/test_ecs_cluster".format(ACCOUNT_ID)
|
||||
)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_delete_service_force():
|
||||
client = boto3.client("ecs", region_name="us-east-1")
|
||||
|
173
tests/test_ecs/test_ecs_capacity_provider.py
Normal file
173
tests/test_ecs/test_ecs_capacity_provider.py
Normal file
@ -0,0 +1,173 @@
|
||||
import boto3
|
||||
|
||||
from moto import mock_ecs
|
||||
from moto.core import ACCOUNT_ID
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_create_capacity_provider():
|
||||
client = boto3.client("ecs", region_name="us-west-1")
|
||||
resp = client.create_capacity_provider(
|
||||
name="my_provider",
|
||||
autoScalingGroupProvider={
|
||||
"autoScalingGroupArn": "asg:arn",
|
||||
"managedScaling": {
|
||||
"status": "ENABLED",
|
||||
"targetCapacity": 5,
|
||||
"maximumScalingStepSize": 2,
|
||||
},
|
||||
"managedTerminationProtection": "DISABLED",
|
||||
},
|
||||
)
|
||||
resp.should.have.key("capacityProvider")
|
||||
|
||||
provider = resp["capacityProvider"]
|
||||
provider.should.have.key("capacityProviderArn")
|
||||
provider.should.have.key("name").equals("my_provider")
|
||||
provider.should.have.key("status").equals("ACTIVE")
|
||||
provider.should.have.key("autoScalingGroupProvider").equals(
|
||||
{
|
||||
"autoScalingGroupArn": "asg:arn",
|
||||
"managedScaling": {
|
||||
"status": "ENABLED",
|
||||
"targetCapacity": 5,
|
||||
"maximumScalingStepSize": 2,
|
||||
},
|
||||
"managedTerminationProtection": "DISABLED",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_create_capacity_provider_with_tags():
|
||||
client = boto3.client("ecs", region_name="us-west-1")
|
||||
resp = client.create_capacity_provider(
|
||||
name="my_provider",
|
||||
autoScalingGroupProvider={"autoScalingGroupArn": "asg:arn"},
|
||||
tags=[{"key": "k1", "value": "v1"}],
|
||||
)
|
||||
resp.should.have.key("capacityProvider")
|
||||
|
||||
provider = resp["capacityProvider"]
|
||||
provider.should.have.key("capacityProviderArn")
|
||||
provider.should.have.key("name").equals("my_provider")
|
||||
provider.should.have.key("tags").equals([{"key": "k1", "value": "v1"}])
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_describe_capacity_provider__using_name():
|
||||
client = boto3.client("ecs", region_name="us-west-1")
|
||||
client.create_capacity_provider(
|
||||
name="my_provider",
|
||||
autoScalingGroupProvider={
|
||||
"autoScalingGroupArn": "asg:arn",
|
||||
"managedScaling": {
|
||||
"status": "ENABLED",
|
||||
"targetCapacity": 5,
|
||||
"maximumScalingStepSize": 2,
|
||||
},
|
||||
"managedTerminationProtection": "DISABLED",
|
||||
},
|
||||
)
|
||||
|
||||
resp = client.describe_capacity_providers(capacityProviders=["my_provider"])
|
||||
resp.should.have.key("capacityProviders").length_of(1)
|
||||
|
||||
provider = resp["capacityProviders"][0]
|
||||
provider.should.have.key("capacityProviderArn")
|
||||
provider.should.have.key("name").equals("my_provider")
|
||||
provider.should.have.key("status").equals("ACTIVE")
|
||||
provider.should.have.key("autoScalingGroupProvider").equals(
|
||||
{
|
||||
"autoScalingGroupArn": "asg:arn",
|
||||
"managedScaling": {
|
||||
"status": "ENABLED",
|
||||
"targetCapacity": 5,
|
||||
"maximumScalingStepSize": 2,
|
||||
},
|
||||
"managedTerminationProtection": "DISABLED",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_describe_capacity_provider__using_arn():
|
||||
client = boto3.client("ecs", region_name="us-west-1")
|
||||
provider_arn = client.create_capacity_provider(
|
||||
name="my_provider",
|
||||
autoScalingGroupProvider={
|
||||
"autoScalingGroupArn": "asg:arn",
|
||||
"managedScaling": {
|
||||
"status": "ENABLED",
|
||||
"targetCapacity": 5,
|
||||
"maximumScalingStepSize": 2,
|
||||
},
|
||||
"managedTerminationProtection": "DISABLED",
|
||||
},
|
||||
)["capacityProvider"]["capacityProviderArn"]
|
||||
|
||||
resp = client.describe_capacity_providers(capacityProviders=[provider_arn])
|
||||
resp.should.have.key("capacityProviders").length_of(1)
|
||||
|
||||
provider = resp["capacityProviders"][0]
|
||||
provider.should.have.key("name").equals("my_provider")
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_describe_capacity_provider__missing():
|
||||
client = boto3.client("ecs", region_name="us-west-1")
|
||||
client.create_capacity_provider(
|
||||
name="my_provider",
|
||||
autoScalingGroupProvider={
|
||||
"autoScalingGroupArn": "asg:arn",
|
||||
"managedScaling": {
|
||||
"status": "ENABLED",
|
||||
"targetCapacity": 5,
|
||||
"maximumScalingStepSize": 2,
|
||||
},
|
||||
"managedTerminationProtection": "DISABLED",
|
||||
},
|
||||
)
|
||||
|
||||
resp = client.describe_capacity_providers(
|
||||
capacityProviders=["my_provider", "another_provider"]
|
||||
)
|
||||
resp.should.have.key("capacityProviders").length_of(1)
|
||||
resp.should.have.key("failures").length_of(1)
|
||||
resp["failures"].should.contain(
|
||||
{
|
||||
"arn": f"arn:aws:ecs:us-west-1:{ACCOUNT_ID}:capacity_provider/another_provider",
|
||||
"reason": "MISSING",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@mock_ecs
|
||||
def test_delete_capacity_provider():
|
||||
client = boto3.client("ecs", region_name="us-west-1")
|
||||
client.create_capacity_provider(
|
||||
name="my_provider", autoScalingGroupProvider={"autoScalingGroupArn": "asg:arn"}
|
||||
)
|
||||
|
||||
resp = client.delete_capacity_provider(capacityProvider="my_provider")
|
||||
resp.should.have.key("capacityProvider")
|
||||
resp["capacityProvider"].should.have.key("name").equals("my_provider")
|
||||
|
||||
# We can't find either provider
|
||||
resp = client.describe_capacity_providers(
|
||||
capacityProviders=["my_provider", "another_provider"]
|
||||
)
|
||||
resp.should.have.key("capacityProviders").length_of(0)
|
||||
resp.should.have.key("failures").length_of(2)
|
||||
resp["failures"].should.contain(
|
||||
{
|
||||
"arn": f"arn:aws:ecs:us-west-1:{ACCOUNT_ID}:capacity_provider/another_provider",
|
||||
"reason": "MISSING",
|
||||
}
|
||||
)
|
||||
resp["failures"].should.contain(
|
||||
{
|
||||
"arn": f"arn:aws:ecs:us-west-1:{ACCOUNT_ID}:capacity_provider/my_provider",
|
||||
"reason": "MISSING",
|
||||
}
|
||||
)
|
Loading…
Reference in New Issue
Block a user