api: snapshot, snapshotpolicy tag support (#3228)

Problem: Currently tags cannot be applied to snapshot when it is being created but through separate “create tags” API calls. For snapshot policies tags cannot be set either at creation or through “create tags” API.

Root Cause: The “create snapshots” API does not support adding tags during creation and it can only be done through “create tags” API. Snapshot policy as a resource does not support tags and no tags can be set for them through any API.

Solution: Tag support for snapshot policy has been added. Snapshot policy with tags when executed will produce snapshots containing the same tags from snapshot policy.

Following APIs have been updated:

Both “create snapshotpolicy” and “create snapshot” now accepts “tags” as a new parameter. The expected format for “tags” parameter is similar to parameter “tags” in “create tags“ API.
Deletion support for tags associated with snapshots policy has been added to “delete snapshotpolicies” API.
Tags set for snapshot policies are added to the Response of “list snapshotpolicies“ API.
UI support for setting tags to snapshots and snapshot policy is provided through the corresponding menus with a new section in each form to set tags.

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2019-06-27 09:21:09 +05:30 committed by Rohit Yadav
parent 14bff7bd03
commit 58474530f6
20 changed files with 423 additions and 135 deletions

View File

@ -58,7 +58,7 @@ public interface ResourceTag extends ControlledEntity, Identity, InternalIdentit
AutoScaleVmGroup(false, true), AutoScaleVmGroup(false, true),
LBStickinessPolicy(false, true), LBStickinessPolicy(false, true),
LBHealthCheckPolicy(false, true), LBHealthCheckPolicy(false, true),
SnapshotPolicy(false, true), SnapshotPolicy(true, true),
GuestOs(false, true), GuestOs(false, true),
NetworkOffering(false, true), NetworkOffering(false, true),
VpcOffering(true, false); VpcOffering(true, false);

View File

@ -19,6 +19,7 @@
package com.cloud.storage; package com.cloud.storage;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.util.Map;
import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd; import org.apache.cloudstack.api.command.user.volume.AttachVolumeCmd;
import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd; import org.apache.cloudstack.api.command.user.volume.CreateVolumeCmd;
@ -93,7 +94,7 @@ public interface VolumeApiService {
Volume detachVolumeFromVM(DetachVolumeCmd cmd); Volume detachVolumeFromVM(DetachVolumeCmd cmd);
Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup) Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map<String, String> tags)
throws ResourceAllocationException; throws ResourceAllocationException;
Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType) throws ResourceAllocationException; Snapshot allocSnapshot(Long volumeId, Long policyId, String snapshotName, Snapshot.LocationType locationType) throws ResourceAllocationException;

View File

