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..92510acf9ba --- /dev/null +++ b/api/src/org/apache/cloudstack/annotation/Annotation.java @@ -0,0 +1,40 @@ +// 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; + +/** + * @since 4.11 + */ +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..a7f2f26651a --- /dev/null +++ b/api/src/org/apache/cloudstack/annotation/AnnotationService.java @@ -0,0 +1,44 @@ +// 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; + +/** + * @since 4.11 + */ +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; + } + } +} diff --git a/api/src/org/apache/cloudstack/api/ApiConstants.java b/api/src/org/apache/cloudstack/api/ApiConstants.java index 3a6a730f777..a366226d1c5 100755 --- 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"; @@ -637,6 +638,8 @@ public class ApiConstants { public static final String PHYSICAL_SIZE = "physicalsize"; public static final String VIRTUAL_SIZE = "virtualsize"; public static final String UTILIZATION = "utilization"; + 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 dd2a6b19f33..139b26126ef 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()); @@ -95,6 +93,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 } @@ -194,6 +193,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..761f2b4fa53 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/annotation/AddAnnotationCmd.java @@ -0,0 +1,83 @@ +// 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.user.Account; +import com.google.common.base.Preconditions; +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; + +/** + * @since 4.11 + */ +@APICommand(name = AddAnnotationCmd.APINAME, description = "add an annotation.", responseObject = AnnotationResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class AddAnnotationCmd extends BaseCmd{ + + @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") + 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; + } + + public static final String APINAME = "addAnnotation"; + + @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!"); + 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() { + // for now all annotations are belong to us + return Account.ACCOUNT_ID_SYSTEM; + // we would of course query the owner of the actual entitytype/entityuuid to return the right account + } +} 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..a0a2dd1952f --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/annotation/ListAnnotationsCmd.java @@ -0,0 +1,83 @@ +// 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.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; + +/** + * @since 4.11 + */ +@APICommand(name = ListAnnotationsCmd.APINAME, description = "Lists annotations.", responseObject = AnnotationResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class ListAnnotationsCmd extends BaseListCmd { + + public static final String APINAME = "listAnnotations"; + + public String getUuid() { + return uuid; + } + + public String getEntityType() { + return entityType; + } + + public String getEntityUuid() { + return entityUuid; + } + + @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; + + @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..5fe7148c484 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/command/admin/annotation/RemoveAnnotationCmd.java @@ -0,0 +1,68 @@ +// 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.user.Account; +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; + +/** + * @since 4.11 + */ +@APICommand(name = RemoveAnnotationCmd.APINAME, description = "remove an annotation.", responseObject = AnnotationResponse.class, + requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) +public class RemoveAnnotationCmd extends BaseCmd{ + + @Parameter(name = ApiConstants.ID, type = CommandType.STRING, required = true, description = "the id of the annotation") + private String uuid; + + public String getUuid() { + return uuid; + } + + public static final String APINAME = "removeAnnotation"; + + @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() { + // for now all annotations are belong to us + return Account.ACCOUNT_ID_SYSTEM; + // we would of course query the owner of the actual entitytype/entityuuid to return the right account + } +} 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..7108be4bf1b 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,9 @@ // 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.annotation.AnnotationService; import org.apache.cloudstack.api.APICommand; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiErrorCode; @@ -28,9 +27,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 +61,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") + private String annotation; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -86,6 +88,10 @@ public class UpdateHostCmd extends BaseCmd { return url; } + public String getAnnotation() { + return annotation; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -109,6 +115,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 b5f81e6b8f6..3c9128fefd4 100644 --- a/api/src/org/apache/cloudstack/api/response/HostResponse.java +++ b/api/src/org/apache/cloudstack/api/response/HostResponse.java @@ -226,6 +226,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() { @@ -453,6 +464,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/client/WEB-INF/classes/resources/messages.properties b/client/WEB-INF/classes/resources/messages.properties index 01d458f215f..35fba1e61ef 100644 --- a/client/WEB-INF/classes/resources/messages.properties +++ b/client/WEB-INF/classes/resources/messages.properties @@ -1536,6 +1536,9 @@ label.release.dedicated.cluster=Release Dedicated Cluster label.dedicate.host=Dedicate Host label.release.dedicated.host=Release Dedicated Host label.number.of.cpu.sockets=The Number of CPU Sockets +label.annotation=Annotation +label.last.annotated=Last annotated at +label.annotated.by=Annotated by label.delete.ucs.manager=Delete UCS Manager label.blades=Blades label.chassis=Chassis 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 afb9fec9ede..8b51bad52cc 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 @@ -348,5 +348,6 @@ + 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..e0a2cb9ed94 --- /dev/null +++ b/engine/schema/src/org/apache/cloudstack/annotation/dao/AnnotationDaoImpl.java @@ -0,0 +1,61 @@ +// 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 javax.ejb.Local; +import java.util.List; + +/** + * @since 4.1 + */ +@Component +@Local(value = {AnnotationDao.class}) +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 5aff66b16a0..57ca17647a6 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 @@ -248,4 +248,7 @@ + + + diff --git a/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java b/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java index acecc92a918..a35cc190b12 100644 --- a/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java +++ b/server/src/com/cloud/api/query/dao/HostJoinDaoImpl.java @@ -16,6 +16,32 @@ // under the License. package com.cloud.api.query.dao; +import com.cloud.api.ApiDBUtils; +import com.cloud.api.query.vo.HostJoinVO; +import com.cloud.gpu.HostGpuGroupsVO; +import com.cloud.gpu.VGPUTypesVO; +import com.cloud.host.Host; +import com.cloud.host.HostStats; +import com.cloud.host.dao.HostDetailsDao; +import com.cloud.hypervisor.Hypervisor; +import com.cloud.storage.StorageStats; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.api.ApiConstants.HostDetails; +import org.apache.cloudstack.api.response.GpuResponse; +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 org.apache.log4j.Logger; +import org.springframework.stereotype.Component; + +import javax.ejb.Local; +import javax.inject.Inject; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Date; @@ -24,35 +50,6 @@ import java.util.Iterator; import java.util.List; import java.util.Set; -import javax.ejb.Local; -import javax.inject.Inject; - -import com.cloud.host.dao.HostDetailsDao; -import org.apache.cloudstack.ha.HAResource; -import org.apache.cloudstack.ha.dao.HAConfigDao; -import org.apache.cloudstack.outofbandmanagement.dao.OutOfBandManagementDao; -import org.apache.log4j.Logger; -import org.springframework.stereotype.Component; - -import org.apache.cloudstack.api.ApiConstants.HostDetails; -import org.apache.cloudstack.api.response.GpuResponse; -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 com.cloud.api.ApiDBUtils; -import com.cloud.api.query.vo.HostJoinVO; -import com.cloud.gpu.HostGpuGroupsVO; -import com.cloud.gpu.VGPUTypesVO; -import com.cloud.host.Host; -import com.cloud.host.HostStats; -import com.cloud.hypervisor.Hypervisor; -import com.cloud.storage.StorageStats; -import com.cloud.utils.db.GenericDaoBase; -import com.cloud.utils.db.SearchBuilder; -import com.cloud.utils.db.SearchCriteria; - @Component @Local(value = {HostJoinDao.class}) public class HostJoinDaoImpl extends GenericDaoBase implements HostJoinDao { @@ -244,6 +241,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..c4663bc5bb8 100644 --- a/server/src/com/cloud/api/query/vo/HostJoinVO.java +++ b/server/src/com/cloud/api/query/vo/HostJoinVO.java @@ -16,7 +16,17 @@ // under the License. package com.cloud.api.query.vo; -import java.util.Date; +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; import javax.persistence.Column; import javax.persistence.Entity; @@ -26,18 +36,7 @@ import javax.persistence.Id; 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.db.GenericDao; -import org.apache.cloudstack.ha.HAConfig; -import org.apache.cloudstack.outofbandmanagement.OutOfBandManagement; +import java.util.Date; /** * Host DB view. @@ -192,6 +191,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 +385,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..2932377bc6e --- /dev/null +++ b/server/src/org/apache/cloudstack/annotation/AnnotationManagerImpl.java @@ -0,0 +1,129 @@ +package org.apache.cloudstack.annotation; + +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; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.List; + +/** + * @since 4.11 + */ +public final class AnnotationManagerImpl extends ManagerBase implements AnnotationService, PluggableService { + public static final Logger s_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 + 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 public AnnotationResponse removeAnnotation(RemoveAnnotationCmd removeAnnotationCmd) { + String uuid = removeAnnotationCmd.getUuid(); + if(s_logger.isDebugEnabled()) { + s_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(s_logger.isDebugEnabled()) { + s_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(s_logger.isDebugEnabled()) { + s_logger.debug("getting annotations for type: " + type); + } + if (cmd.getEntityUuid() != null) { + String uuid = cmd.getEntityUuid().toString(); + if(s_logger.isDebugEnabled()) { + s_logger.debug("getting annotations for entity: " + uuid); + } + annotations = annotationDao.findByEntity(type,cmd.getEntityUuid().toString()); + } else { + annotations = annotationDao.findByEntityType(type); + } + } else { + if(s_logger.isDebugEnabled()) { + s_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-452to453.sql b/setup/db/db/schema-452to453.sql index 7107e17463a..a3000930c12 100644 --- a/setup/db/db/schema-452to453.sql +++ b/setup/db/db/schema-452to453.sql @@ -388,3 +388,128 @@ CREATE VIEW `cloud`.`volume_view` AS -- Out-of-band management driver for nested-cloudstack ALTER TABLE `cloud`.`oobm` MODIFY COLUMN port VARCHAR(255); +-- 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`); + +DROP VIEW IF EXISTS `cloud`.`host_view`; +CREATE VIEW `cloud`.`host_view` AS + SELECT + host.id, + host.uuid, + host.name, + host.status, + host.disconnected, + host.type, + host.private_ip_address, + host.version, + host.hypervisor_type, + host.hypervisor_version, + host.capabilities, + host.last_ping, + host.created, + host.removed, + host.resource_state, + host.mgmt_server_id, + host.cpu_sockets, + host.cpus, + host.speed, + host.ram, + cluster.id cluster_id, + cluster.uuid cluster_uuid, + cluster.name cluster_name, + cluster.cluster_type, + data_center.id data_center_id, + data_center.uuid data_center_uuid, + data_center.name data_center_name, + data_center.networktype data_center_type, + host_pod_ref.id pod_id, + host_pod_ref.uuid pod_uuid, + host_pod_ref.name pod_name, + host_tags.tag, + guest_os_category.id guest_os_category_id, + guest_os_category.uuid guest_os_category_uuid, + guest_os_category.name guest_os_category_name, + mem_caps.used_capacity memory_used_capacity, + mem_caps.reserved_capacity memory_reserved_capacity, + cpu_caps.used_capacity cpu_used_capacity, + cpu_caps.reserved_capacity cpu_reserved_capacity, + async_job.id job_id, + async_job.uuid job_uuid, + async_job.job_status job_status, + async_job.account_id job_account_id, + oobm.enabled AS `oobm_enabled`, + 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`, + `last_annotation_view`.`annotation` AS `annotation`, + `last_annotation_view`.`created` AS `last_annotated`, + `user`.`username` AS `username` + FROM + `cloud`.`host` + LEFT JOIN + `cloud`.`cluster` ON host.cluster_id = cluster.id + LEFT JOIN + `cloud`.`data_center` ON host.data_center_id = data_center.id + LEFT JOIN + `cloud`.`host_pod_ref` ON host.pod_id = host_pod_ref.id + 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 + `cloud`.`host_tags` ON host_tags.host_id = host.id + LEFT JOIN + `cloud`.`op_host_capacity` mem_caps ON host.id = mem_caps.host_id + 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 + `cloud`.`async_job` ON async_job.instance_id = host.id + 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' + 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 \ No newline at end of file diff --git a/test/integration/smoke/test_host_annotations.py b/test/integration/smoke/test_host_annotations.py new file mode 100644 index 00000000000..795e78be39f --- /dev/null +++ b/test/integration/smoke/test_host_annotations.py @@ -0,0 +1,161 @@ +# 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] diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 433ef96be9e..2d96156051f 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -168,6 +168,9 @@ known_categories = { 'StratosphereSsp' : ' Stratosphere SSP', 'Metrics' : 'Metrics', 'Infrastructure' : 'Metrics', + 'listAnnotations' : 'Annotations', + 'addAnnotation' : 'Annotations', + 'removeAnnotation' : 'Annotations', } diff --git a/ui/dictionary2.jsp b/ui/dictionary2.jsp index 8aba0553654..bd080ae0372 100644 --- a/ui/dictionary2.jsp +++ b/ui/dictionary2.jsp @@ -779,6 +779,9 @@ $.extend(dictionary, { 'label.dedicate.host': '', 'label.release.dedicated.host': '', 'label.number.of.cpu.sockets': '', +'label.annotation': '', +'label.last.annotated': '', +'label.annotated.by': '', 'label.delete.ucs.manager': '', 'label.blades': '', 'label.chassis': '', diff --git a/ui/scripts/system.js b/ui/scripts/system.js index aa3d9041951..297375af06a 100644 --- a/ui/scripts/system.js +++ b/ui/scripts/system.js @@ -15741,8 +15741,11 @@ 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("")), dataType: "json", @@ -16749,9 +16752,20 @@ }, disconnected: { label: 'label.last.disconnected' - }, - cpusockets: { - label: 'label.number.of.cpu.sockets' + }, + cpusockets: { + label: 'label.number.of.cpu.sockets' + }, + annotation: { + label: 'label.annotation', + isEditable: true + }, + lastannotated: { + label: 'label.last.annotated', + converter: cloudStack.converters.toLocalDate + }, + username: { + label: 'label.annotated.by' } }, { @@ -16778,6 +16792,7 @@ item.haprovider = item.hostha.haprovider; item.haenabled = item.hostha.haenable; } + item.annotation = item.annotation; $.ajax({ url: createURL("listDedicatedHosts&hostid=" + args.context.hosts[0].id),