ApplicationAutoscaling - Scheduled Actions (#5011)
This commit is contained in:
parent
56a2fd384c
commit
a0eb48d588
@ -226,17 +226,17 @@
|
|||||||
|
|
||||||
## application-autoscaling
|
## application-autoscaling
|
||||||
<details>
|
<details>
|
||||||
<summary>60% implemented</summary>
|
<summary>90% implemented</summary>
|
||||||
|
|
||||||
- [X] delete_scaling_policy
|
- [X] delete_scaling_policy
|
||||||
- [ ] delete_scheduled_action
|
- [X] delete_scheduled_action
|
||||||
- [X] deregister_scalable_target
|
- [X] deregister_scalable_target
|
||||||
- [X] describe_scalable_targets
|
- [X] describe_scalable_targets
|
||||||
- [ ] describe_scaling_activities
|
- [ ] describe_scaling_activities
|
||||||
- [X] describe_scaling_policies
|
- [X] describe_scaling_policies
|
||||||
- [ ] describe_scheduled_actions
|
- [X] describe_scheduled_actions
|
||||||
- [X] put_scaling_policy
|
- [X] put_scaling_policy
|
||||||
- [ ] put_scheduled_action
|
- [X] put_scheduled_action
|
||||||
- [X] register_scalable_target
|
- [X] register_scalable_target
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ application-autoscaling
|
|||||||
|start-h3| Implemented features for this service |end-h3|
|
|start-h3| Implemented features for this service |end-h3|
|
||||||
|
|
||||||
- [X] delete_scaling_policy
|
- [X] delete_scaling_policy
|
||||||
- [ ] delete_scheduled_action
|
- [X] delete_scheduled_action
|
||||||
- [X] deregister_scalable_target
|
- [X] deregister_scalable_target
|
||||||
Registers or updates a scalable target.
|
Registers or updates a scalable target.
|
||||||
|
|
||||||
@ -35,9 +35,13 @@ application-autoscaling
|
|||||||
|
|
||||||
- [ ] describe_scaling_activities
|
- [ ] describe_scaling_activities
|
||||||
- [X] describe_scaling_policies
|
- [X] describe_scaling_policies
|
||||||
- [ ] describe_scheduled_actions
|
- [X] describe_scheduled_actions
|
||||||
|
|
||||||
|
Pagination is not yet implemented
|
||||||
|
|
||||||
|
|
||||||
- [X] put_scaling_policy
|
- [X] put_scaling_policy
|
||||||
- [ ] put_scheduled_action
|
- [X] put_scheduled_action
|
||||||
- [X] register_scalable_target
|
- [X] register_scalable_target
|
||||||
Registers or updates a scalable target.
|
Registers or updates a scalable target.
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from moto.core import BaseBackend, BaseModel
|
from moto.core import ACCOUNT_ID, BaseBackend, BaseModel
|
||||||
from moto.core.utils import BackendDict
|
from moto.core.utils import BackendDict
|
||||||
from moto.ecs import ecs_backends
|
from moto.ecs import ecs_backends
|
||||||
from .exceptions import AWSValidationException
|
from .exceptions import AWSValidationException
|
||||||
@ -65,6 +65,7 @@ class ApplicationAutoscalingBackend(BaseBackend):
|
|||||||
self.ecs_backend = ecs_backends[region]
|
self.ecs_backend = ecs_backends[region]
|
||||||
self.targets = OrderedDict()
|
self.targets = OrderedDict()
|
||||||
self.policies = {}
|
self.policies = {}
|
||||||
|
self.scheduled_actions = list()
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
region = self.region
|
region = self.region
|
||||||
@ -229,6 +230,87 @@ class ApplicationAutoscalingBackend(BaseBackend):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def delete_scheduled_action(
|
||||||
|
self, service_namespace, scheduled_action_name, resource_id, scalable_dimension
|
||||||
|
):
|
||||||
|
self.scheduled_actions = [
|
||||||
|
a
|
||||||
|
for a in self.scheduled_actions
|
||||||
|
if not (
|
||||||
|
a.service_namespace == service_namespace
|
||||||
|
and a.scheduled_action_name == scheduled_action_name
|
||||||
|
and a.resource_id == resource_id
|
||||||
|
and a.scalable_dimension == scalable_dimension
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def describe_scheduled_actions(
|
||||||
|
self, scheduled_action_names, service_namespace, resource_id, scalable_dimension
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Pagination is not yet implemented
|
||||||
|
"""
|
||||||
|
result = [
|
||||||
|
a
|
||||||
|
for a in self.scheduled_actions
|
||||||
|
if a.service_namespace == service_namespace
|
||||||
|
]
|
||||||
|
if scheduled_action_names:
|
||||||
|
result = [
|
||||||
|
a for a in result if a.scheduled_action_name in scheduled_action_names
|
||||||
|
]
|
||||||
|
if resource_id:
|
||||||
|
result = [a for a in result if a.resource_id == resource_id]
|
||||||
|
if scalable_dimension:
|
||||||
|
result = [a for a in result if a.scalable_dimension == scalable_dimension]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def put_scheduled_action(
|
||||||
|
self,
|
||||||
|
service_namespace,
|
||||||
|
schedule,
|
||||||
|
timezone,
|
||||||
|
scheduled_action_name,
|
||||||
|
resource_id,
|
||||||
|
scalable_dimension,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
scalable_target_action,
|
||||||
|
):
|
||||||
|
existing_action = next(
|
||||||
|
(
|
||||||
|
a
|
||||||
|
for a in self.scheduled_actions
|
||||||
|
if a.service_namespace == service_namespace
|
||||||
|
and a.resource_id == resource_id
|
||||||
|
and a.scalable_dimension == scalable_dimension
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if existing_action:
|
||||||
|
existing_action.update(
|
||||||
|
schedule,
|
||||||
|
timezone,
|
||||||
|
scheduled_action_name,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
scalable_target_action,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
action = FakeScheduledAction(
|
||||||
|
service_namespace,
|
||||||
|
schedule,
|
||||||
|
timezone,
|
||||||
|
scheduled_action_name,
|
||||||
|
resource_id,
|
||||||
|
scalable_dimension,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
scalable_target_action,
|
||||||
|
self.region,
|
||||||
|
)
|
||||||
|
self.scheduled_actions.append(action)
|
||||||
|
|
||||||
|
|
||||||
def _target_params_are_valid(namespace, r_id, dimension):
|
def _target_params_are_valid(namespace, r_id, dimension):
|
||||||
"""Check whether namespace, resource_id and dimension are valid and consistent with each other."""
|
"""Check whether namespace, resource_id and dimension are valid and consistent with each other."""
|
||||||
@ -354,4 +436,51 @@ class FakeApplicationAutoscalingPolicy(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeScheduledAction(BaseModel):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
service_namespace,
|
||||||
|
schedule,
|
||||||
|
timezone,
|
||||||
|
scheduled_action_name,
|
||||||
|
resource_id,
|
||||||
|
scalable_dimension,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
scalable_target_action,
|
||||||
|
region,
|
||||||
|
):
|
||||||
|
self.arn = f"arn:aws:autoscaling:{region}:{ACCOUNT_ID}:scheduledAction:{service_namespace}:scheduledActionName/{scheduled_action_name}"
|
||||||
|
self.service_namespace = service_namespace
|
||||||
|
self.schedule = schedule
|
||||||
|
self.timezone = timezone
|
||||||
|
self.scheduled_action_name = scheduled_action_name
|
||||||
|
self.resource_id = resource_id
|
||||||
|
self.scalable_dimension = scalable_dimension
|
||||||
|
self.start_time = start_time
|
||||||
|
self.end_time = end_time
|
||||||
|
self.scalable_target_action = scalable_target_action
|
||||||
|
self.creation_time = time.time()
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
schedule,
|
||||||
|
timezone,
|
||||||
|
scheduled_action_name,
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
scalable_target_action,
|
||||||
|
):
|
||||||
|
if scheduled_action_name:
|
||||||
|
self.scheduled_action_name = scheduled_action_name
|
||||||
|
if schedule:
|
||||||
|
self.schedule = schedule
|
||||||
|
if timezone:
|
||||||
|
self.timezone = timezone
|
||||||
|
if scalable_target_action:
|
||||||
|
self.scalable_target_action = scalable_target_action
|
||||||
|
self.start_time = start_time
|
||||||
|
self.end_time = end_time
|
||||||
|
|
||||||
|
|
||||||
applicationautoscaling_backends = BackendDict(ApplicationAutoscalingBackend, "ec2")
|
applicationautoscaling_backends = BackendDict(ApplicationAutoscalingBackend, "ec2")
|
||||||
|
@ -127,6 +127,63 @@ class ApplicationAutoScalingResponse(BaseResponse):
|
|||||||
if message:
|
if message:
|
||||||
raise AWSValidationException(message)
|
raise AWSValidationException(message)
|
||||||
|
|
||||||
|
def delete_scheduled_action(self):
|
||||||
|
params = json.loads(self.body)
|
||||||
|
service_namespace = params.get("ServiceNamespace")
|
||||||
|
scheduled_action_name = params.get("ScheduledActionName")
|
||||||
|
resource_id = params.get("ResourceId")
|
||||||
|
scalable_dimension = params.get("ScalableDimension")
|
||||||
|
self.applicationautoscaling_backend.delete_scheduled_action(
|
||||||
|
service_namespace=service_namespace,
|
||||||
|
scheduled_action_name=scheduled_action_name,
|
||||||
|
resource_id=resource_id,
|
||||||
|
scalable_dimension=scalable_dimension,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def put_scheduled_action(self):
|
||||||
|
params = json.loads(self.body)
|
||||||
|
service_namespace = params.get("ServiceNamespace")
|
||||||
|
schedule = params.get("Schedule")
|
||||||
|
timezone = params.get("Timezone")
|
||||||
|
scheduled_action_name = params.get("ScheduledActionName")
|
||||||
|
resource_id = params.get("ResourceId")
|
||||||
|
scalable_dimension = params.get("ScalableDimension")
|
||||||
|
start_time = params.get("StartTime")
|
||||||
|
end_time = params.get("EndTime")
|
||||||
|
scalable_target_action = params.get("ScalableTargetAction")
|
||||||
|
self.applicationautoscaling_backend.put_scheduled_action(
|
||||||
|
service_namespace=service_namespace,
|
||||||
|
schedule=schedule,
|
||||||
|
timezone=timezone,
|
||||||
|
scheduled_action_name=scheduled_action_name,
|
||||||
|
resource_id=resource_id,
|
||||||
|
scalable_dimension=scalable_dimension,
|
||||||
|
start_time=start_time,
|
||||||
|
end_time=end_time,
|
||||||
|
scalable_target_action=scalable_target_action,
|
||||||
|
)
|
||||||
|
return json.dumps(dict())
|
||||||
|
|
||||||
|
def describe_scheduled_actions(self):
|
||||||
|
params = json.loads(self.body)
|
||||||
|
scheduled_action_names = params.get("ScheduledActionNames")
|
||||||
|
service_namespace = params.get("ServiceNamespace")
|
||||||
|
resource_id = params.get("ResourceId")
|
||||||
|
scalable_dimension = params.get("ScalableDimension")
|
||||||
|
scheduled_actions = (
|
||||||
|
self.applicationautoscaling_backend.describe_scheduled_actions(
|
||||||
|
scheduled_action_names=scheduled_action_names,
|
||||||
|
service_namespace=service_namespace,
|
||||||
|
resource_id=resource_id,
|
||||||
|
scalable_dimension=scalable_dimension,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
response_obj = {
|
||||||
|
"ScheduledActions": [_build_scheduled_action(a) for a in scheduled_actions]
|
||||||
|
}
|
||||||
|
return json.dumps(response_obj)
|
||||||
|
|
||||||
|
|
||||||
def _build_target(t):
|
def _build_target(t):
|
||||||
return {
|
return {
|
||||||
@ -158,3 +215,20 @@ def _build_policy(p):
|
|||||||
"TargetTrackingScalingPolicyConfiguration"
|
"TargetTrackingScalingPolicyConfiguration"
|
||||||
] = p.target_tracking_scaling_policy_configuration
|
] = p.target_tracking_scaling_policy_configuration
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def _build_scheduled_action(a):
|
||||||
|
response = {
|
||||||
|
"ScheduledActionName": a.scheduled_action_name,
|
||||||
|
"ScheduledActionARN": a.arn,
|
||||||
|
"ServiceNamespace": a.service_namespace,
|
||||||
|
"Schedule": a.schedule,
|
||||||
|
"Timezone": a.timezone,
|
||||||
|
"ResourceId": a.resource_id,
|
||||||
|
"ScalableDimension": a.scalable_dimension,
|
||||||
|
"StartTime": a.start_time,
|
||||||
|
"EndTime": a.end_time,
|
||||||
|
"CreationTime": a.creation_time,
|
||||||
|
"ScalableTargetAction": a.scalable_target_action,
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
@ -2,6 +2,7 @@ import boto3
|
|||||||
import pytest
|
import pytest
|
||||||
import sure # noqa # pylint: disable=unused-import
|
import sure # noqa # pylint: disable=unused-import
|
||||||
from moto import mock_applicationautoscaling, mock_ecs
|
from moto import mock_applicationautoscaling, mock_ecs
|
||||||
|
from moto.core import ACCOUNT_ID
|
||||||
|
|
||||||
DEFAULT_REGION = "us-east-1"
|
DEFAULT_REGION = "us-east-1"
|
||||||
DEFAULT_ECS_CLUSTER = "default"
|
DEFAULT_ECS_CLUSTER = "default"
|
||||||
@ -362,7 +363,7 @@ def test_put_scaling_policy(policy_type, policy_body_kwargs):
|
|||||||
ResourceId=resource_id,
|
ResourceId=resource_id,
|
||||||
ScalableDimension=scalable_dimension,
|
ScalableDimension=scalable_dimension,
|
||||||
PolicyType="ABCDEFG",
|
PolicyType="ABCDEFG",
|
||||||
**policy_body_kwargs
|
**policy_body_kwargs,
|
||||||
)
|
)
|
||||||
e.value.response["Error"]["Message"].should.match(
|
e.value.response["Error"]["Message"].should.match(
|
||||||
r"Unknown policy type .* specified."
|
r"Unknown policy type .* specified."
|
||||||
@ -374,7 +375,7 @@ def test_put_scaling_policy(policy_type, policy_body_kwargs):
|
|||||||
ResourceId=resource_id,
|
ResourceId=resource_id,
|
||||||
ScalableDimension=scalable_dimension,
|
ScalableDimension=scalable_dimension,
|
||||||
PolicyType=policy_type,
|
PolicyType=policy_type,
|
||||||
**policy_body_kwargs
|
**policy_body_kwargs,
|
||||||
)
|
)
|
||||||
response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
|
response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
|
||||||
response["PolicyARN"].should.match(
|
response["PolicyARN"].should.match(
|
||||||
@ -535,3 +536,139 @@ def test_deregister_scalable_target():
|
|||||||
ScalableDimension=scalable_dimension,
|
ScalableDimension=scalable_dimension,
|
||||||
)
|
)
|
||||||
e.value.response["Error"]["Message"].should.match(r"No scalable target found .*")
|
e.value.response["Error"]["Message"].should.match(r"No scalable target found .*")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_applicationautoscaling
|
||||||
|
def test_delete_scheduled_action():
|
||||||
|
client = boto3.client("application-autoscaling", region_name="eu-west-1")
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ecs")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(0)
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
client.put_scheduled_action(
|
||||||
|
ServiceNamespace="ecs",
|
||||||
|
ScheduledActionName=f"ecs_action_{i}",
|
||||||
|
ResourceId=f"ecs:cluster:{i}",
|
||||||
|
ScalableDimension="ecs:service:DesiredCount",
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ecs")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(3)
|
||||||
|
|
||||||
|
client.delete_scheduled_action(
|
||||||
|
ServiceNamespace="ecs",
|
||||||
|
ScheduledActionName="ecs_action_0",
|
||||||
|
ResourceId="ecs:cluster:0",
|
||||||
|
ScalableDimension="ecs:service:DesiredCount",
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ecs")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(2)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_applicationautoscaling
|
||||||
|
def test_describe_scheduled_actions():
|
||||||
|
client = boto3.client("application-autoscaling", region_name="eu-west-1")
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ecs")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(0)
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
client.put_scheduled_action(
|
||||||
|
ServiceNamespace="ecs",
|
||||||
|
ScheduledActionName=f"ecs_action_{i}",
|
||||||
|
ResourceId=f"ecs:cluster:{i}",
|
||||||
|
ScalableDimension="ecs:service:DesiredCount",
|
||||||
|
)
|
||||||
|
client.put_scheduled_action(
|
||||||
|
ServiceNamespace="dynamodb",
|
||||||
|
ScheduledActionName=f"ddb_action_{i}",
|
||||||
|
ResourceId=f"table/table_{i}",
|
||||||
|
ScalableDimension="dynamodb:table:ReadCapacityUnits",
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ecs")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(3)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ec2")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(0)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(
|
||||||
|
ServiceNamespace="dynamodb", ScheduledActionNames=["ddb_action_0"]
|
||||||
|
)
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(1)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(
|
||||||
|
ServiceNamespace="dynamodb",
|
||||||
|
ResourceId="table/table_0",
|
||||||
|
ScalableDimension="dynamodb:table:ReadCapacityUnits",
|
||||||
|
)
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(1)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(
|
||||||
|
ServiceNamespace="dynamodb",
|
||||||
|
ResourceId="table/table_0",
|
||||||
|
ScalableDimension="dynamodb:table:WriteCapacityUnits",
|
||||||
|
)
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(0)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_applicationautoscaling
|
||||||
|
def test_put_scheduled_action():
|
||||||
|
client = boto3.client("application-autoscaling", region_name="ap-southeast-1")
|
||||||
|
client.put_scheduled_action(
|
||||||
|
ServiceNamespace="ecs",
|
||||||
|
ScheduledActionName="action_name",
|
||||||
|
ResourceId="ecs:cluster:x",
|
||||||
|
ScalableDimension="ecs:service:DesiredCount",
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ecs")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(1)
|
||||||
|
|
||||||
|
action = resp["ScheduledActions"][0]
|
||||||
|
action.should.have.key("ScheduledActionName").equals("action_name")
|
||||||
|
action.should.have.key("ScheduledActionARN").equals(
|
||||||
|
f"arn:aws:autoscaling:ap-southeast-1:{ACCOUNT_ID}:scheduledAction:ecs:scheduledActionName/action_name"
|
||||||
|
)
|
||||||
|
action.should.have.key("ServiceNamespace").equals("ecs")
|
||||||
|
action.should.have.key("ResourceId").equals("ecs:cluster:x")
|
||||||
|
action.should.have.key("ScalableDimension").equals("ecs:service:DesiredCount")
|
||||||
|
action.should.have.key("CreationTime")
|
||||||
|
action.shouldnt.have.key("ScalableTargetAction")
|
||||||
|
|
||||||
|
|
||||||
|
@mock_applicationautoscaling
|
||||||
|
def test_put_scheduled_action__use_update():
|
||||||
|
client = boto3.client("application-autoscaling", region_name="ap-southeast-1")
|
||||||
|
client.put_scheduled_action(
|
||||||
|
ServiceNamespace="ecs",
|
||||||
|
ScheduledActionName="action_name",
|
||||||
|
ResourceId="ecs:cluster:x",
|
||||||
|
ScalableDimension="ecs:service:DesiredCount",
|
||||||
|
)
|
||||||
|
client.put_scheduled_action(
|
||||||
|
ServiceNamespace="ecs",
|
||||||
|
ScheduledActionName="action_name_updated",
|
||||||
|
ResourceId="ecs:cluster:x",
|
||||||
|
ScalableDimension="ecs:service:DesiredCount",
|
||||||
|
ScalableTargetAction={
|
||||||
|
"MinCapacity": 12,
|
||||||
|
"MaxCapacity": 23,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = client.describe_scheduled_actions(ServiceNamespace="ecs")
|
||||||
|
resp.should.have.key("ScheduledActions").length_of(1)
|
||||||
|
|
||||||
|
action = resp["ScheduledActions"][0]
|
||||||
|
action.should.have.key("ScheduledActionName").equals("action_name_updated")
|
||||||
|
action.should.have.key("ScheduledActionARN").equals(
|
||||||
|
f"arn:aws:autoscaling:ap-southeast-1:{ACCOUNT_ID}:scheduledAction:ecs:scheduledActionName/action_name"
|
||||||
|
)
|
||||||
|
action.should.have.key("ServiceNamespace").equals("ecs")
|
||||||
|
action.should.have.key("ResourceId").equals("ecs:cluster:x")
|
||||||
|
action.should.have.key("ScalableDimension").equals("ecs:service:DesiredCount")
|
||||||
|
action.should.have.key("CreationTime")
|
||||||
|
action.should.have.key("ScalableTargetAction").equals(
|
||||||
|
{"MaxCapacity": 23, "MinCapacity": 12}
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user