@ -16,6 +16,10 @@
// under the License. // under the License.
package org.apache.cloudstack.api.command.user.snapshot; package org.apache.cloudstack.api.command.user.snapshot;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandJobType; import org.apache.cloudstack.api.ApiCommandJobType;
import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants;
@ -28,6 +32,7 @@ import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.SnapshotPolicyResponse; import org.apache.cloudstack.api.response.SnapshotPolicyResponse;
import org.apache.cloudstack.api.response.SnapshotResponse; import org.apache.cloudstack.api.response.SnapshotResponse;
import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import com.cloud.event.EventTypes; import com.cloud.event.EventTypes;
@ -83,6 +88,9 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd {
@Parameter(name = ApiConstants.ASYNC_BACKUP, type = CommandType.BOOLEAN, required = false, description = "asynchronous backup if true") @Parameter(name = ApiConstants.ASYNC_BACKUP, type = CommandType.BOOLEAN, required = false, description = "asynchronous backup if true")
private Boolean asyncBackup; private Boolean asyncBackup;
@Parameter(name = ApiConstants.TAGS, type = CommandType.MAP, description = "Map of tags (key/value pairs)")
private Map tags;
private String syncObjectType = BaseAsyncCmd.snapshotHostSyncObject; private String syncObjectType = BaseAsyncCmd.snapshotHostSyncObject;
// /////////////////////////////////////////////////// // ///////////////////////////////////////////////////
@ -121,6 +129,18 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd {
} }
} }
public Map<String, String> getTags() {
Map<String, String> tagsMap = new HashMap<>();
if (MapUtils.isNotEmpty(tags)) {
for (Map<String, String> services : (Collection<Map<String, String>>)tags.values()) {
String key = services.get("key");
String value = services.get("value");
tagsMap.put(key, value);
}
}
return tagsMap;
}
private Long getHostId() { private Long getHostId() {
Volume volume = _entityMgr.findById(Volume.class, getVolumeId()); Volume volume = _entityMgr.findById(Volume.class, getVolumeId());
if (volume == null) { if (volume == null) {
@ -196,7 +216,7 @@ public class CreateSnapshotCmd extends BaseAsyncCreateCmd {
Snapshot snapshot; Snapshot snapshot;
try { try {
snapshot = snapshot =
_volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup()); _volumeService.takeSnapshot(getVolumeId(), getPolicyId(), getEntityId(), _accountService.getAccount(getEntityOwnerId()), getQuiescevm(), getLocationType(), getAsyncBackup(), getTags());
if (snapshot != null) { if (snapshot != null) {
SnapshotResponse response = _responseGenerator.createSnapshotResponse(snapshot); SnapshotResponse response = _responseGenerator.createSnapshotResponse(snapshot);

View File

@ -16,9 +16,11 @@
// under the License. // under the License.
package org.apache.cloudstack.api.command.user.snapshot; package org.apache.cloudstack.api.command.user.snapshot;
import org.apache.cloudstack.acl.RoleType; import java.util.Collection;
import org.apache.log4j.Logger; import java.util.HashMap;
import java.util.Map;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode; import org.apache.cloudstack.api.ApiErrorCode;
@ -27,6 +29,8 @@ import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.SnapshotPolicyResponse; import org.apache.cloudstack.api.response.SnapshotPolicyResponse;
import org.apache.cloudstack.api.response.VolumeResponse; import org.apache.cloudstack.api.response.VolumeResponse;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger;
import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.PermissionDeniedException;
@ -68,6 +72,9 @@ public class CreateSnapshotPolicyCmd extends BaseCmd {
@Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the policy to the end user or not", since = "4.4", authorized = {RoleType.Admin}) @Parameter(name = ApiConstants.FOR_DISPLAY, type = CommandType.BOOLEAN, description = "an optional field, whether to the display the policy to the end user or not", since = "4.4", authorized = {RoleType.Admin})
private Boolean display; private Boolean display;
@Parameter(name = ApiConstants.TAGS, type = CommandType.MAP, description = "Map of tags (key/value pairs)")
private Map tags;
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
/////////////////// Accessors /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -133,6 +140,18 @@ public class CreateSnapshotPolicyCmd extends BaseCmd {
return volume.getAccountId(); return volume.getAccountId();
} }
public Map<String, String> getTags() {
Map<String, String> tagsMap = new HashMap<>();
if (MapUtils.isNotEmpty(tags)) {
for (Map<String, String> services : (Collection<Map<String, String>>)tags.values()) {
String key = services.get("key");
String value = services.get("value");
tagsMap.put(key, value);
}
}
return tagsMap;
}
@Override @Override
public void execute() { public void execute() {
SnapshotPolicy result = _snapshotService.createPolicy(this, _accountService.getAccount(getEntityOwnerId())); SnapshotPolicy result = _snapshotService.createPolicy(this, _accountService.getAccount(getEntityOwnerId()));

View File

@ -16,18 +16,20 @@
// under the License. // under the License.
package org.apache.cloudstack.api.response; package org.apache.cloudstack.api.response;
import com.google.gson.annotations.SerializedName; import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse; import org.apache.cloudstack.api.BaseResponseWithTagInformation;
import org.apache.cloudstack.api.EntityReference; import org.apache.cloudstack.api.EntityReference;
import com.cloud.serializer.Param; import com.cloud.serializer.Param;
import com.cloud.storage.snapshot.SnapshotPolicy; import com.cloud.storage.snapshot.SnapshotPolicy;
import com.google.gson.annotations.SerializedName;
@EntityReference(value = SnapshotPolicy.class) @EntityReference(value = SnapshotPolicy.class)
public class SnapshotPolicyResponse extends BaseResponse { public class SnapshotPolicyResponse extends BaseResponseWithTagInformation {
@SerializedName("id") @SerializedName("id")
@Param(description = "the ID of the snapshot policy") @Param(description = "the ID of the snapshot policy")
private String id; private String id;
@ -56,6 +58,10 @@ public class SnapshotPolicyResponse extends BaseResponse {
@Param(description = "is this policy for display to the regular user", since = "4.4", authorized = {RoleType.Admin}) @Param(description = "is this policy for display to the regular user", since = "4.4", authorized = {RoleType.Admin})
private Boolean forDisplay; private Boolean forDisplay;
public SnapshotPolicyResponse() {
tags = new LinkedHashSet<ResourceTagResponse>();
}
public String getId() { public String getId() {
return id; return id;
} }
@ -111,4 +117,8 @@ public class SnapshotPolicyResponse extends BaseResponse {
public void setForDisplay(Boolean forDisplay) { public void setForDisplay(Boolean forDisplay) {
this.forDisplay = forDisplay; this.forDisplay = forDisplay;
} }
public void setTags(Set<ResourceTagResponse> tags) {
this.tags = tags;
}
} }

View File

@ -16,18 +16,20 @@
// under the License. // under the License.
package org.apache.cloudstack.api.response; package org.apache.cloudstack.api.response;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponseWithTagInformation;
import org.apache.cloudstack.api.EntityReference;
import com.cloud.serializer.Param; import com.cloud.serializer.Param;
import com.cloud.storage.Snapshot; import com.cloud.storage.Snapshot;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
import org.apache.cloudstack.api.EntityReference;
import java.util.Date;
import java.util.List;
@EntityReference(value = Snapshot.class) @EntityReference(value = Snapshot.class)
public class SnapshotResponse extends BaseResponse implements ControlledEntityResponse { public class SnapshotResponse extends BaseResponseWithTagInformation implements ControlledEntityResponse {
@SerializedName(ApiConstants.ID) @SerializedName(ApiConstants.ID)
@Param(description = "ID of the snapshot") @Param(description = "ID of the snapshot")
private String id; private String id;
@ -96,10 +98,6 @@ public class SnapshotResponse extends BaseResponse implements ControlledEntityRe
@Param(description = "id of the availability zone") @Param(description = "id of the availability zone")
private String zoneId; private String zoneId;
@SerializedName(ApiConstants.TAGS)
@Param(description = "the list of resource tags associated with snapshot", responseObject = ResourceTagResponse.class)
private List<ResourceTagResponse> tags;
@SerializedName(ApiConstants.REVERTABLE) @SerializedName(ApiConstants.REVERTABLE)
@Param(description = "indicates whether the underlying storage supports reverting the volume to this snapshot") @Param(description = "indicates whether the underlying storage supports reverting the volume to this snapshot")
private boolean revertable; private boolean revertable;
@ -116,6 +114,10 @@ public class SnapshotResponse extends BaseResponse implements ControlledEntityRe
@Param(description = "virtual size of backedup snapshot on image store") @Param(description = "virtual size of backedup snapshot on image store")
private long virtualSize; private long virtualSize;
public SnapshotResponse() {
tags = new LinkedHashSet<ResourceTagResponse>();
}
@Override @Override
public String getObjectId() { public String getObjectId() {
return this.getId(); return this.getId();
@ -206,7 +208,7 @@ public class SnapshotResponse extends BaseResponse implements ControlledEntityRe
this.zoneId = zoneId; this.zoneId = zoneId;
} }
public void setTags(List<ResourceTagResponse> tags) { public void setTags(Set<ResourceTagResponse> tags) {
this.tags = tags; this.tags = tags;
} }

View File

@ -19,9 +19,13 @@ package org.apache.cloudstack.api.command.test;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.isNull; import static org.mockito.Matchers.isNull;
import java.util.HashMap;
import java.util.Map;
import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotCmd; import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotCmd;
@ -32,6 +36,7 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.storage.Snapshot; import com.cloud.storage.Snapshot;
import com.cloud.storage.VolumeApiService; import com.cloud.storage.VolumeApiService;
@ -87,9 +92,8 @@ public class CreateSnapshotCmdTest extends TestCase {
VolumeApiService volumeApiService = Mockito.mock(VolumeApiService.class); VolumeApiService volumeApiService = Mockito.mock(VolumeApiService.class);
Snapshot snapshot = Mockito.mock(Snapshot.class); Snapshot snapshot = Mockito.mock(Snapshot.class);
try { try {
Mockito.when(volumeApiService.takeSnapshot(anyLong(), anyLong(), anyLong(), Mockito.when(volumeApiService.takeSnapshot(anyLong(), anyLong(), anyLong(),
any(Account.class), anyBoolean(), isNull(Snapshot.LocationType.class), anyBoolean())).thenReturn(snapshot); any(Account.class), anyBoolean(), isNull(Snapshot.LocationType.class), anyBoolean(), anyObject())).thenReturn(snapshot);
} catch (Exception e) { } catch (Exception e) {
Assert.fail("Received exception when success expected " + e.getMessage()); Assert.fail("Received exception when success expected " + e.getMessage());
@ -122,7 +126,7 @@ public class CreateSnapshotCmdTest extends TestCase {
try { try {
Mockito.when(volumeApiService.takeSnapshot(anyLong(), anyLong(), anyLong(), Mockito.when(volumeApiService.takeSnapshot(anyLong(), anyLong(), anyLong(),
any(Account.class), anyBoolean(), isNull(Snapshot.LocationType.class), anyBoolean())).thenReturn(null); any(Account.class), anyBoolean(), isNull(Snapshot.LocationType.class), anyBoolean(), anyObject())).thenReturn(null);
} catch (Exception e) { } catch (Exception e) {
Assert.fail("Received exception when success expected " + e.getMessage()); Assert.fail("Received exception when success expected " + e.getMessage());
} }
@ -136,4 +140,24 @@ public class CreateSnapshotCmdTest extends TestCase {
Assert.assertEquals("Failed to create snapshot due to an internal error creating snapshot for volume 123", exception.getDescription()); Assert.assertEquals("Failed to create snapshot due to an internal error creating snapshot for volume 123", exception.getDescription());
} }
} }
@Test
public void testParsingTags() {
final CreateSnapshotCmd createSnapshotCmd = new CreateSnapshotCmd();
final Map<String, String> tag1 = new HashMap<>();
tag1.put("key", "key1");
tag1.put("value", "value1");
final Map<String, String> tag2 = new HashMap<>();
tag2.put("key", "key2");
tag2.put("value", "value2");
final Map<String, String> expectedTags = new HashMap<>();
expectedTags.put("key1", "value1");
expectedTags.put("key2", "value2");
final Map<String, Map<String, String>> tagsParams = new HashMap<>();
tagsParams.put("0", tag1);
tagsParams.put("1", tag2);
ReflectionTestUtils.setField(createSnapshotCmd, "tags", tagsParams);
Assert.assertEquals(createSnapshotCmd.getTags(), expectedTags);
}
} }

View File

@ -0,0 +1,46 @@
// 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.user.snapshot;
import java.util.HashMap;
import java.util.Map;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.test.util.ReflectionTestUtils;
public class CreateSnapshotPolicyCmdTest {
@Test
public void testParsingTags() {
final CreateSnapshotPolicyCmd createSnapshotPolicyCmd = new CreateSnapshotPolicyCmd();
final Map<String, String> tag1 = new HashMap<>();
tag1.put("key", "key1");
tag1.put("value", "value1");
final Map<String, String> tag2 = new HashMap<>();
tag2.put("key", "key2");
tag2.put("value", "value2");
final Map<String, String> expectedTags = new HashMap<>();
expectedTags.put("key1", "value1");
expectedTags.put("key2", "value2");
final Map<String, Map<String, String>> tagsParams = new HashMap<>();
tagsParams.put("0", tag1);
tagsParams.put("1", tag2);
ReflectionTestUtils.setField(createSnapshotPolicyCmd, "tags", tagsParams);
Assert.assertEquals(createSnapshotPolicyCmd.getTags(), expectedTags);
}
}

View File

@ -552,7 +552,7 @@ public class ApiResponseHelper implements ResponseGenerator {
ResourceTagResponse tagResponse = createResourceTagResponse(tag, true); ResourceTagResponse tagResponse = createResourceTagResponse(tag, true);
CollectionUtils.addIgnoreNull(tagResponses, tagResponse); CollectionUtils.addIgnoreNull(tagResponses, tagResponse);
} }
snapshotResponse.setTags(tagResponses); snapshotResponse.setTags(new HashSet<>(tagResponses));
snapshotResponse.setObjectName("snapshot"); snapshotResponse.setObjectName("snapshot");
return snapshotResponse; return snapshotResponse;
@ -654,6 +654,14 @@ public class ApiResponseHelper implements ResponseGenerator {
policyResponse.setForDisplay(policy.isDisplay()); policyResponse.setForDisplay(policy.isDisplay());
policyResponse.setObjectName("snapshotpolicy"); policyResponse.setObjectName("snapshotpolicy");
List<? extends ResourceTag> tags = _resourceTagDao.listBy(policy.getId(), ResourceObjectType.SnapshotPolicy);
List<ResourceTagResponse> tagResponses = new ArrayList<ResourceTagResponse>();
for (ResourceTag tag : tags) {
ResourceTagResponse tagResponse = createResourceTagResponse(tag, false);
CollectionUtils.addIgnoreNull(tagResponses, tagResponse);
}
policyResponse.setTags(new HashSet<>(tagResponses));
return policyResponse; return policyResponse;
} }

View File

@ -80,6 +80,7 @@ import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.cloudstack.utils.imagestore.ImageStoreUtil; import org.apache.cloudstack.utils.imagestore.ImageStoreUtil;
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.joda.time.DateTimeZone; import org.joda.time.DateTimeZone;
@ -114,6 +115,8 @@ import com.cloud.hypervisor.HypervisorCapabilitiesVO;
import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao;
import com.cloud.org.Grouping; import com.cloud.org.Grouping;
import com.cloud.serializer.GsonHelper; import com.cloud.serializer.GsonHelper;
import com.cloud.server.ResourceTag;
import com.cloud.server.TaggedResourceService;
import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.service.dao.ServiceOfferingDetailsDao;
import com.cloud.storage.Storage.ImageFormat; import com.cloud.storage.Storage.ImageFormat;
import com.cloud.storage.dao.DiskOfferingDao; import com.cloud.storage.dao.DiskOfferingDao;
@ -256,6 +259,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
private StoragePoolTagsDao storagePoolTagsDao; private StoragePoolTagsDao storagePoolTagsDao;
@Inject @Inject
private StorageUtil storageUtil; private StorageUtil storageUtil;
@Inject
public TaggedResourceService taggedResourceService;
protected Gson _gson; protected Gson _gson;
@ -2343,7 +2348,16 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
@Override @Override
@ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_CREATE, eventDescription = "taking snapshot", async = true) @ActionEvent(eventType = EventTypes.EVENT_SNAPSHOT_CREATE, eventDescription = "taking snapshot", async = true)
public Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup) public Snapshot takeSnapshot(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup, Map<String, String> tags)
throws ResourceAllocationException {
final Snapshot snapshot = takeSnapshotInternal(volumeId, policyId, snapshotId, account, quiescevm, locationType, asyncBackup);
if (snapshot != null && MapUtils.isNotEmpty(tags)) {
taggedResourceService.createTags(Collections.singletonList(snapshot.getUuid()), ResourceTag.ResourceObjectType.Snapshot, tags, null);
}
return snapshot;
}
private Snapshot takeSnapshotInternal(Long volumeId, Long policyId, Long snapshotId, Account account, boolean quiescevm, Snapshot.LocationType locationType, boolean asyncBackup)
throws ResourceAllocationException { throws ResourceAllocationException {
VolumeInfo volume = volFactory.getVolume(volumeId); VolumeInfo volume = volFactory.getVolume(volumeId);
if (volume == null) { if (volume == null) {

View File

@ -16,8 +16,53 @@
// under the License. // under the License.
package com.cloud.storage.snapshot; package com.cloud.storage.snapshot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotPolicyCmd;
import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotPoliciesCmd;
import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotPoliciesCmd;
import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd;
import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation;
import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import com.cloud.agent.api.Answer; import com.cloud.agent.api.Answer;
import com.cloud.utils.db.GlobalLock;
import com.cloud.agent.api.Command; import com.cloud.agent.api.Command;
import com.cloud.agent.api.DeleteSnapshotsDirCommand; import com.cloud.agent.api.DeleteSnapshotsDirCommand;
import com.cloud.alert.AlertManager; import com.cloud.alert.AlertManager;
@ -42,6 +87,7 @@ import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.projects.Project.ListProjectResourcesCriteria; import com.cloud.projects.Project.ListProjectResourcesCriteria;
import com.cloud.resource.ResourceManager; import com.cloud.resource.ResourceManager;
import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.server.ResourceTag.ResourceObjectType;
import com.cloud.server.TaggedResourceService;
import com.cloud.storage.CreateSnapshotPayload; import com.cloud.storage.CreateSnapshotPayload;
import com.cloud.storage.DataStoreRole; import com.cloud.storage.DataStoreRole;
import com.cloud.storage.ScopeType; import com.cloud.storage.ScopeType;
@ -80,6 +126,7 @@ import com.cloud.utils.Ternary;
import com.cloud.utils.concurrency.NamedThreadFactory; import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.DB; import com.cloud.utils.db.DB;
import com.cloud.utils.db.Filter; import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.db.JoinBuilder; import com.cloud.utils.db.JoinBuilder;
import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria; import com.cloud.utils.db.SearchCriteria;
@ -92,48 +139,6 @@ import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.snapshot.VMSnapshot; import com.cloud.vm.snapshot.VMSnapshot;
import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.VMSnapshotVO;
import com.cloud.vm.snapshot.dao.VMSnapshotDao; import com.cloud.vm.snapshot.dao.VMSnapshotDao;
import org.apache.cloudstack.api.command.user.snapshot.CreateSnapshotPolicyCmd;
import org.apache.cloudstack.api.command.user.snapshot.DeleteSnapshotPoliciesCmd;
import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotPoliciesCmd;
import org.apache.cloudstack.api.command.user.snapshot.ListSnapshotsCmd;
import org.apache.cloudstack.api.command.user.snapshot.UpdateSnapshotPolicyCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation;
import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component @Component
public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implements SnapshotManager, SnapshotApiService, Configurable { public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implements SnapshotManager, SnapshotApiService, Configurable {
@ -192,6 +197,8 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
ResourceManager _resourceMgr; ResourceManager _resourceMgr;
@Inject @Inject
StorageStrategyFactory _storageStrategyFactory; StorageStrategyFactory _storageStrategyFactory;
@Inject
public TaggedResourceService taggedResourceService;
private int _totalRetries; private int _totalRetries;
private int _pauseInterval; private int _pauseInterval;
@ -902,6 +909,11 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
policy.setDisplay(display); policy.setDisplay(display);
_snapshotPolicyDao.update(policy.getId(), policy); _snapshotPolicyDao.update(policy.getId(), policy);
_snapSchedMgr.scheduleOrCancelNextSnapshotJobOnDisplayChange(policy, previousDisplay); _snapSchedMgr.scheduleOrCancelNextSnapshotJobOnDisplayChange(policy, previousDisplay);
taggedResourceService.deleteTags(Collections.singletonList(policy.getUuid()), ResourceObjectType.SnapshotPolicy, null);
}
final Map<String, String> tags = cmd.getTags();
if (MapUtils.isNotEmpty(tags)) {
taggedResourceService.createTags(Collections.singletonList(policy.getUuid()), ResourceObjectType.SnapshotPolicy, tags, null);
} }
} finally { } finally {
createSnapshotPolicyLock.unlock(); createSnapshotPolicyLock.unlock();
@ -916,9 +928,10 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
} }
} }
protected boolean deletePolicy(long userId, Long policyId) { protected boolean deletePolicy(Long policyId) {
SnapshotPolicyVO snapshotPolicy = _snapshotPolicyDao.findById(policyId); SnapshotPolicyVO snapshotPolicy = _snapshotPolicyDao.findById(policyId);
_snapSchedMgr.removeSchedule(snapshotPolicy.getVolumeId(), snapshotPolicy.getId()); _snapSchedMgr.removeSchedule(snapshotPolicy.getVolumeId(), snapshotPolicy.getId());
taggedResourceService.deleteTags(Collections.singletonList(snapshotPolicy.getUuid()), ResourceObjectType.SnapshotPolicy, null);
return _snapshotPolicyDao.remove(policyId); return _snapshotPolicyDao.remove(policyId);
} }
@ -963,8 +976,7 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
public void deletePoliciesForVolume(Long volumeId) { public void deletePoliciesForVolume(Long volumeId) {
List<SnapshotPolicyVO> policyInstances = listPoliciesforVolume(volumeId); List<SnapshotPolicyVO> policyInstances = listPoliciesforVolume(volumeId);
for (SnapshotPolicyVO policyInstance : policyInstances) { for (SnapshotPolicyVO policyInstance : policyInstances) {
Long policyId = policyInstance.getId(); deletePolicy(policyInstance.getId());
deletePolicy(1L, policyId);
} }
// We also want to delete the manual snapshots scheduled for this volume // We also want to delete the manual snapshots scheduled for this volume
// We can only delete the schedules in the future, not the ones which are already executing. // We can only delete the schedules in the future, not the ones which are already executing.
@ -1321,7 +1333,6 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
public boolean deleteSnapshotPolicies(DeleteSnapshotPoliciesCmd cmd) { public boolean deleteSnapshotPolicies(DeleteSnapshotPoliciesCmd cmd) {
Long policyId = cmd.getId(); Long policyId = cmd.getId();
List<Long> policyIds = cmd.getIds(); List<Long> policyIds = cmd.getIds();
Long userId = getSnapshotUserId();
if ((policyId == null) && (policyIds == null)) { if ((policyId == null) && (policyIds == null)) {
throw new InvalidParameterValueException("No policy id (or list of ids) specified."); throw new InvalidParameterValueException("No policy id (or list of ids) specified.");
@ -1335,6 +1346,10 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
throw new InvalidParameterValueException("There are no policy ids"); throw new InvalidParameterValueException("There are no policy ids");
} }
if (policyIds.contains(Snapshot.MANUAL_POLICY_ID)) {
throw new InvalidParameterValueException("Invalid Policy id given: " + Snapshot.MANUAL_POLICY_ID);
}
for (Long policy : policyIds) { for (Long policy : policyIds) {
SnapshotPolicyVO snapshotPolicyVO = _snapshotPolicyDao.findById(policy); SnapshotPolicyVO snapshotPolicyVO = _snapshotPolicyDao.findById(policy);
if (snapshotPolicyVO == null) { if (snapshotPolicyVO == null) {
@ -1348,21 +1363,14 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
_accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, volume); _accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, volume);
} }
boolean success = true;
if (policyIds.contains(Snapshot.MANUAL_POLICY_ID)) {
throw new InvalidParameterValueException("Invalid Policy id given: " + Snapshot.MANUAL_POLICY_ID);
}
for (Long pId : policyIds) { for (Long pId : policyIds) {
if (!deletePolicy(userId, pId)) { if (!deletePolicy(pId)) {
success = false;
s_logger.warn("Failed to delete snapshot policy with Id: " + policyId); s_logger.warn("Failed to delete snapshot policy with Id: " + policyId);
return success; return false;
} }
} }
return success; return true;
} }
@Override @Override

View File

@ -43,6 +43,8 @@ import com.cloud.api.ApiDispatcher;
import com.cloud.api.ApiGsonHelper; import com.cloud.api.ApiGsonHelper;
import com.cloud.event.ActionEventUtils; import com.cloud.event.ActionEventUtils;
import com.cloud.event.EventTypes; import com.cloud.event.EventTypes;
import com.cloud.server.ResourceTag;
import com.cloud.server.TaggedResourceService;
import com.cloud.storage.Snapshot; import com.cloud.storage.Snapshot;
import com.cloud.storage.SnapshotPolicyVO; import com.cloud.storage.SnapshotPolicyVO;
import com.cloud.storage.SnapshotScheduleVO; import com.cloud.storage.SnapshotScheduleVO;
@ -97,6 +99,8 @@ public class SnapshotSchedulerImpl extends ManagerBase implements SnapshotSchedu
protected VMSnapshotDao _vmSnapshotDao; protected VMSnapshotDao _vmSnapshotDao;
@Inject @Inject
protected VMSnapshotManager _vmSnaphostManager; protected VMSnapshotManager _vmSnaphostManager;
@Inject
public TaggedResourceService taggedResourceService;
protected AsyncJobDispatcher _asyncDispatcher; protected AsyncJobDispatcher _asyncDispatcher;
@ -305,6 +309,15 @@ public class SnapshotSchedulerImpl extends ManagerBase implements SnapshotSchedu
params.put("ctxUserId", "1"); params.put("ctxUserId", "1");
params.put("ctxAccountId", "" + volume.getAccountId()); params.put("ctxAccountId", "" + volume.getAccountId());
params.put("ctxStartEventId", String.valueOf(eventId)); params.put("ctxStartEventId", String.valueOf(eventId));
List<? extends ResourceTag> resourceTags = taggedResourceService.listByResourceTypeAndId(ResourceTag.ResourceObjectType.SnapshotPolicy, policyId);
if (resourceTags != null && !resourceTags.isEmpty()) {
int tagNumber = 0;
for (ResourceTag resourceTag : resourceTags) {
params.put("tags[" + tagNumber + "].key", resourceTag.getKey());
params.put("tags[" + tagNumber + "].value", resourceTag.getValue());
tagNumber++;
}
}
final CreateSnapshotCmd cmd = new CreateSnapshotCmd(); final CreateSnapshotCmd cmd = new CreateSnapshotCmd();
ComponentContext.inject(cmd); ComponentContext.inject(cmd);

View File

@ -16,13 +16,31 @@
// under the License. // under the License.
package com.cloud.tags; package com.cloud.tags;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import javax.persistence.EntityExistsException;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import com.cloud.dc.DataCenterVO; import com.cloud.dc.DataCenterVO;
import com.cloud.domain.PartOf; import com.cloud.domain.PartOf;
import com.cloud.event.ActionEvent; import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes; import com.cloud.event.EventTypes;
import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.PermissionDeniedException;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.network.LBHealthCheckPolicyVO; import com.cloud.network.LBHealthCheckPolicyVO;
import com.cloud.network.as.AutoScaleVmGroupVO; import com.cloud.network.as.AutoScaleVmGroupVO;
import com.cloud.network.as.AutoScaleVmProfileVO; import com.cloud.network.as.AutoScaleVmProfileVO;
@ -43,6 +61,7 @@ import com.cloud.network.vpc.NetworkACLVO;
import com.cloud.network.vpc.StaticRouteVO; import com.cloud.network.vpc.StaticRouteVO;
import com.cloud.network.vpc.VpcOfferingVO; import com.cloud.network.vpc.VpcOfferingVO;
import com.cloud.network.vpc.VpcVO; import com.cloud.network.vpc.VpcVO;
import com.cloud.offerings.NetworkOfferingVO;
import com.cloud.projects.ProjectVO; import com.cloud.projects.ProjectVO;
import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag;
import com.cloud.server.ResourceTag.ResourceObjectType; import com.cloud.server.ResourceTag.ResourceObjectType;
@ -74,24 +93,6 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.NicVO; import com.cloud.vm.NicVO;
import com.cloud.vm.UserVmVO; import com.cloud.vm.UserVmVO;
import com.cloud.vm.snapshot.VMSnapshotVO; import com.cloud.vm.snapshot.VMSnapshotVO;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.commons.collections.MapUtils;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import javax.persistence.EntityExistsException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class TaggedResourceManagerImpl extends ManagerBase implements TaggedResourceService { public class TaggedResourceManagerImpl extends ManagerBase implements TaggedResourceService {
public static final Logger s_logger = Logger.getLogger(TaggedResourceManagerImpl.class); public static final Logger s_logger = Logger.getLogger(TaggedResourceManagerImpl.class);
@ -220,6 +221,10 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso
accountId = ((ProjectVO)entity).getProjectAccountId(); accountId = ((ProjectVO)entity).getProjectAccountId();
} }
if (resourceType == ResourceObjectType.SnapshotPolicy) {
accountId = _entityMgr.findById(VolumeVO.class, ((SnapshotPolicyVO)entity).getVolumeId()).getAccountId();
}
if (entity instanceof OwnedBy) { if (entity instanceof OwnedBy) {
accountId = ((OwnedBy)entity).getAccountId(); accountId = ((OwnedBy)entity).getAccountId();
} }
@ -389,7 +394,7 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso
} }
if (tagsToDelete.isEmpty()) { if (tagsToDelete.isEmpty()) {
throw new InvalidParameterValueException("Unable to find any tags which conform to specified delete parameters."); return false;
} }
//Remove the tags //Remove the tags

View File

@ -18,6 +18,7 @@ package com.cloud.storage;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyObject;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doNothing;
@ -65,6 +66,7 @@ import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.Spy; import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.configuration.Resource; import com.cloud.configuration.Resource;
import com.cloud.configuration.Resource.ResourceType; import com.cloud.configuration.Resource.ResourceType;
@ -76,6 +78,7 @@ import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.org.Grouping; import com.cloud.org.Grouping;
import com.cloud.serializer.GsonHelper; import com.cloud.serializer.GsonHelper;
import com.cloud.server.TaggedResourceService;
import com.cloud.storage.Volume.Type; import com.cloud.storage.Volume.Type;
import com.cloud.storage.dao.StoragePoolTagsDao; import com.cloud.storage.dao.StoragePoolTagsDao;
import com.cloud.storage.dao.VolumeDao; import com.cloud.storage.dao.VolumeDao;
@ -409,7 +412,7 @@ public class VolumeApiServiceImplTest {
when(volumeDataFactoryMock.getVolume(anyLong())).thenReturn(volumeInfoMock); when(volumeDataFactoryMock.getVolume(anyLong())).thenReturn(volumeInfoMock);
when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated); when(volumeInfoMock.getState()).thenReturn(Volume.State.Allocated);
when(volumeInfoMock.getPoolId()).thenReturn(1L); when(volumeInfoMock.getPoolId()).thenReturn(1L);
volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false); volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null);
} }
@Test @Test
@ -419,7 +422,10 @@ public class VolumeApiServiceImplTest {
when(volumeInfoMock.getInstanceId()).thenReturn(null); when(volumeInfoMock.getInstanceId()).thenReturn(null);
when(volumeInfoMock.getPoolId()).thenReturn(1L); when(volumeInfoMock.getPoolId()).thenReturn(1L);
when(volumeServiceMock.takeSnapshot(Mockito.any(VolumeInfo.class))).thenReturn(snapshotInfoMock); when(volumeServiceMock.takeSnapshot(Mockito.any(VolumeInfo.class))).thenReturn(snapshotInfoMock);
volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false); final TaggedResourceService taggedResourceService = Mockito.mock(TaggedResourceService.class);
Mockito.when(taggedResourceService.createTags(anyObject(), anyObject(), anyObject(), anyObject())).thenReturn(null);
ReflectionTestUtils.setField(volumeApiServiceImpl, "taggedResourceService", taggedResourceService);
volumeApiServiceImpl.takeSnapshot(5L, Snapshot.MANUAL_POLICY_ID, 3L, null, false, null, false, null);
} }
@Test @Test

View File

@ -4027,7 +4027,7 @@ textarea {
font-size: 14px; font-size: 14px;
} }
.ui-dialog div.form-container div.value label { #label_delete_volumes label {
display: block; display: block;
width: 119px; width: 119px;
margin-top: 2px; margin-top: 2px;
@ -8447,6 +8447,10 @@ div#details-tab-aclRules td.cidrlist span {
display: inline-block; display: inline-block;
} }
.recurring-snapshots .schedule .forms .formContainer {
min-height: 250px;
}
.recurring-snapshots .schedule .add-snapshot-actions { .recurring-snapshots .schedule .add-snapshot-actions {
float: left; float: left;
clear: both; clear: both;
@ -8510,6 +8514,11 @@ div#details-tab-aclRules td.cidrlist span {
left: 5px; left: 5px;
} }
.recurring-snapshots .schedule .forms .tagger form div.value label {
width: 25px;
top: 10px;
}
.recurring-snapshots .schedule .forms form label.error { .recurring-snapshots .schedule .forms form label.error {
float: left; float: left;
width: 100%; width: 100%;
@ -8522,6 +8531,10 @@ div#details-tab-aclRules td.cidrlist span {
margin: 8px 0 0; margin: 8px 0 0;
} }
.recurring-snapshots .schedule .forms .tagger form .field {
margin: 0;
}
.recurring-snapshots .schedule .forms form .name { .recurring-snapshots .schedule .forms form .name {
float: left; float: left;
width: 72px; width: 72px;
@ -8660,6 +8673,11 @@ div#details-tab-aclRules td.cidrlist span {
padding: 0; padding: 0;
} }
.recurring-snapshots .ui-tabs .tagger ul {
margin: 16px auto auto;
padding-bottom: 10px;
}
.recurring-snapshots .ui-tabs ul li a { .recurring-snapshots .ui-tabs ul li a {
width: 76px; width: 76px;
background: url("../images/sprites.png") no-repeat -521px -533px; background: url("../images/sprites.png") no-repeat -521px -533px;
@ -10692,6 +10710,11 @@ div#details-tab-aclRules td.cidrlist span {
display: none; display: none;
} }
.ui-dialog .tagger .tag-info.inside-form {
display: block;
text-align: left;
}
.ui-dialog.editTags .ui-button { .ui-dialog.editTags .ui-button {
float: right; float: right;
} }

