You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2017/10/13 09:55:33 UTC
[cloudstack] branch master updated: CLOUDSTACK-9957 Annotations
(#2181)
This is an automated email from the ASF dual-hosted git repository.
dahn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/master by this push:
new a379230 CLOUDSTACK-9957 Annotations (#2181)
a379230 is described below
commit a379230e8efb3014043cd4efd676178aeba00f2c
Author: dahn <da...@gmail.com>
AuthorDate: Fri Oct 13 11:55:26 2017 +0200
CLOUDSTACK-9957 Annotations (#2181)
* annotations on hosts
* Adding marvin tests
* rebase error
* review comments
* context for owner
* review
* illegal entity test
* entityType check on input
* Annotation events
* rebase issues
---
api/src/com/cloud/event/EventTypes.java | 17 +-
.../apache/cloudstack/annotation/Annotation.java | 37 +++++
.../cloudstack/annotation/AnnotationService.java | 49 ++++++
.../org/apache/cloudstack/api/ApiConstants.java | 4 +
api/src/org/apache/cloudstack/api/BaseCmd.java | 53 +++---
.../command/admin/annotation/AddAnnotationCmd.java | 80 +++++++++
.../admin/annotation/ListAnnotationsCmd.java | 81 ++++++++++
.../admin/annotation/RemoveAnnotationCmd.java | 64 ++++++++
.../api/command/admin/host/UpdateHostCmd.java | 22 ++-
.../api/response/AnnotationResponse.java | 121 ++++++++++++++
.../cloudstack/api/response/HostResponse.java | 23 +++
.../spring-engine-schema-core-daos-context.xml | 1 +
.../apache/cloudstack/annotation/AnnotationVO.java | 154 ++++++++++++++++++
.../cloudstack/annotation/dao/AnnotationDao.java | 30 ++++
.../annotation/dao/AnnotationDaoImpl.java | 59 +++++++
.../core/spring-server-core-managers-context.xml | 3 +
.../com/cloud/api/query/dao/HostJoinDaoImpl.java | 8 +-
server/src/com/cloud/api/query/vo/HostJoinVO.java | 31 +++-
.../annotation/AnnotationManagerImpl.java | 149 +++++++++++++++++
setup/db/db/schema-41000to41100.sql | 85 +++++++---
test/integration/smoke/test_host_annotations.py | 178 +++++++++++++++++++++
tools/apidoc/gen_toc.py | 3 +
ui/l10n/en.js | 3 +
ui/scripts/system.js | 27 +++-
24 files changed, 1215 insertions(+), 67 deletions(-)
diff --git a/api/src/com/cloud/event/EventTypes.java b/api/src/com/cloud/event/EventTypes.java
index f63ffdf..ec3f3ac 100644
--- a/api/src/com/cloud/event/EventTypes.java
+++ b/api/src/com/cloud/event/EventTypes.java
@@ -19,12 +19,6 @@ package com.cloud.event;
import java.util.HashMap;
import java.util.Map;
-import org.apache.cloudstack.acl.Role;
-import org.apache.cloudstack.acl.RolePermission;
-import org.apache.cloudstack.config.Configuration;
-import org.apache.cloudstack.ha.HAConfig;
-import org.apache.cloudstack.usage.Usage;
-
import com.cloud.dc.DataCenter;
import com.cloud.dc.Pod;
import com.cloud.dc.StorageNetworkIpRange;
@@ -75,6 +69,12 @@ import com.cloud.user.User;
import com.cloud.vm.Nic;
import com.cloud.vm.NicSecondaryIp;
import com.cloud.vm.VirtualMachine;
+import org.apache.cloudstack.acl.Role;
+import org.apache.cloudstack.acl.RolePermission;
+import org.apache.cloudstack.annotation.Annotation;
+import org.apache.cloudstack.config.Configuration;
+import org.apache.cloudstack.ha.HAConfig;
+import org.apache.cloudstack.usage.Usage;
public class EventTypes {
@@ -569,6 +569,9 @@ public class EventTypes {
public static final String EVENT_NETSCALER_VM_START = "NETSCALERVM.START";
public static final String EVENT_NETSCALER_VM_STOP = "NETSCALERVM.STOP";
+ public static final String EVENT_ANNOTATION_CREATE = "ANNOTATION.CREATE";
+ public static final String EVENT_ANNOTATION_REMOVE = "ANNOTATION.REMOVE";
+
static {
@@ -953,6 +956,8 @@ public class EventTypes {
entityEventDetails.put(EVENT_NETSCALER_SERVICEPACKAGE_ADD, "NETSCALER.SERVICEPACKAGE.CREATE");
entityEventDetails.put(EVENT_NETSCALER_SERVICEPACKAGE_DELETE, "NETSCALER.SERVICEPACKAGE.DELETE");
+ entityEventDetails.put(EVENT_ANNOTATION_CREATE, Annotation.class);
+ entityEventDetails.put(EVENT_ANNOTATION_REMOVE, Annotation.class);
}
public static String getEntityForEvent(String eventName) {
diff --git a/api/src/org/apache/cloudstack/annotation/Annotation.java b/api/src/org/apache/cloudstack/annotation/Annotation.java
new file mode 100644
index 0000000..90e371e
--- /dev/null
+++ b/api/src/org/apache/cloudstack/annotation/Annotation.java
@@ -0,0 +1,37 @@
+// 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.annotation;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+import java.util.Date;
+
+public interface Annotation extends InternalIdentity, Identity {
+
+ String getAnnotation();
+
+ String getEntityUuid();
+
+ AnnotationService.EntityType getEntityType();
+
+ String getUserUuid();
+
+ Date getCreated();
+
+ Date getRemoved();
+}
diff --git a/api/src/org/apache/cloudstack/annotation/AnnotationService.java b/api/src/org/apache/cloudstack/annotation/AnnotationService.java
new file mode 100644
index 0000000..769a753
--- /dev/null
+++ b/api/src/org/apache/cloudstack/annotation/AnnotationService.java
@@ -0,0 +1,49 @@
+// 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.annotation;
+
+import org.apache.cloudstack.api.command.admin.annotation.AddAnnotationCmd;
+import org.apache.cloudstack.api.command.admin.annotation.ListAnnotationsCmd;
+import org.apache.cloudstack.api.command.admin.annotation.RemoveAnnotationCmd;
+import org.apache.cloudstack.api.response.AnnotationResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+
+public interface AnnotationService {
+ ListResponse<AnnotationResponse> searchForAnnotations(ListAnnotationsCmd cmd);
+
+ AnnotationResponse addAnnotation(AddAnnotationCmd addAnnotationCmd);
+ AnnotationResponse addAnnotation(String text, EntityType type, String uuid);
+
+ AnnotationResponse removeAnnotation(RemoveAnnotationCmd removeAnnotationCmd);
+
+ enum EntityType {
+ HOST("host"), DOMAIN("domain"), VM("vm_instance");
+ private String tableName;
+
+ EntityType(String tableName) {
+ this.tableName = tableName;
+ }
+ static public boolean contains(String representation) {
+ try {
+ /* EntityType tiep = */ valueOf(representation);
+ return true;
+ } catch (IllegalArgumentException iae) {
+ return false;
+ }
+ }
+ }
+}
diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java
index 2300e68..af722bf 100644
--- a/api/src/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/org/apache/cloudstack/api/ApiConstants.java
@@ -25,6 +25,7 @@ public class ApiConstants {
public static final String ADDRESS = "address";
public static final String ALGORITHM = "algorithm";
public static final String ALLOCATED_ONLY = "allocatedonly";
+ public static final String ANNOTATION = "annotation";
public static final String API_KEY = "apikey";
public static final String USER_API_KEY = "userapikey";
public static final String APPLIED = "applied";
@@ -680,6 +681,9 @@ public class ApiConstants {
+ " representing the java supported algorithm, i.e. MD5 or SHA-256. Note that java does not\n"
+ " contain an algorithm called SHA256 or one called sha-256, only SHA-256.";
+ public static final String HAS_ANNOTATION = "hasannotation";
+ public static final String LAST_ANNOTATED = "lastannotated";
+
public enum HostDetails {
all, capacity, events, stats, min;
}
diff --git a/api/src/org/apache/cloudstack/api/BaseCmd.java b/api/src/org/apache/cloudstack/api/BaseCmd.java
index 5be7519..37dbeaa 100644
--- a/api/src/org/apache/cloudstack/api/BaseCmd.java
+++ b/api/src/org/apache/cloudstack/api/BaseCmd.java
@@ -17,32 +17,6 @@
package org.apache.cloudstack.api;
-import java.lang.reflect.Field;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Pattern;
-
-import javax.inject.Inject;
-
-import com.cloud.utils.HttpUtils;
-import org.apache.cloudstack.acl.RoleService;
-import org.apache.log4j.Logger;
-
-import org.apache.cloudstack.acl.RoleType;
-import org.apache.cloudstack.affinity.AffinityGroupService;
-import org.apache.cloudstack.alert.AlertService;
-import org.apache.cloudstack.context.CallContext;
-import org.apache.cloudstack.network.element.InternalLoadBalancerElementService;
-import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService;
-import org.apache.cloudstack.network.lb.InternalLoadBalancerVMService;
-import org.apache.cloudstack.query.QueryService;
-import org.apache.cloudstack.usage.UsageService;
-
import com.cloud.configuration.ConfigurationService;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
@@ -78,11 +52,35 @@ import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.DomainService;
import com.cloud.user.ResourceLimitService;
+import com.cloud.utils.HttpUtils;
import com.cloud.utils.ReflectUtil;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.UUIDManager;
import com.cloud.vm.UserVmService;
import com.cloud.vm.snapshot.VMSnapshotService;
+import org.apache.cloudstack.acl.RoleService;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.affinity.AffinityGroupService;
+import org.apache.cloudstack.alert.AlertService;
+import org.apache.cloudstack.annotation.AnnotationService;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.network.element.InternalLoadBalancerElementService;
+import org.apache.cloudstack.network.lb.ApplicationLoadBalancerService;
+import org.apache.cloudstack.network.lb.InternalLoadBalancerVMService;
+import org.apache.cloudstack.query.QueryService;
+import org.apache.cloudstack.usage.UsageService;
+import org.apache.log4j.Logger;
+
+import javax.inject.Inject;
+import java.lang.reflect.Field;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
public abstract class BaseCmd {
private static final Logger s_logger = Logger.getLogger(BaseCmd.class.getName());
@@ -93,6 +91,7 @@ public abstract class BaseCmd {
public static Pattern newInputDateFormat = Pattern.compile("[\\d]+-[\\d]+-[\\d]+ [\\d]+:[\\d]+:[\\d]+");
private static final DateFormat s_outputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
protected static final Map<Class<?>, List<Field>> fieldsForCmdClass = new HashMap<Class<?>, List<Field>>();
+
public static enum HTTPMethod {
GET, POST, PUT, DELETE
}
@@ -192,6 +191,8 @@ public abstract class BaseCmd {
public AlertService _alertSvc;
@Inject
public UUIDManager _uuidMgr;
+ @Inject
+ public AnnotationService annotationService;
public abstract void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
ResourceAllocationException, NetworkRuleConflictException;
diff --git a/api/src/org/apache/cloudstack/api/command/admin/annotation/AddAnnotationCmd.java b/api/src/org/apache/cloudstack/api/command/admin/annotation/AddAnnotationCmd.java
new file mode 100644
index 0000000..ac8fbc4
--- /dev/null
+++ b/api/src/org/apache/cloudstack/api/command/admin/annotation/AddAnnotationCmd.java
@@ -0,0 +1,80 @@
+// 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.annotation;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.google.common.base.Preconditions;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.annotation.AnnotationService;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.AnnotationResponse;
+import org.apache.cloudstack.context.CallContext;
+
+@APICommand(name = AddAnnotationCmd.APINAME, description = "add an annotation.", responseObject = AnnotationResponse.class,
+ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.11", authorized = {RoleType.Admin})
+public class AddAnnotationCmd extends BaseCmd {
+
+ public static final String APINAME = "addAnnotation";
+
+ @Parameter(name = ApiConstants.ANNOTATION, type = CommandType.STRING, description = "the annotation text")
+ private String annotation;
+ @Parameter(name = ApiConstants.ENTITY_TYPE, type = CommandType.STRING, description = "the entity type (only HOST is allowed atm)")
+ private String entityType;
+ @Parameter(name = ApiConstants.ENTITY_ID, type = CommandType.STRING, description = "the id of the entity to annotate")
+ private String entityUuid;
+
+ public String getAnnotation() {
+ return annotation;
+ }
+
+ public AnnotationService.EntityType getEntityType() {
+ return AnnotationService.EntityType.valueOf(entityType);
+ }
+
+ public String getEntityUuid() {
+ return entityUuid;
+ }
+
+ @Override
+ public void execute()
+ throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException,
+ NetworkRuleConflictException {
+ Preconditions.checkNotNull(entityUuid,"I have to have an entity to set an annotation on!");
+ Preconditions.checkState(AnnotationService.EntityType.contains(entityType),(java.lang.String)"'%s' is ot a valid EntityType to put annotations on", entityType);
+ AnnotationResponse annotationResponse = annotationService.addAnnotation(this);
+ annotationResponse.setResponseName(getCommandName());
+ this.setResponseObject(annotationResponse);
+ }
+
+ @Override
+ public String getCommandName() {
+ return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getAccountId();
+ }
+}
diff --git a/api/src/org/apache/cloudstack/api/command/admin/annotation/ListAnnotationsCmd.java b/api/src/org/apache/cloudstack/api/command/admin/annotation/ListAnnotationsCmd.java
new file mode 100644
index 0000000..4657eb9
--- /dev/null
+++ b/api/src/org/apache/cloudstack/api/command/admin/annotation/ListAnnotationsCmd.java
@@ -0,0 +1,81 @@
+// 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.annotation;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.utils.StringUtils;
+import com.google.common.base.Preconditions;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.BaseListCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.AnnotationResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+
+@APICommand(name = ListAnnotationsCmd.APINAME, description = "Lists annotations.", responseObject = AnnotationResponse.class,
+ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.11", authorized = {RoleType.Admin})
+public class ListAnnotationsCmd extends BaseListCmd {
+
+ public static final String APINAME = "listAnnotations";
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.STRING, description = "the id of the annotation")
+ private String uuid;
+ @Parameter(name = ApiConstants.ENTITY_TYPE, type = CommandType.STRING, description = "the entity type")
+ private String entityType;
+ @Parameter(name = ApiConstants.ENTITY_ID, type = CommandType.STRING, description = "the id of the entity for which to show annotations")
+ private String entityUuid;
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public String getEntityType() {
+ return entityType;
+ }
+
+ public String getEntityUuid() {
+ return entityUuid;
+ }
+
+ @Override public void execute()
+ throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException,
+ NetworkRuleConflictException {
+ // preconditions to check:
+ // if entity type is null entity uuid can not have a value
+ Preconditions.checkArgument(StringUtils.isNotBlank(entityType) ? ! StringUtils.isNotBlank(uuid) : true,
+ "I can search for an anotation on an entity or for a specific annotation, not both");
+ // if uuid has a value entity type and entity uuid can not have a value
+ Preconditions.checkArgument(StringUtils.isNotBlank(uuid) ? entityType == null && entityUuid == null : true,
+ "I will either search for a specific annotation or for annotations on an entity, not both");
+
+ ListResponse<AnnotationResponse> response = annotationService.searchForAnnotations(this);
+ response.setResponseName(getCommandName());
+ this.setResponseObject(response);
+ response.setObjectName("annotations");
+ }
+
+ @Override public String getCommandName() {
+ return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+ }
+}
diff --git a/api/src/org/apache/cloudstack/api/command/admin/annotation/RemoveAnnotationCmd.java b/api/src/org/apache/cloudstack/api/command/admin/annotation/RemoveAnnotationCmd.java
new file mode 100644
index 0000000..581ce45
--- /dev/null
+++ b/api/src/org/apache/cloudstack/api/command/admin/annotation/RemoveAnnotationCmd.java
@@ -0,0 +1,64 @@
+// 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.annotation;
+
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.NetworkRuleConflictException;
+import com.cloud.exception.ResourceAllocationException;
+import com.cloud.exception.ResourceUnavailableException;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseCmd;
+import org.apache.cloudstack.api.Parameter;
+import org.apache.cloudstack.api.ServerApiException;
+import org.apache.cloudstack.api.response.AnnotationResponse;
+import org.apache.cloudstack.context.CallContext;
+
+@APICommand(name = RemoveAnnotationCmd.APINAME, description = "remove an annotation.", responseObject = AnnotationResponse.class,
+ requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.11", authorized = {RoleType.Admin})
+public class RemoveAnnotationCmd extends BaseCmd {
+
+ public static final String APINAME = "removeAnnotation";
+
+ @Parameter(name = ApiConstants.ID, type = CommandType.STRING, required = true, description = "the id of the annotation")
+ private String uuid;
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ @Override
+ public void execute()
+ throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException,
+ NetworkRuleConflictException {
+ AnnotationResponse annotationResponse = annotationService.removeAnnotation(this);
+ annotationResponse.setResponseName(getCommandName());
+ this.setResponseObject(annotationResponse);
+ }
+
+ @Override
+ public String getCommandName() {
+ return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
+ }
+
+ @Override
+ public long getEntityOwnerId() {
+ return CallContext.current().getCallingAccount().getAccountId();
+ }
+}
diff --git a/api/src/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java b/api/src/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java
index c6f6530..aa0a690 100644
--- a/api/src/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java
+++ b/api/src/org/apache/cloudstack/api/command/admin/host/UpdateHostCmd.java
@@ -16,10 +16,10 @@
// under the License.
package org.apache.cloudstack.api.command.admin.host;
-import java.util.List;
-
-import org.apache.log4j.Logger;
-
+import com.cloud.host.Host;
+import com.cloud.user.Account;
+import org.apache.cloudstack.acl.RoleType;
+import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
@@ -28,9 +28,9 @@ import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.HostResponse;
+import org.apache.log4j.Logger;
-import com.cloud.host.Host;
-import com.cloud.user.Account;
+import java.util.List;
@APICommand(name = "updateHost", description = "Updates a host.", responseObject = HostResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@@ -62,6 +62,9 @@ public class UpdateHostCmd extends BaseCmd {
@Parameter(name = ApiConstants.URL, type = CommandType.STRING, description = "the new uri for the secondary storage: nfs://host/path")
private String url;
+ @Parameter(name = ApiConstants.ANNOTATION, type = CommandType.STRING, description = "Add an annotation to this host", since = "4.11", authorized = {RoleType.Admin})
+ private String annotation;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -86,6 +89,10 @@ public class UpdateHostCmd extends BaseCmd {
return url;
}
+ public String getAnnotation() {
+ return annotation;
+ }
+
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@@ -109,6 +116,9 @@ public class UpdateHostCmd extends BaseCmd {
Host result;
try {
result = _resourceService.updateHost(this);
+ if(getAnnotation() != null) {
+ annotationService.addAnnotation(getAnnotation(), AnnotationService.EntityType.HOST, result.getUuid());
+ }
HostResponse hostResponse = _responseGenerator.createHostResponse(result);
hostResponse.setResponseName(getCommandName());
this.setResponseObject(hostResponse);
diff --git a/api/src/org/apache/cloudstack/api/response/AnnotationResponse.java b/api/src/org/apache/cloudstack/api/response/AnnotationResponse.java
new file mode 100644
index 0000000..c16971a
--- /dev/null
+++ b/api/src/org/apache/cloudstack/api/response/AnnotationResponse.java
@@ -0,0 +1,121 @@
+// 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.response;
+
+import com.cloud.serializer.Param;
+import com.google.gson.annotations.SerializedName;
+import org.apache.cloudstack.annotation.Annotation;
+import org.apache.cloudstack.annotation.AnnotationService;
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.api.BaseResponse;
+import org.apache.cloudstack.api.EntityReference;
+
+import java.util.Date;
+
+/**
+ * @since 4.11
+ */
+@EntityReference(value = Annotation.class)
+public class AnnotationResponse extends BaseResponse {
+ @SerializedName(ApiConstants.ID)
+ @Param(description = "the (uu)id of the annotation")
+ private String uuid;
+
+ @SerializedName(ApiConstants.ENTITY_TYPE)
+ @Param(description = "the type of the annotated entity")
+ private String entityType;
+
+ @SerializedName(ApiConstants.ENTITY_ID)
+ @Param(description = "the (uu)id of the entitiy to which this annotation pertains")
+ private String entityUuid;
+
+ @SerializedName(ApiConstants.ANNOTATION)
+ @Param(description = "the contents of the annotation")
+ private String annotation;
+
+ @SerializedName(ApiConstants.USER_ID)
+ @Param(description = "The (uu)id of the user that entered the annotation")
+ private String userUuid;
+
+ @SerializedName(ApiConstants.CREATED)
+ @Param(description = "the creation timestamp for this annotation")
+ private Date created;
+
+ @SerializedName(ApiConstants.REMOVED)
+ @Param(description = "the removal timestamp for this annotation")
+ private Date removed;
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getEntityType() {
+ return entityType;
+ }
+
+ public void setEntityType(String entityType) {
+ this.entityType = entityType;
+ }
+
+ public void setEntityType(AnnotationService.EntityType entityType) {
+ this.entityType = entityType.toString();
+ }
+
+ public String getEntityUuid() {
+ return entityUuid;
+ }
+
+ public void setEntityUuid(String entityUuid) {
+ this.entityUuid = entityUuid;
+ }
+
+ public String getAnnotation() {
+ return annotation;
+ }
+
+ public void setAnnotation(String annotation) {
+ this.annotation = annotation;
+ }
+
+ public String getUserUuid() {
+ return userUuid;
+ }
+
+ public void setUserUuid(String userUuid) {
+ this.userUuid = userUuid;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public Date getRemoved() {
+ return removed;
+ }
+
+ public void setRemoved(Date removed) {
+ this.removed = removed;
+ }
+}
diff --git a/api/src/org/apache/cloudstack/api/response/HostResponse.java b/api/src/org/apache/cloudstack/api/response/HostResponse.java
index 91cb805..b9667ec 100644
--- a/api/src/org/apache/cloudstack/api/response/HostResponse.java
+++ b/api/src/org/apache/cloudstack/api/response/HostResponse.java
@@ -231,6 +231,17 @@ public class HostResponse extends BaseResponse {
@Param(description = "Host details in key/value pairs.", since = "4.5")
private Map details;
+ @SerializedName(ApiConstants.ANNOTATION)
+ @Param(description = "the last annotation set on this host by an admin", since = "4.11")
+ private String annotation;
+
+ @SerializedName(ApiConstants.LAST_ANNOTATED)
+ @Param(description = "the last time this host was annotated", since = "4.11")
+ private Date lastAnnotated;
+
+ @SerializedName(ApiConstants.USERNAME)
+ @Param(description = "the admin that annotated this host", since = "4.11")
+ private String username;
// Default visibility to support accessing the details from unit tests
Map getDetails() {
@@ -458,6 +469,18 @@ public class HostResponse extends BaseResponse {
this.haHost = haHost;
}
+ public void setAnnotation(String annotation) {
+ this.annotation = annotation;
+ }
+
+ public void setLastAnnotated(Date lastAnnotated) {
+ this.lastAnnotated = lastAnnotated;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
public void setDetails(Map details) {
if (details == null) {
diff --git a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
index 654bca9..2075b93 100644
--- a/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
+++ b/engine/schema/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml
@@ -353,4 +353,5 @@
<bean id="LBHealthCheckPolicyDetailsDaoImpl" class="org.apache.cloudstack.resourcedetail.dao.LBHealthCheckPolicyDetailsDaoImpl" />
<bean id="outOfBandManagementDaoImpl" class="org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDaoImpl" />
<bean id="GuestOsDetailsDaoImpl" class="org.apache.cloudstack.resourcedetail.dao.GuestOsDetailsDaoImpl" />
+ <bean id="annotationDaoImpl" class="org.apache.cloudstack.annotation.dao.AnnotationDaoImpl" />
</beans>
diff --git a/engine/schema/src/org/apache/cloudstack/annotation/AnnotationVO.java b/engine/schema/src/org/apache/cloudstack/annotation/AnnotationVO.java
new file mode 100644
index 0000000..982dd6d
--- /dev/null
+++ b/engine/schema/src/org/apache/cloudstack/annotation/AnnotationVO.java
@@ -0,0 +1,154 @@
+// 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.annotation;
+
+import com.cloud.utils.db.GenericDao;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * @since 4.11
+ */
+@Entity
+@Table(name = "annotations")
+public class AnnotationVO implements Annotation {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ @Column(name = "id")
+ private long id;
+
+ @Column(name = "uuid")
+ private String uuid;
+
+ @Column(name = "annotation")
+ private String annotation;
+
+ @Column(name = "entity_uuid")
+ private String entityUuid;
+
+ @Column(name = "entity_type")
+ private AnnotationService.EntityType entityType;
+
+ @Column(name = "user_uuid")
+ private String userUuid;
+
+ @Column(name = GenericDao.CREATED_COLUMN)
+ private Date created;
+
+ @Column(name = GenericDao.REMOVED_COLUMN)
+ private Date removed;
+
+ // construct
+ public AnnotationVO() {
+ this.uuid = UUID.randomUUID().toString();
+ }
+
+ public AnnotationVO(String text, AnnotationService.EntityType type, String uuid) {
+ this();
+ setAnnotation(text);
+ setEntityType(type);
+ setEntityUuid(uuid);
+ }
+
+ public AnnotationVO(String text, String type, String uuid) {
+ this();
+ setAnnotation(text);
+ setEntityType(type);
+ setEntityUuid(uuid);
+ }
+ // access
+
+ @Override
+ public long getId() {
+ return id;
+ }
+
+
+ @Override
+ public String getUuid() {
+ return uuid;
+ }
+
+ @Override
+ public String getAnnotation() {
+ return annotation;
+ }
+
+ @Override
+ public String getEntityUuid() {
+ return entityUuid;
+ }
+
+ @Override
+ public AnnotationService.EntityType getEntityType() {
+ return entityType;
+ }
+
+ @Override
+ public String getUserUuid() {
+ return userUuid;
+ }
+
+ @Override
+ public Date getCreated() {
+ return created;
+ }
+
+ @Override
+ public Date getRemoved() {
+ return removed;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public void setAnnotation(String annotation) {
+ this.annotation = annotation;
+ }
+
+ public void setEntityUuid(String entityUuid) {
+ this.entityUuid = entityUuid;
+ }
+
+ public void setEntityType(String entityType) {
+ this.entityType = AnnotationService.EntityType.valueOf(entityType);
+ }
+
+ public void setEntityType(AnnotationService.EntityType entityType) {
+ this.entityType = entityType;
+ }
+
+ public void setUserUuid(String userUuid) {
+ this.userUuid = userUuid;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public void setRemoved(Date removed) {
+ this.removed = removed;
+ }
+}
diff --git a/engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDao.java b/engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDao.java
new file mode 100644
index 0000000..6bf8484
--- /dev/null
+++ b/engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDao.java
@@ -0,0 +1,30 @@
+// 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.annotation.dao;
+
+import org.apache.cloudstack.annotation.AnnotationVO;
+import com.cloud.utils.db.GenericDao;
+
+import java.util.List;
+
+/**
+ * @since 4.11
+ */
+public interface AnnotationDao extends GenericDao<AnnotationVO, Long> {
+ public List<AnnotationVO> findByEntityType(String entityType);
+ public List<AnnotationVO> findByEntity(String entityType, String entityUuid);
+}
diff --git a/engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDaoImpl.java b/engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDaoImpl.java
new file mode 100644
index 0000000..e2fcc90
--- /dev/null
+++ b/engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDaoImpl.java
@@ -0,0 +1,59 @@
+// 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.annotation.dao;
+
+import com.cloud.utils.db.GenericDaoBase;
+import com.cloud.utils.db.SearchBuilder;
+import com.cloud.utils.db.SearchCriteria;
+import org.apache.cloudstack.annotation.AnnotationVO;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * @since 4.1
+ */
+@Component
+public class AnnotationDaoImpl extends GenericDaoBase<AnnotationVO, Long> implements AnnotationDao {
+ private final SearchBuilder<AnnotationVO> AnnotationSearchByType;
+ private final SearchBuilder<AnnotationVO> AnnotationSearchByTypeAndUuid;
+
+ public AnnotationDaoImpl() {
+ super();
+ AnnotationSearchByType = createSearchBuilder();
+ AnnotationSearchByType.and("entityType", AnnotationSearchByType.entity().getEntityType(), SearchCriteria.Op.EQ);
+ AnnotationSearchByType.done();
+ AnnotationSearchByTypeAndUuid = createSearchBuilder();
+ AnnotationSearchByTypeAndUuid.and("entityType", AnnotationSearchByTypeAndUuid.entity().getEntityType(), SearchCriteria.Op.EQ);
+ AnnotationSearchByTypeAndUuid.and("entityUuid", AnnotationSearchByTypeAndUuid.entity().getEntityUuid(), SearchCriteria.Op.EQ);
+ AnnotationSearchByTypeAndUuid.done();
+
+ }
+
+ @Override public List<AnnotationVO> findByEntityType(String entityType) {
+ SearchCriteria<AnnotationVO> sc = createSearchCriteria();
+ sc.addAnd("entityType", SearchCriteria.Op.EQ, entityType);
+ return listBy(sc);
+ }
+
+ @Override public List<AnnotationVO> findByEntity(String entityType, String entityUuid) {
+ SearchCriteria<AnnotationVO> sc = createSearchCriteria();
+ sc.addAnd("entityType", SearchCriteria.Op.EQ, entityType);
+ sc.addAnd("entityUuid", SearchCriteria.Op.EQ, entityUuid);
+ return listBy(sc, null);
+ }
+}
diff --git a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
index ca9ed57..94e8559 100644
--- a/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
+++ b/server/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml
@@ -283,6 +283,7 @@
<property name="gslbServiceProviders" value="#{gslbServiceProvidersRegistry.registered}" />
</bean>
<bean id="certServiceImpl" class="org.apache.cloudstack.network.ssl.CertServiceImpl" />
+
<bean id="imageStoreUploadMonitorImpl" class="com.cloud.storage.ImageStoreUploadMonitorImpl" />
<!-- the new CA manager -->
@@ -290,4 +291,6 @@
<property name="caProviders" value="#{caProvidersRegistry.registered}" />
</bean>
+ <bean id="annotationService" class="org.apache.cloudstack.annotation.AnnotationManagerImpl" />
+
</beans>
diff --git a/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java b/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java
index 8fc3e42..4d411f2 100644
--- a/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java
+++ b/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java
@@ -35,8 +35,6 @@ import org.apache.cloudstack.api.response.HostForMigrationResponse;
import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.VgpuResponse;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
-import org.apache.cloudstack.ha.HAResource;
-import org.apache.cloudstack.ha.dao.HAConfigDao;
import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao;
import com.cloud.api.ApiDBUtils;
@@ -52,6 +50,9 @@ import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
+import org.apache.cloudstack.ha.HAResource;
+import org.apache.cloudstack.ha.dao.HAConfigDao;
+
@Component
public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements HostJoinDao {
public static final Logger s_logger = Logger.getLogger(HostJoinDaoImpl.class);
@@ -244,6 +245,9 @@ public class HostJoinDaoImpl extends GenericDaoBase<HostJoinVO, Long> implements
hostResponse.setJobId(host.getJobUuid());
hostResponse.setJobStatus(host.getJobStatus());
}
+ hostResponse.setAnnotation(host.getAnnotation());
+ hostResponse.setLastAnnotated(host.getLastAnnotated ());
+ hostResponse.setUsername(host.getUsername());
hostResponse.setObjectName("host");
diff --git a/server/src/com/cloud/api/query/vo/HostJoinVO.java b/server/src/com/cloud/api/query/vo/HostJoinVO.java
index ea2e518..2afe191 100644
--- a/server/src/com/cloud/api/query/vo/HostJoinVO.java
+++ b/server/src/com/cloud/api/query/vo/HostJoinVO.java
@@ -27,15 +27,15 @@ import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
-import org.apache.cloudstack.api.Identity;
-import org.apache.cloudstack.api.InternalIdentity;
-
import com.cloud.host.Host.Type;
import com.cloud.host.Status;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.org.Cluster;
import com.cloud.resource.ResourceState;
+import com.cloud.utils.StringUtils;
import com.cloud.utils.db.GenericDao;
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
import org.apache.cloudstack.ha.HAConfig;
import org.apache.cloudstack.outofbandmanagement.OutOfBandManagement;
@@ -192,6 +192,15 @@ public class HostJoinVO extends BaseViewVO implements InternalIdentity, Identity
@Column(name = "job_status")
private int jobStatus;
+ @Column(name = "annotation")
+ private String annotation;
+
+ @Column(name = "last_annotated")
+ private Date lastAnnotated;
+
+ @Column(name = "username")
+ private String username;
+
@Override
public long getId() {
return this.id;
@@ -377,4 +386,20 @@ public class HostJoinVO extends BaseViewVO implements InternalIdentity, Identity
public String getTag() {
return tag;
}
+
+ public String getAnnotation() {
+ return annotation;
+ }
+
+ public Date getLastAnnotated() {
+ return lastAnnotated;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public boolean isAnnotated() {
+ return StringUtils.isNotBlank(annotation);
+ }
}
diff --git a/server/src/org/apache/cloudstack/annotation/AnnotationManagerImpl.java b/server/src/org/apache/cloudstack/annotation/AnnotationManagerImpl.java
new file mode 100644
index 0000000..2b658d5
--- /dev/null
+++ b/server/src/org/apache/cloudstack/annotation/AnnotationManagerImpl.java
@@ -0,0 +1,149 @@
+// 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.annotation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import com.cloud.event.ActionEvent;
+import com.cloud.event.EventTypes;
+import com.cloud.utils.component.ManagerBase;
+import com.cloud.utils.component.PluggableService;
+import org.apache.cloudstack.annotation.dao.AnnotationDao;
+import org.apache.cloudstack.api.command.admin.annotation.AddAnnotationCmd;
+import org.apache.cloudstack.api.command.admin.annotation.ListAnnotationsCmd;
+import org.apache.cloudstack.api.command.admin.annotation.RemoveAnnotationCmd;
+import org.apache.cloudstack.api.response.AnnotationResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.log4j.Logger;
+
+/**
+ * @since 4.11
+ */
+public final class AnnotationManagerImpl extends ManagerBase implements AnnotationService, PluggableService {
+ public static final Logger LOGGER = Logger.getLogger(AnnotationManagerImpl.class);
+
+ @Inject
+ private AnnotationDao annotationDao;
+
+ @Override
+ public ListResponse<AnnotationResponse> searchForAnnotations(ListAnnotationsCmd cmd) {
+ List<AnnotationVO> annotations = getAnnotationsForApiCmd(cmd);
+ List<AnnotationResponse> annotationResponses = convertAnnotationsToResponses(annotations);
+ return createAnnotationsResponseList(annotationResponses);
+ }
+
+ @Override
+ @ActionEvent(eventType = EventTypes.EVENT_ANNOTATION_CREATE, eventDescription = "creating an annotation on an entity")
+ public AnnotationResponse addAnnotation(AddAnnotationCmd addAnnotationCmd) {
+ return addAnnotation(addAnnotationCmd.getAnnotation(), addAnnotationCmd.getEntityType(), addAnnotationCmd.getEntityUuid());
+ }
+
+ public AnnotationResponse addAnnotation(String text, EntityType type, String uuid) {
+ CallContext ctx = CallContext.current();
+ String userUuid = ctx.getCallingUserUuid();
+
+ AnnotationVO annotation = new AnnotationVO(text, type, uuid);
+ annotation.setUserUuid(userUuid);
+ annotation = annotationDao.persist(annotation);
+ return createAnnotationResponse(annotation);
+ }
+
+ @Override
+ @ActionEvent(eventType = EventTypes.EVENT_ANNOTATION_REMOVE, eventDescription = "removing an annotation on an entity")
+ public AnnotationResponse removeAnnotation(RemoveAnnotationCmd removeAnnotationCmd) {
+ String uuid = removeAnnotationCmd.getUuid();
+ if(LOGGER.isDebugEnabled()) {
+ LOGGER.debug("marking annotation removed: " + uuid);
+ }
+ AnnotationVO annotation = annotationDao.findByUuid(uuid);
+ annotationDao.remove(annotation.getId());
+ return createAnnotationResponse(annotation);
+ }
+
+ private List<AnnotationVO> getAnnotationsForApiCmd(ListAnnotationsCmd cmd) {
+ List<AnnotationVO> annotations;
+ if(cmd.getUuid() != null) {
+ annotations = new ArrayList<>();
+ String uuid = cmd.getUuid().toString();
+ if(LOGGER.isDebugEnabled()) {
+ LOGGER.debug("getting single annotation by uuid: " + uuid);
+ }
+
+ annotations.add(annotationDao.findByUuid(uuid));
+ } else if( ! (cmd.getEntityType() == null || cmd.getEntityType().isEmpty()) ) {
+ String type = cmd.getEntityType();
+ if(LOGGER.isDebugEnabled()) {
+ LOGGER.debug("getting annotations for type: " + type);
+ }
+ if (cmd.getEntityUuid() != null) {
+ String uuid = cmd.getEntityUuid().toString();
+ if(LOGGER.isDebugEnabled()) {
+ LOGGER.debug("getting annotations for entity: " + uuid);
+ }
+ annotations = annotationDao.findByEntity(type,cmd.getEntityUuid().toString());
+ } else {
+ annotations = annotationDao.findByEntityType(type);
+ }
+ } else {
+ if(LOGGER.isDebugEnabled()) {
+ LOGGER.debug("getting all annotations");
+ }
+ annotations = annotationDao.listAll();
+ }
+ return annotations;
+ }
+
+ private List<AnnotationResponse> convertAnnotationsToResponses(List<AnnotationVO> annotations) {
+ List<AnnotationResponse> annotationResponses = new ArrayList<>();
+ for (AnnotationVO annotation : annotations) {
+ annotationResponses.add(createAnnotationResponse(annotation));
+ }
+ return annotationResponses;
+ }
+
+ private ListResponse<AnnotationResponse> createAnnotationsResponseList(List<AnnotationResponse> annotationResponses) {
+ ListResponse<AnnotationResponse> listResponse = new ListResponse<>();
+ listResponse.setResponses(annotationResponses);
+ return listResponse;
+ }
+
+ public static AnnotationResponse createAnnotationResponse(AnnotationVO annotation) {
+ AnnotationResponse response = new AnnotationResponse();
+ response.setUuid(annotation.getUuid());
+ response.setEntityType(annotation.getEntityType());
+ response.setEntityUuid(annotation.getEntityUuid());
+ response.setAnnotation(annotation.getAnnotation());
+ response.setUserUuid(annotation.getUserUuid());
+ response.setCreated(annotation.getCreated());
+ response.setRemoved(annotation.getRemoved());
+ response.setObjectName("annotation");
+
+ return response;
+ }
+
+ @Override public List<Class<?>> getCommands() {
+ final List<Class<?>> cmdList = new ArrayList<>();
+ cmdList.add(AddAnnotationCmd.class);
+ cmdList.add(ListAnnotationsCmd.class);
+ cmdList.add(RemoveAnnotationCmd.class);
+ return cmdList;
+ }
+}
diff --git a/setup/db/db/schema-41000to41100.sql b/setup/db/db/schema-41000to41100.sql
index 14a48d7..e5e6c05 100644
--- a/setup/db/db/schema-41000to41100.sql
+++ b/setup/db/db/schema-41000to41100.sql
@@ -159,10 +159,46 @@ CREATE TABLE IF NOT EXISTS `cloud`.`ha_config` (
DELETE from `cloud`.`configuration` where name='outofbandmanagement.sync.interval';
+-- Annotations specifc changes following
+CREATE TABLE IF NOT EXISTS `cloud`.`annotations` (
+ `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(40) UNIQUE,
+ `annotation` text,
+ `entity_uuid` varchar(40),
+ `entity_type` varchar(32),
+ `user_uuid` varchar(40),
+ `created` datetime COMMENT 'date of creation',
+ `removed` datetime COMMENT 'date of removal',
+ PRIMARY KEY (`id`),
+ KEY (`uuid`),
+ KEY `i_entity` (`entity_uuid`, `entity_type`, `created`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+DROP VIEW IF EXISTS `cloud`.`last_annotation_view`;
+CREATE VIEW `last_annotation_view` AS
+ SELECT
+ `annotations`.`uuid` AS `uuid`,
+ `annotations`.`annotation` AS `annotation`,
+ `annotations`.`entity_uuid` AS `entity_uuid`,
+ `annotations`.`entity_type` AS `entity_type`,
+ `annotations`.`user_uuid` AS `user_uuid`,
+ `annotations`.`created` AS `created`,
+ `annotations`.`removed` AS `removed`
+ FROM
+ `annotations`
+ WHERE
+ `annotations`.`created` IN (SELECT
+ MAX(`annotations`.`created`)
+ FROM
+ `annotations`
+ WHERE
+ `annotations`.`removed` IS NULL
+ GROUP BY `annotations`.`entity_uuid`);
+
-- Host HA changes:
DROP VIEW IF EXISTS `cloud`.`host_view`;
CREATE VIEW `cloud`.`host_view` AS
- select
+ SELECT
host.id,
host.uuid,
host.name,
@@ -210,37 +246,46 @@ CREATE VIEW `cloud`.`host_view` AS
oobm.power_state AS `oobm_power_state`,
ha_config.enabled AS `ha_enabled`,
ha_config.ha_state AS `ha_state`,
- ha_config.provider AS `ha_provider`
- from
+ ha_config.provider AS `ha_provider`,
+ `last_annotation_view`.`annotation` AS `annotation`,
+ `last_annotation_view`.`created` AS `last_annotated`,
+ `user`.`username` AS `username`
+ FROM
`cloud`.`host`
- left join
+ LEFT JOIN
`cloud`.`cluster` ON host.cluster_id = cluster.id
- left join
+ LEFT JOIN
`cloud`.`data_center` ON host.data_center_id = data_center.id
- left join
+ LEFT JOIN
`cloud`.`host_pod_ref` ON host.pod_id = host_pod_ref.id
- left join
+ LEFT JOIN
`cloud`.`host_details` ON host.id = host_details.host_id
- and host_details.name = 'guest.os.category.id'
- left join
- `cloud`.`guest_os_category` ON guest_os_category.id = CONVERT( host_details.value , UNSIGNED)
- left join
+ AND host_details.name = 'guest.os.category.id'
+ LEFT JOIN
+ `cloud`.`guest_os_category` ON guest_os_category.id = CONVERT ( host_details.value, UNSIGNED )
+ LEFT JOIN
`cloud`.`host_tags` ON host_tags.host_id = host.id
- left join
+ LEFT JOIN
`cloud`.`op_host_capacity` mem_caps ON host.id = mem_caps.host_id
- and mem_caps.capacity_type = 0
- left join
+ AND mem_caps.capacity_type = 0
+ LEFT JOIN
`cloud`.`op_host_capacity` cpu_caps ON host.id = cpu_caps.host_id
- and cpu_caps.capacity_type = 1
- left join
+ AND cpu_caps.capacity_type = 1
+ LEFT JOIN
`cloud`.`async_job` ON async_job.instance_id = host.id
- and async_job.instance_type = 'Host'
- and async_job.job_status = 0
- left join
+ AND async_job.instance_type = 'Host'
+ AND async_job.job_status = 0
+ LEFT JOIN
`cloud`.`oobm` ON oobm.host_id = host.id
left join
`cloud`.`ha_config` ON ha_config.resource_id=host.id
- and ha_config.resource_type='Host';
+ and ha_config.resource_type='Host'
+ LEFT JOIN
+ `cloud`.`last_annotation_view` ON `last_annotation_view`.`entity_uuid` = `host`.`uuid`
+ LEFT JOIN
+ `cloud`.`user` ON `user`.`uuid` = `last_annotation_view`.`user_uuid`;
+-- End Of Annotations specific changes
+
-- Out-of-band management driver for nested-cloudstack
ALTER TABLE `cloud`.`oobm` MODIFY COLUMN port VARCHAR(255);
diff --git a/test/integration/smoke/test_host_annotations.py b/test/integration/smoke/test_host_annotations.py
new file mode 100644
index 0000000..45a918f
--- /dev/null
+++ b/test/integration/smoke/test_host_annotations.py
@@ -0,0 +1,178 @@
+# 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.
+""" BVT tests for Hosts and Clusters
+"""
+#Import Local Modules
+import marvin
+from marvin.cloudstackTestCase import *
+from marvin.cloudstackAPI import *
+from marvin.lib.utils import *
+from marvin.lib.base import *
+from marvin.lib.common import *
+from marvin.lib.utils import (random_gen)
+from nose.plugins.attrib import attr
+
+#Import System modules
+import time
+
+_multiprocess_shared_ = True
+
+class TestHostAnnotations(cloudstackTestCase):
+
+ def setUp(self):
+ self.apiclient = self.testClient.getApiClient()
+ self.services = self.testClient.getParsedTestDataConfig()
+ self.zone = get_zone(self.apiclient, self.testClient.getZoneForTests())
+ self.host = list_hosts(self.apiclient,
+ zoneid=self.zone.id,
+ type='Routing')[0]
+ self.cleanup = []
+ self.added_annotations = []
+
+ return
+
+ def tearDown(self):
+ try:
+ #Clean up
+ cleanup_resources(self.apiclient, self.cleanup)
+ self.cleanAnnotations()
+ except Exception as e:
+ raise Exception("Warning: Exception during cleanup : %s" % e)
+ return
+
+ def cleanAnnotations(self):
+ """Remove annotations"""
+ for annotation in self.added_annotations:
+ self.removeAnnotation(annotation.annotation.id)
+
+ def addAnnotation(self, annotation):
+ cmd = addAnnotation.addAnnotationCmd()
+ cmd.entityid = self.host.id
+ cmd.entitytype = "HOST"
+ cmd.annotation = annotation
+
+ self.added_annotations.append(self.apiclient.addAnnotation(cmd))
+
+ return self.added_annotations[-1]
+
+ def removeAnnotation(self, id):
+ cmd = removeAnnotation.removeAnnotationCmd()
+ cmd.id = id
+
+ return self.apiclient.removeAnnotation(cmd)
+
+ def getHostAnnotation(self, hostId):
+ host = list_hosts(self.apiclient,
+ zoneid=self.zone.id,
+ type='Routing')[0]
+ return host.annotation
+
+ @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
+ def test_01_add_annotation(self):
+ """Testing the addAnnotations API ability to add an annoatation per host"""
+ self.addAnnotation("annotation1")
+ self.assertEqual(self.added_annotations[-1].annotation.annotation, "annotation1")
+
+ @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
+ def test_02_add_multiple_annotations(self):
+ """Testing the addAnnotations API ability to add an annoatation per host
+ when there are annotations already.
+ And only the last one stands as annotation attribute on host level."""
+ self.addAnnotation("annotation1")
+ self.assertEqual(self.added_annotations[-1].annotation.annotation, "annotation1")
+
+ # Adds sleep of 1 second just to be sure next annotation will not be created in the same second.
+ time.sleep(1)
+ self.addAnnotation("annotation2")
+ self.assertEqual(self.added_annotations[-1].annotation.annotation, "annotation2")
+
+ # Adds sleep of 1 second just to be sure next annotation will not be created in the same second.
+ time.sleep(1)
+ self.addAnnotation("annotation3")
+ self.assertEqual(self.added_annotations[-1].annotation.annotation, "annotation3")
+
+ #Check that the last one is visible in host details
+ self.assertEqual(self.getHostAnnotation(self.host.id), "annotation3")
+ print
+
+ @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
+ def test_03_user_role_dont_see_annotations(self):
+ """Testing the annotations api are restricted to users"""
+
+ self.addAnnotation("annotation1")
+ self.assertEqual(self.added_annotations[-1].annotation.annotation, "annotation1")
+
+ self.account = Account.create(
+ self.apiclient,
+ self.services["account"],
+ )
+ self.cleanup.append(self.account)
+
+ userApiClient = self.testClient.getUserApiClient(self.account.name, 'ROOT', 'User')
+
+ cmd = addAnnotation.addAnnotationCmd()
+ cmd.entityid = self.host.id
+ cmd.entitytype = "HOST"
+ cmd.annotation = "test"
+
+ try:
+ self.added_annotations.append(userApiClient.addAnnotation(cmd))
+ except Exception:
+ pass
+ else:
+ self.fail("AddAnnotation is allowed for User")
+
+ cmd = listAnnotations.listAnnotationsCmd()
+ try:
+ userApiClient.listAnnotations(cmd)
+ except Exception:
+ pass
+ else:
+ self.fail("ListAnnotations is allowed for User")
+
+ cmd = removeAnnotation.removeAnnotationCmd()
+ cmd.id = self.added_annotations[-1].annotation.id
+ try:
+ userApiClient.removeAnnotation(cmd)
+ except Exception:
+ pass
+ else:
+ self.fail("RemoveAnnotation is allowed for User")
+
+ @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
+ def test_04_remove_annotations(self):
+ """Testing the deleteAnnotation API ability to delete annotation"""
+ self.addAnnotation("annotation1")
+ self.removeAnnotation(self.added_annotations[-1].annotation.id)
+ del self.added_annotations[-1]
+
+
+ @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
+ def test_05_add_annotation_for_invvalid_entityType(self):
+ cmd = addAnnotation.addAnnotationCmd()
+ cmd.entityid = self.host.id
+ cmd.entitytype = "BLA"
+ cmd.annotation = annotation
+
+ try:
+ self.apiclient.addAnnotation(cmd)
+ except CloudstackAPIException as f:
+ log.debug("error message %s" % f)
+ else:
+ self.fail("AddAnnotation is allowed for on an unknown entityType")
+
+ return self.added_annotations[-1]
diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py
index 762a0b0..47af115 100644
--- a/tools/apidoc/gen_toc.py
+++ b/tools/apidoc/gen_toc.py
@@ -181,6 +181,9 @@ known_categories = {
'deleteServicePackageOffering' : 'Load Balancer',
'destroyNsVpx' : 'Load Balancer',
'startNsVpx' : 'Load Balancer',
+ 'listAnnotations' : 'Annotations',
+ 'addAnnotation' : 'Annotations',
+ 'removeAnnotation' : 'Annotations',
'CA': 'Certificate'
}
diff --git a/ui/l10n/en.js b/ui/l10n/en.js
index 39e7072..91bca45 100644
--- a/ui/l10n/en.js
+++ b/ui/l10n/en.js
@@ -427,6 +427,8 @@ var dictionary = {"ICMP.code":"ICMP Code",
"label.allocated":"Allocated",
"label.allocation.state":"Allocation State",
"label.allow":"Allow",
+"label.annotated.by":"Annotator",
+"label.annotation":"Annotation",
"label.anti.affinity":"Anti-affinity",
"label.anti.affinity.group":"Anti-affinity Group",
"label.anti.affinity.groups":"Anti-affinity Groups",
@@ -944,6 +946,7 @@ var dictionary = {"ICMP.code":"ICMP Code",
"label.lang.polish":"Polish",
"label.lang.russian":"Russian",
"label.lang.spanish":"Spanish",
+"label.last.annotated":"Last annotation date",
"label.last.disconnected":"Last Disconnected",
"label.last.name":"Last Name",
"label.lastname.lower":"lastname",
diff --git a/ui/scripts/system.js b/ui/scripts/system.js
index aa71364..e912f3b 100755
--- a/ui/scripts/system.js
+++ b/ui/scripts/system.js
@@ -16062,7 +16062,10 @@
array1.push("&hosttags=" + todb(args.data.hosttags));
if (args.data.oscategoryid != null && args.data.oscategoryid.length > 0)
- array1.push("&osCategoryId=" + args.data.oscategoryid);
+ array1.push("&osCategoryId=" + args.data.oscategoryid);
+
+ if (args.data.annotation != null && args.data.annotation.length > 0)
+ array1.push("&annotation=" + args.data.annotation);
$.ajax({
url: createURL("updateHost&id=" + args.context.hosts[0].id + array1.join("")),
@@ -17073,11 +17076,22 @@
ipaddress: {
label: 'label.ip.address'
},
+ annotation: {
+ label: 'label.annotation',
+ isEditable: true
+ },
+ lastannotated: {
+ label: 'label.last.annotated',
+ converter: cloudStack.converters.toLocalDate
+ },
+ username: {
+ label: 'label.annotated.by'
+ },
disconnected: {
label: 'label.last.disconnected'
- },
- cpusockets: {
- label: 'label.number.of.cpu.sockets'
+ },
+ cpusockets: {
+ label: 'label.number.of.cpu.sockets'
}
}, {
@@ -17099,12 +17113,17 @@
if (item && item.outofbandmanagement) {
item.powerstate = item.outofbandmanagement.powerstate;
}
+
if (item && item.hostha) {
item.hastate = item.hostha.hastate;
item.haprovider = item.hostha.haprovider;
item.haenabled = item.hostha.haenable;
}
+ item.annotation = item.annotation;
+ item.lastannotated = item.lastannotated;
+ item.username = item.username;
+
$.ajax({
url: createURL("listDedicatedHosts&hostid=" + args.context.hosts[0].id),
dataType: "json",
--
To stop receiving notification emails like this one, please contact
['"commits@cloudstack.apache.org" <co...@cloudstack.apache.org>'].