cloudstack-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From d...@apache.org
Subject [cloudstack] branch master updated: CLOUDSTACK-9957 Annotations (#2181)
Date Fri, 13 Oct 2017 09:55:33 GMT
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 <daan.hoogland@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" <commits@cloudstack.apache.org>'].

Mime
View raw message