You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by da...@apache.org on 2023/01/31 08:36:53 UTC

[cloudstack] branch main updated: infra: edge zones (#6840)

This is an automated email from the ASF dual-hosted git repository.

dahn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/main by this push:
     new 3b6ce970971 infra: edge zones (#6840)
3b6ce970971 is described below

commit 3b6ce9709718f6b261a9d9f601a1917142256abb
Author: Abhishek Kumar <ab...@gmail.com>
AuthorDate: Tue Jan 31 14:06:45 2023 +0530

    infra: edge zones (#6840)
    
    Signed-off-by: Abhishek Kumar <ab...@gmail.com>
    Co-authored-by: dahn <da...@onecht.net>
---
 api/src/main/java/com/cloud/dc/DataCenter.java     |   6 +
 .../org/apache/cloudstack/api/ApiConstants.java    |   1 +
 .../api/command/admin/pod/CreatePodCmd.java        |   6 +-
 .../api/command/admin/zone/CreateZoneCmd.java      |  11 +
 .../cloudstack/api/response/ZoneResponse.java      |  12 +
 .../api/command/admin/zone/CreateZoneCmdTest.java  |  35 +++
 .../cloud/configuration/ConfigurationManager.java  |   6 +-
 .../entity/api/db/EngineDataCenterVO.java          |  10 +
 .../src/main/java/com/cloud/dc/DataCenterVO.java   |  13 +
 .../main/java/com/cloud/dc/dao/DataCenterDao.java  |   2 +
 .../java/com/cloud/dc/dao/DataCenterDaoImpl.java   |  21 ++
 .../resources/META-INF/db/schema-41720to41800.sql  |  48 +++-
 .../cloud/agent/manager/MockAgentManagerImpl.java  |  12 +-
 .../contrail/management/ManagementServerMock.java  |   2 +-
 .../main/java/com/cloud/api/ApiResponseHelper.java |  10 +-
 .../java/com/cloud/api/query/QueryManagerImpl.java |   9 +
 .../cloud/api/query/dao/DataCenterJoinDaoImpl.java |   2 +
 .../com/cloud/api/query/dao/UserVmJoinDaoImpl.java |   6 +-
 .../com/cloud/api/query/vo/DataCenterJoinVO.java   |   9 +
 .../configuration/ConfigurationManagerImpl.java    | 144 +++++-----
 .../consoleproxy/ConsoleProxyManagerImpl.java      |  22 +-
 .../router/VirtualNetworkApplianceManagerImpl.java |   5 +
 .../storage/snapshot/SnapshotManagerImpl.java      |  17 +-
 .../consoleproxy/ConsoleAccessManagerImpl.java     |  50 ++--
 .../configuration/ConfigurationManagerTest.java    |  99 ++++++-
 .../consoleproxy/ConsoleProxyManagerTest.java      |  23 ++
 .../storage/snapshot/SnapshotManagerTest.java      |  62 ++++-
 .../cloud/vpc/MockConfigurationManagerImpl.java    |   4 +-
 .../SecondaryStorageManagerImpl.java               |  20 +-
 .../SecondaryStorageManagerTest.java               |  42 ++-
 .../component/test_edgezone_supportedoperations.py | 133 ++++++++++
 .../component/test_interpod_migration.py           |   2 +-
 ui/public/locales/en.json                          |  15 ++
 ui/src/components/view/InfoCard.vue                |   2 +-
 ui/src/config/section/infra/zones.js               |  22 +-
 ui/src/views/compute/CreateKubernetesCluster.vue   |   1 +
 .../views/image/AddKubernetesSupportedVersion.vue  |   1 +
 ui/src/views/image/RegisterOrUploadIso.vue         |   1 +
 ui/src/views/image/RegisterOrUploadTemplate.vue    |  20 +-
 ui/src/views/infra/zone/ZoneWizard.vue             |  41 ++-
 ui/src/views/infra/zone/ZoneWizardAddResources.vue |  45 ++--
 ...TypeStep.vue => ZoneWizardCoreZoneTypeStep.vue} |  13 +
 ui/src/views/infra/zone/ZoneWizardLaunchZone.vue   |  21 +-
 .../infra/zone/ZoneWizardNetworkSetupStep.vue      |  19 +-
 .../zone/ZoneWizardPhysicalNetworkSetupStep.vue    |  13 +-
 .../views/infra/zone/ZoneWizardZoneDetailsStep.vue | 294 ++++++++++++---------
 ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue |  63 ++---
 ui/src/views/network/CreateIsolatedNetworkForm.vue |   2 +-
 ui/src/views/storage/UploadLocalVolume.vue         |   1 +
 ui/src/views/storage/UploadVolume.vue              |   5 +-
 50 files changed, 1046 insertions(+), 377 deletions(-)

diff --git a/api/src/main/java/com/cloud/dc/DataCenter.java b/api/src/main/java/com/cloud/dc/DataCenter.java
index 4a8f6d9d0a0..d6564158b88 100644
--- a/api/src/main/java/com/cloud/dc/DataCenter.java
+++ b/api/src/main/java/com/cloud/dc/DataCenter.java
@@ -32,6 +32,10 @@ public interface DataCenter extends InfrastructureEntity, Grouping, Partition {
         Basic, Advanced,
     }
 
+    public enum Type {
+        Core, Edge,
+    }
+
     String getDns1();
 
     String getDns2();
@@ -83,4 +87,6 @@ public interface DataCenter extends InfrastructureEntity, Grouping, Partition {
     boolean isLocalStorageEnabled();
 
     int getSortKey();
+
+    Type getType();
 }
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 653b38e6ea3..9548ddfc6b4 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -243,6 +243,7 @@ public class ApiConstants {
     public static final String IP_TOTAL = "iptotal";
     public static final String IS_CLEANUP_REQUIRED = "iscleanuprequired";
     public static final String IS_DYNAMIC = "isdynamic";
+    public static final String IS_EDGE = "isedge";
     public static final String IS_EXTRACTABLE = "isextractable";
     public static final String IS_FEATURED = "isfeatured";
     public static final String IS_PORTABLE = "isportable";
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/CreatePodCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/CreatePodCmd.java
index 33fe6c759b2..b15854ca875 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/CreatePodCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/pod/CreatePodCmd.java
@@ -50,16 +50,16 @@ public class CreatePodCmd extends BaseCmd {
                description = "the Zone ID in which the Pod will be created")
     private Long zoneId;
 
-    @Parameter(name = ApiConstants.START_IP, type = CommandType.STRING, required = true, description = "the starting IP address for the Pod")
+    @Parameter(name = ApiConstants.START_IP, type = CommandType.STRING, description = "the starting IP address for the Pod")
     private String startIp;
 
     @Parameter(name = ApiConstants.END_IP, type = CommandType.STRING, description = "the ending IP address for the Pod")
     private String endIp;
 
-    @Parameter(name = ApiConstants.NETMASK, type = CommandType.STRING, required = true, description = "the netmask for the Pod")
+    @Parameter(name = ApiConstants.NETMASK, type = CommandType.STRING, description = "the netmask for the Pod")
     private String netmask;
 
-    @Parameter(name = ApiConstants.GATEWAY, type = CommandType.STRING, required = true, description = "the gateway for the Pod")
+    @Parameter(name = ApiConstants.GATEWAY, type = CommandType.STRING, description = "the gateway for the Pod")
     private String gateway;
 
     @Parameter(name = ApiConstants.ALLOCATION_STATE, type = CommandType.STRING, description = "Allocation state of this Pod for allocation of new resources")
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/CreateZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/CreateZoneCmd.java
index 263d3c824b0..aca3e00d095 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/CreateZoneCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/zone/CreateZoneCmd.java
@@ -87,6 +87,10 @@ public class CreateZoneCmd extends BaseCmd {
     @Parameter(name = ApiConstants.LOCAL_STORAGE_ENABLED, type = CommandType.BOOLEAN, description = "true if local storage offering enabled, false otherwise")
     private Boolean localStorageEnabled;
 
+    @Parameter(name = ApiConstants.IS_EDGE, type = CommandType.BOOLEAN, description = "true if the zone is an edge zone, false otherwise", since = "4.18.0")
+    private Boolean isEdge;
+
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -153,6 +157,13 @@ public class CreateZoneCmd extends BaseCmd {
         return localStorageEnabled;
     }
 
+    public boolean isEdge() {
+        if (isEdge == null) {
+            return false;
+        }
+        return isEdge;
+    }
+
     /////////////////////////////////////////////////////
     /////////////// API Implementation///////////////////
     @Override
diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java
index f4728f946f6..b8824fd66f8 100644
--- a/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java
+++ b/api/src/main/java/org/apache/cloudstack/api/response/ZoneResponse.java
@@ -141,6 +141,10 @@ public class ZoneResponse extends BaseResponseWithAnnotations implements SetReso
     @Param(description = "The maximum value the MTU can have on the VR's public interfaces", since = "4.18.0")
     private Integer routerPublicInterfaceMaxMtu;
 
+    @SerializedName(ApiConstants.TYPE)
+    @Param(description = "the type of the zone - core or edge", since = "4.18.0")
+    String type;
+
     public ZoneResponse() {
         tags = new LinkedHashSet<ResourceTagResponse>();
     }
@@ -352,4 +356,12 @@ public class ZoneResponse extends BaseResponseWithAnnotations implements SetReso
     public void setRouterPublicInterfaceMaxMtu(Integer routerPublicInterfaceMaxMtu) {
         this.routerPublicInterfaceMaxMtu = routerPublicInterfaceMaxMtu;
     }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getType() {
+        return type;
+    }
 }
diff --git a/api/src/test/java/org/apache/cloudstack/api/command/admin/zone/CreateZoneCmdTest.java b/api/src/test/java/org/apache/cloudstack/api/command/admin/zone/CreateZoneCmdTest.java
new file mode 100644
index 00000000000..1e6e7103fbf
--- /dev/null
+++ b/api/src/test/java/org/apache/cloudstack/api/command/admin/zone/CreateZoneCmdTest.java
@@ -0,0 +1,35 @@
+// 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.zone;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+public class CreateZoneCmdTest {
+
+    @Test
+    public void isEdge() {
+        CreateZoneCmd createZoneCmd = new CreateZoneCmd();
+        ReflectionTestUtils.setField(createZoneCmd, "isEdge", null);
+        Assert.assertFalse("Null or no isedge param value for API should return false", createZoneCmd.isEdge());
+        ReflectionTestUtils.setField(createZoneCmd, "isEdge", false);
+        Assert.assertFalse("false value for isedge param value for API should return false", createZoneCmd.isEdge());
+        ReflectionTestUtils.setField(createZoneCmd, "isEdge", true);
+        Assert.assertTrue("true value for isedge param value for API should return true", createZoneCmd.isEdge());
+    }
+}
\ No newline at end of file
diff --git a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java
index 0442cac86c8..cff225541d6 100644
--- a/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java
+++ b/engine/components-api/src/main/java/com/cloud/configuration/ConfigurationManager.java
@@ -127,7 +127,7 @@ public interface ConfigurationManager {
      *
      * @param userId
      * @param podName
-     * @param zoneId
+     * @param zone
      * @param gateway
      * @param cidr
      * @param startIp
@@ -137,7 +137,7 @@ public interface ConfigurationManager {
      *            (true if it is ok to not validate that gateway IP address overlap with Start/End IP of the POD)
      * @return Pod
      */
-    HostPodVO createPod(long userId, String podName, long zoneId, String gateway, String cidr, String startIp, String endIp, String allocationState,
+    HostPodVO createPod(long userId, String podName, DataCenter zone, String gateway, String cidr, String startIp, String endIp, String allocationState,
         boolean skipGatewayOverlapCheck);
 
     /**
@@ -164,7 +164,7 @@ public interface ConfigurationManager {
      */
     DataCenterVO createZone(long userId, String zoneName, String dns1, String dns2, String internalDns1, String internalDns2, String guestCidr, String domain,
         Long domainId, NetworkType zoneType, String allocationState, String networkDomain, boolean isSecurityGroupEnabled, boolean isLocalStorageEnabled, String ip6Dns1,
-        String ip6Dns2);
+        String ip6Dns2, boolean isEdge);
 
     /**
      * Deletes a VLAN from the database, along with all of its IP addresses. Will not delete VLANs that have allocated
diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/datacenter/entity/api/db/EngineDataCenterVO.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/datacenter/entity/api/db/EngineDataCenterVO.java
index 26e982013c2..57382530f40 100644
--- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/datacenter/entity/api/db/EngineDataCenterVO.java
+++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/datacenter/entity/api/db/EngineDataCenterVO.java
@@ -37,6 +37,7 @@ import org.apache.cloudstack.api.Identity;
 import org.apache.cloudstack.engine.datacenter.entity.api.DataCenterResourceEntity.State;
 import org.apache.cloudstack.engine.datacenter.entity.api.DataCenterResourceEntity.State.Event;
 
+import com.cloud.dc.DataCenter;
 import com.cloud.network.Network.Provider;
 import com.cloud.org.Grouping;
 import com.cloud.utils.NumbersUtil;
@@ -156,6 +157,10 @@ public class EngineDataCenterVO implements EngineDataCenter, Identity {
     @Temporal(value = TemporalType.TIMESTAMP)
     protected Date lastUpdated;
 
+    @Column(name = "type")
+    @Enumerated(value = EnumType.STRING)
+    private DataCenter.Type type;
+
     /**
      * Note that state is intentionally missing the setter.  Any updates to
      * the state machine needs to go through the DAO object because someone
@@ -513,4 +518,9 @@ public class EngineDataCenterVO implements EngineDataCenter, Identity {
     public PartitionType partitionType() {
         return PartitionType.Zone;
     }
+
+    @Override
+    public DataCenter.Type getType() {
+        return type;
+    }
 }
diff --git a/engine/schema/src/main/java/com/cloud/dc/DataCenterVO.java b/engine/schema/src/main/java/com/cloud/dc/DataCenterVO.java
index 038daeaa10b..827b72b58b0 100644
--- a/engine/schema/src/main/java/com/cloud/dc/DataCenterVO.java
+++ b/engine/schema/src/main/java/com/cloud/dc/DataCenterVO.java
@@ -138,6 +138,10 @@ public class DataCenterVO implements DataCenter {
     @Column(name = "sort_key")
     int sortKey;
 
+    @Column(name = "type")
+    @Enumerated(value = EnumType.STRING)
+    private DataCenter.Type type;
+
     @Override
     public String getDnsProvider() {
         return dnsProvider;
@@ -472,6 +476,15 @@ public class DataCenterVO implements DataCenter {
         return PartitionType.Zone;
     }
 
+    @Override
+    public DataCenter.Type getType() {
+        return type;
+    }
+
+    public void setType(Type type) {
+        this.type = type;
+    }
+
     @Override
     public String toString() {
         return String.format("Zone {\"id\": \"%s\", \"name\": \"%s\", \"uuid\": \"%s\"}", id, name, uuid);
diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDao.java b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDao.java
index a6cd59f1cc3..0754bbf3591 100644
--- a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDao.java
+++ b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDao.java
@@ -104,6 +104,8 @@ public interface DataCenterDao extends GenericDao<DataCenterVO, Long> {
 
     List<DataCenterVO> listEnabledZones();
 
+    List<Long> listEnabledNonEdgeZoneIds();
+
     DataCenterVO findByToken(String zoneToken);
 
     DataCenterVO findByTokenOrIdOrName(String tokenIdOrName);
diff --git a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDaoImpl.java b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDaoImpl.java
index 385fb406155..3bad5ee7eef 100644
--- a/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDaoImpl.java
+++ b/engine/schema/src/main/java/com/cloud/dc/dao/DataCenterDaoImpl.java
@@ -20,14 +20,17 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 import javax.persistence.TableGenerator;
 
+import org.apache.commons.collections.CollectionUtils;
 import org.apache.log4j.Logger;
 import org.springframework.stereotype.Component;
 
+import com.cloud.dc.DataCenter;
 import com.cloud.dc.DataCenterDetailVO;
 import com.cloud.dc.DataCenterIpAddressVO;
 import com.cloud.dc.DataCenterLinkLocalIpAddressVO;
@@ -63,6 +66,7 @@ public class DataCenterDaoImpl extends GenericDaoBase<DataCenterVO, Long> implem
     protected SearchBuilder<DataCenterVO> ChildZonesSearch;
     protected SearchBuilder<DataCenterVO> DisabledZonesSearch;
     protected SearchBuilder<DataCenterVO> TokenSearch;
+    protected SearchBuilder<DataCenterVO> ZoneAllocationAndNotTypeSearch;
 
     @Inject
     protected DataCenterIpAddressDao _ipAllocDao = null;
@@ -336,6 +340,11 @@ public class DataCenterDaoImpl extends GenericDaoBase<DataCenterVO, Long> implem
         DisabledZonesSearch.and("allocationState", DisabledZonesSearch.entity().getAllocationState(), SearchCriteria.Op.EQ);
         DisabledZonesSearch.done();
 
+        ZoneAllocationAndNotTypeSearch = createSearchBuilder();
+        ZoneAllocationAndNotTypeSearch.and("allocationState", ZoneAllocationAndNotTypeSearch.entity().getAllocationState(), SearchCriteria.Op.EQ);
+        ZoneAllocationAndNotTypeSearch.and("type", ZoneAllocationAndNotTypeSearch.entity().getType(), SearchCriteria.Op.NLIKE);
+        ZoneAllocationAndNotTypeSearch.done();
+
         TokenSearch = createSearchBuilder();
         TokenSearch.and("zoneToken", TokenSearch.entity().getZoneToken(), SearchCriteria.Op.EQ);
         TokenSearch.done();
@@ -399,6 +408,18 @@ public class DataCenterDaoImpl extends GenericDaoBase<DataCenterVO, Long> implem
         return dcs;
     }
 
+    @Override
+    public List<Long> listEnabledNonEdgeZoneIds() {
+        SearchCriteria<DataCenterVO> sc = ZoneAllocationAndNotTypeSearch.create();
+        sc.setParameters("allocationState", Grouping.AllocationState.Enabled);
+        sc.setParameters("type", DataCenter.Type.Edge);
+        List<DataCenterVO> zones = listBy(sc);
+        if (CollectionUtils.isEmpty(zones)) {
+            return new ArrayList<>();
+        }
+        return zones.stream().map(DataCenterVO::getId).collect(Collectors.toList());
+    }
+
     @Override
     public DataCenterVO findByTokenOrIdOrName(String tokenOrIdOrName) {
         DataCenterVO result = findByToken(tokenOrIdOrName);
diff --git a/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql b/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
index cc400143fb2..056a47ba512 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-41720to41800.sql
@@ -39,6 +39,50 @@ UPDATE `cloud`.`networks` SET public_mtu = 1500, private_mtu = 1500 WHERE remove
 UPDATE `cloud`.`vpc` SET public_mtu = 1500 WHERE removed IS NULL;
 UPDATE `cloud`.`nics` SET mtu = 1500 WHERE vm_type='DomainRouter' AND removed IS NULL AND reserver_name IN ('PublicNetworkGuru', 'ExternalGuestNetworkGuru');
 
+-- Add type column to data_center table
+CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.data_center', 'type', 'varchar(32) DEFAULT ''Core'' COMMENT ''the type of the zone'' ');
+
+-- Recreate data_center_view
+DROP VIEW IF EXISTS `cloud`.`data_center_view`;
+CREATE VIEW `cloud`.`data_center_view` AS
+    select
+        data_center.id,
+        data_center.uuid,
+        data_center.name,
+        data_center.is_security_group_enabled,
+        data_center.is_local_storage_enabled,
+        data_center.description,
+        data_center.dns1,
+        data_center.dns2,
+        data_center.ip6_dns1,
+        data_center.ip6_dns2,
+        data_center.internal_dns1,
+        data_center.internal_dns2,
+        data_center.guest_network_cidr,
+        data_center.domain,
+        data_center.networktype,
+        data_center.allocation_state,
+        data_center.zone_token,
+        data_center.dhcp_provider,
+        data_center.type,
+        data_center.removed,
+        data_center.sort_key,
+        domain.id domain_id,
+        domain.uuid domain_uuid,
+        domain.name domain_name,
+        domain.path domain_path,
+        dedicated_resources.affinity_group_id,
+        dedicated_resources.account_id,
+        affinity_group.uuid affinity_group_uuid
+    from
+        `cloud`.`data_center`
+            left join
+        `cloud`.`domain` ON data_center.domain_id = domain.id
+            left join
+        `cloud`.`dedicated_resources` ON data_center.id = dedicated_resources.data_center_id
+            left join
+        `cloud`.`affinity_group` ON dedicated_resources.affinity_group_id = affinity_group.id;
+
 DROP VIEW IF EXISTS `cloud`.`domain_router_view`;
 CREATE VIEW `cloud`.`domain_router_view` AS
     select
@@ -65,7 +109,7 @@ CREATE VIEW `cloud`.`domain_router_view` AS
         data_center.id data_center_id,
         data_center.uuid data_center_uuid,
         data_center.name data_center_name,
-        data_center.networktype data_center_type,
+        data_center.networktype data_center_network_type,
         data_center.dns1 dns1,
         data_center.dns2 dns2,
         data_center.ip6_dns1 ip6_dns1,
@@ -751,7 +795,7 @@ SELECT
     `data_center`.`uuid` AS `data_center_uuid`,
     `data_center`.`name` AS `data_center_name`,
     `data_center`.`is_security_group_enabled` AS `security_group_enabled`,
-    `data_center`.`networktype` AS `data_center_type`,
+    `data_center`.`networktype` AS `data_center_network_type`,
     `host`.`id` AS `host_id`,
     `host`.`uuid` AS `host_uuid`,
     `host`.`name` AS `host_name`,
diff --git a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java
index 7af282724b2..e7902ee99c2 100644
--- a/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java
+++ b/plugins/hypervisors/simulator/src/main/java/com/cloud/agent/manager/MockAgentManagerImpl.java
@@ -28,6 +28,9 @@ import com.cloud.agent.api.MaintainAnswer;
 import com.cloud.agent.api.PingTestCommand;
 import com.cloud.agent.api.routing.NetworkElementCommand;
 import com.cloud.api.commands.SimulatorAddSecondaryAgent;
+import com.cloud.dc.DataCenter;
+import com.cloud.dc.DataCenterVO;
+import com.cloud.dc.dao.DataCenterDao;
 import com.cloud.dc.dao.HostPodDao;
 import com.cloud.exception.DiscoveryException;
 import com.cloud.host.HostVO;
@@ -79,6 +82,8 @@ import java.util.regex.PatternSyntaxException;
 public class MockAgentManagerImpl extends ManagerBase implements MockAgentManager {
     private static final Logger s_logger = Logger.getLogger(MockAgentManagerImpl.class);
     @Inject
+    DataCenterDao dcDao;
+    @Inject
     HostPodDao _podDao = null;
     @Inject
     MockHostDao _mockHostDao = null;
@@ -106,7 +111,12 @@ public class MockAgentManagerImpl extends ManagerBase implements MockAgentManage
 
     private Pair<String, Long> getPodCidr(long podId, long dcId) {
         try {
-
+            DataCenterVO zone = dcDao.findById(dcId);
+            if (DataCenter.Type.Edge.equals(zone.getType())) {
+                String subnet = String.format("172.%d.%d.0", random.nextInt(15) + 16, random.nextInt(6) + 1);
+                s_logger.info(String.format("Pod belongs to an edge zone hence CIDR cannot be found, returning %s/24", subnet));
+                return new Pair<>(subnet, 24L);
+            }
             HashMap<Long, List<Object>> podMap = _podDao.getCurrentPodCidrSubnets(dcId, 0);
             List<Object> cidrPair = podMap.get(podId);
             String cidrAddress = (String)cidrPair.get(0);
diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java
index afbb7355fd6..99d46d57f67 100644
--- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java
+++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/ManagementServerMock.java
@@ -387,7 +387,7 @@ public class ManagementServerMock {
             ConfigurationManager mgr = (ConfigurationManager)_configService;
             _zone =
                 mgr.createZone(User.UID_SYSTEM, "default", "8.8.8.8", null, "8.8.4.4", null, null /* cidr */, "ROOT", Domain.ROOT_DOMAIN, NetworkType.Advanced, null,
-                    null /* networkDomain */, false, false, null, null);
+                    null /* networkDomain */, false, false, null, null, false);
         }
     }
 
