This commit is contained in:
Hanarion 2026-07-04 14:36:10 +00:00 committed by GitHub
commit 7f90d1e0cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1240 additions and 12 deletions

View File

@ -38,11 +38,14 @@ import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd;
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.network.Networks.TrafficType;
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<Long> 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
*

View File

@ -85,6 +85,7 @@ import com.cloud.network.vpc.VpcOffering;
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;
@ -396,6 +397,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";
@ -1089,6 +1095,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);

View File

@ -146,4 +146,6 @@ public interface ServiceOffering extends InfrastructureEntity, InternalIdentity,
Long getVgpuProfileId();
Integer getGpuCount();
long getCategoryId();
}

View File

@ -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.offering;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
public interface ServiceOfferingCategory extends Identity, InternalIdentity {
String getName();
void setName(String name);
int getSortKey();
void setSortKey(int sortKey);
}

View File

@ -546,6 +546,8 @@ 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_CATEGORY_NAME = "categoryname";
public static final String SERVICE_OFFERING_ID = "serviceofferingid";
public static final String SERVICE_OFFERING_NAME = "serviceofferingname";
public static final String SESSIONKEY = "sessionkey";

View File

@ -116,6 +116,7 @@ import org.apache.cloudstack.api.response.SSHKeyPairResponse;
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.network.vpc.VpcOffering;
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);

View File

@ -0,0 +1,85 @@
// 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.api.APICommand;
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 = "4.23.0",
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
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;
}
}

View File

@ -32,6 +32,7 @@ import org.apache.cloudstack.api.ServerApiException;
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 = "4.23.0")
private Long categoryId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -560,6 +569,10 @@ public class CreateServiceOfferingCmd extends BaseCmd {
return Boolean.TRUE.equals(gpuDisplay);
}
public Long getCategoryId() {
return categoryId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -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 org.apache.cloudstack.api.command.admin.offering;
import org.apache.cloudstack.api.APICommand;
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 = "4.23.0",
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
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;
}
}

View File

@ -0,0 +1,71 @@
// 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.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 = "4.23.0",
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
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<ServiceOfferingCategoryResponse> response = _queryService.listServiceOfferingCategories(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}
}

View File

@ -0,0 +1,95 @@
// 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.api.APICommand;
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 = "4.23.0",
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false)
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");
}
}
}

View File

@ -29,6 +29,7 @@ import org.apache.cloudstack.api.Parameter;
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 = "4.23.0")
private Long categoryId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -164,6 +173,8 @@ public class UpdateServiceOfferingCmd extends BaseCmd implements DomainAndZoneId
return Boolean.TRUE.equals(cleanupExternalDetails);
}
public Long getCategoryId() { return categoryId; }
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -23,6 +23,7 @@ import org.apache.cloudstack.api.ApiConstants;
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 = "4.23.0")
private Long categoryId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -193,6 +201,10 @@ public class ListServiceOfferingsCmd extends BaseListProjectAndAccountResourcesC
return gpuEnabled;
}
public Long getCategoryId() {
return categoryId;
}
public void setZoneId(Long zoneId) {
this.zoneId = zoneId;
}

View File

@ -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;
}
}

View File

@ -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 = "4.23.0")
private String categoryId;
@SerializedName("category")
@Param(description = "the name of the service offering category", since = "4.23.0")
private String categoryName;
public ServiceOfferingResponse() {
}
@ -707,4 +715,9 @@ public class ServiceOfferingResponse extends BaseResponseWithAnnotations {
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; }
}

View File

@ -36,6 +36,7 @@ import org.apache.cloudstack.api.command.admin.storage.ListStorageAccessGroupsCm
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;
@ -87,6 +88,7 @@ import org.apache.cloudstack.api.response.RouterHealthCheckResultResponse;
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;
@ -187,6 +189,8 @@ public interface QueryService {
ListResponse<ServiceOfferingResponse> searchForServiceOfferings(ListServiceOfferingsCmd cmd);
ListResponse<ServiceOfferingCategoryResponse> listServiceOfferingCategories(ListServiceOfferingCategoriesCmd cmd);
ListResponse<ZoneResponse> listDataCenters(ListZonesCmd cmd);
ListResponse<TemplateResponse> listTemplates(ListTemplatesCmd cmd);

View File

@ -0,0 +1,100 @@
// 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;
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 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;
}
}

