Skip to content

Commit

Permalink
feat: ensure-vlan
Browse files Browse the repository at this point in the history
  • Loading branch information
hugojosefson committed Jul 9, 2024
1 parent 2bd3cf5 commit 7545363
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 14 deletions.
113 changes: 113 additions & 0 deletions src/incus-app-container-files/cli/commands/ensure-vlan/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { run, s, swallow } from "../../../deps.ts";
import { EnsureVlanInputOptions } from "../../config.ts";
import { BridgeName } from "../../things/bridge-name.ts";
import {
calculateNicParentName,
getNic,
NicParentVlanName,
} from "../../things/nic.ts";
import {
createVlanEtcNetworkInterfacesD,
type Vlan,
} from "../../things/vlan.ts";

/**
* Prints the current setpoint; the containers we want, according to configuration files.
* @param options
*/
export async function ensureVlan(
options: EnsureVlanInputOptions,
): Promise<void> {
if (!options.bridgeName) {
throw new Error("Missing --bridge-name.");
}
if (!options.vlan) {
throw new Error("Missing --vlan.");
}
if (!options.file) {
throw new Error("Missing --file.");
}
await actuallyEnsureVlan(options);
console.log("{}");
}

async function actuallyEnsureVlan(
options: Required<EnsureVlanInputOptions>,
): Promise<void> {
const contents = createVlanEtcNetworkInterfacesD(
options.bridgeName,
options.vlan,
);

const nicName = calculateNicParentName(
options.bridgeName,
options.vlan,
) as NicParentVlanName<BridgeName, Vlan>;

const existingNic: string[] | undefined = await getNic(nicName);
const existingFileContents: string | undefined = await Deno.readTextFile(
options.file,
).catch(swallow(Deno.errors.NotFound));

const wouldWill = options.dryRun ? "Would" : "Will";

if (existingNic) {
console.error(`VLAN ${options.vlan} exists.`);
if (existingFileContents === contents) {
console.error(
`The file ${options.file} matches. All good.`,
);
} else {
console.error(
`Any file ${options.file} does not match. ${wouldWill} take down VLAN ${options.vlan}, write the file, and bring up VLAN ${options.vlan}.`,
);
console.error({ current: s(existingFileContents), desired: s(contents) });
if (!options.dryRun) {
await takeDownVlanOrSwallow(nicName);
await writeVlanFile(options, contents);
await bringUpVlanOrThrow(nicName);
}
}
} else {
console.error(`VLAN ${options.vlan} does not exist.`);
if (existingFileContents === contents) {
console.error(
`The file ${options.file} matches. ${wouldWill} bring up VLAN ${options.vlan}.`,
);
if (!options.dryRun) {
await bringUpVlanOrThrow(nicName);
}
} else {
console.error(
`Any file ${options.file} does not match. ${wouldWill} write the file, and bring up VLAN ${options.vlan}.`,
);
console.error({ current: s(existingFileContents), desired: s(contents) });
if (!options.dryRun) {
await writeVlanFile(options, contents);
await bringUpVlanOrThrow(nicName);
}
}
}
}

async function writeVlanFile(
options: Required<EnsureVlanInputOptions>,
contents: string,
): Promise<void> {
await Deno.writeTextFile(options.file, contents);
}

async function bringUpVlanOrThrow(
nicName: NicParentVlanName<BridgeName, Vlan>,
): Promise<void> {
await run(["ifup", nicName]);
if (!await getNic(nicName)) {
throw new Error(`Failed to bring up ${nicName}.`);
}
}

async function takeDownVlanOrSwallow(
nicName: NicParentVlanName<BridgeName, Vlan>,
): Promise<void> {
await run(["ifdown", nicName]).catch(swallow(Error));
}
11 changes: 11 additions & 0 deletions src/incus-app-container-files/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import { SetupIncusOptions } from "./commands/setup-incus/mod.ts";
import { CommandName } from "./create-cli.ts";
import { createDeepMapKeys } from "../deps.ts";
import { camelCase, parseToml } from "../deps.ts";
import { BridgeName } from "./things/bridge-name.ts";
import { Vlan } from "./things/vlan.ts";

const CONFIG_FILE = `/etc/default/incus-app-container`;

export type Config<AppsDir extends AbsolutePath = AbsolutePath> =
& Partial<InputOptions<"setup-incus", AppsDir>>
& Partial<InputOptions<"setpoint", AppsDir>>
& Partial<InputOptions<"ensure-vlan", AppsDir>>
& Partial<InputOptionsPerCommand<AppsDir>>;

export type InputOptions<C extends CommandName, AppsDir extends AbsolutePath> =
C extends "setup-incus" ? SetupIncusOptions
: C extends "setpoint" ? SetpointInputOptions<AppsDir>
: C extends "ensure-vlan" ? EnsureVlanInputOptions
: never;

