From a379230e8efb3014043cd4efd676178aeba00f2c Mon Sep 17 00:00:00 2001 From: dahn Date: Fri, 13 Oct 2017 11:55:26 +0200 Subject: [PATCH] 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 +- .../cloudstack/annotation/Annotation.java | 37 ++++ .../annotation/AnnotationService.java | 49 +++++ .../apache/cloudstack/api/ApiConstants.java | 4 + .../org/apache/cloudstack/api/BaseCmd.java | 53 +++--- .../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 + .../cloudstack/annotation/AnnotationVO.java | 154 +++++++++++++++ .../annotation/dao/AnnotationDao.java | 30 +++ .../annotation/dao/AnnotationDaoImpl.java | 59 ++++++ .../spring-server-core-managers-context.xml | 3 + .../cloud/api/query/dao/HostJoinDaoImpl.java | 8 +- .../com/cloud/api/query/vo/HostJoinVO.java | 31 ++- .../annotation/AnnotationManagerImpl.java | 149 +++++++++++++++ setup/db/db/schema-41000to41100.sql | 85 +++++++-- .../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(-) create mode 100644 api/src/org/apache/cloudstack/annotation/Annotation.java create mode 100644 api/src/org/apache/cloudstack/annotation/AnnotationService.java create mode 100644 api/src/org/apache/cloudstack/api/command/admin/annotation/AddAnnotationCmd.java create mode 100644 api/src/org/apache/cloudstack/api/command/admin/annotation/ListAnnotationsCmd.java create mode 100644 api/src/org/apache/cloudstack/api/command/admin/annotation/RemoveAnnotationCmd.java create mode 100644 api/src/org/apache/cloudstack/api/response/AnnotationResponse.java create mode 100644 engine/schema/src/org/apache/cloudstack/annotation/AnnotationVO.java create mode 100644 engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDao.java create mode 100644 engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDaoImpl.java create mode 100644 server/src/org/apache/cloudstack/annotation/AnnotationManagerImpl.java create mode 100644 test/integration/smoke/test_host_annotations.py diff --git a/api/src/com/cloud/event/EventTypes.java b/api/src/com/cloud/event/EventTypes.java index f63ffdf1219..ec3f3ac6d10 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 00000000000..90e371e5071 --- /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 00000000000..769a753b587 --- /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 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 2300e680df6..af722bfba7b 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 5be75196d39..37dbeaab841 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, List> fieldsForCmdClass = new HashMap, List>(); + 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 00000000000..ac8fbc40199 --- /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 00000000000..4657eb9e16a --- /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 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 00000000000..581ce45f252 --- /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 c6f6530dc50..aa0a690e2a9 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 00000000000..c16971ae7f3 --- /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 91cb8058813..b9667eca241 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 654bca99920..2075b933b64 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 @@ + 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 00000000000..982dd6db15b --- /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 00000000000..6bf8484560c --- /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 { + public List findByEntityType(String entityType); + public List 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 00000000000..e2fcc905e0b --- /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 implements AnnotationDao { + private final SearchBuilder AnnotationSearchByType; + private final SearchBuilder 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 findByEntityType(String entityType) { + SearchCriteria sc = createSearchCriteria(); + sc.addAnd("entityType", SearchCriteria.Op.EQ, entityType); + return listBy(sc); + } + + @Override public List findByEntity(String entityType, String entityUuid) { + SearchCriteria 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 ca9ed579dc1..94e8559120a 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 @@ + @@ -290,4 +291,6 @@ + + diff --git a/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java b/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java index 8fc3e422d6a..4d411f2f2b4 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 implements HostJoinDao { public static final Logger s_logger = Logger.getLogger(HostJoinDaoImpl.class); @@ -244,6 +245,9 @@ public class HostJoinDaoImpl extends GenericDaoBase 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 ea2e5185c76..2afe1910964 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 00000000000..2b658d55e35 --- /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 searchForAnnotations(ListAnnotationsCmd cmd) { + List annotations = getAnnotationsForApiCmd(cmd); + List 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 getAnnotationsForApiCmd(ListAnnotationsCmd cmd) { + List 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 convertAnnotationsToResponses(List annotations) { + List annotationResponses = new ArrayList<>(); + for (AnnotationVO annotation : annotations) { + annotationResponses.add(createAnnotationResponse(annotation)); + } + return annotationResponses; + } + + private ListResponse createAnnotationsResponseList(List annotationResponses) { + ListResponse 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> getCommands() { + final List> 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 14a48d7f502..e5e6c0552ee 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 00000000000..45a918f4d9d --- /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 762a0b0974e..47af1155434 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 39e70727ba1..91bca455d0b 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 aa7136459e5..e912f3b3ce8 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",