diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index c74a28f628c..7533e58d4f3 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -521,6 +521,7 @@ public class EventTypes { public static final String EVENT_VM_BACKUP_SCHEDULE_CONFIGURE = "BACKUP.SCHEDULE.CONFIGURE"; public static final String EVENT_VM_BACKUP_SCHEDULE_DELETE = "BACKUP.SCHEDULE.DELETE"; public static final String EVENT_VM_BACKUP_USAGE_METRIC = "BACKUP.USAGE.METRIC"; + public static final String EVENT_VM_BACKUP_EDIT = "BACKUP.OFFERING.EDIT"; // external network device events public static final String EVENT_EXTERNAL_NVP_CONTROLLER_ADD = "PHYSICAL.NVPCONTROLLER.ADD"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java new file mode 100644 index 00000000000..6e4baaedd3d --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java @@ -0,0 +1,107 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.backup; + +import javax.inject.Inject; + +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.backup.BackupManager; +import org.apache.cloudstack.backup.BackupOffering; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.user.Account; +import com.cloud.utils.exception.CloudRuntimeException; + +@APICommand(name = "updateBackupOffering", description = "Updates a backup offering.", responseObject = BackupOfferingResponse.class, +requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.16.0") +public class UpdateBackupOfferingCmd extends BaseCmd { + private static final Logger LOGGER = Logger.getLogger(UpdateBackupOfferingCmd.class.getName()); + private static final String APINAME = "updateBackupOffering"; + + @Inject + private BackupManager backupManager; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = BackupOfferingResponse.class, required = true, description = "The ID of the Backup Offering to be updated") + private Long id; + + @Parameter(name = ApiConstants.DESCRIPTION, type = CommandType.STRING, description = "The description of the Backup Offering to be updated") + private String description; + + @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "The name of the Backup Offering to be updated") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + @Override + public void execute() { + try { + if (StringUtils.isAllEmpty(name, description)) { + throw new InvalidParameterValueException(String.format("Can't update Backup Offering [id: %s] because there is no change in name or description.", id)); + } + + BackupOffering result = backupManager.updateBackupOffering(this); + + if (result == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to update backup offering [id: %s, name: %s, description: %s].", id, name, description)); + } + BackupOfferingResponse response = _responseGenerator.createBackupOfferingResponse(result); + response.setResponseName(getCommandName()); + this.setResponseObject(response); + } catch (CloudRuntimeException e) { + ApiErrorCode paramError = e instanceof InvalidParameterValueException ? ApiErrorCode.PARAM_ERROR : ApiErrorCode.INTERNAL_ERROR; + LOGGER.error(String.format("Failed to update Backup Offering [id: %s] due to: [%s].", id, e.getMessage()), e); + throw new ServerApiException(paramError, e.getMessage()); + } + } + + @Override + public String getCommandName() { + return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } +} \ No newline at end of file diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index 57511252d58..b983aca4ad6 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -20,6 +20,7 @@ package org.apache.cloudstack.backup; import java.util.List; import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; +import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupsCmd; @@ -137,4 +138,6 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer * @return returns operation success */ boolean deleteBackup(final Long backupId); + + BackupOffering updateBackupOffering(UpdateBackupOfferingCmd updateBackupOfferingCmd); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java index c5d8790321e..d30385af575 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java @@ -93,6 +93,10 @@ public class BackupOfferingVO implements BackupOffering { return name; } + public void setName(String name) { + this.name = name; + } + public String getExternalId() { return externalId; } @@ -120,6 +124,10 @@ public class BackupOfferingVO implements BackupOffering { return description; } + public void setDescription(String description) { + this.description = description; + } + public Date getCreated() { return created; } diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index 154bbced915..0ef2ada2800 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -36,6 +36,7 @@ import org.apache.cloudstack.api.command.admin.backup.DeleteBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd; import org.apache.cloudstack.api.command.admin.backup.ListBackupProviderOfferingsCmd; import org.apache.cloudstack.api.command.admin.backup.ListBackupProvidersCmd; +import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.AssignVirtualMachineToBackupOfferingCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; @@ -62,6 +63,7 @@ import org.apache.cloudstack.poll.BackgroundPollManager; import org.apache.cloudstack.poll.BackgroundPollTask; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.log4j.Logger; @@ -767,6 +769,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { cmdList.add(ImportBackupOfferingCmd.class); cmdList.add(ListBackupOfferingsCmd.class); cmdList.add(DeleteBackupOfferingCmd.class); + cmdList.add(UpdateBackupOfferingCmd.class); // Assignment cmdList.add(AssignVirtualMachineToBackupOfferingCmd.class); cmdList.add(RemoveVirtualMachineFromBackupOfferingCmd.class); @@ -1050,4 +1053,42 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { return BackupSyncPollingInterval.value() * 1000L; } } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_EDIT, eventDescription = "updating backup offering") + public BackupOffering updateBackupOffering(UpdateBackupOfferingCmd updateBackupOfferingCmd) { + Long id = updateBackupOfferingCmd.getId(); + String name = updateBackupOfferingCmd.getName(); + String description = updateBackupOfferingCmd.getDescription(); + + BackupOfferingVO backupOfferingVO = backupOfferingDao.findById(id); + if (backupOfferingVO == null) { + throw new InvalidParameterValueException(String.format("Unable to find Backup Offering with id: [%s].", id)); + } + + LOG.debug(String.format("Trying to update Backup Offering [id: %s, name: %s, description: %s] to [name: %s, description: %s].", + backupOfferingVO.getUuid(), backupOfferingVO.getName(), backupOfferingVO.getDescription(), name, description)); + + BackupOfferingVO offering = backupOfferingDao.createForUpdate(id); + List fields = new ArrayList<>(); + if (name != null) { + offering.setName(name); + fields.add("name: " + name); + } + + if (description != null) { + offering.setDescription(description); + fields.add("description: " + description); + } + + if (!backupOfferingDao.update(id, offering)) { + LOG.warn(String.format("Couldn't update Backup offering [id: %s] with [%s].", id, String.join(", ", fields))); + } + + BackupOfferingVO response = backupOfferingDao.findById(id); + CallContext.current().setEventDetails(String.format("Backup Offering updated [%s].", + ReflectionToStringBuilderUtils.reflectOnlySelectedFields(response, "id", "name", "description", "externalId"))); + return response; + } + } diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java new file mode 100644 index 00000000000..040f4d36ce4 --- /dev/null +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -0,0 +1,123 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.backup; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd; +import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import com.cloud.exception.InvalidParameterValueException; + +public class BackupManagerTest { + @Spy + @InjectMocks + BackupManagerImpl backupManager = new BackupManagerImpl(); + + @Mock + BackupOfferingDao backupOfferingDao; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + when(backupOfferingDao.findById(null)).thenReturn(null); + when(backupOfferingDao.findById(123l)).thenReturn(null); + + BackupOfferingVO offering = Mockito.spy(BackupOfferingVO.class); + when(offering.getId()).thenReturn(1234l); + when(offering.getName()).thenCallRealMethod(); + when(offering.getDescription()).thenCallRealMethod(); + + BackupOfferingVO offeringUpdate = Mockito.spy(BackupOfferingVO.class); + when(offeringUpdate.getId()).thenReturn(1234l); + when(offeringUpdate.getName()).thenReturn("Old name"); + when(offeringUpdate.getDescription()).thenReturn("Old description"); + + when(backupOfferingDao.findById(1234l)).thenReturn(offering); + when(backupOfferingDao.createForUpdate(1234l)).thenReturn(offeringUpdate); + when(backupOfferingDao.update(1234l, offeringUpdate)).thenAnswer(answer -> { + offering.setName("New name"); + offering.setDescription("New description"); + return true; + }); + } + + @Test + public void testExceptionWhenUpdateWithNullId() { + try { + Long id = null; + + UpdateBackupOfferingCmd cmd = Mockito.spy(UpdateBackupOfferingCmd.class); + when(cmd.getId()).thenReturn(id); + + backupManager.updateBackupOffering(cmd); + } catch (InvalidParameterValueException e) { + assertEquals("Unable to find Backup Offering with id: [null].", e.getMessage()); + } + } + + @Test + public void testExceptionWhenUpdateWithNonExistentId() { + try { + Long id = 123l; + + UpdateBackupOfferingCmd cmd = Mockito.spy(UpdateBackupOfferingCmd.class); + when(cmd.getId()).thenReturn(id); + + backupManager.updateBackupOffering(cmd); + } catch (InvalidParameterValueException e) { + assertEquals("Unable to find Backup Offering with id: [123].", e.getMessage()); + } + } + + @Test + public void testExceptionWhenUpdateWithoutChanges() { + try { + Long id = 1234l; + + UpdateBackupOfferingCmd cmd = Mockito.spy(UpdateBackupOfferingCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getName()).thenReturn(null); + when(cmd.getDescription()).thenReturn(null); + + backupManager.updateBackupOffering(cmd); + } catch (InvalidParameterValueException e) { + assertEquals("Can't update Backup Offering [id: 1234] because there is no change in name or description.", e.getMessage()); + } + } + + @Test + public void testUpdateBackupOfferingSuccess() { + Long id = 1234l; + + UpdateBackupOfferingCmd cmd = Mockito.spy(UpdateBackupOfferingCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getName()).thenReturn("New name"); + when(cmd.getDescription()).thenReturn("New description"); + + BackupOffering updated = backupManager.updateBackupOffering(cmd); + assertEquals("New name", updated.getName()); + assertEquals("New description", updated.getDescription()); + } +} \ No newline at end of file