Skip to content

Commit 10333df

Browse files
committed
add recently added domain id for bkp offering to be inherited in clone operation
1 parent 3511525 commit 10333df

File tree

7 files changed

+270
-4
lines changed

7 files changed

+270
-4
lines changed

api/src/main/java/org/apache/cloudstack/api/command/admin/backup/CloneBackupOfferingCmd.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.cloudstack.api.BaseCmd;
2727
import org.apache.cloudstack.api.Parameter;
2828
import org.apache.cloudstack.api.ServerApiException;
29+
import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
2930
import org.apache.cloudstack.api.response.BackupOfferingResponse;
3031
import org.apache.cloudstack.api.response.ZoneResponse;
3132
import org.apache.cloudstack.backup.BackupManager;
@@ -41,11 +42,15 @@
4142
import com.cloud.exception.ResourceUnavailableException;
4243
import com.cloud.utils.exception.CloudRuntimeException;
4344

45+
import java.util.Arrays;
46+
import java.util.List;
47+
import java.util.function.LongFunction;
48+
4449
@APICommand(name = "cloneBackupOffering",
4550
description = "Clones a backup offering from an existing offering",
4651
responseObject = BackupOfferingResponse.class, since = "4.14.0",
4752
authorized = {RoleType.Admin})
48-
public class CloneBackupOfferingCmd extends BaseAsyncCmd {
53+
public class CloneBackupOfferingCmd extends BaseAsyncCmd implements DomainAndZoneIdResolver {
4954

5055
@Inject
5156
protected BackupManager backupManager;
@@ -74,6 +79,13 @@ public class CloneBackupOfferingCmd extends BaseAsyncCmd {
7479
description = "The zone ID", required = false)
7580
private Long zoneId;
7681

82+
@Parameter(name = ApiConstants.DOMAIN_ID,
83+
type = CommandType.STRING,
84+
description = "the ID of the containing domain(s) as comma separated string, public for public offerings",
85+
since = "4.23.0",
86+
length = 4096)
87+
private String domainIds;
88+
7789
@Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = BaseCmd.CommandType.BOOLEAN,
7890
description = "Whether users are allowed to create adhoc backups and backup schedules", required = false)
7991
private Boolean userDrivenBackups;
@@ -106,6 +118,17 @@ public Boolean getUserDrivenBackups() {
106118
return userDrivenBackups;
107119
}
108120

121+
public List<Long> getDomainIds() {
122+
if (domainIds != null && !domainIds.isEmpty()) {
123+
return Arrays.asList(Arrays.stream(domainIds.split(",")).map(domainId -> Long.parseLong(domainId.trim())).toArray(Long[]::new));
124+
}
125+
LongFunction<List<Long>> defaultDomainsProvider = null;
126+
if (backupManager != null) {
127+
defaultDomainsProvider = backupManager::getBackupOfferingDomains;
128+
}
129+
return resolveDomainIds(domainIds, sourceOfferingId, defaultDomainsProvider, "backup offering");
130+
}
131+
109132
/////////////////////////////////////////////////////
110133
/////////////// API Implementation///////////////////
111134
/////////////////////////////////////////////////////

api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd {
8686
type = CommandType.LIST,
8787
collectionType = CommandType.UUID,
8888
entityType = DomainResponse.class,
89-
description = "the ID of the containing domain(s), null for public offerings")
89+
description = "the ID of the containing domain(s), null for public offerings",
90+
since = "4.23.0")
9091
private List<Long> domainIds;
9192

9293
/////////////////////////////////////////////////////

server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static org.apache.cloudstack.framework.config.ConfigKey.CATEGORY_SYSTEM;
2323

2424
import java.io.UnsupportedEncodingException;
25+
import java.lang.reflect.Field;
2526
import java.net.URI;
2627
import java.net.URISyntaxException;
2728
import java.net.URLDecoder;
@@ -8663,15 +8664,15 @@ public static void applyBooleanIfNotProvided(Object cmd, Map<String, String> req
86638664
}
86648665

86658666
public static void setField(Object obj, String fieldName, Object value) throws Exception {
8666-
java.lang.reflect.Field field = findField(obj.getClass(), fieldName);
8667+
Field field = findField(obj.getClass(), fieldName);
86678668
if (field == null) {
86688669
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy of " + obj.getClass().getName());
86698670
}
86708671
field.setAccessible(true);
86718672
field.set(obj, value);
86728673
}
86738674

