976 lines
25 KiB
TypeScript
976 lines
25 KiB
TypeScript
import 'dotenv/config'
|
|
|
|
// @ts-ignore - bcryptjs has its own types
|
|
import bcrypt from 'bcryptjs'
|
|
|
|
import * as db from '../src/db/index.js'
|
|
import { prisma } from '../src/db/prisma.js'
|
|
|
|
type SeedRole = 'admin' | 'user'
|
|
type HostStatus = 'online' | 'offline' | 'maintenance'
|
|
type HostArchitecture = 'x86_64' | 'aarch64'
|
|
type HostInstanceType = 'container' | 'vm' | 'both'
|
|
type PackageInstanceType = 'container' | 'vm'
|
|
type NetworkMode = 'nat' | 'nat_ipv6' | 'nat_ipv6_nat' | 'ipv6_only' | 'ipv6_nat'
|
|
type IoLimitMode = 'throughput' | 'iops'
|
|
|
|
type SeedCounter = {
|
|
created: number
|
|
updated: number
|
|
}
|
|
|
|
type SeedStats = {
|
|
users: SeedCounter
|
|
hosts: SeedCounter
|
|
packages: SeedCounter
|
|
plans: SeedCounter
|
|
}
|
|
|
|
type SeedUserSpec = {
|
|
username: string
|
|
email: string
|
|
role: SeedRole
|
|
password?: string
|
|
quota?: {
|
|
hostLimit: number
|
|
packageLimit: number
|
|
}
|
|
}
|
|
|
|
type SeedHostSpec = {
|
|
ownerUsername: string
|
|
name: string
|
|
url: string
|
|
location: string
|
|
countryCode: string
|
|
architecture: HostArchitecture
|
|
instanceType: HostInstanceType
|
|
status: HostStatus
|
|
tags: string[]
|
|
natPublicIp: string
|
|
natPortStart: number
|
|
natPortEnd: number
|
|
ipAddress: string
|
|
storageDriver: string
|
|
storageType: string
|
|
storageSize: number
|
|
enableApi: boolean
|
|
isInstalled: boolean
|
|
cpuAllowanceMax: number
|
|
memoryMax: number
|
|
}
|
|
|
|
type SeedPlanSpec = {
|
|
name: string
|
|
description: string
|
|
cpu: number
|
|
memory: number
|
|
disk: number
|
|
portLimit: number
|
|
snapshotLimit: number
|
|
backupLimit: number
|
|
siteLimit: number
|
|
swapSize: number
|
|
trafficLimit: bigint
|
|
trafficLimitSpeed: string
|
|
price: number
|
|
billingCycle: number
|
|
setupFee: number
|
|
isActive: boolean
|
|
sortOrder: number
|
|
slaGuarantee: number | null
|
|
}
|
|
|
|
type SeedPackageSpec = {
|
|
ownerUsername: string
|
|
name: string
|
|
description: string
|
|
hostNames: string[]
|
|
cpuMax: number
|
|
memoryMax: number
|
|
diskMax: number
|
|
bandwidthMax: number | null
|
|
networkMode: NetworkMode
|
|
instanceType: PackageInstanceType
|
|
privileged: boolean
|
|
nested: boolean
|
|
active: boolean
|
|
portLimit: number
|
|
snapshotLimit: number
|
|
backupLimit: number
|
|
siteLimit: number
|
|
monthlyTrafficLimit: bigint | null
|
|
ioLimitMode: IoLimitMode
|
|
limitsRead: string
|
|
limitsWrite: string
|
|
limitsReadIops: number
|
|
limitsWriteIops: number
|
|
limitsIngress: string
|
|
limitsEgress: string
|
|
limitsProcesses: number
|
|
limitsCpuPriority: number
|
|
bootAutostart: boolean
|
|
bootAutostartPriority: number
|
|
bootAutostartDelay: number
|
|
bootHostShutdownTimeout: number
|
|
globalShared: boolean
|
|
globalQuotaMultiplier: number | null
|
|
globalMaxInstances: number | null
|
|
allowInstanceDeletion: boolean
|
|
plans: SeedPlanSpec[]
|
|
}
|
|
|
|
const LOCAL_PASSWORD_SALT = '$2a$10$1234567890123456789012'
|
|
|
|
const stats: SeedStats = {
|
|
users: { created: 0, updated: 0 },
|
|
hosts: { created: 0, updated: 0 },
|
|
packages: { created: 0, updated: 0 },
|
|
plans: { created: 0, updated: 0 }
|
|
}
|
|
|
|
const userIds = new Map<string, number>()
|
|
const hostIds = new Map<string, number>()
|
|
|
|
function bytesFromGb(value: number): bigint {
|
|
return BigInt(value) * 1024n * 1024n * 1024n
|
|
}
|
|
|
|
function hostKey(ownerUsername: string, hostName: string): string {
|
|
return `${ownerUsername}:${hostName}`
|
|
}
|
|
|
|
function countPlans(packages: SeedPackageSpec[]): number {
|
|
return packages.reduce((total, pkg) => total + pkg.plans.length, 0)
|
|
}
|
|
|
|
function getUserId(username: string): number {
|
|
const userId = userIds.get(username)
|
|
if (!userId) {
|
|
throw new Error(`Missing seeded user: ${username}`)
|
|
}
|
|
return userId
|
|
}
|
|
|
|
function getHostId(ownerUsername: string, hostName: string): number {
|
|
const id = hostIds.get(hostKey(ownerUsername, hostName))
|
|
if (!id) {
|
|
throw new Error(`Missing seeded host: ${ownerUsername}/${hostName}`)
|
|
}
|
|
return id
|
|
}
|
|
|
|
async function buildPasswordHash(password: string): Promise<string> {
|
|
return bcrypt.hash(password, LOCAL_PASSWORD_SALT)
|
|
}
|
|
|
|
async function ensureUser(spec: SeedUserSpec): Promise<number> {
|
|
const existing = await db.findUserByUsername(spec.username)
|
|
const passwordHash = spec.password ? await buildPasswordHash(spec.password) : undefined
|
|
|
|
if (!existing) {
|
|
if (!passwordHash) {
|
|
throw new Error(`Cannot create user without password: ${spec.username}`)
|
|
}
|
|
|
|
const userId = await db.createUser(spec.username, spec.email, passwordHash, spec.role)
|
|
|
|
if (spec.quota) {
|
|
await db.updateUserQuotaLimits(userId, {
|
|
hostLimit: spec.quota.hostLimit,
|
|
packageLimit: spec.quota.packageLimit
|
|
})
|
|
}
|
|
|
|
stats.users.created += 1
|
|
userIds.set(spec.username, userId)
|
|
return userId
|
|
}
|
|
|
|
// 幂等处理:账号存在时只同步本脚本关心的字段,不重复创建脏数据。
|
|
await db.updateUser(existing.id, {
|
|
email: spec.email,
|
|
role: spec.role,
|
|
status: 'active',
|
|
...(passwordHash ? { passwordHash } : {})
|
|
})
|
|
|
|
const quota = await db.getUserQuota(existing.id)
|
|
if (!quota) {
|
|
await db.createUserQuota(existing.id, {
|
|
hostLimit: spec.quota?.hostLimit ?? 8,
|
|
packageLimit: spec.quota?.packageLimit ?? 8
|
|
})
|
|
} else if (spec.quota) {
|
|
await db.updateUserQuotaLimits(existing.id, {
|
|
hostLimit: spec.quota.hostLimit,
|
|
packageLimit: spec.quota.packageLimit
|
|
})
|
|
}
|
|
|
|
stats.users.updated += 1
|
|
userIds.set(spec.username, existing.id)
|
|
return existing.id
|
|
}
|
|
|
|
async function ensureHost(spec: SeedHostSpec): Promise<number> {
|
|
const ownerId = getUserId(spec.ownerUsername)
|
|
const existing = await db.getHostByUserAndName(ownerId, spec.name)
|
|
|
|
if (!existing) {
|
|
const hostId = await db.createHost({
|
|
userId: ownerId,
|
|
name: spec.name,
|
|
url: spec.url,
|
|
location: spec.location,
|
|
countryCode: spec.countryCode,
|
|
architecture: spec.architecture,
|
|
instanceType: spec.instanceType,
|
|
tags: spec.tags,
|
|
natPublicIp: spec.natPublicIp,
|
|
natPortStart: spec.natPortStart,
|
|
natPortEnd: spec.natPortEnd,
|
|
ipAddress: spec.ipAddress,
|
|
storageDriver: spec.storageDriver,
|
|
storageType: spec.storageType,
|
|
storageSize: spec.storageSize,
|
|
enableApi: spec.enableApi,
|
|
isInstalled: spec.isInstalled,
|
|
cpuAllowanceMax: spec.cpuAllowanceMax,
|
|
memoryMax: spec.memoryMax
|
|
})
|
|
|
|
await db.updateHostStatus(hostId, spec.status)
|
|
await db.updateHostResources(hostId, {
|
|
architecture: spec.architecture,
|
|
instanceType: spec.instanceType,
|
|
cpuUsed: 0,
|
|
memoryUsed: 0,
|
|
diskUsed: 0,
|
|
cpuAllowanceMax: spec.cpuAllowanceMax,
|
|
memoryMax: spec.memoryMax
|
|
})
|
|
|
|
stats.hosts.created += 1
|
|
hostIds.set(hostKey(spec.ownerUsername, spec.name), hostId)
|
|
return hostId
|
|
}
|
|
|
|
// 关键流程:优先复用现有 helper 同步节点展示信息、状态和配额上限。
|
|
await db.updateHost(existing.id, {
|
|
url: spec.url,
|
|
location: spec.location,
|
|
countryCode: spec.countryCode,
|
|
tags: spec.tags,
|
|
natPublicIp: spec.natPublicIp,
|
|
natPortStart: spec.natPortStart,
|
|
natPortEnd: spec.natPortEnd
|
|
})
|
|
await db.updateHostStatus(existing.id, spec.status)
|
|
await db.updateHostResources(existing.id, {
|
|
architecture: spec.architecture,
|
|
instanceType: spec.instanceType,
|
|
cpuUsed: 0,
|
|
memoryUsed: 0,
|
|
diskUsed: 0,
|
|
cpuAllowanceMax: spec.cpuAllowanceMax,
|
|
memoryMax: spec.memoryMax
|
|
})
|
|
|
|
stats.hosts.updated += 1
|
|
hostIds.set(hostKey(spec.ownerUsername, spec.name), existing.id)
|
|
return existing.id
|
|
}
|
|
|
|
async function ensurePackage(spec: SeedPackageSpec): Promise<number> {
|
|
const ownerId = getUserId(spec.ownerUsername)
|
|
const owner = await db.findUserByUsername(spec.ownerUsername)
|
|
|
|
if (!owner) {
|
|
throw new Error(`Missing package owner: ${spec.ownerUsername}`)
|
|
}
|
|
|
|
const hostIdsForPackage = spec.hostNames.map(hostName => getHostId(spec.ownerUsername, hostName))
|
|
const existing = await db.getPackageByUserAndName(ownerId, spec.name)
|
|
const packageData = {
|
|
name: spec.name,
|
|
description: spec.description,
|
|
cpuMax: spec.cpuMax,
|
|
memoryMax: spec.memoryMax,
|
|
diskMax: spec.diskMax,
|
|
bandwidthMax: spec.bandwidthMax,
|
|
networkMode: spec.networkMode,
|
|
instanceType: spec.instanceType,
|
|
hostIds: hostIdsForPackage,
|
|
privileged: spec.privileged,
|
|
nested: spec.nested,
|
|
active: spec.active,
|
|
portLimit: spec.portLimit,
|
|
snapshotLimit: spec.snapshotLimit,
|
|
backupLimit: spec.backupLimit,
|
|
siteLimit: spec.siteLimit,
|
|
monthlyTrafficLimit: spec.monthlyTrafficLimit,
|
|
ioLimitMode: spec.ioLimitMode,
|
|
limitsRead: spec.limitsRead,
|
|
limitsWrite: spec.limitsWrite,
|
|
limitsReadIops: spec.limitsReadIops,
|
|
limitsWriteIops: spec.limitsWriteIops,
|
|
limitsIngress: spec.limitsIngress,
|
|
limitsEgress: spec.limitsEgress,
|
|
limitsProcesses: spec.limitsProcesses,
|
|
limitsCpuPriority: spec.limitsCpuPriority,
|
|
bootAutostart: spec.bootAutostart,
|
|
bootAutostartPriority: spec.bootAutostartPriority,
|
|
bootAutostartDelay: spec.bootAutostartDelay,
|
|
bootHostShutdownTimeout: spec.bootHostShutdownTimeout,
|
|
globalShared: spec.globalShared,
|
|
globalQuotaMultiplier: spec.globalQuotaMultiplier,
|
|
globalMaxInstances: spec.globalMaxInstances,
|
|
allowInstanceDeletion: spec.allowInstanceDeletion
|
|
}
|
|
|
|
if (!existing) {
|
|
const packageId = await db.createPackage(
|
|
{
|
|
userId: ownerId,
|
|
...packageData
|
|
},
|
|
owner.role === 'admin'
|
|
)
|
|
|
|
stats.packages.created += 1
|
|
return packageId
|
|
}
|
|
|
|
await db.updatePackage(existing.id, packageData, owner.role === 'admin')
|
|
stats.packages.updated += 1
|
|
return existing.id
|
|
}
|
|
|
|
async function ensurePlans(packageId: number, plans: SeedPlanSpec[]): Promise<void> {
|
|
const existingPlans = await db.getPlansByPackageId(packageId)
|
|
const existingByName = new Map(existingPlans.map(plan => [plan.name, plan]))
|
|
const expectedNames = new Set<string>()
|
|
|
|
// 关键流程:方案使用 packageId + name 做唯一识别,重复执行时只更新不重复插入。
|
|
for (const plan of plans) {
|
|
expectedNames.add(plan.name)
|
|
const existing = existingByName.get(plan.name)
|
|
|
|
if (!existing) {
|
|
await db.createPlan({
|
|
packageId,
|
|
name: plan.name,
|
|
description: plan.description,
|
|
cpu: plan.cpu,
|
|
memory: plan.memory,
|
|
disk: plan.disk,
|
|
portLimit: plan.portLimit,
|
|
snapshotLimit: plan.snapshotLimit,
|
|
backupLimit: plan.backupLimit,
|
|
siteLimit: plan.siteLimit,
|
|
swapSize: plan.swapSize,
|
|
trafficLimit: plan.trafficLimit,
|
|
trafficLimitSpeed: plan.trafficLimitSpeed,
|
|
price: plan.price,
|
|
billingCycle: plan.billingCycle,
|
|
setupFee: plan.setupFee,
|
|
isActive: plan.isActive,
|
|
sortOrder: plan.sortOrder,
|
|
slaGuarantee: plan.slaGuarantee
|
|
})
|
|
stats.plans.created += 1
|
|
continue
|
|
}
|
|
|
|
await db.updatePlan(existing.id, {
|
|
description: plan.description,
|
|
cpu: plan.cpu,
|
|
memory: plan.memory,
|
|
disk: plan.disk,
|
|
portLimit: plan.portLimit,
|
|
snapshotLimit: plan.snapshotLimit,
|
|
backupLimit: plan.backupLimit,
|
|
siteLimit: plan.siteLimit,
|
|
swapSize: plan.swapSize,
|
|
trafficLimit: plan.trafficLimit,
|
|
trafficLimitSpeed: plan.trafficLimitSpeed,
|
|
price: plan.price,
|
|
billingCycle: plan.billingCycle,
|
|
setupFee: plan.setupFee,
|
|
isActive: plan.isActive,
|
|
sortOrder: plan.sortOrder,
|
|
slaGuarantee: plan.slaGuarantee
|
|
})
|
|
stats.plans.updated += 1
|
|
}
|
|
|
|
for (const existing of existingPlans) {
|
|
if (!expectedNames.has(existing.name) && existing.isActive) {
|
|
await db.updatePlan(existing.id, {
|
|
isActive: false
|
|
})
|
|
stats.plans.updated += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
const seedUsers: SeedUserSpec[] = [
|
|
{
|
|
username: 'admin',
|
|
email: 'admin@incudal.local',
|
|
role: 'admin'
|
|
},
|
|
{
|
|
username: 'seed-market-alpha',
|
|
email: 'seed-market-alpha@incudal.local',
|
|
role: 'user',
|
|
password: 'SeedAlpha123!',
|
|
quota: {
|
|
hostLimit: 8,
|
|
packageLimit: 8
|
|
}
|
|
},
|
|
{
|
|
username: 'seed-market-beta',
|
|
email: 'seed-market-beta@incudal.local',
|
|
role: 'user',
|
|
password: 'SeedBeta123!',
|
|
quota: {
|
|
hostLimit: 8,
|
|
packageLimit: 8
|
|
}
|
|
}
|
|
]
|
|
|
|
const seedHosts: SeedHostSpec[] = [
|
|
{
|
|
ownerUsername: 'admin',
|
|
name: 'seed-official-us-west',
|
|
url: 'https://seed-official-us-west.local',
|
|
location: 'Los Angeles, US',
|
|
countryCode: 'us',
|
|
architecture: 'x86_64',
|
|
instanceType: 'both',
|
|
status: 'online',
|
|
tags: ['seed', 'official', 'us-west'],
|
|
natPublicIp: '198.51.100.10',
|
|
natPortStart: 20000,
|
|
natPortEnd: 24999,
|
|
ipAddress: '10.10.0.10',
|
|
storageDriver: 'zfs',
|
|
storageType: 'loop',
|
|
storageSize: 400,
|
|
enableApi: true,
|
|
isInstalled: true,
|
|
cpuAllowanceMax: 3200,
|
|
memoryMax: 131072
|
|
},
|
|
{
|
|
ownerUsername: 'admin',
|
|
name: 'seed-official-eu-vm',
|
|
url: 'https://seed-official-eu-vm.local',
|
|
location: 'Frankfurt, DE',
|
|
countryCode: 'de',
|
|
architecture: 'x86_64',
|
|
instanceType: 'vm',
|
|
status: 'online',
|
|
tags: ['seed', 'official', 'eu-central'],
|
|
natPublicIp: '198.51.100.11',
|
|
natPortStart: 25000,
|
|
natPortEnd: 29999,
|
|
ipAddress: '10.10.0.11',
|
|
storageDriver: 'zfs',
|
|
storageType: 'loop',
|
|
storageSize: 600,
|
|
enableApi: true,
|
|
isInstalled: true,
|
|
cpuAllowanceMax: 4800,
|
|
memoryMax: 196608
|
|
},
|
|
{
|
|
ownerUsername: 'admin',
|
|
name: 'seed-official-asia-arm',
|
|
url: 'https://seed-official-asia-arm.local',
|
|
location: 'Singapore, SG',
|
|
countryCode: 'sg',
|
|
architecture: 'aarch64',
|
|
instanceType: 'container',
|
|
status: 'online',
|
|
tags: ['seed', 'official', 'apac'],
|
|
natPublicIp: '198.51.100.12',
|
|
natPortStart: 30000,
|
|
natPortEnd: 34999,
|
|
ipAddress: '10.10.0.12',
|
|
storageDriver: 'zfs',
|
|
storageType: 'loop',
|
|
storageSize: 320,
|
|
enableApi: true,
|
|
isInstalled: true,
|
|
cpuAllowanceMax: 2400,
|
|
memoryMax: 98304
|
|
},
|
|
{
|
|
ownerUsername: 'seed-market-alpha',
|
|
name: 'seed-market-jp-tokyo',
|
|
url: 'https://seed-market-jp-tokyo.local',
|
|
location: 'Tokyo, JP',
|
|
countryCode: 'jp',
|
|
architecture: 'x86_64',
|
|
instanceType: 'container',
|
|
status: 'online',
|
|
tags: ['seed', 'market', 'tokyo'],
|
|
natPublicIp: '203.0.113.20',
|
|
natPortStart: 35000,
|
|
natPortEnd: 38999,
|
|
ipAddress: '10.20.0.20',
|
|
storageDriver: 'zfs',
|
|
storageType: 'loop',
|
|
storageSize: 240,
|
|
enableApi: true,
|
|
isInstalled: true,
|
|
cpuAllowanceMax: 2000,
|
|
memoryMax: 65536
|
|
},
|
|
{
|
|
ownerUsername: 'seed-market-alpha',
|
|
name: 'seed-market-sg-latency',
|
|
url: 'https://seed-market-sg-latency.local',
|
|
location: 'Singapore, SG',
|
|
countryCode: 'sg',
|
|
architecture: 'x86_64',
|
|
instanceType: 'container',
|
|
status: 'online',
|
|
tags: ['seed', 'market', 'singapore'],
|
|
natPublicIp: '203.0.113.21',
|
|
natPortStart: 39000,
|
|
natPortEnd: 42999,
|
|
ipAddress: '10.20.0.21',
|
|
storageDriver: 'zfs',
|
|
storageType: 'loop',
|
|
storageSize: 220,
|
|
enableApi: true,
|
|
isInstalled: true,
|
|
cpuAllowanceMax: 1800,
|
|
memoryMax: 49152
|
|
},
|
|
{
|
|
ownerUsername: 'seed-market-beta',
|
|
name: 'seed-market-us-east',
|
|
url: 'https://seed-market-us-east.local',
|
|
location: 'Ashburn, US',
|
|
countryCode: 'us',
|
|
architecture: 'x86_64',
|
|
instanceType: 'both',
|
|
status: 'online',
|
|
tags: ['seed', 'market', 'us-east'],
|
|
natPublicIp: '203.0.113.22',
|
|
natPortStart: 43000,
|
|
natPortEnd: 46999,
|
|
ipAddress: '10.30.0.22',
|
|
storageDriver: 'zfs',
|
|
storageType: 'loop',
|
|
storageSize: 260,
|
|
enableApi: true,
|
|
isInstalled: true,
|
|
cpuAllowanceMax: 2200,
|
|
memoryMax: 65536
|
|
}
|
|
]
|
|
|
|
const seedPackages: SeedPackageSpec[] = [
|
|
{
|
|
ownerUsername: 'admin',
|
|
name: 'SEED Official Edge Container',
|
|
description: 'Public official container package for local smoke testing across US and APAC.',
|
|
hostNames: ['seed-official-us-west', 'seed-official-asia-arm'],
|
|
cpuMax: 300,
|
|
memoryMax: 8192,
|
|
diskMax: 102400,
|
|
bandwidthMax: 2000,
|
|
networkMode: 'nat_ipv6',
|
|
instanceType: 'container',
|
|
privileged: true,
|
|
nested: true,
|
|
active: true,
|
|
portLimit: 40,
|
|
snapshotLimit: 8,
|
|
backupLimit: 4,
|
|
siteLimit: 12,
|
|
monthlyTrafficLimit: bytesFromGb(800),
|
|
ioLimitMode: 'throughput',
|
|
limitsRead: '250MB',
|
|
limitsWrite: '250MB',
|
|
limitsReadIops: 1200,
|
|
limitsWriteIops: 1200,
|
|
limitsIngress: '1Gbit',
|
|
limitsEgress: '1Gbit',
|
|
limitsProcesses: 1200,
|
|
limitsCpuPriority: 10,
|
|
bootAutostart: true,
|
|
bootAutostartPriority: 20,
|
|
bootAutostartDelay: 15,
|
|
bootHostShutdownTimeout: 45,
|
|
globalShared: true,
|
|
globalQuotaMultiplier: 2,
|
|
globalMaxInstances: 6,
|
|
allowInstanceDeletion: true,
|
|
plans: [
|
|
{
|
|
name: 'Mini',
|
|
description: '25% CPU / 512MB memory / 15GB disk',
|
|
cpu: 25,
|
|
memory: 512,
|
|
disk: 15360,
|
|
portLimit: 10,
|
|
snapshotLimit: 3,
|
|
backupLimit: 1,
|
|
siteLimit: 2,
|
|
swapSize: 0,
|
|
trafficLimit: bytesFromGb(120),
|
|
trafficLimitSpeed: '3Mbit',
|
|
price: 690,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
slaGuarantee: 99.5
|
|
},
|
|
{
|
|
name: 'Standard',
|
|
description: '60% CPU / 2GB memory / 40GB disk',
|
|
cpu: 60,
|
|
memory: 2048,
|
|
disk: 40960,
|
|
portLimit: 20,
|
|
snapshotLimit: 5,
|
|
backupLimit: 2,
|
|
siteLimit: 5,
|
|
swapSize: 512,
|
|
trafficLimit: bytesFromGb(260),
|
|
trafficLimitSpeed: '5Mbit',
|
|
price: 1990,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 20,
|
|
slaGuarantee: 99.9
|
|
},
|
|
{
|
|
name: 'Compute',
|
|
description: '120% CPU / 4GB memory / 80GB disk',
|
|
cpu: 120,
|
|
memory: 4096,
|
|
disk: 81920,
|
|
portLimit: 35,
|
|
snapshotLimit: 8,
|
|
backupLimit: 4,
|
|
siteLimit: 8,
|
|
swapSize: 1024,
|
|
trafficLimit: bytesFromGb(520),
|
|
trafficLimitSpeed: '10Mbit',
|
|
price: 3990,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 30,
|
|
slaGuarantee: 99.95
|
|
}
|
|
]
|
|
},
|
|
{
|
|
ownerUsername: 'admin',
|
|
name: 'SEED Official EU VM',
|
|
description: 'Public official VM package for local billing and scheduler testing.',
|
|
hostNames: ['seed-official-eu-vm'],
|
|
cpuMax: 600,
|
|
memoryMax: 16384,
|
|
diskMax: 204800,
|
|
bandwidthMax: 3000,
|
|
networkMode: 'nat_ipv6',
|
|
instanceType: 'vm',
|
|
privileged: false,
|
|
nested: false,
|
|
active: true,
|
|
portLimit: 20,
|
|
snapshotLimit: 6,
|
|
backupLimit: 4,
|
|
siteLimit: 2,
|
|
monthlyTrafficLimit: bytesFromGb(1000),
|
|
ioLimitMode: 'iops',
|
|
limitsRead: '300MB',
|
|
limitsWrite: '300MB',
|
|
limitsReadIops: 2000,
|
|
limitsWriteIops: 2000,
|
|
limitsIngress: '1Gbit',
|
|
limitsEgress: '1Gbit',
|
|
limitsProcesses: 600,
|
|
limitsCpuPriority: 8,
|
|
bootAutostart: true,
|
|
bootAutostartPriority: 15,
|
|
bootAutostartDelay: 20,
|
|
bootHostShutdownTimeout: 60,
|
|
globalShared: true,
|
|
globalQuotaMultiplier: 1.5,
|
|
globalMaxInstances: 4,
|
|
allowInstanceDeletion: true,
|
|
plans: [
|
|
{
|
|
name: 'VM Starter',
|
|
description: '100% CPU / 2GB memory / 40GB disk',
|
|
cpu: 100,
|
|
memory: 2048,
|
|
disk: 40960,
|
|
portLimit: 6,
|
|
snapshotLimit: 2,
|
|
backupLimit: 1,
|
|
siteLimit: 0,
|
|
swapSize: 0,
|
|
trafficLimit: bytesFromGb(200),
|
|
trafficLimitSpeed: '5Mbit',
|
|
price: 2990,
|
|
billingCycle: 1,
|
|
setupFee: 500,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
slaGuarantee: 99.9
|
|
},
|
|
{
|
|
name: 'VM Business',
|
|
description: '200% CPU / 4GB memory / 80GB disk',
|
|
cpu: 200,
|
|
memory: 4096,
|
|
disk: 81920,
|
|
portLimit: 12,
|
|
snapshotLimit: 4,
|
|
backupLimit: 2,
|
|
siteLimit: 1,
|
|
swapSize: 0,
|
|
trafficLimit: bytesFromGb(400),
|
|
trafficLimitSpeed: '10Mbit',
|
|
price: 5990,
|
|
billingCycle: 1,
|
|
setupFee: 1000,
|
|
isActive: true,
|
|
sortOrder: 20,
|
|
slaGuarantee: 99.95
|
|
}
|
|
]
|
|
},
|
|
{
|
|
ownerUsername: 'seed-market-alpha',
|
|
name: 'SEED Market APAC Burst',
|
|
description: 'Hosted public package for local market testing in Tokyo and Singapore.',
|
|
hostNames: ['seed-market-jp-tokyo', 'seed-market-sg-latency'],
|
|
cpuMax: 240,
|
|
memoryMax: 6144,
|
|
diskMax: 81920,
|
|
bandwidthMax: 1500,
|
|
networkMode: 'nat_ipv6_nat',
|
|
instanceType: 'container',
|
|
privileged: true,
|
|
nested: true,
|
|
active: true,
|
|
portLimit: 32,
|
|
snapshotLimit: 6,
|
|
backupLimit: 3,
|
|
siteLimit: 6,
|
|
monthlyTrafficLimit: bytesFromGb(500),
|
|
ioLimitMode: 'throughput',
|
|
limitsRead: '180MB',
|
|
limitsWrite: '180MB',
|
|
limitsReadIops: 1000,
|
|
limitsWriteIops: 1000,
|
|
limitsIngress: '600Mbit',
|
|
limitsEgress: '600Mbit',
|
|
limitsProcesses: 900,
|
|
limitsCpuPriority: 9,
|
|
bootAutostart: true,
|
|
bootAutostartPriority: 25,
|
|
bootAutostartDelay: 10,
|
|
bootHostShutdownTimeout: 40,
|
|
globalShared: true,
|
|
globalQuotaMultiplier: 1.5,
|
|
globalMaxInstances: 5,
|
|
allowInstanceDeletion: true,
|
|
plans: [
|
|
{
|
|
name: 'Burst Nano',
|
|
description: '30% CPU / 768MB memory / 20GB disk',
|
|
cpu: 30,
|
|
memory: 768,
|
|
disk: 20480,
|
|
portLimit: 8,
|
|
snapshotLimit: 2,
|
|
backupLimit: 1,
|
|
siteLimit: 1,
|
|
swapSize: 0,
|
|
trafficLimit: bytesFromGb(80),
|
|
trafficLimitSpeed: '2Mbit',
|
|
price: 880,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
slaGuarantee: 99.5
|
|
},
|
|
{
|
|
name: 'Burst Plus',
|
|
description: '80% CPU / 2GB memory / 45GB disk',
|
|
cpu: 80,
|
|
memory: 2048,
|
|
disk: 46080,
|
|
portLimit: 16,
|
|
snapshotLimit: 4,
|
|
backupLimit: 2,
|
|
siteLimit: 3,
|
|
swapSize: 512,
|
|
trafficLimit: bytesFromGb(180),
|
|
trafficLimitSpeed: '5Mbit',
|
|
price: 2280,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 20,
|
|
slaGuarantee: 99.8
|
|
},
|
|
{
|
|
name: 'Burst Pro',
|
|
description: '140% CPU / 4GB memory / 70GB disk',
|
|
cpu: 140,
|
|
memory: 4096,
|
|
disk: 71680,
|
|
portLimit: 24,
|
|
snapshotLimit: 6,
|
|
backupLimit: 3,
|
|
siteLimit: 5,
|
|
swapSize: 1024,
|
|
trafficLimit: bytesFromGb(320),
|
|
trafficLimitSpeed: '8Mbit',
|
|
price: 3680,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 30,
|
|
slaGuarantee: 99.9
|
|
}
|
|
]
|
|
},
|
|
{
|
|
ownerUsername: 'seed-market-beta',
|
|
name: 'SEED Market US Balanced',
|
|
description: 'Hosted public package for local market testing in the US East region.',
|
|
hostNames: ['seed-market-us-east'],
|
|
cpuMax: 260,
|
|
memoryMax: 8192,
|
|
diskMax: 102400,
|
|
bandwidthMax: 1800,
|
|
networkMode: 'nat',
|
|
instanceType: 'container',
|
|
privileged: true,
|
|
nested: true,
|
|
active: true,
|
|
portLimit: 28,
|
|
snapshotLimit: 5,
|
|
backupLimit: 3,
|
|
siteLimit: 4,
|
|
monthlyTrafficLimit: bytesFromGb(420),
|
|
ioLimitMode: 'throughput',
|
|
limitsRead: '200MB',
|
|
limitsWrite: '200MB',
|
|
limitsReadIops: 1100,
|
|
limitsWriteIops: 1100,
|
|
limitsIngress: '700Mbit',
|
|
limitsEgress: '700Mbit',
|
|
limitsProcesses: 950,
|
|
limitsCpuPriority: 9,
|
|
bootAutostart: true,
|
|
bootAutostartPriority: 18,
|
|
bootAutostartDelay: 12,
|
|
bootHostShutdownTimeout: 40,
|
|
globalShared: true,
|
|
globalQuotaMultiplier: 1.2,
|
|
globalMaxInstances: 4,
|
|
allowInstanceDeletion: true,
|
|
plans: [
|
|
{
|
|
name: 'Balanced S',
|
|
description: '40% CPU / 1GB memory / 25GB disk',
|
|
cpu: 40,
|
|
memory: 1024,
|
|
disk: 25600,
|
|
portLimit: 10,
|
|
snapshotLimit: 2,
|
|
backupLimit: 1,
|
|
siteLimit: 1,
|
|
swapSize: 0,
|
|
trafficLimit: bytesFromGb(100),
|
|
trafficLimitSpeed: '3Mbit',
|
|
price: 1090,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 10,
|
|
slaGuarantee: 99.6
|
|
},
|
|
{
|
|
name: 'Balanced M',
|
|
description: '90% CPU / 3GB memory / 60GB disk',
|
|
cpu: 90,
|
|
memory: 3072,
|
|
disk: 61440,
|
|
portLimit: 20,
|
|
snapshotLimit: 4,
|
|
backupLimit: 2,
|
|
siteLimit: 2,
|
|
swapSize: 512,
|
|
trafficLimit: bytesFromGb(220),
|
|
trafficLimitSpeed: '6Mbit',
|
|
price: 2490,
|
|
billingCycle: 1,
|
|
setupFee: 0,
|
|
isActive: true,
|
|
sortOrder: 20,
|
|
slaGuarantee: 99.85
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
async function main(): Promise<void> {
|
|
await db.initPrismaDatabase()
|
|
|
|
// 先准备账号,再按账号归属写入节点、公开套餐和套餐方案。
|
|
for (const user of seedUsers) {
|
|
await ensureUser(user)
|
|
}
|
|
|
|
for (const host of seedHosts) {
|
|
await ensureHost(host)
|
|
}
|
|
|
|
for (const pkg of seedPackages) {
|
|
const packageId = await ensurePackage(pkg)
|
|
await ensurePlans(packageId, pkg.plans)
|
|
}
|
|
|
|
console.log('[seed:test-data] completed')
|
|
console.log(
|
|
`[seed:test-data] users created=${stats.users.created} updated=${stats.users.updated}, ` +
|
|
`hosts created=${stats.hosts.created} updated=${stats.hosts.updated}, ` +
|
|
`packages created=${stats.packages.created} updated=${stats.packages.updated}, ` +
|
|
`plans created=${stats.plans.created} updated=${stats.plans.updated}`
|
|
)
|
|
console.log(
|
|
`[seed:test-data] seeded targets users=${seedUsers.length}, hosts=${seedHosts.length}, ` +
|
|
`packages=${seedPackages.length}, plans=${countPlans(seedPackages)}`
|
|
)
|
|
}
|
|
|
|
void main().catch(async error => {
|
|
console.error('[seed:test-data] failed:', error)
|
|
process.exitCode = 1
|
|
}).finally(async () => {
|
|
await prisma.$disconnect()
|
|
})
|