View File

@ -1553,7 +1553,7 @@
</ul> </ul>
<!-- Hourly --> <!-- Hourly -->
<div id="recurring-snapshots-hourly"> <div id="recurring-snapshots-hourly" class="formContainer">
<form> <form>
<input type="hidden" name="snapshot-type" value="hourly" /> <input type="hidden" name="snapshot-type" value="hourly" />
@ -1583,11 +1583,14 @@
<label for="maxsnaps"><translate key="label.snapshots" /></label> <label for="maxsnaps"><translate key="label.snapshots" /></label>
</div> </div>
</div> </div>
<!-- Tags -->
<div class="field taggerContainer"></div>
</form> </form>
</div> </div>
<!-- Daily --> <!-- Daily -->
<div id="recurring-snapshots-daily"> <div id="recurring-snapshots-daily" class="formContainer">
<form> <form>
<input type="hidden" name="snapshot-type" value="daily" /> <input type="hidden" name="snapshot-type" value="daily" />
@ -1617,11 +1620,14 @@
<label for="maxsnaps"><translate key="label.snapshots" /></label> <label for="maxsnaps"><translate key="label.snapshots" /></label>
</div> </div>
</div> </div>
<!-- Tags -->
<div class="field taggerContainer"></div>
</form> </form>
</div> </div>
<!-- Weekly --> <!-- Weekly -->
<div id="recurring-snapshots-weekly"> <div id="recurring-snapshots-weekly" class="formContainer">
<form> <form>
<input type="hidden" name="snapshot-type" value="weekly" /> <input type="hidden" name="snapshot-type" value="weekly" />
@ -1659,11 +1665,14 @@
<label for="maxsnaps"><translate key="label.snapshots" /></label> <label for="maxsnaps"><translate key="label.snapshots" /></label>
</div> </div>
</div> </div>
<!-- Tags -->
<div class="field taggerContainer"></div>
</form> </form>
</div> </div>
<!-- Monthly --> <!-- Monthly -->
<div id="recurring-snapshots-monthly"> <div id="recurring-snapshots-monthly" class="formContainer">
<form> <form>
<input type="hidden" name="snapshot-type" value="monthly" /> <input type="hidden" name="snapshot-type" value="monthly" />
@ -1701,6 +1710,9 @@
<label for="maxsnaps"><translate key="label.snapshots" /></label> <label for="maxsnaps"><translate key="label.snapshots" /></label>
</div> </div>
</div> </div>
<!-- Tags -->
<div class="field taggerContainer"></div>
</form> </form>
</div> </div>
</div> </div>