8674-
public static java.lang.reflect.Field findField(Class<?> clazz, String fieldName) {
8675+
public static Field findField(Class<?> clazz, String fieldName) {
86758676
Class<?> currentClass = clazz;
86768677
while (currentClass != null) {
86778678
try {

server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,10 +372,33 @@ public BackupOffering cloneBackupOffering(final CloneBackupOfferingCmd cmd) {
372372
throw new CloudRuntimeException("Unable to clone backup offering from ID: " + cmd.getSourceOfferingId());
373373
}
374374

375+
List<Long> filteredDomainIds = cmd.getDomainIds() == null ? new ArrayList<>() : new ArrayList<>(cmd.getDomainIds());
376+
Collections.sort(filteredDomainIds);
377+
updateBackupOfferingDomainDetail(savedOffering, filteredDomainIds);
378+
375379
logger.debug("Successfully cloned backup offering '" + sourceOffering.getName() + "' (ID: " + cmd.getSourceOfferingId() + ") to '" + cmd.getName() + "' (ID: " + savedOffering.getId() + ")");
376380
return savedOffering;
377381
}
378382

383+
private void updateBackupOfferingDomainDetail(BackupOfferingVO savedOffering, List<Long> filteredDomainIds) {
384+
if (filteredDomainIds.size() > 1) {
385+
filteredDomainIds = domainHelper.filterChildSubDomains(filteredDomainIds);
386+
}
387+
388+
if (CollectionUtils.isNotEmpty(filteredDomainIds)) {
389+
List<BackupOfferingDetailsVO> detailsVOList = new ArrayList<>();
390+
for (Long domainId : filteredDomainIds) {
391+
if (domainDao.findById(domainId) == null) {
392+
throw new InvalidParameterValueException("Please specify a valid domain id");
393+
}
394+
detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false));
395+
}
396+
if (!detailsVOList.isEmpty()) {
397+
backupOfferingDetailsDao.saveDetails(detailsVOList);
398+
}
399+
}
400+
}
401+
379402
@Override
380403
public List<Long> getBackupOfferingDomains(Long offeringId) {
381404
final BackupOffering backupOffering = backupOfferingDao.findById(offeringId);

server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import com.google.gson.Gson;
7777
import org.apache.cloudstack.api.ApiConstants;
7878
import org.apache.cloudstack.api.ServerApiException;
79+
import org.apache.cloudstack.api.command.admin.backup.CloneBackupOfferingCmd;
7980
import org.apache.cloudstack.api.command.admin.backup.ImportBackupOfferingCmd;
8081
import org.apache.cloudstack.api.command.admin.backup.UpdateBackupOfferingCmd;
8182
import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd;
@@ -132,6 +133,7 @@
132133
import static org.mockito.Mockito.verify;
133134
import static org.mockito.Mockito.when;
134135
import static org.mockito.Mockito.atLeastOnce;
136+
import org.mockito.ArgumentCaptor;
135137

136138
@RunWith(MockitoJUnitRunner.class)
137139
public class BackupManagerTest {
@@ -2518,4 +2520,106 @@ private BackupOfferingVO createMockOffering(Long id, String name) {
25182520
return offering;
25192521
}
25202522

2523+
@Test
2524+
public void testCloneBackupOfferingUsesProvidedDomainIds() {
2525+
Long sourceOfferingId = 1L;
2526+
Long zoneId = 10L;
2527+
Long savedOfferingId = 2L;
2528+
List<Long> providedDomainIds = List.of(11L);
2529+
2530+
// command
2531+
CloneBackupOfferingCmd cmd = Mockito.mock(CloneBackupOfferingCmd.class);
2532+
when(cmd.getSourceOfferingId()).thenReturn(sourceOfferingId);
2533+
when(cmd.getName()).thenReturn("Cloned Offering");
2534+
when(cmd.getDescription()).thenReturn(null);
2535+
when(cmd.getExternalId()).thenReturn(null);
2536+
when(cmd.getUserDrivenBackups()).thenReturn(null);
2537+
when(cmd.getDomainIds()).thenReturn(providedDomainIds);
2538+
2539+
// source offering
2540+
BackupOfferingVO sourceOffering = Mockito.mock(BackupOfferingVO.class);
2541+
when(sourceOffering.getZoneId()).thenReturn(zoneId);
2542+
when(sourceOffering.getExternalId()).thenReturn("ext-src");
2543+
when(sourceOffering.getProvider()).thenReturn("testbackupprovider");
2544+
when(sourceOffering.getDescription()).thenReturn("src desc");
2545+
when(sourceOffering.isUserDrivenBackupAllowed()).thenReturn(true);
2546+
when(sourceOffering.getName()).thenReturn("Source Offering");
2547+
2548+
when(backupOfferingDao.findById(sourceOfferingId)).thenReturn(sourceOffering);
2549+
when(backupOfferingDao.findByName(cmd.getName(), zoneId)).thenReturn(null);
2550+
2551+
BackupOfferingVO savedOffering = Mockito.mock(BackupOfferingVO.class);
2552+
when(savedOffering.getId()).thenReturn(savedOfferingId);
2553+
when(backupOfferingDao.persist(any(BackupOfferingVO.class))).thenReturn(savedOffering);
2554+
2555+
DomainVO domain = Mockito.mock(DomainVO.class);
2556+
when(domainDao.findById(11L)).thenReturn(domain);
2557+
2558+
overrideBackupFrameworkConfigValue();
2559+
2560+
BackupOffering result = backupManager.cloneBackupOffering(cmd);
2561+
2562+
assertEquals(savedOffering, result);
2563+
2564+
ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
2565+
verify(backupOfferingDetailsDao, times(1)).saveDetails(captor.capture());
2566+
List<BackupOfferingDetailsVO> savedDetails = captor.getValue();
2567+
assertEquals(1, savedDetails.size());
2568+
assertEquals(String.valueOf(11L), savedDetails.get(0).getValue());
2569+
}
2570+
2571+
@Test
2572+
public void testCloneBackupOfferingInheritsDomainIdsFromSource() {
2573+
Long sourceOfferingId = 3L;
2574+
Long zoneId = 20L;
2575+
Long savedOfferingId = 4L;
2576+
List<Long> sourceDomainIds = List.of(21L, 22L);
2577+
2578+
CloneBackupOfferingCmd cmd = Mockito.mock(CloneBackupOfferingCmd.class);
2579+
when(cmd.getSourceOfferingId()).thenReturn(sourceOfferingId);
2580+
when(cmd.getName()).thenReturn("Cloned Inherit Offering");
2581+
when(cmd.getDescription()).thenReturn(null);
2582+
when(cmd.getExternalId()).thenReturn(null);
2583+
when(cmd.getUserDrivenBackups()).thenReturn(null);
2584+
// Simulate resolver having provided the source offering domains (the real cmd#getDomainIds() would do this)
2585+
when(cmd.getDomainIds()).thenReturn(sourceDomainIds);
2586+
2587+
BackupOfferingVO sourceOffering = Mockito.mock(BackupOfferingVO.class);
2588+
when(sourceOffering.getZoneId()).thenReturn(zoneId);
2589+
when(sourceOffering.getExternalId()).thenReturn("ext-src-2");
2590+
when(sourceOffering.getProvider()).thenReturn("testbackupprovider");
2591+
when(sourceOffering.getDescription()).thenReturn("src desc 2");
2592+
when(sourceOffering.isUserDrivenBackupAllowed()).thenReturn(false);
2593+
when(sourceOffering.getName()).thenReturn("Source Offering 2");
2594+
2595+
when(backupOfferingDao.findById(sourceOfferingId)).thenReturn(sourceOffering);
2596+
when(backupOfferingDao.findByName(cmd.getName(), zoneId)).thenReturn(null);
2597+
2598+
BackupOfferingVO savedOffering = Mockito.mock(BackupOfferingVO.class);
2599+
when(savedOffering.getId()).thenReturn(savedOfferingId);
2600+
when(backupOfferingDao.persist(any(BackupOfferingVO.class))).thenReturn(savedOffering);
2601+
2602+
// domain handling
2603+
DomainVO domain21 = Mockito.mock(DomainVO.class);
2604+
DomainVO domain22 = Mockito.mock(DomainVO.class);
2605+
when(domainDao.findById(21L)).thenReturn(domain21);
2606+
when(domainDao.findById(22L)).thenReturn(domain22);
2607+
when(domainHelper.filterChildSubDomains(sourceDomainIds)).thenReturn(new ArrayList<>(sourceDomainIds));
2608+
2609+
overrideBackupFrameworkConfigValue();
2610+
2611+
BackupOffering result = backupManager.cloneBackupOffering(cmd);
2612+
assertEquals(savedOffering, result);
2613+
2614+
ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
2615+
verify(backupOfferingDetailsDao, times(1)).saveDetails(captor.capture());
2616+
List<BackupOfferingDetailsVO> savedDetails = captor.getValue();
2617+
assertEquals(2, savedDetails.size());
2618+
List<String> values = new ArrayList<>();
2619+
for (BackupOfferingDetailsVO d : savedDetails) {
2620+
values.add(d.getValue());
2621+
}
2622+
assertTrue(values.contains(String.valueOf(21L)));
2623+
assertTrue(values.contains(String.valueOf(22L)));
2624+
}
25212625
}

ui/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3357,6 +3357,7 @@
33573357
"message.disable.webhook.ssl.verification": "Disabling SSL verification is not recommended",
33583358
"message.discovering.feature": "Discovering features, please wait...",
33593359
"message.disk.offering.created": "Disk offering created:",
3360+
"message.success.clone.backup.offering": "Successfully cloned backup offering",
33603361
"message.success.clone.disk.offering": "Successfully cloned disk offering:",
33613362
"message.success.clone.network.offering": "Successfully cloned network offering:",
33623363
"message.disk.usage.info.data.points": "Each data point represents the difference in read/write data since the last data point.",

0 commit comments

Comments
 (0)