Skip to content

Commit 7dea218

Browse files
authored
feat: [WD-32423] Add ACL default action for Instance NICs (#1772)
## Done Add ACL default action for Instance NICs ~~Currently NON-FUNCTIONAL~~ - [x] Show ACL default action selector in Instance NIC side panel - [x] Be able to edit default action selector (DAS) - [x] Be able to display accurate values in DAS - [x] Show read-only DAS accurately - [x] Disable DAS for non-OVN networks (but still fetch data on bridge networks) ## QA 1. Run the LXD-UI: - On the demo server via the link posted by @webteam-app below. This is only available for PRs created by collaborators of the repo. Ask @Kxiru or @edlerd for access. - With a local copy of this branch, [build and run as described in the docs](https://github.com/canonical/lxd-ui/blob/main/CONTRIBUTING.md#setting-up-for-development). 2. Perform the following QA steps: - Create an OVN network X - Create an ACL for OVN network rejecting everything and assign to OVN network - Create an instance and add custom network X to it. - Edit the custom network to have ACL default values that are NOT the same as those assigned to the network, such as Allow. - Save, and verify that the changes are reflected in the Read-Only and the YAML configuration. ## Screenshots <img width="1623" height="1021" alt="image" src="https://github.com/user-attachments/assets/69d2142e-8c54-4b2a-b6d8-3313232265db" />
2 parents 885f091 + 612cd6d commit 7dea218

11 files changed

Lines changed: 154 additions & 63 deletions

File tree

src/components/forms/NetworkDevicesForm/edit/NetworkDevicePanel.tsx

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ import {
3232
supportsNicDeviceAcls,
3333
getNetworkAcls,
3434
combineAcls,
35+
ovnType,
3536
} from "util/networks";
3637
import usePanelParams, { panels } from "util/usePanelParams";
38+
import NetworkDefaultACLSelector, {
39+
type Direction,
40+
} from "pages/networks/forms/NetworkDefaultACLSelector";
3741
import type { InstanceAndProfileFormikProps } from "types/forms/instanceAndProfileFormProps";
3842
import type { NetworkDeviceFormValues } from "types/forms/networkDevice";
3943

@@ -161,6 +165,10 @@ const NetworkDevicePanel: FC<Props> = ({
161165
acls: combinedAcls,
162166
ipv4: device?.["ipv4.address"] || "",
163167
ipv6: device?.["ipv6.address"] || "",
168+
security_acls_default_egress_action:
169+
device?.["security.acls.default.egress.action"] || "",
170+
security_acls_default_ingress_action:
171+
device?.["security.acls.default.ingress.action"] || "",
164172
};
165173
};
166174

@@ -185,6 +193,10 @@ const NetworkDevicePanel: FC<Props> = ({
185193
userSelectedAcls.length > 0 ? userSelectedAcls.join(",") : undefined,
186194
"ipv4.address": values.ipv4 || undefined,
187195
"ipv6.address": values.ipv6 || undefined,
196+
"security.acls.default.egress.action":
197+
values.security_acls_default_egress_action || undefined,
198+
"security.acls.default.ingress.action":
199+
values.security_acls_default_ingress_action || undefined,
188200
};
189201

190202
const originalDeviceName = deviceName;
@@ -265,6 +277,25 @@ const NetworkDevicePanel: FC<Props> = ({
265277
);
266278
}
267279

280+
const getDefaultEgressIngress = () => {
281+
// Return NIC device default egress/ingress actions if set, fallback to network defaults if not, or rejected if neither are set
282+
return {
283+
Egress:
284+
formik.values.security_acls_default_egress_action ??
285+
selectedNetwork?.config["security.acls.default.egress.action"] ??
286+
"",
287+
Ingress:
288+
formik.values.security_acls_default_ingress_action ??
289+
selectedNetwork?.config["security.acls.default.ingress.action"] ??
290+
"",
291+
};
292+
};
293+
294+
const directionField: Record<Direction, string> = {
295+
Egress: "security_acls_default_egress_action",
296+
Ingress: "security_acls_default_ingress_action",
297+
};
298+
268299
return (
269300
<SidePanel>
270301
<SidePanel.Header>
@@ -306,18 +337,6 @@ const NetworkDevicePanel: FC<Props> = ({
306337
error={formik.touched.network ? formik.errors.network : undefined}
307338
/>
308339

309-
<NetworkAclSelector
310-
project={project}
311-
selectedAcls={selectedAcls}
312-
setSelectedAcls={(selectedItems) => {
313-
void formik.setFieldValue("acls", selectedItems.join(","));
314-
}}
315-
inheritedAcls={networkAcls}
316-
canSelectManualAcls={supportsNicDeviceAcls(selectedNetwork)}
317-
help={getAclHelperText()}
318-
label="ACLs"
319-
/>
320-
321340
{isInstance && selectedNetwork && (
322341
<>
323342
<NetworkDeviceIPAddressEdit
@@ -332,6 +351,30 @@ const NetworkDevicePanel: FC<Props> = ({
332351
/>
333352
</>
334353
)}
354+
355+
<NetworkAclSelector
356+
project={project}
357+
selectedAcls={selectedAcls}
358+
setSelectedAcls={(selectedItems) => {
359+
void formik.setFieldValue("acls", selectedItems.join(","));
360+
}}
361+
inheritedAcls={networkAcls}
362+
canSelectManualAcls={supportsNicDeviceAcls(selectedNetwork)}
363+
help={getAclHelperText()}
364+
label="ACLs"
365+
/>
366+
367+
<NetworkDefaultACLSelector
368+
onChange={(fieldValue, value) => {
369+
void formik.setFieldValue(fieldValue, value);
370+
}}
371+
values={getDefaultEgressIngress()}
372+
disabled={
373+
formik.values.acls?.length === 0 ||
374+
selectedNetwork?.type !== ovnType
375+
}
376+
directionField={directionField}
377+
/>
335378
</Form>
336379
</ScrollableContainer>
337380
</SidePanel.Content>

src/components/forms/NetworkDevicesForm/read/NetworkDeviceRows.tsx

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { combineAcls, getNetworkAcls } from "util/networks";
1313
import { getDeviceAcls } from "util/devices";
1414
import NetworkRichChip from "pages/networks/NetworkRichChip";
1515
import { ROOT_PATH } from "util/rootPath";
16+
import NetworkDefaultACLRead from "pages/networks/forms/NetworkDefaultACLRead";
1617

1718
const getNetworkDeviceIpAddress = ({
1819
network,
@@ -86,9 +87,9 @@ export const getNetworkDeviceRows = ({
8687
if (device.type === "custom-nic") {
8788
rows.push(
8889
getConfigurationRowBase({
89-
className: "no-border-top",
90+
className: "no-border-top device-last-row",
9091
configuration: (
91-
<span
92+
<div
9293
className={classnames({
9394
"u-text--line-through": isDetached,
9495
})}
@@ -97,7 +98,7 @@ export const getNetworkDeviceRows = ({
9798
<Tooltip message="A custom network can be viewed and edited only from the YAML configuration">
9899
<Icon name="information" />
99100
</Tooltip>
100-
</span>
101+
</div>
101102
),
102103
inherited: null,
103104
override: null,
@@ -129,34 +130,6 @@ export const getNetworkDeviceRows = ({
129130
);
130131

131132
const acls = combineAcls(getNetworkAcls(network), getDeviceAcls(device));
132-
if (acls.length > 0) {
133-
rows.push(
134-
getConfigurationRowBase({
135-
className: classnames("no-border-top", {
136-
"device-last-row": !showIpAddresses,
137-
}),
138-
configuration: <div className="u-text--muted">ACLs</div>,
139-
inherited: (
140-
<div>
141-
<ExpandableList
142-
items={acls.map((acl) => (
143-
<ResourceLink
144-
key={acl}
145-
type="network-acl"
146-
value={acl}
147-
to={`${ROOT_PATH}/ui/project/${encodeURIComponent(project || "default")}/network-acl/${encodeURIComponent(acl)}`}
148-
className={classnames("acl-chip", {
149-
"u-text--line-through": isDetached,
150-
})}
151-
/>
152-
))}
153-
/>
154-
</div>
155-
),
156-
override: null,
157-
}),
158-
);
159-
}
160133

161134
if (showIpAddresses) {
162135
const families = ["IPv4", "IPv6"] as const;
@@ -171,7 +144,8 @@ export const getNetworkDeviceRows = ({
171144
rows.push(
172145
getConfigurationRowBase({
173146
className: classnames("no-border-top", {
174-
"device-last-row": index === activeIps.length - 1,
147+
"device-last-row":
148+
acls.length === 0 && index === activeIps.length - 1,
175149
}),
176150
configuration: <div className="u-text--muted">{family}</div>,
177151
inherited: (
@@ -190,6 +164,63 @@ export const getNetworkDeviceRows = ({
190164
);
191165
});
192166
}
167+
168+
if (acls.length > 0) {
169+
rows.push(
170+
getConfigurationRowBase({
171+
className: "no-border-top",
172+
configuration: <div className="u-text--muted">ACLs</div>,
173+
inherited: (
174+
<div>
175+
<ExpandableList
176+
items={acls.map((acl) => (
177+
<ResourceLink
178+
key={acl}
179+
type="network-acl"
180+
value={acl}
181+
to={`${ROOT_PATH}/ui/project/${encodeURIComponent(project || "default")}/network-acl/${encodeURIComponent(acl)}`}
182+
className={classnames("acl-chip", {
183+
"u-text--line-through": isDetached,
184+
})}
185+
/>
186+
))}
187+
/>
188+
</div>
189+
),
190+
override: null,
191+
}),
192+
);
193+
}
194+
195+
if (acls.length > 0) {
196+
const getDefaultEgressIngress = () => {
197+
return {
198+
Egress:
199+
device["security.acls.default.egress.action"] ??
200+
network.config["security.acls.default.egress.action"] ??
201+
"",
202+
Ingress:
203+
device["security.acls.default.ingress.action"] ??
204+
network.config["security.acls.default.ingress.action"] ??
205+
"",
206+
};
207+
};
208+
209+
rows.push(
210+
getConfigurationRowBase({
211+
className: classnames("no-border-top device-last-row acl-defaults", {
212+
"u-text--line-through": isDetached,
213+
}),
214+
configuration: <div className="u-text--muted"></div>,
215+
inherited: (
216+
<div>
217+
<NetworkDefaultACLRead values={getDefaultEgressIngress()} />
218+
</div>
219+
),
220+
override: null,
221+
}),
222+
);
223+
}
193224
}
194225

195226
return rows;

src/pages/networks/forms/NetworkAclSelector.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { FC, ReactNode } from "react";
22
import { MultiSelect } from "@canonical/react-components";
33
import { useNetworkAcls } from "context/useNetworkAcls";
4-
import classnames from "classnames";
54

65
interface Props {
76
project: string;
@@ -52,16 +51,7 @@ const NetworkAclSelector: FC<Props> = ({
5251

5352
return (
5453
<>
55-
{label && (
56-
<label
57-
className={classnames({
58-
"u-text--muted": !isEnabled,
59-
})}
60-
htmlFor={id}
61-
>
62-
{label}
63-
</label>
64-
)}
54+
{label && <label htmlFor={id}>{label}</label>}
6555
<MultiSelect
6656
items={toOptionList(
6757
availableAcls.map((acl) => acl.name),

src/pages/networks/forms/NetworkAcls.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import FormEditButton from "components/FormEditButton";
77
import { ensureEditMode } from "util/instanceEdit";
88
import NetworkAclSelector from "pages/networks/forms/NetworkAclSelector";
99
import { Label } from "@canonical/react-components";
10-
import NetworkDefaultACLSelector from "./NetworkDefaultACLSelector";
10+
import NetworkDefaultACLSelector, {
11+
type Direction,
12+
} from "./NetworkDefaultACLSelector";
1113
import NetworkDefaultACLRead from "./NetworkDefaultACLRead";
1214
import { ROOT_PATH } from "util/rootPath";
1315

@@ -23,6 +25,11 @@ const NetworkAcls: FC<Props> = ({ formik, project }) => {
2325
Ingress: formik.values.security_acls_default_ingress ?? "",
2426
};
2527

28+
const directionField: Record<Direction, string> = {
29+
Egress: "security_acls_default_egress",
30+
Ingress: "security_acls_default_ingress",
31+
};
32+
2633
return (
2734
<div className="general-field">
2835
<div className="general-field-label can-edit">
@@ -87,6 +94,7 @@ const NetworkAcls: FC<Props> = ({ formik, project }) => {
8794
}}
8895
values={defaultEgressIngress}
8996
disabled={formik.values.security_acls.length === 0}
97+
directionField={directionField}
9098
/>
9199
)}
92100
</div>

src/pages/networks/forms/NetworkDefaultACLRead.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ const NetworkDefaultACLRead: FC<{
1414
When no ACL rule matches:
1515
<br />
1616
<Icon name="arrow-left" className="network-default-acl-icon" />
17-
Egress traffic will be{" "}
17+
Egress traffic is:{" "}
1818
<code>{conjugateACLAction(egressAction || "reject")}</code>
1919
<br />
2020
<Icon name="arrow-right" className="network-default-acl-icon" />
21-
Ingress traffic will be{" "}
21+
Ingress traffic is:{" "}
2222
<code>{conjugateACLAction(ingressAction || "reject")}</code>
2323
</div>
2424
);

src/pages/networks/forms/NetworkDefaultACLSelector.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,17 @@ interface Props {
99
onChange: (fieldValue: string, value: string) => void;
1010
values?: Record<Direction, string>;
1111
disabled?: boolean;
12+
directionField: Record<Direction, string>;
1213
}
1314

1415
const NetworkDefaultACLSelector: FC<Props> = ({
1516
values,
1617
onChange,
1718
disabled,
19+
directionField,
1820
}) => {
1921
const DIRECTIONS: Direction[] = ["Egress", "Ingress"];
2022
const ACTIONS = ["allow", "drop", "reject"];
21-
const FIELD_BY_DIRECTION: Record<Direction, string> = {
22-
Egress: "security_acls_default_egress",
23-
Ingress: "security_acls_default_ingress",
24-
};
2523

2624
const options = [
2725
{ label: "Select option", value: "" },
@@ -50,7 +48,7 @@ const NetworkDefaultACLSelector: FC<Props> = ({
5048
className="u-no-margin--bottom"
5149
options={options}
5250
onChange={(e) => {
53-
onChange(FIELD_BY_DIRECTION[direction], e.target.value);
51+
onChange(directionField[direction], e.target.value);
5452
}}
5553
value={values?.[direction]}
5654
disabled={disabled}

src/sass/_network_device_form.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
.network-device-form {
2+
.acl-defaults .inherited {
3+
padding-top: 0;
4+
}
5+
6+
.custom-devices,
7+
.inherited-devices {
8+
.inherited {
9+
width: 12rem;
10+
}
11+
}
12+
213
.scrollable-container {
314
.content-details {
415
.inherited-network-device-configuration-table,

src/types/device.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface LxdNicDevice {
2727
"ipv4.address"?: string;
2828
"ipv6.address"?: string;
2929
"security.acls"?: string;
30+
"security.acls.default.egress.action"?: string;
31+
"security.acls.default.ingress.action"?: string;
3032
}
3133

3234
export interface LxdPhysicalGPUDevice {

src/types/forms/networkDevice.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ export interface NetworkDeviceFormValues {
44
acls?: string;
55
ipv4?: string;
66
ipv6?: string;
7+
security_acls_default_ingress_action?: string;
8+
security_acls_default_egress_action?: string;
79
}

src/util/devices.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ export const isCustomNic = (device: LxdDeviceValue): boolean => {
8585
"security.acls",
8686
"ipv4.address",
8787
"ipv6.address",
88+
"security.acls.default.egress.action",
89+
"security.acls.default.ingress.action",
8890
];
8991
return (
9092
isNicDevice(device) &&

0 commit comments

Comments
 (0)