diff --git a/releasenotes/notes/Support-Attach-Network-And-Detach-Network-For-Zun-Container-e41980df3f67c5b5.yaml b/releasenotes/notes/Support-Attach-Network-And-Detach-Network-For-Zun-Container-e41980df3f67c5b5.yaml
new file mode 100644
index 00000000..26123c6a
--- /dev/null
+++ b/releasenotes/notes/Support-Attach-Network-And-Detach-Network-For-Zun-Container-e41980df3f67c5b5.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Support attach network and detach network for zun container
+
+ 1. Add attach network action for zun container
+
+ 2. Add detach network action for zun container
\ No newline at end of file
diff --git a/src/client/zun/index.js b/src/client/zun/index.js
index 323b2cf6..be15f9e1 100644
--- a/src/client/zun/index.js
+++ b/src/client/zun/index.js
@@ -66,6 +66,14 @@ export class ZunClient extends Base {
key: 'execute',
method: 'post',
},
+ {
+ key: 'network_attach',
+ method: 'post',
+ },
+ {
+ key: 'network_detach',
+ method: 'post',
+ },
],
subResources: [
{
diff --git a/src/locales/en.json b/src/locales/en.json
index f72cdc28..1f4bc279 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -164,6 +164,7 @@
"Attach": "Attach",
"Attach Instance": "Attach Instance",
"Attach Interface": "Attach Interface",
+ "Attach Network": "Attach Network",
"Attach Security Group": "Attach Security Group",
"Attach USB": "Attach USB",
"Attach Volume": "Attach Volume",
@@ -724,6 +725,7 @@
"Detach": "Detach",
"Detach Instance": "Detach Instance",
"Detach Interface": "Detach Interface",
+ "Detach Network": "Detach Network",
"Detach Security Group": "Detach Security Group",
"Detach Volume": "Detach Volume",
"Detach interface": "Detach interface",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index dd941433..4d5e3ff5 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -164,6 +164,7 @@
"Attach": "挂载",
"Attach Instance": "绑定云主机",
"Attach Interface": "挂载网卡",
+ "Attach Network": "绑定网络",
"Attach Security Group": "绑定安全组",
"Attach USB": "挂载USB",
"Attach Volume": "挂载云硬盘",
@@ -724,6 +725,7 @@
"Detach": "解绑",
"Detach Instance": "从云主机解绑",
"Detach Interface": "卸载网卡",
+ "Detach Network": "解绑网络",
"Detach Security Group": "解绑安全组",
"Detach Volume": "卸载云硬盘",
"Detach interface": "卸载网卡",
diff --git a/src/pages/compute/containers/Instance/actions/AttachInterface.jsx b/src/pages/compute/containers/Instance/actions/AttachInterface.jsx
index dddc12e7..b6fa2ecc 100644
--- a/src/pages/compute/containers/Instance/actions/AttachInterface.jsx
+++ b/src/pages/compute/containers/Instance/actions/AttachInterface.jsx
@@ -195,7 +195,8 @@ export class AttachInterface extends ModalAction {
{
title: t('Allocation Pools'),
dataIndex: 'allocation_pools',
- render: (value) => `${value[0].start} -- ${value[0].end}`,
+ render: (value) =>
+ value.length ? `${value[0].start} -- ${value[0].end}` : '-',
},
],
},
diff --git a/src/pages/container-service/containers/Containers/Detail/BaseDetail.jsx b/src/pages/container-service/containers/Containers/Detail/BaseDetail.jsx
index f4ac8784..78beeaeb 100644
--- a/src/pages/container-service/containers/Containers/Detail/BaseDetail.jsx
+++ b/src/pages/container-service/containers/Containers/Detail/BaseDetail.jsx
@@ -224,10 +224,10 @@ export class BaseDetail extends Base {
<>
{value.length
? value.map((it) => {
- const link = this.getLinkRender('networkDetail', it, {
- id: it,
+ const link = this.getLinkRender('networkDetail', it.name, {
+ id: it.id,
});
- return
{link}
;
+ return {link}
;
})
: '-'}
>
@@ -240,11 +240,11 @@ export class BaseDetail extends Base {
<>
{value.length
? value.map((it) => {
- const link = this.getLinkRender('subnetDetail', it.subnet, {
- networkId: it.network,
- id: it.subnet,
+ const link = this.getLinkRender('subnetDetail', it.name, {
+ networkId: it.network_id,
+ id: it.id,
});
- return {link}
;
+ return {link}
;
})
: '-'}
>
diff --git a/src/pages/container-service/containers/Containers/actions/AttachNetwork.jsx b/src/pages/container-service/containers/Containers/actions/AttachNetwork.jsx
new file mode 100644
index 00000000..7aa30945
--- /dev/null
+++ b/src/pages/container-service/containers/Containers/actions/AttachNetwork.jsx
@@ -0,0 +1,87 @@
+// Copyright 2021 99cloud
+//
+// Licensed 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.
+
+import { inject, observer } from 'mobx-react';
+import globalContainersStore from 'src/stores/zun/containers';
+import { ModalAction } from 'containers/Action';
+import { checkItemAction } from 'resources/zun/container';
+
+export class AttachNetwork extends ModalAction {
+ static id = 'AttachNetwork';
+
+ static title = t('Attach Network');
+
+ init() {
+ this.store = globalContainersStore;
+ }
+
+ static get modalSize() {
+ return 'large';
+ }
+
+ getModalSize() {
+ return 'large';
+ }
+
+ get name() {
+ return t('Attach Network');
+ }
+
+ get defaultValue() {
+ const { name } = this.item;
+ const value = {
+ instance: name,
+ };
+ return value;
+ }
+
+ static policy = 'container:network_attach';
+
+ aliasPolicy = 'zun:container:network_attach';
+
+ static allowed = (item) => {
+ return checkItemAction(item, 'network_attach_detach');
+ };
+
+ disabledNetwork = (it) => {
+ const { networks } = this.item;
+ return networks.some((net) => net.id === it.id);
+ };
+
+ get formItems() {
+ return [
+ {
+ name: 'instance',
+ label: t('Instance'),
+ type: 'label',
+ iconType: 'instance',
+ },
+ {
+ name: 'networks',
+ label: t('Networks'),
+ type: 'network-select-table',
+ required: true,
+ disabledFunc: this.disabledNetwork,
+ },
+ ];
+ }
+
+ onSubmit = (values) => {
+ const { networks } = values;
+ const network = networks.selectedRowKeys[0];
+ return this.store.attachNetwork(this.item.id, { network });
+ };
+}
+
+export default inject('rootStore')(observer(AttachNetwork));
diff --git a/src/pages/container-service/containers/Containers/actions/DetachNetwork.jsx b/src/pages/container-service/containers/Containers/actions/DetachNetwork.jsx
new file mode 100644
index 00000000..c95b9b78
--- /dev/null
+++ b/src/pages/container-service/containers/Containers/actions/DetachNetwork.jsx
@@ -0,0 +1,100 @@
+// Copyright 2021 99cloud
+//
+// Licensed 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.
+
+import { inject, observer } from 'mobx-react';
+import globalContainersStore from 'src/stores/zun/containers';
+import { ModalAction } from 'containers/Action';
+import { checkItemAction } from 'resources/zun/container';
+import { networkColumns } from 'resources/neutron/network';
+
+export class DetachNetwork extends ModalAction {
+ static id = 'DetachNetwork';
+
+ static title = t('Detach Network');
+
+ init() {
+ this.store = globalContainersStore;
+ }
+
+ static get modalSize() {
+ return 'large';
+ }
+
+ getModalSize() {
+ return 'large';
+ }
+
+ get name() {
+ return t('Detach Network');
+ }
+
+ get networks() {
+ const { networks = [] } = this.item;
+ return networks;
+ }
+
+ get defaultValue() {
+ const { name } = this.item;
+ const value = {
+ instance: name,
+ };
+ return value;
+ }
+
+ static policy = 'container:network_detach';
+
+ aliasPolicy = 'zun:container:network_detach';
+
+ static allowed = (item) => {
+ return checkItemAction(item, 'network_attach_detach');
+ };
+
+ disabledNetwork = (it) => {
+ const { networks } = this.item;
+ return networks.includes(it.id);
+ };
+
+ get formItems() {
+ return [
+ {
+ name: 'instance',
+ label: t('Instance'),
+ type: 'label',
+ iconType: 'instance',
+ },
+ {
+ name: 'networks',
+ label: t('Networks'),
+ type: 'select-table',
+ data: this.networks,
+ columns: networkColumns(this),
+ filterParams: [
+ {
+ label: t('Name'),
+ name: 'name',
+ },
+ ],
+ required: true,
+ },
+ ];
+ }
+
+ onSubmit = (values) => {
+ const { networks } = values;
+ const network = networks.selectedRowKeys[0];
+ return this.store.detachNetwork(this.item.id, { network });
+ };
+}
+
+export default inject('rootStore')(observer(DetachNetwork));
diff --git a/src/pages/container-service/containers/Containers/actions/index.jsx b/src/pages/container-service/containers/Containers/actions/index.jsx
index 117c4f43..ce7cc678 100644
--- a/src/pages/container-service/containers/Containers/actions/index.jsx
+++ b/src/pages/container-service/containers/Containers/actions/index.jsx
@@ -23,6 +23,8 @@ import EditContainer from './Edit';
import KillContainer from './Kill';
import ForceDeleteContainer from './ForceDelete';
import ExecuteCommandContainer from './ExecuteCommand';
+import AttachNetwork from './AttachNetwork';
+import DetachNetwork from './DetachNetwork';
const statusActions = [
StartContainer,
@@ -32,6 +34,8 @@ const statusActions = [
RebuildContainer,
];
+const resourceActions = [AttachNetwork, DetachNetwork];
+
const actionConfigs = {
rowActions: {
firstAction: DeleteContainer,
@@ -45,6 +49,10 @@ const actionConfigs = {
ExecuteCommandContainer,
],
},
+ {
+ title: t('Related Resources'),
+ actions: resourceActions,
+ },
{
action: EditContainer,
},
diff --git a/src/pages/container-service/containers/Containers/index.jsx b/src/pages/container-service/containers/Containers/index.jsx
index 0ee6da6b..e9279206 100644
--- a/src/pages/container-service/containers/Containers/index.jsx
+++ b/src/pages/container-service/containers/Containers/index.jsx
@@ -97,10 +97,10 @@ export class Containers extends Base {
<>
{value.length
? value.map((it) => {
- const link = this.getLinkRender('networkDetail', it, {
- id: it,
+ const link = this.getLinkRender('networkDetail', it.name, {
+ id: it.id,
});
- return {link}
;
+ return {link}
;
})
: '-'}
>
diff --git a/src/resources/zun/container.js b/src/resources/zun/container.js
index 99a6dab8..fed89ca4 100644
--- a/src/resources/zun/container.js
+++ b/src/resources/zun/container.js
@@ -103,6 +103,12 @@ const validStates = {
states.STOPPED,
states.PAUSED,
],
+ network_attach_detach: [
+ states.CREATED,
+ states.RUNNING,
+ states.STOPPED,
+ states.PAUSED,
+ ],
};
export const checkItemAction = (item, actionName) => {
diff --git a/src/stores/zun/containers.js b/src/stores/zun/containers.js
index a6de3c7e..de9dc1b6 100644
--- a/src/stores/zun/containers.js
+++ b/src/stores/zun/containers.js
@@ -25,26 +25,20 @@ export class ContainersStore extends Base {
return client.glance.images;
}
+ get networkClient() {
+ return client.neutron.networks;
+ }
+
+ get subnetClient() {
+ return client.neutron.subnets;
+ }
+
get mapper() {
return (data) => {
- const { addresses = {} } = data;
- const networks = Object.keys(addresses);
- const addrs = [];
- const subnets = [];
- Object.entries(addresses).forEach(([key, val]) => {
- (val || []).forEach((v) => {
- addrs.push({ network: key, addr: v.addr });
- subnets.push({ network: key, subnet: v.subnet_id });
- });
- });
-
return {
...data,
id: data.uuid,
task_state: data.task_state === null ? 'free' : data.task_state,
- networks,
- addrs,
- subnets,
};
};
}
@@ -104,8 +98,40 @@ export class ContainersStore extends Base {
return this.client.execute(id, data);
}
+ @action
+ async attachNetwork(id, data) {
+ return this.client.network_attach(id, null, data);
+ }
+
+ @action
+ async detachNetwork(id, data) {
+ return this.client.network_detach(id, null, data);
+ }
+
+ async listDidFetch(items) {
+ if (!items.length) return items;
+ const [{ networks: allNetworks }, { subnets: allSubnets }] =
+ await Promise.all([this.networkClient.list(), this.subnetClient.list()]);
+ return items.map((it) => {
+ const { addresses = {} } = it;
+ const networks = [];
+ const addrs = [];
+ const subnets = [];
+ Object.entries(addresses).forEach(([key, val]) => {
+ (val || []).forEach((v) => {
+ const network = allNetworks.find((net) => net.id === key);
+ const subnet = allSubnets.find((sub) => sub.id === v.subnet_id);
+ addrs.push({ network, addr: v.addr, port: v.port });
+ networks.push(network);
+ subnets.push(subnet);
+ });
+ });
+ return { ...it, addrs, networks, subnets };
+ });
+ }
+
async detailDidFetch(item) {
- const { uuid, status, image_driver, image } = item;
+ const { uuid, status, image_driver, image, addresses = {} } = item;
let stats = {};
if (status === 'Running') {
stats = (await this.client.stats.list(uuid)) || {};
@@ -116,7 +142,21 @@ export class ContainersStore extends Base {
item.imageInfo = info;
} catch (error) {}
}
- return { ...item, stats };
+ const [{ networks: allNetworks }, { subnets: allSubnets }] =
+ await Promise.all([this.networkClient.list(), this.subnetClient.list()]);
+ const networks = [];
+ const addrs = [];
+ const subnets = [];
+ Object.entries(addresses).forEach(([key, val]) => {
+ (val || []).forEach((v) => {
+ const network = allNetworks.find((net) => net.id === key);
+ const subnet = allSubnets.find((sub) => sub.id === v.subnet_id);
+ addrs.push({ network, addr: v.addr, port: v.port });
+ networks.push(network);
+ subnets.push(subnet);
+ });
+ });
+ return { ...item, stats, networks, addrs, subnets };
}
async fetchLogs(id) {