export type InputOptionsPerCommand<AppsDir extends AbsolutePath> = {
Expand All @@ -26,6 +30,13 @@ export type SetpointInputOptions<AppsDir extends AbsolutePath> = {
wrap: boolean;
};

export type EnsureVlanInputOptions = {
bridgeName: BridgeName;
vlan: Vlan;
file: AbsolutePath;
dryRun: boolean;
};

export async function getConfig<
AppsDir extends AbsolutePath = AbsolutePath,
>(): Promise<
Expand Down
43 changes: 43 additions & 0 deletions src/incus-app-container-files/cli/create-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import { AbsolutePath, isAbsolutePath } from "./things/absolute-path.ts";
import { DEFAULT_BRIDGE, isBridgeName } from "./things/bridge-name.ts";
import { setupIncus } from "./commands/setup-incus/mod.ts";
import { setpoint } from "./commands/setpoint/mod.ts";
import { ensureVlan } from "./commands/ensure-vlan/mod.ts";
import { Config } from "./config.ts";
import { NO_DEFAULT_VALUE } from "./things/no-default-value.ts";
import { castAndEnforceVlan } from "./things/vlan.ts";

export const COMMAND_NAMES = [
"create",
"list",
"setup-incus",
"setpoint",
"ensure-vlan",
] as const;
export type CommandName = typeof COMMAND_NAMES[number];

Expand Down Expand Up @@ -97,5 +100,45 @@ export async function createCli<
})
);

cli
.command(
"ensure-vlan",
"Ensure that the VLAN is created and activated.",
)
.option(
"--bridge-name <bridge-name>",
{
description: "Name of the network bridge device.",
cast: await enforceType(isBridgeName),
default: NO_DEFAULT_VALUE as unknown as string,
},
)
.option(
"--vlan <vlan>",
{
description: "The VLAN ID to create.",
cast: castAndEnforceVlan,
default: NO_DEFAULT_VALUE as unknown as string,
},
)
.option(
"--file <file>",
{
description:
"Path to the /etc/network/interfaces.d/* file to write to.",
cast: await enforceType(isAbsolutePath),
default: NO_DEFAULT_VALUE as unknown as string,
},
)
.option(
"--dry-run",
{
description: "Do not actually write to any file, or make any changes.",
cast: Boolean,
default: defaults?.["ensure-vlan"]?.dryRun ?? defaults.dryRun ?? false,
},
)
.action(ensureVlan);

return cli;
}
36 changes: 22 additions & 14 deletions src/incus-app-container-files/cli/things/vlan.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isNumber } from "../../deps.ts";
import { isNumber, outdent } from "../../deps.ts";
import { enforceType } from "../../type-guard.ts";
import { BridgeName } from "./bridge-name.ts";

Expand All @@ -16,33 +16,41 @@ export type Vlan = number;

export const enforceVlan = await enforceType(
isVlan,
`a number from ${VLAN_MIN} to ${VLAN_MAX}, or nothing for no VLAN`,
`a number from ${VLAN_MIN} to ${VLAN_MAX}`,
);

export const castAndEnforceVlan = (
vlanString?: string | number | typeof NO_DEFAULT_VALUE,
) => {
if (vlanString === undefined) return undefined;
if (vlanString === "") return undefined;
if (vlanString === NO_DEFAULT_VALUE) return undefined;
const vlan = isNumber(vlanString) ? vlanString : parseInt(vlanString, 10);
vlanString?: unknown,
): Vlan => {
if (typeof vlanString === "symbol" && vlanString === NO_DEFAULT_VALUE) {
return undefined!;
}
const vlan = parseInt(`${vlanString}`, 10);
return enforceVlan(vlan);
};

export type VlanEtcNetworkInterfacesDContent<
BN extends BridgeName,
V extends undefined | Vlan,
> = `auto ${NicParentName<BN, V>}
iface ${NicParentName<BN, V>} inet manual
vlan-raw-device ${BN}
`;

export function createVlanEtcNetworkInterfacesD<
BN extends BridgeName,
V extends undefined | Vlan,
>(
bridgeName: BN,
vlan: V,
): string {
): VlanEtcNetworkInterfacesDContent<BN, V> {
const nicParentName: NicParentName<BN, V> = calculateNicParentName(
bridgeName,
vlan,
);
return `
auto ${nicParentName}
iface ${nicParentName} inet manual
vlan-raw-device ${bridgeName}
`;
return (outdent`
auto ${nicParentName}
iface ${nicParentName} inet manual
vlan-raw-device ${bridgeName}
` + "\n") as VlanEtcNetworkInterfacesDContent<BN, V>;
}

0 comments on commit 7545363

Please sign in to comment.