View File

@ -921,6 +921,10 @@
asyncBackup: { asyncBackup: {
label: 'label.async.backup', label: 'label.async.backup',
isBoolean: true isBoolean: true
},
tags: {
label: 'label.tags',
tagger: true
} }
} }
}, },
@ -935,6 +939,15 @@
name: args.data.name name: args.data.name
}); });
} }
if (!$.isEmptyObject(args.data.tags)) {
$(args.data.tags).each(function(idx, tagData) {
var formattedTagData = {};
formattedTagData["tags[" + _s(idx) + "].key"] = _s(tagData.key);
formattedTagData["tags[" + _s(idx) + "].value"] = _s(tagData.value);
$.extend(data, formattedTagData);
});
}
$.ajax({ $.ajax({
url: createURL("createSnapshot"), url: createURL("createSnapshot"),
data: data, data: data,
@ -1000,7 +1013,9 @@
var snap = args.snapshot; var snap = args.snapshot;
var data = { var data = {
keep: snap.maxsnaps, volumeid: args.context.volumes[0].id,
intervaltype: snap['snapshot-type'],
maxsnaps: snap.maxsnaps,
timezone: snap.timezone timezone: snap.timezone
}; };
@ -1053,15 +1068,18 @@
break; break;
} }
if (!$.isEmptyObject(snap.tags)) {
$(snap.tags).each(function(idx, tagData) {
var formattedTagData = {};
formattedTagData["tags[" + _s(idx) + "].key"] = _s(tagData.key);
formattedTagData["tags[" + _s(idx) + "].value"] = _s(tagData.value);
$.extend(data, formattedTagData);
});
}
$.ajax({ $.ajax({
url: createURL('createSnapshotPolicy'), url: createURL('createSnapshotPolicy'),
data: { data: data,
volumeid: args.context.volumes[0].id,
intervaltype: snap['snapshot-type'],
maxsnaps: snap.maxsnaps,
schedule: data.schedule,
timezone: snap.timezone
},
dataType: 'json', dataType: 'json',
async: true, async: true,
success: function(successData) { success: function(successData) {

View File

@ -61,6 +61,10 @@
} }
}); });
$($snapshots.find('.taggerContainer')).each(function() {
$('<div>').taggerInForm().appendTo(this);
});
// Form validation // Form validation
$snapshots.find('form').validate(); $snapshots.find('form').validate();
@ -70,7 +74,7 @@
if (!$form.valid()) return false; if (!$form.valid()) return false;
var formData = cloudStack.serializeForm($form); var formData = $.extend(cloudStack.serializeForm($form), {'tags' : cloudStack.getTagsFromForm($form)});
actions.add({ actions.add({
context: context, context: context,

View File

@ -149,7 +149,6 @@
var isAsync = false; var isAsync = false;
var isNoDialog = args.noDialog ? args.noDialog : false; var isNoDialog = args.noDialog ? args.noDialog : false;
$(fields).each(function(idx, element) { $(fields).each(function(idx, element) {
var key = this; var key = this;
var field = args.form.fields[key]; var field = args.form.fields[key];
@ -190,7 +189,6 @@
closeOnEscape: false closeOnEscape: false
}); */ }); */
// Label field // Label field
var $name = $('<div>').addClass('name') var $name = $('<div>').addClass('name')
.appendTo($formItem) .appendTo($formItem)
.append( .append(
@ -619,9 +617,9 @@
} }
$input.addClass("disallowSpecialCharacters"); $input.addClass("disallowSpecialCharacters");
$input.datepicker({ $input.datepicker({
dateFormat: 'yy-mm-dd', dateFormat: 'yy-mm-dd',
maxDate: field.maxDate, maxDate: field.maxDate,
minDate: field.minDate minDate: field.minDate
}); });
} else if (field.range) { //2 text fields on the same line (e.g. port range: startPort - endPort) } else if (field.range) { //2 text fields on the same line (e.g. port range: startPort - endPort)
@ -702,6 +700,10 @@
this.oldUnit = newUnit; this.oldUnit = newUnit;
}) })
} else if (field.tagger) {
$name.hide();
$value.hide();
$input = $('<div>').taggerInForm().appendTo($formItem);
} else { //text field } else { //text field
$input = $('<input>').attr({ $input = $('<input>').attr({
name: key, name: key,
@ -717,10 +719,12 @@
$input.addClass("disallowSpecialCharacters"); $input.addClass("disallowSpecialCharacters");
} }
if (field.validation != null) if (!field.tagger) { // Tagger has it's own validation
$input.data('validation-rules', field.validation); if (field.validation != null)
else $input.data('validation-rules', field.validation);
$input.data('validation-rules', {}); else
$input.data('validation-rules', {});
}
var fieldLabel = field.label; var fieldLabel = field.label;
@ -774,7 +778,7 @@
var complete = function($formContainer) { var complete = function($formContainer) {
var $form = $formContainer.find('form'); var $form = $formContainer.find('form');
var data = cloudStack.serializeForm($form); var data = $.extend(cloudStack.serializeForm($form), {'tags' : cloudStack.getTagsFromForm($form)});
if (!$formContainer.find('form').valid()) { if (!$formContainer.find('form').valid()) {
// Ignore hidden field validation // Ignore hidden field validation

View File

@ -46,10 +46,7 @@
$keyField.append($keyLabel, $key); $keyField.append($keyLabel, $key);
$valueField.append($valueLabel, $value); $valueField.append($valueLabel, $value);
$form.append( $form.append($keyField, $valueField, $submit);
$keyField, $valueField,
$submit
);
$form.validate(); $form.validate();
@ -80,9 +77,11 @@
} }
}); });
// Prevent input during submission if (args.isAsyncSubmit) {
$key.attr('disabled', 'disabled'); // Prevent input during submission
$value.attr('disabled', 'disabled'); $key.attr('disabled', 'disabled');
$value.attr('disabled', 'disabled');
}
return false; return false;
} : } :
@ -102,6 +101,8 @@
$label.append($key, '<span>=</span>', $value); $label.append($key, '<span>=</span>', $value);
$label.attr('title', title); $label.attr('title', title);
$label.attr('cloudstack_tag_key', _s(data.key));
$label.attr('cloudstack_tag_value', _s(data.value));
$remove.click(function() { $remove.click(function() {
if (onRemove) onRemove($li, data); if (onRemove) onRemove($li, data);
}); });
@ -173,6 +174,7 @@
}; };
var $inputArea = elems.inputArea({ var $inputArea = elems.inputArea({
isAsyncSubmit: true,
onSubmit: function(args) { onSubmit: function(args) {
var data = args.data; var data = args.data;
var success = args.response.success; var success = args.response.success;
@ -252,4 +254,53 @@
}); });
} }
}); });
$.widget('cloudStack.taggerInForm', {
_init: function(args) {
var $container = this.element.addClass('tagger');
var $tagArea = $('<ul>').addClass('tags');
var $title = elems.info(_l('label.tags')).addClass('title inside-form');
var $loading = $('<div>').addClass('loading-overlay');
var $tags = {};
var onRemoveItem = function($item, data) {
$item.remove();
if ($tags[data.key]) delete $tags[data.key];
else {
cloudStack.dialog.notice({
message: "Unexpected error occured in attempting deletion"
});
}
};
var $inputArea = elems.inputArea({
isAsyncSubmit: false,
onSubmit: function(args) {
var data = args.data;
if ($tags[data.key]) {
cloudStack.dialog.notice({
message: "Key already present. Please delete previous and add again."
});
} else {
var success = args.response.success;
var title = data.key + ' = ' + data.value;
elems.tagItem(title, onRemoveItem, data).appendTo($tagArea);
success();
$tags[data.key] = data.value;
}
}
});
$container.append($title, $inputArea, $tagArea);
}
});
cloudStack.getTagsFromForm = function($form) {
var tagLabels = $($form).find('.tagger .tags .label');
var tags = [];
$(tagLabels).each(function() {
tags.push({'key' : $(this).attr('cloudstack_tag_key'), 'value' : $(this).attr('cloudstack_tag_value')});
});
return tags;
};
}(jQuery, cloudStack)); }(jQuery, cloudStack));