All checks were successful
continuous-integration/drone/push Build is passing
343 lines
9.6 KiB
TypeScript
343 lines
9.6 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-base-to-string */
|
|
|
|
import Editor from "@monaco-editor/react";
|
|
import BookIcon from "@mui/icons-material/Book";
|
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
import { Grid, IconButton, InputAdornment, Tooltip } from "@mui/material";
|
|
import React from "react";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import YAML from "yaml";
|
|
import { VMInfo } from "../../api/VMApi";
|
|
import { RouterLink } from "../RouterLink";
|
|
import { CheckboxInput } from "./CheckboxInput";
|
|
import { EditSection } from "./EditSection";
|
|
import { SelectInput } from "./SelectInput";
|
|
import { TextInput } from "./TextInput";
|
|
|
|
interface CloudInitProps {
|
|
vm: VMInfo;
|
|
onChange?: () => void;
|
|
editable: boolean;
|
|
}
|
|
|
|
export function CloudInitEditor(p: CloudInitProps): React.ReactElement {
|
|
return (
|
|
<>
|
|
<EditSection>
|
|
{/* Attach cloud init disk */}
|
|
<CheckboxInput
|
|
{...p}
|
|
label="Attach Cloud Init disk"
|
|
checked={p.vm.cloud_init.attach_config}
|
|
onValueChange={(v) => {
|
|
p.vm.cloud_init.attach_config = v;
|
|
p.onChange?.();
|
|
}}
|
|
/>
|
|
</EditSection>
|
|
<Grid container spacing={2}>
|
|
<CloudInitMetadata
|
|
{...p}
|
|
editable={p.editable && p.vm.cloud_init.attach_config}
|
|
/>
|
|
<CloudInitRawUserData
|
|
{...p}
|
|
editable={p.editable && p.vm.cloud_init.attach_config}
|
|
/>
|
|
<CloudInitNetworkConfig
|
|
{...p}
|
|
editable={p.editable && p.vm.cloud_init.attach_config}
|
|
/>
|
|
<CloudInitUserDataAssistant
|
|
{...p}
|
|
editable={p.editable && p.vm.cloud_init.attach_config}
|
|
/>
|
|
</Grid>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function CloudInitMetadata(p: CloudInitProps): React.ReactElement {
|
|
// Regenerate instance id
|
|
const reGenerateInstanceId = () => {
|
|
p.vm.cloud_init.instance_id = uuidv4();
|
|
p.onChange?.();
|
|
};
|
|
|
|
return (
|
|
<EditSection title="Metadata">
|
|
{/* Instance ID */}
|
|
<TextInput
|
|
{...p}
|
|
label="Instance ID"
|
|
value={p.vm.cloud_init.instance_id}
|
|
onValueChange={(v) => {
|
|
p.vm.cloud_init.instance_id = v;
|
|
p.onChange?.();
|
|
}}
|
|
endAdornment={
|
|
p.editable ? (
|
|
<InputAdornment position="end">
|
|
<Tooltip title="Generate a new instance ID">
|
|
<IconButton onClick={reGenerateInstanceId}>
|
|
<RefreshIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</InputAdornment>
|
|
) : (
|
|
<></>
|
|
)
|
|
}
|
|
/>
|
|
|
|
{/* Instance hostname */}
|
|
<TextInput
|
|
{...p}
|
|
label="Local hostname"
|
|
value={p.vm.cloud_init.local_hostname}
|
|
onValueChange={(v) => {
|
|
p.vm.cloud_init.local_hostname = v;
|
|
p.onChange?.();
|
|
}}
|
|
/>
|
|
|
|
{/* Data source mode */}
|
|
<SelectInput
|
|
{...p}
|
|
label="Data source mode"
|
|
value={p.vm.cloud_init.dsmode}
|
|
onValueChange={(v) => {
|
|
p.vm.cloud_init.dsmode = v as any;
|
|
p.onChange?.();
|
|
}}
|
|
options={[
|
|
{ label: "None", value: undefined },
|
|
{ value: "Net" },
|
|
{ value: "Local" },
|
|
]}
|
|
/>
|
|
</EditSection>
|
|
);
|
|
}
|
|
|
|
function CloudInitRawUserData(p: CloudInitProps): React.ReactElement {
|
|
return (
|
|
<EditSection
|
|
title="User data"
|
|
actions={
|
|
<RouterLink
|
|
target="_blank"
|
|
to="https://cloudinit.readthedocs.io/en/latest/reference/index.html"
|
|
>
|
|
<Tooltip title="Official reference">
|
|
<IconButton size="small">
|
|
<BookIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</RouterLink>
|
|
}
|
|
>
|
|
<Editor
|
|
theme="vs-dark"
|
|
options={{
|
|
readOnly: !p.editable,
|
|
quickSuggestions: { other: true, comments: true, strings: true },
|
|
wordWrap: "on",
|
|
}}
|
|
language="yaml"
|
|
height={"30vh"}
|
|
value={p.vm.cloud_init.user_data}
|
|
onChange={(v) => {
|
|
p.vm.cloud_init.user_data = v ?? "";
|
|
p.onChange?.();
|
|
}}
|
|
/>
|
|
</EditSection>
|
|
);
|
|
}
|
|
|
|
function CloudInitNetworkConfig(p: CloudInitProps): React.ReactElement {
|
|
if (!p.editable && !p.vm.cloud_init.network_configuration) return <></>;
|
|
return (
|
|
<EditSection
|
|
title="Network configuration"
|
|
actions={
|
|
<RouterLink
|
|
target="_blank"
|
|
to="https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html"
|
|
>
|
|
<Tooltip title="Official network configuration reference">
|
|
<IconButton size="small">
|
|
<BookIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</RouterLink>
|
|
}
|
|
>
|
|
<Editor
|
|
theme="vs-dark"
|
|
options={{
|
|
readOnly: !p.editable,
|
|
quickSuggestions: { other: true, comments: true, strings: true },
|
|
wordWrap: "on",
|
|
}}
|
|
language="yaml"
|
|
height={"30vh"}
|
|
value={p.vm.cloud_init.network_configuration ?? ""}
|
|
onChange={(v) => {
|
|
if (v && v !== "") p.vm.cloud_init.network_configuration = v;
|
|
else p.vm.cloud_init.network_configuration = undefined;
|
|
p.onChange?.();
|
|
}}
|
|
/>
|
|
</EditSection>
|
|
);
|
|
}
|
|
|
|
function CloudInitUserDataAssistant(p: CloudInitProps): React.ReactElement {
|
|
const user_data = React.useMemo(() => {
|
|
return YAML.parseDocument(p.vm.cloud_init.user_data);
|
|
}, [p.vm.cloud_init.user_data]);
|
|
|
|
const onChange = () => {
|
|
p.vm.cloud_init.user_data = user_data.toString();
|
|
|
|
if (!p.vm.cloud_init.user_data.startsWith("#cloud-config"))
|
|
p.vm.cloud_init.user_data = `#cloud-config\n${p.vm.cloud_init.user_data}`;
|
|
|
|
p.onChange?.();
|
|
};
|
|
|
|
const SYSTEMD_NOT_SERIAL = `/bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && sed -i 's/quiet splash//g' /etc/default/grub && update-grub"`;
|
|
|
|
return (
|
|
<EditSection title="User data assistant">
|
|
<CloudInitTextInput
|
|
editable={p.editable}
|
|
name="Default user name"
|
|
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
|
|
attrPath={["user", "name"]}
|
|
onChange={onChange}
|
|
yaml={user_data}
|
|
/>
|
|
<CloudInitTextInput
|
|
editable={p.editable}
|
|
name="Default user password"
|
|
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
|
|
attrPath={["password"]}
|
|
onChange={onChange}
|
|
yaml={user_data}
|
|
/>
|
|
<CloudInitBooleanInput
|
|
editable={p.editable}
|
|
name="Expire password to require new password on next login"
|
|
yaml={user_data}
|
|
attrPath={["chpasswd", "expire"]}
|
|
onChange={onChange}
|
|
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
|
|
/>
|
|
<br />
|
|
<CloudInitBooleanInput
|
|
editable={p.editable}
|
|
name="Enable SSH password auth"
|
|
yaml={user_data}
|
|
attrPath={["ssh_pwauth"]}
|
|
onChange={onChange}
|
|
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#set-passwords"
|
|
/>
|
|
<CloudInitTextInput
|
|
editable={p.editable}
|
|
name="Keyboard layout"
|
|
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#keyboard"
|
|
attrPath={["keyboard", "layout"]}
|
|
onChange={onChange}
|
|
yaml={user_data}
|
|
/>
|
|
<CloudInitTextInput
|
|
editable={p.editable}
|
|
name="Final message"
|
|
refUrl="https://cloudinit.readthedocs.io/en/latest/reference/modules.html#final-message"
|
|
attrPath={["final_message"]}
|
|
onChange={onChange}
|
|
yaml={user_data}
|
|
/>
|
|
{/* /bin/sh -c "rm -f /etc/default/grub.d/50-cloudimg-settings.cfg && update-grub" */}
|
|
<CheckboxInput
|
|
editable={p.editable}
|
|
label="Show all startup messages on tty1, not serial"
|
|
checked={
|
|
!!(user_data.get("runcmd") as any)?.items.find(
|
|
(a: any) => a.value === SYSTEMD_NOT_SERIAL
|
|
)
|
|
}
|
|
onValueChange={(c) => {
|
|
if (!user_data.getIn(["runcmd"])) user_data.addIn(["runcmd"], []);
|
|
|
|
const runcmd = user_data.getIn(["runcmd"]) as any;
|
|
|
|
if (c) {
|
|
runcmd.addIn([], SYSTEMD_NOT_SERIAL);
|
|
} else {
|
|
const idx = runcmd.items.findIndex(
|
|
(o: any) => o.value === SYSTEMD_NOT_SERIAL
|
|
);
|
|
runcmd.items.splice(idx, 1);
|
|
}
|
|
onChange();
|
|
}}
|
|
/>
|
|
</EditSection>
|
|
);
|
|
}
|
|
|
|
function CloudInitTextInput(p: {
|
|
editable: boolean;
|
|
name: string;
|
|
refUrl: string;
|
|
attrPath: Iterable<unknown>;
|
|
yaml: YAML.Document;
|
|
onChange?: () => void;
|
|
}): React.ReactElement {
|
|
return (
|
|
<TextInput
|
|
editable={p.editable}
|
|
label={p.name}
|
|
value={String(p.yaml.getIn(p.attrPath) ?? "")}
|
|
onValueChange={(v) => {
|
|
if (v !== undefined) p.yaml.setIn(p.attrPath, v);
|
|
else p.yaml.deleteIn(p.attrPath);
|
|
p.onChange?.();
|
|
}}
|
|
endAdornment={
|
|
<RouterLink to={p.refUrl} target="_blank">
|
|
<IconButton size="small">
|
|
<BookIcon />
|
|
</IconButton>
|
|
</RouterLink>
|
|
}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function CloudInitBooleanInput(p: {
|
|
editable: boolean;
|
|
name: string;
|
|
refUrl: string;
|
|
attrPath: Iterable<unknown>;
|
|
yaml: YAML.Document;
|
|
onChange?: () => void;
|
|
}): React.ReactElement {
|
|
return (
|
|
<CheckboxInput
|
|
editable={p.editable}
|
|
label={p.name}
|
|
checked={p.yaml.getIn(p.attrPath) === true}
|
|
onValueChange={(v) => {
|
|
if (v) p.yaml.setIn(p.attrPath, v);
|
|
else p.yaml.deleteIn(p.attrPath);
|
|
p.onChange?.();
|
|
}}
|
|
/>
|
|
);
|
|
}
|