diff --git a/server/src/main/java/com/cloud/api/ApiResponseHelper.java b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
index 437ff05d28d..e12e612fe51 100644
--- a/server/src/main/java/com/cloud/api/ApiResponseHelper.java
+++ b/server/src/main/java/com/cloud/api/ApiResponseHelper.java
@@ -37,11 +37,6 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
-import com.cloud.host.ControlState;
-import com.cloud.utils.security.CertificateHelper;
-import com.cloud.user.UserData;
-import com.cloud.api.query.dao.UserVmJoinDao;
-import com.cloud.network.vpc.VpcVO;
 import org.apache.cloudstack.acl.ControlledEntity;
 import org.apache.cloudstack.acl.ControlledEntity.ACLType;
 import org.apache.cloudstack.affinity.AffinityGroup;
@@ -205,6 +200,7 @@ import org.apache.log4j.Logger;
 
 import com.cloud.agent.api.VgpuTypesInfo;
 import com.cloud.api.query.ViewResponseHelper;
+import com.cloud.api.query.dao.UserVmJoinDao;
 import com.cloud.api.query.vo.AccountJoinVO;
 import com.cloud.api.query.vo.AsyncJobJoinVO;
 import com.cloud.api.query.vo.ControlledViewEntity;
@@ -254,6 +250,7 @@ import com.cloud.event.Event;
 import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.exception.PermissionDeniedException;
 import com.cloud.gpu.GPU;
+import com.cloud.host.ControlState;
 import com.cloud.host.Host;
 import com.cloud.host.HostVO;
 import com.cloud.hypervisor.HypervisorCapabilities;
@@ -318,6 +315,7 @@ import com.cloud.network.vpc.PrivateGateway;
 import com.cloud.network.vpc.StaticRoute;
 import com.cloud.network.vpc.Vpc;
 import com.cloud.network.vpc.VpcOffering;
+import com.cloud.network.vpc.VpcVO;
 import com.cloud.offering.DiskOffering;
 import com.cloud.offering.NetworkOffering;
 import com.cloud.offering.NetworkOffering.Detail;
@@ -361,6 +359,7 @@ import com.cloud.user.AccountManager;
 import com.cloud.user.SSHKeyPair;
 import com.cloud.user.User;
 import com.cloud.user.UserAccount;
+import com.cloud.user.UserData;
 import com.cloud.user.UserStatisticsVO;
 import com.cloud.user.dao.UserStatisticsDao;
 import com.cloud.uservm.UserVm;
@@ -374,6 +373,7 @@ import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.net.Dhcp;
 import com.cloud.utils.net.Ip;
 import com.cloud.utils.net.NetUtils;
+import com.cloud.utils.security.CertificateHelper;
 import com.cloud.vm.ConsoleProxyVO;
 import com.cloud.vm.InstanceGroup;
 import com.cloud.vm.Nic;
diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
index 20a139f469f..dba2363d4c9 100644
--- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
+++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java
@@ -183,6 +183,7 @@ import com.cloud.api.query.vo.TemplateJoinVO;
 import com.cloud.api.query.vo.UserAccountJoinVO;
 import com.cloud.api.query.vo.UserVmJoinVO;
 import com.cloud.api.query.vo.VolumeJoinVO;
+import com.cloud.dc.DataCenter;
 import com.cloud.dc.DedicatedResourceVO;
 import com.cloud.dc.dao.DedicatedResourceDao;
 import com.cloud.domain.Domain;
@@ -3034,6 +3035,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
             SearchCriteria<DiskOfferingJoinVO> zoneSC = sb.create();
             zoneSC.setParameters("zoneId", String.valueOf(zoneId));
             sc.addAnd("zoneId", SearchCriteria.Op.SC, zoneSC);
+            DataCenterJoinVO zone = _dcJoinDao.findById(zoneId);
+            if (DataCenter.Type.Edge.equals(zone.getType())) {
+                sc.addAnd("useLocalStorage", Op.EQ, true);
+            }
         }
 
         DiskOffering currentDiskOffering = null;
@@ -3281,6 +3286,10 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q
             SearchCriteria<ServiceOfferingJoinVO> zoneSC = sb.create();
             zoneSC.setParameters("zoneId", String.valueOf(zoneId));
             sc.addAnd("zoneId", SearchCriteria.Op.SC, zoneSC);
+            DataCenterJoinVO zone = _dcJoinDao.findById(zoneId);
+            if (DataCenter.Type.Edge.equals(zone.getType())) {
+                sc.addAnd("useLocalStorage", Op.EQ, true);
+            }
         }
 
         if (cpuNumber != null) {
diff --git a/server/src/main/java/com/cloud/api/query/dao/DataCenterJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/DataCenterJoinDaoImpl.java
index 899cf6d7a5d..50c5275390e 100644
--- a/server/src/main/java/com/cloud/api/query/dao/DataCenterJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/DataCenterJoinDaoImpl.java
@@ -27,6 +27,7 @@ import org.apache.cloudstack.api.response.ResourceIconResponse;
 import org.apache.cloudstack.api.response.ResourceTagResponse;
 import org.apache.cloudstack.api.response.ZoneResponse;
 import org.apache.cloudstack.context.CallContext;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.log4j.Logger;
 import org.springframework.stereotype.Component;
 
@@ -69,6 +70,7 @@ public class DataCenterJoinDaoImpl extends GenericDaoBase<DataCenterJoinVO, Long
         zoneResponse.setName(dataCenter.getName());
         zoneResponse.setSecurityGroupsEnabled(ApiDBUtils.isSecurityGroupEnabledInZone(dataCenter.getId()));
         zoneResponse.setLocalStorageEnabled(dataCenter.isLocalStorageEnabled());
+        zoneResponse.setType(ObjectUtils.defaultIfNull(dataCenter.getType(), DataCenter.Type.Core).toString());
 
         if ((dataCenter.getDescription() != null) && !dataCenter.getDescription().equalsIgnoreCase("null")) {
             zoneResponse.setDescription(dataCenter.getDescription());
diff --git a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
index 6d59b691836..49489c2bfeb 100644
--- a/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
+++ b/server/src/main/java/com/cloud/api/query/dao/UserVmJoinDaoImpl.java
@@ -27,9 +27,6 @@ import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
-import com.cloud.network.vpc.VpcVO;
-import com.cloud.network.vpc.dao.VpcDao;
-import com.cloud.storage.DiskOfferingVO;
 import org.apache.cloudstack.affinity.AffinityGroupResponse;
 import org.apache.cloudstack.annotation.AnnotationService;
 import org.apache.cloudstack.annotation.dao.AnnotationDao;
@@ -53,7 +50,10 @@ import com.cloud.api.ApiResponseHelper;
 import com.cloud.api.query.vo.UserVmJoinVO;
 import com.cloud.gpu.GPU;
 import com.cloud.host.ControlState;
+import com.cloud.network.vpc.VpcVO;
+import com.cloud.network.vpc.dao.VpcDao;
 import com.cloud.service.ServiceOfferingDetailsVO;
+import com.cloud.storage.DiskOfferingVO;
 import com.cloud.storage.GuestOS;
 import com.cloud.user.Account;
 import com.cloud.user.AccountManager;
diff --git a/server/src/main/java/com/cloud/api/query/vo/DataCenterJoinVO.java b/server/src/main/java/com/cloud/api/query/vo/DataCenterJoinVO.java
index 14273f00bf8..23e8766e677 100644
--- a/server/src/main/java/com/cloud/api/query/vo/DataCenterJoinVO.java
+++ b/server/src/main/java/com/cloud/api/query/vo/DataCenterJoinVO.java
@@ -28,6 +28,7 @@ import javax.persistence.Table;
 import org.apache.cloudstack.api.Identity;
 import org.apache.cloudstack.api.InternalIdentity;
 
+import com.cloud.dc.DataCenter;
 import com.cloud.dc.DataCenter.NetworkType;
 import com.cloud.org.Grouping.AllocationState;
 import com.cloud.utils.db.GenericDao;
@@ -120,6 +121,10 @@ public class DataCenterJoinVO extends BaseViewVO implements InternalIdentity, Id
     @Column(name = "sort_key")
     private int sortKey;
 
+    @Column(name = "type")
+    @Enumerated(value = EnumType.STRING)
+    private DataCenter.Type type;
+
     public DataCenterJoinVO() {
     }
 
@@ -228,4 +233,8 @@ public class DataCenterJoinVO extends BaseViewVO implements InternalIdentity, Id
     public int getSortKey() {
         return sortKey;
     }
+
+     public DataCenter.Type getType() {
+        return type;
+    }
 }
diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
index 271f9ad17cf..0f9aca452da 100644
--- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
+++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
@@ -1402,14 +1402,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
         }
     }
 