View File

@ -133,6 +133,9 @@ public class ServiceOfferingVO implements ServiceOffering {
@Column(name = "gpu_display")
private Boolean gpuDisplay;
@Column(name = "category_id")
private long categoryId = 1L; // Default category
// 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.
@ -482,4 +485,13 @@ public class ServiceOfferingVO implements ServiceOffering {
public void setGpuDisplay(Boolean gpuDisplay) {
this.gpuDisplay = gpuDisplay;
}
@Override
public long getCategoryId() {
return categoryId;
}
public void setCategoryId(long categoryId) {
this.categoryId = categoryId;
}
}

View File

@ -0,0 +1,29 @@
// 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, Long> {
ServiceOfferingCategoryVO findByName(String name);
List<ServiceOfferingCategoryVO> listAll();
}

View File

@ -0,0 +1,52 @@
// 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<ServiceOfferingCategoryVO, Long> implements ServiceOfferingCategoryDao {
protected final SearchBuilder<ServiceOfferingCategoryVO> NameSearch;
protected ServiceOfferingCategoryDaoImpl() {
NameSearch = createSearchBuilder();
NameSearch.and("name", NameSearch.entity().getName(), SearchCriteria.Op.EQ);
NameSearch.done();
}
@Override
public ServiceOfferingCategoryVO findByName(String name) {
SearchCriteria<ServiceOfferingCategoryVO> sc = NameSearch.create();
sc.setParameters("name", name);
return findOneBy(sc);
}
@Override
public List<ServiceOfferingCategoryVO> listAll() {
return listAll(null);
}
}

View File

@ -59,5 +59,7 @@ public interface ServiceOfferingDao extends GenericDao<ServiceOfferingVO, Long>
List<Long> listIdsByHostTag(String tag);
List<ServiceOfferingVO> listByCategoryId(Long categoryId);
void addCheckForGpuEnabled(SearchBuilder<ServiceOfferingVO> serviceOfferingSearch, Boolean gpuEnabled);
}

View File

