diff --git a/api/src/main/java/com/cloud/configuration/ConfigurationService.java b/api/src/main/java/com/cloud/configuration/ConfigurationService.java index 729f72b23ca2..05275a287dd9 100644 --- a/api/src/main/java/com/cloud/configuration/ConfigurationService.java +++ b/api/src/main/java/com/cloud/configuration/ConfigurationService.java @@ -38,11 +38,14 @@ import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.IsAccountAllowedToCreateOfferingsWithTagsCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.pod.DeletePodCmd; import org.apache.cloudstack.api.command.admin.pod.UpdatePodCmd; import org.apache.cloudstack.api.command.admin.region.CreatePortableIpRangeCmd; @@ -75,6 +78,7 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import com.cloud.offering.ServiceOfferingCategory; import com.cloud.user.Account; import com.cloud.utils.Pair; @@ -157,6 +161,30 @@ public interface ConfigurationService { */ List getServiceOfferingZones(Long serviceOfferingId); + /** + * Creates a service offering category + * + * @param cmd - the command specifying name and sort key + * @return the newly created service offering category + */ + ServiceOfferingCategory createServiceOfferingCategory(CreateServiceOfferingCategoryCmd cmd); + + /** + * Deletes a service offering category + * + * @param cmd - the command specifying category id + * @return true if successful, false otherwise + */ + boolean deleteServiceOfferingCategory(DeleteServiceOfferingCategoryCmd cmd); + + /** + * Updates a service offering category + * + * @param cmd - the command specifying category id, name, and/or sort key + * @return updated service offering category + */ + ServiceOfferingCategory updateServiceOfferingCategory(UpdateServiceOfferingCategoryCmd cmd); + /** * Updates a disk offering * diff --git a/api/src/main/java/com/cloud/event/EventTypes.java b/api/src/main/java/com/cloud/event/EventTypes.java index 7f45c8acd3f7..038d52a4431e 100644 --- a/api/src/main/java/com/cloud/event/EventTypes.java +++ b/api/src/main/java/com/cloud/event/EventTypes.java @@ -90,6 +90,7 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import com.cloud.offering.ServiceOfferingCategory; import com.cloud.projects.Project; import com.cloud.server.ResourceTag; import com.cloud.storage.GuestOS; @@ -404,6 +405,11 @@ public class EventTypes { public static final String EVENT_SERVICE_OFFERING_EDIT = "SERVICE.OFFERING.EDIT"; public static final String EVENT_SERVICE_OFFERING_DELETE = "SERVICE.OFFERING.DELETE"; + // Service Offering Categories + public static final String EVENT_SERVICE_OFFERING_CATEGORY_CREATE = "SERVICE.OFFERING.CATEGORY.CREATE"; + public static final String EVENT_SERVICE_OFFERING_CATEGORY_EDIT = "SERVICE.OFFERING.CATEGORY.EDIT"; + public static final String EVENT_SERVICE_OFFERING_CATEGORY_DELETE = "SERVICE.OFFERING.CATEGORY.DELETE"; + // Disk Offerings public static final String EVENT_DISK_OFFERING_CREATE = "DISK.OFFERING.CREATE"; public static final String EVENT_DISK_OFFERING_CLONE = "DISK.OFFERING.CLONE"; @@ -1125,6 +1131,9 @@ public class EventTypes { entityEventDetails.put(EVENT_SERVICE_OFFERING_CLONE, ServiceOffering.class); entityEventDetails.put(EVENT_SERVICE_OFFERING_EDIT, ServiceOffering.class); entityEventDetails.put(EVENT_SERVICE_OFFERING_DELETE, ServiceOffering.class); + entityEventDetails.put(EVENT_SERVICE_OFFERING_CATEGORY_CREATE, ServiceOfferingCategory.class); + entityEventDetails.put(EVENT_SERVICE_OFFERING_CATEGORY_EDIT, ServiceOfferingCategory.class); + entityEventDetails.put(EVENT_SERVICE_OFFERING_CATEGORY_DELETE, ServiceOfferingCategory.class); // Disk Offerings entityEventDetails.put(EVENT_DISK_OFFERING_CREATE, DiskOffering.class); diff --git a/api/src/main/java/com/cloud/offering/ServiceOffering.java b/api/src/main/java/com/cloud/offering/ServiceOffering.java index 532123e4373a..62d98c50a197 100644 --- a/api/src/main/java/com/cloud/offering/ServiceOffering.java +++ b/api/src/main/java/com/cloud/offering/ServiceOffering.java @@ -146,4 +146,6 @@ enum StorageType { Long getVgpuProfileId(); Integer getGpuCount(); + + Long getCategoryId(); } diff --git a/api/src/main/java/com/cloud/offering/ServiceOfferingCategory.java b/api/src/main/java/com/cloud/offering/ServiceOfferingCategory.java new file mode 100644 index 000000000000..77f2d6d1010d --- /dev/null +++ b/api/src/main/java/com/cloud/offering/ServiceOfferingCategory.java @@ -0,0 +1,33 @@ +// 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 com.cloud.offering; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +public interface ServiceOfferingCategory extends Identity, InternalIdentity { + + boolean isDefault(); + + String getName(); + + void setName(String name); + + int getSortKey(); + + void setSortKey(int sortKey); +} diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java index e12a709e6f5c..7e64414558d5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiCommandResourceType.java @@ -72,6 +72,7 @@ public enum ApiCommandResourceType { Domain(com.cloud.domain.Domain.class), DiskOffering(com.cloud.offering.DiskOffering.class), ServiceOffering(com.cloud.offering.ServiceOffering.class), + ServiceOfferingCategory(com.cloud.offering.ServiceOfferingCategory.class), NetworkOffering(com.cloud.offering.NetworkOffering.class), VpcOffering(com.cloud.network.vpc.VpcOffering.class), BackupOffering(org.apache.cloudstack.backup.BackupOffering.class), diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 7485029f22cb..3b64e45f640f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -568,6 +568,7 @@ public class ApiConstants { public static final String SENT_BYTES = "sentbytes"; public static final String SERIAL = "serial"; public static final String SERVICE_IP = "serviceip"; + public static final String SERVICE_OFFERING_CATEGORY_ID = "categoryid"; public static final String SERVICE_OFFERING_ID = "serviceofferingid"; public static final String SERVICE_OFFERING_NAME = "serviceofferingname"; public static final String SESSIONKEY = "sessionkey"; diff --git a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java index 6e880c89432f..8e19cf994fb5 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java +++ b/api/src/main/java/org/apache/cloudstack/api/ResponseGenerator.java @@ -116,6 +116,7 @@ import org.apache.cloudstack.api.response.SecondaryStorageHeuristicsResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; import org.apache.cloudstack.api.response.ServiceResponse; import org.apache.cloudstack.api.response.SharedFSResponse; import org.apache.cloudstack.api.response.Site2SiteCustomerGatewayResponse; @@ -224,6 +225,7 @@ import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import com.cloud.offering.ServiceOfferingCategory; import com.cloud.org.Cluster; import com.cloud.projects.Project; import com.cloud.projects.ProjectAccount; @@ -271,6 +273,8 @@ public interface ResponseGenerator { ServiceOfferingResponse createServiceOfferingResponse(ServiceOffering offering); + ServiceOfferingCategoryResponse createServiceOfferingCategoryResponse(ServiceOfferingCategory category); + ConfigurationResponse createConfigurationResponse(Configuration cfg); ConfigurationGroupResponse createConfigurationGroupResponse(ConfigurationGroup cfgGroup); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCategoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCategoryCmd.java new file mode 100644 index 000000000000..f0154e40a0c9 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCategoryCmd.java @@ -0,0 +1,93 @@ +// 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.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; + +import com.cloud.offering.ServiceOfferingCategory; +import com.cloud.user.Account; + +@APICommand(name = "createServiceOfferingCategory", + description = "Creates a service offering category.", + responseObject = ServiceOfferingCategoryResponse.class, + since = "24.0", + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin}) +public class CreateServiceOfferingCategoryCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.NAME, + type = CommandType.STRING, + required = true, + description = "the name of the service offering category") + private String name; + + @Parameter(name = ApiConstants.SORT_KEY, + type = CommandType.INTEGER, + description = "sort key of the service offering category, default is 0") + private Integer sortKey; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getName() { + return name; + } + + public Integer getSortKey() { + return sortKey; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + ServiceOfferingCategory result = _configService.createServiceOfferingCategory(this); + if (result != null) { + ServiceOfferingCategoryResponse response = _responseGenerator.createServiceOfferingCategoryResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to create service offering category"); + } + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.ServiceOfferingCategory; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java index 4363d6861ba1..02f1af9f7af1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/CreateServiceOfferingCmd.java @@ -32,6 +32,7 @@ import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; import org.apache.cloudstack.api.response.VgpuProfileResponse; import org.apache.cloudstack.api.response.VsphereStoragePoliciesResponse; import org.apache.cloudstack.api.response.ZoneResponse; @@ -288,6 +289,14 @@ public class CreateServiceOfferingCmd extends BaseCmd { since = "4.21.0") private Map externalDetails; + @Parameter(name = ApiConstants.SERVICE_OFFERING_CATEGORY_ID, + type = CommandType.UUID, + entityType = ServiceOfferingCategoryResponse.class, + required = false, + description = "the ID of the service offering category to associate with this offering", + since = "24.0") + private Long categoryId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -560,6 +569,10 @@ public Boolean getGpuDisplay() { return Boolean.TRUE.equals(gpuDisplay); } + public Long getCategoryId() { + return categoryId; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/DeleteServiceOfferingCategoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/DeleteServiceOfferingCategoryCmd.java new file mode 100644 index 000000000000..9e2a23933897 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/DeleteServiceOfferingCategoryCmd.java @@ -0,0 +1,89 @@ +// 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.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; +import org.apache.cloudstack.api.response.SuccessResponse; + +import com.cloud.user.Account; + +@APICommand(name = "deleteServiceOfferingCategory", + description = "Deletes a service offering category.", + responseObject = SuccessResponse.class, + since = "24.0", + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin}) +public class DeleteServiceOfferingCategoryCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = ServiceOfferingCategoryResponse.class, + required = true, + description = "the ID of the service offering category") + private Long id; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + boolean result = _configService.deleteServiceOfferingCategory(this); + if (result) { + SuccessResponse response = new SuccessResponse(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to delete service offering category"); + } + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.ServiceOfferingCategory; + } + + @Override + public Long getApiResourceId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/ListServiceOfferingCategoriesCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/ListServiceOfferingCategoriesCmd.java new file mode 100644 index 000000000000..4379ebf73699 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/ListServiceOfferingCategoriesCmd.java @@ -0,0 +1,73 @@ +// 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.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; + +@APICommand(name = "listServiceOfferingCategories", + description = "Lists service offering categories.", + responseObject = ServiceOfferingCategoryResponse.class, + since = "24.0", + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}) +public class ListServiceOfferingCategoriesCmd extends BaseListCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = ServiceOfferingCategoryResponse.class, + description = "ID of the service offering category") + private Long id; + + @Parameter(name = ApiConstants.NAME, + type = CommandType.STRING, + description = "name of the service offering category") + private String name; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() { + ListResponse response = _queryService.listServiceOfferingCategories(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCategoryCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCategoryCmd.java new file mode 100644 index 000000000000..e44e09cc4ac2 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCategoryCmd.java @@ -0,0 +1,108 @@ +// 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.offering; + +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; + +import com.cloud.offering.ServiceOfferingCategory; +import com.cloud.user.Account; + +@APICommand(name = "updateServiceOfferingCategory", + description = "Updates a service offering category", + responseObject = ServiceOfferingCategoryResponse.class, + since = "24.0", + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = false, + authorized = {RoleType.Admin}) +public class UpdateServiceOfferingCategoryCmd extends BaseCmd { + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ID, + type = CommandType.UUID, + entityType = ServiceOfferingCategoryResponse.class, + required = true, + description = "the ID of the service offering category") + private Long id; + + @Parameter(name = ApiConstants.NAME, + type = CommandType.STRING, + description = "the name of the service offering category") + private String name; + + @Parameter(name = ApiConstants.SORT_KEY, + type = CommandType.INTEGER, + description = "sort key of the service offering category") + private Integer sortKey; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Integer getSortKey() { + return sortKey; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_ID_SYSTEM; + } + + @Override + public void execute() { + ServiceOfferingCategory result = _configService.updateServiceOfferingCategory(this); + if (result != null) { + ServiceOfferingCategoryResponse response = _responseGenerator.createServiceOfferingCategoryResponse(result); + response.setResponseName(getCommandName()); + setResponseObject(response); + } else { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update service offering category"); + } + } + + @Override + public ApiCommandResourceType getApiResourceType() { + return ApiCommandResourceType.ServiceOfferingCategory; + } + + @Override + public Long getApiResourceId() { + return getId(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java index 8e37499c95ed..6ca8e3a6bebd 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; @@ -108,6 +109,14 @@ public class UpdateServiceOfferingCmd extends BaseCmd implements DomainAndZoneId since = "4.22.0") protected Boolean cleanupExternalDetails; + @Parameter(name = ApiConstants.SERVICE_OFFERING_CATEGORY_ID, + type = CommandType.UUID, + entityType = ServiceOfferingCategoryResponse.class, + required = false, + description = "the ID of the service offering category to associate", + since = "24.0") + private Long categoryId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -164,6 +173,8 @@ public boolean isCleanupExternalDetails() { return Boolean.TRUE.equals(cleanupExternalDetails); } + public Long getCategoryId() { return categoryId; } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java index cd6baaf827bb..47d25348fc33 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/offering/ListServiceOfferingsCmd.java @@ -23,6 +23,7 @@ import org.apache.cloudstack.api.BaseListProjectAndAccountResourcesCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.TemplateResponse; import org.apache.cloudstack.api.response.UserVmResponse; @@ -124,6 +125,13 @@ public class ListServiceOfferingsCmd extends BaseListProjectAndAccountResourcesC since = "4.21.0") private Boolean gpuEnabled; + @Parameter(name = ApiConstants.SERVICE_OFFERING_CATEGORY_ID, + type = CommandType.UUID, + entityType = ServiceOfferingCategoryResponse.class, + description = "the ID of the service offering category", + since = "24.0") + private Long categoryId; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -193,6 +201,10 @@ public Boolean getGpuEnabled() { return gpuEnabled; } + public Long getCategoryId() { + return categoryId; + } + public void setZoneId(Long zoneId) { this.zoneId = zoneId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingCategoryResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingCategoryResponse.java new file mode 100644 index 000000000000..e198aa9163b8 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingCategoryResponse.java @@ -0,0 +1,65 @@ +// 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 org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; +import org.apache.cloudstack.api.EntityReference; + +import com.cloud.offering.ServiceOfferingCategory; +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; + +@EntityReference(value = ServiceOfferingCategory.class) +public class ServiceOfferingCategoryResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of the service offering category") + private String id; + + @SerializedName(ApiConstants.NAME) + @Param(description = "the name of the service offering category") + private String name; + + @SerializedName(ApiConstants.SORT_KEY) + @Param(description = "sort key of the service offering category") + private Integer sortKey; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getSortKey() { + return sortKey; + } + + public void setSortKey(Integer sortKey) { + this.sortKey = sortKey; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java index 99c50829f048..63e9289024ff 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/ServiceOfferingResponse.java @@ -286,6 +286,14 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations { @Param(description = "Action to be taken once lease is over", since = "4.21.0") private String leaseExpiryAction; + @SerializedName("categoryid") + @Param(description = "the ID of the service offering category", since = "24.0") + private String categoryId; + + @SerializedName("category") + @Param(description = "the name of the service offering category", since = "24.0") + private String categoryName; + public ServiceOfferingResponse() { } @@ -707,4 +715,9 @@ public void setGpuDisplay(Boolean gpuDisplay) { public void setPurgeResources(Boolean purgeResources) { this.purgeResources = purgeResources; } + + public String getCategoryId() { return categoryId; } + public void setCategoryId(String categoryId) { this.categoryId = categoryId; } + public String getCategoryName() { return categoryName; } + public void setCategoryName(String categoryName) { this.categoryName = categoryName; } } diff --git a/api/src/main/java/org/apache/cloudstack/query/QueryService.java b/api/src/main/java/org/apache/cloudstack/query/QueryService.java index 4203f54aa06c..e61a20e57f4b 100644 --- a/api/src/main/java/org/apache/cloudstack/query/QueryService.java +++ b/api/src/main/java/org/apache/cloudstack/query/QueryService.java @@ -36,6 +36,7 @@ import org.apache.cloudstack.api.command.admin.storage.ListStoragePoolsCmd; import org.apache.cloudstack.api.command.admin.storage.ListStorageTagsCmd; import org.apache.cloudstack.api.command.admin.storage.heuristics.ListSecondaryStorageSelectorsCmd; +import org.apache.cloudstack.api.command.admin.offering.ListServiceOfferingCategoriesCmd; import org.apache.cloudstack.api.command.admin.user.ListUsersCmd; import org.apache.cloudstack.api.command.user.account.ListAccountsCmd; import org.apache.cloudstack.api.command.user.account.ListProjectAccountsCmd; @@ -89,6 +90,7 @@ import org.apache.cloudstack.api.response.SecondaryStorageHeuristicsResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; import org.apache.cloudstack.api.response.SnapshotResponse; import org.apache.cloudstack.api.response.StorageAccessGroupResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; @@ -195,6 +197,8 @@ public interface QueryService { ListResponse searchForServiceOfferings(ListServiceOfferingsCmd cmd); + ListResponse listServiceOfferingCategories(ListServiceOfferingCategoriesCmd cmd); + ListResponse listDataCenters(ListZonesCmd cmd); ListResponse listTemplates(ListTemplatesCmd cmd); diff --git a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingCategoryVO.java b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingCategoryVO.java new file mode 100644 index 000000000000..3fb32c46af42 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingCategoryVO.java @@ -0,0 +1,112 @@ +// 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 com.cloud.service; + +import java.util.UUID; + +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 com.cloud.offering.ServiceOfferingCategory; + +@Entity +@Table(name = "service_offering_category") +public class ServiceOfferingCategoryVO implements ServiceOfferingCategory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "name") + private String name; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "sort_key") + private int sortKey; + + @Column(name = "is_default") + private boolean isDefault; + + public ServiceOfferingCategoryVO() { + this.uuid = UUID.randomUUID().toString(); + } + + public ServiceOfferingCategoryVO(String name) { + this.name = name; + this.uuid = UUID.randomUUID().toString(); + } + + public ServiceOfferingCategoryVO(String name, int sortKey) { + this.name = name; + this.sortKey = sortKey; + this.uuid = UUID.randomUUID().toString(); + } + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public boolean isDefault() { + return isDefault; + } + + public void setDefault(boolean isDefault) { + this.isDefault = isDefault; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + @Override + public int getSortKey() { + return sortKey; + } + + @Override + public void setSortKey(int sortKey) { + this.sortKey = sortKey; + } +} diff --git a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java index cfe8049f5b2c..06307beae029 100644 --- a/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java +++ b/engine/schema/src/main/java/com/cloud/service/ServiceOfferingVO.java @@ -133,6 +133,9 @@ public class ServiceOfferingVO implements ServiceOffering { @Column(name = "gpu_display") private Boolean gpuDisplay; + @Column(name = "category_id") + private Long categoryId; + // This is a delayed load value. If the value is null, // then this field has not been loaded yet. // Call service offering dao to load it. @@ -209,6 +212,7 @@ public ServiceOfferingVO(ServiceOfferingVO offering) { diskOfferingStrictness = offering.diskOfferingStrictness; vgpuProfileId = offering.vgpuProfileId; gpuCount = offering.gpuCount; + categoryId = offering.categoryId; } @Override @@ -482,4 +486,13 @@ public Boolean getGpuDisplay() { public void setGpuDisplay(Boolean gpuDisplay) { this.gpuDisplay = gpuDisplay; } + + @Override + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(long categoryId) { + this.categoryId = categoryId; + } } diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingCategoryDao.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingCategoryDao.java new file mode 100644 index 000000000000..ea1089f5a7fe --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingCategoryDao.java @@ -0,0 +1,31 @@ +// 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 com.cloud.service.dao; + +import java.util.List; + +import com.cloud.service.ServiceOfferingCategoryVO; +import com.cloud.utils.db.GenericDao; + +public interface ServiceOfferingCategoryDao extends GenericDao { + + ServiceOfferingCategoryVO findByName(String name); + + ServiceOfferingCategoryVO findDefault(); + + List listAll(); +} diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingCategoryDaoImpl.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingCategoryDaoImpl.java new file mode 100644 index 000000000000..2dcffe746727 --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingCategoryDaoImpl.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 com.cloud.service.dao; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.cloud.service.ServiceOfferingCategoryVO; +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.utils.db.DB; + +@Component +@DB() +public class ServiceOfferingCategoryDaoImpl extends GenericDaoBase implements ServiceOfferingCategoryDao { + + protected final SearchBuilder NameSearch; + protected final SearchBuilder DefaultSearch; + + protected ServiceOfferingCategoryDaoImpl() { + NameSearch = createSearchBuilder(); + NameSearch.and("name", NameSearch.entity().getName(), SearchCriteria.Op.EQ); + NameSearch.done(); + + DefaultSearch = createSearchBuilder(); + DefaultSearch.and("isDefault", DefaultSearch.entity().isDefault(), SearchCriteria.Op.EQ); + DefaultSearch.done(); + } + + @Override + public ServiceOfferingCategoryVO findByName(String name) { + SearchCriteria sc = NameSearch.create(); + sc.setParameters("name", name); + return findOneBy(sc); + } + + @Override + public ServiceOfferingCategoryVO findDefault() { + SearchCriteria sc = DefaultSearch.create(); + sc.setParameters("isDefault", true); + return findOneBy(sc); + } + + @Override + public List listAll() { + return listAll(null); + } +} diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java index d3bab4fcbe27..1f496b462ed5 100644 --- a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDao.java @@ -59,5 +59,7 @@ List createSystemServiceOfferings(String name, String uniqueN List listIdsByHostTag(String tag); + List listByCategoryId(Long categoryId); + void addCheckForGpuEnabled(SearchBuilder serviceOfferingSearch, Boolean gpuEnabled); } diff --git a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java index f360770ad686..2855336abb30 100644 --- a/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/service/dao/ServiceOfferingDaoImpl.java @@ -29,6 +29,7 @@ import org.springframework.stereotype.Component; import com.cloud.event.UsageEventVO; +import com.cloud.service.ServiceOfferingCategoryVO; import com.cloud.service.ServiceOfferingDetailsVO; import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.Storage.ProvisioningType; @@ -51,11 +52,14 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase UniqueNameSearch; protected final SearchBuilder ServiceOfferingsByKeywordSearch; protected final SearchBuilder PublicCpuRamSearch; protected final SearchBuilder SearchComputeOfferingByComputeOnlyDiskOffering; + protected final SearchBuilder CategoryIdSearch; public ServiceOfferingDaoImpl() { super(); @@ -79,6 +83,33 @@ public ServiceOfferingDaoImpl() { SearchComputeOfferingByComputeOnlyDiskOffering = createSearchBuilder(); SearchComputeOfferingByComputeOnlyDiskOffering.and("disk_offering_id", SearchComputeOfferingByComputeOnlyDiskOffering.entity().getDiskOfferingId(), SearchCriteria.Op.EQ); SearchComputeOfferingByComputeOnlyDiskOffering.done(); + + CategoryIdSearch = createSearchBuilder(); + CategoryIdSearch.and("category_id", CategoryIdSearch.entity().getCategoryId(), SearchCriteria.Op.EQ); + CategoryIdSearch.done(); + } + + @Override + public List listByCategoryId(Long categoryId) { + SearchCriteria sc = CategoryIdSearch.create(); + sc.setParameters("category_id", categoryId); + return listBy(sc); + } + + @Override + public ServiceOfferingVO persist(ServiceOfferingVO offering) { + assignDefaultCategory(offering); + return super.persist(offering); + } + + void assignDefaultCategory(ServiceOfferingVO offering) { + if (offering.getCategoryId() == null) { + ServiceOfferingCategoryVO category = serviceOfferingCategoryDao.findDefault(); + if (category == null) { + throw new CloudRuntimeException("Default service offering category not found"); + } + offering.setCategoryId(category.getId()); + } } @Override diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml index a0dada545617..ecc43ab7c5de 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml @@ -57,6 +57,7 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql index 3365f1f855ba..18c639f2c98e 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42300to2400.sql @@ -239,3 +239,32 @@ CREATE TABLE IF NOT EXISTS `cloud`.`instance_boot_group_details` ( PRIMARY KEY (`id`), CONSTRAINT `fk_instance_boot_group_details__group_id` FOREIGN KEY (`boot_group_id`) REFERENCES `instance_boot_group` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `cloud`.`service_offering_category` ( + `id` bigint unsigned NOT NULL auto_increment, + `name` varchar(255) NOT NULL, + `uuid` varchar(40) NOT NULL, + `sort_key` int NOT NULL DEFAULT 0, + `is_default` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + CONSTRAINT `uc_service_offering_category__uuid` UNIQUE (`uuid`), + CONSTRAINT `uc_service_offering_category__name` UNIQUE (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.service_offering_category', 'is_default', 'tinyint(1) NOT NULL DEFAULT 0'); +-- The previous implementation reserved ID 1 for the default, even if it was renamed. +UPDATE `cloud`.`service_offering_category` AS `legacy` +LEFT JOIN (SELECT `id` FROM `cloud`.`service_offering_category` WHERE `is_default` = 1 LIMIT 1) AS `current_default` ON 1 = 1 +SET `legacy`.`is_default` = 1 +WHERE `legacy`.`id` = 1 AND `current_default`.`id` IS NULL; +INSERT INTO `cloud`.`service_offering_category` (name, uuid, is_default) +SELECT 'Default', UUID(), 1 +WHERE NOT EXISTS (SELECT 1 FROM `cloud`.`service_offering_category` WHERE `is_default` = 1) +ON DUPLICATE KEY UPDATE `is_default` = 1; + +CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.service_offering', 'category_id', 'bigint unsigned DEFAULT NULL'); +UPDATE `cloud`.`service_offering` SET `category_id` = (SELECT `id` FROM `cloud`.`service_offering_category` WHERE `is_default` = 1) +WHERE `category_id` IS NULL; +CALL `cloud`.`IDEMPOTENT_DROP_FOREIGN_KEY`('cloud.service_offering', 'fk_service_offering__category_id'); +ALTER TABLE `cloud`.`service_offering` MODIFY COLUMN `category_id` bigint unsigned NOT NULL; +ALTER TABLE `cloud`.`service_offering` ADD CONSTRAINT `fk_service_offering__category_id` FOREIGN KEY (`category_id`) REFERENCES `cloud`.`service_offering_category` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql b/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql index eb987af3ffb6..933f32c10d9d 100644 --- a/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql +++ b/engine/schema/src/main/resources/META-INF/db/views/cloud.service_offering_view.sql @@ -85,6 +85,9 @@ SELECT `vgpu_profile`.`max_resolution_y` AS `vgpu_profile_max_resolution_y`, `service_offering`.`gpu_count` AS `gpu_count`, `service_offering`.`gpu_display` AS `gpu_display`, + `service_offering`.`category_id` AS `category_id`, + `service_offering_category`.`uuid` AS `category_uuid`, + `service_offering_category`.`name` AS `category_name`, GROUP_CONCAT(DISTINCT(domain.id)) AS domain_id, GROUP_CONCAT(DISTINCT(domain.uuid)) AS domain_uuid, GROUP_CONCAT(DISTINCT(domain.name)) AS domain_name, @@ -105,6 +108,8 @@ FROM LEFT JOIN `cloud`.`gpu_card` ON vgpu_profile.card_id = gpu_card.id LEFT JOIN + `cloud`.`service_offering_category` ON service_offering.category_id = service_offering_category.id + LEFT JOIN `cloud`.`service_offering_details` AS `domain_details` ON `domain_details`.`service_offering_id` = `service_offering`.`id` AND `domain_details`.`name`='domainid' LEFT JOIN `cloud`.`domain` AS `domain` ON FIND_IN_SET(`domain`.`id`, `domain_details`.`value`) diff --git a/engine/schema/src/test/java/com/cloud/service/dao/ServiceOfferingDaoImplTest.java b/engine/schema/src/test/java/com/cloud/service/dao/ServiceOfferingDaoImplTest.java new file mode 100644 index 000000000000..c06642420ab3 --- /dev/null +++ b/engine/schema/src/test/java/com/cloud/service/dao/ServiceOfferingDaoImplTest.java @@ -0,0 +1,76 @@ +// 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 com.cloud.service.dao; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import com.cloud.service.ServiceOfferingCategoryVO; +import com.cloud.service.ServiceOfferingVO; +import com.cloud.utils.exception.CloudRuntimeException; + +@RunWith(MockitoJUnitRunner.class) +public class ServiceOfferingDaoImplTest { + @Mock + private ServiceOfferingCategoryDao categoryDao; + + @InjectMocks + private ServiceOfferingDaoImpl offeringDao = new ServiceOfferingDaoImpl(); + + private ServiceOfferingVO newOffering() { + return new ServiceOfferingVO("test", 1, 512, 500, null, null, false, "test", false, null, false); + } + + @Test + public void assignsFlaggedDefaultCategory() { + ServiceOfferingCategoryVO category = new ServiceOfferingCategoryVO("Renamed"); + category.setId(42L); + category.setDefault(true); + when(categoryDao.findDefault()).thenReturn(category); + ServiceOfferingVO offering = newOffering(); + + offeringDao.assignDefaultCategory(offering); + + Assert.assertEquals(Long.valueOf(42L), offering.getCategoryId()); + } + + @Test + public void preservesExplicitCategory() { + ServiceOfferingVO offering = newOffering(); + offering.setCategoryId(7L); + + offeringDao.assignDefaultCategory(offering); + + Assert.assertEquals(Long.valueOf(7L), offering.getCategoryId()); + verify(categoryDao, never()).findDefault(); + } + + @Test + public void rejectsMissingDefaultCategory() { + when(categoryDao.findDefault()).thenReturn(null); + + Assert.assertThrows(CloudRuntimeException.class, () -> offeringDao.assignDefaultCategory(newOffering())); + } +} diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index 345f7e5f628e..d797c744346b 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -162,6 +162,7 @@ import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.SecurityGroupRuleResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; import org.apache.cloudstack.api.response.ServiceResponse; import org.apache.cloudstack.api.response.SharedFSResponse; import org.apache.cloudstack.api.response.Site2SiteCustomerGatewayResponse; @@ -384,6 +385,7 @@ import com.cloud.offering.NetworkOffering; import com.cloud.offering.NetworkOffering.Detail; import com.cloud.offering.ServiceOffering; +import com.cloud.offering.ServiceOfferingCategory; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.org.Cluster; @@ -689,6 +691,16 @@ public ServiceOfferingResponse createServiceOfferingResponse(ServiceOffering off return ApiDBUtils.newServiceOfferingResponse(vOffering); } + @Override + public ServiceOfferingCategoryResponse createServiceOfferingCategoryResponse(ServiceOfferingCategory category) { + ServiceOfferingCategoryResponse response = new ServiceOfferingCategoryResponse(); + response.setId(category.getUuid()); + response.setName(category.getName()); + response.setSortKey(category.getSortKey()); + response.setObjectName("serviceofferingcategory"); + return response; + } + @Override public ConfigurationResponse createConfigurationResponse(Configuration cfg) { ConfigurationResponse cfgResponse = new ConfigurationResponse(); diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index 12ee7fbf35af..dc0f0972160d 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -77,6 +77,7 @@ import org.apache.cloudstack.api.command.admin.internallb.ListInternalLBVMsCmd; import org.apache.cloudstack.api.command.admin.iso.ListIsosCmdByAdmin; import org.apache.cloudstack.api.command.admin.management.ListMgmtsCmd; +import org.apache.cloudstack.api.command.admin.offering.ListServiceOfferingCategoriesCmd; import org.apache.cloudstack.api.command.admin.pod.ListPodsByCmd; import org.apache.cloudstack.api.command.admin.resource.icon.ListResourceIconCmd; import org.apache.cloudstack.api.command.admin.router.GetRouterHealthCheckResultsCmd; @@ -148,6 +149,7 @@ import org.apache.cloudstack.api.response.SecondaryStorageHeuristicsResponse; import org.apache.cloudstack.api.response.SecurityGroupResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; +import org.apache.cloudstack.api.response.ServiceOfferingCategoryResponse; import org.apache.cloudstack.api.response.SnapshotResponse; import org.apache.cloudstack.api.response.StorageAccessGroupResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; @@ -309,8 +311,10 @@ import com.cloud.server.ResourceMetaDataService; import com.cloud.server.ResourceTag; import com.cloud.server.ResourceTag.ResourceObjectType; +import com.cloud.service.ServiceOfferingCategoryVO; import com.cloud.service.ServiceOfferingDetailsVO; import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingCategoryDao; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.BucketVO; @@ -493,6 +497,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject ServiceOfferingDetailsDao _srvOfferingDetailsDao; + @Inject + ServiceOfferingCategoryDao _serviceOfferingCategoryDao; + @Inject DiskOfferingDao _diskOfferingDao; @@ -4167,6 +4174,7 @@ private Pair, Integer> searchForServiceOfferingIdsAndCount(ListServic ServiceOffering.State state = cmd.getState(); final Long vgpuProfileId = cmd.getVgpuProfileId(); final Boolean gpuEnabled = cmd.getGpuEnabled(); + Long categoryId = cmd.getCategoryId(); final Account owner = accountMgr.finalizeOwner(caller, accountName, domainId, projectId); @@ -4214,6 +4222,10 @@ private Pair, Integer> searchForServiceOfferingIdsAndCount(ListServic _srvOfferingDao.addCheckForGpuEnabled(serviceOfferingSearch, gpuEnabled); } + if (categoryId != null) { + serviceOfferingSearch.and("categoryId", serviceOfferingSearch.entity().getCategoryId(), Op.EQ); + } + if (vmId != null) { currentVmOffering = _srvOfferingDao.findByIdIncludingRemoved(vmInstance.getId(), vmInstance.getServiceOfferingId()); diskOffering = _diskOfferingDao.findByIdIncludingRemoved(currentVmOffering.getDiskOfferingId()); @@ -4507,6 +4519,10 @@ private Pair, Integer> searchForServiceOfferingIdsAndCount(ListServic sc.setParameters("vgpuProfileId", vgpuProfileId); } + if (categoryId != null) { + sc.setParameters("categoryId", categoryId); + } + if (vmId != null) { if (!currentVmOffering.isDynamic()) { sc.setParameters("idNEQ", currentVmOffering.getId()); @@ -6583,6 +6599,46 @@ private List searchForBucketsInternal(ListBucketsCmd cmd) { return bucketDao.searchByIds(bktIds); } + @Override + public ListResponse listServiceOfferingCategories(ListServiceOfferingCategoriesCmd cmd) { + Long id = cmd.getId(); + String name = cmd.getName(); + + Filter searchFilter = new Filter(ServiceOfferingCategoryVO.class, "sortKey", true, cmd.getStartIndex(), cmd.getPageSizeVal()); + SearchBuilder sb = _serviceOfferingCategoryDao.createSearchBuilder(); + + if (id != null) { + sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ); + } + + if (name != null) { + sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + } + + sb.done(); + SearchCriteria sc = sb.create(); + + if (id != null) { + sc.setParameters("id", id); + } + + if (name != null) { + sc.setParameters("name", name); + } + + Pair, Integer> result = _serviceOfferingCategoryDao.searchAndCount(sc, searchFilter); + ListResponse response = new ListResponse<>(); + List responses = new ArrayList<>(); + + for (ServiceOfferingCategoryVO category : result.first()) { + ServiceOfferingCategoryResponse categoryResponse = responseGenerator.createServiceOfferingCategoryResponse(category); + responses.add(categoryResponse); + } + + response.setResponses(responses, result.second()); + return response; + } + @Override public ListResponse listBackupServiceJobs(ListBackupServiceJobsCmd cmd) { ListResponse responses = new ListResponse<>(); diff --git a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java index 579425a68c13..2e4ef94bc037 100644 --- a/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/ServiceOfferingJoinDaoImpl.java @@ -147,6 +147,13 @@ public ServiceOfferingResponse newServiceOfferingResponse(ServiceOfferingJoinVO offeringResponse.setMaxResolutionY(offering.getMaxResolutionY()); offeringResponse.setGpuCount(offering.getGpuCount()); offeringResponse.setGpuDisplay(offering.getGpuDisplay()); + + // Set category information if available (joined via service_offering_view) + if (offering.getCategoryUuid() != null) { + offeringResponse.setCategoryId(offering.getCategoryUuid()); + offeringResponse.setCategoryName(offering.getCategoryName()); + } + offeringResponse.setNetworkRate(offering.getRateMbps()); offeringResponse.setHostTag(offering.getHostTag()); offeringResponse.setDeploymentPlanner(offering.getDeploymentPlanner()); diff --git a/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java index 1d86d14cf63a..906c87f182a9 100644 --- a/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java +++ b/server/src/main/java/com/cloud/api/query/vo/ServiceOfferingJoinVO.java @@ -263,6 +263,15 @@ public class ServiceOfferingJoinVO extends BaseViewVO implements InternalIdentit @Column(name = "gpu_display") private Boolean gpuDisplay; + @Column(name = "category_id") + private Long categoryId; + + @Column(name = "category_uuid") + private String categoryUuid; + + @Column(name = "category_name") + private String categoryName; + public ServiceOfferingJoinVO() { } @@ -557,4 +566,10 @@ public Integer getGpuCount() { public Boolean getGpuDisplay() { return gpuDisplay; } + + public Long getCategoryId() { return categoryId; } + + public String getCategoryUuid() { return categoryUuid; } + + public String getCategoryName() { return categoryName; } } diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index 17dbced54233..6afc7699f023 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -82,11 +82,14 @@ import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.IsAccountAllowedToCreateOfferingsWithTagsCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.pod.DeletePodCmd; import org.apache.cloudstack.api.command.admin.pod.UpdatePodCmd; import org.apache.cloudstack.api.command.admin.region.CreatePortableIpRangeCmd; @@ -267,6 +270,7 @@ import com.cloud.offering.NetworkOffering.Availability; import com.cloud.offering.NetworkOffering.Detail; import com.cloud.offering.ServiceOffering; +import com.cloud.offering.ServiceOfferingCategory; import com.cloud.offerings.NetworkOfferingDetailsVO; import com.cloud.offerings.NetworkOfferingServiceMapVO; import com.cloud.offerings.NetworkOfferingVO; @@ -281,6 +285,8 @@ import com.cloud.server.ManagementService; import com.cloud.service.ServiceOfferingDetailsVO; import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.ServiceOfferingCategoryVO; +import com.cloud.service.dao.ServiceOfferingCategoryDao; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.DiskOfferingVO; @@ -377,6 +383,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati @Inject ServiceOfferingDetailsDao _serviceOfferingDetailsDao; @Inject + ServiceOfferingCategoryDao _serviceOfferingCategoryDao; + @Inject DiskOfferingDao _diskOfferingDao; @Inject DiskOfferingDetailsDao diskOfferingDetailsDao; @@ -3538,6 +3546,12 @@ public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) } } + // validate optional category id + final Long serviceOfferingCategoryId = cmd.getCategoryId(); + if (serviceOfferingCategoryId != null && _serviceOfferingCategoryDao.findById(serviceOfferingCategoryId) == null) { + throw new InvalidParameterValueException("Please specify a valid service offering category id"); + } + // validate lease properties and set leaseExpiryAction Integer leaseDuration = cmd.getLeaseDuration(); VMLeaseManager.ExpiryAction leaseExpiryAction = validateAndGetLeaseExpiryAction(leaseDuration, cmd.getLeaseExpiryAction()); @@ -3553,7 +3567,7 @@ public ServiceOffering createServiceOffering(final CreateServiceOfferingCmd cmd) cmd.getIopsReadRate(), cmd.getIopsReadRateMax(), cmd.getIopsReadRateMaxLength(), cmd.getIopsWriteRate(), cmd.getIopsWriteRateMax(), cmd.getIopsWriteRateMaxLength(), cmd.getHypervisorSnapshotReserve(), cmd.getCacheMode(), storagePolicyId, cmd.getDynamicScalingEnabled(), diskOfferingId, - cmd.getDiskOfferingStrictness(), cmd.isCustomized(), cmd.getEncryptRoot(), vgpuProfileId, gpuCount, cmd.getGpuDisplay(), cmd.isPurgeResources(), leaseDuration, leaseExpiryAction); + cmd.getDiskOfferingStrictness(), cmd.isCustomized(), cmd.getEncryptRoot(), vgpuProfileId, gpuCount, cmd.getGpuDisplay(), cmd.isPurgeResources(), leaseDuration, leaseExpiryAction, serviceOfferingCategoryId); } private Integer validateVgpuProfileAndGetGpuCount(final Long vgpuProfileId, Integer gpuCount) { @@ -3583,7 +3597,7 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole Long iopsWriteRate, Long iopsWriteRateMax, Long iopsWriteRateMaxLength, final Integer hypervisorSnapshotReserve, String cacheMode, final Long storagePolicyID, final boolean dynamicScalingEnabled, final Long diskOfferingId, final boolean diskOfferingStrictness, - final boolean isCustomized, final boolean encryptRoot, Long vgpuProfileId, Integer gpuCount, Boolean gpuDisplay, final boolean purgeResources, Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction) { + final boolean isCustomized, final boolean encryptRoot, Long vgpuProfileId, Integer gpuCount, Boolean gpuDisplay, final boolean purgeResources, Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction, final Long categoryId) { // Filter child domains when both parent and child domains are present List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); @@ -3668,6 +3682,10 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole serviceOffering.setVgpuProfileId(vgpuProfileId); serviceOffering.setGpuCount(gpuCount); serviceOffering.setGpuDisplay(gpuDisplay); + // Set category if provided (categoryId was validated in caller) + if (categoryId != null) { + serviceOffering.setCategoryId(categoryId); + } DiskOfferingVO diskOffering = null; if (diskOfferingId == null) { @@ -3967,6 +3985,13 @@ public ServiceOffering cloneServiceOffering(final CloneServiceOfferingCmd cmd) { final Long diskOfferingId = getOrDefault(cmd.getDiskOfferingId(), sourceOffering.getDiskOfferingId()); + // validate the overridden category id, otherwise inherit the source offering's category + final Long cmdCategoryId = cmd.getCategoryId(); + if (cmdCategoryId != null && _serviceOfferingCategoryDao.findById(cmdCategoryId) == null) { + throw new InvalidParameterValueException("Please specify a valid service offering category id"); + } + final Long categoryId = getOrDefault(cmdCategoryId, sourceOffering.getCategoryId()); + return createServiceOffering(userId, systemUse, vmType, name, cpuNumber, memory, cpuSpeed, displayText, provisioningType, localStorageRequired, offerHa, limitCpuUse, isVolatile, tags, domainIds, zoneIds, hostTag, networkRate, @@ -3978,7 +4003,7 @@ public ServiceOffering cloneServiceOffering(final CloneServiceOfferingCmd cmd) { diskParams.iopsWriteRate, diskParams.iopsWriteRateMax, diskParams.iopsWriteRateMaxLength, diskParams.hypervisorSnapshotReserve, diskParams.cacheMode, customParams.storagePolicy, dynamicScalingEnabled, diskOfferingId, diskOfferingStrictness, isCustomized, encryptRoot, - vgpuProfileId, finalGpuCount, gpuDisplay, purgeResources, leaseParams.leaseDuration, leaseParams.leaseExpiryAction); + vgpuProfileId, finalGpuCount, gpuDisplay, purgeResources, leaseParams.leaseDuration, leaseParams.leaseExpiryAction, categoryId); } private ServiceOfferingVO getAndValidateSourceOffering(Long sourceOfferingId) { @@ -4380,11 +4405,17 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) boolean purgeResources = cmd.isPurgeResources(); final Map externalDetails = cmd.getExternalDetails(); final boolean cleanupExternalDetails = cmd.isCleanupExternalDetails(); + final Long categoryId = cmd.getCategoryId(); if (userId == null) { userId = Long.valueOf(User.UID_SYSTEM); } + // Validate category if provided + if (categoryId != null && _serviceOfferingCategoryDao.findById(categoryId) == null) { + throw new InvalidParameterValueException("Please specify a valid service offering category id"); + } + // Verify input parameters final ServiceOffering offeringHandle = _entityMgr.findById(ServiceOffering.class, id); if (offeringHandle == null) { @@ -4477,7 +4508,7 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) throw new InvalidParameterValueException(String.format("Unable to update service offering: %s by id user: %s because it is not root-admin or domain-admin", offeringHandle, user)); } - final boolean updateNeeded = name != null || displayText != null || sortKey != null || storageTags != null || hostTags != null || state != null; + final boolean updateNeeded = name != null || displayText != null || sortKey != null || storageTags != null || hostTags != null || state != null || categoryId != null; final boolean serviceOfferingExternalDetailsNeedUpdate = serviceOfferingExternalDetailsNeedUpdate(offeringDetails, externalDetails, cleanupExternalDetails); final boolean detailsUpdateNeeded = !filteredDomainIds.equals(existingDomainIds) || @@ -4505,6 +4536,10 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) offering.setState(state); } + if (categoryId != null) { + offering.setCategoryId(categoryId); + } + DiskOfferingVO diskOffering = _diskOfferingDao.findById(offeringHandle.getDiskOfferingId()); updateOfferingTagsIfIsNotNull(storageTags, diskOffering); @@ -9564,4 +9599,96 @@ public void setScope(String scope) { this.scope = scope; } } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_CATEGORY_CREATE, eventDescription = "creating service offering category") + public ServiceOfferingCategory createServiceOfferingCategory(CreateServiceOfferingCategoryCmd cmd) { + String name = cmd.getName(); + Integer sortKey = cmd.getSortKey(); + + // Check if category with same name already exists + ServiceOfferingCategoryVO existingCategory = _serviceOfferingCategoryDao.findByName(name); + if (existingCategory != null) { + throw new InvalidParameterValueException("Service offering category with name " + name + " already exists"); + } + + ServiceOfferingCategoryVO category = new ServiceOfferingCategoryVO(name); + if (sortKey != null) { + category.setSortKey(sortKey); + } + + category = _serviceOfferingCategoryDao.persist(category); + CallContext.current().setEventResourceId(category.getId()); + CallContext.current().setEventDetails("Service offering category id=" + category.getId()); + return category; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_CATEGORY_DELETE, eventDescription = "deleting service offering category") + public boolean deleteServiceOfferingCategory(DeleteServiceOfferingCategoryCmd cmd) { + Long categoryId = cmd.getId(); + + ServiceOfferingCategoryVO category = _serviceOfferingCategoryDao.findById(categoryId); + if (category == null) { + throw new InvalidParameterValueException("Unable to find service offering category with id " + categoryId); + } + + if (category.isDefault()) { + throw new InvalidParameterValueException("Cannot delete the default service offering category"); + } + + // Check if any service offering is using this category + List offeringsUsingCategory = _serviceOfferingDao.listByCategoryId(categoryId); + if (offeringsUsingCategory != null && !offeringsUsingCategory.isEmpty()) { + throw new InvalidParameterValueException("Cannot delete service offering category with id " + categoryId + + " because it is in use by one or more service offerings."); + } + boolean result = _serviceOfferingCategoryDao.remove(categoryId); + if (result) { + CallContext.current().setEventDetails("Service offering category id=" + categoryId); + } + return result; + } + + @Override + @ActionEvent(eventType = EventTypes.EVENT_SERVICE_OFFERING_CATEGORY_EDIT, eventDescription = "updating service offering category") + public ServiceOfferingCategory updateServiceOfferingCategory(UpdateServiceOfferingCategoryCmd cmd) { + Long categoryId = cmd.getId(); + String name = cmd.getName(); + Integer sortKey = cmd.getSortKey(); + + // Validate category exists + ServiceOfferingCategoryVO category = _serviceOfferingCategoryDao.findById(categoryId); + if (category == null) { + throw new InvalidParameterValueException("Unable to find service offering category with id " + categoryId); + } + + // Check if at least one parameter is being updated + if (name == null && sortKey == null) { + throw new InvalidParameterValueException("Please specify at least one parameter to update (name or sortKey)"); + } + + // If name is being updated, check for duplicates + if (name != null && !name.equals(category.getName())) { + ServiceOfferingCategoryVO existingCategory = _serviceOfferingCategoryDao.findByName(name); + if (existingCategory != null) { + throw new InvalidParameterValueException("A service offering category with name '" + name + "' already exists"); + } + category.setName(name); + } + + // Update sort key if provided + if (sortKey != null) { + category.setSortKey(sortKey); + } + + // Persist changes + boolean updated = _serviceOfferingCategoryDao.update(categoryId, category); + if (!updated) { + throw new CloudRuntimeException("Failed to update service offering category"); + } + + CallContext.current().setEventDetails("Service offering category id=" + categoryId); + return _serviceOfferingCategoryDao.findById(categoryId); + } } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index e48cf112806a..e8b6d2380cd1 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -168,11 +168,15 @@ import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.IsAccountAllowedToCreateOfferingsWithTagsCmd; +import org.apache.cloudstack.api.command.admin.offering.ListServiceOfferingCategoriesCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.outofbandmanagement.ChangeOutOfBandManagementPasswordCmd; import org.apache.cloudstack.api.command.admin.outofbandmanagement.ConfigureOutOfBandManagementCmd; import org.apache.cloudstack.api.command.admin.outofbandmanagement.DisableOutOfBandManagementForClusterCmd; @@ -3923,6 +3927,10 @@ public List> getCommands() { cmdList.add(IsAccountAllowedToCreateOfferingsWithTagsCmd.class); cmdList.add(UpdateDiskOfferingCmd.class); cmdList.add(UpdateServiceOfferingCmd.class); + cmdList.add(CreateServiceOfferingCategoryCmd.class); + cmdList.add(ListServiceOfferingCategoriesCmd.class); + cmdList.add(UpdateServiceOfferingCategoryCmd.class); + cmdList.add(DeleteServiceOfferingCategoryCmd.class); cmdList.add(CreatePodCmd.class); cmdList.add(DeletePodCmd.class); cmdList.add(ListPodsByCmd.class); diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java index 67d43bf6933b..9377325fbeae 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerCloneIntegrationTest.java @@ -30,6 +30,8 @@ import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.ServiceOfferingCategoryVO; +import com.cloud.service.dao.ServiceOfferingCategoryDao; import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.service.dao.ServiceOfferingDetailsDao; import com.cloud.storage.DiskOfferingVO; @@ -95,6 +97,9 @@ public class ConfigurationManagerCloneIntegrationTest { @Mock private ServiceOfferingDetailsDao serviceOfferingDetailsDao; + @Mock + private ServiceOfferingCategoryDao serviceOfferingCategoryDao; + @Mock private DiskOfferingDao diskOfferingDao; @@ -167,6 +172,9 @@ public void setUp() { anyInt(), anyString(), any(), anyLong(), anyBoolean(), anyBoolean()); + Mockito.lenient().when(serviceOfferingCategoryDao.findById(anyLong())) + .thenReturn(mock(ServiceOfferingCategoryVO.class)); + // User/Account DAO stubs used by createDiskOffering Mockito.lenient().when(userDao.findById(anyLong())).thenReturn(userVO); Mockito.lenient().when(userVO.getAccountId()).thenReturn(1L); @@ -198,6 +206,7 @@ public void testCloneServiceOfferingInheritsAllPropertiesFromSource() { ServiceOfferingVO sourceOffering = mock(ServiceOfferingVO.class); when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getCategoryId()).thenReturn(42L); when(sourceOffering.getName()).thenReturn("source-offering"); when(sourceOffering.getDisplayText()).thenReturn("Source Display Text"); when(sourceOffering.getCpu()).thenReturn(2); @@ -251,7 +260,7 @@ public void testCloneServiceOfferingInheritsAllPropertiesFromSource() { anyString(), anyMap(), anyLong(), any(Boolean.class), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), any(Integer.class), anyString(), anyLong(), anyBoolean(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), - anyLong(), any(Integer.class), any(Boolean.class), anyBoolean(), any(Integer.class), any(VMLeaseManager.ExpiryAction.class) + anyLong(), any(Integer.class), any(Boolean.class), anyBoolean(), any(Integer.class), any(VMLeaseManager.ExpiryAction.class), anyLong() ); ServiceOffering result = configurationManager.cloneServiceOffering(cmd); @@ -270,6 +279,7 @@ public void testCloneServiceOfferingOverridesProvidedParameters() { ServiceOfferingVO sourceOffering = mock(ServiceOfferingVO.class); when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getCategoryId()).thenReturn(42L); when(sourceOffering.getName()).thenReturn("source-offering"); when(sourceOffering.getDisplayText()).thenReturn("Source Display Text"); when(sourceOffering.getCpu()).thenReturn(2); @@ -328,7 +338,7 @@ public void testCloneServiceOfferingOverridesProvidedParameters() { anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyInt(), anyString(), anyLong(), anyBoolean(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), anyLong(), anyInt(), anyBoolean(), - anyBoolean(), anyInt(), any()); + anyBoolean(), anyInt(), any(), anyLong()); ServiceOffering result = configurationManager.cloneServiceOffering(cmd); @@ -341,6 +351,7 @@ public void testCloneServiceOfferingOverridesProvidedParameters() { Assert.assertEquals("Cloned offering should override HA", Boolean.FALSE, result.isOfferHA()); } + @Test(expected = InvalidParameterValueException.class) public void testCloneDiskOfferingFailsWhenSourceNotFound() { CloneDiskOfferingCmd cmd = mock(CloneDiskOfferingCmd.class); @@ -520,6 +531,7 @@ public void testCloneServiceOfferingCanInheritDetailsFromSource() { ServiceOfferingVO sourceOffering = mock(ServiceOfferingVO.class); when(sourceOffering.getId()).thenReturn(sourceId); + when(sourceOffering.getCategoryId()).thenReturn(42L); when(sourceOffering.getCpu()).thenReturn(2); when(sourceOffering.getSpeed()).thenReturn(1000); when(sourceOffering.getRamSize()).thenReturn(2048); @@ -553,7 +565,7 @@ public void testCloneServiceOfferingCanInheritDetailsFromSource() { anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyLong(), anyInt(), anyString(), anyLong(), anyBoolean(), anyLong(), anyBoolean(), anyBoolean(), anyBoolean(), anyLong(), anyInt(), anyBoolean(), - anyBoolean(), anyInt(), any()); + anyBoolean(), anyInt(), any(), anyLong()); ServiceOffering result = configurationManager.cloneServiceOffering(cmd); diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index 9a0b150780e4..6e6022467b15 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -40,9 +40,13 @@ import com.cloud.network.element.NsxProviderVO; import com.cloud.offering.DiskOffering; import com.cloud.offering.NetworkOffering; +import com.cloud.offering.ServiceOfferingCategory; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDao; +import com.cloud.service.ServiceOfferingCategoryVO; import com.cloud.service.ServiceOfferingVO; +import com.cloud.service.dao.ServiceOfferingCategoryDao; +import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.StorageManager; import com.cloud.storage.dao.VMTemplateZoneDao; @@ -62,7 +66,10 @@ import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.command.admin.config.ResetCfgCmd; import org.apache.cloudstack.api.command.admin.network.CreateNetworkOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCategoryCmd; +import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.zone.DeleteZoneCmd; import org.apache.cloudstack.config.Configuration; import org.apache.cloudstack.context.CallContext; @@ -145,6 +152,10 @@ public class ConfigurationManagerImplTest { @Mock UpdateDiskOfferingCmd updateDiskOfferingCmdMock; @Mock + ServiceOfferingCategoryDao serviceOfferingCategoryDaoMock; + @Mock + ServiceOfferingDao serviceOfferingDaoMock; + @Mock NsxProviderDao nsxProviderDao; @Mock NetrisProviderDao netrisProviderDao; @@ -263,7 +274,6 @@ public void validateIfStringValueIsInRangeTestInvalidValuesReturnString() { } } - @Test public void validateIfStringValueIsInRangeTestMultipleRangesValidValueReturnNull() { Mockito.doReturn("returnMsg1").when(configurationManagerImplSpy).validateRangePrivateIp(Mockito.anyString(), Mockito.anyString()); @@ -281,7 +291,6 @@ public void validateIfStringValueIsInRangeTestMultipleRangesInvalidValueReturnMe Assert.assertEquals("The provided value is neither returnMsg1 NOR returnMsg2 NOR returnMsg3.", testVariable); } - @Test public void validateRangePrivateIpTestValidValueReturnNull() { try (MockedStatic ignored = Mockito.mockStatic(NetUtils.class)) { @@ -723,7 +732,6 @@ public void validateValueTypeTestReturnsFalseWhenValueIsStringAndTypeIsBoolean() @Test public void validateValueTypeTestReturnsTrueWhenValueIsTrueAndTypeIsBoolean() { Assert.assertTrue(configurationManagerImplSpy.validateValueType("true", Boolean.class)); - } @Test @@ -954,7 +962,6 @@ public void testValidateConfigurationAllowedOnlyForDefaultAdmin_withValidConfigN } } - @Test public void getConfigurationTypeWrapperClassTestReturnsConfigType() { Config configuration = Config.AlertEmailAddresses; @@ -1410,4 +1417,131 @@ public void testGetExternalNetworkProviderReturnsNullWhenNoExternalProviders() { mapWithEmptySet.put(Network.Service.Firewall, Collections.emptySet()); Assert.assertNull(ConfigurationManagerImpl.getExternalNetworkProvider(null, mapWithEmptySet)); } + + @Test + public void testCreateServiceOfferingCategoryWithDuplicateNameThrowsException() { + CreateServiceOfferingCategoryCmd cmd = mock(CreateServiceOfferingCategoryCmd.class); + when(cmd.getName()).thenReturn("Production"); + when(serviceOfferingCategoryDaoMock.findByName("Production")).thenReturn(new ServiceOfferingCategoryVO("Production")); + + Assert.assertThrows(InvalidParameterValueException.class, () -> + configurationManagerImplSpy.createServiceOfferingCategory(cmd)); + } + + @Test + public void testCreateServiceOfferingCategoryPersistsCategoryWithSortKey() { + CreateServiceOfferingCategoryCmd cmd = mock(CreateServiceOfferingCategoryCmd.class); + when(cmd.getName()).thenReturn("Production"); + when(cmd.getSortKey()).thenReturn(5); + when(serviceOfferingCategoryDaoMock.findByName("Production")).thenReturn(null); + ServiceOfferingCategoryVO persisted = new ServiceOfferingCategoryVO("Production", 5); + when(serviceOfferingCategoryDaoMock.persist(any(ServiceOfferingCategoryVO.class))).thenReturn(persisted); + + CallContext callContext = mock(CallContext.class); + try (MockedStatic ignored = Mockito.mockStatic(CallContext.class)) { + when(CallContext.current()).thenReturn(callContext); + + ServiceOfferingCategory result = configurationManagerImplSpy.createServiceOfferingCategory(cmd); + + Assert.assertEquals(persisted, result); + Assert.assertEquals("Production", result.getName()); + Assert.assertEquals(5, result.getSortKey()); + Mockito.verify(callContext).setEventResourceId(persisted.getId()); + } + } + + @Test + public void testDeleteServiceOfferingCategoryWithUnknownIdThrowsException() { + DeleteServiceOfferingCategoryCmd cmd = mock(DeleteServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(42L); + when(serviceOfferingCategoryDaoMock.findById(42L)).thenReturn(null); + + Assert.assertThrows(InvalidParameterValueException.class, () -> + configurationManagerImplSpy.deleteServiceOfferingCategory(cmd)); + } + + @Test + public void testDeleteDefaultServiceOfferingCategoryThrowsException() { + DeleteServiceOfferingCategoryCmd cmd = mock(DeleteServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(42L); + ServiceOfferingCategoryVO category = new ServiceOfferingCategoryVO("Renamed"); + category.setDefault(true); + when(serviceOfferingCategoryDaoMock.findById(42L)).thenReturn(category); + + Assert.assertThrows(InvalidParameterValueException.class, () -> + configurationManagerImplSpy.deleteServiceOfferingCategory(cmd)); + } + + @Test + public void testDeleteServiceOfferingCategoryInUseThrowsException() { + DeleteServiceOfferingCategoryCmd cmd = mock(DeleteServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(2L); + when(serviceOfferingCategoryDaoMock.findById(2L)).thenReturn(new ServiceOfferingCategoryVO("Production")); + when(serviceOfferingDaoMock.listByCategoryId(2L)).thenReturn(List.of(mock(ServiceOfferingVO.class))); + + Assert.assertThrows(InvalidParameterValueException.class, () -> + configurationManagerImplSpy.deleteServiceOfferingCategory(cmd)); + } + + @Test + public void testDeleteUnusedServiceOfferingCategorySucceeds() { + DeleteServiceOfferingCategoryCmd cmd = mock(DeleteServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(1L); + when(serviceOfferingCategoryDaoMock.findById(1L)).thenReturn(new ServiceOfferingCategoryVO("Production")); + when(serviceOfferingDaoMock.listByCategoryId(1L)).thenReturn(Collections.emptyList()); + when(serviceOfferingCategoryDaoMock.remove(1L)).thenReturn(true); + + CallContext callContext = mock(CallContext.class); + try (MockedStatic ignored = Mockito.mockStatic(CallContext.class)) { + when(CallContext.current()).thenReturn(callContext); + + Assert.assertTrue(configurationManagerImplSpy.deleteServiceOfferingCategory(cmd)); + } + } + + @Test + public void testUpdateServiceOfferingCategoryWithUnknownIdThrowsException() { + UpdateServiceOfferingCategoryCmd cmd = mock(UpdateServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(42L); + when(serviceOfferingCategoryDaoMock.findById(42L)).thenReturn(null); + + Assert.assertThrows(InvalidParameterValueException.class, () -> + configurationManagerImplSpy.updateServiceOfferingCategory(cmd)); + } + + @Test + public void testUpdateServiceOfferingCategoryWithoutParametersThrowsException() { + UpdateServiceOfferingCategoryCmd cmd = mock(UpdateServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(2L); + when(cmd.getName()).thenReturn(null); + when(cmd.getSortKey()).thenReturn(null); + when(serviceOfferingCategoryDaoMock.findById(2L)).thenReturn(new ServiceOfferingCategoryVO("Production")); + + Assert.assertThrows(InvalidParameterValueException.class, () -> + configurationManagerImplSpy.updateServiceOfferingCategory(cmd)); + } + + @Test + public void testUpdateServiceOfferingCategoryWithDuplicateNameThrowsException() { + UpdateServiceOfferingCategoryCmd cmd = mock(UpdateServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(2L); + when(cmd.getName()).thenReturn("Staging"); + when(serviceOfferingCategoryDaoMock.findById(2L)).thenReturn(new ServiceOfferingCategoryVO("Production")); + when(serviceOfferingCategoryDaoMock.findByName("Staging")).thenReturn(new ServiceOfferingCategoryVO("Staging")); + + Assert.assertThrows(InvalidParameterValueException.class, () -> + configurationManagerImplSpy.updateServiceOfferingCategory(cmd)); + } + + @Test + public void testUpdateServiceOfferingCategoryFailureThrowsException() { + UpdateServiceOfferingCategoryCmd cmd = mock(UpdateServiceOfferingCategoryCmd.class); + when(cmd.getId()).thenReturn(2L); + when(cmd.getSortKey()).thenReturn(3); + when(serviceOfferingCategoryDaoMock.findById(2L)).thenReturn(new ServiceOfferingCategoryVO("Production")); + when(serviceOfferingCategoryDaoMock.update(anyLong(), any(ServiceOfferingCategoryVO.class))).thenReturn(false); + + Assert.assertThrows(CloudRuntimeException.class, () -> + configurationManagerImplSpy.updateServiceOfferingCategory(cmd)); + } } diff --git a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java index a8d3927f910e..beaa4e46eb7d 100644 --- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java @@ -42,6 +42,7 @@ import com.cloud.offering.NetworkOffering; import com.cloud.offering.NetworkOffering.Availability; import com.cloud.offering.ServiceOffering; +import com.cloud.offering.ServiceOfferingCategory; import com.cloud.offerings.NetworkOfferingVO; import com.cloud.offerings.dao.NetworkOfferingDaoImpl; import com.cloud.org.Grouping.AllocationState; @@ -65,11 +66,14 @@ import org.apache.cloudstack.api.command.admin.offering.CloneServiceOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.CreateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.DeleteServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.offering.IsAccountAllowedToCreateOfferingsWithTagsCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateDiskOfferingCmd; import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCmd; +import org.apache.cloudstack.api.command.admin.offering.UpdateServiceOfferingCategoryCmd; import org.apache.cloudstack.api.command.admin.pod.DeletePodCmd; import org.apache.cloudstack.api.command.admin.pod.UpdatePodCmd; import org.apache.cloudstack.api.command.admin.region.CreatePortableIpRangeCmd; @@ -168,6 +172,21 @@ public List getServiceOfferingZones(Long serviceOfferingId) { return null; } + @Override + public ServiceOfferingCategory createServiceOfferingCategory(CreateServiceOfferingCategoryCmd cmd) { + throw new UnsupportedOperationException("createServiceOfferingCategory is not implemented in this mock"); + } + + @Override + public boolean deleteServiceOfferingCategory(DeleteServiceOfferingCategoryCmd cmd) { + throw new UnsupportedOperationException("deleteServiceOfferingCategory is not implemented in this mock"); + } + + @Override + public ServiceOfferingCategory updateServiceOfferingCategory(UpdateServiceOfferingCategoryCmd cmd) { + throw new UnsupportedOperationException("updateServiceOfferingCategory is not implemented in this mock"); + } + /* (non-Javadoc) * @see com.cloud.configuration.ConfigurationService#updateDiskOffering(org.apache.cloudstack.api.commands.UpdateDiskOfferingCmd) */ diff --git a/server/src/test/java/org/apache/cloudstack/service/ServiceOfferingVOTest.java b/server/src/test/java/org/apache/cloudstack/service/ServiceOfferingVOTest.java index 0a1cac35a67b..4f275b600027 100644 --- a/server/src/test/java/org/apache/cloudstack/service/ServiceOfferingVOTest.java +++ b/server/src/test/java/org/apache/cloudstack/service/ServiceOfferingVOTest.java @@ -52,4 +52,10 @@ public void notDynamic() { Assert.assertTrue(!offering.isDynamic()); } + @Test + public void copyPreservesCategory() { + offering.setCategoryId(42L); + Assert.assertEquals(Long.valueOf(42L), new ServiceOfferingVO(offering).getCategoryId()); + } + } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 0acafc3b8015..cf7743dd7fa8 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -550,6 +550,8 @@ "label.capacitybytes": "Capacity bytes", "label.capacityiops": "IOPS total", "label.category": "Category", +"label.categoryid": "Category", +"label.categoryname": "Category name", "label.certchain": "Chain", "label.certificate": "Certificate", "label.certificate.chain": "Certificate chain", @@ -1285,6 +1287,10 @@ "label.guest.os.category": "Guest OS Category", "label.guest.os.categories": "Guest OS Categories", "label.guest.os.hypervisor.mappings": "Guest OS mappings", +"label.service.offering.categories": "Service Offering Categories", +"label.add.service.offering.category": "Add Service Offering Category", +"label.action.delete.service.offering.category": "Delete Service Offering Category", +"message.action.delete.service.offering.category": "Please confirm that you want to delete this service offering category", "label.guest.start.ip": "Guest start IP", "label.guest.traffic": "Guest traffic", "label.guestcidraddress": "Guest CIDR", diff --git a/ui/src/components/offering/ComputeOfferingForm.vue b/ui/src/components/offering/ComputeOfferingForm.vue index 0e293895f3fc..12f164e1479d 100644 --- a/ui/src/components/offering/ComputeOfferingForm.vue +++ b/ui/src/components/offering/ComputeOfferingForm.vue @@ -39,6 +39,25 @@ v-model:value="form.displaytext" :placeholder="$t('label.displaytext')"/> + + + + + {{ category.name }} + + +