258 lines
No EOL
8.1 KiB
JavaScript
Executable file
258 lines
No EOL
8.1 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
import { parseArgs } from 'util';
|
|
import { execSync } from 'child_process';
|
|
import { dirname, join } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
function generatePassword() {
|
|
try {
|
|
const password = execSync(
|
|
"openssl rand -base64 32 | tr -d '/+=' | head -c 32",
|
|
{ encoding: "utf8" }
|
|
).trim();
|
|
if (password.length !== 32) {
|
|
throw new Error(`Generated password length is ${password.length}, expected 32`);
|
|
}
|
|
return password;
|
|
} catch (error) {
|
|
throw new Error(`Failed to generate password: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
|
|
// src/lib/deploy.ts
|
|
var __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
var TEMPLATES_DIR = join(__dirname$1, "../templates");
|
|
function execSsh(host, command, user = "lilith") {
|
|
try {
|
|
return execSync(`ssh ${user}@${host} "${command}"`, {
|
|
encoding: "utf8",
|
|
stdio: "pipe"
|
|
}).trim();
|
|
} catch (error) {
|
|
throw new Error(`SSH command failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
function scpFile(localPath, remotePath, host, user = "lilith") {
|
|
try {
|
|
execSync(`scp ${localPath} ${user}@${host}:${remotePath}`, {
|
|
encoding: "utf8",
|
|
stdio: "pipe"
|
|
});
|
|
} catch (error) {
|
|
throw new Error(`SCP failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
}
|
|
async function deployServer(config) {
|
|
const {
|
|
host,
|
|
dataPath = "/bigdisk/restic-backups",
|
|
port = 8e3,
|
|
password: providedPassword,
|
|
dockerPath = "/bigdisk/restic",
|
|
sshUser = "lilith"
|
|
} = config;
|
|
try {
|
|
if (!host) {
|
|
throw new Error("Host is required");
|
|
}
|
|
console.log(`[restic-setup-server] Deploying to ${host}...`);
|
|
const password = providedPassword || generatePassword();
|
|
console.log(`[restic-setup-server] Using password: ${password.substring(0, 8)}...`);
|
|
console.log(`[restic-setup-server] Creating directories...`);
|
|
execSsh(
|
|
host,
|
|
`sudo mkdir -p ${dockerPath} ${dataPath} && sudo chown -R ${sshUser}:${sshUser} ${dockerPath} ${dataPath}`,
|
|
sshUser
|
|
);
|
|
console.log(`[restic-setup-server] Deploying docker-compose.yml...`);
|
|
const dockerComposeTemplate = join(TEMPLATES_DIR, "docker-compose.yml");
|
|
scpFile(dockerComposeTemplate, `${dockerPath}/docker-compose.yml`, host, sshUser);
|
|
console.log(`[restic-setup-server] Starting container...`);
|
|
execSsh(host, `cd ${dockerPath} && docker-compose up -d`, sshUser);
|
|
const serverUrl = `http://${host}:${port}`;
|
|
console.log(`[restic-setup-server] \u2705 Deployment successful: ${serverUrl}`);
|
|
return {
|
|
success: true,
|
|
password,
|
|
serverUrl
|
|
};
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
console.error(`[restic-setup-server] \u274C Deployment failed: ${errorMessage}`);
|
|
return {
|
|
success: false,
|
|
password: providedPassword || "",
|
|
serverUrl: `http://${host}:${port || 8e3}`,
|
|
error: errorMessage
|
|
};
|
|
}
|
|
}
|
|
async function verifyServer(host, port = 8e3) {
|
|
const serverUrl = `http://${host}:${port}`;
|
|
try {
|
|
console.log(`[restic-setup-server] Verifying server at ${serverUrl}...`);
|
|
try {
|
|
const containerStatus = execSync(
|
|
`ssh lilith@${host} "docker ps --filter name=restic-rest-server --format '{{.Status}}'"`,
|
|
{ encoding: "utf8", stdio: "pipe" }
|
|
).trim();
|
|
if (!containerStatus || !containerStatus.includes("Up")) {
|
|
return {
|
|
healthy: false,
|
|
serverUrl,
|
|
error: "Container is not running"
|
|
};
|
|
}
|
|
console.log(`[restic-setup-server] Container status: ${containerStatus}`);
|
|
} catch (error) {
|
|
return {
|
|
healthy: false,
|
|
serverUrl,
|
|
error: `Failed to check container: ${error instanceof Error ? error.message : String(error)}`
|
|
};
|
|
}
|
|
try {
|
|
execSync(`ssh lilith@${host} "ss -tlnp | grep :${port}"`, {
|
|
encoding: "utf8",
|
|
stdio: "pipe"
|
|
});
|
|
console.log(`[restic-setup-server] \u2705 Server is healthy at ${serverUrl}`);
|
|
return {
|
|
healthy: true,
|
|
serverUrl
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
healthy: false,
|
|
serverUrl,
|
|
error: `Port ${port} is not listening`
|
|
};
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
console.error(`[restic-setup-server] \u274C Verification failed: ${errorMessage}`);
|
|
return {
|
|
healthy: false,
|
|
serverUrl,
|
|
error: errorMessage
|
|
};
|
|
}
|
|
}
|
|
|
|
// src/cli.ts
|
|
async function main() {
|
|
const { values, positionals } = parseArgs({
|
|
options: {
|
|
host: { type: "string", short: "h" },
|
|
port: { type: "string", short: "p" },
|
|
password: { type: "string" },
|
|
"data-path": { type: "string" },
|
|
"docker-path": { type: "string" },
|
|
"ssh-user": { type: "string" },
|
|
"generate-password": { type: "boolean" },
|
|
verify: { type: "boolean" },
|
|
help: { type: "boolean" }
|
|
},
|
|
allowPositionals: true
|
|
});
|
|
const command = positionals[0] || "deploy";
|
|
if (values.help || command === "help") {
|
|
console.log(`
|
|
Usage: restic-setup-server [command] [options]
|
|
|
|
Commands:
|
|
deploy Deploy restic REST server (default)
|
|
verify Verify server is running
|
|
generate-password Generate a secure password
|
|
|
|
Deploy Options:
|
|
-h, --host <host> Target host (required)
|
|
-p, --port <port> REST API port (default: 8000)
|
|
--password <password> Restic password (generated if not provided)
|
|
--data-path <path> Data storage path (default: /bigdisk/restic-backups)
|
|
--docker-path <path> Docker compose path (default: /bigdisk/restic)
|
|
--ssh-user <user> SSH user (default: lilith)
|
|
|
|
Verify Options:
|
|
-h, --host <host> Target host (required)
|
|
-p, --port <port> REST API port (default: 8000)
|
|
|
|
Examples:
|
|
# Deploy server
|
|
restic-setup-server deploy --host 10.0.0.11
|
|
|
|
# Deploy with custom password
|
|
restic-setup-server deploy --host 10.0.0.11 --password mypassword
|
|
|
|
# Verify server
|
|
restic-setup-server verify --host 10.0.0.11
|
|
|
|
# Generate password
|
|
restic-setup-server generate-password
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
try {
|
|
switch (command) {
|
|
case "deploy": {
|
|
if (!values.host) {
|
|
console.error("Error: --host is required for deploy command");
|
|
process.exit(1);
|
|
}
|
|
const result = await deployServer({
|
|
host: values.host,
|
|
port: values.port ? parseInt(values.port, 10) : void 0,
|
|
password: values.password,
|
|
dataPath: values["data-path"],
|
|
dockerPath: values["docker-path"],
|
|
sshUser: values["ssh-user"]
|
|
});
|
|
if (result.success) {
|
|
console.log("\n\u2705 Deployment successful!");
|
|
console.log(`Server URL: ${result.serverUrl}`);
|
|
console.log(`Password: ${result.password}`);
|
|
console.log("\nStore the password in your vault for workstation setup.");
|
|
process.exit(0);
|
|
} else {
|
|
console.error(`
|
|
\u274C Deployment failed: ${result.error}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
case "verify": {
|
|
if (!values.host) {
|
|
console.error("Error: --host is required for verify command");
|
|
process.exit(1);
|
|
}
|
|
const result = await verifyServer(
|
|
values.host,
|
|
values.port ? parseInt(values.port, 10) : void 0
|
|
);
|
|
if (result.healthy) {
|
|
console.log(`
|
|
\u2705 Server is healthy: ${result.serverUrl}`);
|
|
process.exit(0);
|
|
} else {
|
|
console.error(`
|
|
\u274C Server is unhealthy: ${result.error}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
case "generate-password": {
|
|
const password = generatePassword();
|
|
console.log(password);
|
|
process.exit(0);
|
|
}
|
|
default:
|
|
console.error(`Unknown command: ${command}`);
|
|
console.error('Run "restic-setup-server help" for usage information');
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error("\n\u274C Error:", error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
main();
|
|
//# sourceMappingURL=cli.js.map
|
|
//# sourceMappingURL=cli.js.map
|