@ -56,6 +56,7 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase<ServiceOfferingVO, Lo
protected final SearchBuilder<ServiceOfferingVO> ServiceOfferingsByKeywordSearch;
protected final SearchBuilder<ServiceOfferingVO> PublicCpuRamSearch;
protected final SearchBuilder<ServiceOfferingVO> SearchComputeOfferingByComputeOnlyDiskOffering;
protected final SearchBuilder<ServiceOfferingVO> CategoryIdSearch;
public ServiceOfferingDaoImpl() {
super();
@ -79,6 +80,17 @@ public class ServiceOfferingDaoImpl extends GenericDaoBase<ServiceOfferingVO, Lo
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<ServiceOfferingVO> listByCategoryId(Long categoryId) {
SearchCriteria<ServiceOfferingVO> sc = CategoryIdSearch.create();
sc.setParameters("category_id", categoryId);
return listBy(sc);
}
@Override

View File

@ -56,6 +56,7 @@
<bean id="roleDaoImpl" class="org.apache.cloudstack.acl.dao.RoleDaoImpl" />
<bean id="serviceOfferingDaoImpl" class="com.cloud.service.dao.ServiceOfferingDaoImpl" />
<bean id="serviceOfferingDetailsDaoImpl" class="com.cloud.service.dao.ServiceOfferingDetailsDaoImpl"/>
<bean id="serviceOfferingCategoryDaoImpl" class="com.cloud.service.dao.ServiceOfferingCategoryDaoImpl" />
<bean id="snapshotDaoImpl" class="com.cloud.storage.dao.SnapshotDaoImpl" />
<bean id="snapshotZoneDaoImpl" class="com.cloud.storage.dao.SnapshotZoneDaoImpl" />
<bean id="snapshotDataStoreDaoImpl" class="org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDaoImpl" />

View File

@ -19,6 +19,21 @@
-- Schema upgrade from 4.22.1.0 to 4.23.0.0
--;
CREATE TABLE IF NOT EXISTS `cloud`.`service_offering_category` (
`id` bigint unsigned NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
`uuid` varchar(40),
`sort_key` int 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;
INSERT INTO `cloud`.`service_offering_category` (id, name, uuid) VALUES (1, 'Default', UUID());
ALTER TABLE `cloud`.`service_offering` ADD COLUMN `category_id` bigint unsigned NOT NULL DEFAULT 1;
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;
CREATE TABLE `cloud`.`backup_offering_details` (
`id` bigint unsigned NOT NULL auto_increment,
`backup_offering_id` bigint unsigned NOT NULL COMMENT 'Backup offering id',

View File

@ -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`)

View File

@ -162,6 +162,7 @@ import org.apache.cloudstack.api.response.SecondaryStorageHeuristicsResponse;
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.DiskOffering;
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;
@ -674,6 +676,16 @@ public class ApiResponseHelper implements ResponseGenerator, ResourceIdSupport {
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();

View File

@ -76,6 +76,7 @@ import org.apache.cloudstack.api.command.admin.host.ListHostsCmd;
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;
@ -145,6 +146,7 @@ import org.apache.cloudstack.api.response.RouterHealthCheckResultResponse;
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;
@ -301,8 +303,10 @@ import com.cloud.server.ResourceManagerUtil;
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;
@ -484,6 +488,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
@Inject
ServiceOfferingDetailsDao _srvOfferingDetailsDao;
@Inject
ServiceOfferingCategoryDao _serviceOfferingCategoryDao;
@Inject
DiskOfferingDao _diskOfferingDao;
@ -4095,6 +4102,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
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);
@ -4142,6 +4150,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
_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());
@ -4435,6 +4447,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
sc.setParameters("vgpuProfileId", vgpuProfileId);
}
if (categoryId != null) {
sc.setParameters("categoryId", categoryId);
}
if (vmId != null) {
if (!currentVmOffering.isDynamic()) {
sc.setParameters("idNEQ", currentVmOffering.getId());
@ -6354,6 +6370,46 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
return bucketDao.searchByIds(bktIds);
}
@Override
public ListResponse<ServiceOfferingCategoryResponse> listServiceOfferingCategories(ListServiceOfferingCategoriesCmd cmd) {
Long id = cmd.getId();
String name = cmd.getName();
Filter searchFilter = new Filter(ServiceOfferingCategoryVO.class, "sortKey", true, cmd.getStartIndex(), cmd.getPageSizeVal());
SearchBuilder<ServiceOfferingCategoryVO> 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<ServiceOfferingCategoryVO> sc = sb.create();
if (id != null) {
sc.setParameters("id", id);
}
if (name != null) {
sc.setParameters("name", name);
}
Pair<List<ServiceOfferingCategoryVO>, Integer> result = _serviceOfferingCategoryDao.searchAndCount(sc, searchFilter);
ListResponse<ServiceOfferingCategoryResponse> response = new ListResponse<>();
List<ServiceOfferingCategoryResponse> 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 String getConfigComponentName() {
return QueryService.class.getSimpleName();

View File

@ -147,6 +147,13 @@ public class ServiceOfferingJoinDaoImpl extends GenericDaoBase<ServiceOfferingJo
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());

View File

@ -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 class ServiceOfferingJoinVO extends BaseViewVO implements InternalIdentit
public Boolean getGpuDisplay() {
return gpuDisplay;
}
public Long getCategoryId() { return categoryId; }
public String getCategoryUuid() { return categoryUuid; }
public String getCategoryName() { return categoryName; }
}

View File

@ -82,11 +82,14 @@ import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd;
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;
@ -266,6 +269,7 @@ import com.cloud.offering.NetworkOffering;
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;
@ -280,6 +284,8 @@ import com.cloud.resourcelimit.CheckedReservation;
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;
@ -376,6 +382,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
@Inject
ServiceOfferingDetailsDao _serviceOfferingDetailsDao;
@Inject
ServiceOfferingCategoryDao _serviceOfferingCategoryDao;
@Inject
DiskOfferingDao _diskOfferingDao;
@Inject
DiskOfferingDetailsDao diskOfferingDetailsDao;
@ -3529,6 +3537,12 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
}
}
// 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());
@ -3544,7 +3558,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
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) {
@ -3574,7 +3588,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
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<Long> filteredDomainIds = domainHelper.filterChildSubDomains(domainIds);
@ -3659,6 +3673,10 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
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) {
@ -3969,7 +3987,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
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, sourceOffering.getCategoryId());
}
private ServiceOfferingVO getAndValidateSourceOffering(Long sourceOfferingId) {
@ -4371,11 +4389,17 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
boolean purgeResources = cmd.isPurgeResources();
final Map<String, String> 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) {
@ -4468,7 +4492,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
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) ||
@ -4496,6 +4520,10 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
offering.setState(state);
}
if (categoryId != null) {
offering.setCategoryId(categoryId);
}
DiskOfferingVO diskOffering = _diskOfferingDao.findById(offeringHandle.getDiskOfferingId());
updateOfferingTagsIfIsNotNull(storageTags, diskOffering);
@ -9552,4 +9580,97 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
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().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);
}
// Check if any service offering is using this category
// For now we'll just check if it's the default category (id=1)
if (categoryId == 1L) {
throw new InvalidParameterValueException("Cannot delete the default service offering category");
}
// Check if any service offering is using this category
List<ServiceOfferingVO> 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);
}
}

View File

@ -168,11 +168,15 @@ import org.apache.cloudstack.api.command.admin.offering.CloneDiskOfferingCmd;
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;
@ -3904,6 +3908,10 @@ public class ManagementServerImpl extends MutualExclusiveIdsManagerBase implemen
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);

View File

@ -251,7 +251,7 @@ public class ConfigurationManagerCloneIntegrationTest {
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);
@ -328,7 +328,7 @@ public class ConfigurationManagerCloneIntegrationTest {
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);
@ -553,7 +553,7 @@ public class ConfigurationManagerCloneIntegrationTest {
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);

View File

@ -42,6 +42,7 @@ import com.cloud.offering.DiskOffering;
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.CloneDiskOfferingCmd;
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,24 @@ public class MockConfigurationManagerImpl extends ManagerBase implements Configu
return null;
}
@Override
public ServiceOfferingCategory createServiceOfferingCategory(CreateServiceOfferingCategoryCmd cmd) {
// TODO Auto-generated method stub
return null;
}
@Override
public boolean deleteServiceOfferingCategory(DeleteServiceOfferingCategoryCmd cmd) {
// TODO Auto-generated method stub
return false;
}
@Override
public ServiceOfferingCategory updateServiceOfferingCategory(UpdateServiceOfferingCategoryCmd cmd) {
// TODO Auto-generated method stub
return null;
}
/* (non-Javadoc)
* @see com.cloud.configuration.ConfigurationService#updateDiskOffering(org.apache.cloudstack.api.commands.UpdateDiskOfferingCmd)
*/

View File

@ -535,6 +535,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",
@ -1222,6 +1224,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",

View File

@ -39,6 +39,25 @@
v-model:value="form.displaytext"
:placeholder="$t('label.displaytext')"/>
</a-form-item>
<a-form-item name="categoryid" ref="categoryid" v-if="!isSystem">
<template #label>
<tooltip-label :title="$t('label.category')" :tooltip="apiParams.categoryid ? apiParams.categoryid.description : $t('label.category')"/>
</template>
<a-select
:getPopupContainer="(trigger) => trigger.parentNode"
v-model:value="form.categoryid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:loading="categoryLoading"
:placeholder="$t('label.category')">
<a-select-option v-for="category in categories" :key="category.id" :label="category.name">
{{ category.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item name="systemvmtype" ref="systemvmtype" v-if="isSystem">
<template #label>
<tooltip-label :title="$t('label.systemvmtype')" :tooltip="apiParams.systemvmtype.description"/>
@ -667,7 +686,9 @@ export default {
leaseexpiryaction: undefined,
vgpuProfiles: [],
vgpuProfileLoading: false,
externalDetailsEnabled: false
externalDetailsEnabled: false,
categories: [],
categoryLoading: false
}
},
created () {
@ -688,6 +709,7 @@ export default {
this.fetchDomainData()
this.fetchZoneData()
this.fetchGPUCards()
this.fetchCategories()
if (isAdmin()) {
this.fetchStorageTagData()
this.fetchDeploymentPlannerData()
@ -709,6 +731,14 @@ export default {
this.gpuCardLoading = false
})
},
fetchCategories () {
this.categoryLoading = true
getAPI('listServiceOfferingCategories', {}).then(json => {
this.categories = json.listserviceofferingcategoriesresponse.serviceofferingcategory || []
}).finally(() => {
this.categoryLoading = false
})
},
addDiskOffering () { this.showDiskOfferingModal = true },
fetchDiskOfferings () {
this.diskOfferingLoading = true

View File

@ -1053,6 +1053,9 @@
<template v-if="column.key === 'vgpuActions'">
<slot name="actionButtons" :record="record" :actions="actions"></slot>
</template>
<template v-if="column.key === 'category' && $route.path.split('/')[1] === 'computeoffering'">
<router-link :to="{ path: '/serviceofferingcategory/' + record.categoryid }">{{ text }}</router-link>
</template>
</template>
<template #footer>
<span v-if="hasSelected">
@ -1330,6 +1333,9 @@ export default {
case 'guestoscategory':
apiCommand = 'updateOsCategory'
break
case 'serviceofferingcategory':
apiCommand = 'updateServiceOfferingCategory'
break
}
return apiCommand
},

View File

@ -202,6 +202,40 @@ export default {
}
]
},
{
name: 'serviceofferingcategory',
title: 'label.service.offering.categories',
icon: 'shopping-outlined',
permission: ['listServiceOfferingCategories'],
columns: ['name', 'order'],
details: ['name', 'id'],
actions: [
{
api: 'createServiceOfferingCategory',
icon: 'plus-outlined',
label: 'label.add.service.offering.category',
listView: true,
dataView: false,
args: ['name']
},
{
api: 'updateServiceOfferingCategory',
icon: 'edit-outlined',
label: 'label.edit',
dataView: true,
popup: true,
args: ['name']
},
{
api: 'deleteServiceOfferingCategory',
icon: 'delete-outlined',
label: 'label.action.delete.service.offering.category',
message: 'message.action.delete.service.offering.category',
dataView: true,
popup: true
}
]
},
{
name: 'guestos',
title: 'label.guest.os',

View File

@ -30,7 +30,7 @@ export default {
docHelp: 'adminguide/service_offerings.html#compute-and-disk-service-offerings',
icon: 'cloud-outlined',
permission: ['listServiceOfferings'],
searchFilters: ['name', 'gpuenabled', 'zoneid', 'domainid', 'cpunumber', 'cpuspeed', 'memory'],
searchFilters: ['name', 'gpuenabled', 'zoneid', 'domainid', 'cpunumber', 'cpuspeed', 'memory', 'categoryid'],
params: () => {
var params = {}
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
@ -39,9 +39,9 @@ export default {
return params
},
filters: ['active', 'inactive'],
columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'domain', 'zone', 'order'],
columns: ['name', 'displaytext', 'state', 'cpunumber', 'cpuspeed', 'memory', 'gpu', 'category', 'domain', 'zone', 'order'],
details: () => {
var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails']
var fields = ['name', 'id', 'displaytext', 'offerha', 'provisioningtype', 'storagetype', 'iscustomized', 'iscustomizediops', 'limitcpuuse', 'cpunumber', 'cpuspeed', 'memory', 'hosttags', 'tags', 'storageaccessgroups', 'storagetags', 'domain', 'zone', 'created', 'dynamicscalingenabled', 'diskofferingstrictness', 'encryptroot', 'purgeresources', 'leaseduration', 'gpucardid', 'gpucardname', 'vgpuprofileid', 'vgpuprofilename', 'gpucount', 'gpudisplay', 'leaseexpiryaction', 'externaldetails', 'category']
if (store.getters.apis.createServiceOffering &&
store.getters.apis.createServiceOffering.params.filter(x => x.name === 'storagepolicy').length > 0) {
fields.splice(6, 0, 'vspherestoragepolicy')
@ -96,8 +96,11 @@ export default {
label: 'label.edit',
docHelp: 'adminguide/service_offerings.html#modifying-or-deleting-a-service-offering',
dataView: true,
args: ['name', 'displaytext', 'storagetags', 'hosttags', 'externaldetails'],
args: ['name', 'displaytext', 'categoryid', 'storagetags', 'hosttags', 'externaldetails'],
mapping: {
categoryid: {
api: 'listServiceOfferingCategories'
},
externaldetails: {
transformedvalue: (record) => { return getFilteredExternalDetails(record.serviceofferingdetails) }
}

View File

@ -261,8 +261,11 @@
:minimum-cpunumber="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpunumber ? selectedTemplateConfiguration.cpunumber : 0"
:minimum-cpuspeed="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpuspeed ? selectedTemplateConfiguration.cpuspeed : 0"
:minimum-memory="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.memory ? selectedTemplateConfiguration.memory : 0"
:service-offering-categories="options.serviceOfferingCategories"
:selected-service-offering-category-id="selectedServiceOfferingCategoryId"
@select-compute-item="($event) => updateComputeOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('serviceOfferings', $event)"
@service-offering-category-change="($event) => handleServiceOfferingCategoryChange($event)"
></compute-offering-selection>
<compute-selection
v-if="serviceOffering && (serviceOffering.iscustomized || serviceOffering.iscustomizediops)"
@ -2333,6 +2336,46 @@ export default {
console.error('Error fetching guestOsCategories:', e)
})
},
fetchServiceOfferingCategories () {
this.loading.serviceOfferingCategories = true
return new Promise((resolve, reject) => {
getAPI('listServiceOfferingCategories').then(json => {
const categories = json.listserviceofferingcategoriesresponse.serviceofferingcategory || []
this.options.serviceOfferingCategories = [
{
id: '-1',
name: this.$t('label.all')
},
...categories
]
resolve()
}).catch(error => {
console.error('Error fetching service offering categories:', error)
this.options.serviceOfferingCategories = [
{
id: '-1',
name: this.$t('label.all')
}
]
reject(error)
}).finally(() => {
this.loading.serviceOfferingCategories = false
})
})
},
handleServiceOfferingCategoryChange (categoryId) {
this.selectedServiceOfferingCategoryId = categoryId
const params = {
page: 1,
pageSize: 10
}
if (categoryId && categoryId !== '-1') {
params.categoryid = categoryId
} else {
this.deleteFrom(this.params.serviceOfferings.options, ['categoryid'])
}
this.handleSearchFilter('serviceOfferings', params)
},
changeArchitecture (arch) {
this.selectedArchitecture = arch
this.updateImages()
@ -3112,6 +3155,8 @@ export default {
if (this.isModernImageSelection && guestOsFetch) {
await guestOsFetch
}
// Load service offering categories
this.fetchServiceOfferingCategories()
this.fetchImages()
this.updateTemplateKey()
this.formModel = toRaw(this.form)

View File

@ -167,8 +167,11 @@
:minimum-cpunumber="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpunumber ? selectedTemplateConfiguration.cpunumber : 0"
:minimum-cpuspeed="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.cpuspeed ? selectedTemplateConfiguration.cpuspeed : 0"
:minimum-memory="templateConfigurationExists && selectedTemplateConfiguration && selectedTemplateConfiguration.memory ? selectedTemplateConfiguration.memory : 0"
:service-offering-categories="options.serviceOfferingCategories"
:selected-service-offering-category-id="selectedServiceOfferingCategoryId"
@select-compute-item="($event) => updateComputeOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('serviceOfferings', $event)"
@service-offering-category-change="($event) => handleServiceOfferingCategoryChange($event)"
></compute-offering-selection>
<compute-selection
v-if="serviceOffering && (serviceOffering.iscustomized || serviceOffering.iscustomizediops)"
@ -935,8 +938,10 @@ export default {
rootdisksize: null,
disksize: null
},
selectedServiceOfferingCategoryId: '-1',
options: {
guestOsCategories: [],
serviceOfferingCategories: [],
templates: {},
isos: {},
hypervisors: [],
@ -960,6 +965,7 @@ export default {
loading: {
deploy: false,
guestOsCategories: false,
serviceOfferingCategories: false,
templates: false,
isos: false,
hypervisors: false,
@ -2639,6 +2645,8 @@ export default {
if (this.isModernImageSelection && guestOsFetch) {
await guestOsFetch
}
// Load service offering categories
this.fetchServiceOfferingCategories()
this.fetchAllTemplates()
this.updateTemplateKey()
this.formModel = toRaw(this.form)
@ -2700,6 +2708,46 @@ export default {
this.params[name].options = { ...this.params[name].options, ...options }
this.fetchOptions(this.params[name], name)
},
fetchServiceOfferingCategories () {
this.loading.serviceOfferingCategories = true
return new Promise((resolve, reject) => {
getAPI('listServiceOfferingCategories').then(json => {
const categories = json.listserviceofferingcategoriesresponse.serviceofferingcategory || []
this.options.serviceOfferingCategories = [
{
id: '-1',
name: this.$t('label.all')
},
...categories
]
resolve()
}).catch(error => {
console.error('Error fetching service offering categories:', error)
this.options.serviceOfferingCategories = [
{
id: '-1',
name: this.$t('label.all')
}
]
reject(error)
}).finally(() => {
this.loading.serviceOfferingCategories = false
})
})
},
handleServiceOfferingCategoryChange (categoryId) {
this.selectedServiceOfferingCategoryId = categoryId
const params = {
page: 1,
pageSize: 10
}
if (categoryId && categoryId !== '-1') {
params.categoryid = categoryId
} else {
this.deleteFrom(this.params.serviceOfferings.options, ['categoryid'])
}
this.handleSearchFilter('serviceOfferings', params)
},
onTabChange (key, type) {
this[type] = key
},

View File

@ -17,6 +17,25 @@
<template>
<div>
<a-form-item
:label="$t('label.category')"
name="serviceofferingcategoryid"
ref="serviceofferingcategoryid"
v-if="serviceOfferingCategories && serviceOfferingCategories.length > 0"
style="margin-bottom: 16px;">
<block-radio-group-select
:maxBlocks="16"
:items="serviceOfferingCategories"
:selectedValue="selectedServiceOfferingCategoryId"
:horizontalGutter="6"
:verticalGutter="6"
blockSize="medium"
@change="handleServiceOfferingCategoryChange">
<template #radio-option="{ item }">
{{ item.name }}
</template>
</block-radio-group-select>
</a-form-item>
<div style="margin-bottom: 16px; display: flex; justify-content: right; align-items: center;">
<div v-if="showGpuFilter" style="display: flex; align-items: center; margin-right: 16px;">
<span>{{ $t('label.show.only.gpu.enabled.offerings') }}</span>
@ -96,8 +115,13 @@
</template>
<script>
import BlockRadioGroupSelect from '@/components/widgets/BlockRadioGroupSelect.vue'
export default {
name: 'ComputeOfferingSelection',
components: {
BlockRadioGroupSelect
},
props: {
computeItems: {
type: Array,
@ -152,6 +176,14 @@ export default {
type: Boolean,
required: false,
default: false
},
serviceOfferingCategories: {
type: Array,
default: () => []
},
selectedServiceOfferingCategoryId: {
type: String,
default: '-1'
}
},
data () {
@ -335,6 +367,9 @@ export default {
}
},
methods: {
handleServiceOfferingCategoryChange (categoryId) {
this.$emit('service-offering-category-change', categoryId)
},
onSelectRow (value) {
this.selectedRowKeys = value
this.$emit('select-compute-item', value[0])
@ -402,4 +437,26 @@ export default {
:deep(.ant-table-tbody) > tr > td {
cursor: pointer;
}
.radio-option {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
.radio-option__icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.ellipsis {
width: 100%;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>

View File

@ -359,6 +359,11 @@ export default {
params.diskofferingid = values.diskofferingid
}
if (values.categoryid) {
params.categoryid = values.categoryid
}
// Add GPU parameters
if (values.vgpuprofile) {
params.vgpuprofileid = values.vgpuprofile
}

View File

@ -502,6 +502,10 @@ export default {
params.diskofferingid = values.diskofferingid
}
if (values.categoryid) {
params.categoryid = values.categoryid
}
if (values.vgpuprofile) {
params.vgpuprofileid = values.vgpuprofile
}