diff --git a/pkg/harvester/config/feature-flags.js b/pkg/harvester/config/feature-flags.js index 7e8a1f00..ee702552 100644 --- a/pkg/harvester/config/feature-flags.js +++ b/pkg/harvester/config/feature-flags.js @@ -42,7 +42,8 @@ const FEATURE_FLAGS = { 'vmMachineTypes', 'customSupportBundle', 'csiOnlineExpandValidation', - 'vmNetworkMigration' + 'vmNetworkMigration', + 'kubeovnVpcSubnet' ] }; diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index b7dba1ad..eb32c92d 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -424,6 +424,7 @@ export function init($plugin, store) { [ HCI.CLUSTER_NETWORK, HCI.NETWORK_ATTACHMENT, + HCI.VPC, HCI.LB, HCI.IP_POOL, ], @@ -550,6 +551,21 @@ export function init($plugin, store) { exact: false }); + configureType(HCI.VPC, { hiddenNamespaceGroupButton: true, canYaml: false }); + + virtualType({ + labelKey: 'harvester.vpc.label', + name: HCI.VPC, + namespaced: true, + weight: 187, + route: { + name: `${ PRODUCT_NAME }-c-cluster-resource`, + params: { resource: HCI.VPC } + }, + exact: false, + ifHaveType: HCI.VPC, + }); + configureType(HCI.SNAPSHOT, { isCreatable: false, location: { diff --git a/pkg/harvester/config/harvester-map.js b/pkg/harvester/config/harvester-map.js index 75f2b5ad..cef1d5f2 100644 --- a/pkg/harvester/config/harvester-map.js +++ b/pkg/harvester/config/harvester-map.js @@ -70,6 +70,7 @@ export const ADD_ONS = { RANCHER_MONITORING: 'rancher-monitoring', VM_IMPORT_CONTROLLER: 'vm-import-controller', LVM_DRIVER: 'lvm.driver.harvesterhci.io', + KUBEOVN_OPERATOR: 'kubeovn-operator', }; export const CSI_SECRETS = { diff --git a/pkg/harvester/config/query-params.js b/pkg/harvester/config/query-params.js index 6c56a44c..5faf9f63 100644 --- a/pkg/harvester/config/query-params.js +++ b/pkg/harvester/config/query-params.js @@ -1 +1,2 @@ export const CLUSTER_NETWORK = 'clusterNetwork'; +export const VPC = 'vpc'; diff --git a/pkg/harvester/config/table-headers.js b/pkg/harvester/config/table-headers.js index 2dc69822..b753aa5b 100644 --- a/pkg/harvester/config/table-headers.js +++ b/pkg/harvester/config/table-headers.js @@ -104,3 +104,30 @@ export const HARVESTER_DESCRIPTION = { ...DESCRIPTION, width: 150, }; + +// The CIDR_BLOCK column in VPC list page +export const CIDR_BLOCK = { + name: 'cidrBlock', + labelKey: 'harvester.subnet.cidrBlock.label', + sort: 'cidrBlock', + value: 'spec.cidrBlock', + align: 'left', +}; + +// The Protocol column in VPC list page +export const PROTOCOL = { + name: 'protocol', + labelKey: 'harvester.subnet.protocol.label', + sort: 'protocol', + value: 'spec.protocol', + align: 'left', +}; + +// The Provider column in VPC list page +export const PROVIDER = { + name: 'provider', + labelKey: 'harvester.subnet.provider.label', + sort: 'provider', + value: 'spec.provider', + align: 'left', +}; diff --git a/pkg/harvester/config/types.js b/pkg/harvester/config/types.js index 552f9361..b336ca4a 100644 --- a/pkg/harvester/config/types.js +++ b/pkg/harvester/config/types.js @@ -5,10 +5,16 @@ export const BACKUP_TYPE = { export const NETWORK_TYPE = { L2VLAN: 'L2VlanNetwork', - UNTAGGED: 'UntaggedNetwork' + UNTAGGED: 'UntaggedNetwork', + OVERLAY: 'OverlayNetwork', }; export const VOLUME_MODE = { BLOCK: 'Block', FILE_SYSTEM: 'Filesystem' }; + +export const NETWORK_PROTOCOL = { + IPv4: 'IPv4', + IPv6: 'IPv6', +}; diff --git a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue index b362ec63..3b03d562 100644 --- a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue +++ b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue @@ -6,14 +6,13 @@ import { LabeledInput } from '@components/Form/LabeledInput'; import { RadioGroup } from '@components/Form/Radio'; import NameNsDescription from '@shell/components/form/NameNsDescription'; import LabeledSelect from '@shell/components/form/LabeledSelect'; - import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import CreateEditView from '@shell/mixins/create-edit-view'; import { allHash } from '@shell/utils/promise'; import { HCI } from '../types'; import { NETWORK_TYPE } from '../config/types'; -const { L2VLAN, UNTAGGED } = NETWORK_TYPE; +const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE; const AUTO = 'auto'; const MANUAL = 'manual'; @@ -44,13 +43,10 @@ export default { data() { const config = JSON.parse(this.value.spec.config); + const annotations = this.value?.metadata?.annotations || {}; const layer3Network = JSON.parse(annotations[HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE] || '{}'); - if ((config.bridge || '').endsWith('-br')) { - config.bridge = config.bridge.slice(0, -3); - } - const type = this.value.vlanType || L2VLAN ; return { @@ -78,6 +74,30 @@ export default { }, computed: { + clusterBridge: { + get() { + if (!this.config.bridge) { + return ''; + } + + // remove -br suffix if exists + return this.config?.bridge?.endsWith('-br') ? this.config.bridge.slice(0, -3) : ''; + }, + + set(neu) { + if (neu === '') { + this.config.bridge = ''; + + return; + } + + if (!neu.endsWith('-br')) { + this.config.bridge = `${ neu }-br`; + } else { + this.config.bridge = neu; + } + } + }, modeOptions() { return [{ label: this.t('harvester.network.layer3Network.mode.auto'), @@ -88,6 +108,14 @@ export default { }]; }, + kubeovnVpcSubnetSupport() { + return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet'); + }, + + longhornV2LVMSupport() { + return this.$store.getters['harvester-common/getFeatureEnabled']('longhornV2LVMSupport'); + }, + clusterNetworkOptions() { const inStore = this.$store.getters['currentProduct'].inStore; const clusterNetworks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || []; @@ -103,8 +131,30 @@ export default { }); }, - networkType() { - return [L2VLAN, UNTAGGED]; + networkTypes() { + const types = [L2VLAN, UNTAGGED]; + + if (this.kubeovnVpcSubnetSupport) { + types.push(OVERLAY); + } + + return types; + }, + + isL2VlanNetwork() { + if (this.isView) { + return this.value.vlanType === L2VLAN; + } + + return this.type === L2VLAN; + }, + + isOverlayNetwork() { + if (this.isView) { + return this.value.vlanType === OVERLAY; + } + + return this.type === OVERLAY; }, isUntaggedNetwork() { @@ -116,36 +166,54 @@ export default { } }, + watch: { + type(newType) { + if (newType === OVERLAY) { + this.config.type = 'kube-ovn'; + this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`; + this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock'; + } else { + this.config.type = 'bridge'; + this.config.promiscMode = true; + this.config.ipam = {}; + this.config.bridge = ''; + delete this.config.provider; + delete this.config.server_socket; + } + } + }, + methods: { async saveNetwork(buttonCb) { const errors = []; - if (!this.config.vlan && !this.isUntaggedNetwork) { - errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') })); - } - - if (!this.config.bridge) { - errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') })); - } - - if (this.layer3Network.mode === MANUAL) { - if (!this.layer3Network.gateway) { - errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') })); + if (this.isL2VlanNetwork || this.isUntaggedNetwork) { + if (!this.config.vlan && !this.isUntaggedNetwork) { + errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') })); } - if (!this.layer3Network.cidr) { - errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') })); + + if (!this.config.bridge) { + errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.clusterNetwork.label') })); } + + if (this.layer3Network.mode === MANUAL) { + if (!this.layer3Network.gateway) { + errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.gateway.label') })); + } + if (!this.layer3Network.cidr) { + errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.network.layer3Network.cidr.label') })); + } + } + + if (errors.length > 0) { + buttonCb(false); + this.errors = errors; + + return false; + } + this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network)); } - if (errors.length > 0) { - buttonCb(false); - this.errors = errors; - - return false; - } - - this.value.setAnnotation(HCI_LABELS_ANNOTATIONS.NETWORK_ROUTE, JSON.stringify(this.layer3Network)); - await this.save(buttonCb); }, @@ -169,14 +237,19 @@ export default { updateBeforeSave() { this.config.name = this.value.metadata.name; + if (this.isOverlayNetwork) { + this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`; + delete this.config.bridge; + delete this.config.promiscMode; + delete this.config.vlan; + delete this.config.ipam; + } + if (this.isUntaggedNetwork) { delete this.config.vlan; } - this.value.spec.config = JSON.stringify({ - ...this.config, - bridge: `${ this.config.bridge }-br`, - }); + this.value.spec.config = JSON.stringify({ ...this.config }); }, } }; @@ -212,14 +285,15 @@ export default { - -
-
- -
-
+ +import CruResource from '@shell/components/CruResource'; +import NameNsDescription from '@shell/components/form/NameNsDescription'; +import LabeledSelect from '@shell/components/form/LabeledSelect'; +import { LabeledInput } from '@components/Form/LabeledInput'; +import Tab from '@shell/components/Tabbed/Tab'; +import { NETWORK_ATTACHMENT } from '@shell/config/types'; +import Loading from '@shell/components/Loading'; +import CreateEditView from '@shell/mixins/create-edit-view'; +import { RadioGroup } from '@components/Form/Radio'; +import { NETWORK_PROTOCOL, NETWORK_TYPE } from '@pkg/harvester/config/types'; +import { set } from '@shell/utils/object'; +import ArrayList from '@shell/components/form/ArrayList'; +import { allHash } from '@shell/utils/promise'; +import { HCI } from '../../types'; +import ResourceTabs from '@shell/components/form/ResourceTabs/index'; + +export default { + name: 'EditSubnet', + + emits: ['update:value'], + + components: { + CruResource, + LabeledInput, + LabeledSelect, + NameNsDescription, + Tab, + RadioGroup, + ArrayList, + ResourceTabs, + Loading, + }, + + mixins: [CreateEditView], + + inheritAttrs: false, + + created() { + const vpc = this.$route.query.vpc || ''; + + set(this.value, 'spec', this.value.spec || { + cidrBlock: '', + protocol: NETWORK_PROTOCOL.IPv4, + provider: '', + vpc, + gatewayIP: '', + excludeIps: [], + private: false + }); + }, + + async fetch() { + const inStore = this.$store.getters['currentProduct'].inStore; + + const hash = { + vpc: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VPC }), + nad: this.$store.dispatch(`${ inStore }/findAll`, { type: NETWORK_ATTACHMENT }), + }; + + await allHash(hash); + }, + + computed: { + showAllowSubnets() { + return this.value?.spec?.private === true; + }, + + doneLocationOverride() { + return this.value.doneOverride; + }, + + allowSubnetTooltip() { + return this.t('harvester.subnet.allowSubnet.tooltip', null, true); + }, + + excludeIPsTooltip() { + return this.t('harvester.subnet.excludeIPs.tooltip', null, true); + }, + + protocolOptions() { + return Object.values(NETWORK_PROTOCOL); + }, + + provider: { + get() { + const raw = this.value.spec.provider; + + if (!raw) { + return ''; + } + const vmNet = raw.split('.')[0] || ''; + const ns = raw.split('.')[1] || ''; + + return `${ ns }/${ vmNet }`; + }, + + set(value) { + const ns = value.split('/')[0] || ''; + const vmNet = value.split('/')[1] || ''; + const provider = `${ vmNet }.${ ns }.ovn`; + + set(this.value, 'spec.provider', provider); + } + }, + + providerOptions() { + const inStore = this.$store.getters['currentProduct'].inStore; + const vmNets = this.$store.getters[`${ inStore }/all`](NETWORK_ATTACHMENT) || []; + + return vmNets.filter((net) => net.vlanType === NETWORK_TYPE.OVERLAY).map((n) => ({ + label: n.id, + value: n.id, + })); + }, + + vpcOptions() { + const inStore = this.$store.getters['currentProduct'].inStore; + const vpcs = this.$store.getters[`${ inStore }/all`](HCI.VPC) || []; + + return vpcs.map((n) => ({ + label: n.id, + value: n.id, + })); + } + }, + + methods: { + async saveSubnet(buttonCb) { + const errors = []; + const name = this.value?.metadata?.name; + + try { + if (!name) { + errors.push(this.t('validation.required', { key: this.t('generic.name') }, true)); + } else if (!this.value?.spec?.cidrBlock) { + errors.push(this.t('validation.required', { key: this.t('harvester.subnet.cidrBlock.label') }, true)); + } else if (!this.value?.spec?.provider) { + errors.push(this.t('validation.required', { key: this.t('harvester.subnet.provider.label') }, true)); + } else if (this.value.spec.excludeIps.includes('')) { + errors.push(this.t('harvester.validation.subnet.excludeIps')); + } + + if (errors.length > 0) { + buttonCb(false); + this.errors = errors; + + return false; + } + await this.value.save(); + buttonCb(true); + this.done(); + } catch (e) { + this.errors = [e]; + buttonCb(false); + } + }, + }, +}; + + + + diff --git a/pkg/harvester/edit/kubeovn.io.vpc/StaticRoutes.vue b/pkg/harvester/edit/kubeovn.io.vpc/StaticRoutes.vue new file mode 100644 index 00000000..3969366a --- /dev/null +++ b/pkg/harvester/edit/kubeovn.io.vpc/StaticRoutes.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/pkg/harvester/edit/kubeovn.io.vpc/VpcPeerings.vue b/pkg/harvester/edit/kubeovn.io.vpc/VpcPeerings.vue new file mode 100644 index 00000000..ea02bfd0 --- /dev/null +++ b/pkg/harvester/edit/kubeovn.io.vpc/VpcPeerings.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/pkg/harvester/edit/kubeovn.io.vpc/index.vue b/pkg/harvester/edit/kubeovn.io.vpc/index.vue new file mode 100644 index 00000000..a7027bb7 --- /dev/null +++ b/pkg/harvester/edit/kubeovn.io.vpc/index.vue @@ -0,0 +1,120 @@ + + + diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index 7c73e016..7c08966f 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -396,6 +396,8 @@ harvester: ruleFileTip: 'The file you have chosen ends in an extension that we do not support. We only accept image files that end in .img, .iso, .qcow, .qcow2, .raw.' hash: sha512: 'Invalid SHA512 checksum.' + subnet: + excludeIps: 'Exclude IPs cannot be empty. Please remove or fill in the exclude IPs.' dashboard: label: Dashboard @@ -1015,6 +1017,59 @@ harvester: progress: Restore in progress complete: Restore completed + subnet: + cidrBlock: + tooltip: The subnet range in CIDR notation. Note that the CIDR blocks of different Subnets' within the same VPC cannot overlap. + label: CIDR Block + placeholder: e.g. 172.20.0.0/16 + protocol: + label: Protocol + provider: + tooltip: Network provider for this Subnet. Must be one of the Virtual Machine Networks in OverlayNetwork type. + label: Provider + vpc: + label: Virtual Private Cloud + gateway: + label: Gateway IP + placeholder: e.g. 172.20.0.1 + private: + label: Private Subnet + tooltip: Enable network isolation for this Subnet. When enabled, VMs can only communicate within this subnet, even if other subnets exist under the same VPC. + allowSubnet: + label: Allow Subnets + tooltip: You can specify certain subnets which can communicate with this subnet. + addSubnet: Add Allow Subnet + placeholder: e.g. 172.16.0.0/16 + excludeIPs: + tooltip: The IP address list to reserve from automatic assignment. The gateway IP address is always excluded and will be automatically added to the list. + + vpc: + noAddonEnabled: + prefix: The kubeovn-operator add-on is not enabled, click + middle: here + suffix: to enable the add-on to successfully create VPC and subnet. + label: Virtual Private Cloud + noChild: There is no subnet defined in this Virtual Private Cloud. + createSubnet: Create Subnet + staticRoutes: + label: Static Routes + cidr: + label: CIDR + placeholder: e.g. 172.16.0.0/16 + nextHopIP: + tooltip: The localConnectIP on the other end of the peering VPC. + label: Next Hop IP + placeholder: e.g. 169.254.0.2 + vpcPeerings: + label: VPC Peerings + localConnectIP: + tooltip: The designated IP address in CIDR notation of the VPC peering endpoint. Note that both IP addresses (for the VPC and the other VPC) should belong to the same subnet range and should not conflict with the cidrBlock in any existing subnets. + label: Local Connect IP + placeholder: e.g. 169.254.0.1/16 + remoteVpc: + label: Remote VPC + + network: label: Virtual Machine Networks tabs: @@ -1079,8 +1134,8 @@ harvester: clusterNetwork: Cluster Network vlan: VLAN ID exclude: - label: Exclude - placeholder: e.g. 172.16.0.1/32 + label: Exclude IPs + placeholder: e.g. 172.16.0.1 invalid: '"Exclude list" is invalid.' addIp: Add Exclude IP warning: 'WARNING:
Any change to storage-network requires shutting down all virtual machines before applying this setting.
Users have to ensure the cluster network is configured and VLAN Configuration will cover all nodes and ensure the network connectivity is working and expected in all nodes.' @@ -1706,6 +1761,11 @@ typeLabel: one { Virtual Machine Backup } other { Virtual Machine Backups } } + kubeovn.io.vpc: |- + {count, plural, + one { Virtual Private Cloud } + other { Virtual Private Clouds } + } harvesterhci.io.cloudtemplate: |- {count, plural, one { Cloud Configuration Template } diff --git a/pkg/harvester/list/kubeovn.io.vpc.vue b/pkg/harvester/list/kubeovn.io.vpc.vue new file mode 100644 index 00000000..c63b72bb --- /dev/null +++ b/pkg/harvester/list/kubeovn.io.vpc.vue @@ -0,0 +1,285 @@ + + + + + diff --git a/pkg/harvester/list/network.harvesterhci.io.clusternetwork.vue b/pkg/harvester/list/network.harvesterhci.io.clusternetwork.vue index dd163e43..f9f4ba95 100644 --- a/pkg/harvester/list/network.harvesterhci.io.clusternetwork.vue +++ b/pkg/harvester/list/network.harvesterhci.io.clusternetwork.vue @@ -2,12 +2,10 @@ import Loading from '@shell/components/Loading'; import ResourceTable from '@shell/components/ResourceTable'; import Masthead from '@shell/components/ResourceList/Masthead'; - import { allHash } from '@shell/utils/promise'; import { STATE, AGE, NAME } from '@shell/config/table-headers'; import { mapPref, GROUP_RESOURCES } from '@shell/store/prefs'; import { NODE } from '@shell/config/types'; - import { PRODUCT_NAME as HARVESTER_PRODUCT } from '../config/harvester'; import { CLUSTER_NETWORK } from '../config/query-params'; import { HCI } from '../types'; @@ -38,7 +36,6 @@ export default { computed: { groupPreference: mapPref(GROUP_RESOURCES), - headers() { return [ STATE, @@ -209,7 +206,6 @@ export default { {{ groupLabel(group) }} -