Management API: Expose lastStatusCode property of action entities (#1313)
* Enhance Mgmt REST API to expose lastStatusCode property of actions * Add unit test
This commit is contained in:
@@ -75,6 +75,9 @@ public class MgmtAction extends MgmtBaseEntity {
|
||||
@JsonProperty
|
||||
private String rolloutName;
|
||||
|
||||
@JsonProperty
|
||||
private Integer lastStatusCode;
|
||||
|
||||
public MgmtMaintenanceWindow getMaintenanceWindow() {
|
||||
return maintenanceWindow;
|
||||
}
|
||||
@@ -155,4 +158,12 @@ public class MgmtAction extends MgmtBaseEntity {
|
||||
this.detailStatus = detailStatus;
|
||||
}
|
||||
|
||||
public Integer getLastStatusCode() {
|
||||
return lastStatusCode;
|
||||
}
|
||||
|
||||
public void setLastStatusCode(final Integer lastStatusCode) {
|
||||
this.lastStatusCode = lastStatusCode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ import org.eclipse.hawkbit.repository.builder.TargetCreate;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
||||
import org.eclipse.hawkbit.repository.model.ActionStatus;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.AutoConfirmationStatus;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.MetaData;
|
||||
import org.eclipse.hawkbit.repository.model.PollStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
@@ -257,6 +257,10 @@ public final class MgmtTargetMapper {
|
||||
|
||||
result.setDetailStatus(action.getStatus().toString().toLowerCase());
|
||||
|
||||
action.getLastActionStatusCode().ifPresent(statusCode -> {
|
||||
result.setLastStatusCode(statusCode);
|
||||
});
|
||||
|
||||
final Rollout rollout = action.getRollout();
|
||||
if (rollout != null) {
|
||||
result.setRollout(rollout.getId());
|
||||
|
||||
@@ -50,6 +50,7 @@ import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType;
|
||||
import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
|
||||
import org.eclipse.hawkbit.repository.ActionFields;
|
||||
import org.eclipse.hawkbit.repository.Identifiable;
|
||||
import org.eclipse.hawkbit.repository.builder.ActionStatusCreate;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
@@ -141,8 +142,8 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
final int limitSize = 2;
|
||||
final String knownTargetId = "targetId";
|
||||
final List<Action> actions = generateTargetWithTwoUpdatesWithOneOverride(knownTargetId);
|
||||
controllerManagement.addUpdateActionStatus(
|
||||
entityFactory.actionStatus().create(actions.get(0).getId()).status(Status.FINISHED).message("test"));
|
||||
assertThat(actions).hasSize(2);
|
||||
updateActionStatus(actions.get(0), Status.FINISHED, null, "test");
|
||||
|
||||
final PageRequest pageRequest = PageRequest.of(0, 1000, Direction.ASC, ActionFields.ID.getFieldName());
|
||||
final Action action = deploymentManagement.findActionsByTarget(knownTargetId, pageRequest).getContent().get(0);
|
||||
@@ -1353,7 +1354,7 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
if (confirmationFlowActive) {
|
||||
enableConfirmationFlow();
|
||||
}
|
||||
|
||||
|
||||
final JSONObject jsonPayload = new JSONObject();
|
||||
jsonPayload.put("id", set.getId());
|
||||
if (confirmationRequired != null) {
|
||||
@@ -2050,9 +2051,9 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
final JSONObject bodyValid = getAssignmentObject(dsId, MgmtActionType.FORCED, 98);
|
||||
|
||||
mvc.perform(post("/rest/v1/targets/{targetId}/assignedDS", targetId).content(bodyValid.toString())
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled")));
|
||||
.contentType(MediaType.APPLICATION_JSON)).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.errorCode", equalTo("hawkbit.server.error.multiassignmentNotEnabled")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2207,7 +2208,7 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
@Description("Ensures that a post request for creating target with target type works.")
|
||||
void createTargetWithExistingTargetType() throws Exception {
|
||||
// create target type
|
||||
List<TargetType> targetTypes = testdataFactory.createTargetTypes("targettype", 1);
|
||||
final List<TargetType> targetTypes = testdataFactory.createTargetTypes("targettype", 1);
|
||||
assertThat(targetTypes).hasSize(1);
|
||||
|
||||
final Target target = entityFactory.target().create().controllerId("targetcontroller").name("testtarget")
|
||||
@@ -2229,11 +2230,11 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
@Description("Ensures that a put request for updating targets with target type works.")
|
||||
void updateTargetTypeInTarget() throws Exception {
|
||||
// create target type
|
||||
List<TargetType> targetTypes = testdataFactory.createTargetTypes("targettype", 2);
|
||||
final List<TargetType> targetTypes = testdataFactory.createTargetTypes("targettype", 2);
|
||||
assertThat(targetTypes).hasSize(2);
|
||||
|
||||
String controllerId = "targetcontroller";
|
||||
Target target = testdataFactory.createTarget(controllerId, "testtarget", targetTypes.get(0).getId());
|
||||
final String controllerId = "targetcontroller";
|
||||
final Target target = testdataFactory.createTarget(controllerId, "testtarget", targetTypes.get(0).getId());
|
||||
|
||||
assertThat(target).isNotNull();
|
||||
assertThat(target.getTargetType().getId()).isEqualTo(targetTypes.get(0).getId());
|
||||
@@ -2250,13 +2251,14 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
@Test
|
||||
@Description("Ensures that a post request for creating targets with unknown target type fails.")
|
||||
void addingNonExistingTargetTypeInTargetShouldFail() throws Exception {
|
||||
long unknownTargetTypeId = 999;
|
||||
String errorMsg = String.format("TargetType with given identifier {%s} does not exist.", unknownTargetTypeId);
|
||||
final long unknownTargetTypeId = 999;
|
||||
final String errorMsg = String.format("TargetType with given identifier {%s} does not exist.",
|
||||
unknownTargetTypeId);
|
||||
|
||||
Optional<TargetType> targetType = targetTypeManagement.get(unknownTargetTypeId);
|
||||
final Optional<TargetType> targetType = targetTypeManagement.get(unknownTargetTypeId);
|
||||
assertThat(targetType).isNotPresent();
|
||||
|
||||
String controllerId = "targetcontroller";
|
||||
final String controllerId = "targetcontroller";
|
||||
final Target target = entityFactory.target().create().controllerId(controllerId).name("testtarget").build();
|
||||
|
||||
final String targetList = JsonBuilder.targets(Collections.singletonList(target), false, unknownTargetTypeId);
|
||||
@@ -2271,12 +2273,12 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
@Description("Ensures that a post request for assign target type to target works.")
|
||||
void assignTargetTypeToTarget() throws Exception {
|
||||
// create target type
|
||||
TargetType targetType = testdataFactory.findOrCreateTargetType("targettype");
|
||||
final TargetType targetType = testdataFactory.findOrCreateTargetType("targettype");
|
||||
assertThat(targetType).isNotNull();
|
||||
|
||||
// create target
|
||||
String targetControllerId = "targetcontroller";
|
||||
Target target = testdataFactory.createTarget(targetControllerId, "testtarget");
|
||||
final String targetControllerId = "targetcontroller";
|
||||
final Target target = testdataFactory.createTarget(targetControllerId, "testtarget");
|
||||
assertThat(target).isNotNull();
|
||||
|
||||
// assign target type over rest resource
|
||||
@@ -2292,11 +2294,11 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
@Description("Ensures that a post request for assign a invalid target type to target fails.")
|
||||
void assignInvalidTargetTypeToTargetFails() throws Exception {
|
||||
// Invalid target type ID
|
||||
long invalidTargetTypeId = 999;
|
||||
final long invalidTargetTypeId = 999;
|
||||
|
||||
// create target
|
||||
String targetControllerId = "targetcontroller";
|
||||
Target target = testdataFactory.createTarget(targetControllerId, "testtarget");
|
||||
final String targetControllerId = "targetcontroller";
|
||||
final Target target = testdataFactory.createTarget(targetControllerId, "testtarget");
|
||||
assertThat(target).isNotNull();
|
||||
|
||||
// assign invalid target type over rest resource
|
||||
@@ -2304,7 +2306,8 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
.content("{\"id\":" + invalidTargetTypeId + "}").contentType(MediaType.APPLICATION_JSON))
|
||||
.andDo(MockMvcResultPrinter.print()).andExpect(status().isNotFound());
|
||||
|
||||
// verify response json exception message if body does not include id field
|
||||
// verify response json exception message if body does not include id
|
||||
// field
|
||||
final MvcResult mvcResult = mvc
|
||||
.perform(post(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/" + targetControllerId + "/targettype")
|
||||
.content("{\"unknownfield\":" + invalidTargetTypeId + "}")
|
||||
@@ -2321,11 +2324,12 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
@Description("Ensures that a delete request for unassign target type from target works.")
|
||||
void unassignTargetTypeFromTarget() throws Exception {
|
||||
// create target type
|
||||
List<TargetType> targetTypes = testdataFactory.createTargetTypes("targettype", 1);
|
||||
final List<TargetType> targetTypes = testdataFactory.createTargetTypes("targettype", 1);
|
||||
assertThat(targetTypes).hasSize(1);
|
||||
|
||||
String targetControllerId = "targetcontroller";
|
||||
Target target = testdataFactory.createTarget(targetControllerId, "testtarget", targetTypes.get(0).getId());
|
||||
final String targetControllerId = "targetcontroller";
|
||||
final Target target = testdataFactory.createTarget(targetControllerId, "testtarget",
|
||||
targetTypes.get(0).getId());
|
||||
|
||||
assertThat(target).isNotNull();
|
||||
assertThat(target.getTargetType().getId()).isEqualTo(targetTypes.get(0).getId());
|
||||
@@ -2384,16 +2388,15 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
|
||||
// GET with all possible responses
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/" + TARGET_V1_AUTO_CONFIRM,
|
||||
knownTargetId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("active", equalTo(Boolean.TRUE)))
|
||||
.andExpect(initiator == null ? jsonPath("initiator").doesNotExist()
|
||||
: jsonPath("initiator", equalTo(initiator)))
|
||||
.andExpect(remark == null ? jsonPath("remark").doesNotExist() : jsonPath("remark", equalTo(remark)))
|
||||
.andExpect(jsonPath("_links.deactivate").exists())
|
||||
.andExpect(jsonPath("_links.activate").doesNotExist());
|
||||
knownTargetId)).andDo(MockMvcResultPrinter.print()).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("active", equalTo(Boolean.TRUE)))
|
||||
.andExpect(initiator == null ? jsonPath("initiator").doesNotExist()
|
||||
: jsonPath("initiator", equalTo(initiator)))
|
||||
.andExpect(remark == null ? jsonPath("remark").doesNotExist() : jsonPath("remark", equalTo(remark)))
|
||||
.andExpect(jsonPath("_links.deactivate").exists())
|
||||
.andExpect(jsonPath("_links.activate").doesNotExist());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void getAutoConfirmStateFromTargetsEndpoint() throws Exception {
|
||||
final String knownTargetId = "targetId";
|
||||
@@ -2453,6 +2456,64 @@ class MgmtTargetResourceTest extends AbstractManagementApiIntegrationTest {
|
||||
.andExpect(jsonPath("autoConfirmActive").exists()).andExpect(jsonPath("_links.autoConfirm").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verifies that the status code that was reported in the last action status update is correctly exposed via the action.")
|
||||
void lastActionStatusCode() throws Exception {
|
||||
|
||||
// prepare test
|
||||
final DistributionSet dsA = testdataFactory.createDistributionSet("");
|
||||
final Target target = testdataFactory.createTarget("knownTargetId");
|
||||
final Action action = getFirstAssignedAction(assignDistributionSet(dsA, Collections.singletonList(target)));
|
||||
|
||||
// no status update yet -> no status code
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}",
|
||||
target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode").doesNotExist());
|
||||
|
||||
// update action status with status code
|
||||
updateActionStatus(action, Status.RUNNING, 100);
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}",
|
||||
target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode", equalTo(100)))
|
||||
.andExpect(jsonPath("detailStatus", equalTo("running")));
|
||||
|
||||
// update action status without a status code
|
||||
updateActionStatus(action, Status.RUNNING, null);
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}",
|
||||
target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode").doesNotExist())
|
||||
.andExpect(jsonPath("detailStatus", equalTo("running")));
|
||||
|
||||
// update action status with status code
|
||||
updateActionStatus(action, Status.ERROR, 432);
|
||||
mvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/actions/{actionId}",
|
||||
target.getControllerId(), action.getId())).andDo(MockMvcResultPrinter.print())
|
||||
.andExpect(status().isOk()).andExpect(jsonPath("lastStatusCode", equalTo(432)))
|
||||
.andExpect(jsonPath("detailStatus", equalTo("error")));
|
||||
}
|
||||
|
||||
private Action updateActionStatus(final Action action, final Status status, final Integer statusCode) {
|
||||
return updateActionStatus(action, status, statusCode, null);
|
||||
}
|
||||
|
||||
private Action updateActionStatus(final Action action, final Status status, final Integer statusCode,
|
||||
final String message) {
|
||||
|
||||
assertThat(action).isNotNull();
|
||||
assertThat(status).isNotNull();
|
||||
|
||||
final ActionStatusCreate actionStatus = entityFactory.actionStatus().create(action.getId());
|
||||
actionStatus.status(status);
|
||||
if (statusCode != null) {
|
||||
actionStatus.code(statusCode);
|
||||
}
|
||||
if (message != null) {
|
||||
actionStatus.message(message);
|
||||
}
|
||||
|
||||
return controllerManagement.addUpdateActionStatus(actionStatus);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> possibleActiveStates() {
|
||||
return Stream.of(Arguments.of("someInitiator", "someRemark"), Arguments.of(null, "someRemark"),
|
||||
Arguments.of("someInitiator", null), Arguments.of(null, null));
|
||||
|
||||
@@ -162,6 +162,8 @@ public final class MgmtApiModelProperties {
|
||||
|
||||
public static final String ACTION_STATUS_CODE = "(Optional) Code provided by the device related to the status.";
|
||||
|
||||
public static final String ACTION_LAST_STATUS_CODE = "(Optional) Code provided as part of the last status update that was sent by the device.";
|
||||
|
||||
public static final String ACTION_STATUS_LIST = "List of action status.";
|
||||
|
||||
public static final String ACTION_EXECUTION_STATUS = "Status of action.";
|
||||
|
||||
@@ -52,7 +52,8 @@ public class ActionResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
@Description("Handles the GET request of retrieving all actions. Required Permission: READ_TARGET.")
|
||||
public void getActions() throws Exception {
|
||||
enableMultiAssignments();
|
||||
generateRolloutActionForTarget(targetId);
|
||||
final Action action = generateRolloutActionForTarget(targetId);
|
||||
provideCodeFeedback(action, 200);
|
||||
|
||||
mockMvc.perform(get(MgmtRestConstants.ACTION_V1_REQUEST_MAPPING)).andExpect(status().isOk())
|
||||
.andDo(MockMvcResultPrinter.print())
|
||||
@@ -74,6 +75,8 @@ public class ActionResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
fieldWithPath("content[].detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS)
|
||||
.attributes(key("value").value(
|
||||
"['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")),
|
||||
optionalRequestFieldWithPath("content[].lastStatusCode")
|
||||
.description(MgmtApiModelProperties.ACTION_LAST_STATUS_CODE).type("Integer"),
|
||||
fieldWithPath("content[]._links").description(MgmtApiModelProperties.LINK_TO_ACTION),
|
||||
fieldWithPath("content[].id").description(MgmtApiModelProperties.ACTION_ID),
|
||||
fieldWithPath("content[].weight").description(MgmtApiModelProperties.ACTION_WEIGHT),
|
||||
|
||||
@@ -107,8 +107,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
fieldWithPath("content[].autoConfirmActive")
|
||||
.description(MgmtApiModelProperties.AUTO_CONFIRM_ACTIVE),
|
||||
fieldWithPath("content[].installedAt").description(MgmtApiModelProperties.INSTALLED_AT),
|
||||
fieldWithPath("content[].lastModifiedAt").description(
|
||||
ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"),
|
||||
fieldWithPath("content[].lastModifiedAt")
|
||||
.description(ApiModelPropertiesGeneric.LAST_MODIFIED_AT).type("Number"),
|
||||
fieldWithPath("content[].lastModifiedBy")
|
||||
.description(ApiModelPropertiesGeneric.LAST_MODIFIED_BY).type("String"),
|
||||
fieldWithPath("content[].ipAddress").description(MgmtApiModelProperties.IP_ADDRESS)
|
||||
@@ -362,6 +362,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
public void getActionFromTarget() throws Exception {
|
||||
enableMultiAssignments();
|
||||
final Action action = generateRolloutActionForTarget(targetId, true, true);
|
||||
provideCodeFeedback(action, 200);
|
||||
|
||||
assertThat(deploymentManagement.findAction(action.getId()).get().getActionType())
|
||||
.isEqualTo(ActionType.TIMEFORCED);
|
||||
|
||||
@@ -390,6 +392,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
fieldWithPath("detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS)
|
||||
.attributes(key("value").value(
|
||||
"['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")),
|
||||
optionalRequestFieldWithPath("lastStatusCode")
|
||||
.description(MgmtApiModelProperties.ACTION_LAST_STATUS_CODE).type("Integer"),
|
||||
fieldWithPath("rollout").description(MgmtApiModelProperties.ACTION_ROLLOUT),
|
||||
fieldWithPath("rolloutName").description(MgmtApiModelProperties.ACTION_ROLLOUT_NAME),
|
||||
fieldWithPath("_links.self").ignored(),
|
||||
@@ -406,6 +410,7 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
enableMultiAssignments();
|
||||
final Action action = generateActionForTarget(targetId, true, true, getTestSchedule(2), getTestDuration(1),
|
||||
getTestTimeZone());
|
||||
provideCodeFeedback(action, 200);
|
||||
|
||||
mockMvc.perform(get(MgmtRestConstants.TARGET_V1_REQUEST_MAPPING + "/{targetId}/"
|
||||
+ MgmtRestConstants.TARGET_V1_ACTIONS + "/{actionId}", targetId, action.getId()))
|
||||
@@ -432,6 +437,8 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
fieldWithPath("detailStatus").description(MgmtApiModelProperties.ACTION_DETAIL_STATUS)
|
||||
.attributes(key("value").value(
|
||||
"['finished', 'error', 'running', 'warning', 'scheduled', 'canceling', 'canceled', 'download', 'downloaded', 'retrieved', 'cancel_rejected']")),
|
||||
optionalRequestFieldWithPath("lastStatusCode")
|
||||
.description(MgmtApiModelProperties.ACTION_LAST_STATUS_CODE).type("Integer"),
|
||||
fieldWithPath("maintenanceWindow")
|
||||
.description(MgmtApiModelProperties.MAINTENANCE_WINDOW),
|
||||
fieldWithPath("maintenanceWindow.schedule")
|
||||
@@ -1005,11 +1012,6 @@ public class TargetResourceDocumentationTest extends AbstractApiRestDocumentatio
|
||||
return generateActionForTarget(knownControllerId, inSync, timeforced, null, null, null, true);
|
||||
}
|
||||
|
||||
private Action generateActionForTarget(final String knownControllerId, final boolean inSync,
|
||||
final boolean timeforced) throws Exception {
|
||||
return generateActionForTarget(knownControllerId, inSync, timeforced, null, null, null);
|
||||
}
|
||||
|
||||
private Action generateActionForTarget(final String knownControllerId, final boolean inSync,
|
||||
final boolean timeforced, final String maintenanceWindowSchedule, final String maintenanceWindowDuration,
|
||||
final String maintenanceWindowTimeZone) throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user