/* 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?.(); }} /> ); }