diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 4d4ead277e5d..3eb26b6010d9 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -996,6 +996,7 @@ public class ApiConstants { public static final String VPC_OFF_NAME = "vpcofferingname"; public static final String VPC_OFFERING_CONSERVE_MODE = "vpcofferingconservemode"; public static final String NETWORK = "network"; + public static final String VPC_ACCESS = "vpcaccess"; public static final String VPC_ID = "vpcid"; public static final String VPC_NAME = "vpcname"; public static final String VPC_GATEWAY_ID = "vpcgatewayid"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/network/ListNetworksCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ListNetworksCmd.java index 79f2cf8c7449..aab11543f21d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/user/network/ListNetworksCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/network/ListNetworksCmd.java @@ -240,7 +240,7 @@ public void execute() { private void updateNetworkResponse(List response) { for (NetworkResponse networkResponse : response) { ResourceIcon resourceIcon = resourceIconManager.getByResourceTypeAndUuid(ResourceTag.ResourceObjectType.Network, networkResponse.getId()); - if (resourceIcon == null && networkResponse.getVpcId() != null) { + if (resourceIcon == null && networkResponse.getVpcId() != null && networkResponse.getVpcAccess()) { resourceIcon = resourceIconManager.getByResourceTypeAndUuid(ResourceTag.ResourceObjectType.Vpc, networkResponse.getVpcId()); } if (resourceIcon == null) { diff --git a/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java index cdc3115c8293..e8197a8baea3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/IPAddressResponse.java @@ -143,6 +143,10 @@ public class IPAddressResponse extends BaseResponseWithAnnotations implements Co @Param(description = "Purpose of the IP address. In Acton this value is not null for IPs with isSystem=true, and can have either StaticNat or LB value") private String purpose; + @SerializedName(ApiConstants.VPC_ACCESS) + @Param(description = "Whether the calling account has access to this network's VPC", since = "4.21.0") + private boolean vpcAccess; + @SerializedName(ApiConstants.VPC_ID) @Param(description = "VPC ID the IP belongs to") private String vpcId; @@ -301,6 +305,10 @@ public void setPurpose(String purpose) { this.purpose = purpose; } + public void setVpcAccess(boolean vpcAccess) { + this.vpcAccess = vpcAccess; + } + public void setVpcId(String vpcId) { this.vpcId = vpcId; } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java index 3a3663af2551..40323c1217f4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/NetworkResponse.java @@ -203,6 +203,10 @@ public class NetworkResponse extends BaseResponseWithAssociatedNetwork implement @Param(description = "True if Network supports specifying IP ranges, false otherwise") private Boolean specifyIpRanges; + @SerializedName(ApiConstants.VPC_ACCESS) + @Param(description = "Whether the calling account has access to this network's VPC", since = "4.21.0") + private Boolean vpcAccess; + @SerializedName(ApiConstants.VPC_ID) @Param(description = "VPC the Network belongs to") private String vpcId; @@ -523,6 +527,14 @@ public void setSpecifyIpRanges(Boolean specifyIpRanges) { this.specifyIpRanges = specifyIpRanges; } + public void setVpcAccess(boolean vpcAccess) { + this.vpcAccess = vpcAccess; + } + + public Boolean getVpcAccess() { + return vpcAccess; + } + public void setVpcId(String vpcId) { this.vpcId = vpcId; } diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java index a8551b4c6693..9ce3674fcb9e 100644 --- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java +++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java @@ -1167,7 +1167,7 @@ public IPAddressResponse createIPAddressResponse(ResponseView view, IpAddress ip } - setVpcIdInResponse(ipAddr.getVpcId(), ipResponse::setVpcId, ipResponse::setVpcName); + setVpcIdInResponse(ipAddr.getVpcId(), ipResponse::setVpcId, ipResponse::setVpcName, ipResponse::setVpcAccess); // Network id the ip is associated with (if associated networkId is @@ -1242,20 +1242,43 @@ public IPAddressResponse createIPAddressResponse(ResponseView view, IpAddress ip return ipResponse; } + protected void setVpcIdInResponse(Long vpcId, Consumer vpcUuidSetter, Consumer vpcNameSetter, Consumer vpcAccessSetter) { + if (vpcId == null) { + return; + } + Vpc vpc = ApiDBUtils.findVpcById(vpcId); + if (vpc == null) { + return; + } - private void setVpcIdInResponse(Long vpcId, Consumer vpcUuidSetter, Consumer vpcNameSetter) { - if (vpcId != null) { - Vpc vpc = ApiDBUtils.findVpcById(vpcId); - if (vpc != null) { - try { - _accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, false, vpc); - vpcUuidSetter.accept(vpc.getUuid()); - } catch (PermissionDeniedException e) { - logger.debug("Not setting the vpcId to the response because the caller does not have access to the VPC"); - } - vpcNameSetter.accept(vpc.getName()); - } + try { + _accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, false, vpc); + vpcAccessSetter.accept(true); + } catch (PermissionDeniedException e) { + vpcAccessSetter.accept(false); + logger.debug("Setting [{}] as false because the caller does not have access to the VPC [{}].", ApiConstants.VPC_ACCESS, vpc); + } + vpcNameSetter.accept(vpc.getName()); + vpcUuidSetter.accept(vpc.getUuid()); + } + + protected void setAclIdInResponse(Network network, NetworkResponse response) { + if (network.getNetworkACLId() == null) { + return; + } + + NetworkACL acl = ApiDBUtils.findByNetworkACLId(network.getNetworkACLId()); + if (acl == null) { + return; } + + if (Boolean.FALSE.equals(response.getVpcAccess()) && acl.getVpcId() != 0) { + logger.debug("[{}] not set in response, since caller does not have access to it.", acl); + return; + } + + response.setAclId(acl.getUuid()); + response.setAclName(acl.getName()); } private void showVmInfoForSharedNetworks(boolean forVirtualNetworks, IpAddress ipAddr, IPAddressResponse ipResponse) { @@ -2799,7 +2822,8 @@ public NetworkResponse createNetworkResponse(ResponseView view, Network network) response.setSpecifyIpRanges(network.getSpecifyIpRanges()); - setVpcIdInResponse(network.getVpcId(), response::setVpcId, response::setVpcName); + setVpcIdInResponse(network.getVpcId(), response::setVpcId, response::setVpcName, response::setVpcAccess); + setAclIdInResponse(network, response); setResponseAssociatedNetworkInformation(response, network.getId()); @@ -2816,14 +2840,6 @@ public NetworkResponse createNetworkResponse(ResponseView view, Network network) response.setHasAnnotation(annotationDao.hasAnnotations(network.getUuid(), AnnotationService.EntityType.NETWORK.name(), _accountMgr.isRootAdmin(CallContext.current().getCallingAccount().getId()))); - if (network.getNetworkACLId() != null) { - NetworkACL acl = ApiDBUtils.findByNetworkACLId(network.getNetworkACLId()); - if (acl != null) { - response.setAclId(acl.getUuid()); - response.setAclName(acl.getName()); - } - } - response.setStrechedL2Subnet(network.isStrechedL2Network()); if (network.isStrechedL2Network()) { Set networkSpannedZones = new HashSet(); diff --git a/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java b/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java index c0c019f6dbd8..7a1f667a0141 100644 --- a/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java +++ b/server/src/test/java/com/cloud/api/ApiResponseHelperTest.java @@ -29,6 +29,47 @@ import java.util.TimeZone; import java.util.UUID; +import com.cloud.capacity.Capacity; +import com.cloud.configuration.Resource; +import com.cloud.domain.DomainVO; +import com.cloud.exception.PermissionDeniedException; +import com.cloud.network.Network; +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.network.vpc.NetworkACL; +import com.cloud.network.vpc.VpcVO; +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 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.IpQuarantineResponse; +import org.apache.cloudstack.api.response.NetworkResponse; +import org.apache.cloudstack.api.response.NicSecondaryIpResponse; +import org.apache.cloudstack.api.response.UnmanagedInstanceResponse; +import org.apache.cloudstack.api.response.UsageRecordResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.usage.UsageService; +import org.apache.cloudstack.vm.UnmanagedInstanceTO; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -42,40 +83,16 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; -import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.api.ResponseObject; -import org.apache.cloudstack.api.response.AutoScaleVmGroupResponse; -import org.apache.cloudstack.api.response.AutoScaleVmProfileResponse; import org.apache.cloudstack.api.response.ConsoleSessionResponse; -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.api.response.TrafficTypeResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.usage.UsageService; -import org.apache.cloudstack.vm.UnmanagedInstanceTO; -import com.cloud.capacity.Capacity; -import com.cloud.configuration.Resource; -import com.cloud.domain.DomainVO; import com.cloud.host.HostVO; import com.cloud.network.Networks; import com.cloud.network.PhysicalNetworkTrafficType; -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.network.dao.PhysicalNetworkVO; import com.cloud.network.dao.PhysicalNetworkTrafficTypeVO; import com.cloud.resource.icon.ResourceIconVO; @@ -83,19 +100,7 @@ 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.ConsoleSessionVO; -import com.cloud.vm.NicSecondaryIp; import com.cloud.vm.VMInstanceVO; import static org.junit.Assert.assertEquals; @@ -150,6 +155,12 @@ public class ApiResponseHelperTest { @Mock private VMInstanceVO vmInstanceVOMock; + @Mock + private VpcVO vpcVOMock; + + @Mock + private NetworkACL networkACLMock; + @Spy @InjectMocks ApiResponseHelper apiResponseHelper = new ApiResponseHelper(); @@ -168,6 +179,9 @@ public class ApiResponseHelperTest { static long autoScaleUserId = 7L; + static final String A_NAME = "name"; + static final String A_UUID = "021f94d4-73f9-4a9a-b003-1df9dd968a09"; + @Before public void injectMocks() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { @@ -800,4 +814,135 @@ public void createConsoleSessionResponseTestShouldReturnFullResponse() { Assert.assertEquals(expected.getVmName(), response.getVmName()); } } + + @Test + public void setVpcIdInResponseTestNullVpcIdReturnNull() { + NetworkResponse networkResponse = new NetworkResponse(); + + apiResponseHelper.setVpcIdInResponse(null, networkResponse::setVpcId, networkResponse::setVpcName, networkResponse::setVpcAccess); + Assert.assertNull(networkResponse.getVpcId()); + Assert.assertNull(networkResponse.getVpcName()); + Assert.assertNull(networkResponse.getVpcAccess()); + } + + @Test + public void setVpcIdInResponseTestNullVpcReturnNull() { + NetworkResponse networkResponse = new NetworkResponse(); + + try (MockedStatic utils = Mockito.mockStatic(ApiDBUtils.class)) { + utils.when(() -> ApiDBUtils.findVpcById(1L)).thenReturn(null); + apiResponseHelper.setVpcIdInResponse(1L, networkResponse::setVpcId, networkResponse::setVpcName, networkResponse::setVpcAccess); + } + Assert.assertNull(networkResponse.getVpcId()); + Assert.assertNull(networkResponse.getVpcName()); + Assert.assertNull(networkResponse.getVpcAccess()); + } + + @Test + public void setVpcIdInResponseCallerHasAccessReturnVpcAccessTrueAndVpcIdAndVpcName() { + NetworkResponse networkResponse = new NetworkResponse(); + Mockito.when(vpcVOMock.getName()).thenReturn(A_NAME); + Mockito.when(vpcVOMock.getUuid()).thenReturn(A_UUID); + + try (MockedStatic utils = Mockito.mockStatic(ApiDBUtils.class)) { + utils.when(() -> ApiDBUtils.findVpcById(1L)).thenReturn(vpcVOMock); + apiResponseHelper.setVpcIdInResponse(1L, networkResponse::setVpcId, networkResponse::setVpcName, networkResponse::setVpcAccess); + }; + Assert.assertEquals(A_UUID, networkResponse.getVpcId()); + Assert.assertEquals(A_NAME, networkResponse.getVpcName()); + Assert.assertTrue(networkResponse.getVpcAccess()); + } + + @Test + public void setVpcIdInResponseCallerDoesNotHaveAccessReturnVpcAccessFalseAndVpcIdAndVpcName() { + NetworkResponse networkResponse = new NetworkResponse(); + Mockito.when(vpcVOMock.getName()).thenReturn(A_NAME); + Mockito.when(vpcVOMock.getUuid()).thenReturn(A_UUID); + + try (MockedStatic utils = Mockito.mockStatic(ApiDBUtils.class)) { + utils.when(() -> ApiDBUtils.findVpcById(1L)).thenReturn(vpcVOMock); + Mockito.doThrow(PermissionDeniedException.class).when(accountManagerMock).checkAccess(Mockito.any(), Mockito.any(), Mockito.anyBoolean(), Mockito.any()); + apiResponseHelper.setVpcIdInResponse(1L, networkResponse::setVpcId, networkResponse::setVpcName, networkResponse::setVpcAccess); + }; + Assert.assertEquals(A_UUID, networkResponse.getVpcId()); + Assert.assertEquals(A_NAME, networkResponse.getVpcName()); + Assert.assertFalse(networkResponse.getVpcAccess()); + } + + @Test + public void setAclIdInResponseTestNullNetworkAclIdReturnNull() { + NetworkResponse networkResponse = new NetworkResponse(); + Network networkMock = Mockito.mock(Network.class); + Mockito.when(networkMock.getNetworkACLId()).thenReturn(null); + + apiResponseHelper.setAclIdInResponse(networkMock, networkResponse); + Assert.assertNull(networkResponse.getAclId()); + Assert.assertNull(networkResponse.getAclName()); + } + + @Test + public void setAclIdInResponseTestNullAclReturnNull() { + NetworkResponse networkResponse = new NetworkResponse(); + Network networkMock = Mockito.mock(Network.class); + Mockito.when(networkMock.getNetworkACLId()).thenReturn(1L); + + try (MockedStatic utils = Mockito.mockStatic(ApiDBUtils.class)) { + utils.when(() -> ApiDBUtils.findByNetworkACLId(1L)).thenReturn(null); + apiResponseHelper.setAclIdInResponse(networkMock, networkResponse); + } + Assert.assertNull(networkResponse.getAclId()); + Assert.assertNull(networkResponse.getAclName()); + } + + @Test + public void setAclIdInResponseTestCallerDoesNotHaveAccessReturnNull() { + NetworkResponse networkResponse = new NetworkResponse(); + networkResponse.setVpcAccess(false); + Network networkMock = Mockito.mock(Network.class); + Mockito.when(networkMock.getNetworkACLId()).thenReturn(1L); + Mockito.when(networkACLMock.getVpcId()).thenReturn(2L); + + try (MockedStatic utils = Mockito.mockStatic(ApiDBUtils.class)) { + utils.when(() -> ApiDBUtils.findByNetworkACLId(1L)).thenReturn(networkACLMock); + apiResponseHelper.setAclIdInResponse(networkMock, networkResponse); + } + Assert.assertNull(networkResponse.getAclId()); + Assert.assertNull(networkResponse.getAclName()); + } + + @Test + public void setAclIdInResponseTestCallerDoesNotHaveAccessButAclIsGlobalReturnAclIdAndAclName() { + NetworkResponse networkResponse = new NetworkResponse(); + networkResponse.setVpcAccess(false); + Network networkMock = Mockito.mock(Network.class); + Mockito.when(networkMock.getNetworkACLId()).thenReturn(1L); + Mockito.when(networkACLMock.getVpcId()).thenReturn(0L); + Mockito.when(networkACLMock.getName()).thenReturn(A_NAME); + Mockito.when(networkACLMock.getUuid()).thenReturn(A_UUID); + + try (MockedStatic utils = Mockito.mockStatic(ApiDBUtils.class)) { + utils.when(() -> ApiDBUtils.findByNetworkACLId(1L)).thenReturn(networkACLMock); + apiResponseHelper.setAclIdInResponse(networkMock, networkResponse); + } + Assert.assertEquals(A_UUID, networkResponse.getAclId()); + Assert.assertEquals(A_NAME, networkResponse.getAclName()); + } + + @Test + public void setAclIdInResponseTestCallerHasAccessReturnAclIdAndAclName() { + NetworkResponse networkResponse = new NetworkResponse(); + networkResponse.setVpcAccess(true); + Network networkMock = Mockito.mock(Network.class); + Mockito.when(networkMock.getNetworkACLId()).thenReturn(1L); + Mockito.lenient().when(networkACLMock.getVpcId()).thenReturn(2L); + Mockito.when(networkACLMock.getName()).thenReturn(A_NAME); + Mockito.when(networkACLMock.getUuid()).thenReturn(A_UUID); + + try (MockedStatic utils = Mockito.mockStatic(ApiDBUtils.class)) { + utils.when(() -> ApiDBUtils.findByNetworkACLId(1L)).thenReturn(networkACLMock); + apiResponseHelper.setAclIdInResponse(networkMock, networkResponse); + } + Assert.assertEquals(A_UUID, networkResponse.getAclId()); + Assert.assertEquals(A_NAME, networkResponse.getAclName()); + } } diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue index 996e30ead3b5..e67e2a41fc6c 100644 --- a/ui/src/components/view/InfoCard.vue +++ b/ui/src/components/view/InfoCard.vue @@ -504,7 +504,129 @@ {{ resource.autoscalevmgroupname || resource.autoscalevmgroupid }} + + +
+
{{ $t('label.resource') }}
+
+ +
+
+
+
{{ $t('label.vmname') }}
+
+ + {{ resource.vmname || resource.vm || resource.virtualmachinename || resource.virtualmachineid }} + +
+
+
+
{{ $t('label.volume') }}
+
+ + {{ resource.volumename || resource.volume || resource.volumeid }} + {{ resource.volumename || resource.volume || resource.volumeid }} +
+
+
+
{{ $t('label.associatednetwork') }}
+
+ + {{ resource.associatednetworkname || resource.associatednetwork || resource.associatednetworkid }} +
+
+
+
{{ $t('label.network') }}
+
+ + {{ resource.sourceipaddressnetworkname || resource.sourceipaddressnetworkid }} +
+
+
+
{{ $t('label.guestnetwork') }}
+
+ + {{ resource.guestnetworkname || resource.guestnetworkid }} +
+
+
+
{{ $t('label.public.ip') }}
+
+ + {{ resource.publicip }} + + +
+
+
+
{{ $t('label.vpcname') }}
+
+ + + + + {{ resource.vpcname || resource.vpcid }} + {{ resource.vpcname || resource.vpcid }} +
+
+ +
+
{{ $t('label.aclid') }}
+
+ + + + + {{ resource.aclname || resource.aclid }} +
+
+ +
+
{{ $t('label.affinitygroup') }}
+ + + {{ group.name }} + , + +
+
+
{{ resource.templateformat === 'ISO'? $t('label.iso') : $t('label.templatename') }}
+
+ + + {{ resource.templatedisplaytext || resource.templatename || resource.templateid }} +
+
+
+
{{ $t('label.isoname') }}
+
+ + + {{ resource.isodisplaytext || resource.isoname || resource.isoid }} +
+
+
+
{{ $t('label.system.offering') }}
+
{{ $t('label.serviceofferingname') }}
+
+ + {{ resource.serviceofferingname || resource.serviceofferingid }} + {{ resource.serviceofferingname || resource.serviceofferingid }} + {{ resource.serviceofferingname || resource.serviceofferingid }} +
+
+
+
{{ $t('label.diskoffering') }}
+
+ +
+ {{ resource.rootdiskofferingdisplaytext }} + {{ resource.rootdiskofferingdisplaytext }}
+
{{ $t('label.keypairs') }}
diff --git a/ui/src/components/view/ListView.vue b/ui/src/components/view/ListView.vue index b03293efacae..8e46c1002fff 100644 --- a/ui/src/components/view/ListView.vue +++ b/ui/src/components/view/ListView.vue @@ -582,7 +582,7 @@ {{ text }}