ui,api,server: template categorization based on os (#10773)

Adds new interface for image selection (template/iso) for an instance in UI.
Old interface can still be used and it can be configured using UI configuration (config.json)

OS categories/Guest OS categories have been improved with ability to create new categories, delete an existing category, and marking a category as featured to allow it to show up in the UI in the image selection interface.

New APIs added:
- addOsCategory
- deleteOsCategory
- updateOsCategory

APIs updated:
- updateOsType
- listTemplates
- listOsCategories

Several improvements in UI especially related to forms - DeloyVM, ReinstallVM, CreateVnfAppliance, AddAutoscaleGroup.

DeployVM form can now be opened from template/ISO details view with query params.

Reorganized (removed and added some) OS categories to the following (in the same order):
```
1. Ubuntu
2. Debian
3. Fedora
4. CentOS
5. Rocky Linux
6. Alma Linux
7. Oracle
8. RedHat
9. SUSE
10. Windows
11. Other
```

Documentation PR: https://github.com/apache/cloudstack-documentation/pull/500

Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com>
This commit is contained in:
Abhishek Kumar 2025-06-10 15:25:51 +05:30 committed by GitHub
parent 41b4f0afd5
commit bce17b627d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 5027 additions and 1289 deletions

View File

@ -688,6 +688,9 @@ public class EventTypes {
public static final String EVENT_EXTERNAL_OPENDAYLIGHT_CONFIGURE_CONTROLLER = "PHYSICAL.ODLCONTROLLER.CONFIGURE";
//Guest OS related events
public static final String EVENT_GUEST_OS_CATEGORY_ADD = "GUEST.OS.CATEGORY.ADD";
public static final String EVENT_GUEST_OS_CATEGORY_DELETE = "GUEST.OS.CATEGORY.DELETE";
public static final String EVENT_GUEST_OS_CATEGORY_UPDATE = "GUEST.OS.CATEGORY.UPDATE";
public static final String EVENT_GUEST_OS_ADD = "GUEST.OS.ADD";
public static final String EVENT_GUEST_OS_REMOVE = "GUEST.OS.REMOVE";
public static final String EVENT_GUEST_OS_UPDATE = "GUEST.OS.UPDATE";

View File

@ -25,12 +25,15 @@ import org.apache.cloudstack.api.command.admin.cluster.ListClustersCmd;
import org.apache.cloudstack.api.command.admin.config.ListCfgGroupsByCmd;
import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd;
import org.apache.cloudstack.api.command.admin.config.UpdateHypervisorCapabilitiesCmd;
import org.apache.cloudstack.api.command.admin.guest.AddGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.AddGuestOsCmd;
import org.apache.cloudstack.api.command.admin.guest.AddGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.guest.DeleteGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.GetHypervisorGuestOsNamesCmd;
import org.apache.cloudstack.api.command.admin.guest.ListGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.guest.RemoveGuestOsCmd;
import org.apache.cloudstack.api.command.admin.guest.RemoveGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsCmd;
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.host.ListHostsCmd;
@ -168,6 +171,12 @@ public interface ManagementService {
*/
Pair<List<? extends GuestOsCategory>, Integer> listGuestOSCategoriesByCriteria(ListGuestOsCategoriesCmd cmd);
GuestOsCategory addGuestOsCategory(AddGuestOsCategoryCmd cmd);
GuestOsCategory updateGuestOsCategory(UpdateGuestOsCategoryCmd cmd);
boolean deleteGuestOsCategory(DeleteGuestOsCategoryCmd cmd);
/**
* Obtains a list of all guest OS mappings
*

View File

@ -16,7 +16,9 @@
// under the License.
package com.cloud.server;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface ResourceIconManager {
@ -25,4 +27,8 @@ public interface ResourceIconManager {
boolean deleteResourceIcon(List<String> resourceIds, ResourceTag.ResourceObjectType resourceType);
ResourceIcon getByResourceTypeAndUuid(ResourceTag.ResourceObjectType type, String resourceId);
Map<Long, ResourceIcon> getByResourceTypeAndIds(ResourceTag.ResourceObjectType type, Collection<Long> resourceIds);
Map<String, ResourceIcon> getByResourceTypeAndUuids(ResourceTag.ResourceObjectType type, Collection<String> resourceUuids);
}

View File

@ -66,6 +66,7 @@ public interface ResourceTag extends ControlledEntity, Identity, InternalIdentit
LBStickinessPolicy(false, true),
LBHealthCheckPolicy(false, true),
SnapshotPolicy(true, true),
GuestOsCategory(false, false, true),
GuestOs(false, true),
NetworkOffering(false, true),
VpcOffering(true, false),

View File

@ -16,6 +16,8 @@
// under the License.
package com.cloud.storage;
import java.util.Date;
import org.apache.cloudstack.api.Identity;
import org.apache.cloudstack.api.InternalIdentity;
@ -27,4 +29,7 @@ public interface GuestOsCategory extends Identity, InternalIdentity {
void setName(String name);
boolean isFeatured();
Date getCreated();
}

View File

@ -61,6 +61,7 @@ public enum ApiCommandResourceType {
AffinityGroup(org.apache.cloudstack.affinity.AffinityGroup.class),
InternalLbVm(com.cloud.network.router.VirtualRouter.class),
DedicatedGuestVlanRange(com.cloud.network.GuestVlan.class),
GuestOsCategory(com.cloud.storage.GuestOsCategory.class),
GuestOs(com.cloud.storage.GuestOS.class),
GuestOsMapping(com.cloud.storage.GuestOSHypervisor.class),
Network(com.cloud.network.Network.class),

View File

@ -299,6 +299,7 @@ public class ApiConstants {
public static final String IS_EXTRACTABLE = "isextractable";
public static final String IS_FEATURED = "isfeatured";
public static final String IS_IMPLICIT = "isimplicit";
public static final String IS_ISO = "isiso";
public static final String IS_PORTABLE = "isportable";
public static final String IS_PUBLIC = "ispublic";
public static final String IS_PERSISTENT = "ispersistent";

View File

@ -22,20 +22,16 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import com.cloud.bgp.ASNumber;
import com.cloud.bgp.ASNumberRange;
import org.apache.cloudstack.storage.object.Bucket;
import org.apache.cloudstack.affinity.AffinityGroup;
import org.apache.cloudstack.affinity.AffinityGroupResponse;
import org.apache.cloudstack.api.ApiConstants.HostDetails;
import org.apache.cloudstack.api.ApiConstants.VMDetails;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.command.user.job.QueryAsyncJobResultCmd;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.ApplicationLoadBalancerResponse;
import org.apache.cloudstack.api.response.ASNRangeResponse;
import org.apache.cloudstack.api.response.ASNumberResponse;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.ApplicationLoadBalancerResponse;
import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.AutoScalePolicyResponse;
import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse;
@ -60,10 +56,10 @@ import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.DomainRouterResponse;
import org.apache.cloudstack.api.response.EventResponse;
import org.apache.cloudstack.api.response.ExtractResponse;
import org.apache.cloudstack.api.response.SharedFSResponse;
import org.apache.cloudstack.api.response.FirewallResponse;
import org.apache.cloudstack.api.response.FirewallRuleResponse;
import org.apache.cloudstack.api.response.GlobalLoadBalancerResponse;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.GuestOSResponse;
import org.apache.cloudstack.api.response.GuestOsMappingResponse;
import org.apache.cloudstack.api.response.GuestVlanRangeResponse;
@ -73,11 +69,11 @@ import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.HypervisorCapabilitiesResponse;
import org.apache.cloudstack.api.response.HypervisorGuestOsNamesResponse;
import org.apache.cloudstack.api.response.IPAddressResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
import org.apache.cloudstack.api.response.ImageStoreResponse;
import org.apache.cloudstack.api.response.InstanceGroupResponse;
import org.apache.cloudstack.api.response.InternalLoadBalancerElementResponse;
import org.apache.cloudstack.api.response.IpForwardingRuleResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
import org.apache.cloudstack.api.response.IsolationMethodResponse;
import org.apache.cloudstack.api.response.LBHealthCheckResponse;
import org.apache.cloudstack.api.response.LBStickinessResponse;
@ -115,6 +111,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.ServiceResponse;
import org.apache.cloudstack.api.response.SharedFSResponse;
import org.apache.cloudstack.api.response.Site2SiteCustomerGatewayResponse;
import org.apache.cloudstack.api.response.Site2SiteVpnConnectionResponse;
import org.apache.cloudstack.api.response.Site2SiteVpnGatewayResponse;
@ -159,10 +156,14 @@ import org.apache.cloudstack.region.PortableIp;
import org.apache.cloudstack.region.PortableIpRange;
import org.apache.cloudstack.region.Region;
import org.apache.cloudstack.secstorage.heuristics.Heuristic;
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.storage.object.Bucket;
import org.apache.cloudstack.storage.object.ObjectStore;
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.usage.Usage;
import org.apache.cloudstack.vm.UnmanagedInstanceTO;
import com.cloud.bgp.ASNumber;
import com.cloud.bgp.ASNumberRange;
import com.cloud.capacity.Capacity;
import com.cloud.configuration.ResourceCount;
import com.cloud.configuration.ResourceLimit;
@ -223,10 +224,11 @@ import com.cloud.projects.ProjectAccount;
import com.cloud.projects.ProjectInvitation;
import com.cloud.region.ha.GlobalLoadBalancerRule;
import com.cloud.resource.RollingMaintenanceManager;
import com.cloud.server.ResourceTag;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceTag;
import com.cloud.storage.GuestOS;
import com.cloud.storage.GuestOSHypervisor;
import com.cloud.storage.GuestOsCategory;
import com.cloud.storage.ImageStore;
import com.cloud.storage.Snapshot;
import com.cloud.storage.StoragePool;
@ -240,14 +242,13 @@ import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.user.UserData;
import com.cloud.uservm.UserVm;
import com.cloud.utils.net.Ip;
import com.cloud.utils.Pair;
import com.cloud.utils.net.Ip;
import com.cloud.vm.InstanceGroup;
import com.cloud.vm.Nic;
import com.cloud.vm.NicSecondaryIp;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.snapshot.VMSnapshot;
import org.apache.cloudstack.vm.UnmanagedInstanceTO;
public interface ResponseGenerator {
UserResponse createUserResponse(UserAccount user);
@ -485,6 +486,10 @@ public interface ResponseGenerator {
AutoScaleVmGroupResponse createAutoScaleVmGroupResponse(AutoScaleVmGroup vmGroup);
GuestOSCategoryResponse createGuestOSCategoryResponse(GuestOsCategory guestOsCategory);
GuestOSCategoryResponse createGuestOSCategoryResponse(GuestOsCategory guestOsCategory, boolean showIcon);
GuestOSResponse createGuestOSResponse(GuestOS os);
GuestOsMappingResponse createGuestOSMappingResponse(GuestOSHypervisor osHypervisor);
@ -572,4 +577,6 @@ public interface ResponseGenerator {
BackupRepositoryResponse createBackupRepositoryResponse(BackupRepository repository);
SharedFSResponse createSharedFSResponse(ResponseView view, SharedFS sharedFS);
void updateTemplateIsoResponsesForIcons(List<TemplateResponse> responses, ResourceTag.ResourceObjectType type);
}

View File

@ -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.guest;
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.GuestOSCategoryResponse;
import com.cloud.storage.GuestOsCategory;
import com.cloud.user.Account;
@APICommand(name = "addOsCategory",
description = "Adds a new OS category",
responseObject = GuestOSCategoryResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.21.0",
authorized = {RoleType.Admin})
public class AddGuestOsCategoryCmd extends BaseCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name of the OS category",
required = true)
private String name;
@Parameter(name = ApiConstants.IS_FEATURED, type = CommandType.BOOLEAN,
description = "Whether the category is featured or not")
private Boolean featured;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public String getName() {
return name;
}
public boolean isFeatured() {
return Boolean.TRUE.equals(featured);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
GuestOsCategory guestOs = _mgr.addGuestOsCategory(this);
if (guestOs != null) {
GuestOSCategoryResponse response = _responseGenerator.createGuestOSCategoryResponse(guestOs);
response.setResponseName(getCommandName());
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to add new OS category");
}
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.GuestOsCategory;
}
}

View File

@ -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.guest;
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.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.SuccessResponse;
import com.cloud.user.Account;
@APICommand(name = "deleteOsCategory",
description = "Deletes an OS category",
responseObject = SuccessResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.21.0",
authorized = {RoleType.Admin})
public class DeleteGuestOsCategoryCmd extends BaseCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class,
required = true, description = "ID of the OS category")
private Long id;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public void execute() {
boolean result = _mgr.deleteGuestOsCategory(this);
if (result) {
SuccessResponse response = new SuccessResponse(getCommandName());
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to remove OS category");
}
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.GuestOsCategory;
}
@Override
public Long getApiResourceId() {
return getId();
}
}

View File

@ -0,0 +1,113 @@
// 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.guest;
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.GuestOSCategoryResponse;
import com.cloud.storage.GuestOsCategory;
import com.cloud.user.Account;
@APICommand(name = "updateOsCategory",
description = "Updates an OS category",
responseObject = GuestOSCategoryResponse.class,
requestHasSensitiveInfo = false,
responseHasSensitiveInfo = false,
since = "4.21.0",
authorized = {RoleType.Admin})
public class UpdateGuestOsCategoryCmd extends BaseCmd {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class,
required = true, description = "ID of the OS category")
private Long id;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "Name for the OS category")
private String name;
@Parameter(name = ApiConstants.IS_FEATURED, type = CommandType.BOOLEAN,
description = "Whether the category is featured or not")
private Boolean featured;
@Parameter(name = ApiConstants.SORT_KEY, type = CommandType.INTEGER,
description = "sort key of the OS category for listing")
private Integer sortKey;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Boolean isFeatured() {
return featured;
}
public Integer getSortKey() {
return sortKey;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;
}
@Override
public void execute() {
GuestOsCategory guestOs = _mgr.updateGuestOsCategory(this);
if (guestOs != null) {
GuestOSCategoryResponse response = _responseGenerator.createGuestOSCategoryResponse(guestOs);
response.setResponseName(getCommandName());
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to update OS category");
}
}
@Override
public ApiCommandResourceType getApiResourceType() {
return ApiCommandResourceType.GuestOsCategory;
}
@Override
public Long getApiResourceId() {
return getId();
}
}

View File

@ -16,8 +16,12 @@
// under the License.
package org.apache.cloudstack.api.command.admin.guest;
import org.apache.commons.collections.MapUtils;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
@ -25,18 +29,14 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.GuestOSResponse;
import org.apache.cloudstack.acl.RoleType;
import org.apache.commons.collections.MapUtils;
import com.cloud.event.EventTypes;
import com.cloud.storage.GuestOS;
import com.cloud.user.Account;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
@APICommand(name = "updateGuestOs", description = "Updates the information about Guest OS", responseObject = GuestOSResponse.class,
since = "4.4.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class UpdateGuestOsCmd extends BaseAsyncCmd {
@ -50,7 +50,7 @@ public class UpdateGuestOsCmd extends BaseAsyncCmd {
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuestOSResponse.class, required = true, description = "UUID of the Guest OS")
private Long id;
@Parameter(name = ApiConstants.OS_DISPLAY_NAME, type = CommandType.STRING, required = true, description = "Unique display name for Guest OS")
@Parameter(name = ApiConstants.OS_DISPLAY_NAME, type = CommandType.STRING, description = "Unique display name for Guest OS")
private String osDisplayName;
@Parameter(name = ApiConstants.DETAILS, type = CommandType.MAP, required = false, description = "Map of (key/value pairs)")
@ -59,6 +59,12 @@ public class UpdateGuestOsCmd extends BaseAsyncCmd {
@Parameter(name="forDisplay", type=CommandType.BOOLEAN, description="whether this guest OS is available for end users", authorized = {RoleType.Admin})
private Boolean display;
@Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class,
description = "the ID of the OS category", since = "4.21.0")
private Long osCategoryId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -71,9 +77,9 @@ public class UpdateGuestOsCmd extends BaseAsyncCmd {
return osDisplayName;
}
public Map getDetails() {
Map<String, String> detailsMap = new HashMap<>();;
if (MapUtils.isNotEmpty(detailsMap)) {
public Map<String, String> getDetails() {
Map<String, String> detailsMap = new HashMap<>();
if (MapUtils.isNotEmpty(details)) {
Collection<?> servicesCollection = details.values();
Iterator<?> iter = servicesCollection.iterator();
while (iter.hasNext()) {
@ -90,6 +96,10 @@ public class UpdateGuestOsCmd extends BaseAsyncCmd {
return display;
}
public Long getOsCategoryId() {
return osCategoryId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////

View File

@ -26,7 +26,9 @@ import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import com.cloud.cpu.CPU;
import com.cloud.storage.GuestOsCategory;
import com.cloud.utils.Pair;
@ -39,12 +41,48 @@ public class ListGuestOsCategoriesCmd extends BaseListCmd {
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, description = "list Os category by id")
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = GuestOSCategoryResponse.class, description = "List OS category by id")
private Long id;
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "list os category by name", since = "3.0.1")
@Parameter(name = ApiConstants.NAME, type = CommandType.STRING, description = "List OS category by name", since = "3.0.1")
private String name;
@Parameter(name = ApiConstants.IS_FEATURED,
type = CommandType.BOOLEAN,
description = "List available OS categories by featured or not",
since = "4.21.0")
private Boolean featured;
@Parameter(name = ApiConstants.IS_ISO,
type = CommandType.BOOLEAN,
description = "List OS categories for which an ISO is available",
since = "4.21.0")
private Boolean iso;
@Parameter(name = ApiConstants.IS_VNF, type = CommandType.BOOLEAN,
description = "List OS categories for which a VNF template is available",
since = "4.21.0")
private Boolean vnf;
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.UUID,
entityType = ZoneResponse.class,
description = "List available OS categories types for the zone",
since = "4.21.0")
private Long zoneId;
@Parameter(name = ApiConstants.ARCH,
type = CommandType.STRING,
description = "List OS categories types available for given CPU architecture",
since = "4.21.0")
private String arch;
@Parameter(name = ApiConstants.SHOW_RESOURCE_ICON,
type = CommandType.BOOLEAN,
description = "flag to display the resource image for the OS category",
since = "4.21.0")
private Boolean showIcon;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -57,6 +95,30 @@ public class ListGuestOsCategoriesCmd extends BaseListCmd {
return name;
}
public Boolean isFeatured() {
return featured;
}
public Boolean isIso() {
return iso;
}
public Boolean isVnf() {
return vnf;
}
public Long getZoneId() {
return zoneId;
}
public CPU.CPUArch getArch() {
return arch == null ? null : CPU.CPUArch.fromType(arch);
}
public boolean isShowIcon() {
return Boolean.TRUE.equals(showIcon);
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@ -64,14 +126,11 @@ public class ListGuestOsCategoriesCmd extends BaseListCmd {
@Override
public void execute() {
Pair<List<? extends GuestOsCategory>, Integer> result = _mgr.listGuestOSCategoriesByCriteria(this);
ListResponse<GuestOSCategoryResponse> response = new ListResponse<GuestOSCategoryResponse>();
List<GuestOSCategoryResponse> osCatResponses = new ArrayList<GuestOSCategoryResponse>();
ListResponse<GuestOSCategoryResponse> response = new ListResponse<>();
List<GuestOSCategoryResponse> osCatResponses = new ArrayList<>();
for (GuestOsCategory osCategory : result.first()) {
GuestOSCategoryResponse categoryResponse = new GuestOSCategoryResponse();
categoryResponse.setId(osCategory.getUuid());
categoryResponse.setName(osCategory.getName());
categoryResponse.setObjectName("oscategory");
GuestOSCategoryResponse categoryResponse = _responseGenerator.createGuestOSCategoryResponse(osCategory,
isShowIcon());
osCatResponses.add(categoryResponse);
}

View File

@ -16,11 +16,6 @@
// under the License.
package org.apache.cloudstack.api.command.user.iso;
import com.cloud.cpu.CPU;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceTag;
import org.apache.cloudstack.api.response.ResourceIconResponse;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
@ -28,16 +23,17 @@ import org.apache.cloudstack.api.BaseListTaggedResourcesCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.TemplateResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import com.cloud.template.VirtualMachineTemplate.TemplateFilter;
import com.cloud.user.Account;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import com.cloud.cpu.CPU;
import com.cloud.server.ResourceTag;
import com.cloud.template.VirtualMachineTemplate.TemplateFilter;
import com.cloud.user.Account;
@APICommand(name = "listIsos", description = "Lists all available ISO files.", responseObject = TemplateResponse.class, responseView = ResponseView.Restricted,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@ -95,6 +91,11 @@ public class ListIsosCmd extends BaseListTaggedResourcesCmd implements UserCmd {
since = "4.20")
private String arch;
@Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType= GuestOSCategoryResponse.class,
description = "the ID of the OS category for the ISO",
since = "4.21.0")
private Long osCategoryId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -173,6 +174,10 @@ public class ListIsosCmd extends BaseListTaggedResourcesCmd implements UserCmd {
return CPU.CPUArch.fromType(arch);
}
public Long getOsCategoryId() {
return osCategoryId;
}
/////////////////////////////////////////////////////
/////////////// API Implementation///////////////////
/////////////////////////////////////////////////////
@ -190,24 +195,14 @@ public class ListIsosCmd extends BaseListTaggedResourcesCmd implements UserCmd {
@Override
public void execute() {
ListResponse<TemplateResponse> response = _queryService.listIsos(this);
if (response != null && response.getCount() > 0 && getShowIcon()) {
updateIsoResponse(response.getResponses());
if (response != null && getShowIcon()) {
_responseGenerator.updateTemplateIsoResponsesForIcons(response.getResponses(),
ResourceTag.ResourceObjectType.ISO);
}
response.setResponseName(getCommandName());
setResponseObject(response);
}
private void updateIsoResponse(List<TemplateResponse> response) {
for (TemplateResponse templateResponse : response) {
ResourceIcon resourceIcon = resourceIconManager.getByResourceTypeAndUuid(ResourceTag.ResourceObjectType.ISO, templateResponse.getId());
if (resourceIcon == null) {
continue;
}
ResourceIconResponse iconResponse = _responseGenerator.createResourceIconResponse(resourceIcon);
templateResponse.setResourceIconResponse(iconResponse);
}
}
public Long getStoragePoolId() {
return null;
};

View File

@ -16,17 +16,11 @@
// under the License.
package org.apache.cloudstack.api.command.user.template;
import com.cloud.cpu.CPU;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceTag;
import org.apache.cloudstack.api.response.ResourceIconResponse;
import org.apache.commons.collections.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
@ -34,15 +28,20 @@ import org.apache.cloudstack.api.BaseListTaggedResourcesCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.command.user.UserCmd;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.TemplateResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.cpu.CPU;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.server.ResourceTag;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.template.VirtualMachineTemplate.TemplateFilter;
import com.cloud.user.Account;
import org.apache.commons.lang3.StringUtils;
@APICommand(name = "listTemplates", description = "List all public, private, and privileged templates.", responseObject = TemplateResponse.class, entityType = {VirtualMachineTemplate.class}, responseView = ResponseView.Restricted,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@ -111,6 +110,11 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User
since = "4.20")
private String arch;
@Parameter(name = ApiConstants.OS_CATEGORY_ID, type = CommandType.UUID, entityType= GuestOSCategoryResponse.class,
description = "the ID of the OS category for the template",
since = "4.21.0")
private Long osCategoryId;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -205,6 +209,10 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User
return CPU.CPUArch.fromType(arch);
}
public Long getOsCategoryId() {
return osCategoryId;
}
@Override
public String getCommandName() {
return s_name;
@ -218,24 +226,14 @@ public class ListTemplatesCmd extends BaseListTaggedResourcesCmd implements User
@Override
public void execute() {
ListResponse<TemplateResponse> response = _queryService.listTemplates(this);
if (response != null && response.getCount() > 0 && getShowIcon()) {
updateTemplateResponse(response.getResponses());
if (response != null && getShowIcon()) {
_responseGenerator.updateTemplateIsoResponsesForIcons(response.getResponses(),
ResourceTag.ResourceObjectType.Template);
}
response.setResponseName(getCommandName());
setResponseObject(response);
}
private void updateTemplateResponse(List<TemplateResponse> response) {
for (TemplateResponse templateResponse : response) {
ResourceIcon resourceIcon = resourceIconManager.getByResourceTypeAndUuid(ResourceTag.ResourceObjectType.Template, templateResponse.getId());
if (resourceIcon == null) {
continue;
}
ResourceIconResponse iconResponse = _responseGenerator.createResourceIconResponse(resourceIcon);
templateResponse.setResourceIconResponse(iconResponse);
}
}
public List<Long> getIds() {
if (ids == null) {
return Collections.emptyList();

View File

@ -16,10 +16,15 @@
// under the License.
package org.apache.cloudstack.api.command.user.vm;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.affinity.AffinityGroupResponse;
@ -54,6 +59,7 @@ import com.cloud.cpu.CPU;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceTag;
import com.cloud.storage.GuestOS;
import com.cloud.vm.VirtualMachine;
@ -331,22 +337,75 @@ public class ListVMsCmd extends BaseListRetrieveOnlyResourceCountCmd implements
setResponseObject(response);
}
protected void updateVMResponse(List<UserVmResponse> response) {
for (UserVmResponse vmResponse : response) {
ResourceIcon resourceIcon = resourceIconManager.getByResourceTypeAndUuid(ResourceTag.ResourceObjectType.UserVm, vmResponse.getId());
if (resourceIcon == null) {
ResourceTag.ResourceObjectType type = ResourceTag.ResourceObjectType.Template;
String uuid = vmResponse.getTemplateId();
if (vmResponse.getIsoId() != null) {
uuid = vmResponse.getIsoId();
type = ResourceTag.ResourceObjectType.ISO;
protected Map<String, ResourceIcon> getResourceIconsUsingOsCategory(List<UserVmResponse> responses) {
Set<String> guestOsIds = responses.stream().map(UserVmResponse::getGuestOsId).collect(Collectors.toSet());
List<GuestOS> guestOSList = _entityMgr.listByUuids(GuestOS.class, guestOsIds);
Map<String, GuestOS> guestOSMap = guestOSList.stream()
.collect(Collectors.toMap(GuestOS::getUuid, Function.identity()));
Set<Long> guestOsCategoryIds = guestOSMap.values().stream()
.map(GuestOS::getCategoryId)
.collect(Collectors.toSet());
Map<Long, ResourceIcon> guestOsCategoryIcons =
resourceIconManager.getByResourceTypeAndIds(ResourceTag.ResourceObjectType.GuestOsCategory,
guestOsCategoryIds);
Map<String, ResourceIcon> vmIcons = new HashMap<>();
for (UserVmResponse response : responses) {
GuestOS guestOS = guestOSMap.get(response.getGuestOsId());
if (guestOS != null) {
vmIcons.put(response.getId(), guestOsCategoryIcons.get(guestOS.getCategoryId()));
}
resourceIcon = resourceIconManager.getByResourceTypeAndUuid(type, uuid);
if (resourceIcon == null) {
}
return vmIcons;
}
protected Map<String, ResourceIcon> getResourceIconsForUsingTemplateIso(List<UserVmResponse> responses) {
Map<String, String> vmTemplateIsoIdMap = new HashMap<>();
Set<String> templateUuids = new HashSet<>();
Set<String> isoUuids = new HashSet<>();
for (UserVmResponse vmResponse : responses) {
if (vmResponse.getTemplateId() != null) {
templateUuids.add(vmResponse.getTemplateId());
vmTemplateIsoIdMap.put(vmResponse.getId(), vmResponse.getTemplateId());
}
if (vmResponse.getIsoId() != null) {
isoUuids.add(vmResponse.getIsoId());
vmTemplateIsoIdMap.put(vmResponse.getId(), vmResponse.getIsoId());
}
}
Map<String, ResourceIcon> templateOrIsoIcons = resourceIconManager.getByResourceTypeAndUuids(ResourceTag.ResourceObjectType.Template, templateUuids);
templateOrIsoIcons.putAll(resourceIconManager.getByResourceTypeAndUuids(ResourceTag.ResourceObjectType.ISO, isoUuids));
Map<String, ResourceIcon> vmIcons = new HashMap<>();
List<UserVmResponse> noTemplateIsoIconResponses = new ArrayList<>();
for (UserVmResponse response : responses) {
String uuid = vmTemplateIsoIdMap.get(response.getId());
if (StringUtils.isNotBlank(uuid) && templateOrIsoIcons.containsKey(uuid)) {
vmIcons.put(response.getId(),
templateOrIsoIcons.get(vmTemplateIsoIdMap.get(response.getId())));
continue;
}
noTemplateIsoIconResponses.add(response);
}
ResourceIconResponse iconResponse = _responseGenerator.createResourceIconResponse(resourceIcon);
vmIcons.putAll(getResourceIconsUsingOsCategory(noTemplateIsoIconResponses));
return vmIcons;
}
protected void updateVMResponse(List<UserVmResponse> responses) {
if (CollectionUtils.isEmpty(responses)) {
return;
}
Set<String> vmUuids = responses.stream().map(UserVmResponse::getId).collect(Collectors.toSet());
Map<String, ResourceIcon> vmIcons = resourceIconManager.getByResourceTypeAndUuids(ResourceTag.ResourceObjectType.UserVm, vmUuids);
List<UserVmResponse> noVmIconResponses = responses
.stream()
.filter(r -> !vmIcons.containsKey(r.getId()))
.collect(Collectors.toList());
vmIcons.putAll(getResourceIconsForUsingTemplateIso(noVmIconResponses));
for (UserVmResponse vmResponse : responses) {
ResourceIcon icon = vmIcons.get(vmResponse.getId());
if (icon == null) {
continue;
}
ResourceIconResponse iconResponse = _responseGenerator.createResourceIconResponse(icon);
vmResponse.setResourceIconResponse(iconResponse);
}
}

View File

@ -16,6 +16,8 @@
// under the License.
package org.apache.cloudstack.api.response;
import java.util.Date;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
@ -26,7 +28,7 @@ import com.cloud.serializer.Param;
import com.cloud.storage.GuestOsCategory;
@EntityReference(value = GuestOsCategory.class)
public class GuestOSCategoryResponse extends BaseResponse {
public class GuestOSCategoryResponse extends BaseResponse implements SetResourceIconResponse {
@SerializedName(ApiConstants.ID)
@Param(description = "the ID of the OS category")
private String id;
@ -35,6 +37,18 @@ public class GuestOSCategoryResponse extends BaseResponse {
@Param(description = "the name of the OS category")
private String name;
@SerializedName(ApiConstants.IS_FEATURED)
@Param(description = "Whether the OS category is featured", since = "4.21.0")
private Boolean featured;
@SerializedName(ApiConstants.RESOURCE_ICON)
@Param(description = "Base64 string representation of the resource icon", since = "4.21.0")
private ResourceIconResponse resourceIconResponse;
@SerializedName(ApiConstants.CREATED)
@Param(description = "Date when the OS category was created." )
private Date created;
public String getId() {
return id;
}
@ -50,4 +64,17 @@ public class GuestOSCategoryResponse extends BaseResponse {
public void setName(String name) {
this.name = name;
}
public void setFeatured(Boolean featured) {
this.featured = featured;
}
@Override
public void setResourceIconResponse(ResourceIconResponse resourceIconResponse) {
this.resourceIconResponse = resourceIconResponse;
}
public void setCreated(Date created) {
this.created = created;
}
}

View File

@ -93,6 +93,8 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements
@Param(description = "the name of the OS type for this template.")
private String osTypeName;
private transient Long osTypeCategoryId;
@SerializedName(ApiConstants.ACCOUNT_ID)
@Param(description = "the account id to which the template belongs")
private String accountId;
@ -285,6 +287,14 @@ public class TemplateResponse extends BaseResponseWithTagInformation implements
this.osTypeName = osTypeName;
}
public Long getOsTypeCategoryId() {
return osTypeCategoryId;
}
public void setOsTypeCategoryId(Long osTypeCategoryId) {
this.osTypeCategoryId = osTypeCategoryId;
}
public void setId(String id) {
this.id = id;
}

View File

@ -0,0 +1,46 @@
// 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.guest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
public class AddGuestOsCategoryCmdTest {
@Test
public void testGetArch() {
AddGuestOsCategoryCmd cmd = new AddGuestOsCategoryCmd();
Assert.assertNull(cmd.getName());
String name = "Name";
ReflectionTestUtils.setField(cmd, "name", name);
Assert.assertEquals(name, cmd.getName());
}
@Test
public void testIsFeatured() {
AddGuestOsCategoryCmd cmd = new AddGuestOsCategoryCmd();
Assert.assertFalse(cmd.isFeatured());
ReflectionTestUtils.setField(cmd, "featured", false);
Assert.assertFalse(cmd.isFeatured());
ReflectionTestUtils.setField(cmd, "featured", true);
Assert.assertTrue(cmd.isFeatured());
}
}

View File

@ -0,0 +1,55 @@
// 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.guest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
public class UpdateGuestOsCategoryCmdTest {
@Test
public void testGetArch() {
UpdateGuestOsCategoryCmd cmd = new UpdateGuestOsCategoryCmd();
Assert.assertNull(cmd.getName());
String name = "Name";
ReflectionTestUtils.setField(cmd, "name", name);
Assert.assertEquals(name, cmd.getName());
}
@Test
public void testIsFeatured() {
UpdateGuestOsCategoryCmd cmd = new UpdateGuestOsCategoryCmd();
Assert.assertNull(cmd.isFeatured());
ReflectionTestUtils.setField(cmd, "featured", false);
Assert.assertFalse(cmd.isFeatured());
ReflectionTestUtils.setField(cmd, "featured", true);
Assert.assertTrue(cmd.isFeatured());
}
@Test
public void testGetSortKey() {
UpdateGuestOsCategoryCmd cmd = new UpdateGuestOsCategoryCmd();
Assert.assertNull(cmd.getSortKey());
Integer sortKey = 100;
ReflectionTestUtils.setField(cmd, "sortKey", sortKey);
Assert.assertEquals(sortKey, cmd.getSortKey());
}
}

View File

@ -0,0 +1,37 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.admin.guest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
@RunWith(MockitoJUnitRunner.class)
public class UpdateGuestOsCmdTest {
@Test
public void testGetZoneId() {
UpdateGuestOsCmd cmd = new UpdateGuestOsCmd();
Assert.assertNull(cmd.getOsCategoryId());
Long osCategoryId = 100L;
ReflectionTestUtils.setField(cmd, "osCategoryId", osCategoryId);
Assert.assertEquals(osCategoryId, cmd.getOsCategoryId());
}
}

View File

@ -0,0 +1,87 @@
// 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.user.guest;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.cpu.CPU;
@RunWith(MockitoJUnitRunner.class)
public class ListGuestOsCategoriesCmdTest {
@Test
public void testIsFeatured() {
ListGuestOsCategoriesCmd cmd = new ListGuestOsCategoriesCmd();
Assert.assertNull(cmd.isFeatured());
ReflectionTestUtils.setField(cmd, "featured", false);
Assert.assertFalse(cmd.isFeatured());
ReflectionTestUtils.setField(cmd, "featured", true);
Assert.assertTrue(cmd.isFeatured());
}
@Test
public void testIsIso() {
ListGuestOsCategoriesCmd cmd = new ListGuestOsCategoriesCmd();
Assert.assertNull(cmd.isIso());
ReflectionTestUtils.setField(cmd, "iso", false);
Assert.assertFalse(cmd.isIso());
ReflectionTestUtils.setField(cmd, "iso", true);
Assert.assertTrue(cmd.isIso());
}
@Test
public void testIsVnf() {
ListGuestOsCategoriesCmd cmd = new ListGuestOsCategoriesCmd();
Assert.assertNull(cmd.isVnf());
ReflectionTestUtils.setField(cmd, "vnf", false);
Assert.assertFalse(cmd.isVnf());
ReflectionTestUtils.setField(cmd, "vnf", true);
Assert.assertTrue(cmd.isVnf());
}
@Test
public void testGetZoneId() {
ListGuestOsCategoriesCmd cmd = new ListGuestOsCategoriesCmd();
Assert.assertNull(cmd.getZoneId());
Long zoneId = 100L;
ReflectionTestUtils.setField(cmd, "zoneId", zoneId);
Assert.assertEquals(zoneId, cmd.getZoneId());
}
@Test
public void testGetArch() {
ListGuestOsCategoriesCmd cmd = new ListGuestOsCategoriesCmd();
Assert.assertNull(cmd.getArch());
CPU.CPUArch arch = CPU.CPUArch.getDefault();
ReflectionTestUtils.setField(cmd, "arch", arch.getType());
Assert.assertEquals(arch, cmd.getArch());
}
@Test
public void testIsShowIcon() {
ListGuestOsCategoriesCmd cmd = new ListGuestOsCategoriesCmd();
Assert.assertFalse(cmd.isShowIcon());
ReflectionTestUtils.setField(cmd, "showIcon", false);
Assert.assertFalse(cmd.isShowIcon());
ReflectionTestUtils.setField(cmd, "showIcon", true);
Assert.assertTrue(cmd.isShowIcon());
}
}

View File

@ -0,0 +1,223 @@
// 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.user.vm;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyList;
import static org.mockito.Mockito.anySet;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.response.ResourceIconResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceIconManager;
import com.cloud.server.ResourceTag;
import com.cloud.storage.GuestOS;
import com.cloud.utils.db.EntityManager;
public class ListVMsCmdTest {
EntityManager _entityMgr;
ResourceIconManager resourceIconManager;
ResponseGenerator _responseGenerator;
ListVMsCmd cmd;
@Before
public void setup() {
_entityMgr = mock(EntityManager.class);
resourceIconManager = mock(ResourceIconManager.class);
_responseGenerator = mock(ResponseGenerator.class);
cmd = spy(ListVMsCmd.class);
cmd._entityMgr = _entityMgr;
cmd.resourceIconManager = resourceIconManager;
cmd._responseGenerator = _responseGenerator;
}
@Test
public void testUpdateVMResponse_withMixedIcons() {
String vm1Uuid = UUID.randomUUID().toString();
UserVmResponse vm1 = mock(UserVmResponse.class);
when(vm1.getId()).thenReturn(vm1Uuid);
String vm2Uuid = UUID.randomUUID().toString();
UserVmResponse vm2 = mock(UserVmResponse.class);
when(vm2.getId()).thenReturn(vm2Uuid);
List<UserVmResponse> responses = Arrays.asList(vm1, vm2);
ResourceIcon icon1 = mock(ResourceIcon.class);
ResourceIcon icon2 = mock(ResourceIcon.class);
Map<String, ResourceIcon> initialIcons = new HashMap<>();
initialIcons.put(vm1Uuid, icon1);
when(resourceIconManager.getByResourceTypeAndUuids(ResourceTag.ResourceObjectType.UserVm, Set.of(vm1Uuid, vm2Uuid)))
.thenReturn(initialIcons);
Map<String, ResourceIcon> fallbackIcons = Map.of(vm2Uuid, icon2);
doReturn(fallbackIcons).when(cmd).getResourceIconsForUsingTemplateIso(anyList());
ResourceIconResponse iconResponse1 = new ResourceIconResponse();
ResourceIconResponse iconResponse2 = new ResourceIconResponse();
when(_responseGenerator.createResourceIconResponse(icon1)).thenReturn(iconResponse1);
when(_responseGenerator.createResourceIconResponse(icon2)).thenReturn(iconResponse2);
cmd.updateVMResponse(responses);
verify(vm1).setResourceIconResponse(iconResponse1);
verify(vm2).setResourceIconResponse(iconResponse2);
}
@Test
public void testUpdateVMResponse_withEmptyList() {
cmd.updateVMResponse(Collections.emptyList());
verify(resourceIconManager, never()).getByResourceTypeAndIds(Mockito.any(), Mockito.anyCollection());
}
@Test
public void testGetResourceIconsForUsingTemplateIso_withValidData() {
String vm1Uuid = UUID.randomUUID().toString();
String template1Uuid = UUID.randomUUID().toString();
UserVmResponse vm1 = mock(UserVmResponse.class);
when(vm1.getId()).thenReturn(vm1Uuid);
when(vm1.getTemplateId()).thenReturn(template1Uuid);
when(vm1.getIsoId()).thenReturn(null);
String vm2Uuid = UUID.randomUUID().toString();
String iso2Uuid = UUID.randomUUID().toString();
UserVmResponse vm2 = mock(UserVmResponse.class);
when(vm2.getId()).thenReturn(vm2Uuid);
when(vm2.getTemplateId()).thenReturn(null);
when(vm2.getIsoId()).thenReturn(iso2Uuid);
List<UserVmResponse> responses = Arrays.asList(vm1, vm2);
Map<String, ResourceIcon> templateIcons = new HashMap<>();
templateIcons.put(template1Uuid, mock(ResourceIcon.class));
Map<String, ResourceIcon> isoIcons = new HashMap<>();
isoIcons.put(iso2Uuid, mock(ResourceIcon.class));
when(resourceIconManager.getByResourceTypeAndUuids(ResourceTag.ResourceObjectType.Template, Set.of(template1Uuid)))
.thenReturn(templateIcons);
when(resourceIconManager.getByResourceTypeAndUuids(ResourceTag.ResourceObjectType.ISO, Set.of(iso2Uuid)))
.thenReturn(isoIcons);
doReturn(Collections.emptyMap()).when(cmd).getResourceIconsUsingOsCategory(anyList());
Map<String, ResourceIcon> result = cmd.getResourceIconsForUsingTemplateIso(responses);
assertEquals(2, result.size());
assertTrue(result.containsKey(vm1Uuid));
assertTrue(result.containsKey(vm2Uuid));
assertEquals(templateIcons.get(template1Uuid), result.get(vm1Uuid));
assertEquals(isoIcons.get(iso2Uuid), result.get(vm2Uuid));
}
@Test
public void testGetResourceIconsForUsingTemplateIso_withMissingIcons() {
String vm1Uuid = UUID.randomUUID().toString();
String template1Uuid = UUID.randomUUID().toString();
UserVmResponse vm1 = mock(UserVmResponse.class);
when(vm1.getId()).thenReturn(vm1Uuid);
when(vm1.getTemplateId()).thenReturn(template1Uuid);
when(vm1.getIsoId()).thenReturn(null);
List<UserVmResponse> responses = List.of(vm1);
when(resourceIconManager.getByResourceTypeAndUuids(eq(ResourceTag.ResourceObjectType.Template), anySet()))
.thenReturn(Collections.emptyMap());
when(resourceIconManager.getByResourceTypeAndUuids(eq(ResourceTag.ResourceObjectType.ISO), anySet()))
.thenReturn(Collections.emptyMap());
Map<String, ResourceIcon> fallbackIcons = Map.of(vm1Uuid, mock(ResourceIcon.class));
doReturn(fallbackIcons).when(cmd).getResourceIconsUsingOsCategory(anyList());
Map<String, ResourceIcon> result = cmd.getResourceIconsForUsingTemplateIso(responses);
assertEquals(1, result.size());
assertEquals(fallbackIcons.get("vm1"), result.get("vm1"));
}
@Test
public void testGetResourceIconsUsingOsCategory_withValidData() {
String vm1Uuid = UUID.randomUUID().toString();
String os1Uuid = UUID.randomUUID().toString();
UserVmResponse vm1 = mock(UserVmResponse.class);
when(vm1.getGuestOsId()).thenReturn(os1Uuid);
when(vm1.getId()).thenReturn(vm1Uuid);
String vm2Uuid = UUID.randomUUID().toString();
String os2Uuid = UUID.randomUUID().toString();
UserVmResponse vm2 = mock(UserVmResponse.class);
when(vm2.getGuestOsId()).thenReturn(os2Uuid);
when(vm2.getId()).thenReturn(vm2Uuid);
List<UserVmResponse> responses = Arrays.asList(vm1, vm2);
GuestOS guestOS1 = mock(GuestOS.class);
when(guestOS1.getUuid()).thenReturn(os1Uuid);
when(guestOS1.getCategoryId()).thenReturn(10L);
GuestOS guestOS2 = mock(GuestOS.class);
when(guestOS2.getUuid()).thenReturn(os2Uuid);
when(guestOS2.getCategoryId()).thenReturn(20L);
when(_entityMgr.listByUuids(eq(GuestOS.class), anySet()))
.thenReturn(Arrays.asList(guestOS1, guestOS2));
ResourceIcon icon1 = mock(ResourceIcon.class);
ResourceIcon icon2 = mock(ResourceIcon.class);
Map<Long, ResourceIcon> categoryIcons = new HashMap<>();
categoryIcons.put(10L, icon1);
categoryIcons.put(20L, icon2);
when(resourceIconManager.getByResourceTypeAndIds(eq(ResourceTag.ResourceObjectType.GuestOsCategory), anySet()))
.thenReturn(categoryIcons);
Map<String, ResourceIcon> result = cmd.getResourceIconsUsingOsCategory(responses);
assertEquals(2, result.size());
assertEquals(icon1, result.get(vm1Uuid));
assertEquals(icon2, result.get(vm2Uuid));
}
@Test
public void testGetResourceIconsUsingOsCategory_missingGuestOS() {
String vm1Uuid = UUID.randomUUID().toString();
String os1Uuid = UUID.randomUUID().toString();
UserVmResponse vm1 = mock(UserVmResponse.class);
when(vm1.getGuestOsId()).thenReturn(vm1Uuid);
when(vm1.getId()).thenReturn(os1Uuid);
List<UserVmResponse> responses = Collections.singletonList(vm1);
when(_entityMgr.listByUuids(eq(GuestOS.class), anySet()))
.thenReturn(Collections.emptyList());
Map<String, ResourceIcon> result = cmd.getResourceIconsUsingOsCategory(responses);
assertTrue(result.isEmpty());
}
@Test
public void testGetResourceIconsUsingOsCategory_missingIcon() {
UserVmResponse vm1 = mock(UserVmResponse.class);
String vmUuid = UUID.randomUUID().toString();
String osUuid = UUID.randomUUID().toString();
when(vm1.getGuestOsId()).thenReturn(osUuid);
when(vm1.getId()).thenReturn(vmUuid);
List<UserVmResponse> responses = Collections.singletonList(vm1);
GuestOS guestOS1 = mock(GuestOS.class);
when(guestOS1.getCategoryId()).thenReturn(10L);
when(guestOS1.getUuid()).thenReturn(osUuid);
when(_entityMgr.listByUuids(eq(GuestOS.class), anySet()))
.thenReturn(Collections.singletonList(guestOS1));
when(resourceIconManager.getByResourceTypeAndIds(eq(ResourceTag.ResourceObjectType.GuestOsCategory), anySet()))
.thenReturn(Collections.emptyMap());
Map<String, ResourceIcon> result = cmd.getResourceIconsUsingOsCategory(responses);
assertTrue(result.containsKey(vmUuid));
assertNull(result.get(vmUuid));
}
}

View File

@ -22,10 +22,13 @@ import com.cloud.server.ResourceTag;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.api.response.ResourceIconResponse;
import java.util.Collection;
import java.util.List;
public interface ResourceIconDao extends GenericDao<ResourceIconVO, Long> {
ResourceIconResponse newResourceIconResponse(ResourceIcon resourceIconVO);
ResourceIconVO findByResourceUuid(String resourceUuid, ResourceTag.ResourceObjectType resourceType);
List<ResourceIconVO> listByResourceTypeAndIds(ResourceTag.ResourceObjectType resourceType, Collection<Long> resourceIds);
List<ResourceIconVO> listByResourceTypeAndUuids(ResourceTag.ResourceObjectType resourceType, Collection<String> resourceUuids);
List<ResourceIconResponse> listResourceIcons(List<String> resourceUuids, ResourceTag.ResourceObjectType resourceType);
}

View File

@ -24,8 +24,10 @@ import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.response.ResourceIconResponse;
import org.apache.commons.collections.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class ResourceIconDaoImpl extends GenericDaoBase<ResourceIconVO, Long> implements ResourceIconDao {
@ -58,11 +60,36 @@ public class ResourceIconDaoImpl extends GenericDaoBase<ResourceIconVO, Long> im
}
@Override
public List<ResourceIconResponse> listResourceIcons(List<String> resourceUuids, ResourceTag.ResourceObjectType resourceType) {
public List<ResourceIconVO> listByResourceTypeAndIds(ResourceTag.ResourceObjectType resourceType,
Collection<Long> resourceIds) {
if (CollectionUtils.isEmpty(resourceIds)) {
return new ArrayList<>();
}
SearchBuilder<ResourceIconVO> sb = createSearchBuilder();
sb.and("resourceId", sb.entity().getResourceId(), SearchCriteria.Op.IN);
sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<ResourceIconVO> sc = sb.create();
sc.setParameters("resourceId", resourceIds.toArray());
sc.setParameters("resourceType", resourceType);
return listBy(sc);
}
@Override
public List<ResourceIconVO> listByResourceTypeAndUuids(ResourceTag.ResourceObjectType resourceType,
Collection<String> resourceUuids) {
if (CollectionUtils.isEmpty(resourceUuids)) {
return new ArrayList<>();
}
SearchCriteria<ResourceIconVO> sc = AllFieldsSearch.create();
sc.setParameters("uuid", resourceUuids.toArray());
sc.setParameters("resourceType", resourceType);
List<ResourceIconVO> resourceIcons = listBy(sc);
return listBy(sc);
}
@Override
public List<ResourceIconResponse> listResourceIcons(List<String> resourceUuids, ResourceTag.ResourceObjectType resourceType) {
List<ResourceIconVO> resourceIcons = listByResourceTypeAndUuids(resourceType, resourceUuids);
List<ResourceIconResponse> iconResponses = new ArrayList<>();
for (ResourceIconVO resourceIcon : resourceIcons) {
ResourceIconResponse response = new ResourceIconResponse();

View File

@ -16,6 +16,7 @@
// under the License.
package com.cloud.storage;
import java.util.Date;
import java.util.UUID;
import javax.persistence.Column;
@ -25,6 +26,8 @@ import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import com.cloud.utils.db.GenericDao;
@Entity
@Table(name = "guest_os_category")
public class GuestOSCategoryVO implements GuestOsCategory {
@ -39,6 +42,26 @@ public class GuestOSCategoryVO implements GuestOsCategory {
@Column(name = "uuid")
String uuid = UUID.randomUUID().toString();
@Column(name = "featured")
boolean featured;
@Column(name = "sort_key")
private int sortKey;
@Column(name = GenericDao.CREATED_COLUMN)
private Date created;
@Column(name = GenericDao.REMOVED_COLUMN)
private Date removed;
public GuestOSCategoryVO() {
}
public GuestOSCategoryVO(String name, boolean featured) {
this.name = name;
this.featured = featured;
}
@Override
public long getId() {
return id;
@ -59,7 +82,25 @@ public class GuestOSCategoryVO implements GuestOsCategory {
return this.uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
@Override
public boolean isFeatured() {
return featured;
}
public void setFeatured(Boolean featured) {
this.featured = featured;
}
public void setSortKey(int key) {
sortKey = key;
}
public int getSortKey() {
return sortKey;
}
@Override
public Date getCreated() {
return created;
}
}

View File

@ -27,7 +27,6 @@ import com.cloud.utils.db.GenericDaoBase;
public class GuestOSCategoryDaoImpl extends GenericDaoBase<GuestOSCategoryVO, Long> implements GuestOSCategoryDao {
protected GuestOSCategoryDaoImpl() {
}
@Override

View File

@ -16,14 +16,14 @@
// under the License.
package com.cloud.storage.dao;
import java.util.List;
import java.util.Set;
import com.cloud.storage.GuestOS;
import com.cloud.storage.GuestOSVO;
import com.cloud.utils.Pair;
import com.cloud.utils.db.GenericDao;
import java.util.List;
import java.util.Set;
public interface GuestOSDao extends GenericDao<GuestOSVO, Long> {
GuestOSVO findOneByDisplayName(String displayName);
@ -36,4 +36,6 @@ public interface GuestOSDao extends GenericDao<GuestOSVO, Long> {
List<GuestOSVO> listByDisplayName(String displayName);
Pair<List<? extends GuestOS>, Integer> listGuestOSByCriteria(Long startIndex, Long pageSize, Long id, Long osCategoryId, String description, String keyword, Boolean forDisplay);
List<Long> listIdsByCategoryId(final long categoryId);
}

View File

@ -25,19 +25,20 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.cloud.storage.GuestOS;
import com.cloud.utils.Pair;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;
import com.cloud.storage.GuestOS;
import com.cloud.storage.GuestOSVO;
import com.cloud.utils.Pair;
import com.cloud.utils.db.DB;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.utils.exception.CloudRuntimeException;
@Component
public class GuestOSDaoImpl extends GenericDaoBase<GuestOSVO, Long> implements GuestOSDao {
@ -152,4 +153,14 @@ public class GuestOSDaoImpl extends GenericDaoBase<GuestOSVO, Long> implements G
return new Pair<>(result.first(), result.second());
}
@Override
public List<Long> listIdsByCategoryId(final long categoryId) {
GenericSearchBuilder<GuestOSVO, Long> sb = createSearchBuilder(Long.class);
sb.selectFields(sb.entity().getId());
sb.and("categoryId", sb.entity().getCategoryId(), SearchCriteria.Op.EQ);
sb.done();
SearchCriteria<Long> sc = sb.create();
sc.setParameters("categoryId", categoryId);
return customSearch(sc, null);
}
}

View File

@ -58,6 +58,8 @@ public interface VMTemplateDao extends GenericDao<VMTemplateVO, Long>, StateDao<
public List<VMTemplateVO> listInZoneByState(long dataCenterId, VirtualMachineTemplate.State... states);
public List<Long> listTemplateIsoByArchVnfAndZone(Long dataCenterId, CPU.CPUArch arch, Boolean isIso, Boolean isVnf);
public List<VMTemplateVO> listAllActive();
public List<VMTemplateVO> listByState(VirtualMachineTemplate.State... states);

View File

@ -17,6 +17,7 @@
package com.cloud.storage.dao;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
@ -520,6 +521,48 @@ public class VMTemplateDaoImpl extends GenericDaoBase<VMTemplateVO, Long> implem
return listBy(sc);
}
@Override
public List<Long> listTemplateIsoByArchVnfAndZone(Long dataCenterId, CPU.CPUArch arch, Boolean isIso,
Boolean isVnf) {
GenericSearchBuilder<VMTemplateVO, Long> sb = createSearchBuilder(Long.class);
sb.select(null, Func.DISTINCT, sb.entity().getGuestOSId());
sb.and("state", sb.entity().getState(), SearchCriteria.Op.IN);
sb.and("type", sb.entity().getTemplateType(), SearchCriteria.Op.IN);
sb.and("arch", sb.entity().getArch(), SearchCriteria.Op.EQ);
if (isIso != null) {
sb.and("isIso", sb.entity().getFormat(), isIso ? SearchCriteria.Op.EQ : SearchCriteria.Op.NEQ);
}
if (dataCenterId != null) {
SearchBuilder<VMTemplateZoneVO> templateZoneSearch = _templateZoneDao.createSearchBuilder();
templateZoneSearch.and("removed", templateZoneSearch.entity().getRemoved(), SearchCriteria.Op.NULL);
templateZoneSearch.and("zoneId", templateZoneSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
sb.join("templateZoneSearch", templateZoneSearch, templateZoneSearch.entity().getTemplateId(),
sb.entity().getId(), JoinBuilder.JoinType.INNER);
templateZoneSearch.done();
}
sb.done();
SearchCriteria<Long> sc = sb.create();
List<TemplateType> types = new ArrayList<>(Arrays.asList(TemplateType.USER, TemplateType.BUILTIN,
TemplateType.PERHOST));
if (isVnf == null) {
types.add(TemplateType.VNF);
} else if (isVnf) {
types = Collections.singletonList(TemplateType.VNF);
}
sc.setParameters("type", types.toArray());
sc.setParameters("state", VirtualMachineTemplate.State.Active);
if (dataCenterId != null) {
sc.setJoinParameters("templateZoneSearch", "zoneId", dataCenterId);
}
if (arch != null) {
sc.setParameters("arch", arch);
}
if (isIso != null) {
sc.setParameters("isIso", ImageFormat.ISO);
}
return customSearch(sc, null);
}
@Override
public List<VMTemplateVO> listAllActive() {
SearchCriteria<VMTemplateVO> sc = ActiveTmpltSearch.create();

View File

@ -80,3 +80,93 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host', 'storage_access_groups', 'var
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.cluster', 'storage_access_groups', 'varchar(255) DEFAULT NULL COMMENT "storage access groups for the hosts in the cluster"');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.host_pod_ref', 'storage_access_groups', 'varchar(255) DEFAULT NULL COMMENT "storage access groups for the hosts in the pod"');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.data_center', 'storage_access_groups', 'varchar(255) DEFAULT NULL COMMENT "storage access groups for the hosts in the zone"');
-- Add featured column for guest_os_category
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.guest_os_category', 'featured', 'tinyint(1) NOT NULL DEFAULT 0 COMMENT "whether the category is featured or not" AFTER `uuid`');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.guest_os_category', 'sort_key', 'int NOT NULL DEFAULT 0 COMMENT "sort key used for customising sort method" AFTER `featured`');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.guest_os_category', 'created', 'datetime COMMENT "date on which the category was created" AFTER `sort_key`');
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.guest_os_category', 'removed', 'datetime COMMENT "date removed if not null" AFTER `created`');
UPDATE `cloud`.`guest_os_category` SET `featured` = 1 WHERE `name` NOT IN ('Novel', 'None');
-- Begin: Changes for Guest OS category cleanup
-- Add new OS categories if not present
DROP PROCEDURE IF EXISTS `cloud`.`INSERT_CATEGORY_IF_NOT_EXIST`;
CREATE PROCEDURE `cloud`.`INSERT_CATEGORY_IF_NOT_EXIST`(IN os_name VARCHAR(255))
BEGIN
IF NOT EXISTS ((SELECT 1 FROM `cloud`.`guest_os_category` WHERE name = os_name))
THEN
INSERT INTO `cloud`.`guest_os_category` (name, uuid)
VALUES (os_name, UUID())
; END IF
; END;
CALL `cloud`.`INSERT_CATEGORY_IF_NOT_EXIST`('Fedora');
CALL `cloud`.`INSERT_CATEGORY_IF_NOT_EXIST`('Rocky Linux');
CALL `cloud`.`INSERT_CATEGORY_IF_NOT_EXIST`('Alma Linux');
-- Move existing guest OS to new categories
DROP PROCEDURE IF EXISTS `cloud`.`UPDATE_CATEGORY_FOR_GUEST_OSES`;
CREATE PROCEDURE `cloud`.`UPDATE_CATEGORY_FOR_GUEST_OSES`(IN category_name VARCHAR(255), IN os_name VARCHAR(255))
BEGIN
DECLARE category_id BIGINT
; SELECT `id` INTO category_id
FROM `cloud`.`guest_os_category`
WHERE `name` = category_name
LIMIT 1
; IF category_id IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Category not found'
; END IF
; UPDATE `cloud`.`guest_os`
SET `category_id` = category_id
WHERE `display_name` LIKE CONCAT('%', os_name, '%')
; END;
CALL `cloud`.`UPDATE_CATEGORY_FOR_GUEST_OSES`('Rocky Linux', 'Rocky Linux');
CALL `cloud`.`UPDATE_CATEGORY_FOR_GUEST_OSES`('Alma Linux', 'Alma Linux');
CALL `cloud`.`UPDATE_CATEGORY_FOR_GUEST_OSES`('Fedora', 'Fedora');
-- Move existing guest OS whose category will be deleted to Other category
DROP PROCEDURE IF EXISTS `cloud`.`UPDATE_NEW_AND_DELETE_OLD_CATEGORY_FOR_GUEST_OS`;
CREATE PROCEDURE `cloud`.`UPDATE_NEW_AND_DELETE_OLD_CATEGORY_FOR_GUEST_OS`(IN to_category_name VARCHAR(255), IN from_category_name VARCHAR(255))
BEGIN
DECLARE done INT DEFAULT 0
; DECLARE to_category_id BIGINT
; SELECT id INTO to_category_id
FROM `cloud`.`guest_os_category`
WHERE `name` = to_category_name
LIMIT 1
; IF to_category_id IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'ToCategory not found'
; END IF
; UPDATE `cloud`.`guest_os`
SET `category_id` = to_category_id
WHERE `category_id` = (SELECT `id` FROM `cloud`.`guest_os_category` WHERE `name` = from_category_name)
; UPDATE `cloud`.`guest_os_category` SET `removed`=now() WHERE `name` = from_category_name
; END;
CALL `cloud`.`UPDATE_NEW_AND_DELETE_OLD_CATEGORY_FOR_GUEST_OS`('Other', 'Novel');
CALL `cloud`.`UPDATE_NEW_AND_DELETE_OLD_CATEGORY_FOR_GUEST_OS`('Other', 'None');
CALL `cloud`.`UPDATE_NEW_AND_DELETE_OLD_CATEGORY_FOR_GUEST_OS`('Other', 'Unix');
CALL `cloud`.`UPDATE_NEW_AND_DELETE_OLD_CATEGORY_FOR_GUEST_OS`('Other', 'Mac');
-- Add featured column for cloud.guest_os_category
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.guest_os_category', 'featured', 'tinyint(1) NOT NULL DEFAULT 0 COMMENT "whether the category is featured or not" AFTER `uuid`');
UPDATE `cloud`.`guest_os_category` SET featured = 1;
-- Add sort_key column for cloud.guest_os_category
CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.guest_os_category', 'sort_key', 'int NOT NULL DEFAULT 0 COMMENT "sort key used for customising sort method" AFTER `featured`');
-- Update sort order for all guest OS categories
UPDATE `cloud`.`guest_os_category`
SET `sort_key` = CASE
WHEN `name` = 'Ubuntu' THEN 1
WHEN `name` = 'Debian' THEN 2
WHEN `name` = 'Fedora' THEN 3
WHEN `name` = 'CentOS' THEN 4
WHEN `name` = 'Rocky Linux' THEN 5
WHEN `name` = 'Alma Linux' THEN 6
WHEN `name` = 'Oracle' THEN 7
WHEN `name` = 'RedHat' THEN 8
WHEN `name` = 'SUSE' THEN 9
WHEN `name` = 'Windows' THEN 10
WHEN `name` = 'Other' THEN 11
ELSE `sort_key`
END;
-- End: Changes for Guest OS category cleanup

View File

@ -41,6 +41,7 @@ SELECT
`vm_template`.`guest_os_id` AS `guest_os_id`,
`guest_os`.`uuid` AS `guest_os_uuid`,
`guest_os`.`display_name` AS `guest_os_name`,
`guest_os`.`category_id` AS `guest_os_category_id`,
`vm_template`.`bootable` AS `bootable`,
`vm_template`.`prepopulate` AS `prepopulate`,
`vm_template`.`cross_zones` AS `cross_zones`,

View File

@ -21,7 +21,11 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
@ -44,8 +48,11 @@ import com.cloud.host.dao.HostDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.Storage;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.VMTemplateZoneVO;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.GenericSearchBuilder;
import com.cloud.utils.db.JoinBuilder;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
@ -55,6 +62,9 @@ public class VMTemplateDaoImplTest {
@Mock
HostDao hostDao;
@Mock
VMTemplateZoneDao templateZoneDao;
@Spy
@InjectMocks
VMTemplateDaoImpl templateDao = new VMTemplateDaoImpl();
@ -186,4 +196,107 @@ public class VMTemplateDaoImplTest {
VMTemplateVO result = templateDao.findLatestTemplateByTypeAndHypervisorAndArch(hypervisorType, arch, type);
assertNull(result);
}
private void mockTemplateZoneJoin() {
VMTemplateZoneVO templateZoneVO = mock(VMTemplateZoneVO.class);
SearchBuilder<VMTemplateZoneVO> templateZoneVOSearchBuilder = mock(SearchBuilder.class);
when(templateZoneVOSearchBuilder.entity()).thenReturn(templateZoneVO);
when(templateZoneDao.createSearchBuilder()).thenReturn(templateZoneVOSearchBuilder);
}
@Test
public void testListTemplateIsoByArchAndZone_WithDataCenterId() {
Long dataCenterId = 1L;
CPU.CPUArch arch = CPU.CPUArch.getDefault();
Boolean isIso = true;
VMTemplateVO templateVO = mock(VMTemplateVO.class);
GenericSearchBuilder<VMTemplateVO, Long> searchBuilder = mock(GenericSearchBuilder.class);
when(searchBuilder.entity()).thenReturn(templateVO);
SearchCriteria<Long>searchCriteria = mock(SearchCriteria.class);
when(templateDao.createSearchBuilder(Long.class)).thenReturn(searchBuilder);
when(searchBuilder.create()).thenReturn(searchCriteria);
mockTemplateZoneJoin();
doReturn(new ArrayList<>()).when(templateDao).customSearch(searchCriteria, null);
List<Long> result = templateDao.listTemplateIsoByArchVnfAndZone(dataCenterId, arch, isIso, false);
assertNotNull(result);
verify(searchBuilder, times(1)).select(null, SearchCriteria.Func.DISTINCT, templateVO.getGuestOSId());
verify(searchBuilder, times(1)).and(eq("state"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("type"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("arch"), any(), eq(SearchCriteria.Op.EQ));
verify(searchBuilder, times(1)).and(eq("isIso"), any(), eq(SearchCriteria.Op.EQ));
verify(searchBuilder, times(1)).join(eq("templateZoneSearch"), any(), any(), any(), eq(JoinBuilder.JoinType.INNER));
verify(templateDao, times(1)).customSearch(searchCriteria, null);
}
@Test
public void testListTemplateIsoByArchAndZone_WithoutDataCenterId() {
Long dataCenterId = null;
CPU.CPUArch arch = CPU.CPUArch.getDefault();
Boolean isIso = false;
VMTemplateVO templateVO = mock(VMTemplateVO.class);
GenericSearchBuilder<VMTemplateVO, Long> searchBuilder = mock(GenericSearchBuilder.class);
when(searchBuilder.entity()).thenReturn(templateVO);
SearchCriteria<Long>searchCriteria = mock(SearchCriteria.class);
when(templateDao.createSearchBuilder(Long.class)).thenReturn(searchBuilder);
when(searchBuilder.create()).thenReturn(searchCriteria);
doReturn(new ArrayList<>()).when(templateDao).customSearch(searchCriteria, null);
List<Long> result = templateDao.listTemplateIsoByArchVnfAndZone(dataCenterId, arch, isIso, false);
assertNotNull(result);
verify(searchBuilder, times(1)).select(null, SearchCriteria.Func.DISTINCT, templateVO.getGuestOSId());
verify(searchBuilder, times(1)).and(eq("state"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("type"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("arch"), any(), eq(SearchCriteria.Op.EQ));
verify(searchBuilder, times(1)).and(eq("isIso"), any(), eq(SearchCriteria.Op.NEQ));
verify(searchBuilder, never()).join(eq("templateZoneSearch"), any(), any(), any(), eq(JoinBuilder.JoinType.INNER));
verify(templateDao, times(1)).customSearch(searchCriteria, null);
}
@Test
public void testListTemplateIsoByArchAndZone_WithoutArch() {
Long dataCenterId = 1L;
CPU.CPUArch arch = null;
Boolean isIso = true;
VMTemplateVO templateVO = mock(VMTemplateVO.class);
GenericSearchBuilder<VMTemplateVO, Long> searchBuilder = mock(GenericSearchBuilder.class);
when(searchBuilder.entity()).thenReturn(templateVO);
SearchCriteria<Long>searchCriteria = mock(SearchCriteria.class);
when(templateDao.createSearchBuilder(Long.class)).thenReturn(searchBuilder);
when(searchBuilder.create()).thenReturn(searchCriteria);
mockTemplateZoneJoin();
doReturn(new ArrayList<>()).when(templateDao).customSearch(searchCriteria, null);
List<Long> result = templateDao.listTemplateIsoByArchVnfAndZone(dataCenterId, arch, isIso, false);
assertNotNull(result);
verify(searchBuilder, times(1)).select(null, SearchCriteria.Func.DISTINCT, templateVO.getGuestOSId());
verify(searchBuilder, times(1)).and(eq("state"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("type"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("arch"), any(), eq(SearchCriteria.Op.EQ));
verify(searchBuilder, times(1)).and(eq("isIso"), any(), eq(SearchCriteria.Op.EQ));
verify(searchBuilder, times(1)).join(eq("templateZoneSearch"), any(), any(), any(), eq(JoinBuilder.JoinType.INNER));
verify(templateDao, times(1)).customSearch(searchCriteria, null);
}
@Test
public void testListTemplateIsoByArchAndZone_WithoutIsIso() {
Long dataCenterId = 1L;
CPU.CPUArch arch = CPU.CPUArch.getDefault();
Boolean isIso = null;
VMTemplateVO templateVO = mock(VMTemplateVO.class);
GenericSearchBuilder<VMTemplateVO, Long> searchBuilder = mock(GenericSearchBuilder.class);
when(searchBuilder.entity()).thenReturn(templateVO);
SearchCriteria<Long>searchCriteria = mock(SearchCriteria.class);
when(templateDao.createSearchBuilder(Long.class)).thenReturn(searchBuilder);
when(searchBuilder.create()).thenReturn(searchCriteria);
mockTemplateZoneJoin();
doReturn(new ArrayList<>()).when(templateDao).customSearch(searchCriteria, null);
List<Long> result = templateDao.listTemplateIsoByArchVnfAndZone(dataCenterId, arch, isIso, false);
assertNotNull(result);
verify(searchBuilder, times(1)).select(null, SearchCriteria.Func.DISTINCT, templateVO.getGuestOSId());
verify(searchBuilder, times(1)).and(eq("state"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("type"), any(), eq(SearchCriteria.Op.IN));
verify(searchBuilder, times(1)).and(eq("arch"), any(), eq(SearchCriteria.Op.EQ));
verify(searchBuilder, never()).and(eq("isIso"), any(), eq(SearchCriteria.Op.NEQ));
verify(searchBuilder, never()).and(eq("isIso"), any(), eq(SearchCriteria.Op.EQ));
verify(searchBuilder, times(1)).join(eq("templateZoneSearch"), any(), any(), any(), eq(JoinBuilder.JoinType.INNER));
verify(templateDao, times(1)).customSearch(searchCriteria, null);
}
}

View File

@ -17,6 +17,7 @@
package com.cloud.dao;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@ -56,6 +57,13 @@ public class EntityManagerImpl extends ManagerBase implements EntityManager {
return dao.findByUuid(uuid);
}
@Override
public <T> List<T> listByUuids(Class<T> entityType, Collection<String> uuids) {
// Finds and returns a unique VO using uuid, null if entity not found in db
GenericDao<? extends T, String> dao = (GenericDao<? extends T, String>)GenericDaoBase.getDao(entityType);
return (List<T>)dao.listByUuids(uuids);
}
@Override
public <T> T findByUuidIncludingRemoved(Class<T> entityType, String uuid) {
// Finds and returns a unique VO using uuid, null if entity not found in db

View File

@ -17,6 +17,8 @@
package com.cloud.utils.db;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@ -56,6 +58,10 @@ public interface GenericDao<T, ID extends Serializable> {
// Finds one unique VO using uuid
T findByUuid(String uuid);
default List<T> listByUuids(Collection<String> uuids) {
return new ArrayList<>();
}
// Finds one unique VO using uuid including removed entities
T findByUuidIncludingRemoved(String uuid);

View File

@ -1006,6 +1006,17 @@ public abstract class GenericDaoBase<T, ID extends Serializable> extends Compone
return findOneBy(sc);
}
@Override
@DB()
public List<T> listByUuids(final Collection<String> uuids) {
if (org.apache.commons.collections.CollectionUtils.isEmpty(uuids)) {
return Collections.emptyList();
}
SearchCriteria<T> sc = createSearchCriteria();
sc.addAnd("uuid", SearchCriteria.Op.IN, uuids.toArray());
return listBy(sc);
}
@Override
@DB()
public T findByUuidIncludingRemoved(final String uuid) {

View File

@ -39,16 +39,6 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import com.cloud.bgp.ASNumber;
import com.cloud.bgp.ASNumberRange;
import com.cloud.dc.ASNumberRangeVO;
import com.cloud.dc.ASNumberVO;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.dao.ASNumberDao;
import com.cloud.dc.dao.ASNumberRangeDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.BucketVO;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.ControlledEntity.ACLType;
import org.apache.cloudstack.affinity.AffinityGroup;
@ -63,12 +53,12 @@ import org.apache.cloudstack.api.BaseResponseWithAssociatedNetwork;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.command.user.job.QueryAsyncJobResultCmd;
import org.apache.cloudstack.api.response.ASNRangeResponse;
import org.apache.cloudstack.api.response.ASNumberResponse;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.ApplicationLoadBalancerInstanceResponse;
import org.apache.cloudstack.api.response.ApplicationLoadBalancerResponse;
import org.apache.cloudstack.api.response.ApplicationLoadBalancerRuleResponse;
import org.apache.cloudstack.api.response.ASNRangeResponse;
import org.apache.cloudstack.api.response.ASNumberResponse;
import org.apache.cloudstack.api.response.AsyncJobResponse;
import org.apache.cloudstack.api.response.AutoScalePolicyResponse;
import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse;
@ -99,10 +89,10 @@ import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.api.response.DomainRouterResponse;
import org.apache.cloudstack.api.response.EventResponse;
import org.apache.cloudstack.api.response.ExtractResponse;
import org.apache.cloudstack.api.response.SharedFSResponse;
import org.apache.cloudstack.api.response.FirewallResponse;
import org.apache.cloudstack.api.response.FirewallRuleResponse;
import org.apache.cloudstack.api.response.GlobalLoadBalancerResponse;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.GuestOSResponse;
import org.apache.cloudstack.api.response.GuestOsMappingResponse;
import org.apache.cloudstack.api.response.GuestVlanRangeResponse;
@ -164,6 +154,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.ServiceResponse;
import org.apache.cloudstack.api.response.SharedFSResponse;
import org.apache.cloudstack.api.response.Site2SiteCustomerGatewayResponse;
import org.apache.cloudstack.api.response.Site2SiteVpnConnectionResponse;
import org.apache.cloudstack.api.response.Site2SiteVpnGatewayResponse;
@ -230,10 +221,10 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.storage.sharedfs.query.vo.SharedFSJoinVO;
import org.apache.cloudstack.storage.object.Bucket;
import org.apache.cloudstack.storage.object.ObjectStore;
import org.apache.cloudstack.storage.sharedfs.SharedFS;
import org.apache.cloudstack.storage.sharedfs.query.vo.SharedFSJoinVO;
import org.apache.cloudstack.usage.Usage;
import org.apache.cloudstack.usage.UsageService;
import org.apache.cloudstack.usage.UsageTypes;
@ -241,8 +232,8 @@ import org.apache.cloudstack.vm.UnmanagedInstanceTO;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.cloud.agent.api.VgpuTypesInfo;
import com.cloud.api.query.ViewResponseHelper;
@ -271,6 +262,8 @@ import com.cloud.api.query.vo.UserVmJoinVO;
import com.cloud.api.query.vo.VolumeJoinVO;
import com.cloud.api.query.vo.VpcOfferingJoinVO;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.bgp.ASNumber;
import com.cloud.bgp.ASNumberRange;
import com.cloud.capacity.Capacity;
import com.cloud.capacity.CapacityVO;
import com.cloud.capacity.dao.CapacityDaoImpl.SummedCapacity;
@ -279,6 +272,8 @@ import com.cloud.configuration.Resource.ResourceOwnerType;
import com.cloud.configuration.Resource.ResourceType;
import com.cloud.configuration.ResourceCount;
import com.cloud.configuration.ResourceLimit;
import com.cloud.dc.ASNumberRangeVO;
import com.cloud.dc.ASNumberVO;
import com.cloud.dc.ClusterDetailsDao;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.DataCenter;
@ -289,7 +284,11 @@ import com.cloud.dc.Pod;
import com.cloud.dc.StorageNetworkIpRange;
import com.cloud.dc.Vlan;
import com.cloud.dc.Vlan.VlanType;
import com.cloud.dc.VlanDetailsVO;
import com.cloud.dc.VlanVO;
import com.cloud.dc.dao.ASNumberDao;
import com.cloud.dc.dao.ASNumberRangeDao;
import com.cloud.dc.dao.VlanDetailsDao;
import com.cloud.domain.Domain;
import com.cloud.domain.DomainVO;
import com.cloud.event.Event;
@ -299,6 +298,7 @@ import com.cloud.gpu.GPU;
import com.cloud.host.ControlState;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.hypervisor.HypervisorCapabilities;
import com.cloud.network.GuestVlan;
import com.cloud.network.GuestVlanRange;
@ -377,10 +377,13 @@ import com.cloud.projects.ProjectAccount;
import com.cloud.projects.ProjectInvitation;
import com.cloud.region.ha.GlobalLoadBalancerRule;
import com.cloud.resource.RollingMaintenanceManager;
import com.cloud.resource.icon.ResourceIconVO;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceIconManager;
import com.cloud.server.ResourceTag;
import com.cloud.server.ResourceTag.ResourceObjectType;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.storage.BucketVO;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.GuestOS;
@ -521,6 +524,8 @@ public class ApiResponseHelper implements ResponseGenerator {
BgpPeerDao bgpPeerDao;
@Inject
RoutedIpv4Manager routedIpv4Manager;
@Inject
ResourceIconManager resourceIconManager;
@Override
public UserResponse createUserResponse(User user) {
@ -3902,6 +3907,30 @@ public class ApiResponseHelper implements ResponseGenerator {
return response;
}
@Override
public GuestOSCategoryResponse createGuestOSCategoryResponse(GuestOsCategory guestOsCategory) {
return createGuestOSCategoryResponse(guestOsCategory, true);
}
@Override
public GuestOSCategoryResponse createGuestOSCategoryResponse(GuestOsCategory guestOsCategory, boolean showIcon) {
GuestOSCategoryResponse categoryResponse = new GuestOSCategoryResponse();
categoryResponse.setId(guestOsCategory.getUuid());
categoryResponse.setName(guestOsCategory.getName());
categoryResponse.setFeatured(guestOsCategory.isFeatured());
categoryResponse.setCreated(guestOsCategory.getCreated());
if (showIcon) {
ResourceIconVO resourceIcon = ApiDBUtils.getResourceIconByResourceUUID(guestOsCategory.getUuid(),
ResourceObjectType.GuestOsCategory);
if (resourceIcon != null) {
ResourceIconResponse iconResponse = ApiDBUtils.newResourceIconResponse(resourceIcon);
categoryResponse.setResourceIconResponse(iconResponse);
}
}
categoryResponse.setObjectName("oscategory");
return categoryResponse;
}
@Override
public GuestOSResponse createGuestOSResponse(GuestOS guestOS) {
GuestOSResponse response = new GuestOSResponse();
@ -5482,4 +5511,39 @@ public class ApiResponseHelper implements ResponseGenerator {
SharedFSJoinVO sharedFSView = ApiDBUtils.newSharedFSView(sharedFS);
return ApiDBUtils.newSharedFSResponse(view, sharedFSView);
}
protected Map<String, ResourceIcon> getResourceIconsUsingOsCategory(List<TemplateResponse> responses) {
Set<Long> guestOsCategoryIds = responses.stream().map(TemplateResponse::getOsTypeCategoryId).collect(Collectors.toSet());
Map<Long, ResourceIcon> guestOsCategoryIcons =
resourceIconManager.getByResourceTypeAndIds(ResourceTag.ResourceObjectType.GuestOsCategory,
guestOsCategoryIds);
Map<String, ResourceIcon> vmIcons = new HashMap<>();
for (TemplateResponse response : responses) {
vmIcons.put(response.getId(), guestOsCategoryIcons.get(response.getOsTypeCategoryId()));
}
return vmIcons;
}
@Override
public void updateTemplateIsoResponsesForIcons(List<TemplateResponse> responses,
ResourceTag.ResourceObjectType type) {
if (CollectionUtils.isEmpty(responses)) {
return;
}
Set<String> uuids = responses.stream().map(TemplateResponse::getId).collect(Collectors.toSet());
Map<String, ResourceIcon> templateIcons = resourceIconManager.getByResourceTypeAndUuids(type, uuids);
List<TemplateResponse> noTemplateIconResponses = responses
.stream()
.filter(r -> !templateIcons.containsKey(r.getId()))
.collect(Collectors.toList());
templateIcons.putAll(getResourceIconsUsingOsCategory(noTemplateIconResponses));
for (TemplateResponse response : responses) {
ResourceIcon icon = templateIcons.get(response.getId());
if (icon == null) {
continue;
}
ResourceIconResponse iconResponse = createResourceIconResponse(icon);
response.setResourceIconResponse(iconResponse);
}
}
}

View File

@ -316,6 +316,7 @@ import com.cloud.storage.VolumeApiServiceImpl;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.BucketDao;
import com.cloud.storage.dao.DiskOfferingDao;
import com.cloud.storage.dao.GuestOSDao;
import com.cloud.storage.dao.StoragePoolHostDao;
import com.cloud.storage.dao.StoragePoolTagsDao;
import com.cloud.storage.dao.VMTemplateDao;
@ -643,6 +644,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
@Inject
HostPodDao podDao;
@Inject
GuestOSDao guestOSDao;
private SearchCriteria<ServiceOfferingJoinVO> getMinimumCpuServiceOfferingJoinSearchCriteria(int cpu) {
SearchCriteria<ServiceOfferingJoinVO> sc = _srvOfferingJoinDao.createSearchCriteria();
SearchCriteria<ServiceOfferingJoinVO> sc1 = _srvOfferingJoinDao.createSearchCriteria();
@ -4791,7 +4795,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
null, cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), cmd.getStoragePoolId(),
cmd.getImageStoreId(), hypervisorType, showDomr, cmd.listInReadyState(), permittedAccounts, caller,
listProjectResourcesCriteria, tags, showRemovedTmpl, cmd.getIds(), parentTemplateId, cmd.getShowUnique(),
templateType, isVnf, cmd.getArch());
templateType, isVnf, cmd.getArch(), cmd.getOsCategoryId());
}
private Pair<List<TemplateJoinVO>, Integer> searchForTemplatesInternal(Long templateId, String name, String keyword,
@ -4800,7 +4804,7 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
boolean showDomr, boolean onlyReady, List<Account> permittedAccounts, Account caller,
ListProjectResourcesCriteria listProjectResourcesCriteria, Map<String, String> tags,
boolean showRemovedTmpl, List<Long> ids, Long parentTemplateId, Boolean showUnique, String templateType,
Boolean isVnf, CPU.CPUArch arch) {
Boolean isVnf, CPU.CPUArch arch, Long osCategoryId) {
// check if zone is configured, if not, just return empty list
List<HypervisorType> hypers = null;
@ -4828,10 +4832,13 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
if (storagePoolId != null) {
SearchBuilder<VMTemplateStoragePoolVO> storagePoolSb = templatePoolDao.createSearchBuilder();
storagePoolSb.and("pool_id", storagePoolSb.entity().getPoolId(), SearchCriteria.Op.EQ);
sb.join("storagePool", storagePoolSb, storagePoolSb.entity().getTemplateId(), sb.entity().getId(), JoinBuilder.JoinType.INNER);
}
if (osCategoryId != null) {
sb.and("guestOsIdIN", sb.entity().getGuestOSId(), Op.IN);
}
SearchCriteria<TemplateJoinVO> sc = sb.create();
if (imageStoreId != null) {
@ -4846,6 +4853,15 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
sc.setJoinParameters("storagePool", "pool_id", storagePoolId);
}
if (osCategoryId != null) {
List<Long> guestOsIds = guestOSDao.listIdsByCategoryId(osCategoryId);
if (CollectionUtils.isNotEmpty(guestOsIds)) {
sc.setParameters("guestOsIdIN", guestOsIds.toArray());
} else {
return new Pair<>(new ArrayList<>(), 0);
}
}
// verify templateId parameter and specially handle it
if (templateId != null) {
template = _templateDao.findByIdIncludingRemoved(templateId); // Done for backward compatibility - Bug-5221
@ -5231,7 +5247,8 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
return searchForTemplatesInternal(cmd.getId(), cmd.getIsoName(), cmd.getKeyword(), isoFilter, true, cmd.isBootable(),
cmd.getPageSizeVal(), cmd.getStartIndex(), cmd.getZoneId(), cmd.getStoragePoolId(), cmd.getImageStoreId(),
hypervisorType, true, cmd.listInReadyState(), permittedAccounts, caller, listProjectResourcesCriteria,
tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null, cmd.getArch());
tags, showRemovedISO, null, null, cmd.getShowUnique(), null, null,
cmd.getArch(), cmd.getOsCategoryId());
}
@Override

View File

@ -28,54 +28,53 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import com.cloud.deployasis.DeployAsIsConstants;
import com.cloud.deployasis.TemplateDeployAsIsDetailVO;
import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.VnfTemplateDetailVO;
import com.cloud.storage.VnfTemplateNicVO;
import com.cloud.storage.dao.VnfTemplateDetailsDao;
import com.cloud.storage.dao.VnfTemplateNicDao;
import com.cloud.user.dao.UserDataDao;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.response.VnfNicResponse;
import org.apache.cloudstack.api.response.VnfTemplateResponse;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.utils.security.DigestHelper;
import org.springframework.stereotype.Component;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.response.ChildTemplateResponse;
import org.apache.cloudstack.api.response.TemplateResponse;
import org.apache.cloudstack.api.response.VnfNicResponse;
import org.apache.cloudstack.api.response.VnfTemplateResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateState;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.query.QueryService;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreVO;
import org.apache.cloudstack.utils.security.DigestHelper;
import org.springframework.stereotype.Component;
import com.cloud.api.ApiDBUtils;
import com.cloud.api.ApiResponseHelper;
import com.cloud.api.query.vo.ResourceTagJoinVO;
import com.cloud.api.query.vo.TemplateJoinVO;
import com.cloud.deployasis.DeployAsIsConstants;
import com.cloud.deployasis.TemplateDeployAsIsDetailVO;
import com.cloud.deployasis.dao.TemplateDeployAsIsDetailsDao;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.storage.DataStoreRole;
import com.cloud.storage.Storage;
import com.cloud.storage.Storage.TemplateType;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VMTemplateStorageResourceAssoc.Status;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.VnfTemplateDetailVO;
import com.cloud.storage.VnfTemplateNicVO;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.storage.dao.VMTemplateDetailsDao;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VnfTemplateDetailsDao;
import com.cloud.storage.dao.VnfTemplateNicDao;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.user.dao.UserDataDao;
import com.cloud.utils.Pair;
import com.cloud.utils.db.Filter;
import com.cloud.utils.db.SearchBuilder;
@ -262,6 +261,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation<Templa
templateResponse.setOsTypeId(template.getGuestOSUuid());
templateResponse.setOsTypeName(template.getGuestOSName());
templateResponse.setOsTypeCategoryId(template.getGuestOSCategoryId());
// populate owner.
ApiResponseHelper.populateOwner(templateResponse, template);
@ -402,6 +402,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation<Templa
response.setFormat(result.getFormat());
response.setOsTypeId(result.getGuestOSUuid());
response.setOsTypeName(result.getGuestOSName());
response.setOsTypeCategoryId(result.getGuestOSCategoryId());
response.setBootable(result.isBootable());
response.setHypervisor(result.getHypervisorType().getHypervisorDisplayName());
response.setDynamicallyScalable(result.isDynamicallyScalable());
@ -490,6 +491,7 @@ public class TemplateJoinDaoImpl extends GenericDaoBaseWithTagInformation<Templa
isoResponse.setOsTypeId(iso.getGuestOSUuid());
isoResponse.setOsTypeName(iso.getGuestOSName());
isoResponse.setOsTypeCategoryId(iso.getGuestOSCategoryId());
isoResponse.setBits(iso.getBits());
isoResponse.setPasswordEnabled(iso.isEnablePassword());

View File

@ -27,19 +27,19 @@ import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import com.cloud.cpu.CPU;
import com.cloud.user.Account;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.util.CPUArchConverter;
import org.apache.cloudstack.util.HypervisorTypeConverter;
import com.cloud.cpu.CPU;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Storage;
import com.cloud.storage.VMTemplateStorageResourceAssoc.Status;
import com.cloud.template.VirtualMachineTemplate;
import com.cloud.template.VirtualMachineTemplate.State;
import com.cloud.user.Account;
import com.cloud.utils.db.GenericDao;
import org.apache.cloudstack.util.CPUArchConverter;
import org.apache.cloudstack.util.HypervisorTypeConverter;
@Entity
@Table(name = "template_view")
@ -112,6 +112,9 @@ public class TemplateJoinVO extends BaseViewWithTagInformationVO implements Cont
@Column(name = "guest_os_name")
private String guestOSName;
@Column(name = "guest_os_category_id")
private Long guestOSCategoryId;
@Column(name = "bootable")
private boolean bootable = true;
@ -405,6 +408,10 @@ public class TemplateJoinVO extends BaseViewWithTagInformationVO implements Cont
return guestOSName;
}
public Long getGuestOSCategoryId() {
return guestOSCategoryId;
}
public boolean isBootable() {
return bootable;
}

View File

@ -16,8 +16,11 @@
// under the License.
package com.cloud.resourceicon;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.inject.Inject;
@ -247,4 +250,18 @@ public class ResourceIconManagerImpl extends ManagerBase implements ResourceIcon
sc.setParameters("resourceType", resourceType);
return resourceIconDao.search(sc, null);
}
@Override
public Map<Long, ResourceIcon> getByResourceTypeAndIds(ResourceTag.ResourceObjectType resourceType, Collection<Long> resourceIds) {
List<ResourceIconVO> icons = resourceIconDao.listByResourceTypeAndIds(resourceType, resourceIds);
return icons.stream().collect(Collectors.toMap(ResourceIconVO::getResourceId, Function.identity()));
}
@Override
public Map<String, ResourceIcon> getByResourceTypeAndUuids(ResourceTag.ResourceObjectType resourceType, Collection<String> resourceUuids) {
List<ResourceIconVO> icons = resourceIconDao.listByResourceTypeAndUuids(resourceType, resourceUuids);
return icons.stream().collect(Collectors.toMap(ResourceIconVO::getResourceUuid, Function.identity()));
}
}

View File

@ -89,12 +89,15 @@ import org.apache.cloudstack.api.command.admin.domain.ListDomainsCmd;
import org.apache.cloudstack.api.command.admin.domain.ListDomainsCmdByAdmin;
import org.apache.cloudstack.api.command.admin.domain.MoveDomainCmd;
import org.apache.cloudstack.api.command.admin.domain.UpdateDomainCmd;
import org.apache.cloudstack.api.command.admin.guest.AddGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.AddGuestOsCmd;
import org.apache.cloudstack.api.command.admin.guest.AddGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.guest.DeleteGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.GetHypervisorGuestOsNamesCmd;
import org.apache.cloudstack.api.command.admin.guest.ListGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.guest.RemoveGuestOsCmd;
import org.apache.cloudstack.api.command.admin.guest.RemoveGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsCmd;
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsMappingCmd;
import org.apache.cloudstack.api.command.admin.host.AddHostCmd;
@ -643,6 +646,8 @@ import org.apache.cloudstack.utils.identity.ManagementServerNode;
import org.apache.cloudstack.vm.lease.VMLeaseManager;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.cloud.agent.AgentManager;
@ -2752,29 +2757,114 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
@Override
public Pair<List<? extends GuestOsCategory>, Integer> listGuestOSCategoriesByCriteria(final ListGuestOsCategoriesCmd cmd) {
final Filter searchFilter = new Filter(GuestOSCategoryVO.class, "id", true, cmd.getStartIndex(), cmd.getPageSizeVal());
final Filter searchFilter = new Filter(GuestOSCategoryVO.class, "sortKey", true,
cmd.getStartIndex(), cmd.getPageSizeVal());
searchFilter.addOrderBy(GuestOSCategoryVO.class, "id", true);
final Long id = cmd.getId();
final String name = cmd.getName();
final String keyword = cmd.getKeyword();
final Boolean featured = cmd.isFeatured();
final Boolean isIso = cmd.isIso();
final Boolean isVnf = cmd.isVnf();
final Long zoneId = cmd.getZoneId();
final CPU.CPUArch arch = cmd.getArch();
final SearchCriteria<GuestOSCategoryVO> sc = _guestOSCategoryDao.createSearchCriteria();
final SearchBuilder<GuestOSCategoryVO> sb = _guestOSCategoryDao.createSearchBuilder();
sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
sb.and("name", sb.entity().getName(), SearchCriteria.Op.LIKE);
sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE);
sb.and("featured", sb.entity().isFeatured(), SearchCriteria.Op.EQ);
if (ObjectUtils.anyNotNull(zoneId, arch, isIso, isVnf)) {
final SearchBuilder<GuestOSVO> guestOsSearch = _guestOSDao.createSearchBuilder();
guestOsSearch.and("ids", guestOsSearch.entity().getId(), SearchCriteria.Op.IN);
sb.join("guestOsSearch", guestOsSearch, guestOsSearch.entity().getCategoryId(), sb.entity().getId(),
JoinType.INNER);
guestOsSearch.done();
sb.groupBy(sb.entity().getId());
}
sb.done();
SearchCriteria<GuestOSCategoryVO> sc = sb.create();
if (id != null) {
sc.addAnd("id", SearchCriteria.Op.EQ, id);
sc.setParameters("id", id);
}
if (name != null) {
sc.addAnd("name", SearchCriteria.Op.LIKE, "%" + name + "%");
sc.setParameters("name", "%" + name + "%");
}
if (keyword != null) {
sc.addAnd("name", SearchCriteria.Op.LIKE, "%" + keyword + "%");
sc.setParameters("name", "%" + keyword + "%");
}
if (featured != null) {
sc.setParameters("featured", featured);
}
if (ObjectUtils.anyNotNull(zoneId, arch, isIso, isVnf)) {
List<Long> guestOsIds = templateDao.listTemplateIsoByArchVnfAndZone(zoneId, arch, isIso, isVnf);
if (CollectionUtils.isEmpty(guestOsIds)) {
return new Pair<>(Collections.emptyList(), 0);
}
sc.setJoinParameters("guestOsSearch", "ids", guestOsIds.toArray());
}
final Pair<List<GuestOSCategoryVO>, Integer> result = _guestOSCategoryDao.searchAndCount(sc, searchFilter);
return new Pair<>(result.first(), result.second());
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_GUEST_OS_CATEGORY_ADD, eventDescription = "adding OS category")
public GuestOsCategory addGuestOsCategory(AddGuestOsCategoryCmd cmd) {
final String name = cmd.getName();
final boolean featured = cmd.isFeatured();
final GuestOSCategoryVO guestOSCategory = new GuestOSCategoryVO(name, featured);
GuestOsCategory guestOsCategory = _guestOSCategoryDao.persist(guestOSCategory);
CallContext.current().setEventResourceId(guestOsCategory.getId());
CallContext.current().setEventResourceType(ApiCommandResourceType.GuestOsCategory);
return guestOSCategory;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_GUEST_OS_CATEGORY_UPDATE, eventDescription = "updating OS category")
public GuestOsCategory updateGuestOsCategory(UpdateGuestOsCategoryCmd cmd) {
final long id = cmd.getId();
final String name = cmd.getName();
final Boolean featured = cmd.isFeatured();
Integer sortKey = cmd.getSortKey();
final GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(id);
if (guestOSCategory == null) {
throw new InvalidParameterValueException("Invalid OS category ID specified");
}
if (ObjectUtils.allNull(name, featured, sortKey)) {
return guestOSCategory;
}
if (StringUtils.isNotBlank(name)) {
guestOSCategory.setName(name);
}
if (featured != null) {
guestOSCategory.setFeatured(featured);
}
if (sortKey != null) {
guestOSCategory.setSortKey(sortKey);
}
if (!_guestOSCategoryDao.update(id, guestOSCategory)) {
return null;
}
return guestOSCategory;
}
@Override
@ActionEvent(eventType = EventTypes.EVENT_GUEST_OS_CATEGORY_DELETE, eventDescription = "deleting OS category")
public boolean deleteGuestOsCategory(DeleteGuestOsCategoryCmd cmd) {
final long id = cmd.getId();
final GuestOSCategoryVO guestOSCategory = _guestOSCategoryDao.findById(id);
if (guestOSCategory == null) {
throw new InvalidParameterValueException("Invalid OS category ID specified");
}
List<Long> guestOses = _guestOSDao.listIdsByCategoryId(id);
if (!guestOses.isEmpty()) {
throw new InvalidParameterValueException(String.format(
"Unable to delete the OS category. %d guest OS exist for it.", guestOses.size()));
}
return _guestOSCategoryDao.remove(id);
}
@Override
public Pair<List<? extends GuestOSHypervisor>, Integer> listGuestOSMappingByCriteria(final ListGuestOsMappingCmd cmd) {
final String guestOsId = "guestOsId";
@ -2992,6 +3082,10 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
public GuestOS updateGuestOs(final UpdateGuestOsCmd cmd) {
final Long id = cmd.getId();
final String displayName = cmd.getOsDisplayName();
final Long osCategoryId = cmd.getOsCategoryId();
final Boolean display = cmd.getForDisplay();
final Map<String, String> details = cmd.getDetails();
boolean updateNeeded = false;
//check if guest OS exists
final GuestOS guestOsHandle = ApiDBUtils.findGuestOSById(id);
@ -2999,27 +3093,46 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
throw new InvalidParameterValueException("Guest OS not found. Please specify a valid ID for the Guest OS");
}
if (!guestOsHandle.getIsUserDefined()) {
throw new InvalidParameterValueException("Unable to modify system defined guest OS");
}
persistGuestOsDetails(cmd.getDetails(), id);
//Check if update is needed
if (displayName.equals(guestOsHandle.getDisplayName())) {
return guestOsHandle;
}
if (StringUtils.isNotBlank(displayName) && !displayName.equals(guestOsHandle.getDisplayName())) {
//Check if another Guest OS by same name exists
final GuestOS duplicate = ApiDBUtils.findGuestOSByDisplayName(displayName);
if (duplicate != null) {
throw new InvalidParameterValueException("The specified Guest OS name : " + displayName + " already exists. Please specify a unique guest OS name");
}
updateNeeded = true;
}
if (osCategoryId != null) {
if (_guestOSCategoryDao.findById(osCategoryId) == null) {
throw new InvalidParameterValueException("Invalid OS category ID specified");
}
updateNeeded = true;
}
if (!guestOsHandle.getIsUserDefined() && (StringUtils.isNotBlank(displayName) || MapUtils.isNotEmpty(details)
|| display != null)) {
throw new InvalidParameterValueException("Unable to modify system defined guest OS");
}
if (MapUtils.isNotEmpty(details)) {
persistGuestOsDetails(details, id);
}
if (!updateNeeded) {
return guestOsHandle;
}
final GuestOSVO guestOs = _guestOSDao.createForUpdate(id);
if (StringUtils.isNotBlank(displayName)) {
guestOs.setDisplayName(displayName);
}
if (cmd.getForDisplay() != null) {
guestOs.setDisplay(cmd.getForDisplay());
}
if (osCategoryId != null) {
guestOs.setCategoryId(osCategoryId);
}
if (_guestOSDao.update(id, guestOs)) {
return _guestOSDao.findById(id);
} else {
@ -3722,6 +3835,9 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(ListPortForwardingRulesCmd.class);
cmdList.add(UpdatePortForwardingRuleCmd.class);
cmdList.add(ListGuestOsCategoriesCmd.class);
cmdList.add(AddGuestOsCategoryCmd.class);
cmdList.add(UpdateGuestOsCategoryCmd.class);
cmdList.add(DeleteGuestOsCategoryCmd.class);
cmdList.add(ListGuestOsCmd.class);
cmdList.add(ListGuestOsMappingCmd.class);
cmdList.add(AddGuestOsCmd.class);

View File

@ -58,6 +58,7 @@ import com.cloud.server.ResourceManagerUtil;
import com.cloud.server.ResourceTag;
import com.cloud.service.ServiceOfferingVO;
import com.cloud.storage.DiskOfferingVO;
import com.cloud.storage.GuestOsCategory;
import com.cloud.storage.SnapshotPolicyVO;
import com.cloud.storage.SnapshotVO;
import com.cloud.storage.VMTemplateVO;
@ -117,6 +118,7 @@ public class ResourceManagerUtilImpl implements ResourceManagerUtil {
s_typeMap.put(ResourceTag.ResourceObjectType.NetworkOffering, NetworkOfferingVO.class);
s_typeMap.put(ResourceTag.ResourceObjectType.VpcOffering, VpcOfferingVO.class);
s_typeMap.put(ResourceTag.ResourceObjectType.Domain, DomainVO.class);
s_typeMap.put(ResourceTag.ResourceObjectType.GuestOsCategory, GuestOsCategory.class);
}
@Inject

View File

@ -16,37 +16,36 @@
// under the License.
package com.cloud.api;
import com.cloud.capacity.Capacity;
import com.cloud.configuration.Resource;
import com.cloud.domain.DomainVO;
import com.cloud.network.PublicIpQuarantine;
import com.cloud.network.as.AutoScaleVmGroup;
import com.cloud.network.as.AutoScaleVmGroupVO;
import com.cloud.network.as.AutoScaleVmProfileVO;
import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.storage.VMTemplateVO;
import com.cloud.usage.UsageVO;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.User;
import com.cloud.user.UserData;
import com.cloud.user.UserDataVO;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserDataDao;
import com.cloud.utils.net.Ip;
import com.cloud.vm.NicSecondaryIp;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse;
import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse;
import org.apache.cloudstack.api.response.DirectDownloadCertificateResponse;
import org.apache.cloudstack.api.response.GuestOSCategoryResponse;
import org.apache.cloudstack.api.response.IpQuarantineResponse;
import org.apache.cloudstack.api.response.NicSecondaryIpResponse;
import org.apache.cloudstack.api.response.ResourceIconResponse;
import org.apache.cloudstack.api.response.TemplateResponse;
import org.apache.cloudstack.api.response.UnmanagedInstanceResponse;
import org.apache.cloudstack.api.response.UsageRecordResponse;
import org.apache.cloudstack.context.CallContext;
@ -63,21 +62,38 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Field;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;
import com.cloud.capacity.Capacity;
import com.cloud.configuration.Resource;
import com.cloud.domain.DomainVO;
import com.cloud.network.PublicIpQuarantine;
import com.cloud.network.as.AutoScaleVmGroup;
import com.cloud.network.as.AutoScaleVmGroupVO;
import com.cloud.network.as.AutoScaleVmProfileVO;
import com.cloud.network.as.dao.AutoScaleVmGroupVmMapDao;
import com.cloud.network.dao.IPAddressDao;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.network.dao.LoadBalancerVO;
import com.cloud.network.dao.NetworkServiceMapDao;
import com.cloud.network.dao.NetworkVO;
import com.cloud.resource.icon.ResourceIconVO;
import com.cloud.server.ResourceIcon;
import com.cloud.server.ResourceIconManager;
import com.cloud.server.ResourceTag;
import com.cloud.storage.GuestOsCategory;
import com.cloud.storage.VMTemplateVO;
import com.cloud.usage.UsageVO;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.AccountVO;
import com.cloud.user.User;
import com.cloud.user.UserData;
import com.cloud.user.UserDataVO;
import com.cloud.user.UserVO;
import com.cloud.user.dao.UserDataDao;
import com.cloud.utils.net.Ip;
import com.cloud.vm.NicSecondaryIp;
@RunWith(MockitoJUnitRunner.class)
public class ApiResponseHelperTest {
@ -105,6 +121,9 @@ public class ApiResponseHelperTest {
@Mock
IPAddressDao ipAddressDaoMock;
@Mock
ResourceIconManager resourceIconManager;
@Spy
@InjectMocks
ApiResponseHelper apiResponseHelper = new ApiResponseHelper();
@ -481,4 +500,135 @@ public class ApiResponseHelperTest {
Assert.assertTrue(apiResponseHelper.capacityListingForSingleNonGpuType(List.of(c1, c2)));
Assert.assertFalse(apiResponseHelper.capacityListingForSingleNonGpuType(List.of(c1, c2, c3)));
}
@Test
public void testCreateGuestOSCategoryResponse_WithResourceIcon() {
GuestOsCategory guestOsCategory = Mockito.mock(GuestOsCategory.class);
ResourceIconVO resourceIconVO = Mockito.mock(ResourceIconVO.class);
String uuid = UUID.randomUUID().toString();
String name = "Ubuntu";
boolean featured = true;
Mockito.when(guestOsCategory.getUuid()).thenReturn(uuid);
Mockito.when(guestOsCategory.getName()).thenReturn(name);
Mockito.when(guestOsCategory.isFeatured()).thenReturn(featured);
ResourceIconResponse mockIconResponse = Mockito.mock(ResourceIconResponse.class);
try (MockedStatic<ApiDBUtils> ignored = Mockito.mockStatic(ApiDBUtils.class)) {
Mockito.when(ApiDBUtils.getResourceIconByResourceUUID(uuid, ResourceTag.ResourceObjectType.GuestOsCategory)).thenReturn(resourceIconVO);
Mockito.when(ApiDBUtils.newResourceIconResponse(resourceIconVO)).thenReturn(mockIconResponse);
GuestOSCategoryResponse response = apiResponseHelper.createGuestOSCategoryResponse(guestOsCategory);
Assert.assertNotNull(response);
Assert.assertEquals(uuid, response.getId());
Assert.assertEquals(name, response.getName());
Object obj = ReflectionTestUtils.getField(response, "featured");
if (obj == null) {
Assert.fail("Invalid featured value");
}
Assert.assertTrue((Boolean)obj);
obj = ReflectionTestUtils.getField(response, "resourceIconResponse");
Assert.assertNotNull(obj);
Assert.assertEquals("oscategory", response.getObjectName());
}
}
@Test
public void testCreateGuestOSCategoryResponse_WithoutResourceIcon() {
GuestOsCategory guestOsCategory = Mockito.mock(GuestOsCategory.class);
String uuid = "1234";
String name = "Ubuntu";
boolean featured = false;
Mockito.when(guestOsCategory.getUuid()).thenReturn(uuid);
Mockito.when(guestOsCategory.getName()).thenReturn(name);
Mockito.when(guestOsCategory.isFeatured()).thenReturn(featured);
try (MockedStatic<ApiDBUtils> ignored = Mockito.mockStatic(ApiDBUtils.class)) {
when(ApiDBUtils.getResourceIconByResourceUUID(uuid, ResourceTag.ResourceObjectType.GuestOsCategory)).thenReturn(null);
GuestOSCategoryResponse response = apiResponseHelper.createGuestOSCategoryResponse(guestOsCategory);
Assert.assertNotNull(response);
Assert.assertEquals(uuid, response.getId());
Assert.assertEquals(name, response.getName());
Object obj = ReflectionTestUtils.getField(response, "featured");
if (obj == null) {
Assert.fail("Invalid featured value");
}
Assert.assertFalse((Boolean)obj);
obj = ReflectionTestUtils.getField(response, "resourceIconResponse");
Assert.assertNull(obj);
Assert.assertEquals("oscategory", response.getObjectName());
}
}
@Test
public void testCreateGuestOSCategoryResponse_WithShowIconFalse() {
GuestOsCategory guestOsCategory = Mockito.mock(GuestOsCategory.class);
Mockito.when(guestOsCategory.getUuid()).thenReturn(UUID.randomUUID().toString());
try (MockedStatic<ApiDBUtils> mockedStatic = Mockito.mockStatic(ApiDBUtils.class)) {
GuestOSCategoryResponse response = apiResponseHelper.createGuestOSCategoryResponse(guestOsCategory, false);
Assert.assertNotNull(response);
mockedStatic.verify(() -> ApiDBUtils.getResourceIconByResourceUUID(Mockito.any(), Mockito.any()),
Mockito.never());
}
}
@Test
public void testGetResourceIconsUsingOsCategory_withValidData() {
TemplateResponse template1 = Mockito.mock(TemplateResponse.class);
when(template1.getId()).thenReturn("t1");
when(template1.getOsTypeCategoryId()).thenReturn(100L);
TemplateResponse template2 = Mockito.mock(TemplateResponse.class);
when(template2.getId()).thenReturn("t2");
when(template2.getOsTypeCategoryId()).thenReturn(200L);
List<TemplateResponse> responses = Arrays.asList(template1, template2);
Map<Long, ResourceIcon> icons = new HashMap<>();
ResourceIcon icon1 = Mockito.mock(ResourceIcon.class);
ResourceIcon icon2 = Mockito.mock(ResourceIcon.class);
icons.put(100L, icon1);
icons.put(200L, icon2);
when(resourceIconManager.getByResourceTypeAndIds(Mockito.eq(ResourceTag.ResourceObjectType.GuestOsCategory), Mockito.anySet()))
.thenReturn(icons);
Map<String, ResourceIcon> result = apiResponseHelper.getResourceIconsUsingOsCategory(responses);
assertEquals(2, result.size());
assertEquals(icon1, result.get("t1"));
assertEquals(icon2, result.get("t2"));
}
@Test
public void testGetResourceIconsUsingOsCategory_missingIcons() {
TemplateResponse template1 = Mockito.mock(TemplateResponse.class);
when(template1.getId()).thenReturn("t1");
when(template1.getOsTypeCategoryId()).thenReturn(100L);
List<TemplateResponse> responses = List.of(template1);
when(resourceIconManager.getByResourceTypeAndIds(Mockito.eq(ResourceTag.ResourceObjectType.GuestOsCategory), Mockito.anySet())).thenReturn(Collections.emptyMap());
Map<String, ResourceIcon> result = apiResponseHelper.getResourceIconsUsingOsCategory(responses);
assertTrue(result.containsKey("t1"));
assertNull(result.get("t1"));
}
@Test
public void testUpdateTemplateIsoResponsesForIcons_withMixedIcons() {
TemplateResponse template1 = Mockito.mock(TemplateResponse.class);
when(template1.getId()).thenReturn("t1");
TemplateResponse template2 = Mockito.mock(TemplateResponse.class);
when(template2.getId()).thenReturn("t2");
List<TemplateResponse> responses = Arrays.asList(template1, template2);
Map<String, ResourceIcon> isoIcons = new HashMap<>();
isoIcons.put("t1", Mockito.mock(ResourceIcon.class));
when(resourceIconManager.getByResourceTypeAndUuids(ResourceTag.ResourceObjectType.ISO, Set.of("t1", "t2")))
.thenReturn(isoIcons);
Map<String, ResourceIcon> fallbackIcons = Map.of("t2", Mockito.mock(ResourceIcon.class));
Mockito.doReturn(fallbackIcons).when(apiResponseHelper).getResourceIconsUsingOsCategory(Mockito.anyList());
ResourceIconResponse iconResponse1 = new ResourceIconResponse();
ResourceIconResponse iconResponse2 = new ResourceIconResponse();
Mockito.doReturn(iconResponse1).when(apiResponseHelper).createResourceIconResponse(isoIcons.get("t1"));
Mockito.doReturn(iconResponse2).when(apiResponseHelper).createResourceIconResponse(fallbackIcons.get("t2"));
apiResponseHelper.updateTemplateIsoResponsesForIcons(responses, ResourceTag.ResourceObjectType.ISO);
verify(template1).setResourceIconResponse(iconResponse1);
verify(template2).setResourceIconResponse(iconResponse2);
}
@Test
public void testUpdateTemplateIsoResponsesForIcons_emptyInput() {
apiResponseHelper.updateTemplateIsoResponsesForIcons(Collections.emptyList(),
ResourceTag.ResourceObjectType.Template);
Mockito.verify(resourceIconManager, Mockito.never()).getByResourceTypeAndUuids(Mockito.any(),
Mockito.anyCollection());
}
}

View File

@ -16,6 +16,56 @@
// under the License.
package com.cloud.server;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd;
import org.apache.cloudstack.api.command.admin.guest.AddGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.DeleteGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsCategoryCmd;
import org.apache.cloudstack.api.command.user.address.ListPublicIpAddressesCmd;
import org.apache.cloudstack.api.command.user.guest.ListGuestOsCategoriesCmd;
import org.apache.cloudstack.api.command.user.ssh.RegisterSSHKeyPairCmd;
import org.apache.cloudstack.api.command.user.userdata.DeleteUserDataCmd;
import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd;
import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd;
import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.framework.config.ConfigDepot;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
import org.apache.cloudstack.userdata.UserDataManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.test.util.ReflectionTestUtils;
import com.cloud.cpu.CPU;
import com.cloud.dc.Vlan.VlanType;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.InvalidParameterValueException;
@ -26,7 +76,12 @@ import com.cloud.host.dao.HostDetailsDao;
import com.cloud.network.IpAddress;
import com.cloud.network.IpAddressManagerImpl;
import com.cloud.network.dao.IPAddressVO;
import com.cloud.storage.GuestOSCategoryVO;
import com.cloud.storage.GuestOSVO;
import com.cloud.storage.GuestOsCategory;
import com.cloud.storage.VMTemplateVO;
import com.cloud.storage.dao.GuestOSCategoryDao;
import com.cloud.storage.dao.GuestOSDao;
import com.cloud.storage.dao.VMTemplateDao;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
@ -46,46 +101,6 @@ import com.cloud.vm.UserVmDetailVO;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.UserVmDetailsDao;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.command.admin.config.ListCfgsByCmd;
import org.apache.cloudstack.api.command.user.address.ListPublicIpAddressesCmd;
import org.apache.cloudstack.api.command.user.ssh.RegisterSSHKeyPairCmd;
import org.apache.cloudstack.api.command.user.userdata.DeleteUserDataCmd;
import org.apache.cloudstack.api.command.user.userdata.ListUserDataCmd;
import org.apache.cloudstack.api.command.user.userdata.RegisterUserDataCmd;
import org.apache.cloudstack.config.Configuration;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.framework.config.ConfigDepot;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.config.impl.ConfigurationVO;
import org.apache.cloudstack.userdata.UserDataManager;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class ManagementServerImplTest {
@ -118,7 +133,7 @@ public class ManagementServerImplTest {
UserDataDao _userDataDao;
@Mock
VMTemplateDao _templateDao;
VMTemplateDao templateDao;
@Mock
AnnotationDao annotationDao;
@ -129,9 +144,6 @@ public class ManagementServerImplTest {
@Mock
UserDataManager userDataManager;
@Spy
ManagementServerImpl spy = new ManagementServerImpl();
@Mock
UserVmDetailsDao userVmDetailsDao;
@ -147,23 +159,22 @@ public class ManagementServerImplTest {
@Mock
DomainDao domainDao;
@Mock
GuestOSCategoryDao _guestOSCategoryDao;
@Mock
GuestOSDao _guestOSDao;
@Spy
@InjectMocks
ManagementServerImpl spy = new ManagementServerImpl();
private AutoCloseable closeable;
@Before
public void setup() throws IllegalAccessException, NoSuchFieldException {
closeable = MockitoAnnotations.openMocks(this);
CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class));
spy._accountMgr = _accountMgr;
spy.userDataDao = _userDataDao;
spy.templateDao = _templateDao;
spy._userVmDao = _userVmDao;
spy.annotationDao = annotationDao;
spy._UserVmDetailsDao = userVmDetailsDao;
spy._detailsDao = hostDetailsDao;
spy.userDataManager = userDataManager;
spy._configDao = configDao;
spy._configDepot = configDepot;
spy._domainDao = domainDao;
}
@After
@ -407,7 +418,7 @@ public class ManagementServerImplTest {
Mockito.when(userData.getId()).thenReturn(1L);
when(_userDataDao.findById(1L)).thenReturn(userData);
when(_templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(new ArrayList<VMTemplateVO>());
when(templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(new ArrayList<VMTemplateVO>());
when(_userVmDao.findByUserDataId(1L)).thenReturn(new ArrayList<UserVmVO>());
when(_userDataDao.remove(1L)).thenReturn(true);
@ -437,7 +448,7 @@ public class ManagementServerImplTest {
VMTemplateVO vmTemplateVO = Mockito.mock(VMTemplateVO.class);
List<VMTemplateVO> linkedTemplates = new ArrayList<>();
linkedTemplates.add(vmTemplateVO);
when(_templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(linkedTemplates);
when(templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(linkedTemplates);
spy.deleteUserData(cmd);
}
@ -461,7 +472,7 @@ public class ManagementServerImplTest {
Mockito.when(userData.getId()).thenReturn(1L);
when(_userDataDao.findById(1L)).thenReturn(userData);
when(_templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(new ArrayList<VMTemplateVO>());
when(templateDao.findTemplatesLinkedToUserdata(1L)).thenReturn(new ArrayList<VMTemplateVO>());
UserVmVO userVmVO = Mockito.mock(UserVmVO.class);
List<UserVmVO> vms = new ArrayList<>();
@ -740,4 +751,272 @@ public class ManagementServerImplTest {
Assert.assertEquals("0.85", result.first().get(0).getValue());
}
@Test
public void testAddGuestOsCategory() {
AddGuestOsCategoryCmd addCmd = Mockito.mock(AddGuestOsCategoryCmd.class);
String name = "Ubuntu";
boolean featured = true;
Mockito.when(addCmd.getName()).thenReturn(name);
Mockito.when(addCmd.isFeatured()).thenReturn(featured);
Mockito.doAnswer((Answer<GuestOSCategoryVO>) invocation -> (GuestOSCategoryVO)invocation.getArguments()[0]).when(_guestOSCategoryDao).persist(Mockito.any(GuestOSCategoryVO.class));
GuestOsCategory result = spy.addGuestOsCategory(addCmd);
Assert.assertNotNull(result);
Assert.assertEquals(name, result.getName());
Assert.assertEquals(featured, result.isFeatured());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).persist(any(GuestOSCategoryVO.class));
}
@Test
public void testUpdateGuestOsCategory() {
UpdateGuestOsCategoryCmd updateCmd = Mockito.mock(UpdateGuestOsCategoryCmd.class);
GuestOSCategoryVO guestOSCategory = new GuestOSCategoryVO("Old name", false);
long id = 1L;
String name = "Updated Name";
Boolean featured = true;
Integer sortKey = 10;
Mockito.when(updateCmd.getId()).thenReturn(id);
Mockito.when(updateCmd.getName()).thenReturn(name);
Mockito.when(updateCmd.isFeatured()).thenReturn(featured);
Mockito.when(updateCmd.getSortKey()).thenReturn(sortKey);
Mockito.when(_guestOSCategoryDao.findById(id)).thenReturn(guestOSCategory);
Mockito.when(_guestOSCategoryDao.update(Mockito.eq(id), any(GuestOSCategoryVO.class))).thenReturn(true);
GuestOsCategory result = spy.updateGuestOsCategory(updateCmd);
Assert.assertNotNull(result);
Assert.assertEquals(name, result.getName());
Assert.assertEquals(featured, result.isFeatured());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).findById(id);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).update(Mockito.eq(id), any(GuestOSCategoryVO.class));
}
@Test(expected = InvalidParameterValueException.class)
public void testUpdateGuestOsCategory_ThrowsExceptionWhenCategoryNotFound() {
UpdateGuestOsCategoryCmd updateCmd = Mockito.mock(UpdateGuestOsCategoryCmd.class);
long id = 1L;
when(updateCmd.getId()).thenReturn(id);
when(_guestOSCategoryDao.findById(id)).thenReturn(null);
spy.updateGuestOsCategory(updateCmd);
}
@Test
public void testUpdateGuestOsCategory_NoChanges() {
UpdateGuestOsCategoryCmd updateCmd = Mockito.mock(UpdateGuestOsCategoryCmd.class);
GuestOSCategoryVO guestOSCategory = new GuestOSCategoryVO("Old name", false);
long id = 1L;
when(updateCmd.getId()).thenReturn(id);
when(updateCmd.getName()).thenReturn(null);
when(updateCmd.isFeatured()).thenReturn(null);
when(updateCmd.getSortKey()).thenReturn(null);
when(_guestOSCategoryDao.findById(id)).thenReturn(guestOSCategory);
GuestOsCategory result = spy.updateGuestOsCategory(updateCmd);
Assert.assertNotNull(result);
Assert.assertNotNull(result.getName());
Assert.assertFalse(result.isFeatured());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).findById(id);
Mockito.verify(_guestOSCategoryDao, Mockito.never()).update(Mockito.eq(id), any(GuestOSCategoryVO.class));
}
@Test
public void testUpdateGuestOsCategory_UpdateNameOnly() {
UpdateGuestOsCategoryCmd updateCmd = Mockito.mock(UpdateGuestOsCategoryCmd.class);
GuestOSCategoryVO guestOSCategory = new GuestOSCategoryVO("Old name", false);
long id = 1L;
String name = "Updated Name";
Mockito.when(updateCmd.getId()).thenReturn(id);
Mockito.when(updateCmd.getName()).thenReturn(name);
Mockito.when(updateCmd.isFeatured()).thenReturn(null);
Mockito.when(updateCmd.getSortKey()).thenReturn(null);
Mockito.when(_guestOSCategoryDao.findById(id)).thenReturn(guestOSCategory);
Mockito.when(_guestOSCategoryDao.update(Mockito.eq(id), any(GuestOSCategoryVO.class))).thenReturn(true);
GuestOsCategory result = spy.updateGuestOsCategory(updateCmd);
Assert.assertNotNull(result);
Assert.assertEquals(name, result.getName());
Assert.assertFalse(result.isFeatured());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).findById(id);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).update(Mockito.eq(id), any(GuestOSCategoryVO.class));
}
@Test
public void testDeleteGuestOsCategory_Successful() {
DeleteGuestOsCategoryCmd deleteCmd = Mockito.mock(DeleteGuestOsCategoryCmd.class);
GuestOSCategoryVO guestOSCategory = Mockito.mock(GuestOSCategoryVO.class);
long id = 1L;
Mockito.when(deleteCmd.getId()).thenReturn(id);
Mockito.when(_guestOSCategoryDao.findById(id)).thenReturn(guestOSCategory);
Mockito.when(_guestOSDao.listIdsByCategoryId(id)).thenReturn(Arrays.asList());
Mockito.when(_guestOSCategoryDao.remove(id)).thenReturn(true);
boolean result = spy.deleteGuestOsCategory(deleteCmd);
Assert.assertTrue(result);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).findById(id);
Mockito.verify(_guestOSDao, Mockito.times(1)).listIdsByCategoryId(id);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).remove(id);
}
@Test(expected = InvalidParameterValueException.class)
public void testDeleteGuestOsCategory_ThrowsExceptionWhenCategoryNotFound() {
DeleteGuestOsCategoryCmd deleteCmd = Mockito.mock(DeleteGuestOsCategoryCmd.class);
long id = 1L;
Mockito.when(deleteCmd.getId()).thenReturn(id);
Mockito.when(_guestOSCategoryDao.findById(id)).thenReturn(null);
spy.deleteGuestOsCategory(deleteCmd);
}
@Test(expected = InvalidParameterValueException.class)
public void testDeleteGuestOsCategory_ThrowsExceptionWhenGuestOsExists() {
DeleteGuestOsCategoryCmd deleteCmd = Mockito.mock(DeleteGuestOsCategoryCmd.class);
GuestOSCategoryVO guestOSCategory = Mockito.mock(GuestOSCategoryVO.class);
long id = 1L;
Mockito.when(deleteCmd.getId()).thenReturn(id);
Mockito.when(_guestOSCategoryDao.findById(id)).thenReturn(guestOSCategory);
Mockito.when(_guestOSDao.listIdsByCategoryId(id)).thenReturn(Arrays.asList(1L));
spy.deleteGuestOsCategory(deleteCmd);
}
private void mockGuestOsJoin() {
GuestOSVO vo = mock(GuestOSVO.class);
SearchBuilder<GuestOSVO> sb = mock(SearchBuilder.class);
when(sb.entity()).thenReturn(vo);
when(_guestOSDao.createSearchBuilder()).thenReturn(sb);
}
@Test
public void testListGuestOSCategoriesByCriteria_Success() {
ListGuestOsCategoriesCmd listCmd = Mockito.mock(ListGuestOsCategoriesCmd.class);
GuestOSCategoryVO guestOSCategory = Mockito.mock(GuestOSCategoryVO.class);
Filter filter = Mockito.mock(Filter.class);
Long id = 1L;
String name = "Ubuntu";
String keyword = "Linux";
Boolean featured = true;
Long zoneId = 1L;
CPU.CPUArch arch = CPU.CPUArch.getDefault();
Boolean isIso = true;
Boolean isVnf = false;
Mockito.when(listCmd.getId()).thenReturn(id);
Mockito.when(listCmd.getName()).thenReturn(name);
Mockito.when(listCmd.getKeyword()).thenReturn(keyword);
Mockito.when(listCmd.isFeatured()).thenReturn(featured);
Mockito.when(listCmd.getZoneId()).thenReturn(zoneId);
Mockito.when(listCmd.getArch()).thenReturn(arch);
Mockito.when(listCmd.isIso()).thenReturn(isIso);
Mockito.when(listCmd.isVnf()).thenReturn(isVnf);
SearchBuilder<GuestOSCategoryVO> searchBuilder = Mockito.mock(SearchBuilder.class);
Mockito.when(searchBuilder.entity()).thenReturn(guestOSCategory);
SearchCriteria<GuestOSCategoryVO> searchCriteria = Mockito.mock(SearchCriteria.class);
Mockito.when(_guestOSCategoryDao.createSearchBuilder()).thenReturn(searchBuilder);
Mockito.when(searchBuilder.create()).thenReturn(searchCriteria);
Mockito.when(templateDao.listTemplateIsoByArchVnfAndZone(zoneId, arch, isIso, isVnf)).thenReturn(Arrays.asList(1L, 2L));
Pair<List<GuestOSCategoryVO>, Integer> mockResult = new Pair<>(Arrays.asList(guestOSCategory), 1);
mockGuestOsJoin();
Mockito.when(_guestOSCategoryDao.searchAndCount(Mockito.eq(searchCriteria), Mockito.any())).thenReturn(mockResult);
Pair<List<? extends GuestOsCategory>, Integer> result = spy.listGuestOSCategoriesByCriteria(listCmd);
Assert.assertNotNull(result);
Assert.assertEquals(1, result.second().intValue());
Assert.assertEquals(1, result.first().size());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).createSearchBuilder();
Mockito.verify(templateDao, Mockito.times(1)).listTemplateIsoByArchVnfAndZone(zoneId, arch, isIso, isVnf);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).searchAndCount(Mockito.eq(searchCriteria), Mockito.any());
}
@Test
public void testListGuestOSCategoriesByCriteria_NoResults() {
ListGuestOsCategoriesCmd listCmd = Mockito.mock(ListGuestOsCategoriesCmd.class);
GuestOSCategoryVO guestOSCategory = Mockito.mock(GuestOSCategoryVO.class);
Long id = 1L;
String name = "CentOS";
String keyword = "Linux";
Boolean featured = false;
Long zoneId = 1L;
CPU.CPUArch arch = CPU.CPUArch.getDefault();
Boolean isIso = false;
Boolean isVnf = false;
Mockito.when(listCmd.getId()).thenReturn(id);
Mockito.when(listCmd.getName()).thenReturn(name);
Mockito.when(listCmd.getKeyword()).thenReturn(keyword);
Mockito.when(listCmd.isFeatured()).thenReturn(featured);
Mockito.when(listCmd.getZoneId()).thenReturn(zoneId);
Mockito.when(listCmd.getArch()).thenReturn(arch);
Mockito.when(listCmd.isIso()).thenReturn(isIso);
Mockito.when(listCmd.isVnf()).thenReturn(isVnf);
SearchBuilder<GuestOSCategoryVO> searchBuilder = Mockito.mock(SearchBuilder.class);
Mockito.when(searchBuilder.entity()).thenReturn(guestOSCategory);
SearchCriteria<GuestOSCategoryVO> searchCriteria = Mockito.mock(SearchCriteria.class);
Mockito.when(_guestOSCategoryDao.createSearchBuilder()).thenReturn(searchBuilder);
Mockito.when(searchBuilder.create()).thenReturn(searchCriteria);
Mockito.when(templateDao.listTemplateIsoByArchVnfAndZone(zoneId, arch, isIso, isVnf)).thenReturn(Arrays.asList(1L, 2L));
Pair<List<GuestOSCategoryVO>, Integer> mockResult = new Pair<>(Arrays.asList(), 0);
Mockito.when(_guestOSCategoryDao.searchAndCount(Mockito.eq(searchCriteria), Mockito.any())).thenReturn(mockResult);
mockGuestOsJoin();
Pair<List<? extends GuestOsCategory>, Integer> result = spy.listGuestOSCategoriesByCriteria(listCmd);
Assert.assertNotNull(result);
Assert.assertEquals(0, result.second().intValue());
Assert.assertEquals(0, result.first().size());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).createSearchBuilder();
Mockito.verify(templateDao, Mockito.times(1)).listTemplateIsoByArchVnfAndZone(zoneId, arch, isIso, isVnf);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).searchAndCount(Mockito.eq(searchCriteria), Mockito.any());
}
@Test
public void testListGuestOSCategoriesByCriteria_NoGuestOsIdsFound() {
ListGuestOsCategoriesCmd listCmd = Mockito.mock(ListGuestOsCategoriesCmd.class);
GuestOSCategoryVO guestOSCategory = Mockito.mock(GuestOSCategoryVO.class);
Long id = 1L;
String name = "Ubuntu";
String keyword = "Linux";
Boolean featured = true;
Long zoneId = 1L;
CPU.CPUArch arch = CPU.CPUArch.getDefault();
Boolean isIso = true;
Boolean isVnf = false;
Mockito.when(listCmd.getId()).thenReturn(id);
Mockito.when(listCmd.getName()).thenReturn(name);
Mockito.when(listCmd.getKeyword()).thenReturn(keyword);
Mockito.when(listCmd.isFeatured()).thenReturn(featured);
Mockito.when(listCmd.getZoneId()).thenReturn(zoneId);
Mockito.when(listCmd.getArch()).thenReturn(arch);
Mockito.when(listCmd.isIso()).thenReturn(isIso);
Mockito.when(listCmd.isVnf()).thenReturn(isVnf);
SearchBuilder<GuestOSCategoryVO> searchBuilder = Mockito.mock(SearchBuilder.class);
Mockito.when(searchBuilder.entity()).thenReturn(guestOSCategory);
SearchCriteria<GuestOSCategoryVO> searchCriteria = Mockito.mock(SearchCriteria.class);
Mockito.when(_guestOSCategoryDao.createSearchBuilder()).thenReturn(searchBuilder);
Mockito.when(searchBuilder.create()).thenReturn(searchCriteria);
Mockito.when(templateDao.listTemplateIsoByArchVnfAndZone(zoneId, arch, isIso, isVnf)).thenReturn(Arrays.asList(1L, 2L));
Pair<List<GuestOSCategoryVO>, Integer> mockResult = new Pair<>(Arrays.asList(), 0);
when(_guestOSCategoryDao.searchAndCount(Mockito.eq(searchCriteria), Mockito.any())).thenReturn(mockResult);
mockGuestOsJoin();
Pair<List<? extends GuestOsCategory>, Integer> result = spy.listGuestOSCategoriesByCriteria(listCmd);
Assert.assertNotNull(result);
Assert.assertEquals(0, result.second().intValue());
Assert.assertEquals(0, result.first().size());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).createSearchBuilder();
Mockito.verify(templateDao, Mockito.times(1)).listTemplateIsoByArchVnfAndZone(zoneId, arch, isIso, isVnf);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).searchAndCount(Mockito.eq(searchCriteria), Mockito.any());
}
@Test
public void testListGuestOSCategoriesByCriteria_FilterById() {
ListGuestOsCategoriesCmd listCmd = Mockito.mock(ListGuestOsCategoriesCmd.class);
GuestOSCategoryVO guestOSCategory = Mockito.mock(GuestOSCategoryVO.class);
Long id = 1L;
Mockito.when(listCmd.getId()).thenReturn(id);
Mockito.when(listCmd.getZoneId()).thenReturn(null);
Mockito.when(listCmd.isIso()).thenReturn(null);
Mockito.when(listCmd.isVnf()).thenReturn(null);
SearchBuilder<GuestOSCategoryVO> searchBuilder = Mockito.mock(SearchBuilder.class);
Mockito.when(searchBuilder.entity()).thenReturn(guestOSCategory);
SearchCriteria<GuestOSCategoryVO> searchCriteria = Mockito.mock(SearchCriteria.class);
Mockito.when(_guestOSCategoryDao.createSearchBuilder()).thenReturn(searchBuilder);
Mockito.when(searchBuilder.create()).thenReturn(searchCriteria);
Pair<List<GuestOSCategoryVO>, Integer> mockResult = new Pair<>(Arrays.asList(guestOSCategory), 1);
Mockito.when(_guestOSCategoryDao.searchAndCount(Mockito.eq(searchCriteria), Mockito.any())).thenReturn(mockResult);
mockGuestOsJoin();
Pair<List<? extends GuestOsCategory>, Integer> result = spy.listGuestOSCategoriesByCriteria(listCmd);
Assert.assertNotNull(result);
Assert.assertEquals(1, result.second().intValue());
Assert.assertEquals(1, result.first().size());
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).createSearchBuilder();
Mockito.verify(searchCriteria, Mockito.times(1)).setParameters("id", id);
Mockito.verify(_guestOSCategoryDao, Mockito.times(1)).searchAndCount(Mockito.eq(searchCriteria), Mockito.any());
}
}

View File

@ -97,5 +97,8 @@
"basicZoneEnabled": true,
"multipleServer": false,
"allowSettingTheme": true,
"imageSelectionInterface": "modern",
"showUserCategoryForModernImageSelection": true,
"showAllCategoryForModernImageSelection": false,
"docHelpMappings": {}
}

View File

@ -87,8 +87,9 @@
"label.action.delete.egress.firewall": "Delete egress firewall rule",
"label.action.delete.firewall": "Delete firewall rule",
"label.action.delete.interface.static.route": "Remove Tungsten Fabric interface static route",
"label.action.delete.guest.os": "Delete guest os",
"label.action.delete.guest.os.hypervisor.mapping": "Delete guest os hypervisor mapping",
"label.action.delete.guest.os": "Delete guest OS",
"label.action.delete.guest.os.category": "Delete guest OS category",
"label.action.delete.guest.os.hypervisor.mapping": "Delete guest OS hypervisor mapping",
"label.action.delete.ip.range": "Delete IP range",
"label.action.delete.iso": "Delete ISO",
"label.action.delete.load.balancer": "Delete load balancer rule",
@ -263,8 +264,9 @@
"label.add.firewall": "Add firewall rule",
"label.add.firewallrule": "Add Firewall Rule",
"label.add.guest.network": "Add guest Network",
"label.add.guest.os": "Add guest os",
"label.add.guest.os.hypervisor.mapping": "Add guest os hypervisor mapping",
"label.add.guest.os": "Add guest OS",
"label.add.guest.os.category": "Add guest OS category",
"label.add.guest.os.hypervisor.mapping": "Add guest OS hypervisor mapping",
"label.add.host": "Add host",
"label.add.ingress.rule": "Add ingress rule",
"label.add.intermediate.certificate": "Add intermediate certificate",
@ -1069,6 +1071,8 @@
"label.guest.netmask": "Guest netmask",
"label.guest.networks": "Guest Networks",
"label.guest.os": "Guest OS",
"label.guest.os.category": "Guest OS Category",
"label.guest.os.categories": "Guest OS Categories",
"label.guest.os.hypervisor.mappings": "Guest OS mappings",
"label.guest.start.ip": "Guest start IP",
"label.guest.traffic": "Guest traffic",
@ -1143,6 +1147,8 @@
"label.ikelifetime": "IKE lifetime (second)",
"label.ikepolicy": "IKE policy",
"label.ikeversion": "IKE version",
"label.image": "Image",
"label.image.type": "Image type",
"label.images": "Images",
"label.imagestoreid": "Secondary Storage",
"label.import.backup.offering": "Import backup offering",
@ -1651,6 +1657,7 @@
"label.operator.equal": "Equals to",
"label.optional": "Optional",
"label.order": "Order",
"label.os": "Operating System",
"label.oscategoryid": "OS category",
"label.oscategoryname": "OS category name",
"label.osname": "OS name",
@ -1806,6 +1813,7 @@
"label.provisioningtype.fat": "Fat provisioning",
"label.provisioningtype.sparse": "Sparse provisioning",
"label.provisioningtype.thin": "Thin provisioning",
"label.public": "Public",
"label.publicmtu": "Public Interface MTU",
"label.public.interface": "Public interface",
"label.public.ip": "Public IP address",
@ -2305,7 +2313,8 @@
"label.tariffvalue": "Tariff value",
"label.tcp": "TCP",
"label.tcp.proxy": "TCP proxy",
"label.template": "Select a template",
"label.template": "Template",
"label.template.select": "Select a template",
"label.templatetag": "Tag",
"label.template.select.existing": "Select an existing template",
"label.template.temporary.import": "Use a temporary template for import",
@ -2698,6 +2707,7 @@
"message.action.delete.ingress.rule": "Please confirm that you want to delete this ingress rule.",
"message.action.delete.ipv4.subnet": "Please confirm that you want to delete this IPv4 subnet.",
"message.action.delete.guest.os": "Please confirm that you want to delete this guest os. System defined entry cannot be deleted.",
"message.action.delete.guest.os.category": "Please confirm that you want to delete this guest os category.",
"message.action.delete.guest.os.hypervisor.mapping": "Please confirm that you want to delete this guest os hypervisor mapping. System defined entry cannot be deleted.",
"message.action.delete.instance.group": "Please confirm that you want to delete the Instance group.",
"message.action.delete.interface.static.route": "Please confirm that you want to remove this interface Static Route?",
@ -3306,6 +3316,7 @@
"message.installwizard.tooltip.tungsten.provider.name": "Tungsten provider name is required",
"message.installwizard.tooltip.tungsten.provider.port": "Tungsten provider port is required",
"message.installwizard.tooltip.tungsten.provider.vrouterport": "Tungsten provider vrouter port is required",
"message.instance.architecture": "Please select Instance architecture",
"message.instances.managed": "Instances controlled by CloudStack.",
"message.instances.unmanaged": "Instances not controlled by CloudStack.",
"message.instances.migrate.vmware": "Instances that can be migrated from VMware.",

View File

@ -139,6 +139,10 @@ export default {
},
fetchResourceIcon (id) {
return new Promise((resolve, reject) => {
if (this.$store.getters.avatar) {
this.image = this.$store.getters.avatar
resolve(this.image)
}
api('listUsers', {
id: id,
showicon: true
@ -146,6 +150,7 @@ export default {
const response = json.listusersresponse.user || []
if (response?.[0]) {
this.image = response[0]?.icon?.base64image || ''
this.$store.commit('SET_AVATAR', this.image)
resolve(this.image)
}
}).catch(error => {

View File

@ -0,0 +1,150 @@
// 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.
<template>
<span>
<a-dropdown-button
type="primary"
:loading="loading"
v-if="allowed"
:disabled="!zones || zones.length === 0">
<rocket-outlined />
{{ $t('label.create.vm') }}
<template #icon><down-outlined /></template>
<template #overlay>
<a-menu type="primary" @click="handleDeployInstanceMenu">
<a-menu-item v-for="zone in zones" :key="zone.id">
<span v-if="zone.icon && zone.icon.base64image">
<resource-icon :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/>
</span>
<global-outlined v-else style="margin-right: 5px" />
{{ zone.name }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown-button>
</span>
</template>
<script>
import { api } from '@/api'
import ResourceIcon from '@/components/view/ResourceIcon'
export default {
name: 'ImageDeployInstanceButton',
components: {
ResourceIcon
},
props: {
resource: {
type: Object,
required: true
},
osCategoryId: {
type: String,
default: null
}
},
emits: ['update-zones'],
data () {
return {
imageApi: 'listTemplates',
loading: false,
zones: []
}
},
mounted () {
if (this.$route.meta.name === 'iso') {
this.imageApi = 'listIsos'
}
setTimeout(() => {
this.fetchData()
}, 100)
},
computed: {
allowed () {
return (this.$route.meta.name === 'template' ||
(this.$route.meta.name === 'iso' && this.resource.bootable))
}
},
methods: {
arrayHasItems (array) {
return array !== null && array !== undefined && Array.isArray(array) && array.length > 0
},
fetchData () {
this.fetchResourceData()
},
fetchResourceData () {
const params = {}
params.id = this.resource.id
params.templatefilter = 'executable'
params.listall = true
params.page = this.page
params.pagesize = this.pageSize
this.dataSource = []
this.itemCount = 0
this.fetchLoading = true
this.zones = []
api(this.imageApi, params).then(json => {
const imageResponse = json?.[this.imageApi.toLowerCase() + 'response']?.[this.$route.meta.name] || []
this.zones = imageResponse.map(i => ({
id: i.zoneid,
name: i.zonename
}))
}).catch(error => {
this.$notifyError(error)
this.loading = false
}).finally(() => {
if (this.zones.length !== 0) {
this.$emit('update-zones', this.zones)
}
this.fetchZonesForIcon()
})
},
fetchZonesForIcon () {
if (!this.zones) {
return
}
const zoneids = this.zones.map(z => z.id)
this.loading = true
api('listZones', { showicon: true, ids: zoneids.join(',') }).then(json => {
this.zones = json.listzonesresponse.zone || []
}).finally(() => {
this.loading = false
if (this.zones.length !== 0) {
this.$emit('update-zones', this.zones)
}
})
},
handleDeployInstanceMenu (e) {
const query = { zoneid: e.key }
query[this.$route.meta.name + 'id'] = this.resource.id
if (this.resource.arch) {
query.arch = this.resource.arch
}
if (this.osCategoryId) {
query.oscategoryid = this.osCategoryId
}
this.$router.push({
path: '/action/deployVirtualMachine',
query: query
})
}
}
}
</script>

View File

@ -30,11 +30,11 @@
<edit-outlined class="upload-icon"/>
</div>
<slot name="avatar">
<span v-if="(resource.icon && resource.icon.base64image || images.template || images.iso || resourceIcon) && !['router', 'systemvm', 'volume'].includes($route.path.split('/')[1])">
<resource-icon :image="getImage(resource.icon && resource.icon.base64image || images.template || images.iso || resourceIcon)" size="4x" style="margin-right: 5px"/>
<span v-if="resourceIcon && !['router', 'systemvm', 'volume'].includes($route.path.split('/')[1])">
<resource-icon :image="resourceIcon" size="4x" style="margin-right: 5px"/>
</span>
<span v-else>
<os-logo v-if="resource.ostypeid || resource.ostypename" :osId="resource.ostypeid" :osName="resource.ostypename" size="3x" @update-osname="setResourceOsType"/>
<os-logo v-if="resource.ostypeid || resource.ostypename || ['guestoscategory'].includes($route.path.split('/')[1])" :osId="resource.ostypeid" :osName="resource.ostypename || resource.name" size="3x" @update-osname="setResourceOsType"/>
<render-icon v-else-if="typeof $route.meta.icon ==='string'" style="font-size: 36px" :icon="$route.meta.icon" />
<font-awesome-icon
v-else-if="$route.meta.icon && Array.isArray($route.meta.icon)"
@ -158,11 +158,16 @@
<div class="resource-detail-item" v-if="resource.ostypename && resource.ostypeid">
<div class="resource-detail-item__label">{{ $t('label.ostypename') }}</div>
<div class="resource-detail-item__details">
<span v-if="resource.icon && resource.icon.base64image || images.template || images.iso">
<resource-icon :image="getImage(images.template || images.iso)" size="1x" style="margin-right: 5px"/>
<span v-if="images.guestoscategory">
<resource-icon :image="images.guestoscategory" size="1x" style="margin-right: 5px"/>
</span>
<os-logo v-else :osId="resource.ostypeid" :osName="resource.ostypename" size="lg" style="margin-left: -1px" />
<span style="margin-left: 8px">{{ resource.ostypename }}</span>
<span style="margin-left: 8px">
<router-link v-if="$router.resolve('/guestos/' + resource.ostypeid).matched[0].redirect !== '/exception/404'" :to="{ path: '/guestos/' + resource.ostypeid }">
{{ resource.ostypename }}
</router-link>
<span v-else>{{ resource.ostypename }}</span>
</span>
</div>
</div>
<div class="resource-detail-item" v-if="resource.ipaddress">
@ -449,7 +454,7 @@
<div class="resource-detail-item__label">{{ $t('label.project') }}</div>
<div class="resource-detail-item__details">
<span v-if="images.project">
<resource-icon :image="getImage(images.project)" size="1x" style="margin-right: 5px"/>
<resource-icon :image="images.project" size="1x" style="margin-right: 5px"/>
</span>
<project-outlined v-else />
<router-link v-if="!isStatic && resource.projectid" :to="{ path: '/project/' + resource.projectid }">{{ resource.project || resource.projectname || resource.projectid }}</router-link>
@ -546,7 +551,7 @@
<div class="resource-detail-item__label">{{ $t('label.vpcname') }}</div>
<div class="resource-detail-item__details">
<span v-if="images.vpc">
<resource-icon :image="getImage(images.vpc)" size="1x" style="margin-right: 5px"/>
<resource-icon :image="images.vpc" size="1x" style="margin-right: 5px"/>
</span>
<deployment-unit-outlined v-else />
<router-link :to="{ path: '/vpc/' + resource.vpcid }">{{ resource.vpcname || resource.vpcid }}</router-link>
@ -557,7 +562,7 @@
<div class="resource-detail-item__label">{{ $t('label.aclid') }}</div>
<div class="resource-detail-item__details">
<span v-if="images.acl">
<resource-icon :image="getImage(images.acl)" size="1x" style="margin-right: 5px"/>
<resource-icon :image="images.acl" size="1x" style="margin-right: 5px"/>
</span>
<deployment-unit-outlined v-else />
<router-link :to="{ path: '/acllist/' + resource.aclid }">{{ resource.aclname || resource.aclid }}</router-link>
@ -578,7 +583,7 @@
<div class="resource-detail-item" v-if="resource.templateid">
<div class="resource-detail-item__label">{{ resource.templateformat === 'ISO'? $t('label.iso') : $t('label.templatename') }}</div>
<div class="resource-detail-item__details">
<resource-icon v-if="resource.icon" :image="getImage(resource.icon.base64image)" size="1x" style="margin-right: 5px"/>
<resource-icon v-if="images.template || images.guestoscategory" :image="images.template || images.guestoscategory" size="1x" style="margin-right: 5px"/>
<SaveOutlined v-else />
<router-link :to="{ path: (resource.templateformat === 'ISO' ? '/iso/' : '/template/') + resource.templateid }">{{ resource.templatedisplaytext || resource.templatename || resource.templateid }} </router-link>
</div>
@ -586,7 +591,7 @@
<div class="resource-detail-item" v-if="resource.isoid">
<div class="resource-detail-item__label">{{ $t('label.isoname') }}</div>
<div class="resource-detail-item__details">
<resource-icon v-if="resource.icon" :image="getImage(resource.icon.base64image)" size="1x" style="margin-right: 5px"/>
<resource-icon v-if="images.iso || (resource.isoid === resource.templateid && images.guestoscategory)" :image="images.iso || images.guestoscategory" size="1x" style="margin-right: 5px"/>
<UsbOutlined v-else />
<router-link :to="{ path: '/iso/' + resource.isoid }">{{ resource.isodisplaytext || resource.isoname || resource.isoid }} </router-link>
</div>
@ -677,7 +682,7 @@
<div class="resource-detail-item__label">{{ $t('label.zone') }}</div>
<div class="resource-detail-item__details">
<span v-if="images.zone">
<resource-icon :image="getImage(images.zone)" size="1x" style="margin-right: 5px"/>
<resource-icon :image="images.zone" size="1x" style="margin-right: 5px"/>
</span>
<global-outlined v-else />
<router-link v-if="!isStatic && $router.resolve('/zone/' + resource.zoneid).matched[0].redirect !== '/exception/404'" :to="{ path: '/zone/' + resource.zoneid }">{{ resource.zone || resource.zonename || resource.zoneid }}</router-link>
@ -712,7 +717,7 @@
<div class="resource-detail-item__label">{{ $t('label.account') }}</div>
<div class="resource-detail-item__details">
<span v-if="images.account">
<resource-icon :image="getImage(images.account)" size="1x" style="margin-right: 5px"/>
<resource-icon :image="images.account" size="1x" style="margin-right: 5px"/>
</span>
<user-outlined v-else />
<router-link v-if="!isStatic && $store.getters.userInfo.roletype !== 'User'" :to="{ path: '/account', query: { name: resource.account, domainid: resource.domainid } }">{{ resource.account }}</router-link>
@ -730,7 +735,7 @@
<div class="resource-detail-item" v-if="resource.domainid">
<div class="resource-detail-item__label">{{ $t('label.domain') }}</div>
<div class="resource-detail-item__details">
<resource-icon v-if="images.domain" :image="getImage(images.domain)" size="1x" style="margin-right: 5px"/>
<resource-icon v-if="images.domain" :image="images.domain" size="1x" style="margin-right: 5px"/>
<block-outlined v-else />
<router-link v-if="!isStatic && $store.getters.userInfo.roletype !== 'User'" :to="{ path: '/domain/' + resource.domainid, query: { tab: 'details'} }">{{ resource.domain || resource.domainid }}</router-link>
<span v-else>{{ resource.domain || resource.domainid }}</span>
@ -788,6 +793,10 @@
</a-button>
</router-link>
</div>
<image-deploy-instance-button
v-if="'deployVirtualMachine' in $store.getters.apis && ['template', 'iso'].includes($route.meta.name)"
:resource="resource"
:osCategoryId="osCategoryId" />
</div>
<div class="account-center-tags" v-if="showKeys || resource.apikeyaccess">
@ -894,6 +903,7 @@ import UploadResourceIcon from '@/components/view/UploadResourceIcon'
import eventBus from '@/config/eventBus'
import ResourceIcon from '@/components/view/ResourceIcon'
import ResourceLabel from '@/components/widgets/ResourceLabel'
import ImageDeployInstanceButton from '@/components/view/ImageDeployInstanceButton.vue'
export default {
name: 'InfoCard',
@ -905,7 +915,8 @@ export default {
TooltipButton,
UploadResourceIcon,
ResourceIcon,
ResourceLabel
ResourceLabel,
ImageDeployInstanceButton
},
props: {
resource: {
@ -951,7 +962,8 @@ export default {
network: ''
},
newResource: {},
validLinks: {}
validLinks: {},
osCategoryId: null
}
},
watch: {
@ -975,9 +987,6 @@ export default {
}
this.updateResourceAdditionalData()
}
},
async templateIcon () {
this.getIcons()
}
},
created () {
@ -1007,22 +1016,31 @@ export default {
}
return [this.resource.keypairs.toString()]
},
templateIcon () {
return this.resource.templateid
isResourceShowingParentResourceIcon () {
const resourcesShowParentResourceIcon = ['guestos']
const routeName = this.$route.path.split('/')?.[1] || null
if (!routeName) {
return false
}
return resourcesShowParentResourceIcon.includes(routeName)
},
resourceIcon () {
if (this.$showIcon()) {
if (!this.$showIcon() && !this.isResourceShowingParentResourceIcon) {
return null
}
if (this.resource?.icon?.base64image) {
return this.resource.icon.base64image
}
if (this.resource?.resourceIcon?.base64image) {
return this.resource.resourceIcon.base64image
}
}
return null
return this.images.template || this.images.iso || this.images.guestoscategory || null
},
routeFromResourceType () {
return this.$getRouteFromResourceType(this.resource.resourcetype)
},
isModernImageSelection () {
return this.$config.imageSelectionInterface === undefined || this.$config.imageSelectionInterface === 'modern'
}
},
methods: {
@ -1037,6 +1055,15 @@ export default {
this.getTags()
}
}
const osId = this.resource.guestosid || this.resource.ostypeid
if (osId && 'listOsTypes' in this.$store.getters.apis) {
api('listOsTypes', { id: osId }).then(json => {
this.osCategoryId = json?.listostypesresponse?.ostype?.[0]?.oscategoryid || null
if (this.osCategoryId) {
this.fetchResourceIcon(this.osCategoryId, 'guestoscategory')
}
})
}
this.getIcons()
},
showUploadModal (show) {
@ -1048,43 +1075,34 @@ export default {
this.showUpload = false
}
},
getImage (image) {
return (image || this.resource?.icon?.base64image)
},
async getIcons () {
this.images = {
zone: '',
template: '',
iso: '',
domain: '',
account: '',
project: '',
vpc: '',
network: ''
}
getIcons () {
this.images = {}
if (this.resource.templateid) {
await this.fetchResourceIcon(this.resource.templateid, 'template')
this.fetchResourceIcon(this.resource.templateid, 'template')
}
if (this.resource.isoid) {
await this.fetchResourceIcon(this.resource.isoid, 'iso')
this.fetchResourceIcon(this.resource.isoid, 'iso')
}
if (this.resource.zoneid) {
await this.fetchResourceIcon(this.resource.zoneid, 'zone')
this.fetchResourceIcon(this.resource.zoneid, 'zone')
}
if (this.resource.domainid) {
await this.fetchResourceIcon(this.resource.domainid, 'domain')
this.fetchResourceIcon(this.resource.domainid, 'domain')
}
if (this.resource.account) {
await this.fetchAccount()
this.fetchAccount()
}
if (this.resource.projectid) {
await this.fetchResourceIcon(this.resource.projectid, 'project')
this.fetchResourceIcon(this.resource.projectid, 'project')
}
if (this.resource.vpcid) {
await this.fetchResourceIcon(this.resource.vpcid, 'vpc')
this.fetchResourceIcon(this.resource.vpcid, 'vpc')
}
if (this.resource.networkid) {
await this.fetchResourceIcon(this.resource.networkid, 'network')
this.fetchResourceIcon(this.resource.networkid, 'network')
}
if (this.resource.oscategoryid) {
this.fetchResourceIcon(this.resource.oscategoryid, 'guestoscategory')
}
},
fetchAccount () {
@ -1109,19 +1127,14 @@ export default {
resourcetype: type
}).then(json => {
const response = json.listresourceiconresponse.icon || []
if (response?.[0]) {
this.images[type] = response[0].base64image
this.images[type] = response?.[0]?.base64image || null
resolve(this.images)
} else {
this.images[type] = ''
resolve(this.images)
}
}).catch(error => {
reject(error)
})
})
} else {
this.images.type = ''
this.images.type = null
}
},
setData () {
@ -1252,7 +1265,6 @@ export default {
query[item.param] = this.resource.id
}
}
return query
}
}

View File

@ -58,7 +58,7 @@
</span>
<span v-if="$showIcon() && !['vm', 'vnfapp'].includes($route.path.split('/')[1])" style="margin-right: 5px">
<resource-icon v-if="$showIcon() && record.icon && record.icon.base64image" :image="record.icon.base64image" size="2x"/>
<os-logo v-else-if="record.ostypename" :osName="record.ostypename" size="xl" />
<os-logo v-else-if="record.ostypename || ['guestoscategory'].includes($route.path.split('/')[1])" :osName="record.ostypename || record.name" size="xl" />
<render-icon v-else-if="typeof $route.meta.icon ==='string'" style="font-size: 16px;" :icon="$route.meta.icon"/>
<render-icon v-else style="font-size: 16px;" :svgIcon="$route.meta.icon" />
</span>
@ -241,6 +241,15 @@
</span>
<span v-else>{{ text }}</span>
</template>
<template v-if="column.key === 'oscategoryname'">
<span v-if="('listOsCategories' in $store.getters.apis) && record.oscategoryid">
<router-link :to="{ path: '/guestoscategory/' + record.oscategoryid }">{{ text }}</router-link>
</span>
<span v-else>{{ text }}</span>
</template>
<template v-if="column.key === 'isuserdefined'">
<span>{{ text ? $t('label.yes') : $t('label.no') }}</span>
</template>
<template v-if="column.key === 'state'">
<status v-if="$route.path.startsWith('/host')" :text="getHostState(record)" displayText />
<status v-else :text="text ? text : ''" displayText :styles="{ 'min-width': '80px' }" />
@ -485,6 +494,9 @@
<template v-if="['startdate', 'enddate'].includes(column.key) && ['usage'].includes($route.path.split('/')[1])">
{{ $toLocaleDate(text.replace('\'T\'', ' ')) }}
</template>
<template v-if="['isfeatured'].includes(column.key) && ['guestoscategory'].includes($route.path.split('/')[1])">
{{ record.isfeatured ? $t('label.yes') : $t('label.no') }}
</template>
<template v-if="column.key === 'order'">
<div class="shift-btns">
<a-tooltip :name="text" placement="top">
@ -756,7 +768,7 @@ export default {
'vmsnapshot', 'backup', 'guestnetwork', 'vpc', 'publicip', 'vpnuser', 'vpncustomergateway', 'vnfapp',
'project', 'account', 'systemvm', 'router', 'computeoffering', 'systemoffering',
'diskoffering', 'backupoffering', 'networkoffering', 'vpcoffering', 'ilbvm', 'kubernetes', 'comment', 'buckets',
'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers'
'webhook', 'webhookdeliveries', 'sharedfs', 'ipv4subnets', 'asnumbers', 'guestos'
].includes(this.$route.name)
},
getDateAtTimeZone (date, timezone) {
@ -859,8 +871,9 @@ export default {
case 'vpcoffering':
apiString = 'updateVPCOffering'
break
default:
apiString = 'updateTemplate'
case 'guestoscategory':
apiString = 'updateOsCategory'
break
}
return apiString
},

View File

@ -16,7 +16,7 @@
// under the License.
<template>
<img :src="getImg()" :height="getDimensions()" :width="getDimensions()" :style="{ marginTop: (getDimensions() === 56 || ['deployVirtualMachine'].includes($route.path.split('/')[2])) ? '' : '-5px' }"/>
<img :src="computedImage" :height="dimensions" :width="dimensions" :style="{ marginTop: (dimensions === 56 || ['deployVirtualMachine'].includes($route.path.split('/')[2])) ? '' : '-5px' }"/>
</template>
<script>
export default {
@ -34,15 +34,17 @@ export default {
data () {
return {}
},
methods: {
getImg () {
computed: {
computedImage () {
if (!this.image) {
return null
}
if (this.image.startsWith('data:image/png')) {
return this.image
} else {
return 'data:image/png;charset=utf-8;base64, ' + this.image
}
return 'data:image/png;charset=utf-8;base64, ' + this.image
},
getDimensions () {
dimensions () {
const num = Number(this.size)
if (Number.isInteger(num) && num > 0) {
return num
@ -54,6 +56,8 @@ export default {
return 24
case '1x':
return 16
case 'os':
return 28
default:
return 16
}

View File

@ -309,7 +309,7 @@ export default {
'clusterid', 'podid', 'groupid', 'entitytype', 'accounttype', 'systemvmtype', 'scope', 'provider',
'type', 'scope', 'managementserverid', 'serviceofferingid',
'diskofferingid', 'networkid', 'usagetype', 'restartrequired',
'displaynetwork', 'guestiptype', 'usersource', 'arch'].includes(item)
'displaynetwork', 'guestiptype', 'usersource', 'arch', 'oscategoryid', 'templatetype'].includes(item)
) {
type = 'list'
} else if (item === 'tags') {
@ -458,6 +458,13 @@ export default {
this.fields[typeIndex].opts = this.$fetchCpuArchitectureTypes()
this.fields[typeIndex].loading = false
}
if (arrayField.includes('templatetype')) {
const typeIndex = this.fields.findIndex(item => item.name === 'templatetype')
this.fields[typeIndex].loading = true
this.fields[typeIndex].opts = this.$fetchTemplateTypes()
this.fields[typeIndex].loading = false
}
},
async fetchDynamicFieldData (arrayField, searchKeyword) {
const promises = []
@ -477,6 +484,7 @@ export default {
let networkIndex = -1
let usageTypeIndex = -1
let volumeIndex = -1
let osCategoryIndex = -1
if (arrayField.includes('type')) {
if (this.$route.path === '/alert') {
@ -580,6 +588,12 @@ export default {
promises.push(await this.fetchVolumes(searchKeyword))
}
if (arrayField.includes('oscategoryid')) {
osCategoryIndex = this.fields.findIndex(item => item.name === 'oscategoryid')
this.fields[osCategoryIndex].loading = true
promises.push(await this.fetchOsCategories(searchKeyword))
}
Promise.all(promises).then(response => {
if (typeIndex > -1) {
const types = response.filter(item => item.type === 'type')
@ -676,6 +690,13 @@ export default {
this.fields[usageTypeIndex].opts = this.sortArray(usageTypes[0].data)
}
}
if (osCategoryIndex > -1) {
const osCategories = response.filter(item => item.type === 'oscategoryid')
if (osCategories && osCategories.length > 0) {
this.fields[osCategoryIndex].opts = this.sortArray(osCategories[0].data)
}
}
}).finally(() => {
if (typeIndex > -1) {
this.fields[typeIndex].loading = false
@ -722,6 +743,9 @@ export default {
if (usageTypeIndex > -1) {
this.fields[usageTypeIndex].loading = false
}
if (osCategoryIndex > -1) {
this.fields[osCategoryIndex].loading = false
}
if (Array.isArray(arrayField)) {
this.fillFormFieldValues()
}
@ -1006,6 +1030,19 @@ export default {
})
})
},
fetchOsCategories (searchKeyword) {
return new Promise((resolve, reject) => {
api('listOsCategories', { showicon: true, keyword: searchKeyword }).then(json => {
const osCategories = json.listoscategoriesresponse.oscategory
resolve({
type: 'oscategoryid',
data: osCategories
})
}).catch(error => {
reject(error.response.headers['x-description'])
})
})
},
fetchGuestNetworkTypes () {
const types = []
if (this.apiName.indexOf('listNetworks') > -1) {

View File

@ -0,0 +1,154 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<div>
<a-radio-group
v-if="items.length <= maxBlocks"
v-model:value="localValue"
@change="handleChange()">
<a-row type="flex" :gutter="[horizontalGutter, verticalGutter]" justify="start">
<div v-for="item in items" :key="item.id">
<a-col :span="6">
<a-radio-button
:value="item.id"
style="border-width: 2px"
:class="blockRadioButtonClass">
<slot name="radio-option" :item="item"></slot>
</a-radio-button>
</a-col>
</div>
</a-row>
</a-radio-group>
<a-select
v-else
v-model:value="localValue"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="handleChange()"
:loading="loading"
v-focus="true">
<a-select-option v-for="item in items" :key="item.id" :label="item.name">
<slot name="select-option" :item="item"></slot>
</a-select-option>
</a-select>
</div>
</template>
<script>
export default {
name: 'BlockRadioGroupSelect',
props: {
blockSize: {
type: String,
default: 'large'
},
selectedValue: {
type: [String, Number],
default: ''
},
horizontalGutter: {
type: Number,
default: 16
},
verticalGutter: {
type: Number,
default: 18
},
items: {
type: Array,
default: () => []
},
maxBlocks: {
type: Number,
default: 8
},
loading: {
type: Boolean,
default: false
}
},
data () {
return {
localValue: this.selectedValue
}
},
watch: {
selectedValue (newValue) {
if (this.localValue === newValue) {
return
}
this.localValue = newValue
}
},
computed: {
blockRadioButtonClass () {
if (['square', 'medium', 'small'].includes(this.blockSize)) {
return this.blockSize + '-block-radio-button'
}
return 'large-block-radio-button'
}
},
emits: ['change'],
methods: {
handleChange () {
this.$emit('change', this.localValue)
}
}
}
</script>
<style lang="less" scoped>
.large-block-radio-button {
width:100%;
min-width: 345px;
height: 60px;
display: flex;
padding-left: 20px;
align-items: center;
}
.medium-block-radio-button {
width:100%;
min-width: 160px;
height: 60px;
display: flex;
padding-left: 20px;
align-items: center;
}
.small-block-radio-button {
width:100%;
min-width: 80px;
height: 60px;
display: flex;
padding-left: 20px;
align-items: center;
}
.square-block-radio-button {
width: 88px;
height: 88px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 0;
box-sizing: border-box;
}
</style>

View File

@ -21,7 +21,7 @@
{{ name }}
</template>
<font-awesome-icon
:icon="['fab', logo]"
:icon="logo"
:size="size"
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#666' }]"
/>
@ -50,7 +50,7 @@ export default {
data () {
return {
name: '',
osLogo: 'linux'
osLogo: ['fas', 'image']
}
},
computed: {
@ -87,35 +87,34 @@ export default {
}
})
},
getFontAwesomeIcon (name) {
return ['fab', name]
},
discoverOsLogo (name) {
this.name = name
const osname = name.toLowerCase()
if (osname.includes('centos')) {
this.osLogo = 'centos'
} else if (osname.includes('debian')) {
this.osLogo = 'debian'
} else if (osname.includes('ubuntu')) {
this.osLogo = 'ubuntu'
} else if (osname.includes('suse')) {
this.osLogo = 'suse'
} else if (osname.includes('redhat')) {
this.osLogo = 'redhat'
} else if (osname.includes('fedora')) {
this.osLogo = 'fedora'
} else if (osname.includes('linux')) {
this.osLogo = 'linux'
} else if (osname.includes('bsd')) {
this.osLogo = 'freebsd'
} else if (osname.includes('apple')) {
this.osLogo = 'apple'
} else if (osname.includes('window') || osname.includes('dos')) {
this.osLogo = 'windows'
} else if (osname.includes('oracle')) {
this.osLogo = 'java'
} else {
this.osLogo = 'linux'
}
this.$emit('update-osname', this.name)
const osname = name.toLowerCase()
const logos = [
{ name: 'centos' },
{ name: 'debian' },
{ name: 'ubuntu' },
{ name: 'suse' },
{ name: 'redhat' },
{ name: 'fedora' },
{ name: 'linux' },
{ name: 'bsd', alternate: 'freebsd' },
{ name: 'apple' },
{ name: 'macos', alternate: 'apple' },
{ name: 'window', alternate: 'windows' },
{ name: 'dos', alternate: 'windows' },
{ name: 'oracle', alternate: 'java' }
]
const match = logos.find(entry => osname.includes(entry.name))
if (match) {
this.osLogo = ['fab', match.alternate ? match.alternate : match.name]
return
}
this.osLogo = ['fas', 'image']
}
}
}

View File

@ -187,6 +187,56 @@ export default {
}
]
},
{
name: 'guestoscategory',
title: 'label.guest.os.categories',
docHelp: 'adminguide/guest_os.html#guest-os-categories',
icon: 'group-outlined',
permission: ['listOsCategories', 'addOsCategory'],
columns: ['name', 'isfeatured', 'created', 'order'],
details: ['name', 'isfeatured', 'created'],
related: [{
name: 'guestos',
title: 'label.guest.os',
param: 'oscategoryid'
},
{
name: 'template',
title: 'label.templates',
param: 'oscategoryid'
},
{
name: 'iso',
title: 'label.isos',
param: 'oscategoryid'
}],
actions: [
{
api: 'addOsCategory',
icon: 'plus-outlined',
label: 'label.add.guest.os.category',
listView: true,
dataView: false,
args: ['name', 'isfeatured']
},
{
api: 'updateOsCategory',
icon: 'edit-outlined',
label: 'label.edit',
dataView: true,
popup: true,
args: ['name', 'isfeatured']
},
{
api: 'deleteOsCategory',
icon: 'delete-outlined',
label: 'label.action.delete.guest.os.category',
message: 'message.action.delete.guest.os.category',
dataView: true,
popup: true
}
]
},
{
name: 'guestos',
title: 'label.guest.os',
@ -195,6 +245,7 @@ export default {
permission: ['listOsTypes', 'listOsCategories'],
columns: ['name', 'oscategoryname', 'isuserdefined'],
details: ['name', 'oscategoryname', 'isuserdefined'],
searchFilters: ['oscategoryid'],
related: [{
name: 'guestoshypervisormapping',
title: 'label.guest.os.hypervisor.mappings',
@ -221,7 +272,14 @@ export default {
label: 'label.edit',
dataView: true,
popup: true,
args: ['osdisplayname']
groupAction: true,
groupMap: (selection, values) => { return selection.map(x => { return { id: x, oscategoryid: values.oscategoryid } }) },
args: (record, store, isGroupAction) => {
if (isGroupAction) {
return ['oscategoryid']
}
return ['osdisplayname', 'oscategoryid']
}
},
{
api: 'addGuestOsMapping',

View File

@ -67,7 +67,7 @@ export default {
return fields
},
searchFilters: () => {
var filters = ['name', 'zoneid', 'tags', 'arch']
var filters = ['name', 'zoneid', 'tags', 'arch', 'oscategoryid', 'templatetype']
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
filters.push('storageid')
filters.push('imagestoreid')
@ -235,7 +235,7 @@ export default {
},
details: ['name', 'id', 'displaytext', 'checksum', 'ostypename', 'size', 'arch', 'bootable', 'isready', 'passwordenabled', 'directdownload', 'isextractable', 'ispublic', 'isfeatured', 'isdynamicallyscalable', 'crosszones', 'account', 'domain', 'created', 'userdatadetails', 'userdatapolicy', 'url'],
searchFilters: () => {
var filters = ['name', 'zoneid', 'tags', 'arch']
var filters = ['name', 'zoneid', 'tags', 'arch', 'oscategoryid']
if (['Admin', 'DomainAdmin'].includes(store.getters.userInfo.roletype)) {
filters.push('storageid')
filters.push('imagestoreid')

View File

@ -48,6 +48,7 @@ import {
Divider,
DatePicker,
TimePicker,
Typography,
Upload,
Progress,
Skeleton,
@ -117,6 +118,7 @@ export default {
app.use(Divider)
app.use(DatePicker)
app.use(TimePicker)
app.use(Typography)
app.use(Upload)
app.use(Progress)
app.use(Skeleton)

View File

@ -83,6 +83,7 @@ import {
FileDoneOutlined,
FileProtectOutlined,
FileTextOutlined,
FileZipOutlined,
FilterOutlined,
FilterTwoTone,
FireOutlined,
@ -99,6 +100,7 @@ import {
GlobalOutlined,
GoldOutlined,
GoogleOutlined,
GroupOutlined,
HddOutlined,
HomeOutlined,
IdcardOutlined,
@ -253,6 +255,7 @@ export default {
app.component('FileDoneOutlined', FileDoneOutlined)
app.component('FileProtectOutlined', FileProtectOutlined)
app.component('FileTextOutlined', FileTextOutlined)
app.component('FileZipOutlined', FileZipOutlined)
app.component('FilterOutlined', FilterOutlined)
app.component('FilterTwoTone', FilterTwoTone)
app.component('FireOutlined', FireOutlined)
@ -269,6 +272,7 @@ export default {
app.component('GlobalOutlined', GlobalOutlined)
app.component('GoldOutlined', GoldOutlined)
app.component('GoogleOutlined', GoogleOutlined)
app.component('GroupOutlined', GroupOutlined)
app.component('HddOutlined', HddOutlined)
app.component('HomeOutlined', HomeOutlined)
app.component('IdcardOutlined', IdcardOutlined)

View File

@ -37,7 +37,8 @@ import {
genericUtilPlugin,
localesPlugin,
dialogUtilPlugin,
cpuArchitectureUtilPlugin
cpuArchitectureUtilPlugin,
imagesUtilPlugin
} from './utils/plugins'
import { VueAxios } from './utils/request'
import directives from './utils/directives'
@ -55,6 +56,7 @@ vueApp.use(localesPlugin)
vueApp.use(genericUtilPlugin)
vueApp.use(dialogUtilPlugin)
vueApp.use(cpuArchitectureUtilPlugin)
vueApp.use(imagesUtilPlugin)
vueApp.use(extensions)
vueApp.use(directives)

View File

@ -404,10 +404,11 @@ const user = {
}).catch(ignored => {})
}
api('listUsers', { id: Cookies.get('userid') }).then(response => {
api('listUsers', { id: Cookies.get('userid'), showicon: true }).then(response => {
const result = response.listusersresponse.user[0]
commit('SET_INFO', result)
commit('SET_NAME', result.firstname + ' ' + result.lastname)
commit('SET_AVATAR', result.icon?.base64image || '')
store.dispatch('SetCsLatestVersion', result.rolename)
}).catch(error => {
reject(error)

View File

@ -338,7 +338,7 @@ export const showIconPlugin = {
if (resource) {
resourceType = resource
}
if (['zone', 'zones', 'template', 'iso', 'account', 'accountuser', 'vm', 'domain', 'project', 'vpc', 'guestnetwork'].includes(resourceType)) {
if (['zone', 'zones', 'template', 'iso', 'account', 'accountuser', 'vm', 'domain', 'project', 'vpc', 'guestnetwork', 'guestoscategory'].includes(resourceType)) {
return true
} else {
return false
@ -412,6 +412,7 @@ export const resourceTypePlugin = {
case 'VpnCustomerGateway':
case 'AutoScaleVmGroup':
case 'QuotaTariff':
case 'GuestOsCategory':
return resourceType.toLowerCase()
}
return ''
@ -543,10 +544,24 @@ export const cpuArchitectureUtilPlugin = {
install (app) {
app.config.globalProperties.$fetchCpuArchitectureTypes = function () {
const architectures = [
{ id: 'x86_64', name: 'AMD 64 bits (x86_64)' },
{ id: 'x86_64', name: 'Intel/AMD 64 bits (x86_64)' },
{ id: 'aarch64', name: 'ARM 64 bits (aarch64)' }
]
return architectures.map(item => ({ ...item, description: item.name }))
}
}
}
export const imagesUtilPlugin = {
install (app) {
app.config.globalProperties.$fetchTemplateTypes = function () {
const baseTypes = ['USER', 'VNF']
const adminTypes = ['SYSTEM', 'BUILTIN', 'ROUTING']
const types = [...baseTypes]
if (store.getters.userInfo?.roletype === 'Admin') {
types.push(...adminTypes)
}
return types.map(type => ({ id: type, name: type, description: type }))
}
}
}

View File

@ -1261,6 +1261,9 @@ export default {
}
var paramName = param.name
var extractedParamName = paramName.replace('ids', '').replace('id', '').toLowerCase()
if (extractedParamName.endsWith('ory')) {
extractedParamName = extractedParamName.slice(0, -3) + 'orie'
}
var params = { listall: true }
for (const filter in filters) {
params[filter] = filters[filter]

View File

@ -175,7 +175,7 @@
class="auth-btn github-auth"
style="height: 38px; width: 185px; padding: 0; margin-bottom: 5px;" >
<img src="/assets/github.svg" style="width: 32px; padding: 5px" />
<a-text>Sign in with Github</a-text>
<a-typography-text>Sign in with Github</a-typography-text>
</a-button>
</div>
<div class="social-auth" v-if="googleprovider">
@ -187,7 +187,7 @@
class="auth-btn google-auth"
style="height: 38px; width: 185px; padding: 0" >
<img src="/assets/google.svg" style="width: 32px; padding: 5px" />
<a-text>Sign in with Google</a-text>
<a-typography-text>Sign in with Google</a-typography-text>
</a-button>
</div>
</div>

View File

@ -34,72 +34,70 @@
<div style="margin-top: 15px">
<span>{{ $t('message.select.a.zone') }}</span><br/>
<a-form-item :label="$t('label.zoneid')" name="zoneid" ref="zoneid">
<div v-if="zones.length <= 8">
<a-row type="flex" :gutter="[16,18]" justify="start">
<div v-for="(zoneItem, idx) in zones" :key="idx">
<a-radio-group
:key="idx"
:size="large"
v-model:value="form.zoneid"
@change="onSelectZoneId(zoneItem.id)">
<a-col :span="6">
<a-radio-button
:value="zoneItem.id"
style="border-width: 2px"
class="zone-radio-button">
<span>
<resource-icon
v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
:image="zoneItem.icon.base64image"
size="2x" />
<global-outlined size="2x" v-else />
{{ zoneItem.name }}
</span>
</a-radio-button>
</a-col>
</a-radio-group>
</div>
</a-row>
</div>
<a-select
v-else
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="onSelectZoneId"
:loading="loading.zones"
v-focus="true"
>
<a-select-option v-for="zone1 in zones" :key="zone1.id" :label="zone1.name">
<span>
<resource-icon v-if="zone1.icon && zone1.icon.base64image" :image="zone1.icon.base64image" size="1x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px" />
{{ zone1.name }}
</span>
</a-select-option>
</a-select>
<zone-block-radio-group-select
:items="zones"
:selectedValue="form.zoneid"
@change="onSelectZoneId" />
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.template')"
v-if="!zoneSelected || isZoneSelectedMultiArch"
:title="$t('label.arch')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected" style="margin-top: 15px">
{{ $t('message.instance.architecture') }}
<block-radio-group-select
style="margin-top: 5px;"
:items="architectureTypes.opts"
:selectedValue="selectedArchitecture"
@change="changeArchitecture">
<template #radio-option="{ item }">
<span>{{ item.name || item.description }}</span>
</template>
<template #select-option="{ item }">
<span>{{ item.name || item.description }}</span>
</template>
</block-radio-group-select>
</div>
</template>
</a-step>
<a-step
:title="$t('label.template.select')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected" style="margin-top: 15px">
<os-based-image-selection
v-if="isModernImageSelection"
:imageTypeSelectionAllowed="false"
:imagePreSelected="!!this.queryTemplateId"
:guestOsCategoriesSelectionDisallowed="!this.queryGuestOsCategoryId && !!this.queryTemplateId"
:guestOsCategories="options.guestOsCategories"
:guestOsCategoriesLoading="loading.guestOsCategories"
:selectedGuestOsCategoryId="form.guestoscategoryid"
:imageItems="options.templates"
:imagesLoading="loading.templates"
:diskSizeSelectionDeployAsIsMessageVisible="template && template.deployasis"
:rootDiskOverrideDisabled="rootDiskSizeFixed > 0 || (template && template.deployasis) || showOverrideDiskOfferingOption"
:rootDiskOverrideChecked="form.rootdisksizeitem"
:filterOption="filterOption"
:preFillContent="dataPreFill"
@change-guest-os-category="onSelectGuestOsCategory"
@handle-image-search-filter="($event) => fetchAllTemplates($event)"
@update-image="updateFieldValue"
@update-disk-size="updateFieldValue" />
<a-card
:tabList="tabList"
:activeTabKey="tabKey"
@tabChange="key => onTabChange(key, 'tabKey')">
<div v-if="tabKey === 'templateid'">
v-else
:tabList="imageTypeList"
:activeTabKey="imageType">
<div>
{{ $t('message.template.desc') }}
<template-iso-selection
input-decorator="templateid"
:items="options.templates"
:selected="tabKey"
:selected="imageType"
:loading="loading.templates"
:preFillContent="dataPreFill"
:key="templateKey"
@ -109,10 +107,10 @@
{{ $t('label.override.rootdisk.size') }}
<a-switch
v-model:checked="form.rootdisksizeitem"
:disabled="rootDiskSizeFixed > 0 || template.deployasis || showOverrideDiskOfferingOption"
:disabled="rootDiskSizeFixed > 0 || (template && template.deployasis) || showOverrideDiskOfferingOption"
@change="val => { showRootDiskSizeChanger = val }"
style="margin-left: 10px;"/>
<div v-if="template.deployasis"> {{ $t('message.deployasis') }} </div>
<div v-if="template && template.deployasis"> {{ $t('message.deployasis') }} </div>
</div>
<disk-size-selection
v-if="showRootDiskSizeChanger"
@ -220,7 +218,7 @@
<span v-if="serviceOffering && !serviceOffering.diskofferingstrictness">
<a-step
:status="zoneSelected ? 'process' : 'wait'"
v-if="!template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
v-if="template && !template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
<template #description>
<div v-if="zoneSelected">
<multi-disk-selection
@ -244,7 +242,7 @@
:value="overrideDiskOffering ? overrideDiskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPreFill"
:isIsoSelected="tabKey==='isoid'"
:isIsoSelected="imageType==='isoid'"
:isRootDiskOffering="true"
@on-selected-root-disk-size="onSelectRootDiskSize"
@select-disk-offering-item="($event) => updateOverrideDiskOffering($event)"
@ -273,7 +271,7 @@
<a-step
:title="$t('label.data.disk')"
:status="zoneSelected ? 'process' : 'wait'"
v-if="!template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
v-if="template && !template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
<template #description>
<div v-if="zoneSelected">
<multi-disk-selection
@ -286,7 +284,7 @@
</a-step>
<a-step
v-else
:title="tabKey === 'templateid' ? $t('label.data.disk') : $t('label.disk.size')"
:title="imageType === 'templateid' ? $t('label.data.disk') : $t('label.disk.size')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected">
@ -305,7 +303,7 @@
:value="diskOffering ? diskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPreFill"
:isIsoSelected="tabKey==='isoid'"
:isIsoSelected="imageType==='isoid'"
@on-selected-disk-size="onSelectDiskSize"
@select-disk-offering-item="($event) => updateDiskOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
@ -768,13 +766,13 @@
</template>
<a-card>
<div v-if="this.template && this.template.userdataid">
<a-text type="primary">
<a-typography-text>
Userdata "{{ $t(this.template.userdataname) }}" is linked with template "{{ $t(this.template.name) }}" with override policy "{{ $t(this.template.userdatapolicy) }}"
</a-text><br/><br/>
</a-typography-text><br/><br/>
<div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
<a-text type="primary" v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
<a-typography-text v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
Enter the values for the variables in userdata
</a-text>
</a-typography-text>
<a-input-group>
<a-table
size="small"
@ -1034,12 +1032,15 @@ import eventBus from '@/config/eventBus'
import InfoCard from '@/components/view/InfoCard'
import ResourceIcon from '@/components/view/ResourceIcon'
import ZoneBlockRadioGroupSelect from '@views/compute/wizard/ZoneBlockRadioGroupSelect.vue'
import BlockRadioGroupSelect from '@/components/widgets/BlockRadioGroupSelect'
import ComputeOfferingSelection from '@views/compute/wizard/ComputeOfferingSelection'
import ComputeSelection from '@views/compute/wizard/ComputeSelection'
import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
import MultiDiskSelection from '@views/compute/wizard/MultiDiskSelection'
import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
import OsBasedImageSelection from '@views/compute/wizard/OsBasedImageSelection'
import AffinityGroupSelection from '@views/compute/wizard/AffinityGroupSelection'
import NetworkSelection from '@views/compute/wizard/NetworkSelection'
import NetworkConfiguration from '@views/compute/wizard/NetworkConfiguration'
@ -1057,6 +1058,10 @@ const STATUS_FAILED = 'error'
export default {
name: 'Wizard',
components: {
InfoCard,
ResourceIcon,
ZoneBlockRadioGroupSelect,
BlockRadioGroupSelect,
SshKeyPairSelection,
UserDataSelection,
NetworkConfiguration,
@ -1064,14 +1069,13 @@ export default {
LoadBalancerSelection,
AffinityGroupSelection,
TemplateIsoSelection,
OsBasedImageSelection,
DiskSizeSelection,
MultiDiskSelection,
DiskOfferingSelection,
InfoCard,
ComputeOfferingSelection,
ComputeSelection,
SecurityGroupSelection,
ResourceIcon,
TooltipLabel,
InstanceNicsNetworkSelectListView
},
@ -1103,7 +1107,10 @@ export default {
},
zoneId: '',
zoneSelected: false,
isZoneSelectedMultiArch: false,
dynamicscalingenabled: true,
imageType: 'templateid',
imageSearchFilters: null,
templateKey: 0,
showRegisteredUserdata: true,
doUserdataOverride: false,
@ -1126,6 +1133,7 @@ export default {
disksize: null
},
options: {
guestOsCategories: [],
templates: {},
serviceOfferings: [],
diskOfferings: [],
@ -1139,6 +1147,7 @@ export default {
rowCount: {},
loading: {
deploy: false,
guestOsCategories: false,
templates: false,
serviceOfferings: false,
diskOfferings: false,
@ -1251,16 +1260,9 @@ export default {
templateUserDataParams: [],
templateUserDataValues: {},
overrideDiskOffering: {},
templateFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
initDataConfig: {},
defaultNetworkId: '',
dataNetworkCreated: [],
tabKey: 'templateid',
userdataTabKey: 'userdataregistered',
dataPreFill: {},
showDetails: false,
@ -1280,7 +1282,9 @@ export default {
zones: [],
selectedZone: '',
formModel: {},
nicToNetworkSelection: []
nicToNetworkSelection: [],
selectedArchitecture: null,
architectureTypes: {}
}
},
computed: {
@ -1380,7 +1384,7 @@ export default {
options: {
zoneid: _.get(this.zone, 'id'),
networkid: this.defaultNetworkId,
id: this.lbRuleId,
id: this.queryLbRuleId,
projectid: store.getters.project ? store.getters.project.id : null,
domainid: store.getters.project && store.getters.project.id ? null : store.getters.userInfo.domainid,
account: store.getters.project && store.getters.project.id ? null : store.getters.userInfo.account,
@ -1389,6 +1393,18 @@ export default {
keyword: undefined,
showIcon: true
}
},
guestOsCategories: {
list: 'listOsCategories',
options: {
zoneid: _.get(this.zone, 'id'),
isfeatured: true,
isiso: _.get(this.form, 'imagetype') === 'isoid',
arch: this.selectedArchitecture,
isvnf: false,
showicon: true
},
field: 'guestoscategoryid'
}
}
},
@ -1406,21 +1422,29 @@ export default {
templateConfigurationExists () {
return this.vm.templateid && this.templateConfigurations && this.templateConfigurations.length > 0
},
templateId () {
queryZoneId () {
return this.$route.query.zoneid || null
},
queryArchId () {
return this.$route.query.arch || null
},
queryTemplateId () {
return this.$route.query.templateid || null
},
networkId () {
queryNetworkId () {
return this.$route.query.networkid || null
},
lbRuleId () {
queryGuestOsCategoryId () {
return this.$route.query.oscategoryid || null
},
queryLbRuleId () {
return this.$route.query.lbruleid || null
},
tabList () {
const tabList = [{
imageTypeList () {
return [{
key: 'templateid',
tab: this.$t('label.templates')
}]
return tabList
},
userdataTabList () {
let tabList = []
@ -1451,6 +1475,18 @@ export default {
},
isCustomizedIOPS () {
return this.rootDiskSelected?.iscustomizediops || this.serviceOffering?.iscustomizediops || false
},
isModernImageSelection () {
return this.$config.imageSelectionInterface === undefined || this.$config.imageSelectionInterface === 'modern'
},
imageSelection () {
return this.isModernImageSelection ? 'modern' : 'legacy'
},
showUserCategoryForModernImageSelection () {
return this.$config.showUserCategoryForModernImageSelection === undefined || this.$config.showUserCategoryForModernImageSelection
},
showAllCategoryForModernImageSelection () {
return this.$config.showAllCategoryForModernImageSelection
}
},
watch: {
@ -1466,7 +1502,7 @@ export default {
Object.keys(vmgroupConfig).forEach(field => {
this.vm[field] = this.vmgroupConfig[field]
})
this.template = ''
this.template = null
for (const key in this.options.templates) {
var template = _.find(_.get(this.options.templates[key], 'template', []), (option) => option.id === vmgroupConfig.templateid)
if (template) {
@ -1545,7 +1581,7 @@ export default {
}
}
if (!this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
if (this.template && !this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
this.vm.diskofferingid = ''
this.vm.diskofferingname = ''
this.vm.diskofferingsize = ''
@ -1766,6 +1802,27 @@ export default {
}
}
},
getImageFilters (params, forReset) {
if (this.isModernImageSelection) {
if (this.form.guestoscategoryid === '0') {
return ['self']
}
if (this.isModernImageSelection && params && !forReset) {
if (params.featured) {
return ['featured']
} else if (params.public) {
return ['community']
}
}
return this.isNormalAndDomainUser ? ['executable'] : ['all']
}
return [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
]
},
getPropertyQualifiers (qualifiers, type) {
var result = ''
switch (type) {
@ -1805,18 +1862,25 @@ export default {
let zones = []
let apiName = ''
const params = {}
if (this.templateId) {
if (this.queryZoneId) {
zones.push(this.queryZoneId)
if (this.queryTemplateId) {
this.dataPreFill.templateid = this.queryTemplateId
}
return resolve(zones)
} else if (this.queryTemplateId) {
apiName = 'listTemplates'
params.listall = true
params.templatefilter = this.isNormalAndDomainUser ? 'executable' : 'all'
params.id = this.templateId
} else if (this.networkId) {
params.id = this.queryTemplateId
this.dataPreFill.templateid = this.queryTemplateId
} else if (this.queryNetworkId) {
params.listall = true
params.id = this.networkId
params.id = this.queryNetworkId
apiName = 'listNetworks'
} else if (this.lbRuleId) {
} else if (this.queryLbRuleId) {
params.listall = true
params.id = this.lbRuleId
params.id = this.queryLbRuleId
apiName = 'listLoadBalancerRules'
}
if (!apiName) return resolve(zones)
@ -1840,6 +1904,10 @@ export default {
})
},
async fetchData () {
this.architectureTypes.opts = this.$fetchCpuArchitectureTypes()
if (this.queryArchId) {
this.architectureTypes.opts = this.architectureTypes.opts.filter(o => o.id === this.queryArchId)
}
const zones = await this.fetchZoneByQuery()
if (zones && zones.length === 1) {
this.selectedZone = zones[0]
@ -1986,25 +2054,18 @@ export default {
},
updateFieldValue (name, value) {
if (name === 'templateid') {
this.tabKey = 'templateid'
this.form.templateid = value
this.resetFromTemplateConfiguration()
let template = ''
for (const key in this.options.templates) {
var t = _.find(_.get(this.options.templates[key], 'template', []), (option) => option.id === value)
if (t) {
this.template = t
this.templateConfigurations = []
this.selectedTemplateConfiguration = {}
this.templateNics = []
this.templateLicenses = []
this.templateProperties = {}
this.updateTemplateParameters()
template = t
for (const entry of Object.values(this.options.templates)) {
template = entry?.template.find(option => option.id === value) || null
if (template) {
this.template = template
break
}
}
if (template) {
this.resetTemplateAssociatedResources()
var size = template.size / (1024 * 1024 * 1024) || 0 // bytes to GB
this.dataPreFill.minrootdisksize = Math.ceil(size)
this.updateTemplateLinkedUserData(this.template.userdataid)
@ -2392,6 +2453,58 @@ export default {
getText (option) {
return _.get(option, 'displaytext', _.get(option, 'name'))
},
fetchGuestOsCategories (skipFetchImages) {
const key = 'guestOsCategories'
const params = this.params[key]
if (this.queryGuestOsCategoryId) {
params.options.id = this.queryGuestOsCategoryId
} else if (this.queryTemplateId) {
this.fetchAllTemplates()
return Promise.resolve()
}
return this.fetchOptions(params, key, ['zones'])
.then((res) => {
if (!this.options.guestOsCategories) {
this.options.guestOsCategories = []
}
if (!this.queryGuestOsCategoryId) {
if (this.showUserCategoryForModernImageSelection) {
const userCategory = {
id: '0',
name: this.$t('label.user')
}
if (this.$store.getters.avatar) {
userCategory.icon = {
base64image: this.$store.getters.avatar
}
}
this.options.guestOsCategories.push(userCategory)
}
if (this.showAllCategoryForModernImageSelection) {
this.options.guestOsCategories.push({
id: '-1',
name: this.$t('label.all')
})
}
}
this.form.guestoscategoryid = this.options.guestOsCategories[0].id
if (skipFetchImages) {
return
}
this.fetchAllTemplates()
})
.catch((e) => {
console.error('Error fetching guestOsCategories:', e)
})
},
changeArchitecture (arch) {
this.selectedArchitecture = arch
if (this.isModernImageSelection) {
this.fetchGuestOsCategories()
return
}
this.fetchAllTemplates()
},
async handleSubmit (e) {
console.log('wizard submit')
e.preventDefault()
@ -2552,7 +2665,7 @@ export default {
if (this.showRootDiskSizeChanger && values.rootdisksize && values.rootdisksize > 0) {
createVmGroupData.rootdisksize = values.rootdisksize
} else if (this.rootDiskSizeFixed > 0 && !this.template.deployasis) {
} else if (this.rootDiskSizeFixed > 0 && this.template && !this.template.deployasis) {
createVmGroupData.rootdisksize = this.rootDiskSizeFixed
}
@ -2799,10 +2912,9 @@ export default {
})
},
fetchOptions (param, name, exclude) {
if (exclude && exclude.length > 0) {
if (exclude.includes(name)) {
return
}
return new Promise((resolve, reject) => {
if (exclude && exclude.length > 0 && exclude.includes(name)) {
return resolve(null)
}
this.loading[name] = true
param.loading = true
@ -2817,10 +2929,10 @@ export default {
if (Object.keys(responseItem).length === 0) {
this.rowCount[name] = 0
this.options[name] = []
return
return resolve(null)
}
if (!responseKey.includes('response')) {
return
return resolve(null)
}
_.map(responseItem, (response, key) => {
if (key === 'count') {
@ -2848,23 +2960,38 @@ export default {
}
}
})
resolve(response)
}).catch(function (error) {
console.log(error.stack)
param.loading = false
reject(error)
}).finally(() => {
this.loading[name] = false
})
})
},
fetchTemplates (templateFilter, params) {
const args = Object.assign({}, params)
if (args.keyword || args.category !== templateFilter) {
if (this.isModernImageSelection && this.form.guestoscategoryid && !['-1', '0'].includes(this.form.guestoscategoryid)) {
args.oscategoryid = this.form.guestoscategoryid
}
if (args.keyword || (args.category && args.category !== templateFilter)) {
args.page = 1
args.pageSize = args.pageSize || 10
}
args.zoneid = _.get(this.zone, 'id')
if (this.isZoneSelectedMultiArch) {
args.arch = this.selectedArchitecture
}
args.templatefilter = templateFilter
args.details = 'all'
args.showicon = 'true'
args.id = this.queryTemplateId
args.isvnf = false
delete args.category
delete args.public
delete args.featured
return new Promise((resolve, reject) => {
api('listTemplates', args).then((response) => {
@ -2879,14 +3006,16 @@ export default {
const promises = []
const templates = {}
this.loading.templates = true
this.templateFilter.forEach((filter) => {
this.imageSearchFilters = params
const templateFilters = this.getImageFilters(params)
templateFilters.forEach((filter) => {
templates[filter] = { count: 0, template: [] }
promises.push(this.fetchTemplates(filter, params))
})
this.options.templates = templates
Promise.all(promises).then((response) => {
response.forEach((resItem, idx) => {
templates[this.templateFilter[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
templates[templateFilters[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
this.options.templates = { ...templates }
})
}).catch((reason) => {
@ -2898,35 +3027,71 @@ export default {
filterOption (input, option) {
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
},
resetTemplatesList () {
const templates = {}
const templateFilters = this.getImageFilters(null, true)
templateFilters.forEach((filter) => {
templates[filter] = { count: 0, template: [] }
})
this.options.templates = templates
},
resetTemplateAssociatedResources () {
this.templateConfigurations = []
this.selectedTemplateConfiguration = {}
this.templateNics = []
this.templateLicenses = []
this.templateProperties = {}
},
async fetchZoneOptions () {
let guestOsFetch = null
for (const [name, param] of Object.entries(this.params)) {
if (this.queryNetworkId && name === 'networks') {
param.options = { id: this.queryNetworkId }
}
if (name === 'loadbalancers') {
if (!this.queryLbRuleId && !this.defaultNetworkId) {
continue
}
if (this.queryLbRuleId) {
param.options = { id: this.queryLbRuleId }
}
}
const shouldLoad = !('isLoad' in param) || param.isLoad
if (!shouldLoad) continue
if (this.isModernImageSelection && name === 'guestOsCategories') {
guestOsFetch = this.fetchGuestOsCategories(true)
} else {
this.fetchOptions(param, name, ['zones'])
}
}
if (this.isModernImageSelection && guestOsFetch) {
await guestOsFetch
}
this.fetchAllTemplates()
this.updateTemplateKey()
this.formModel = toRaw(this.form)
},
onSelectZoneId (value) {
if (this.dataPreFill.zoneid !== value) {
this.dataPreFill = {}
}
this.zoneId = value
this.zone = _.find(this.options.zones, (option) => option.id === value)
this.zoneSelected = true
this.isZoneSelectedMultiArch = this.zone.ismultiarch
if (this.isZoneSelectedMultiArch) {
this.selectedArchitecture = this.architectureTypes.opts[0].id
}
this.selectedZone = this.zoneId
this.form.zoneid = this.zoneId
this.form.templateid = undefined
this.tabKey = 'templateid'
_.each(this.params, (param, name) => {
if (this.networkId && name === 'networks') {
param.options = {
id: this.networkId
}
}
if (this.lbRuleId && name === 'loadbalancers') {
param.options = {
id: this.lbRuleId
}
}
if (!('isLoad' in param) || param.isLoad) {
this.fetchOptions(param, name, ['zones'])
}
})
if (this.tabKey === 'templateid') {
this.fetchAllTemplates()
}
this.updateTemplateKey()
this.formModel = toRaw(this.form)
this.resetTemplatesList()
this.fetchZoneOptions()
},
onSelectGuestOsCategory (value) {
this.form.guestoscategoryid = value
this.fetchAllTemplates(this.imageSearchFilters)
},
handleSearchFilter (name, options) {
this.params[name].options = { ...this.params[name].options, ...options }
@ -3184,7 +3349,7 @@ export default {
.vm-info-card {
.ant-card-body {
min-height: 250px;
max-height: calc(100vh - 150px);
max-height: calc(100vh - 250px);
overflow-y: auto;
scroll-behavior: smooth;
}

File diff suppressed because it is too large Load Diff

View File

@ -34,72 +34,70 @@
<div style="margin-top: 15px">
<span>{{ $t('message.select.a.zone') }}</span><br/>
<a-form-item :label="$t('label.zoneid')" name="zoneid" ref="zoneid">
<div v-if="zones.length <= 8">
<a-row type="flex" :gutter="[16, 18]" justify="start">
<div v-for="(zoneItem, idx) in zones" :key="idx">
<a-radio-group
:key="idx"
:size="large"
v-model:value="form.zoneid"
@change="onSelectZoneId(zoneItem.id)">
<a-col :span="6">
<a-radio-button
:value="zoneItem.id"
style="border-width: 2px"
class="zone-radio-button">
<span>
<resource-icon
v-if="zoneItem && zoneItem.icon && zoneItem.icon.base64image"
:image="zoneItem.icon.base64image"
size="2x" />
<global-outlined size="2x" v-else />
{{ zoneItem.name }}
</span>
</a-radio-button>
</a-col>
</a-radio-group>
</div>
</a-row>
</div>
<a-select
v-else
v-model:value="form.zoneid"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
@change="onSelectZoneId"
:loading="loading.zones"
v-focus="true"
>
<a-select-option v-for="zone1 in zones" :key="zone1.id" :label="zone1.name">
<span>
<resource-icon v-if="zone1.icon && zone1.icon.base64image" :image="zone1.icon.base64image" size="2x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px" />
{{ zone1.name }}
</span>
</a-select-option>
</a-select>
<zone-block-radio-group-select
:items="zones"
:selectedValue="form.zoneid"
@change="onSelectZoneId" />
</a-form-item>
</div>
</template>
</a-step>
<a-step
:title="$t('label.template')"
v-if="!zoneSelected || isZoneSelectedMultiArch"
:title="$t('label.arch')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected" style="margin-top: 15px">
{{ $t('message.instance.architecture') }}
<block-radio-group-select
style="margin-top: 5px;"
:items="architectureTypes.opts"
:selectedValue="selectedArchitecture"
@change="changeArchitecture">
<template #radio-option="{ item }">
<span>{{ item.name || item.description }}</span>
</template>
<template #select-option="{ item }">
<span>{{ item.name || item.description }}</span>
</template>
</block-radio-group-select>
</div>
</template>
</a-step>
<a-step
:title="$t('label.template.select')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected" style="margin-top: 15px">
<os-based-image-selection
v-if="isModernImageSelection"
:imageTypeSelectionAllowed="false"
:imagePreSelected="!!this.queryTemplateId"
:guestOsCategoriesSelectionDisallowed="guestOsCategoriesSelectionDisallowed"
:guestOsCategories="options.guestOsCategories"
:guestOsCategoriesLoading="loading.guestOsCategories"
:selectedGuestOsCategoryId="form.guestoscategoryid"
:imageItems="options.templates"
:imagesLoading="loading.templates"
:diskSizeSelectionDeployAsIsMessageVisible="template && template.deployasis"
:rootDiskOverrideDisabled="rootDiskSizeFixed > 0 || (template && template.deployasis) || showOverrideDiskOfferingOption"
:rootDiskOverrideChecked="form.rootdisksizeitem"
:filterOption="filterOption"
:preFillContent="dataPreFill"
@change-guest-os-category="onSelectGuestOsCategory"
@handle-image-search-filter="($event) => fetchAllTemplates($event)"
@update-image="updateFieldValue"
@update-disk-size="updateFieldValue" />
<a-card
:tabList="tabList"
:activeTabKey="tabKey"
@tabChange="key => onTabChange(key, 'tabKey')">
<div v-if="tabKey === 'templateid'">
v-else
:tabList="imageTypeList"
:activeTabKey="imageType">
<div>
{{ $t('message.template.desc') }}
<template-iso-selection
input-decorator="templateid"
:items="options.templates"
:selected="tabKey"
:selected="imageType"
:loading="loading.templates"
:preFillContent="dataPreFill"
:key="templateKey"
@ -109,10 +107,10 @@
{{ $t('label.override.rootdisk.size') }}
<a-switch
v-model:checked="form.rootdisksizeitem"
:disabled="rootDiskSizeFixed > 0 || template.deployasis || showOverrideDiskOfferingOption"
:disabled="rootDiskSizeFixed > 0 || (template && template.deployasis) || showOverrideDiskOfferingOption"
@change="val => { showRootDiskSizeChanger = val }"
style="margin-left: 10px;"/>
<div v-if="template.deployasis"> {{ $t('message.deployasis') }} </div>
<div v-if="template && template.deployasis"> {{ $t('message.deployasis') }} </div>
</div>
<disk-size-selection
v-if="showRootDiskSizeChanger"
@ -148,9 +146,7 @@
v-model:value="form.templateConfiguration"
defaultActiveFirstOption
:placeholder="$t('message.ovf.configurations')"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:filterOption="filterOption"
@change="onSelectTemplateConfigurationId"
>
<a-select-option v-for="opt in templateConfigurations" :key="opt.id" :label="opt.name || opt.description">
@ -207,19 +203,10 @@
<a-input v-model:value="form.memory"/>
</a-form-item>
</span>
<span v-if="tabKey!=='isoid'">
{{ $t('label.override.root.diskoffering') }}
<a-switch
v-model:checked="showOverrideDiskOfferingOption"
:checked="serviceOffering && !serviceOffering.diskofferingstrictness && showOverrideDiskOfferingOption"
:disabled="(serviceOffering && serviceOffering.diskofferingstrictness)"
@change="val => { updateOverrideRootDiskShowParam(val) }"
style="margin-left: 10px;"/>
</span>
<span v-if="tabKey!=='isoid' && serviceOffering && !serviceOffering.diskofferingstrictness">
<span v-if="imageType!=='isoid' && serviceOffering && !serviceOffering.diskofferingstrictness">
<a-step
:status="zoneSelected ? 'process' : 'wait'"
v-if="!template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
v-if="template && !template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
<template #description>
<div v-if="zoneSelected">
<multi-disk-selection
@ -243,7 +230,7 @@
:value="overrideDiskOffering ? overrideDiskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPreFill"
:isIsoSelected="tabKey==='isoid'"
:isIsoSelected="imageType==='isoid'"
:isRootDiskOffering="true"
@on-selected-root-disk-size="onSelectRootDiskSize"
@select-disk-offering-item="($event) => updateOverrideDiskOffering($event)"
@ -272,7 +259,7 @@
<a-step
:title="$t('label.data.disk')"
:status="zoneSelected ? 'process' : 'wait'"
v-if="!template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
v-if="template && !template.deployasis && template.childtemplates && template.childtemplates.length > 0" >
<template #description>
<div v-if="zoneSelected">
<multi-disk-selection
@ -285,7 +272,7 @@
</a-step>
<a-step
v-else-if="vm.templateid && template.templatetype !== 'VNF'"
:title="tabKey === 'templateid' ? $t('label.data.disk') : $t('label.disk.size')"
:title="imageType === 'templateid' ? $t('label.data.disk') : $t('label.disk.size')"
:status="zoneSelected ? 'process' : 'wait'">
<template #description>
<div v-if="zoneSelected">
@ -304,7 +291,7 @@
:value="diskOffering ? diskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPreFill"
:isIsoSelected="tabKey==='isoid'"
:isIsoSelected="imageType==='isoid'"
@on-selected-disk-size="onSelectDiskSize"
@select-disk-offering-item="($event) => updateDiskOffering($event)"
@handle-search-filter="($event) => handleSearchFilter('diskOfferings', $event)"
@ -439,9 +426,7 @@
optionFilterProp="value"
v-model:value="form['properties.' + escapePropertyKey(property.key)]"
:placeholder="property.description"
:filterOption="(input, option) => {
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
:filterOption="filterOption"
>
<a-select-option v-for="opt in getPropertyQualifiers(property.qualifiers, 'select')" :key="opt">
{{ opt }}
@ -520,7 +505,7 @@
</a-form-item>
</div>
<div
v-if="vm.templateid && ['KVM', 'VMware', 'XenServer'].includes(hypervisor) && !template.deployasis">
v-if="vm.templateid && ['KVM', 'VMware', 'XenServer'].includes(hypervisor) && this.template && !template.deployasis">
<a-form-item :label="$t('label.boottype')" name="boottype" ref="boottype">
<a-select
v-model:value="form.boottype"
@ -547,7 +532,7 @@
</div>
<a-form-item
:label="$t('label.bootintosetup')"
v-if="zoneSelected && ((tabKey === 'isoid' && hypervisor === 'VMware') || (tabKey === 'templateid' && template && template.hypervisor === 'VMware'))"
v-if="zoneSelected && ((imageType === 'isoid' && hypervisor === 'VMware') || (imageType === 'templateid' && template && template.hypervisor === 'VMware'))"
name="bootintosetup"
ref="bootintosetup">
<a-switch v-model:checked="form.bootintosetup" />
@ -567,13 +552,13 @@
<a-form-item :label="$t('label.userdata')">
<a-card>
<div v-if="this.template && this.template.userdataid">
<a-text type="primary">
<a-typography-text>
Userdata "{{ $t(this.template.userdataname) }}" is linked with template "{{ $t(this.template.name) }}" with override policy "{{ $t(this.template.userdatapolicy) }}"
</a-text><br/><br/>
</a-typography-text><br/><br/>
<div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
<a-text type="primary" v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
<a-typography-text v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
Enter the values for the variables in userdata
</a-text>
</a-typography-text>
<a-input-group>
<a-table
size="small"
@ -592,13 +577,13 @@
</div>
</div>
<div v-if="this.iso && this.iso.userdataid">
<a-text type="primary">
<a-typography-text>
Userdata "{{ $t(this.iso.userdataname) }}" is linked with ISO "{{ $t(this.iso.name) }}" with override policy "{{ $t(this.iso.userdatapolicy) }}"
</a-text><br/><br/>
</a-typography-text><br/><br/>
<div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
<a-text type="primary" v-if="this.iso && this.iso.userdataid && templateUserDataParams.length > 0">
<a-typography-text v-if="this.iso && this.iso.userdataid && templateUserDataParams.length > 0">
Enter the values for the variables in userdata
</a-text>
</a-typography-text>
<a-input-group>
<a-table
size="small"
@ -864,12 +849,15 @@ import eventBus from '@/config/eventBus'
import InfoCard from '@/components/view/InfoCard'
import ResourceIcon from '@/components/view/ResourceIcon'
import ZoneBlockRadioGroupSelect from '@views/compute/wizard/ZoneBlockRadioGroupSelect.vue'
import BlockRadioGroupSelect from '@/components/widgets/BlockRadioGroupSelect'
import ComputeOfferingSelection from '@views/compute/wizard/ComputeOfferingSelection'
import ComputeSelection from '@views/compute/wizard/ComputeSelection'
import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
import MultiDiskSelection from '@views/compute/wizard/MultiDiskSelection'
import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
import OsBasedImageSelection from '@views/compute/wizard/OsBasedImageSelection'
import AffinityGroupSelection from '@views/compute/wizard/AffinityGroupSelection'
import NetworkSelection from '@views/compute/wizard/NetworkSelection'
import NetworkConfiguration from '@views/compute/wizard/NetworkConfiguration'
@ -883,21 +871,24 @@ import InstanceNicsNetworkSelectListView from '@/components/view/InstanceNicsNet
export default {
name: 'Wizard',
components: {
InfoCard,
ResourceIcon,
ZoneBlockRadioGroupSelect,
BlockRadioGroupSelect,
SshKeyPairSelection,
UserDataSelection,
NetworkConfiguration,
NetworkSelection,
AffinityGroupSelection,
TemplateIsoSelection,
OsBasedImageSelection,
DiskSizeSelection,
MultiDiskSelection,
DiskOfferingSelection,
InfoCard,
ComputeOfferingSelection,
ComputeSelection,
SecurityGroupSelection,
VnfNicsSelection,
ResourceIcon,
TooltipLabel,
InstanceNicsNetworkSelectListView
},
@ -917,7 +908,10 @@ export default {
podId: null,
clusterId: null,
zoneSelected: false,
isZoneSelectedMultiArch: false,
dynamicscalingenabled: true,
imageType: 'templateid',
imageSearchFilters: null,
templateKey: 0,
showRegisteredUserdata: true,
doUserdataOverride: false,
@ -943,6 +937,7 @@ export default {
disksize: null
},
options: {
guestOsCategories: [],
templates: {},
isos: {},
hypervisors: [],
@ -966,6 +961,7 @@ export default {
rowCount: {},
loading: {
deploy: false,
guestOsCategories: false,
templates: false,
isos: false,
hypervisors: false,
@ -1029,23 +1025,10 @@ export default {
templateUserDataParams: [],
templateUserDataValues: {},
overrideDiskOffering: {},
templateFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
isoFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
initDataConfig: {},
defaultnetworkid: '',
networkConfig: [],
dataNetworkCreated: [],
tabKey: 'templateid',
userdataTabKey: 'userdataregistered',
dataPreFill: {},
showDetails: false,
@ -1065,7 +1048,9 @@ export default {
zones: [],
selectedZone: '',
formModel: {},
nicToNetworkSelection: []
nicToNetworkSelection: [],
selectedArchitecture: null,
architectureTypes: {}
}
},
computed: {
@ -1201,6 +1186,18 @@ export default {
zoneid: _.get(this.zone, 'id'),
name: 'enable.dynamic.scale.vm'
}
},
guestOsCategories: {
list: 'listOsCategories',
options: {
zoneid: _.get(this.zone, 'id'),
isfeatured: true,
isiso: _.get(this.form, 'imagetype') === 'isoid',
arch: this.selectedArchitecture,
isvnf: true,
showicon: true
},
field: 'guestoscategoryid'
}
}
},
@ -1274,34 +1271,38 @@ export default {
templateConfigurationExists () {
return this.vm.templateid && this.templateConfigurations && this.templateConfigurations.length > 0
},
templateId () {
queryZoneId () {
return this.$route.query.zoneid || null
},
queryArchId () {
return this.$route.query.arch || null
},
queryTemplateId () {
return this.$route.query.templateid || null
},
isoId () {
return this.$route.query.isoid || null
},
networkId () {
queryNetworkId () {
return this.$route.query.networkid || null
},
tabList () {
const tabList = [{
queryGuestOsCategoryId () {
return this.$route.query.oscategoryid || null
},
imageTypeList () {
return [{
key: 'templateid',
tab: this.$t('label.templates')
}]
return tabList
},
userdataTabList () {
let tabList = []
tabList = [{
return [
{
key: 'userdataregistered',
tab: this.$t('label.userdata.registered')
},
{
key: 'userdatatext',
tab: this.$t('label.userdata.text')
}]
return tabList
}
]
},
showVnfNicsSection () {
return this.networks && this.networks.length > 0 && this.vm.templateid && this.templateVnfNics && this.templateVnfNics.length > 0
@ -1339,6 +1340,21 @@ export default {
},
isCustomizedIOPS () {
return this.rootDiskSelected?.iscustomizediops || this.serviceOffering?.iscustomizediops || false
},
isModernImageSelection () {
return this.$config.imageSelectionInterface === undefined || this.$config.imageSelectionInterface === 'modern'
},
imageSelection () {
return this.isModernImageSelection ? 'modern' : 'legacy'
},
showUserCategoryForModernImageSelection () {
return this.$config.showUserCategoryForModernImageSelection === undefined || this.$config.showUserCategoryForModernImageSelection
},
showAllCategoryForModernImageSelection () {
return this.$config.showAllCategoryForModernImageSelection
},
guestOsCategoriesSelectionDisallowed () {
return (!this.queryGuestOsCategoryId || this.options.guestOsCategories.length === 0) && (!!this.queryTemplateId || !!this.queryIsoId)
}
},
watch: {
@ -1354,7 +1370,7 @@ export default {
Object.keys(vnfAppConfig).forEach(field => {
this.vm[field] = this.vnfAppConfig[field]
})
this.template = ''
this.template = null
for (const key in this.options.templates) {
var template = _.find(_.get(this.options.templates[key], 'template', []), (option) => option.id === vnfAppConfig.templateid)
if (template) {
@ -1363,7 +1379,7 @@ export default {
}
}
this.iso = ''
this.iso = null
for (const key in this.options.isos) {
var iso = _.find(_.get(this.options.isos[key], 'iso', []), (option) => option.id === vnfAppConfig.isoid)
if (iso) {
@ -1487,7 +1503,7 @@ export default {
}
}
if (!this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
if (this.template && !this.template.deployasis && this.template.childtemplates && this.template.childtemplates.length > 0) {
this.vm.diskofferingid = ''
this.vm.diskofferingname = ''
this.vm.diskofferingsize = ''
@ -1527,7 +1543,6 @@ export default {
provide () {
return {
vmFetchTemplates: this.fetchAllTemplates,
vmFetchIsos: this.fetchAllIsos,
vmFetchNetworks: this.fetchNetwork
}
},
@ -1626,6 +1641,27 @@ export default {
}
}
},
getImageFilters (params, forReset) {
if (this.isModernImageSelection) {
if (this.form.guestoscategoryid === '0') {
return ['self']
}
if (this.isModernImageSelection && params && !forReset) {
if (params.featured) {
return ['featured']
} else if (params.public) {
return ['community']
}
}
return this.isNormalAndDomainUser ? ['executable'] : ['all']
}
return [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
]
},
getPropertyQualifiers (qualifiers, type) {
var result = ''
switch (type) {
@ -1665,19 +1701,21 @@ export default {
let zones = []
let apiName = ''
const params = {}
if (this.templateId) {
if (this.queryZoneId) {
zones.push(this.queryZoneId)
if (this.queryTemplateId) {
this.dataPreFill.templateid = this.queryTemplateId
}
return resolve(zones)
} else if (this.queryTemplateId) {
apiName = 'listVnfTemplates'
params.listall = true
params.templatefilter = this.isNormalAndDomainUser ? 'executable' : 'all'
params.id = this.templateId
} else if (this.isoId) {
params.id = this.queryTemplateId
this.dataPreFill.templateid = this.queryTemplateId
} else if (this.queryNetworkId) {
params.listall = true
params.isofilter = this.isNormalAndDomainUser ? 'executable' : 'all'
params.id = this.isoId
apiName = 'listIsos'
} else if (this.networkId) {
params.listall = true
params.id = this.networkId
params.id = this.queryNetworkId
apiName = 'listNetworks'
}
if (!apiName) return resolve(zones)
@ -1701,6 +1739,10 @@ export default {
})
},
async fetchData () {
this.architectureTypes.opts = this.$fetchCpuArchitectureTypes()
if (this.queryArchId) {
this.architectureTypes.opts = this.architectureTypes.opts.filter(o => o.id === this.queryArchId)
}
const zones = await this.fetchZoneByQuery()
if (zones && zones.length === 1) {
this.selectedZone = zones[0]
@ -1816,27 +1858,20 @@ export default {
},
updateFieldValue (name, value) {
if (name === 'templateid') {
this.tabKey = 'templateid'
this.form.templateid = value
this.form.isoid = null
this.resetFromTemplateConfiguration()
let template = ''
for (const key in this.options.templates) {
var t = _.find(_.get(this.options.templates[key], 'template', []), (option) => option.id === value)
if (t) {
this.template = t
this.templateConfigurations = []
this.selectedTemplateConfiguration = {}
this.templateNics = []
this.templateLicenses = []
this.templateProperties = {}
this.templateVnfNics = []
this.updateTemplateParameters()
template = t
for (const entry of Object.values(this.options.templates)) {
template = entry?.template.find(option => option.id === value) || null
if (template) {
this.template = template
break
}
}
if (template) {
this.resetTemplateAssociatedResources()
this.updateTemplateParameters()
var size = template.size / (1024 * 1024 * 1024) || 0 // bytes to GB
this.dataPreFill.minrootdisksize = Math.ceil(size)
this.updateTemplateLinkedUserData(template.userdataid)
@ -1851,19 +1886,6 @@ export default {
this.form.iodriverpolicy = template.details?.['io.policy']
this.form.keyboard = template.details?.keyboard
}
} else if (name === 'isoid') {
this.templateConfigurations = []
this.selectedTemplateConfiguration = {}
this.templateNics = []
this.templateLicenses = []
this.templateProperties = {}
this.templateVnfNics = []
this.tabKey = 'isoid'
this.resetFromTemplateConfiguration()
this.form.isoid = value
this.form.templateid = null
this.updateTemplateLinkedUserData(this.iso.userdataid)
this.userdataDefaultOverridePolicy = this.iso.userdatapolicy
} else if (['cpuspeed', 'cpunumber', 'memory'].includes(name)) {
this.vm[name] = value
this.form[name] = value
@ -1977,6 +1999,60 @@ export default {
getText (option) {
return _.get(option, 'displaytext', _.get(option, 'name'))
},
fetchGuestOsCategories (skipFetchImages) {
const key = 'guestOsCategories'
const params = this.params[key]
if (this.queryGuestOsCategoryId) {
params.options.id = this.queryGuestOsCategoryId
} else if (this.queryTemplateId) {
this.fetchAllTemplates()
return Promise.resolve()
}
return this.fetchOptions(params, key, ['zones'])
.then((res) => {
if (!this.options.guestOsCategories) {
this.options.guestOsCategories = []
}
if (!this.queryGuestOsCategoryId) {
if (this.showUserCategoryForModernImageSelection) {
const userCategory = {
id: '0',
name: this.$t('label.user')
}
if (this.$store.getters.avatar) {
userCategory.icon = {
base64image: this.$store.getters.avatar
}
}
this.options.guestOsCategories.push(userCategory)
}
if (this.showAllCategoryForModernImageSelection) {
this.options.guestOsCategories.push({
id: '-1',
name: this.$t('label.all')
})
}
}
if (this.options.guestOsCategories.length > 0) {
this.form.guestoscategoryid = this.options.guestOsCategories[0].id
}
if (skipFetchImages) {
return
}
this.fetchAllTemplates()
})
.catch((e) => {
console.error('Error fetching guestOsCategories:', e)
})
},
changeArchitecture (arch) {
this.selectedArchitecture = arch
if (this.isModernImageSelection) {
this.fetchGuestOsCategories()
return
}
this.fetchAllTemplates()
},
handleSubmitAndStay (e) {
this.form.stayonpage = true
this.handleSubmit(e.domEvent)
@ -2096,7 +2172,7 @@ export default {
createVnfAppData.userdata = this.$toBase64AndURIEncoded(values.userdata)
}
// step 2: select template/iso
if (this.tabKey === 'templateid') {
if (this.imageType === 'templateid') {
createVnfAppData.templateid = values.templateid
values.hypervisor = null
} else {
@ -2105,7 +2181,7 @@ export default {
if (this.showRootDiskSizeChanger && values.rootdisksize && values.rootdisksize > 0) {
createVnfAppData.rootdisksize = values.rootdisksize
} else if (this.rootDiskSizeFixed > 0 && !this.template.deployasis) {
} else if (this.rootDiskSizeFixed > 0 && this.template && !this.template.deployasis) {
createVnfAppData.rootdisksize = this.rootDiskSizeFixed
}
@ -2320,9 +2396,10 @@ export default {
} else {
credentials.push(this.$t('message.vnf.no.credentials'))
}
const credentialsDesc = credentials.join('<br>')
this.$notification.success({
message: `${this.$t('message.vnf.credentials.default')} ` + name,
description: (<span v-html={credentials.join('<br>')}></span>),
description: (<span v-html={credentialsDesc}></span>),
duration: 0
})
eventBus.emit('vm-refresh-data')
@ -2394,10 +2471,9 @@ export default {
})
},
fetchOptions (param, name, exclude) {
if (exclude && exclude.length > 0) {
if (exclude.includes(name)) {
return
}
return new Promise((resolve, reject) => {
if (exclude && exclude.length > 0 && exclude.includes(name)) {
return resolve(null)
}
this.loading[name] = true
param.loading = true
@ -2412,10 +2488,10 @@ export default {
if (Object.keys(responseItem).length === 0) {
this.rowCount[name] = 0
this.options[name] = []
return
return resolve(null)
}
if (!responseKey.includes('response')) {
return
return resolve(null)
}
_.map(responseItem, (response, key) => {
if (key === 'count') {
@ -2449,24 +2525,37 @@ export default {
}
}
})
resolve(response)
}).catch(function (error) {
console.log(error.stack)
param.loading = false
reject(error)
}).finally(() => {
this.loading[name] = false
})
})
},
fetchTemplates (templateFilter, params) {
const args = Object.assign({}, params)
if (args.keyword || args.category !== templateFilter) {
if (this.isModernImageSelection && this.form.guestoscategoryid && !['-1', '0'].includes(this.form.guestoscategoryid)) {
args.oscategoryid = this.form.guestoscategoryid
}
if (args.keyword || (args.category && args.category !== templateFilter)) {
args.page = 1
args.pageSize = args.pageSize || 10
}
args.zoneid = _.get(this.zone, 'id')
if (this.isZoneSelectedMultiArch) {
args.arch = this.selectedArchitecture
}
args.templatefilter = templateFilter
args.details = 'all'
args.showicon = 'true'
args.id = this.templateId
args.id = this.queryTemplateId
delete args.category
delete args.public
delete args.featured
return new Promise((resolve, reject) => {
api('listVnfTemplates', args).then((response) => {
@ -2477,39 +2566,20 @@ export default {
})
})
},
fetchIsos (isoFilter, params) {
const args = Object.assign({}, params)
if (args.keyword || args.category !== isoFilter) {
args.page = 1
args.pageSize = args.pageSize || 10
}
args.zoneid = _.get(this.zone, 'id')
args.isoFilter = isoFilter
args.bootable = true
args.showicon = 'true'
args.id = this.isoId
return new Promise((resolve, reject) => {
api('listIsos', args).then((response) => {
resolve(response)
}).catch((reason) => {
// ToDo: Handle errors
reject(reason)
})
})
},
fetchAllTemplates (params) {
const promises = []
const templates = {}
this.loading.templates = true
this.templateFilter.forEach((filter) => {
this.imageSearchFilters = params
const templateFilters = this.getImageFilters(params)
templateFilters.forEach((filter) => {
templates[filter] = { count: 0, template: [] }
promises.push(this.fetchTemplates(filter, params))
})
this.options.templates = templates
Promise.all(promises).then((response) => {
response.forEach((resItem, idx) => {
templates[this.templateFilter[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
templates[templateFilters[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
this.options.templates = { ...templates }
})
}).catch((reason) => {
@ -2518,36 +2588,60 @@ export default {
this.loading.templates = false
})
},
fetchAllIsos (params) {
const promises = []
const isos = {}
this.loading.isos = true
this.isoFilter.forEach((filter) => {
isos[filter] = { count: 0, iso: [] }
promises.push(this.fetchIsos(filter, params))
})
this.options.isos = isos
Promise.all(promises).then((response) => {
response.forEach((resItem, idx) => {
isos[this.isoFilter[idx]] = _.isEmpty(resItem.listisosresponse) ? { count: 0, iso: [] } : resItem.listisosresponse
this.options.isos = { ...isos }
})
}).catch((reason) => {
console.log(reason)
}).finally(() => {
this.loading.isos = false
})
},
filterOption (input, option) {
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
},
resetTemplatesList () {
const templates = {}
const templateFilters = this.getImageFilters(null, true)
templateFilters.forEach((filter) => {
templates[filter] = { count: 0, template: [] }
})
this.options.templates = templates
},
resetTemplateAssociatedResources () {
this.templateConfigurations = []
this.selectedTemplateConfiguration = {}
this.templateNics = []
this.templateLicenses = []
this.templateProperties = {}
this.templateVnfNics = []
},
async fetchZoneOptions () {
let guestOsFetch = null
for (const [name, param] of Object.entries(this.params)) {
if (this.queryNetworkId && name === 'networks') {
param.options = { id: this.queryNetworkId }
}
const shouldLoad = !('isLoad' in param) || param.isLoad
if (!shouldLoad) continue
if (this.isModernImageSelection && name === 'guestOsCategories') {
guestOsFetch = this.fetchGuestOsCategories(true)
} else {
this.fetchOptions(param, name, ['zones'])
}
}
if (this.isModernImageSelection && guestOsFetch) {
await guestOsFetch
}
this.fetchAllTemplates()
this.updateTemplateKey()
this.formModel = toRaw(this.form)
},
onSelectZoneId (value) {
if (this.dataPreFill.zoneid !== value) {
this.dataPreFill = {}
}
this.zoneId = value
this.podId = null
this.clusterId = null
this.zone = _.find(this.options.zones, (option) => option.id === value)
this.zoneSelected = true
this.isZoneSelectedMultiArch = this.zone.ismultiarch
if (this.isZoneSelectedMultiArch) {
this.selectedArchitecture = this.architectureTypes.opts[0].id
}
this.form.startvm = true
this.form.vnfconfiguremanagement = false
this.form.vnfcidrlist = '0.0.0.0/0'
@ -2558,27 +2652,8 @@ export default {
this.form.hostid = undefined
this.form.templateid = undefined
this.form.isoid = undefined
this.tabKey = 'templateid'
if (this.isoId) {
this.tabKey = 'isoid'
}
_.each(this.params, (param, name) => {
if (this.networkId && name === 'networks') {
param.options = {
id: this.networkId
}
}
if (!('isLoad' in param) || param.isLoad) {
this.fetchOptions(param, name, ['zones'])
}
})
if (this.tabKey === 'templateid') {
this.fetchAllTemplates()
} else {
this.fetchAllIsos()
}
this.updateTemplateKey()
this.formModel = toRaw(this.form)
this.resetTemplatesList()
this.fetchZoneOptions()
},
onSelectPodId (value) {
this.podId = value
@ -2603,15 +2678,16 @@ export default {
this.form.hostid = undefined
}
},
onSelectGuestOsCategory (value) {
this.form.guestoscategoryid = value
this.fetchAllTemplates(this.imageSearchFilters)
},
handleSearchFilter (name, options) {
this.params[name].options = { ...this.params[name].options, ...options }
this.fetchOptions(this.params[name], name)
},
onTabChange (key, type) {
this[type] = key
if (key === 'isoid') {
this.fetchAllIsos()
}
},
onUserdataTabChange (key, type) {
this[type] = key

View File

@ -19,28 +19,35 @@
<a-form
v-ctrl-enter="handleSubmit"
@finish="handleSubmit"
layout="vertical"
>
<a-alert
type="warning"
show-icon
>
<template #message><span
style="margin-bottom: 5px"
v-html="$t('message.reinstall.vm')"
/></template>
layout="vertical">
<a-alert type="warning" show-icon>
<template #message>
<span style="margin-bottom: 5px" v-html="$t('message.reinstall.vm')" />
</template>
</a-alert>
<a-form-item>
<os-based-image-selection
v-if="isModernImageSelection"
:imageTypeSelectionAllowed="false"
:guestOsCategories="options.guestOsCategories"
:guestOsCategoriesLoading="loading.guestOsCategories"
:selectedGuestOsCategoryId="selectedGuestOsCategoryId"
:imageItems="options.templates"
:imagesLoading="loading.templates"
:filterOption="filterOption"
:preFillContent="dataPreFill"
@change-guest-os-category="onSelectGuestOsCategory"
@handle-image-search-filter="($event) => fetchAllTemplates($event)"
@update-image="updateFieldValue" />
<a-form-item v-else>
<template-iso-selection
input-decorator="templateid"
:items="templates"
:items="options.templates"
:selected="tabKey"
:loading="loading.templates"
:preFillContent="dataPrefill"
:preFillContent="dataPreFill"
:key="templateKey"
@handle-search-filter="($event) => fetchAllTemplates($event)"
@update-template-iso="updateFieldValue"
/>
@update-template-iso="updateFieldValue" />
</a-form-item>
<a-form-item>
<template #label>
@ -56,12 +63,12 @@
</a-form-item>
<a-form-item v-if="overrideDiskOffering">
<disk-offering-selection
:items="diskOfferings"
:row-count="diskOfferingCount"
:items="options.diskOfferings"
:row-count="count.diskOfferings"
:zoneId="resource.zoneId"
:value="diskOffering ? diskOffering.id : ''"
:loading="loading.diskOfferings"
:preFillContent="dataPrefill"
:preFillContent="dataPreFill"
:isIsoSelected="false"
:isRootDiskOffering="true"
@on-selected-disk-size="onSelectDiskSize"
@ -127,6 +134,7 @@
import { api } from '@/api'
import DiskOfferingSelection from '@views/compute/wizard/DiskOfferingSelection'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
import OsBasedImageSelection from '@views/compute/wizard/OsBasedImageSelection'
import TemplateIsoSelection from '@views/compute/wizard/TemplateIsoSelection'
import TooltipLabel from '@/components/widgets/TooltipLabel'
import _ from 'lodash'
@ -137,6 +145,7 @@ export default {
DiskOfferingSelection,
DiskSizeSelection,
TemplateIsoSelection,
OsBasedImageSelection,
TooltipLabel
},
props: {
@ -151,10 +160,19 @@ export default {
overrideDiskOffering: false,
overrideDiskSize: false,
expungeDisk: false,
selectedDiskOffering: {},
selectedGuestOsCategoryId: null,
options: {
templates: {},
diskOfferings: [],
guestOsCategories: []
},
loading: {
templates: false,
diskOfferings: false
diskOfferings: false,
guestOsCategories: false
},
count: {
diskOfferings: 0
},
rootDiskSizeKey: 'details[0].rootdisksize',
minIopsKey: 'details[0].minIops',
@ -162,16 +180,11 @@ export default {
rootdisksize: 0,
minIops: 0,
maxIops: 0,
templateFilter: [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
],
diskOffering: {},
diskOfferingCount: 0,
imageSearchFilters: null,
templateid: null,
templateKey: 0,
dataPrefill: {
dataPreFill: {
templateid: this.resource.templateid,
diskofferingid: this.resource.diskofferingid
}
@ -183,10 +196,52 @@ export default {
created () {
this.fetchData()
},
computed: {
isNormalAndDomainUser () {
return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype)
},
isModernImageSelection () {
return this.$config.imageSelectionInterface === undefined || this.$config.imageSelectionInterface === 'modern'
},
imageSelection () {
return this.isModernImageSelection ? 'modern' : 'legacy'
},
showUserCategoryForModernImageSelection () {
return this.$config.showUserCategoryForModernImageSelection === undefined || this.$config.showUserCategoryForModernImageSelection
},
showAllCategoryForModernImageSelection () {
return this.$config.showAllCategoryForModernImageSelection
}
},
methods: {
fetchData () {
this.fetchDiskOfferings({})
if (this.isModernImageSelection) {
this.fetchGuestOsCategories()
} else {
this.fetchAllTemplates()
}
},
getImageFilters (params, forReset) {
if (this.isModernImageSelection) {
if (this.selectedGuestOsCategoryId === '0') {
return ['self']
}
if (this.isModernImageSelection && params && !forReset) {
if (params.featured) {
return ['featured']
} else if (params.public) {
return ['community']
}
}
return this.isNormalAndDomainUser ? ['executable'] : ['all']
}
return [
'featured',
'community',
'selfexecutable',
'sharedexecutable'
]
},
closeAction () {
this.$emit('close-action')
@ -244,19 +299,56 @@ export default {
this.closeAction()
})
},
fetchGuestOsCategories () {
this.loading.guestOsCategories = true
const params = {
zoneid: this.resource.zoneid,
arch: this.resource.arch,
isiso: false,
featured: true,
showicon: true
}
api('listOsCategories', params).then((response) => {
this.options.guestOsCategories = response?.listoscategoriesresponse?.oscategory || []
if (this.showUserCategoryForModernImageSelection) {
const userCategory = {
id: '0',
name: this.$t('label.user')
}
if (this.$store.getters.avatar) {
userCategory.icon = {
base64image: this.$store.getters.avatar
}
}
this.options.guestOsCategories.push(userCategory)
}
if (this.showAllCategoryForModernImageSelection) {
this.options.guestOsCategories.push({
id: '-1',
name: this.$t('label.all')
})
}
this.selectedGuestOsCategoryId = this.options.guestOsCategories[0].id
}).finally(() => {
this.loading.guestOsCategories = false
this.fetchAllTemplates()
})
},
fetchAllTemplates (params) {
const promises = []
const templates = {}
this.loading.templates = true
this.templateFilter.forEach((filter) => {
this.imageSearchFilters = params
const templateFilters = this.getImageFilters(params)
templateFilters.forEach((filter) => {
templates[filter] = { count: 0, template: [] }
promises.push(this.fetchTemplates(filter, params))
})
this.templates = templates
this.options.templates = templates
Promise.all(promises).then((response) => {
response.forEach((resItem, idx) => {
templates[this.templateFilter[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
this.templates = { ...templates }
templates[templateFilters[idx]] = _.isEmpty(resItem.listtemplatesresponse) ? { count: 0, template: [] } : resItem.listtemplatesresponse
this.options.templates = { ...templates }
})
}).catch((reason) => {
console.log(reason)
@ -266,12 +358,18 @@ export default {
},
fetchTemplates (templateFilter, params) {
const args = Object.assign({}, params)
if (args.keyword || args.category !== templateFilter) {
if (this.isModernImageSelection && this.selectedGuestOsCategoryId && !['-1', '0'].includes(this.selectedGuestOsCategoryId)) {
args.oscategoryid = this.selectedGuestOsCategoryId
}
if (args.keyword || (args.category && args.category !== templateFilter)) {
args.page = 1
args.pageSize = args.pageSize || 10
}
args.zoneid = _.get(this.zone, 'id')
args.zoneid = this.resource.zoneid
args.templatefilter = templateFilter
if (this.resource.arch) {
args.arch = this.resource.arch
}
args.details = 'all'
args.showicon = 'true'
@ -285,17 +383,24 @@ export default {
},
fetchDiskOfferings (params) {
api('listDiskOfferings', { zoneid: this.resource.zoneid, listall: true, ...params }).then((response) => {
this.diskOfferings = response?.listdiskofferingsresponse?.diskoffering || []
this.diskOfferingCount = response?.listdiskofferingsresponse?.count || 0
this.options.diskOfferings = response?.listdiskofferingsresponse?.diskoffering || []
this.count.diskOfferings = response?.listdiskofferingsresponse?.count || 0
})
},
onSelectGuestOsCategory (value) {
this.selectedGuestOsCategoryId = value
this.fetchAllTemplates(this.imageSearchFilters)
},
onSelectDiskSize (rowSelected) {
this.diskOffering = rowSelected
this.dataPrefill.diskofferingid = rowSelected.id
this.dataPreFill.diskofferingid = rowSelected.id
},
updateFieldValue (input, value) {
this[input] = value
this.dataPrefill[input] = value
this.dataPreFill[input] = value
},
filterOption (input, option) {
return option.label.toUpperCase().indexOf(input.toUpperCase()) >= 0
}
}
}

View File

@ -26,13 +26,13 @@
v-ctrl-enter="handleSubmit"
@finish="handleSubmit">
<div v-if="template && template.userdataid">
<a-text type="primary">
<a-typography-text>
The template "{{ $t(this.template.name) }}" is linked to Userdata "{{ $t(this.template.userdataname) }}" with override policy "{{ $t(this.template.userdatapolicy) }}"
</a-text><br/><br/>
</a-typography-text><br/><br/>
<div v-if="templateUserDataParams.length > 0 && !doUserdataOverride">
<a-text type="primary" v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
<a-typography-text v-if="this.template && this.template.userdataid && templateUserDataParams.length > 0">
Enter the values for the variables in userdata
</a-text>
</a-typography-text>
<a-input-group>
<a-table
size="small"

View File

@ -179,6 +179,7 @@ export default {
}
}
},
emits: ['update-network-config', 'select-default-network-item', 'handler-error'],
methods: {
initForm () {
this.formRef = ref()

View File

@ -0,0 +1,208 @@
// 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.
<template>
<a-list
class="form-item-scroll"
itemLayout="vertical"
size="small"
:dataSource="imagesList"
:pagination="false">
<template #renderItem="{ item, index }">
<a-list-item :key="item.id" @click="onClickRow(item)">
<a-radio-group
class="radio-group"
:key="index"
v-model:value="value"
@change="($event) => updateSelectionTemplateIso($event.target.value)">
<a-radio
class="radio-group__radio"
:value="item.id">
<resource-icon
v-if="item.icon && item.icon.base64image"
class="radio-group__os-logo"
:image="item.icon.base64image"
size="2x" />
<os-logo
v-else
class="radio-group__os-logo"
size="2x"
:osId="item.ostypeid"
:os-name="item.osName" />
&nbsp;
{{ item.displaytext }}
<span v-if="item?.projectid">
| <project-outlined /> {{ item.project }}
</span>
<a-tooltip :title="$t('label.passwordenabled')" v-if="item.passwordenabled">
<lock-outlined style="margin-left: 8px;"/>
</a-tooltip>
<a-tooltip :title="$t('label.userdata')" v-if="item.userdataid">
<solution-outlined style="margin-left: 8px;"/>
</a-tooltip>
<a-tooltip :title="$t('label.isdynamicallyscalable')" v-if="item.isdynamicallyscalable">
<arrows-alt-outlined style="margin-left: 8px;"/>
</a-tooltip>
<a-tooltip :title="$t('label.isextractable')" v-if="item.isextractable">
<file-zip-outlined style="margin-left: 8px;"/>
</a-tooltip>
<a-tag v-if="item.isfeatured" style="margin-left: 8px;">
{{ $t('label.isfeatured') }}
</a-tag>
<a-tag v-if="item.ispublic" style="margin-left: 8px;">
{{ $t('label.ispublic') }}
</a-tag>
<a-tag v-if="item.directdownload" style="margin-left: 8px;">
{{ $t('label.directdownload') }}
</a-tag>
<a-tag v-if="item.requireshvm" style="margin-left: 8px;">
{{ $t('label.requireshvm') }}
</a-tag>
</a-radio>
</a-radio-group>
</a-list-item>
</template>
</a-list>
<div style="display: block; text-align: right;">
<a-pagination
size="small"
:current="options.page"
:pageSize="options.pageSize"
:total="itemCount"
:showTotal="total => `${$t('label.total')} ${total} ${$t('label.items')}`"
:pageSizeOptions="['10', '20', '40', '80', '100', '200']"
@change="onChangePage"
@showSizeChange="onChangePageSize"
showSizeChanger>
<template #buildOptionText="props">
<span>{{ props.value }} / {{ $t('label.page') }}</span>
</template>
</a-pagination>
</div>
</template>
<script>
import OsLogo from '@/components/widgets/OsLogo'
import ResourceIcon from '@/components/view/ResourceIcon'
export default {
name: 'OsBasedImageRadioGroup',
components: {
OsLogo,
ResourceIcon
},
props: {
imagesList: {
type: Array,
default: () => []
},
inputDecorator: {
type: String,
default: ''
},
selected: {
type: String,
default: ''
},
itemCount: {
type: Number,
default: 0
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
return {
value: '',
image: '',
options: {
page: 1,
pageSize: 10
}
}
},
mounted () {
this.onSelectTemplateIso()
},
watch: {
selected (newVal, oldVal) {
if (newVal === oldVal) return
this.onSelectTemplateIso()
}
},
emits: ['emit-update-image', 'handle-search-filter'],
methods: {
onSelectTemplateIso () {
if (this.inputDecorator === 'templateid') {
this.value = this.preFillContent?.templateid ? this.preFillContent.templateid : this.selected
} else {
this.value = this.preFillContent?.isoid ? this.preFillContent.isoid : this.selected
}
this.$emit('emit-update-image', this.inputDecorator, this.value)
},
updateSelectionTemplateIso (id) {
this.$emit('emit-update-image', this.inputDecorator, id)
},
onChangePage (page, pageSize) {
this.options.page = page
this.options.pageSize = pageSize
this.$emit('handle-search-filter', this.options)
},
onChangePageSize (page, pageSize) {
this.options.page = page
this.options.pageSize = pageSize
this.$emit('handle-search-filter', this.options)
},
onClickRow (os) {
this.value = os.id
this.$emit('emit-update-image', this.inputDecorator, this.value)
}
}
}
</script>
<style lang="less" scoped>
.radio-group {
margin: 0.5rem 0;
:deep(.ant-radio) {
margin-right: 0px;
}
&__os-logo {
margin-top: -4px;
}
}
:deep(.ant-spin-container) {
max-height: 200px;
overflow-y: auto;
}
.pagination {
margin-top: 20px;
float: right;
}
:deep(.ant-list-split) .ant-list-item {
cursor: pointer;
}
</style>

View File

@ -0,0 +1,373 @@
// 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.
<template>
<div>
<a-form-item v-if="imageTypeSelectionAllowed" :label="$t('label.type')" name="imagetype" ref="imagetype">
<a-radio-group
v-model:value="localSelectedImageType"
button-style="solid"
:disabled="imagePreSelected"
@change="emitChangeImageType()">
<a-radio-button value="templateid">{{ $t('label.template') }}</a-radio-button>
<a-radio-button value="isoid">{{ $t('label.iso') }}</a-radio-button>
</a-radio-group>
<div style="margin-top: 5px; margin-bottom: 5px;">
{{ $t('message.' + localSelectedImageType.replace('id', '') + '.desc') }}
</div>
</a-form-item>
<a-form-item
:label="$t('label.os')"
name="guestoscategoryid"
ref="guestoscategoryid"
v-if="!guestOsCategoriesSelectionDisallowed">
<block-radio-group-select
:maxBlocks="16"
:items="guestOsCategories"
:selectedValue="localSelectedGuestOsCategoryId"
:horizontalGutter="6"
:verticalGutter="6"
blockSize="square"
@change="handleGuestOsCategoryChange">
<template #radio-option="{ item }">
<div class="radio-option">
<div class="radio-opion__icon">
<resource-icon v-if="item.icon && item.icon.base64image" :image="item.icon.base64image" size="os" style="margin-bottom: 2px; margin-left: 1px" />
<font-awesome-icon v-else-if="['-1', '0'].includes(item.id)" :icon="['fas', item.id === '0' ? 'user' : 'images']" size="2x" :style="categoryFontAwesomeIconStyle" />
<os-logo v-else size="2x" :os-name="item.name" />
</div>
<a-tooltip placement="top" :title="item.name">
<div class="ellipsis">{{ item.name }}</div>
</a-tooltip>
</div>
</template>
<template #select-option="{ item }">
<span>
<resource-icon v-if="item.icon && item.icon.base64image" :image="item.icon.base64image" size="2x" style="margin-right: 5px"/>
<font-awesome-icon
v-else-if="item.id === '0'"
:icon="['fas', 'user']"
size="2x"
:style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#666' }]"
/>
<os-logo v-else :os-name="item.name" style="margin-right: 5px" />
{{ item.name }}
</span>
</template>
</block-radio-group-select>
</a-form-item>
<a-card>
<os-based-image-selection-search-view
v-if="!imagePreSelected"
class="search-input"
:filtersDisabled="searchFiltersDisabled"
@search="handleImageSearch">
</os-based-image-selection-search-view>
<a-spin :spinning="imagesLoading">
<os-based-image-radio-group
:imagesList="imagesList"
:itemCount="imagesCount"
:input-decorator="localSelectedImageType"
:selected="selectedImageId"
:preFillContent="preFillContent"
@emit-update-image="updateImage"
@handle-search-filter="($event) => eventPagination($event)"
/>
</a-spin>
<div v-if="diskSizeSelectionAllowed">
<div>
{{ $t('label.override.rootdisk.size') }}
<a-switch
v-model:checked="localRootDiskOverrideChecked"
:disabled="rootDiskOverrideDisabled"
@change="handleRootDiskOverrideCheckedChange"
style="margin-left: 10px;"/>
<div v-if="diskSizeSelectionDeployAsIsMessageVisible"> {{ $t('message.deployasis') }} </div>
</div>
<disk-size-selection
v-if="showRootDiskSizeChanger"
:input-decorator="diskSizeSelectionInputDecorator"
:preFillContent="preFillContent"
:isCustomized="true"
:minDiskSize="preFillContent.minrootdisksize"
@update-disk-size="emitUpdateDiskSize"
style="margin-top: 10px;"/>
</div>
<a-form-item :label="$t('label.hypervisor')" v-if="localSelectedImageType === 'isoid'">
<a-select
v-model:value="localSelectedIsoHypervisor"
:preFillContent="preFillContent"
:options="isoHypervisorItems"
@change="handleIsoHypervisorChange()"
showSearch
optionFilterProp="label"
:filterOption="filterOption" />
</a-form-item>
</a-card>
</div>
</template>
<script>
import BlockRadioGroupSelect from '@/components/widgets/BlockRadioGroupSelect.vue'
import ResourceIcon from '@/components/view/ResourceIcon'
import OsLogo from '@/components/widgets/OsLogo'
import OsBasedImageSelectionSearchView from '@views/compute/wizard/OsBasedImageSelectionSearchView'
import OsBasedImageRadioGroup from '@views/compute/wizard/OsBasedImageRadioGroup'
import DiskSizeSelection from '@views/compute/wizard/DiskSizeSelection'
export default {
name: 'OsBasedImageSelection',
components: {
BlockRadioGroupSelect,
ResourceIcon,
OsLogo,
OsBasedImageSelectionSearchView,
OsBasedImageRadioGroup,
DiskSizeSelection
},
props: {
imageTypeSelectionAllowed: {
type: Boolean,
default: true
},
selectedImageType: {
type: String,
default: 'templateid'
},
imagePreSelected: {
type: Boolean,
default: false
},
guestOsCategoriesSelectionDisallowed: {
type: Boolean,
default: false
},
guestOsCategories: {
type: Array,
default: () => []
},
guestOsCategoriesLoading: {
type: Boolean,
default: false
},
selectedGuestOsCategoryId: {
type: String,
default: undefined
},
imageItems: {
type: Object,
default: () => {}
},
imagesLoading: {
type: Boolean,
default: false
},
diskSizeSelectionAllowed: {
type: Boolean,
default: true
},
diskSizeSelectionDeployAsIsMessageVisible: {
type: Boolean,
default: false
},
diskSizeSelectionInputDecorator: {
type: String,
default: 'rootdisksize'
},
rootDiskOverrideDisabled: {
type: Boolean,
default: false
},
rootDiskOverrideChecked: {
type: Boolean,
default: false
},
isoHypervisorItems: {
type: Array,
default: () => []
},
selectedIsoHypervisor: {
type: String,
default: undefined
},
filterOption: {
type: Function,
required: true
},
preFillContent: {
type: Object,
default: () => {}
}
},
data () {
return {
filterType: 'executable',
selectedImageId: '',
imageSearchFilters: {},
showRootDiskSizeChanger: false,
// Local data properties to mirror props
localSelectedImageType: this.selectedImageType,
localSelectedGuestOsCategoryId: this.selectedGuestOsCategoryId,
localRootDiskOverrideChecked: this.rootDiskOverrideChecked,
localSelectedIsoHypervisor: this.selectedIsoHypervisor
}
},
mounted () {
this.filterType = this.defaultImageFilter
},
watch: {
selectedImageType (newValue) {
this.localSelectedImageType = newValue
},
guestOsCategories (newValue) {
this.imageSearchFilters = {}
},
selectedGuestOsCategoryId (newValue) {
this.localSelectedGuestOsCategoryId = newValue
this.updateImageFilterType()
},
rootDiskOverrideChecked (newValue) {
this.localRootDiskOverrideChecked = newValue
},
selectedIsoHypervisor (newValue) {
this.localSelectedIsoHypervisor = newValue
}
},
computed: {
defaultImageFilter () {
return ['DomainAdmin', 'User'].includes(this.$store.getters.userInfo.roletype) ? 'executable' : 'all'
},
imagesList () {
if (!this.localSelectedImageType || !this.imageItems || !this.imageItems[this.filterType]) {
return []
}
const imageTypeKey = this.localSelectedImageType.slice(0, -2)
return this.imageItems[this.filterType][imageTypeKey] || []
},
imagesCount () {
return this.imageItems[this.filterType] ? this.imageItems[this.filterType].count || 0 : 0
},
selectedCategory () {
if (this.localSelectedGuestOsCategoryId && this.guestOsCategories) {
return this.guestOsCategories.find(option => option.id === this.localSelectedGuestOsCategoryId)
}
return null
},
categoryFontAwesomeIconStyle () {
return [this.$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#666' }]
},
searchFiltersDisabled () {
return this.selectedCategory?.disableimagefilters
}
},
emits: ['change-image-type', 'change-guest-os-category', 'update-image', 'handle-image-search-filter', 'change-root-disk-override-checked', 'update-disk-size', 'change-iso-hypervisor'],
methods: {
emitChangeImageType () {
this.$emit('change-image-type', this.localSelectedImageType)
},
handleGuestOsCategoryChange (value) {
this.localSelectedGuestOsCategoryId = value
this.$emit('change-guest-os-category', this.localSelectedGuestOsCategoryId)
},
updateImage (decorator, id) {
this.selectedImageId = id
this.$emit('update-image', decorator, id)
},
handleImageSearch (searchFilters) {
this.imageSearchFilters = {
page: 1,
pageSize: 10
}
Object.assign(this.imageSearchFilters, searchFilters)
this.updateImageFilterType()
this.emitSearchFilter()
},
updateImageFilterType () {
this.filterType = this.defaultImageFilter
if (this.localSelectedGuestOsCategoryId === '0') {
this.filterType = 'self'
} else {
if (this.imageSearchFilters?.featured) {
this.filterType = 'featured'
} else if (this.imageSearchFilters?.public) {
this.filterType = 'community'
}
}
},
eventPagination (pagination) {
Object.assign(this.imageSearchFilters, pagination)
this.emitSearchFilter()
},
emitSearchFilter () {
this.$emit('handle-image-search-filter', this.imageSearchFilters)
},
changeFilterType (value) {
this.filterType = value
},
handleRootDiskOverrideCheckedChange (value) {
this.showRootDiskSizeChanger = value
this.$emit('change-root-disk-override-checked', value)
},
emitUpdateDiskSize (decorator, value) {
this.$emit('update-disk-size', decorator, value)
},
handleIsoHypervisorChange () {
this.$emit('change-iso-hypervisor', this.localIsoHypervisor)
}
}
}
</script>
<style lang="less" scoped>
.search-input {
z-index: 8;
@media (max-width: 600px) {
position: relative;
width: 100%;
top: 0;
right: 0;
}
}
:deep(.ant-tabs-nav-scroll) {
min-height: 45px;
}
.radio-option {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
padding-top: 8px;
}
.ellipsis {
max-width: 80px;
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.radio-opion__icon {
width: 30px;
height: 30px;
object-fit: contain;
}
</style>

View File

@ -0,0 +1,119 @@
// 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.
<template>
<span class="filter-group">
<a-row type="flex">
<a-col flex="200px" v-if="!filtersDisabled">
<a-select
mode="multiple"
class="filter-select"
:placeholder="$t('label.filterby')"
v-model:value="filterValues"
@change="handleFilterChange"
showSearch
allowClear
:showArrow="true"
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}">
<template #suffixIcon><filter-outlined class="ant-select-suffix" /></template>
<a-select-option
v-for="filter in filters"
:key="filter"
:label="$t('label.' + filter)">
{{ $t('label.' + filter) }}
</a-select-option>
</a-select>
</a-col>
<a-col flex="auto">
<a-input-search
class="filter-search"
:placeholder="$t('label.search')"
v-model:value="searchedText"
allowClear
@search="handleTextSearch" />
</a-col>
</a-row>
</span>
</template>
<script>
export default {
name: 'OsBasedImageSelectionSearchView',
props: {
filtersDisabled: {
type: Boolean,
default: false
}
},
data () {
return {
filters: [
'public',
'featured'
],
filterValues: undefined,
searchedText: null,
paramsFilter: {}
}
},
created () {
},
methods: {
handleTextSearch (value) {
this.searchedText = value
if (value) {
this.paramsFilter.keyword = value
} else {
delete this.paramsFilter.keyword
}
const params = this.paramsFilter
if (this.filtersDisabled) {
delete this.paramsFilter.public
delete this.paramsFilter.featured
}
this.$emit('search', params)
},
handleFilterChange () {
this.paramsFilter = {}
if (Array.isArray(this.filterValues)) {
this.filterValues.forEach(e => {
this.paramsFilter[e] = true
})
}
if (this.searchedText) {
this.paramsFilter.keyword = this.searchedText
}
this.$emit('search', this.paramsFilter)
}
}
}
</script>
<style lang="less" scoped>
.filter-group .ant-select,
.filter-group .ant-input-search {
margin-right: 12px;
}
.filter-select {
width: 200px;
}
</style>

View File

@ -0,0 +1,51 @@
// 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.
<template>
<block-radio-group-select>
<template #radio-option="{ item }">
<span>
<resource-icon
v-if="item && item.icon && item.icon.base64image"
:image="item.icon.base64image"
size="2x" />
<global-outlined size="2x" v-else />
{{ item.name }}
</span>
</template>
<template #select-option="{ item }">
<span>
<resource-icon v-if="item.icon && zone1.icon.base64image" :image="item.icon.base64image" size="2x" style="margin-right: 5px"/>
<global-outlined v-else style="margin-right: 5px" />
{{ item.name }}
</span>
</template>
</block-radio-group-select>
</template>
<script>
import BlockRadioGroupSelect from '@/components/widgets/BlockRadioGroupSelect.vue'
import ResourceIcon from '@/components/view/ResourceIcon'
export default {
name: 'ZoneBlockRadioGroupSelect',
components: {
BlockRadioGroupSelect,
ResourceIcon
}
}
</script>

View File

@ -38,8 +38,8 @@
:rowExpandable="(record) => record.downloaddetails.length > 0">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'zonename'">
<span v-if="fetchZoneIcon(record.zoneid)">
<resource-icon :image="zoneIcon" size="1x" style="margin-right: 5px"/>
<span v-if="record.zoneicon && record.zoneicon.base64image">
<resource-icon :image="record.zoneicon.base64image" size="2x" style="margin-right: 5px"/>
</span>
<global-outlined v-else style="margin-right: 5px" />
<span> {{ record.zonename }} </span>
@ -52,6 +52,14 @@
<span v-if="record.created">{{ $toLocaleDate(record.created) }}</span>
</template>
<template v-if="column.key === 'actions'">
<span style="margin-right: 5px" v-if="'deployVirtualMachine' in $store.getters.apis">
<tooltip-button
:disabled="!record.isready"
:title="$t('label.vm.add')"
icon="rocket-outlined"
@onClick="onAddInstance(record)"/>
</span>
<span v-if="isActionsOnIsoPermitted">
<span style="margin-right: 5px">
<tooltip-button
:tooltip="$t('label.action.copy.iso')"
@ -77,6 +85,7 @@
icon="delete-outlined" />
</a-popconfirm>
</span>
</span>
</template>
</template>
<template #expandedRowRender="{ record }">
@ -162,7 +171,7 @@
}"
:loading="zoneLoading"
v-focus="true">
<a-select-option v-for="zone in zones" :key="zone.id" :label="zone.name">
<a-select-option v-for="zone in copyZones" :key="zone.id" :label="zone.name">
<div>
<span v-if="zone.icon && zone.icon.base64image">
<resource-icon :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/>
@ -237,6 +246,7 @@ export default {
showCopyActionForm: false,
currentRecord: {},
zones: [],
copyZones: [],
zoneLoading: false,
copyLoading: false,
deleteLoading: false,
@ -278,6 +288,13 @@ export default {
key: 'isready',
title: this.$t('label.isready'),
dataIndex: 'isready'
},
{
key: 'actions',
title: '',
dataIndex: 'actions',
fixed: 'right',
width: 130
}
]
this.storagePoolInnerColumns = [
@ -308,15 +325,6 @@ export default {
dataIndex: 'downloadState'
}
]
if (this.isActionPermitted()) {
this.columns.push({
key: 'actions',
title: '',
dataIndex: 'actions',
fixed: 'right',
width: 100
})
}
const userInfo = this.$store.getters.userInfo
if (!['Admin'].includes(userInfo.roletype) &&
@ -332,6 +340,16 @@ export default {
}
}
},
computed: {
isActionsOnIsoPermitted () {
return (['Admin'].includes(this.$store.getters.userInfo.roletype) || // If admin or owner or belongs to current project
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.account === this.$store.getters.userInfo.account) ||
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.projectid && this.$store.getters.project && this.$store.getters.project.id && this.resource.projectid === this.$store.getters.project.id)) &&
(this.resource.isready || !this.resource.status || this.resource.status.indexOf('Downloaded') === -1) && // Iso is ready or downloaded
this.resource.account !== 'system'
}
},
emits: ['update-zones'],
methods: {
initForm () {
this.formRef = ref()
@ -358,16 +376,10 @@ export default {
this.$notifyError(error)
}).finally(() => {
this.fetchLoading = false
this.updateImageZones()
})
this.fetchZoneData()
},
fetchZoneIcon (zoneid) {
const zoneItem = this.zones.filter(zone => zone.id === zoneid)
if (zoneItem?.[0]?.icon?.base64image) {
this.zoneIcon = zoneItem[0].icon.base64image
return true
}
return false
this.fetchOsCategoryId()
},
handleChangePage (page, pageSize) {
this.page = page
@ -379,13 +391,6 @@ export default {
this.pageSize = pageSize
this.fetchData()
},
isActionPermitted () {
return (['Admin'].includes(this.$store.getters.userInfo.roletype) || // If admin or owner or belongs to current project
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.account === this.$store.getters.userInfo.account) ||
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.projectid && this.$store.getters.project && this.$store.getters.project.id && this.resource.projectid === this.$store.getters.project.id)) &&
(this.resource.isready || !this.resource.status || this.resource.status.indexOf('Downloaded') === -1) && // Iso is ready or downloaded
this.resource.account !== 'system'
},
setSelection (selection) {
this.selectedRowKeys = selection
this.$emit('selection-change', this.selectedRowKeys)
@ -501,9 +506,41 @@ export default {
this.zoneLoading = true
api('listZones', { showicon: true }).then(json => {
const zones = json.listzonesresponse.zone || []
this.zones = [...zones.filter((zone) => this.currentRecord.zoneid !== zone.id)]
this.zones = zones
this.copyZones = [...zones.filter((zone) => this.currentRecord.zoneid !== zone.id)]
}).finally(() => {
this.zoneLoading = false
this.updateImageZones()
})
},
updateImageZones () {
if (!Array.isArray(this.dataSource) || !Array.isArray(this.zones) ||
this.dataSource.length === 0 || this.zones.length === 0) {
return
}
const imageZones = []
this.dataSource.forEach(item => {
const zone = this.zones.find(zone => item.zoneid === zone.id)
if (zone && zone.icon) {
item.zoneicon = zone.icon
}
imageZones.push(zone)
})
if (imageZones.length !== 0) {
this.$emit('update-zones', imageZones)
}
},
fetchOsCategoryId () {
const needed = this.$route.meta.name === 'iso' &&
'listOsTypes' in this.$store.getters.apis &&
this.resource && this.resource.ostypeid &&
(this.$config.imageSelectionInterface === undefined ||
this.$config.imageSelectionInterface === 'modern')
if (!needed) {
return
}
api('listOsTypes', { id: this.resource.ostypeid }).then(json => {
this.osCategoryId = json?.listostypesresponse?.ostype?.[0]?.oscategoryid || null
})
},
showCopyIso (record) {
@ -558,6 +595,19 @@ export default {
},
closeModal () {
this.showConfirmationAction = false
},
onAddInstance (record) {
const query = { isoid: this.resource.id, zoneid: record.zoneid }
if (this.resource.arch) {
query.arch = this.resource.arch
}
if (this.osCategoryId) {
query.oscategoryid = this.osCategoryId
}
this.$router.push({
path: '/action/deployVirtualMachine',
query: query
})
}
}
}

View File

@ -582,7 +582,7 @@ export default {
this.fetchCustomHypervisorName()
this.fetchZone()
this.fetchOsTypes()
this.fetchTemplateTypes()
this.templateTypes.opts = this.$fetchTemplateTypes()
this.architectureTypes.opts = this.$fetchCpuArchitectureTypes()
this.fetchUserData()
this.fetchUserdataPolicy()
@ -726,33 +726,6 @@ export default {
this.osTypes.loading = false
})
},
fetchTemplateTypes () {
this.templateTypes.opts = []
const templatetypes = []
templatetypes.push({
id: 'USER',
description: 'USER'
})
templatetypes.push({
id: 'VNF',
description: 'VNF'
})
if (this.isAdminRole) {
templatetypes.push({
id: 'SYSTEM',
description: 'SYSTEM'
})
templatetypes.push({
id: 'BUILTIN',
description: 'BUILTIN'
})
templatetypes.push({
id: 'ROUTING',
description: 'ROUTING'
})
}
this.templateTypes.opts = templatetypes
},
fetchUserData () {
const params = {}
params.listAll = true

View File

@ -38,8 +38,8 @@
:rowExpandable="(record) => record.downloaddetails.length > 0">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'zonename'">
<span v-if="fetchZoneIcon(record.zoneid)">
<resource-icon :image="zoneIcon" size="2x" style="margin-right: 5px"/>
<span v-if="record.zoneicon && record.zoneicon.base64image">
<resource-icon :image="record.zoneicon.base64image" size="2x" style="margin-right: 5px"/>
</span>
<global-outlined v-else style="margin-right: 5px" />
<span> {{ record.zonename }} </span>
@ -52,21 +52,32 @@
<span v-if="record.created">{{ $toLocaleDate(record.created) }}</span>
</template>
<template v-if="column.key === 'actions'">
<span style="margin-right: 5px" v-if="'deployVirtualMachine' in $store.getters.apis">
<tooltip-button
:disabled="!record.isready"
:title="$t('label.vm.add')"
icon="rocket-outlined"
@onClick="onAddInstance(record)"/>
</span>
<span v-if="isActionsOnTemplatePermitted">
<span style="margin-right: 5px">
<tooltip-button
style="margin-right: 5px"
:disabled="!('copyTemplate' in $store.getters.apis && record.isready)"
:title="$t('label.action.copy.template')"
icon="copy-outlined"
:loading="copyLoading"
@onClick="showCopyTemplate(record)" />
</span>
<span style="margin-right: 5px">
<tooltip-button
style="margin-right: 5px"
:disabled="!('deleteTemplate' in $store.getters.apis)"
:title="$t('label.action.delete.template')"
type="primary"
:danger="true"
icon="delete-outlined"
@onClick="onShowDeleteModal(record)"/>
</span>
</span>
</template>
</template>
<template #expandedRowRender="{ record }">
@ -168,7 +179,7 @@
}"
:loading="zoneLoading"
v-focus="true">
<a-select-option v-for="zone in zones" :key="zone.id" :label="zone.name">
<a-select-option v-for="zone in copyZones" :key="zone.id" :label="zone.name">
<div>
<span v-if="zone.icon && zone.icon.base64image">
<resource-icon :image="zone.icon.base64image" size="2x" style="margin-right: 5px"/>
@ -282,6 +293,7 @@ export default {
showCopyActionForm: false,
currentRecord: {},
zones: [],
copyZones: [],
zoneLoading: false,
copyLoading: false,
deleteLoading: false,
@ -298,7 +310,8 @@ export default {
confirmMessage: this.$t('label.confirm.delete.templates')
},
modalWidth: '30vw',
showTable: false
showTable: false,
osCategoryId: null
}
},
beforeCreate () {
@ -324,6 +337,12 @@ export default {
key: 'isready',
title: this.$t('label.isready'),
dataIndex: 'isready'
},
{
key: 'actions',
title: '',
dataIndex: 'actions',
width: 130
}
]
this.imageStoreInnerColumns = [
@ -354,14 +373,6 @@ export default {
dataIndex: 'downloadState'
}
]
if (this.isActionPermitted()) {
this.columns.push({
key: 'actions',
title: '',
dataIndex: 'actions',
width: 100
})
}
const userInfo = this.$store.getters.userInfo
if (!['Admin'].includes(userInfo.roletype) &&
@ -378,6 +389,16 @@ export default {
}
}
},
computed: {
isActionsOnTemplatePermitted () {
return (['Admin'].includes(this.$store.getters.userInfo.roletype) || // If admin or owner or belongs to current project
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.account === this.$store.getters.userInfo.account) ||
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.projectid && this.$store.getters.project && this.$store.getters.project.id && this.resource.projectid === this.$store.getters.project.id)) &&
(this.resource.isready || !this.resource.status || this.resource.status.indexOf('Downloaded') === -1) && // Template is ready or downloaded
this.resource.templatetype !== 'SYSTEM'
}
},
emits: ['update-zones'],
methods: {
initForm () {
this.formRef = ref()
@ -404,16 +425,10 @@ export default {
this.$notifyError(error)
}).finally(() => {
this.fetchLoading = false
this.updateImageZones()
})
this.fetchZoneData()
},
fetchZoneIcon (zoneid) {
const zoneItem = this.zones.filter(zone => zone.id === zoneid)
if (zoneItem?.[0]?.icon?.base64image) {
this.zoneIcon = zoneItem[0].icon.base64image
return true
}
return false
this.fetchOsCategoryId()
},
handleChangePage (page, pageSize) {
this.page = page
@ -425,13 +440,6 @@ export default {
this.pageSize = pageSize
this.fetchData()
},
isActionPermitted () {
return (['Admin'].includes(this.$store.getters.userInfo.roletype) || // If admin or owner or belongs to current project
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.account === this.$store.getters.userInfo.account) ||
(this.resource.domainid === this.$store.getters.userInfo.domainid && this.resource.projectid && this.$store.getters.project && this.$store.getters.project.id && this.resource.projectid === this.$store.getters.project.id)) &&
(this.resource.isready || !this.resource.status || this.resource.status.indexOf('Downloaded') === -1) && // Template is ready or downloaded
this.resource.templatetype !== 'SYSTEM'
},
setSelection (selection) {
this.selectedRowKeys = selection
if (selection?.length > 0) {
@ -566,9 +574,41 @@ export default {
this.zoneLoading = true
api('listZones', { showicon: true }).then(json => {
const zones = json.listzonesresponse.zone || []
this.zones = [...zones.filter((zone) => this.currentRecord.zoneid !== zone.id)]
this.zones = zones
this.copyZones = [...zones.filter((zone) => this.currentRecord.zoneid !== zone.id)]
}).finally(() => {
this.zoneLoading = false
this.updateImageZones()
})
},
updateImageZones () {
if (!Array.isArray(this.dataSource) || !Array.isArray(this.zones) ||
this.dataSource.length === 0 || this.zones.length === 0) {
return
}
const imageZones = []
this.dataSource.forEach(item => {
const zone = this.zones.find(zone => item.zoneid === zone.id)
if (zone && zone.icon) {
item.zoneicon = zone.icon
}
imageZones.push(zone)
})
if (imageZones.length !== 0) {
this.$emit('update-zones', imageZones)
}
},
fetchOsCategoryId () {
const needed = this.$route.meta.name === 'template' &&
'listOsTypes' in this.$store.getters.apis &&
this.resource && this.resource.ostypeid &&
(this.$config.imageSelectionInterface === undefined ||
this.$config.imageSelectionInterface === 'modern')
if (!needed) {
return
}
api('listOsTypes', { id: this.resource.ostypeid }).then(json => {
this.osCategoryId = json?.listostypesresponse?.ostype?.[0]?.oscategoryid || null
})
},
showCopyTemplate (record) {
@ -634,6 +674,19 @@ export default {
}).catch(error => {
this.formRef.value.scrollToField(error.errorFields[0].name)
})
},
onAddInstance (record) {
const query = { templateid: this.resource.id, zoneid: record.zoneid }
if (this.resource.arch) {
query.arch = this.resource.arch
}
if (this.osCategoryId) {
query.oscategoryid = this.osCategoryId
}
this.$router.push({
path: '/action/deployVirtualMachine',
query: query
})
}
}
}

View File

@ -20,6 +20,7 @@
package com.cloud.utils.db;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
@ -46,6 +47,8 @@ public interface EntityManager {
*/
public <T> T findByUuid(Class<T> entityType, String uuid);
<T> List<T> listByUuids(Class<T> entityType, Collection<String> uuids);
/**
* Finds a unique entity by uuid string, including those removed entries
* @param <T> entity class