-    private void checkPodAttributes(final long podId, final String podName, final long zoneId, final String gateway, final String cidr, final String startIp, final String endIp, final String allocationStateStr,
-            final boolean checkForDuplicates, final boolean skipGatewayOverlapCheck) {
-        if (checkForDuplicates) {
-            // Check if the pod already exists
-            if (validPod(podName, zoneId)) {
-                throw new InvalidParameterValueException("A pod with name: " + podName + " already exists in zone " + zoneId + ". Please specify a different pod name. ");
-            }
-        }
+    private void checkPodAttributesForNonEdgeZone(final long podId, final String podName, final DataCenter zone, final String gateway,
+          final String cidr, final String startIp, final String endIp, final boolean skipGatewayOverlapCheck) {
 
         String cidrAddress;
         long cidrSize;
@@ -1426,8 +1420,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
         checkIpRange(startIp, endIp, cidrAddress, cidrSize);
 
         // Check if the IP range overlaps with the public ip
-        if(StringUtils.isNotEmpty(startIp)) {
-            checkOverlapPublicIpRange(zoneId, startIp, endIp);
+        if (StringUtils.isNotEmpty(startIp)) {
+            checkOverlapPublicIpRange(zone.getId(), startIp, endIp);
         }
 
         // Check if the gateway is a valid IP address
@@ -1449,7 +1443,21 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
 
         final String checkPodCIDRs = _configDao.getValue("check.pod.cidrs");
         if (checkPodCIDRs == null || checkPodCIDRs.trim().isEmpty() || Boolean.parseBoolean(checkPodCIDRs)) {
-            checkPodCidrSubnets(zoneId, podId, cidr);
+            checkPodCidrSubnets(zone.getId(), podId, cidr);
+        }
+    }
+
+    private void checkPodAttributes(final long podId, final String podName, final DataCenter zone, final String gateway, final String cidr, final String startIp, final String endIp, final String allocationStateStr,
+            final boolean checkForDuplicates, final boolean skipGatewayOverlapCheck) {
+        if (checkForDuplicates) {
+            // Check if the pod already exists
+            if (validPod(podName, zone.getId())) {
+                throw new InvalidParameterValueException("A pod with name: " + podName + " already exists in zone " + zone.getId() + ". Please specify a different pod name. ");
+            }
+        }
+
+        if (!DataCenter.Type.Edge.equals(zone.getType())) {
+            checkPodAttributesForNonEdgeZone(podId, podName, zone, gateway, cidr, startIp, endIp, skipGatewayOverlapCheck);
         }
 
         if (allocationStateStr != null && !allocationStateStr.isEmpty()) {
@@ -2098,7 +2106,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
         // Verify pod's attributes
         final String cidr = NetUtils.ipAndNetMaskToCidr(gateway, netmask);
         final boolean checkForDuplicates = !oldPodName.equals(name);
-        checkPodAttributes(id, name, pod.getDataCenterId(), gateway, cidr, startIp, endIp, allocationStateStr, checkForDuplicates, true);
+        final DataCenterVO zone = _zoneDao.findById(pod.getDataCenterId());
+        checkPodAttributes(id, name, zone, gateway, cidr, startIp, endIp, allocationStateStr, checkForDuplicates, true);
 
         // Valid check is already done in checkPodAttributes method.
         final String cidrAddress = getCidrAddress(cidr);
@@ -2161,49 +2170,63 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
         return pod;
     }
 
-    @Override
-    @ActionEvent(eventType = EventTypes.EVENT_POD_CREATE, eventDescription = "creating pod", async = false)
-    public Pod createPod(final long zoneId, final String name, final String startIp, final String endIp, final String gateway, final String netmask, String allocationState) {
-        // Check if the gateway is a valid IP address
+    private void checkPodRangeParametersBasicsForNonEdgeZone(final String startIp, final String endIp, final String gateway, final String netmask) {
+        if (!NetUtils.isValidIp4(startIp)) {
+            throw new InvalidParameterValueException("The start IP is invalid");
+        }
+        if (endIp != null && !NetUtils.isValidIp4(endIp)) {
+            throw new InvalidParameterValueException("The end IP is invalid");
+        }
         if (!NetUtils.isValidIp4(gateway)) {
             throw new InvalidParameterValueException("The gateway is invalid");
         }
-
         if (!NetUtils.isValidIp4Netmask(netmask)) {
             throw new InvalidParameterValueException("The netmask is invalid");
         }
+    }
 
-        final String cidr = NetUtils.ipAndNetMaskToCidr(gateway, netmask);
+    @Override
+    @ActionEvent(eventType = EventTypes.EVENT_POD_CREATE, eventDescription = "creating pod", async = false)
+    public Pod createPod(final long zoneId, final String name, final String startIp, final String endIp, final String gateway, final String netmask, String allocationState) {
+        final DataCenterVO zone = _zoneDao.findById(zoneId);
+        if (zone == null) {
+            throw new InvalidParameterValueException("Please specify a valid zone.");
+        }
+        final Account account = CallContext.current().getCallingAccount();
+        if (Grouping.AllocationState.Disabled == zone.getAllocationState()
+                && !_accountMgr.isRootAdmin(account.getId())) {
+            throw new PermissionDeniedException("Cannot perform this operation, Zone is currently disabled: " + zoneId);
+        }
+
+        String cidr = null;
+        if (!DataCenter.Type.Edge.equals(zone.getType())) {
+            checkPodRangeParametersBasicsForNonEdgeZone(startIp, endIp, gateway, netmask);
+            cidr = NetUtils.ipAndNetMaskToCidr(gateway, netmask);
+        } else {
+            if (ObjectUtils.anyNotNull(startIp, endIp, gateway, netmask)) {
+                throw new InvalidParameterValueException("IP range parameters can not be specified for a pod in an edge zone");
+            }
+        }
 
         final Long userId = CallContext.current().getCallingUserId();
 
         if (allocationState == null) {
             allocationState = Grouping.AllocationState.Enabled.toString();
         }
-        return createPod(userId.longValue(), name, zoneId, gateway, cidr, startIp, endIp, allocationState, false);
+        return createPod(userId.longValue(), name, zone, gateway, cidr, startIp, endIp, allocationState, false);
     }
 
     @Override
     @DB
-    public HostPodVO createPod(final long userId, final String podName, final long zoneId, final String gateway, final String cidr, final String startIp, String endIp, final String allocationStateStr,
+    public HostPodVO createPod(final long userId, final String podName, final DataCenter zone, final String gateway, final String cidr, String startIp, String endIp, final String allocationStateStr,
             final boolean skipGatewayOverlapCheck) {
-
-        // Check if the zone is valid
-        if (!validZone(zoneId)) {
-            throw new InvalidParameterValueException("Please specify a valid zone.");
+        final String cidrAddress = DataCenter.Type.Edge.equals(zone.getType()) ? "" : getCidrAddress(cidr);
+        final int cidrSize = DataCenter.Type.Edge.equals(zone.getType()) ? 0 : getCidrSize(cidr);
+        if (DataCenter.Type.Edge.equals(zone.getType())) {
+            startIp = null;
+            endIp = null;
         }
 
-        // Check if zone is disabled
-        final DataCenterVO zone = _zoneDao.findById(zoneId);
-        final Account account = CallContext.current().getCallingAccount();
-        if (Grouping.AllocationState.Disabled == zone.getAllocationState()
-                && !_accountMgr.isRootAdmin(account.getId())) {
-            throw new PermissionDeniedException("Cannot perform this operation, Zone is currently disabled: " + zoneId);
-        }
-
-        final String cidrAddress = getCidrAddress(cidr);
-        final int cidrSize = getCidrSize(cidr);
-
         // endIp is an optional parameter; if not specified - default it to the
         // end ip of the pod's cidr
         if (StringUtils.isNotEmpty(startIp)) {
@@ -2213,18 +2236,15 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
         }
 
         // Validate new pod settings
-        checkPodAttributes(-1, podName, zoneId, gateway, cidr, startIp, endIp, allocationStateStr, true, skipGatewayOverlapCheck);
+        checkPodAttributes(-1, podName, zone, gateway, cidr, startIp, endIp, allocationStateStr, true, skipGatewayOverlapCheck);
 
         // Create the new pod in the database
-        String ipRange;
-
+        String ipRange = null;
         if (StringUtils.isNotEmpty(startIp)) {
             ipRange = startIp + "-" + endIp + "-" + DefaultForSystemVmsForPodIpRange + "-" + DefaultVlanForPodIpRange;
-        } else {
-            throw new InvalidParameterValueException("Start ip is required parameter");
         }
 
-        final HostPodVO podFinal = new HostPodVO(podName, zoneId, gateway, cidrAddress, cidrSize, ipRange);
+        final HostPodVO podFinal = new HostPodVO(podName, zone.getId(), StringUtils.defaultIfEmpty(gateway, "") , cidrAddress, cidrSize, ipRange);
 
         Grouping.AllocationState allocationState = null;
         if (allocationStateStr != null && !allocationStateStr.isEmpty()) {
@@ -2232,26 +2252,24 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
             podFinal.setAllocationState(allocationState);
         }
 
+        final String startIpFinal = startIp;
         final String endIpFinal = endIp;
-        return Transaction.execute(new TransactionCallback<HostPodVO>() {
-            @Override
-            public HostPodVO doInTransaction(final TransactionStatus status) {
+        return Transaction.execute((TransactionCallback<HostPodVO>) status -> {
 
-                final HostPodVO pod = _podDao.persist(podFinal);
+            final HostPodVO pod = _podDao.persist(podFinal);
 
-                if (StringUtils.isNotEmpty(startIp)) {
-                    _zoneDao.addPrivateIpAddress(zoneId, pod.getId(), startIp, endIpFinal, false, null);
-                }
+            if (StringUtils.isNotEmpty(startIpFinal)) {
+                _zoneDao.addPrivateIpAddress(zone.getId(), pod.getId(), startIpFinal, endIpFinal, false, null);
+            }
 
-                final String[] linkLocalIpRanges = NetUtils.getLinkLocalIPRange(_configDao.getValue(Config.ControlCidr.key()));
-                if (linkLocalIpRanges != null) {
-                    _zoneDao.addLinkLocalIpAddress(zoneId, pod.getId(), linkLocalIpRanges[0], linkLocalIpRanges[1]);
-                }
+            final String[] linkLocalIpRanges = NetUtils.getLinkLocalIPRange(_configDao.getValue(Config.ControlCidr.key()));
+            if (linkLocalIpRanges.length > 1) {
+                _zoneDao.addLinkLocalIpAddress(zone.getId(), pod.getId(), linkLocalIpRanges[0], linkLocalIpRanges[1]);
+            }
 
-                CallContext.current().putContextParameter(Pod.class, pod.getUuid());
+            CallContext.current().putContextParameter(Pod.class, pod.getUuid());
 
-                return pod;
-            }
+            return pod;
         });
     }
 
@@ -2624,7 +2642,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
                 if (allocationStateStr != null && !allocationStateStr.isEmpty()) {
                     final Grouping.AllocationState allocationState = Grouping.AllocationState.valueOf(allocationStateStr);
 
-                    if (allocationState == Grouping.AllocationState.Enabled) {
+                    if (allocationState == Grouping.AllocationState.Enabled && !DataCenter.Type.Edge.equals(zone.getType())) {
                         // check if zone has necessary trafficTypes before enabling
                         try {
                             PhysicalNetwork mgmtPhyNetwork;
@@ -2694,7 +2712,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
     @DB
     public DataCenterVO createZone(final long userId, final String zoneName, final String dns1, final String dns2, final String internalDns1, final String internalDns2, final String guestCidr, final String domain,
             final Long domainId, final NetworkType zoneType, final String allocationStateStr, final String networkDomain, final boolean isSecurityGroupEnabled, final boolean isLocalStorageEnabled,
-            final String ip6Dns1, final String ip6Dns2) {
+            final String ip6Dns1, final String ip6Dns2, final boolean isEdge) {
 
         // checking the following params outside checkzoneparams method as we do
         // not use these params for updatezone
@@ -2728,6 +2746,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
             // physical network and providers setup.
             zoneFinal.setAllocationState(Grouping.AllocationState.Disabled);
         }
+        zoneFinal.setType(isEdge ? DataCenter.Type.Edge : DataCenter.Type.Core);
 
         return Transaction.execute(new TransactionCallback<DataCenterVO>() {
             @Override
@@ -2838,6 +2857,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
         final String networkDomain = cmd.getDomain();
         boolean isSecurityGroupEnabled = cmd.getSecuritygroupenabled();
         final boolean isLocalStorageEnabled = cmd.getLocalStorageEnabled();
+        final boolean isEdge = cmd.isEdge();
 
         if (allocationState == null) {
             allocationState = Grouping.AllocationState.Disabled.toString();
@@ -2856,6 +2876,10 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
             throw new InvalidParameterValueException("guestCidrAddress parameter is not supported for Basic zone");
         }
 
+        if (!NetworkType.Advanced.equals(zoneType) && isEdge) {
+            throw new InvalidParameterValueException("Only advanced network type zones can be edge zones");
+        }
+
         DomainVO domainVO = null;
 
         if (domainId != null) {
@@ -2867,7 +2891,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
         }
 
         return createZone(userId, zoneName, dns1, dns2, internalDns1, internalDns2, guestCidr, domainVO != null ? domainVO.getName() : null, domainId, zoneType, allocationState,
-                networkDomain, isSecurityGroupEnabled, isLocalStorageEnabled, ip6Dns1, ip6Dns2);
+                networkDomain, isSecurityGroupEnabled, isLocalStorageEnabled, ip6Dns1, ip6Dns2, isEdge);
     }
 
     @Override
@@ -5711,10 +5735,6 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
     }
 
     private boolean validPod(final String podName, final long zoneId) {
-        if (!validZone(zoneId)) {
-            return false;
-        }
-
         return _podDao.findByName(podName, zoneId) != null;
     }
 
diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
index e0c48956a63..c1d4a22bf77 100644
--- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
+++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java
@@ -21,15 +21,17 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
-import com.cloud.utils.PasswordGenerator;
 import org.apache.cloudstack.agent.lb.IndirectAgentLB;
 import org.apache.cloudstack.ca.CAManager;
 import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
@@ -48,6 +50,7 @@ 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.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.log4j.Logger;
 
 import com.cloud.agent.AgentManager;
@@ -117,6 +120,7 @@ import com.cloud.user.AccountManager;
 import com.cloud.utils.DateUtil;
 import com.cloud.utils.NumbersUtil;
 import com.cloud.utils.Pair;
+import com.cloud.utils.PasswordGenerator;
 import com.cloud.utils.StringUtils;
 import com.cloud.utils.component.ManagerBase;
 import com.cloud.utils.db.DB;
@@ -148,10 +152,6 @@ import com.cloud.vm.dao.VMInstanceDao;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonParseException;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.apache.commons.lang3.BooleanUtils;
 
 /**
  * Class to manage console proxys. <br><br>
@@ -1491,15 +1491,11 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
 
     @Override
     public Long[] getScannablePools() {
-        List<DataCenterVO> zones = dataCenterDao.listEnabledZones();
-
-        Long[] dcIdList = new Long[zones.size()];
-        int i = 0;
-        for (DataCenterVO dc : zones) {
-            dcIdList[i++] = dc.getId();
+        List<Long> zoneIds = dataCenterDao.listEnabledNonEdgeZoneIds();
+        if (s_logger.isDebugEnabled()) {
+            s_logger.debug(String.format("Enabled non-edge zones available for scan: %s", org.apache.commons.lang3.StringUtils.join(zoneIds, ",")));
         }
-
-        return dcIdList;
+        return zoneIds.toArray(Long[]::new);
     }
 
     @Override
diff --git a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java
index 2cd377a04f9..24581aa1ee7 100644
--- a/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java
+++ b/server/src/main/java/com/cloud/network/router/VirtualNetworkApplianceManagerImpl.java
@@ -3320,6 +3320,11 @@ Configurable, StateListener<VirtualMachine.State, VirtualMachine.Event, VirtualM
     @Override
     public ConfigKey<?>[] getConfigKeys() {
         return new ConfigKey<?>[] {
+                RouterTemplateKvm,
+                RouterTemplateVmware,
+                RouterTemplateHyperV,
+                RouterTemplateLxc,
+                RouterTemplateOvm3,
                 UseExternalDnsServers,
                 RouterVersionCheckEnabled,
                 SetServiceMonitor,
diff --git a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java
index 6b66b999ead..bd8811b2a15 100755
--- a/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java
+++ b/server/src/main/java/com/cloud/storage/snapshot/SnapshotManagerImpl.java
@@ -78,7 +78,10 @@ import com.cloud.api.query.MutualExclusiveIdsManagerBase;
 import com.cloud.configuration.Config;
 import com.cloud.configuration.Resource.ResourceType;
 import com.cloud.dc.ClusterVO;
+import com.cloud.dc.DataCenter;
+import com.cloud.dc.DataCenterVO;
 import com.cloud.dc.dao.ClusterDao;
+import com.cloud.dc.dao.DataCenterDao;
 import com.cloud.domain.dao.DomainDao;
 import com.cloud.event.ActionEvent;
 import com.cloud.event.ActionEventUtils;
@@ -216,6 +219,8 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
 
     @Inject
     protected SnapshotHelper snapshotHelper;
+    @Inject
+    DataCenterDao dataCenterDao;
 
     private int _totalRetries;
     private int _pauseInterval;
@@ -223,6 +228,14 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
 
     private ScheduledExecutorService backupSnapshotExecutor;
 
+    protected boolean isBackupSnapshotToSecondaryForZone(long zoneId) {
+        if (Boolean.FALSE.equals(SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value())) {
+            return false;
+        }
+        DataCenterVO zone = dataCenterDao.findById(zoneId);
+        return !DataCenter.Type.Edge.equals(zone.getType());
+    }
+
     @Override
     public String getConfigComponentName() {
         return SnapshotManager.class.getSimpleName();
@@ -1250,7 +1263,7 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
             }
 
             SnapshotInfo snapshotOnPrimary = snapshotStrategy.takeSnapshot(snapshot);
-            boolean backupSnapToSecondary = SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value() == null || SnapshotInfo.BackupSnapshotAfterTakingSnapshot.value();
+            boolean backupSnapToSecondary = isBackupSnapshotToSecondaryForZone(snapshot.getDataCenterId());
 
             if (backupSnapToSecondary) {
                 backupSnapshotToSecondary(payload.getAsyncBackup(), snapshotStrategy, snapshotOnPrimary);
@@ -1262,7 +1275,7 @@ public class SnapshotManagerImpl extends MutualExclusiveIdsManagerBase implement
             try {
                 postCreateSnapshot(volume.getId(), snapshotId, payload.getSnapshotPolicyId());
 
-                DataStoreRole dataStoreRole = snapshotHelper.getDataStoreRole(snapshot);
+                DataStoreRole dataStoreRole = backupSnapToSecondary ? snapshotHelper.getDataStoreRole(snapshot) : DataStoreRole.Primary;
 
                 SnapshotDataStoreVO snapshotStoreRef = _snapshotStoreDao.findBySnapshot(snapshotId, dataStoreRole);
                 if (snapshotStoreRef == null) {
diff --git a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java
index fb80983415d..0f5fdca5381 100644
--- a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java
@@ -16,11 +16,33 @@
 // under the License.
 package org.apache.cloudstack.consoleproxy;
 
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.inject.Inject;
+import javax.naming.ConfigurationException;
+
+import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
+import org.apache.cloudstack.context.CallContext;
+import org.apache.cloudstack.framework.security.keys.KeysManager;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.log4j.Logger;
+
 import com.cloud.agent.AgentManager;
 import com.cloud.agent.api.Answer;
 import com.cloud.agent.api.GetVmVncTicketAnswer;
 import com.cloud.agent.api.GetVmVncTicketCommand;
 import com.cloud.consoleproxy.ConsoleProxyManager;
+import com.cloud.dc.DataCenter;
+import com.cloud.dc.dao.DataCenterDao;
 import com.cloud.exception.AgentUnavailableException;
 import com.cloud.exception.OperationTimedoutException;
 import com.cloud.exception.PermissionDeniedException;
@@ -48,25 +70,6 @@ import com.cloud.vm.dao.ConsoleSessionDao;
 import com.cloud.vm.dao.UserVmDetailsDao;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
-import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
-import org.apache.cloudstack.context.CallContext;
-import org.apache.cloudstack.framework.security.keys.KeysManager;
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.ObjectUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.log4j.Logger;
-
-import javax.crypto.Mac;
-import javax.crypto.spec.SecretKeySpec;
-import javax.inject.Inject;
-import javax.naming.ConfigurationException;
-
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
 
 public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAccessManager {
 
@@ -87,6 +90,8 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
     @Inject
     private ConsoleProxyManager consoleProxyManager;
     @Inject
+    DataCenterDao dataCenterDao;
+    @Inject
     private ConsoleSessionDao consoleSessionDao;
 
     private static KeysManager secretKeysManager;
@@ -135,6 +140,13 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
                 return new ConsoleEndpoint(false, null, "Permission denied");
             }
 
+            DataCenter zone = dataCenterDao.findById(vm.getDataCenterId());
+            if (zone != null && DataCenter.Type.Edge.equals(zone.getType())) {
+                String errorMsg = "Console access is not supported for Edge zones";
+                s_logger.error(errorMsg);
+                return new ConsoleEndpoint(false, null, errorMsg);
+            }
+
             String sessionUuid = UUID.randomUUID().toString();
             return generateAccessEndpoint(vmId, sessionUuid, extraSecurityToken, clientAddress);
         } catch (Exception e) {
diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerTest.java
index 7515f125972..05e38f66da9 100644
--- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerTest.java
+++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerTest.java
@@ -46,6 +46,8 @@ import org.apache.cloudstack.api.command.admin.network.ListGuestNetworkIpv6Prefi
 import org.apache.cloudstack.api.command.admin.vlan.CreateVlanIpRangeCmd;
 import org.apache.cloudstack.api.command.admin.vlan.DedicatePublicIpRangeCmd;
 import org.apache.cloudstack.api.command.admin.vlan.ReleasePublicIpRangeCmd;
+import org.apache.cloudstack.api.command.admin.zone.CreateZoneCmd;
+import org.apache.cloudstack.api.command.admin.zone.UpdateZoneCmd;
 import org.apache.cloudstack.api.command.user.network.ListNetworkOfferingsCmd;
 import org.apache.cloudstack.context.CallContext;
 import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
@@ -66,12 +68,14 @@ import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 import org.mockito.stubbing.Answer;
+import org.springframework.test.util.ReflectionTestUtils;
 
 import com.cloud.api.query.dao.NetworkOfferingJoinDao;
 import com.cloud.api.query.vo.NetworkOfferingJoinVO;
 import com.cloud.configuration.Resource.ResourceType;
 import com.cloud.dc.AccountVlanMapVO;
 import com.cloud.dc.ClusterVO;
+import com.cloud.dc.DataCenter;
 import com.cloud.dc.DataCenter.NetworkType;
 import com.cloud.dc.DataCenterGuestIpv6Prefix;
 import com.cloud.dc.DataCenterGuestIpv6PrefixVO;
@@ -106,10 +110,10 @@ import com.cloud.network.dao.Ipv6GuestPrefixSubnetNetworkMapDao;
 import com.cloud.network.dao.PhysicalNetworkDao;
 import com.cloud.network.dao.PhysicalNetworkVO;
 import com.cloud.projects.ProjectManager;
-import com.cloud.storage.dao.DiskOfferingDao;
-import com.cloud.storage.dao.StoragePoolTagsDao;
 import com.cloud.storage.DiskOfferingVO;
 import com.cloud.storage.VolumeVO;
+import com.cloud.storage.dao.DiskOfferingDao;
+import com.cloud.storage.dao.StoragePoolTagsDao;
 import com.cloud.storage.dao.VolumeDao;
 import com.cloud.user.Account;
 import com.cloud.user.AccountManager;
@@ -1230,4 +1234,95 @@ public class ConfigurationManagerTest {
             throw new RuntimeException(e);
         }
     }
+
+    private void mockPersistDatacenterForCreateZone() {
+        Mockito.when(_zoneDao.persist(Mockito.any(DataCenterVO.class))).thenAnswer((Answer<DataCenterVO>) invocation -> {
+            DataCenterVO zone = (DataCenterVO)invocation.getArguments()[0];
+            ReflectionTestUtils.setField(zone, "uuid", UUID.randomUUID().toString());
+            ReflectionTestUtils.setField(zone, "id", 1L);
+            return zone;
+        });
+    }
+
+    @Test
+    public void testCreateEdgeZone() {
+        CreateZoneCmd cmd = Mockito.mock(CreateZoneCmd.class);
+        Mockito.when(cmd.isEdge()).thenReturn(true);
+        Mockito.when(cmd.getNetworkType()).thenReturn(NetworkType.Advanced.toString());
+        Mockito.when(cmd.getDomainId()).thenReturn(null);
+        mockPersistDatacenterForCreateZone();
+        DataCenter zone = configurationMgr.createZone(cmd);
+        Assert.assertNotNull(zone);
+        Assert.assertEquals(NetworkType.Advanced, zone.getNetworkType());
+        Assert.assertEquals(DataCenter.Type.Edge, zone.getType());
+    }
+
+    @Test
+    public void testCreateCoreZone() {
+        CreateZoneCmd cmd = Mockito.mock(CreateZoneCmd.class);
+        Mockito.when(cmd.isEdge()).thenReturn(false);
+        Mockito.when(cmd.getNetworkType()).thenReturn(NetworkType.Advanced.toString());
+        Mockito.when(cmd.getDomainId()).thenReturn(null);
+        mockPersistDatacenterForCreateZone();
+        DataCenter zone = configurationMgr.createZone(cmd);
+        Assert.assertNotNull(zone);
+        Assert.assertEquals(NetworkType.Advanced, zone.getNetworkType());
+        Assert.assertEquals(DataCenter.Type.Core, zone.getType());
+    }
+
+    @Test
+    public void testCreateBasicZone() {
+        CreateZoneCmd cmd = Mockito.mock(CreateZoneCmd.class);
+        Mockito.when(cmd.isEdge()).thenReturn(false);
+        Mockito.when(cmd.getNetworkType()).thenReturn(NetworkType.Basic.toString());
+        Mockito.when(cmd.getDomainId()).thenReturn(null);
+        mockPersistDatacenterForCreateZone();
+        DataCenter zone = configurationMgr.createZone(cmd);
+        Assert.assertNotNull(zone);
+        Assert.assertEquals(NetworkType.Basic, zone.getNetworkType());
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void testCreateBasicEdgeZoneFailure() {
+        CreateZoneCmd cmd = Mockito.mock(CreateZoneCmd.class);
+        Mockito.when(cmd.isEdge()).thenReturn(true);
+        Mockito.when(cmd.getNetworkType()).thenReturn(NetworkType.Basic.toString());
+        Mockito.when(cmd.getDomainId()).thenReturn(null);
+        configurationMgr.createZone(cmd);
+    }
+
+    @Test
+    public void testEditEdgeZone() {
+        // editZone should be successful despite no Public network
+        final Long zoneId = 1L;
+        UpdateZoneCmd cmd = Mockito.mock(UpdateZoneCmd.class);
+        Mockito.when(cmd.getId()).thenReturn(zoneId);
+        Mockito.when(cmd.getZoneName()).thenReturn("NewName");
+        DataCenterVO zone = Mockito.mock(DataCenterVO.class);
+        Mockito.when(zone.getNetworkType()).thenReturn(NetworkType.Advanced);
+        Mockito.when(zone.getType()).thenReturn(DataCenter.Type.Edge);
+        Mockito.when(zone.getId()).thenReturn(zoneId);
+        Mockito.when(_zoneDao.findById(Mockito.anyLong())).thenReturn(zone);
+        Mockito.when(_networkModel.getDefaultPhysicalNetworkByZoneAndTrafficType(zoneId, Networks.TrafficType.Public)).thenReturn(null);
+        Mockito.when(_zoneDao.update(Mockito.anyLong(), Mockito.any(DataCenterVO.class))).thenReturn(true);
+        configurationMgr.editZone(cmd);
+    }
+
+    @Test
+    public void testEdgeZoneCreatePod() {
+        final long zoneId = 1L;
+        DataCenterVO zone = Mockito.mock(DataCenterVO.class);
+        Mockito.when(zone.getNetworkType()).thenReturn(NetworkType.Advanced);
+        Mockito.when(zone.getType()).thenReturn(DataCenter.Type.Edge);
+        Mockito.when(zone.getId()).thenReturn(1L);
+        Mockito.when(_zoneDao.findById(Mockito.anyLong())).thenReturn(zone);
+        Mockito.when(_configDao.getValue(Config.ControlCidr.key())).thenReturn(Config.ControlCidr.getDefaultValue());
+        Mockito.when(_podDao.persist(Mockito.any(HostPodVO.class))).thenAnswer((Answer<HostPodVO>) invocation -> {
+            HostPodVO pod = (HostPodVO)invocation.getArguments()[0];
+            ReflectionTestUtils.setField(pod, "uuid", UUID.randomUUID().toString());
+            ReflectionTestUtils.setField(pod, "id", 1L);
+            return pod;
+        });
+        configurationMgr.createPod(zoneId, "TestPod", null, null, null, null, null);
+    }
 }
diff --git a/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyManagerTest.java b/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyManagerTest.java
index 5dc7df807d2..3a50c5c4685 100644
--- a/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyManagerTest.java
+++ b/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyManagerTest.java
@@ -24,7 +24,10 @@ import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import org.apache.log4j.Logger;
 import org.junit.Assert;
@@ -275,4 +278,24 @@ public class ConsoleProxyManagerTest {
         ConsoleProxyStatus result = new ConsoleProxyManagerImpl().parseJsonToConsoleProxyStatus(null);
         Assert.assertEquals(expectedResult, result);
     }
+
+    private void verifyScannablePoolsZoneIds(List<Long> expected, Long[] result) {
+        Assert.assertNotNull(result);
+        Assert.assertEquals(expected.size(), result.length);
+        for (int i = 0; i < expected.size(); ++i) {
+            Assert.assertEquals(expected.get(i), result[i]);
+        }
+    }
+
+    @Test
+    public void testGetScannablePools() {
+        List<Long> dbZoneIds = new ArrayList<>();
+        Mockito.when(dataCenterDaoMock.listEnabledNonEdgeZoneIds()).thenReturn(dbZoneIds);
+        ConsoleProxyManagerImpl consoleProxyManager = new ConsoleProxyManagerImpl();
+        ReflectionTestUtils.setField(consoleProxyManager, "dataCenterDao", dataCenterDaoMock);
+        verifyScannablePoolsZoneIds(dbZoneIds, consoleProxyManager.getScannablePools());
+        dbZoneIds = Arrays.asList(2L, 3L);
+        Mockito.when(dataCenterDaoMock.listEnabledNonEdgeZoneIds()).thenReturn(dbZoneIds);
+        verifyScannablePoolsZoneIds(dbZoneIds, consoleProxyManager.getScannablePools());
+    }
 }
diff --git a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java
index f17d300596a..8f623d5a433 100755
--- a/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java
+++ b/server/src/test/java/com/cloud/storage/snapshot/SnapshotManagerTest.java
@@ -24,7 +24,11 @@ import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import org.apache.cloudstack.acl.ControlledEntity;
@@ -37,10 +41,11 @@ import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotService;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy;
 import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotStrategy.SnapshotOperation;
-import org.apache.cloudstack.snapshot.SnapshotHelper;
 import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.VolumeDataFactory;
 import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.cloudstack.framework.config.ConfigKey;
+import org.apache.cloudstack.snapshot.SnapshotHelper;
 import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.SnapshotDataStoreVO;
@@ -49,12 +54,21 @@ import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.BDDMockito;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
+import org.mockito.verification.VerificationMode;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
 
 import com.cloud.configuration.Resource.ResourceType;
+import com.cloud.dc.DataCenter;
+import com.cloud.dc.DataCenterVO;
+import com.cloud.dc.dao.DataCenterDao;
 import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.exception.ResourceAllocationException;
 import com.cloud.hypervisor.Hypervisor;
@@ -88,15 +102,6 @@ import com.cloud.vm.dao.UserVmDao;
 import com.cloud.vm.snapshot.VMSnapshot;
 import com.cloud.vm.snapshot.VMSnapshotVO;
 import com.cloud.vm.snapshot.dao.VMSnapshotDao;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.runner.RunWith;
-import org.mockito.BDDMockito;
-import org.mockito.verification.VerificationMode;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
 
 @RunWith(PowerMockRunner.class)
 @PrepareForTest(GlobalLock.class)
@@ -171,6 +176,8 @@ public class SnapshotManagerTest {
 
     @Mock
     TaggedResourceService taggedResourceServiceMock;
+    @Mock
+    DataCenterDao dataCenterDao;
 
     SnapshotPolicyVO snapshotPolicyVoInstance;
 
@@ -207,6 +214,7 @@ public class SnapshotManagerTest {
         _snapshotMgr._snapshotPolicyDao = snapshotPolicyDaoMock;
         _snapshotMgr._snapSchedMgr = snapshotSchedulerMock;
         _snapshotMgr.taggedResourceService = taggedResourceServiceMock;
+        _snapshotMgr.dataCenterDao = dataCenterDao;
 
         when(_snapshotDao.findById(anyLong())).thenReturn(snapshotMock);
         when(snapshotMock.getVolumeId()).thenReturn(TEST_VOLUME_ID);
@@ -533,4 +541,38 @@ public class SnapshotManagerTest {
           Mockito.any(DateUtil.IntervalType.class), Mockito.anyInt(), Mockito.anyBoolean(), Mockito.anyBoolean());
         Mockito.verify(_snapshotMgr, timesVerification).createTagsForSnapshotPolicy(Mockito.any(), Mockito.any());
     }
+
+    private void mockForBackupSnapshotToSecondaryZoneTest(final Boolean configValue, final DataCenter.Type dcType) {
+        try {
+            Field f = ConfigKey.class.getDeclaredField("_value");
+            f.setAccessible(true);
+            f.set(SnapshotInfo.BackupSnapshotAfterTakingSnapshot, configValue);
+        } catch (NoSuchFieldException | IllegalAccessException e) {
+            Assert.fail(String.format("Failed to override config: %s", e.getMessage()));
+        }
+        DataCenterVO dc = Mockito.mock(DataCenterVO.class);
+        Mockito.when(dc.getType()).thenReturn(dcType);
+        Mockito.when(dataCenterDao.findById(1L)).thenReturn(dc);
+    }
+    @Test
+    public void testIsBackupSnapshotToSecondaryForCoreZoneNullConfig() {
+        mockForBackupSnapshotToSecondaryZoneTest(null, DataCenter.Type.Core);
+        Assert.assertTrue(_snapshotMgr.isBackupSnapshotToSecondaryForZone(1L));
+    }
+    @Test
+    public void testIsBackupSnapshotToSecondaryForCoreZoneEnabledConfig() {
+        mockForBackupSnapshotToSecondaryZoneTest(true, DataCenter.Type.Core);
+        Assert.assertTrue(_snapshotMgr.isBackupSnapshotToSecondaryForZone(1L));
+    }
+    @Test
+    public void testIsBackupSnapshotToSecondaryForCoreZoneDisabledConfig() {
+        mockForBackupSnapshotToSecondaryZoneTest(false, DataCenter.Type.Core);
+        Assert.assertFalse(_snapshotMgr.isBackupSnapshotToSecondaryForZone(1L));
+    }
+
+    @Test
+    public void testIsBackupSnapshotToSecondaryForEdgeZone() {
+        mockForBackupSnapshotToSecondaryZoneTest(true, DataCenter.Type.Edge);
+        Assert.assertFalse(_snapshotMgr.isBackupSnapshotToSecondaryForZone(1L));
+    }
 }
diff --git a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java
index 088f4e67e8f..56f2428dd55 100644
--- a/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java
+++ b/server/src/test/java/com/cloud/vpc/MockConfigurationManagerImpl.java
@@ -506,7 +506,7 @@ public class MockConfigurationManagerImpl extends ManagerBase implements Configu
      * @see com.cloud.configuration.ConfigurationManager#createPod(long, java.lang.String, long, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
      */
     @Override
-    public HostPodVO createPod(long userId, String podName, long zoneId, String gateway, String cidr, String startIp, String endIp, String allocationState,
+    public HostPodVO createPod(long userId, String podName, DataCenter zone, String gateway, String cidr, String startIp, String endIp, String allocationState,
         boolean skipGatewayOverlapCheck) {
         // TODO Auto-generated method stub
         return null;
@@ -632,7 +632,7 @@ public class MockConfigurationManagerImpl extends ManagerBase implements Configu
     @Override
     public DataCenterVO createZone(long userId, String zoneName, String dns1, String dns2, String internalDns1, String internalDns2, String guestCidr, String domain,
         Long domainId, NetworkType zoneType, String allocationState, String networkDomain, boolean isSecurityGroupEnabled, boolean isLocalStorageEnabled, String ip6Dns1,
-        String ip6Dns2) {
+        String ip6Dns2, boolean isEdge) {
         // TODO Auto-generated method stub
         return null;
     }
diff --git a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java
index 9c9b4f56324..68ed368483b 100644
--- a/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java
+++ b/services/secondary-storage/controller/src/main/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerImpl.java
@@ -30,8 +30,6 @@ import java.util.Map;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
-import com.cloud.storage.VolumeApiService;
-import com.cloud.utils.PasswordGenerator;
 import org.apache.cloudstack.agent.lb.IndirectAgentLB;
 import org.apache.cloudstack.ca.CAManager;
 import org.apache.cloudstack.context.CallContext;
@@ -50,6 +48,8 @@ import org.apache.cloudstack.storage.datastore.db.TemplateDataStoreDao;
 import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
 import org.apache.cloudstack.utils.identity.ManagementServerNode;
 import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.log4j.Logger;
 
 import com.cloud.agent.AgentManager;
@@ -112,6 +112,7 @@ import com.cloud.service.dao.ServiceOfferingDao;
 import com.cloud.storage.ImageStoreDetailsUtil;
 import com.cloud.storage.Storage;
 import com.cloud.storage.VMTemplateVO;
+import com.cloud.storage.VolumeApiService;
 import com.cloud.storage.dao.SnapshotDao;
 import com.cloud.storage.dao.StoragePoolHostDao;
 import com.cloud.storage.dao.UploadDao;
@@ -127,6 +128,7 @@ import com.cloud.user.AccountService;
 import com.cloud.utils.DateUtil;
 import com.cloud.utils.NumbersUtil;
 import com.cloud.utils.Pair;
+import com.cloud.utils.PasswordGenerator;
 import com.cloud.utils.component.ManagerBase;
 import com.cloud.utils.db.GlobalLock;
 import com.cloud.utils.db.QueryBuilder;
@@ -150,8 +152,6 @@ import com.cloud.vm.VirtualMachineProfile;
 import com.cloud.vm.dao.SecondaryStorageVmDao;
 import com.cloud.vm.dao.UserVmDetailsDao;
 import com.cloud.vm.dao.VMInstanceDao;
-import org.apache.commons.lang3.BooleanUtils;
-import org.apache.commons.lang3.StringUtils;
 
 /**
 * Class to manage secondary storages. <br><br>
@@ -1307,15 +1307,11 @@ public class SecondaryStorageManagerImpl extends ManagerBase implements Secondar
 
     @Override
     public Long[] getScannablePools() {
-        List<DataCenterVO> zones = _dcDao.listEnabledZones();
-
-        Long[] dcIdList = new Long[zones.size()];
-        int i = 0;
-        for (DataCenterVO dc : zones) {
-            dcIdList[i++] = dc.getId();
+        List<Long> zoneIds = _dcDao.listEnabledNonEdgeZoneIds();
+        if (s_logger.isDebugEnabled()) {
+            s_logger.debug(String.format("Enabled non-edge zones available for scan: %s", StringUtils.join(zoneIds, ",")));
         }
-
-        return dcIdList;
+        return zoneIds.toArray(Long[]::new);
     }
 
     @Override
diff --git a/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerTest.java b/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerTest.java
index 98348fe2908..fe67b24cac2 100644
--- a/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerTest.java
+++ b/services/secondary-storage/controller/src/test/java/org/apache/cloudstack/secondarystorage/SecondaryStorageManagerTest.java
@@ -17,7 +17,17 @@
 
 package org.apache.cloudstack.secondarystorage;
 
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
 import org.junit.Assert;
 import org.junit.Before;
@@ -27,8 +37,8 @@ import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
-import com.cloud.dc.DataCenterVO;
 import com.cloud.dc.DataCenter.NetworkType;
+import com.cloud.dc.DataCenterVO;
 import com.cloud.dc.dao.DataCenterDao;
 import com.cloud.hypervisor.Hypervisor;
 import com.cloud.network.Networks.TrafficType;
@@ -37,16 +47,6 @@ import com.cloud.network.dao.NetworkVO;
 import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.vm.NicProfile;
 import com.cloud.vm.VirtualMachineProfile;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyLong;
-import static org.mockito.Mockito.when;
-import static org.mockito.AdditionalMatchers.not;
-import static org.mockito.Mockito.eq;
 
 public class SecondaryStorageManagerTest {
     @Mock
@@ -197,7 +197,7 @@ public class SecondaryStorageManagerTest {
     }
 
     @Test
-    public void validateVerifySshAccessOnManagementNicForSystemVm(){
+    public void validateVerifySshAccessOnManagementNicForSystemVm() {
         Hypervisor.HypervisorType[] hypervisorTypesArray = Hypervisor.HypervisorType.values();
         List<Hypervisor.HypervisorType> hypervisorTypesThatMustReturnManagementNic = new ArrayList<>(Arrays.asList(Hypervisor.HypervisorType.Hyperv));
 
@@ -218,4 +218,22 @@ public class SecondaryStorageManagerTest {
             Assert.assertEquals(expectedResult, result);
         }
     }
+
+    private void verifyScannablePoolsZoneIds(List<Long> expected, Long[] result) {
+        Assert.assertNotNull(result);
+        Assert.assertEquals(expected.size(), result.length);
+        for (int i = 0; i < expected.size(); ++i) {
+            Assert.assertEquals(expected.get(i), result[i]);
+        }
+    }
+
+    @Test
+    public void testGetScannablePools() {
+        List<Long> dbZoneIds = new ArrayList<>();
+        Mockito.when(_dcDao.listEnabledNonEdgeZoneIds()).thenReturn(dbZoneIds);
+        verifyScannablePoolsZoneIds(dbZoneIds, _ssMgr.getScannablePools());
+        dbZoneIds = Arrays.asList(1L, 2L, 3L);
+        Mockito.when(_dcDao.listEnabledNonEdgeZoneIds()).thenReturn(dbZoneIds);
+        verifyScannablePoolsZoneIds(dbZoneIds, _ssMgr.getScannablePools());
+    }
 }
diff --git a/test/integration/component/test_edgezone_supportedoperations.py b/test/integration/component/test_edgezone_supportedoperations.py
new file mode 100644
index 00000000000..c3f12686523
--- /dev/null
+++ b/test/integration/component/test_edgezone_supportedoperations.py
@@ -0,0 +1,133 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+""" BVT tests for supported VM and network operations on an Edge zone
+"""
+# Import Local Modules
+from marvin.cloudstackTestCase import cloudstackTestCase
+from marvin.cloudstackAPI import (attachVolume,
+                                  detachVolume,
+                                  deleteVolume,
+                                  attachIso,
+                                  detachIso,
+                                  deleteIso,
+                                  startVirtualMachine,
+                                  stopVirtualMachine,
+                                  migrateVirtualMachineWithVolume)
+from marvin.lib.utils import (cleanup_resources)
+from marvin.lib.base import (Account,
+                             Host,
+                             Pod,
+                             StoragePool,
+                             ServiceOffering,
+                             DiskOffering,
+                             VirtualMachine,
+                             Iso,
+                             Volume)
+from marvin.lib.common import (get_domain,
+                               get_zone,
+                               get_template)
+from marvin.lib.decoratorGenerators import skipTestIf
+from marvin.codes import FAILED, PASS
+from nose.plugins.attrib import attr
+# Import System modules
+import time
+
+_multiprocess_shared_ = True
+
+
+class TestEdgeZoneSupportedOperations(cloudstackTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        testClient = super(TestVMMigration, cls).getClsTestClient()
+        cls.apiclient = testClient.getApiClient()
+        cls.services = testClient.getParsedTestDataConfig()
+        cls.domain = get_domain(cls.apiclient)
+        cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests())
+        cls.services['mode'] = cls.zone.networktype
+        cls.hypervisor = cls.testClient.getHypervisorInfo()
+        cls.cleanup = []
+        cls.testsNotSupported = False
+        if cls.zone.type != 'Edge':
+            cls.testsNotSupported = True
+            return
+        cls.services["test_templates"]["kvm"]["directdownload"] = "true"
+        cls.template = Template.register(cls.apiclient, cls.services["test_templates"]["kvm"],
+                          zoneid=cls.zone.id, hypervisor=cls.hypervisor)
+        cls.cleanup.append(cls.template)
+        cls.services["virtual_machine"]["zoneid"] = cls.zone.id
+        cls.services["virtual_machine"]["template"] = cls.template.id
+        cls.services["virtual_machine"]["hypervisor"] = cls.hypervisor
+            cls.services["service_offerings"]["tiny"]["storagetype"] = "local"
+        cls.service_offering = ServiceOffering.create(
+            cls.apiclient,
+            cls.services["service_offerings"]["tiny"]
+        )
+        cls.cleanup.append(cls.service_offering)
+        cls.l2_network_offering = NetworkOffering.create(
+            cls.apiclient,
+            cls.services["l2-l2_network_offering"],
+        )
+        cls.cleanup.append(cls.l2_network_offering)
+        cls.l2_network_offering.update(cls.apiclient, state='Enabled')
+        cls.domain = get_domain(cls.apiclient)
+        cls.account = Account.create(
+            cls.apiclient,
+            cls.services["account"],
+            admin=False,
+            domainid=cls.domain.id
+        )
+        cls.cleanup.append(cls.account)
+
+    @classmethod
+    def tearDownClass(cls):
+        super(TestMetrics, cls).tearDownClass()
+
+    def setUp(self):
+        self.userapiclient = self.testClient.getUserApiClient(
+            UserName=self.account.name,
+            DomainName=self.account.domain
+        )
+        self.cleanup = []
+
+    def tearDown(self):
+        super(TestMetrics, self).tearDown()
+
+    @skipTestIf("testsNotSupported")
+    @attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
+    def test_01_deploy_vm_l2network(self):
+        """Test to deploy VM in a L2 network
+        """
+        network = Network.create(
+            self.apiclient,
+            self.services["l2-network"],
+            zoneid=self.zone.id,
+            networkofferingid=self.l2_network_offering.id
+        )
+        self.cleanup.append(network)
+        vm = VirtualMachine.create(
+            self.apiclient,
+            self.services["virtual_machine"],
+            serviceofferingid=self.service_offering.id
+            networkids=network.id
+        )
+        self.cleanup.append(vm)
+        self.assertEqual(
+            vm.state,
+            "Running",
+            "Check VM deployed in edge zone with a L2 network is running"
+        )
diff --git a/test/integration/component/test_interpod_migration.py b/test/integration/component/test_interpod_migration.py
index b8b2e975975..e00e0d7e310 100644
--- a/test/integration/component/test_interpod_migration.py
+++ b/test/integration/component/test_interpod_migration.py
@@ -14,7 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-""" BVT tests for Virtual Machine Life Cycle
+""" BVT tests for Virtual Machine migration across pods
 """
 # Import Local Modules
 from marvin.cloudstackTestCase import cloudstackTestCase
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index a4d0d0b6197..5f17374e17c 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -451,6 +451,8 @@
 "label.copy": "Copy",
 "label.copy.clipboard": "Copy to clipboard",
 "label.copyid": "Copy ID",
+"label.core": "Core",
+"label.core.zone.type": "Core zone type",
 "label.counter": "Counter",
 "label.counter.name": "Name of the counter for which the policy will be evaluated",
 "label.cpu": "CPU",
@@ -666,6 +668,15 @@
 "label.duration.7days": "7 days",
 "label.dynamicscalingenabled": "Dynamic scaling enabled",
 "label.dynamicscalingenabled.tooltip": "VM can dynamically scale only when dynamic scaling is enabled on template, service offering and global setting.",
+<<<<<<< HEAD
+"label.diskofferingstrictness": "Disk offering strictness",
+"label.disksizestrictness": "Disk size strictness",
+"label.computeonly.offering": "Compute only disk offering",
+"label.computeonly.offering.tooltip": "Option to specify root disk related information in the compute offering or to directly link a disk offering to the compute offering",
+"label.edge": "Edge",
+"label.edge.zone": "Edge Zone",
+=======
+>>>>>>> main
 "label.edit": "Edit",
 "label.edit.acl.list": "Edit ACL list",
 "label.edit.acl.rule": "Edit ACL rule",
@@ -2260,6 +2271,8 @@
 "message.deployasis": "Selected template is Deploy As-Is i.e., the VM is deployed by importing an OVA with vApps directly into vCenter. Root disk(s) resize is allowed only on stopped VMs for such templates.",
 "message.desc.advanced.zone": "This is recommended and allows more sophisticated network topologies. This network model provides the most flexibility in defining guest networks and providing custom network offerings such as firewall, VPN, or load balancer support.",
 "message.desc.basic.zone": "Provide a single network where each VM instance is assigned an IP directly from the network. Guest isolation can be provided through layer-3 means such as security groups (IP address source filtering).",
+"message.desc.core.zone": "Core Zones are intended for Datacenter based deployments and allow the full range of networking and other functionality in Apache CloudStack. Core zones have a number of prerequisites and rely on the presence of shared storage and helper VMs.",
+"message.desc.edge.zone": "Edge Zones are lightweight zones, designed for deploying in edge computing scenarios. They are limited in functionality but have far fewer prerequisites than core zones.<br><br>Please refer to the Apache CloudStack documentation for more information on Zone Types<br><a href='http://docs.cloudstack.apache.org/en/latest/installguide/configuration.html#adding-a-zone'>http://docs.cloudstack.apache.org/en/latest/installguide/configuration.html#adding-a-zone</a>",
 "message.desc.cluster": "Each pod must contain one or more clusters. We will add the first cluster now. A cluster provides a way to group hosts. The hosts in a cluster all have identical hardware, run the same hypervisor, are on the same subnet, and access the same shared storage. Each cluster consists of one or more hosts and one or more primary storage servers.",
 "message.desc.create.ssh.key.pair": "Please fill in the following data to create or register a ssh key pair.<br><br>(1) If public key is set, CloudStack will register the public key. You can use it through your private key.<br><br>(2) If public key is not set, CloudStack will create a new SSH key pair. In this case, please copy and save the private key. CloudStack will not keep it.<br>",
 "message.desc.created.ssh.key.pair": "Created a SSH key pair.",
@@ -2271,6 +2284,8 @@
 "message.desc.register.user.data": "Please fill in the following data to register a user data.",
 "message.desc.registered.user.data": "Registered a User Data.",
 "message.desc.zone": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. A zone consists of one or more pods (each of which contains hosts and primary storage servers) and a secondary storage server which is shared by all pods in the zone.",
+"message.desc.zone.edge": "A zone is the largest organizational unit in CloudStack, and it typically corresponds to a single datacenter. Zones provide physical isolation and redundancy. An edge zone consists of one or more hosts (each of which provides local storage as primary storage servers). Only shared and L2 networks can be deployed in such zones and functionalities that require secondary storages are not supported.",
+"message.zone.edge.local.storage": "Local storage will be used by default for user VMs and virtual routers",
 "message.detach.disk": "Are you sure you want to detach this disk?",
 "message.detach.iso.confirm": "Please confirm that you want to detach the ISO from this virtual instance.",
 "message.disable.account": "Please confirm that you want to disable this account. By disabling the account, all users for this account will no longer have access to their cloud resources. All running virtual machines will be immediately shut down.",
diff --git a/ui/src/components/view/InfoCard.vue b/ui/src/components/view/InfoCard.vue
index f7eb3a529db..43d24a8f121 100644
--- a/ui/src/components/view/InfoCard.vue
+++ b/ui/src/components/view/InfoCard.vue
@@ -631,7 +631,7 @@
         <a-divider/>
         <div v-for="item in $route.meta.related" :key="item.path">
           <router-link
-            v-if="$router.resolve('/' + item.name).matched[0].redirect !== '/exception/404'"
+            v-if="(item.show === undefined || item.show(resource)) && $router.resolve('/' + item.name).matched[0].redirect !== '/exception/404'"
             :to="{ name: item.name, query: getRouterQuery(item) }">
             <a-button style="margin-right: 10px">
               <template #icon>
diff --git a/ui/src/config/section/infra/zones.js b/ui/src/config/section/infra/zones.js
index d6208586092..cc9fa1697fe 100644
--- a/ui/src/config/section/infra/zones.js
+++ b/ui/src/config/section/infra/zones.js
@@ -24,7 +24,7 @@ export default {
   icon: 'global-outlined',
   permission: ['listZonesMetrics'],
   columns: () => {
-    const fields = ['name', 'allocationstate', 'networktype', 'clusters']
+    const fields = ['name', 'allocationstate', 'type', 'networktype', 'clusters']
     const metricsFields = ['cpuused', 'cpumaxdeviation', 'cpuallocated', 'cputotal', 'memoryused', 'memorymaxdeviation', 'memoryallocated', 'memorytotal']
     if (store.getters.metrics) {
       fields.push(...metricsFields)
@@ -32,15 +32,21 @@ export default {
     fields.push('order')
     return fields
   },
-  details: ['name', 'id', 'allocationstate', 'networktype', 'guestcidraddress', 'localstorageenabled', 'securitygroupsenabled', 'dns1', 'dns2', 'internaldns1', 'internaldns2'],
+  details: ['name', 'id', 'allocationstate', 'type', 'networktype', 'guestcidraddress', 'localstorageenabled', 'securitygroupsenabled', 'dns1', 'dns2', 'internaldns1', 'internaldns2'],
   related: [{
     name: 'pod',
     title: 'label.pods',
-    param: 'zoneid'
+    param: 'zoneid',
+    show: (record) => {
+      return record.type !== 'Edge'
+    }
   }, {
     name: 'cluster',
     title: 'label.clusters',
-    param: 'zoneid'
+    param: 'zoneid',
+    show: (record) => {
+      return record.type !== 'Edge'
+    }
   }, {
     name: 'host',
     title: 'label.hosts',
@@ -52,7 +58,10 @@ export default {
   }, {
     name: 'imagestore',
     title: 'label.secondary.storage',
-    param: 'zoneid'
+    param: 'zoneid',
+    show: (record) => {
+      return record.type !== 'Edge'
+    }
   }],
   resourceType: 'Zone',
   tabs: [{
@@ -63,7 +72,8 @@ export default {
     component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/PhysicalNetworksTab.vue')))
   }, {
     name: 'system.vms',
-    component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/SystemVmsTab.vue')))
+    component: shallowRef(defineAsyncComponent(() => import('@/views/infra/zone/SystemVmsTab.vue'))),
+    show: (record) => { return record.isEdge !== true }
   }, {
     name: 'resources',
     component: shallowRef(defineAsyncComponent(() => import('@/views/infra/Resources.vue')))
diff --git a/ui/src/views/compute/CreateKubernetesCluster.vue b/ui/src/views/compute/CreateKubernetesCluster.vue
index 3bf0fd6df98..9ca7b0aa7c8 100644
--- a/ui/src/views/compute/CreateKubernetesCluster.vue
+++ b/ui/src/views/compute/CreateKubernetesCluster.vue
@@ -343,6 +343,7 @@ export default {
         const listZones = json.listzonesresponse.zone
         if (listZones) {
           this.zones = this.zones.concat(listZones)
+          this.zones = this.zones.filter(zone => zone.type !== 'Edge')
         }
       }).finally(() => {
         this.zoneLoading = false
diff --git a/ui/src/views/image/AddKubernetesSupportedVersion.vue b/ui/src/views/image/AddKubernetesSupportedVersion.vue
index f72bd64df9f..0647dc84d44 100644
--- a/ui/src/views/image/AddKubernetesSupportedVersion.vue
+++ b/ui/src/views/image/AddKubernetesSupportedVersion.vue
@@ -195,6 +195,7 @@ export default {
         const listZones = json.listzonesresponse.zone
         if (listZones) {
           this.zones = this.zones.concat(listZones)
+          this.zones = this.zones.filter(zone => zone.type !== 'Edge')
         }
       }).finally(() => {
         this.zoneLoading = false
diff --git a/ui/src/views/image/RegisterOrUploadIso.vue b/ui/src/views/image/RegisterOrUploadIso.vue
index 75111d54125..cf1435a6034 100644
--- a/ui/src/views/image/RegisterOrUploadIso.vue
+++ b/ui/src/views/image/RegisterOrUploadIso.vue
@@ -283,6 +283,7 @@ export default {
         const listZones = json.listzonesresponse.zone
         if (listZones) {
           this.zones = this.zones.concat(listZones)
+          this.zones = this.zones.filter(zone => zone.type !== 'Edge')
         }
       }).finally(() => {
         this.zoneLoading = false
diff --git a/ui/src/views/image/RegisterOrUploadTemplate.vue b/ui/src/views/image/RegisterOrUploadTemplate.vue
index 2c06ebe997a..e4d4687c326 100644
--- a/ui/src/views/image/RegisterOrUploadTemplate.vue
+++ b/ui/src/views/image/RegisterOrUploadTemplate.vue
@@ -107,7 +107,7 @@
               @change="handlerSelectZone"
               :placeholder="apiParams.zoneid.description"
               :loading="zones.loading">
-              <a-select-option :value="zone.id" v-for="zone in zones.opts" :key="zone.id" :label="zone.name || zone.description">
+              <a-select-option :value="zone.id" v-for="zone in filteredZones" :key="zone.id" :label="zone.name || zone.description">
                 <span>
                   <resource-icon v-if="zone.icon" :image="zone.icon.base64image" size="1x" style="margin-right: 5px"/>
                   <global-outlined v-else style="margin-right: 5px" />
@@ -421,6 +421,13 @@ export default {
   computed: {
     isAdminRole () {
       return this.$store.getters.userInfo.roletype === 'Admin'
+    },
+    filteredZones () {
+      let zoneList = this.zones.opts
+      if (zoneList && zoneList.length > 0 && this.currentForm === 'Upload') {
+        zoneList = zoneList.filter(zone => zone.type !== 'Edge')
+      }
+      return zoneList
     }
   },
   methods: {
@@ -532,9 +539,14 @@ export default {
         listZones = listZones.concat(listZonesResponse)
         this.zones.opts = listZones
       }).finally(() => {
-        this.form.zoneid = (this.zones.opts && this.zones.opts[1]) ? this.zones.opts[1].id : ''
+        this.form.zoneid = (this.filteredZones && this.filteredZones[1]) ? this.filteredZones[1].id : ''
+        if (!this.form.zoneid) {
+          this.form.zoneid = (this.filteredZones && this.filteredZones[0] && this.filteredZones[0].id !== this.$t('label.all.zone')) ? this.filteredZones[0].id : ''
+        }
         this.zones.loading = false
-        this.fetchHyperVisor({ zoneid: this.form.zoneid })
+        if (this.form.zoneid) {
+          this.fetchHyperVisor({ zoneid: this.form.zoneid })
+        }
       })
     },
     fetchHyperVisor (params) {
@@ -548,7 +560,7 @@ export default {
         }
         if (this.currentForm !== 'Upload') {
           listhyperVisors.push({
-            name: 'Any'
+            name: 'Simulator'
           })
         }
         this.hyperVisor.opts = listhyperVisors
diff --git a/ui/src/views/infra/zone/ZoneWizard.vue b/ui/src/views/infra/zone/ZoneWizard.vue
index a3bc01deed8..710a1d18835 100644
--- a/ui/src/views/infra/zone/ZoneWizard.vue
+++ b/ui/src/views/infra/zone/ZoneWizard.vue
@@ -23,7 +23,7 @@
       size="small"
       :current="currentStep">
       <a-step
-        v-for="(item, index) in steps"
+        v-for="(item, index) in zoneSteps"
         :key="item.title"
         :title="$t(item.title)"
         :ref="`step${index}`">
@@ -31,13 +31,21 @@
     </a-steps>
     <div>
       <zone-wizard-zone-type-step
-        v-if="currentStep === 0"
+        v-if="zoneSteps[currentStep].name === 'type'"
         @nextPressed="nextPressed"
         @fieldsChanged="onFieldsChanged"
         :prefillContent="zoneConfig"
       />
+      <zone-wizard-core-zone-type-step
+        v-else-if="zoneSteps[currentStep].name === 'coreType'"
+        @nextPressed="nextPressed"
+        @backPressed="backPressed"
+        @fieldsChanged="onFieldsChanged"
+        :isFixError="stepFixError"
+        :prefillContent="zoneConfig"
+      />
       <zone-wizard-zone-details-step
-        v-else-if="currentStep === 1"
+        v-else-if="zoneSteps[currentStep].name === 'details'"
         @nextPressed="nextPressed"
         @backPressed="backPressed"
         @fieldsChanged="onFieldsChanged"
@@ -46,7 +54,7 @@
         :prefillContent="zoneConfig"
       />
       <zone-wizard-network-setup-step
-        v-else-if="currentStep === 2"
+        v-else-if="zoneSteps[currentStep].name === 'network'"
         @nextPressed="nextPressed"
         @backPressed="backPressed"
         @fieldsChanged="onFieldsChanged"
@@ -56,7 +64,7 @@
         :prefillContent="zoneConfig"
       />
       <zone-wizard-add-resources
-        v-else-if="currentStep === 3"
+        v-else-if="zoneSteps[currentStep].name === 'resources'"
         @nextPressed="nextPressed"
         @backPressed="backPressed"
         @fieldsChanged="onFieldsChanged"
@@ -83,6 +91,7 @@
 <script>
 import { mixinDevice } from '@/utils/mixin.js'
 import ZoneWizardZoneTypeStep from '@views/infra/zone/ZoneWizardZoneTypeStep'
+import ZoneWizardCoreZoneTypeStep from '@views/infra/zone/ZoneWizardCoreZoneTypeStep'
 import ZoneWizardZoneDetailsStep from '@views/infra/zone/ZoneWizardZoneDetailsStep'
 import ZoneWizardNetworkSetupStep from '@views/infra/zone/ZoneWizardNetworkSetupStep'
 import ZoneWizardAddResources from '@views/infra/zone/ZoneWizardAddResources'
@@ -91,6 +100,7 @@ import ZoneWizardLaunchZone from '@views/infra/zone/ZoneWizardLaunchZone'
 export default {
   components: {
     ZoneWizardZoneTypeStep,
+    ZoneWizardCoreZoneTypeStep,
     ZoneWizardZoneDetailsStep,
     ZoneWizardNetworkSetupStep,
     ZoneWizardAddResources,
@@ -104,32 +114,44 @@ export default {
       launchZone: false,
       launchData: {},
       stepChild: '',
+      coreZoneTypeStep: {
+        name: 'coreType',
+        title: 'label.core.zone.type',
+        step: [],
+        description: this.$t('message.select.zone.description'),
+        hint: this.$t('message.select.zone.hint')
+      },
       steps: [
         {
+          name: 'type',
           title: 'label.zone.type',
           step: [],
           description: this.$t('message.select.zone.description'),
           hint: this.$t('message.select.zone.hint')
         },
         {
+          name: 'details',
           title: 'label.zone.details',
           step: ['stepAddZone', 'dedicateZone'],
           description: this.$t('message.zone.detail.description'),
           hint: this.$t('message.zone.detail.hint')
         },
         {
+          name: 'network',
           title: 'label.network',
           step: ['physicalNetwork', 'netscaler', 'pod', 'guestTraffic', 'storageTraffic', 'publicTraffic'],
           description: this.$t('message.network.description'),
           hint: this.$t('message.network.hint')
         },
         {
+          name: 'resources',
           title: 'label.add.resources',
           step: ['clusterResource', 'hostResource', 'primaryResource', 'secondaryResource'],
           description: this.$t('message.add.resource.description'),
           hint: this.$t('message.add.resource.hint')
         },
         {
+          name: 'launch',
           title: 'label.launch',
           step: ['launchZone'],
           description: this.$t('message.launch.zone.description'),
@@ -139,6 +161,15 @@ export default {
       zoneConfig: {}
     }
   },
+  computed: {
+    zoneSteps () {
+      var steps = [...this.steps]
+      if (this.zoneConfig.zoneSuperType !== 'Edge') {
+        steps.splice(1, 0, this.coreZoneTypeStep)
+      }
+      return steps
+    }
+  },
   methods: {
     nextPressed () {
       this.currentStep++
diff --git a/ui/src/views/infra/zone/ZoneWizardAddResources.vue b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
index 9cee7732a5d..4c919af94ed 100644
--- a/ui/src/views/infra/zone/ZoneWizardAddResources.vue
+++ b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
@@ -18,6 +18,7 @@
 <template>
   <div style="width: auto;">
     <a-steps
+      v-if="steps.length > 1"
       ref="resourceStep"
       progressDot
       :current="currentStep"
@@ -54,7 +55,7 @@
         :isFixError="isFixError"
       />
       <static-inputs-form
-        v-if="(!localstorageenabled || !localstorageenabledforsystemvm) && checkVisibleResource('primaryResource')"
+        v-if="!isEdgeZone && (!localstorageenabled || !localstorageenabledforsystemvm) && checkVisibleResource('primaryResource')"
         @nextPressed="nextPressed"
         @backPressed="handleBack"
         @fieldsChanged="fieldsChanged"
@@ -65,7 +66,7 @@
         :isFixError="isFixError"
       />
       <static-inputs-form
-        v-if="checkVisibleResource('secondaryResource')"
+        v-if="!isEdgeZone && checkVisibleResource('secondaryResource')"
         @nextPressed="nextPressed"
         @backPressed="handleBack"
         @fieldsChanged="fieldsChanged"
@@ -76,7 +77,7 @@
         :isFixError="isFixError"
       />
     </div>
-    <div v-else>
+    <div v-else-if="!isEdgeZone">
       <static-inputs-form
         v-if="checkVisibleResource('primaryResource')"
         @nextPressed="nextPressed"
@@ -133,6 +134,9 @@ export default {
     zoneType () {
       return this.prefillContent?.zoneType || null
     },
+    isAdvancedZone () {
+      return this.zoneType === 'Advanced'
+    },
     hypervisor () {
       return this.prefillContent?.hypervisor || null
     },
@@ -142,14 +146,19 @@ export default {
     localstorageenabledforsystemvm () {
       return this.prefillContent?.localstorageenabledforsystemvm || false
     },
+    isEdgeZone () {
+      return this.prefillContent?.zoneSuperType === 'Edge' || false
+    },
     steps () {
       const steps = []
       const hypervisor = this.prefillContent.hypervisor ? this.prefillContent.hypervisor : null
-      steps.push({
-        title: 'label.cluster',
-        fromKey: 'clusterResource',
-        description: 'message.desc.cluster'
-      })
+      if (!this.isEdgeZone) {
+        steps.push({
+          title: 'label.cluster',
+          fromKey: 'clusterResource',
+          description: 'message.desc.cluster'
+        })
+      }
       if (hypervisor !== 'VMware') {
         steps.push({
           title: 'label.host',
@@ -157,18 +166,20 @@ export default {
           description: 'message.desc.host'
         })
       }
-      if (!this.localstorageenabled || !this.localstorageenabledforsystemvm) {
+      if (!this.isEdgeZone) {
+        if (!this.localstorageenabled || !this.localstorageenabledforsystemvm) {
+          steps.push({
+            title: 'label.primary.storage',
+            fromKey: 'primaryResource',
+            description: 'message.desc.primary.storage'
+          })
+        }
         steps.push({
-          title: 'label.primary.storage',
-          fromKey: 'primaryResource',
-          description: 'message.desc.primary.storage'
+          title: 'label.secondary.storage',
+          fromKey: 'secondaryResource',
+          description: 'message.desc.secondary.storage'
         })
       }
-      steps.push({
-        title: 'label.secondary.storage',
-        fromKey: 'secondaryResource',
-        description: 'message.desc.secondary.storage'
-      })
 
       return steps
     },
diff --git a/ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue b/ui/src/views/infra/zone/ZoneWizardCoreZoneTypeStep.vue
similarity index 95%
copy from ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue
copy to ui/src/views/infra/zone/ZoneWizardCoreZoneTypeStep.vue
index 430ea688c37..6473c6a356e 100644
--- a/ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue
+++ b/ui/src/views/infra/zone/ZoneWizardCoreZoneTypeStep.vue
@@ -75,6 +75,12 @@
       </a-form-item>
     </a-form>
     <div class="form-action">
+      <a-button
+        @click="handleBack"
+        class="button-back"
+        v-if="!isFixError">
+        {{ $t('label.previous') }}
+      </a-button>
       <a-button ref="submit" type="primary" @click="handleSubmit" class="button-next">
         {{ $t('label.next') }}
       </a-button>
@@ -92,6 +98,10 @@ export default {
       default: function () {
         return {}
       }
+    },
+    isFixError: {
+      type: Boolean,
+      default: false
     }
   },
   data: () => ({
@@ -141,6 +151,9 @@ export default {
       })
       this.formModel = toRaw(this.form)
     },
+    handleBack () {
+      this.$emit('backPressed')
+    },
     handleSubmit () {
       this.formRef.value.validate().then(() => {
         this.$emit('nextPressed')
diff --git a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
index 95dc10954ac..2fce9272060 100644
--- a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
+++ b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
@@ -161,6 +161,9 @@ export default {
     sgEnabled () {
       return this.prefillContent?.securityGroupsEnabled || false
     },
+    isEdgeZone () {
+      return this.prefillContent?.zoneSuperType === 'Edge' || false
+    },
     havingNetscaler () {
       return this.prefillContent?.networkOfferingSelected?.havingNetscaler || false
     },
@@ -189,7 +192,8 @@ export default {
         this.stepData.tasks = []
         this.stepData.stepMove = this.stepData.stepMove.filter(item => item.indexOf('createStorageNetworkIpRange') === -1)
       }
-      this.handleSubmit()
+      console.log('step-data', this.stepData)
+      // this.handleSubmit()
     }
   },
   methods: {
@@ -333,6 +337,7 @@ export default {
       params.internaldns1 = this.prefillContent?.internalDns1 || null
       params.internaldns2 = this.prefillContent?.internalDns2 || null
       params.domain = this.prefillContent?.networkDomain || null
+      params.isedge = this.prefillContent?.zoneSuperType === 'Edge' || false
 
       try {
         if (!this.stepData.stepMove.includes('createZone')) {
@@ -617,11 +622,11 @@ export default {
 
           try {
             // Advanced SG-disabled zone
-            if (!this.sgEnabled) {
+            if (!this.sgEnabled && !this.isEdgeZone) {
               // ***** VPC Virtual Router ***** (begin) *****
               await this.configVpcVirtualRouter(physicalNetwork)
               // ***** VPC Virtual Router ***** (end) *****
-            } else {
+            } else if (this.sgEnabled && !this.isEdgeZone) {
               this.stepData.physicalNetworkReturned = physicalNetwork
               await this.stepEnableSecurityGroupProvider()
             }
@@ -643,7 +648,7 @@ export default {
       }
     },
     async configOvs (physicalNetwork) {
-      if (this.stepData.stepMove.includes('configOvs' + physicalNetwork.id)) {
+      if (this.isEdgeZone || this.stepData.stepMove.includes('configOvs' + physicalNetwork.id)) {
         return
       }
 
@@ -663,7 +668,7 @@ export default {
       this.stepData.stepMove.push('configOvs' + physicalNetwork.id)
     },
     async configInternalLBVM (physicalNetwork) {
-      if (this.stepData.stepMove.includes('configInternalLBVM' + physicalNetwork.id)) {
+      if (this.isEdgeZone || this.stepData.stepMove.includes('configInternalLBVM' + physicalNetwork.id)) {
         return
       }
 
@@ -816,7 +821,7 @@ export default {
 
       const params = {}
       params.zoneId = this.stepData.zoneReturned.id
-      params.name = this.prefillContent?.podName || null
+      params.name = this.prefillContent?.podName || this.stepData.zoneReturned.type === 'Edge' ? 'Pod-' + this.stepData.zoneReturned.name : null
       params.gateway = this.prefillContent?.podReservedGateway || null
       params.netmask = this.prefillContent?.podReservedNetmask || null
       params.startIp = this.prefillContent?.podReservedStartIp || null
@@ -869,7 +874,7 @@ export default {
       if (
         (this.isBasicZone &&
           (this.havingSG && this.havingEIP && this.havingELB)) ||
-        (this.isAdvancedZone && !this.sgEnabled)) {
+        (this.isAdvancedZone && !this.sgEnabled && !this.isEdgeZone)) {
         this.setStepStatus(STATUS_FINISH)
         this.currentStep++
         this.addStep('message.configuring.public.traffic', 'publicTraffic')
@@ -1141,7 +1146,7 @@ export default {
       }
       params.clustertype = clusterType
       params.podId = this.stepData.podReturned.id
-      let clusterName = this.prefillContent.clusterName
+      let clusterName = this.prefillContent.clusterName || this.stepData.zoneReturned.type === 'Edge' ? 'Cluster-' + this.stepData.zoneReturned.name : null
 
       if (hypervisor === 'VMware') {
         params.username = this.prefillContent?.vCenterUsername || null
diff --git a/ui/src/views/infra/zone/ZoneWizardNetworkSetupStep.vue b/ui/src/views/infra/zone/ZoneWizardNetworkSetupStep.vue
index 1a91a43a039..06ffb516993 100644
--- a/ui/src/views/infra/zone/ZoneWizardNetworkSetupStep.vue
+++ b/ui/src/views/infra/zone/ZoneWizardNetworkSetupStep.vue
@@ -68,7 +68,7 @@
       @backPressed="handleBack"
       @fieldsChanged="fieldsChanged"
       @submitLaunchZone="submitLaunchZone"
-      :fields="podFields"
+      :fields="filteredPodFields"
       :prefillContent="prefillContent"
       :description="podSetupDescription"
       :isFixError="isFixError"
@@ -151,12 +151,18 @@ export default {
     zoneType () {
       return this.prefillContent?.zoneType || null
     },
+    isAdvancedZone () {
+      return this.zoneType === 'Advanced'
+    },
     sgEnabled () {
       return this.prefillContent?.securityGroupsEnabled || false
     },
     havingNetscaler () {
       return this.prefillContent?.networkOfferingSelected?.havingNetscaler || false
     },
+    isEdgeZone () {
+      return this.prefillContent?.zoneSuperType === 'Edge' || false
+    },
     guestTrafficRangeMode () {
       return this.zoneType === 'Basic' ||
         (this.zoneType === 'Advanced' && this.sgEnabled)
@@ -311,6 +317,14 @@ export default {
         })
       }
 
+      return fields
+    },
+    filteredPodFields () {
+      var fields = [...this.podFields]
+      if (this.isEdgeZone) {
+        fields = fields.filter(x => !['podReservedGateway', 'podReservedNetmask', 'podReservedStartIp', 'podReservedStopIp'].includes(x.key))
+        return fields
+      }
       return fields
     }
   },
@@ -363,7 +377,7 @@ export default {
           title: 'label.end.reserved.system.ip',
           key: 'podReservedStopIp',
           placeHolder: 'message.installwizard.tooltip.addpod.reservedsystemendip',
-          required: false,
+          required: true,
           ipV4: true,
           message: 'message.error.ipv4.address'
         }
@@ -449,6 +463,7 @@ export default {
     },
     filteredSteps () {
       return this.allSteps.filter(step => {
+        if (step.formKey === 'pod' && this.isEdgeZone) return false
         if (!step.trafficType) return true
         if (this.physicalNetworks) {
           let neededTraffic = false
diff --git a/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue b/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue
index 5be870c7964..f5a4d580ec3 100644
--- a/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue
+++ b/ui/src/views/infra/zone/ZoneWizardPhysicalNetworkSetupStep.vue
@@ -305,6 +305,9 @@ export default {
     securityGroupsEnabled () {
       return this.isAdvancedZone && (this.prefillContent?.securityGroupsEnabled || false)
     },
+    isEdgeZone () {
+      return this.prefillContent?.zoneSuperType === 'Edge' || false
+    },
     networkOfferingSelected () {
       return this.prefillContent.networkOfferingSelected
     },
@@ -312,11 +315,17 @@ export default {
       if (!this.isAdvancedZone) { // Basic zone
         return (this.networkOfferingSelected && (this.networkOfferingSelected.havingEIP || this.networkOfferingSelected.havingELB))
       } else {
-        return !this.securityGroupsEnabled
+        return !this.securityGroupsEnabled && !this.isEdgeZone
       }
     },
+    needsManagementTraffic () {
+      return !this.isEdgeZone
+    },
     requiredTrafficTypes () {
-      const traffics = ['management', 'guest']
+      const traffics = ['guest']
+      if (this.needsManagementTraffic) {
+        traffics.push('management')
+      }
       if (this.needsPublicTraffic) {
         traffics.push('public')
       }
diff --git a/ui/src/views/infra/zone/ZoneWizardZoneDetailsStep.vue b/ui/src/views/infra/zone/ZoneWizardZoneDetailsStep.vue
index 21dc0058a03..42e30a72376 100644
--- a/ui/src/views/infra/zone/ZoneWizardZoneDetailsStep.vue
+++ b/ui/src/views/infra/zone/ZoneWizardZoneDetailsStep.vue
@@ -39,74 +39,76 @@
           v-focus="true"
         />
       </a-form-item>
-      <a-form-item
-        name="ipv4Dns1"
-        ref="ipv4Dns1"
-        :label="$t('label.ipv4.dns1')"
-        v-bind="formItemLayout"
-        has-feedback>
-        <a-input v-model:value="form.ipv4Dns1" />
-      </a-form-item>
-      <a-form-item
-        name="ipv4Dns2"
-        ref="ipv4Dns2"
-        :label="$t('label.ipv4.dns2')"
-        v-bind="formItemLayout"
-        has-feedback>
-        <a-input v-model:value="form.ipv4Dns2" />
-      </a-form-item>
-      <a-form-item
-        name="ipv6Dns1"
-        ref="ipv6Dns1"
-        :label="$t('label.ipv6.dns1')"
-        v-bind="formItemLayout"
-        v-if="isAdvancedZone"
-        has-feedback>
-        <a-input v-model:value="form.ipv6Dns1" />
-      </a-form-item>
-      <a-form-item
-        name="ipv6Dns2"
-        ref="ipv6Dns2"
-        :label="$t('label.ipv6.dns2')"
-        v-bind="formItemLayout"
-        v-if="isAdvancedZone"
-        has-feedback>
-        <a-input v-model:value="form.ipv6Dns2" />
-      </a-form-item>
-      <a-form-item
-        name="ipv6Cidr"
-        ref="ipv6Cidr"
-        :label="$t('label.ip6cidr')"
-        v-bind="formItemLayout"
-        v-if="isAdvancedZone && securityGroupsEnabled"
-        has-feedback>
-        <a-input v-model:value="form.ipv6Cidr" />
-      </a-form-item>
-      <a-form-item
-        name="ip6gateway"
-        ref="ip6gateway"
-        :label="$t('label.ip6gateway')"
-        v-bind="formItemLayout"
-        v-if="isAdvancedZone && securityGroupsEnabled"
-        has-feedback>
-        <a-input v-model:value="form.ip6gateway" />
-      </a-form-item>
-      <a-form-item
-        name="internalDns1"
-        ref="internalDns1"
-        :label="$t('label.internal.dns.1')"
-        v-bind="formItemLayout"
-        has-feedback>
-        <a-input v-model:value="form.internalDns1" />
-      </a-form-item>
-      <a-form-item
-        name="internalDns2"
-        ref="internalDns2"
-        :label="$t('label.internal.dns.2')"
-        v-bind="formItemLayout"
-        has-feedback>
-        <a-input v-model:value="form.internalDns2" />
-      </a-form-item>
+      <div v-if="!this.isEdgeZone">
+        <a-form-item
+          name="ipv4Dns1"
+          ref="ipv4Dns1"
+          :label="$t('label.ipv4.dns1')"
+          v-bind="formItemLayout"
+          has-feedback>
+          <a-input v-model:value="form.ipv4Dns1" />
+        </a-form-item>
+        <a-form-item
+          name="ipv4Dns2"
+          ref="ipv4Dns2"
+          :label="$t('label.ipv4.dns2')"
+          v-bind="formItemLayout"
+          has-feedback>
+          <a-input v-model:value="form.ipv4Dns2" />
+        </a-form-item>
+        <a-form-item
+          name="ipv6Dns1"
+          ref="ipv6Dns1"
+          :label="$t('label.ipv6.dns1')"
+          v-bind="formItemLayout"
+          v-if="isAdvancedZone"
+          has-feedback>
+          <a-input v-model:value="form.ipv6Dns1" />
+        </a-form-item>
+        <a-form-item
+          name="ipv6Dns2"
+          ref="ipv6Dns2"
+          :label="$t('label.ipv6.dns2')"
+          v-bind="formItemLayout"
+          v-if="isAdvancedZone"
+          has-feedback>
+          <a-input v-model:value="form.ipv6Dns2" />
+        </a-form-item>
+        <a-form-item
+          name="ipv6Cidr"
+          ref="ipv6Cidr"
+          :label="$t('label.ip6cidr')"
+          v-bind="formItemLayout"
+          v-if="isAdvancedZone && securityGroupsEnabled"
+          has-feedback>
+          <a-input v-model:value="form.ipv6Cidr" />
+        </a-form-item>
+        <a-form-item
+          name="ip6gateway"
+          ref="ip6gateway"
+          :label="$t('label.ip6gateway')"
+          v-bind="formItemLayout"
+          v-if="isAdvancedZone && securityGroupsEnabled"
+          has-feedback>
+          <a-input v-model:value="form.ip6gateway" />
+        </a-form-item>
+        <a-form-item
+          name="internalDns1"
+          ref="internalDns1"
+          :label="$t('label.internal.dns.1')"
+          v-bind="formItemLayout"
+          has-feedback>
+          <a-input v-model:value="form.internalDns1" />
+        </a-form-item>
+        <a-form-item
+          name="internalDns2"
+          ref="internalDns2"
+          :label="$t('label.internal.dns.2')"
+          v-bind="formItemLayout"
+          has-feedback>
+          <a-input v-model:value="form.internalDns2" />
+        </a-form-item>
+      </div>
       <a-form-item
         name="hypervisor"
         ref="hypervisor"
@@ -117,6 +119,7 @@
           v-model:value="form.hypervisor"
           :placeholder="$t('message.error.hypervisor.type')"
           :loading="hypervisors === null"
+          :disabled="this.isEdgeZone"
           showSearch
           optionFilterProp="label"
           :filterOption="(input, option) => {
@@ -127,46 +130,48 @@
           </a-select-option>
         </a-select>
       </a-form-item>
-      <a-form-item
-        name="networkOfferingId"
-        ref="networkOfferingId"
-        :label="$t('label.network.offering')"
-        v-bind="formItemLayout"
-        v-if="!isAdvancedZone || securityGroupsEnabled"
-        has-feedback>
-        <a-select
-          :loading="availableNetworkOfferings === null"
-          v-model:value="form.networkOfferingId"
-          :placeholder="$t('message.error.network.offering')"
-          showSearch
-          optionFilterProp="label"
-          :filterOption="(input, option) => {
-            return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
-          }" >
-          <a-select-option
-            v-for="networkOffering in availableNetworkOfferings"
-            :key="networkOffering.id">
-            {{ networkOffering.displaytext || networkOffering.name || networkOffering.description }}
-          </a-select-option>
-        </a-select>
-      </a-form-item>
-      <a-form-item
-        name="networkDomain"
-        ref="networkDomain"
-        :label="$t('label.network.domain')"
-        v-bind="formItemLayout"
-        has-feedback>
-        <a-input v-model:value="form.networkDomain" />
-      </a-form-item>
-      <a-form-item
-        name="guestcidraddress"
-        ref="guestcidraddress"
-        :label="$t('label.guest.cidr')"
-        v-bind="formItemLayout"
-        v-if="isAdvancedZone && !securityGroupsEnabled"
-        has-feedback>
-        <a-input v-model:value="form.guestcidraddress" />
-      </a-form-item>
+      <div v-if="!this.isEdgeZone">
+        <a-form-item
+          name="networkOfferingId"
+          ref="networkOfferingId"
+          :label="$t('label.network.offering')"
+          v-bind="formItemLayout"
+          v-if="!isAdvancedZone || securityGroupsEnabled"
+          has-feedback>
+          <a-select
+            :loading="availableNetworkOfferings === null"
+            v-model:value="form.networkOfferingId"
+            :placeholder="$t('message.error.network.offering')"
+            showSearch
+            optionFilterProp="label"
+            :filterOption="(input, option) => {
+              return option.children[0].children.toLowerCase().indexOf(input.toLowerCase()) >= 0
+            }" >
+            <a-select-option
+              v-for="networkOffering in availableNetworkOfferings"
+              :key="networkOffering.id">
+              {{ networkOffering.displaytext || networkOffering.name || networkOffering.description }}
+            </a-select-option>
+          </a-select>
+        </a-form-item>
+        <a-form-item
+          name="networkDomain"
+          ref="networkDomain"
+          :label="$t('label.network.domain')"
+          v-bind="formItemLayout"
+          has-feedback>
+          <a-input v-model:value="form.networkDomain" />
+        </a-form-item>
+        <a-form-item
+          name="guestcidraddress"
+          ref="guestcidraddress"
+          :label="$t('label.guest.cidr')"
+          v-bind="formItemLayout"
+          v-if="isAdvancedZone && !securityGroupsEnabled"
+          has-feedback>
+          <a-input v-model:value="form.guestcidraddress" />
+        </a-form-item>
+      </div>
       <a-form-item
         name="isDedicated"
         ref="isDedicated"
@@ -195,28 +200,35 @@
           </a-select-option>
         </a-select>
       </a-form-item>
-      <a-form-item
-        name="account"
-        ref="account"
-        :label="$t('label.account')"
-        v-bind="formItemLayout"
-        v-if="isDedicated">
-        <a-input v-model:value="form.account" />
-      </a-form-item>
-      <a-form-item
-        name="localstorageenabled"
-        ref="localstorageenabled"
-        :label="$t('label.local.storage.enabled')"
-        v-bind="formItemLayout">
-        <a-switch v-model:checked="form.localstorageenabled" />
-      </a-form-item>
-      <a-form-item
-        name="localstorageenabledforsystemvm"
-        ref="localstorageenabledforsystemvm"
-        :label="$t('label.local.storage.enabled.system.vms')"
-        v-bind="formItemLayout">
-        <a-switch v-model:checked="form.localstorageenabledforsystemvm" />
-      </a-form-item>
+      <a-alert style="margin-top: 5px" type="warning" v-if="this.isEdgeZone">
+        <template #message>
+          <span v-html="$t('message.zone.edge.local.storage')" />
+        </template>
+      </a-alert>
+      <div v-else>
+        <a-form-item
+          name="account"
+          ref="account"
+          :label="$t('label.account')"
+          v-bind="formItemLayout"
+          v-if="isDedicated">
+          <a-input v-model:value="form.account" />
+        </a-form-item>
+        <a-form-item
+          name="localstorageenabled"
+          ref="localstorageenabled"
+          :label="$t('label.local.storage.enabled')"
+          v-bind="formItemLayout">
+          <a-switch v-model:checked="form.localstorageenabled"/>
+        </a-form-item>
+        <a-form-item
+          name="localstorageenabledforsystemvm"
+          ref="localstorageenabledforsystemvm"
+          :label="$t('label.local.storage.enabled.system.vms')"
+          v-bind="formItemLayout">
+          <a-switch v-model:checked="form.localstorageenabledforsystemvm" />
+        </a-form-item>
+      </div>
     </a-form>
     <div class="form-action">
       <a-button
@@ -251,7 +263,6 @@ export default {
     }
   },
   data: () => ({
-    description: 'message.desc.zone',
     formItemLayout: {
       labelCol: { span: 8 },
       wrapperCol: { span: 12 }
@@ -328,10 +339,19 @@ export default {
     securityGroupsEnabled () {
       return this.isAdvancedZone && (this.prefillContent?.securityGroupsEnabled || false)
     },
+    isEdgeZone () {
+      return this.prefillContent?.zoneSuperType === 'Edge' || false
+    },
+    description () {
+      return this.isEdgeZone ? 'message.desc.zone.edge' : 'message.desc.zone'
+    },
     name () {
       return this.prefillContent?.name || null
     },
     ipv4Dns1 () {
+      if (this.isEdgeZone) {
+        return '8.8.8.8'
+      }
       return this.prefillContent?.ipv4Dns1 || null
     },
     ipv4Dns2 () {
@@ -344,6 +364,9 @@ export default {
       return this.prefillContent?.ipv6Dns2 || null
     },
     internalDns1 () {
+      if (this.isEdgeZone) {
+        return '8.8.8.8'
+      }
       return this.prefillContent?.internalDns1 || null
     },
     internalDns2 () {
@@ -356,6 +379,9 @@ export default {
       return this.prefillContent?.ip6gateway || null
     },
     currentHypervisor () {
+      if (this.isEdgeZone) {
+        return 'KVM'
+      }
       if (this.prefillContent.hypervisor) {
         return this.prefillContent.hypervisor
       } else if (this.hypervisors && this.hypervisors.length > 0) {
@@ -393,9 +419,15 @@ export default {
       return this.prefillContent?.account || null
     },
     localstorageenabled () {
+      if (this.isEdgeZone) {
+        return true
+      }
       return this.prefillContent?.localstorageenabled || false
     },
     localstorageenabledforsystemvm () {
+      if (this.isEdgeZone) {
+        return true
+      }
       return this.prefillContent?.localstorageenabledforsystemvm || false
     }
   },
diff --git a/ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue b/ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue
index 430ea688c37..bcd231b70f5 100644
--- a/ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue
+++ b/ui/src/views/infra/zone/ZoneWizardZoneTypeStep.vue
@@ -25,49 +25,25 @@
       @finish="handleSubmit"
       v-ctrl-enter="handleSubmit"
      >
-      <a-form-item name="zoneType" ref="zoneType">
-        <a-radio-group v-model:value="form.zoneType">
+      <a-form-item name="zoneSuperType" ref="zoneSuperType">
+        <a-radio-group v-model:value="form.zoneSuperType">
           <a-card class="card-item">
             <a-row :gutter="12">
               <a-col :md="6" :lg="6">
-                <a-radio class="card-form-item" value="Advanced" v-if="$config.basicZoneEnabled">{{ $t('label.advanced') }}</a-radio>
-                <span style="margin-top: 20px;" class="card-form-item" v-else>
-                  <setting-outlined style="margin-right: 10px" />
-                  {{ $t('label.advanced') }}
-                </span>
+                <a-radio class="card-form-item" value="Core">{{ $t('label.core') }}</a-radio>
               </a-col>
               <a-col :md="18" :lg="18">
-                <a-card class="ant-form-text zone-support">{{ $t(zoneDescription.Advanced) }}</a-card>
-              </a-col>
-            </a-row>
-            <a-row :gutter="12">
-              <a-col :md="6" :lg="6" style="margin-top: 15px">
-                <a-form-item
-                  name="securityGroupsEnabled"
-                  ref="securityGroupsEnabled"
-                  class="card-form-item"
-                  v-bind="formItemLayout">
-                  <a-switch
-                    class="card-form-item"
-                    v-model:checked="form.securityGroupsEnabled"
-                    :disabled="!isAdvancedZone"
-                    v-focus="true"
-                  />
-                </a-form-item>
-                <span>{{ $t('label.menu.security.groups') }}</span>
-              </a-col>
-              <a-col :md="18" :lg="18" style="margin-top: 15px;">
-                <a-card class="zone-support">{{ $t(zoneDescription.SecurityGroups) }}</a-card>
+                <a-card class="ant-form-text zone-support">{{ $t(zoneDescription.Core) }}</a-card>
               </a-col>
             </a-row>
           </a-card>
-          <a-card class="card-item" v-if="$config.basicZoneEnabled">
+          <a-card class="card-item">
             <a-row :gutter="12">
               <a-col :md="6" :lg="6">
-                <a-radio class="card-form-item" value="Basic">{{ $t('label.basic') }}</a-radio>
+                <a-radio class="card-form-item" value="Edge">{{ $t('label.edge') }}</a-radio>
               </a-col>
               <a-col :md="18" :lg="18">
-                <a-card class="ant-form-text zone-support">{{ $t(zoneDescription.Basic) }}</a-card>
+                <a-card class="ant-form-text zone-support"><span v-html="$t(zoneDescription.Edge)"></span></a-card>
               </a-col>
             </a-row>
           </a-card>
@@ -100,9 +76,8 @@ export default {
       wrapperCol: { span: 14 }
     },
     zoneDescription: {
-      Basic: 'message.desc.basic.zone',
-      Advanced: 'message.desc.advanced.zone',
-      SecurityGroups: 'message.advanced.security.group'
+      Core: 'message.desc.core.zone',
+      Edge: 'message.desc.edge.zone'
     },
     formModel: {}
   }),
@@ -119,29 +94,25 @@ export default {
     }
   },
   computed: {
-    isAdvancedZone () {
-      return this.zoneType === 'Advanced'
-    },
-    zoneType () {
-      return this.prefillContent.zoneType ? this.prefillContent.zoneType : 'Advanced'
-    },
-    securityGroupsEnabled () {
-      return this.isAdvancedZone && (this.prefillContent?.securityGroupsEnabled || false)
+    zoneSuperType () {
+      return this.prefillContent.zoneSuperType ? this.prefillContent.zoneSuperType : 'Core'
     }
   },
   methods: {
     initForm () {
       this.formRef = ref()
       this.form = reactive({
-        zoneType: this.zoneType,
-        securityGroupsEnabled: this.securityGroupsEnabled
+        zoneSuperType: this.zoneSuperType
       })
       this.rules = reactive({
-        zoneType: [{ required: true, message: this.$t('message.error.zone.type') }]
+        zoneSuperType: [{ required: true, message: this.$t('message.error.zone.type') }]
       })
       this.formModel = toRaw(this.form)
     },
     handleSubmit () {
+      if (this.form.zoneSuperType === 'Edge') {
+        this.form.zoneType = 'Advanced'
+      }
       this.formRef.value.validate().then(() => {
         this.$emit('nextPressed')
       }).catch(error => {
@@ -168,6 +139,8 @@ export default {
 
     .card-form-item {
       float: left;
+      font-weight: bold;
+        font-size: 15px;
     }
 
     .checkbox-advance {
diff --git a/ui/src/views/network/CreateIsolatedNetworkForm.vue b/ui/src/views/network/CreateIsolatedNetworkForm.vue
index c4b5fc83f38..8c5e7080fa4 100644
--- a/ui/src/views/network/CreateIsolatedNetworkForm.vue
+++ b/ui/src/views/network/CreateIsolatedNetworkForm.vue
@@ -430,7 +430,7 @@ export default {
       api('listZones', params).then(json => {
         for (const i in json.listzonesresponse.zone) {
           const zone = json.listzonesresponse.zone[i]
-          if (zone.networktype === 'Advanced' && zone.securitygroupsenabled !== true) {
+          if (zone.networktype === 'Advanced' && zone.securitygroupsenabled !== true && zone.type !== 'Edge') {
             this.zones.push(zone)
           }
         }
diff --git a/ui/src/views/storage/UploadLocalVolume.vue b/ui/src/views/storage/UploadLocalVolume.vue
index afaf8f96d58..3548e027ede 100644
--- a/ui/src/views/storage/UploadLocalVolume.vue
+++ b/ui/src/views/storage/UploadLocalVolume.vue
@@ -221,6 +221,7 @@ export default {
       api('listZones', { showicon: true }).then(json => {
         if (json && json.listzonesresponse && json.listzonesresponse.zone) {
           this.zones = json.listzonesresponse.zone
+          this.zones = this.zones.filter(zone => zone.type !== 'Edge')
           if (this.zones.length > 0) {
             this.onZoneChange(this.zones[0].id)
           }
diff --git a/ui/src/views/storage/UploadVolume.vue b/ui/src/views/storage/UploadVolume.vue
index 0cf477bec1f..fa5083188c9 100644
--- a/ui/src/views/storage/UploadVolume.vue
+++ b/ui/src/views/storage/UploadVolume.vue
@@ -210,8 +210,9 @@ export default {
       this.loading = true
       api('listZones', { showicon: true }).then(json => {
         this.zones = json.listzonesresponse.zone || []
-        this.selectedZoneId = this.zones[0].id || ''
-        this.fetchDiskOfferings(this.selectedZoneId)
+        this.zones = this.zones.filter(zone => zone.type !== 'Edge')
+        this.form.zoneId = this.zones[0].id || ''
+        this.fetchDiskOfferings(this.form.zoneId)
       }).finally(() => {
         this.loading = false
       })