* Copyright (C) 2021 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import React, { useContext, useEffect, useState } from 'react';
import cockpit from 'cockpit';
import { getPackageManager } from "packagemanager";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import {
Modal, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { BondDialog, getGhostSettings as getBondGhostSettings } from './bond.jsx';
import { BridgeDialog, getGhostSettings as getBridgeGhostSettings } from './bridge.jsx';
import { BridgePortDialog } from './bridgeport.jsx';
import { IpSettingsDialog } from './ip-settings.jsx';
import { TeamDialog, getGhostSettings as getTeamGhostSettings } from './team.jsx';
import { TeamPortDialog } from './teamport.jsx';
import { VlanDialog, getGhostSettings as getVlanGhostSettings } from './vlan.jsx';
import { WireGuardDialog, getWireGuardGhostSettings } from './wireguard.jsx';
import { MtuDialog } from './mtu.jsx';
import { MacDialog } from './mac.jsx';
import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { ModelContext } from './model-context.jsx';
import { useDialogs } from "dialogs.jsx";
import { install_dialog } from "cockpit-components-install-dialog.jsx";
import { read_os_release } from "os-release.js";
import { TypeaheadSelect } from "cockpit-components-typeahead-select";
import {
apply_group_member,
syn_click,
with_checkpoint, with_settings_checkpoint,
connection_devices,
settings_applier,
show_unexpected_error,
} from './interfaces.js';
import { isNonPersistentMultiCon } from './utils.js';
const _ = cockpit.gettext;
const NM_CAPABILITY_TEAM = 1;
export const MacMenu = ({ idPrefix, model, mac, setMAC }) => {
const [optionsMap, setOptionsMap] = useState([]);
useEffect(() => {
const macs = {};
model.list_interfaces().forEach(iface => {
if (iface.Device && iface.Device.HwAddress && iface.Device.HwAddress !== "00:00:00:00:00:00") {
if (!macs[iface.Device.HwAddress])
macs[iface.Device.HwAddress] = [];
macs[iface.Device.HwAddress].push(iface.Name);
}
});
const optionsMapInit = [];
Object.keys(macs).sort().forEach(mac => {
optionsMapInit.push({
content: cockpit.format("$0 ($1)", mac, macs[mac].join(", ")),
value: mac,
});
});
optionsMapInit.push(
{ content: _("Permanent"), value: "permanent" },
{ content: _("Preserve"), value: "preserve" },
{ content: _("Random"), value: "random" },
{ content: _("Stable"), value: "stable" },
);
setOptionsMap(optionsMapInit);
}, [model]);
const clearSelection = () => {
setMAC(undefined);
};
const onSelect = (_, selection) => {
setMAC(selection);
};
return (
<TypeaheadSelect toggleProps={{ id: idPrefix + "-mac-input" }}
isScrollable
placeholder=""
isCreatable
createOptionMessage={val => cockpit.format(_("Use $0"), val)}
onClearSelection={clearSelection}
onSelect={onSelect}
selected={mac}
selectOptions={optionsMap} />
);
};
export const MemberInterfaceChoices = ({ idPrefix, memberChoices, setMemberChoices, model, group }) => {
return (
<Stack id={idPrefix + "-interface-members-list"}>
{Object.keys(memberChoices).map((iface, idx) => (
<Checkbox data-iface={iface}
id={idPrefix + "-interface-members-" + iface}
isChecked={memberChoices[iface]}
key={iface}
label={iface}
onChange={(_event, checked) => setMemberChoices({ ...memberChoices, [iface]: checked })}
/>
))}
</Stack>
);
};
export const Name = ({ idPrefix, iface, setIface }) => {
return (
<FormGroup fieldId={idPrefix + "-interface-name-input"} label={_("Name")}>
<TextInput id={idPrefix + "-interface-name-input"} value={iface} onChange={(_event, value) => setIface(value)} />
</FormGroup>
);
};
export const NetworkModal = ({ dialogError, help, idPrefix, title, onSubmit, children, isFormHorizontal, isCreateDialog, submitDisabled = false }) => {
const Dialogs = useDialogs();
return (
<Modal id={idPrefix + "-dialog"} position="top" variant="medium"
isOpen
onClose={Dialogs.close}
>
<ModalHeader title={title} help={help} />
<ModalBody>
<Form id={idPrefix + "-body"} onSubmit={onSubmit} isHorizontal={isFormHorizontal !== false}>
{dialogError && <ModalError id={idPrefix + "-error"} dialogError={_("Failed to save settings")} dialogErrorDetail={dialogError} />}
{children}
</Form>
</ModalBody>
<ModalFooter>
<Button variant='primary' id={idPrefix + "-save"} onClick={onSubmit} isDisabled={submitDisabled}>
{isCreateDialog ? _("Add") : _("Save")}
</Button>
<Button variant='link' id={idPrefix + "-cancel"} onClick={Dialogs.close}>
{_("Cancel")}
</Button>
</ModalFooter>
</Modal>
);
};
export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) => {
const Dialogs = useDialogs();
const model = useContext(ModelContext);
if (type === "team" && !model.get_manager().Capabilities.includes(NM_CAPABILITY_TEAM))
return null;
const con = iface && iface.MainConnection;
const dev = iface && iface.Device;
const getName = () => {
let name;
for (let i = 0; i < 100; i++) {
name = type + i;
if (!model.find_interface(name))
break;
}
return name;
};
const newIfaceName = !iface ? getName() : undefined;
let settings = connectionSettings;
if (!settings) {
if (type === 'bond') settings = getBondGhostSettings({ newIfaceName });
if (type === 'vlan') settings = getVlanGhostSettings();
if (type === 'team') settings = getTeamGhostSettings({ newIfaceName });
if (type === 'bridge') settings = getBridgeGhostSettings({ newIfaceName });
if (type === 'wg') settings = getWireGuardGhostSettings({ newIfaceName });
}
const properties = { connection: con, dev, settings };
async function resolveDeps(type) {
if (type === 'wg') {
try {
await cockpit.script("command -v wg");
} catch {
const os_release = await read_os_release();
try {
await getPackageManager();
if (os_release.PLATFORM_ID !== "platform:el8")
await install_dialog("wireguard-tools");
} catch (exc) {
console.log("no package manager support");
}
}
}
}
function show() {
let dlg = null;
if (type === 'bond')
dlg = <BondDialog {...properties} />;
else if (type === 'vlan')
dlg = <VlanDialog {...properties} />;
else if (type === 'team')
dlg = <TeamDialog {...properties} />;
else if (type === 'bridge')
dlg = <BridgeDialog {...properties} />;
else if (type === 'wg')
dlg = <WireGuardDialog {...properties} />;
else if (type === 'mtu')
dlg = <MtuDialog {...properties} />;
else if (type === 'mac')
dlg = <MacDialog {...properties} />;
else if (type === 'teamport')
dlg = <TeamPortDialog {...properties} />;
else if (type === 'bridgeport')
dlg = <BridgePortDialog {...properties} />;
else if (type === 'ipv4')
dlg = <IpSettingsDialog topic="ipv4" {...properties} />;
else if (type === 'ipv6')
dlg = <IpSettingsDialog topic="ipv6" {...properties} />;
if (dlg)
resolveDeps(type)
.then(() => Dialogs.show(dlg))
.catch(err => console.error("NetworkAction Dialog failed:", err));
}
return (
<Button id={"networking-" + (!iface ? "add-" : "edit-") + type}
isInline={!!iface}
onClick={syn_click(model, show)}
variant={!iface ? "secondary" : "link"}>
{buttonText || _("edit")}
</Button>
);
};
function reactivateConnection({ con, dev }) {
if (con.Settings.connection.interface_name &&
con.Settings.connection.interface_name !== dev.Interface) {
return dev.disconnect()
.then(() => con.activate(null, null))
.catch(show_unexpected_error);
} else {
return con.activate(dev, null)
.catch(show_unexpected_error);
}
}
export const dialogSave = ({ model, dev, connection, members, membersInit, settings, setDialogError, onClose }) => {
const apply_settings = settings_applier(model, dev, connection);
const iface = settings.connection.interface_name ?? dev?.Interface;
const type = settings.connection.type;
const membersChanged = members ? Object.keys(membersInit).some(iface => membersInit[iface] !== members[iface]) : false;
model.set_operation_in_progress(true);
const modify = () => {
return ((members !== undefined)
? apply_group_member(members,
model,
apply_settings,
connection,
settings,
type)
: apply_settings(settings))
.then(() => {
onClose();
if (connection && iface)
cockpit.location.go([iface]);
else if (!connection && iface)
return model.synchronize().then(() => cockpit.location.go([iface]));
if (connection && dev?.ActiveConnection?.Connection === connection && !isNonPersistentMultiCon(connection)) {
return reactivateConnection({ con: connection, dev });
}
})
.catch(ex => setDialogError(typeof ex === 'string' ? ex : ex.message))
.then(() => model.set_operation_in_progress(false));
};
if (connection) {
with_settings_checkpoint(model, modify,
{
...(type !== 'vlan' && {
devices: (membersChanged ? [] : connection_devices(connection))
}),
hack_does_add_or_remove: type === 'vlan' || membersChanged || isNonPersistentMultiCon(connection),
rollback_on_failure: type !== 'vlan' && membersChanged
});
} else {
try {
with_checkpoint(
model,
modify,
{
fail_text: cockpit.format(_("Creating this $0 will break the connection to the server, and will make the administration UI unavailable."), type === 'vlan' ? 'VLAN' : type),
anyway_text: _("Create it"),
hack_does_add_or_remove: true,
rollback_on_failure: type !== 'vlan',
});
} catch (e) {
setDialogError(typeof e === 'string' ? e : e.message);
}
}
};