Files
incudal/server/scripts/seed-bulk-local-data.ts
2026-06-20 14:26:29 +08:00

1691 lines
54 KiB
TypeScript

import 'dotenv/config'
// @ts-ignore - bcryptjs ships its own types in this repo setup
import bcrypt from 'bcryptjs'
import { prisma } from '../src/db/prisma.js'
type SeedRole = 'admin' | 'user'
type SeedUserStatus = 'active' | 'banned'
type SeedHostStatus = 'online' | 'offline' | 'maintenance'
type SeedHostInstanceType = 'container' | 'vm' | 'both'
type SeedPackageInstanceType = 'container' | 'vm'
type SeedNetworkMode = 'nat' | 'nat_ipv6' | 'nat_ipv6_nat' | 'ipv6_only' | 'ipv6_nat'
type SeedInstanceStatus = 'running' | 'stopped' | 'suspended' | 'error'
type SeedTrafficStatus = 'NORMAL' | 'WARNING' | 'LIMITED'
type SeedProtocol = 'tcp' | 'udp'
type SeedProxyStatus = 'pending' | 'active' | 'error'
type SeedBackupStatus = 'ready' | 'error'
type SeedTicketPriority = 'low' | 'normal' | 'high' | 'urgent'
type SeedTicketStatus = 'open' | 'in_progress' | 'resolved' | 'closed'
type SeedBalanceLogType = 'recharge' | 'consume' | 'gift' | 'admin_adjust'
type SeedNotificationChannelType = 'telegram' | 'discord' | 'email' | 'webhook'
type SeedAnnouncementType = 'system_broadcast' | 'host_broadcast' | 'admin_message' | 'host_message'
type SeedPaymentProviderType = 'yipay' | 'stripe' | 'alipay_direct' | 'wechat_direct' | 'manual'
type SeedRechargeStatus = 'pending' | 'paid' | 'completed' | 'failed' | 'cancelled' | 'refunded'
type SeedAffLogType = 'new_purchase' | 'renew' | 'convert'
type SeedAffWithdrawalStatus = 'pending' | 'approved' | 'rejected'
type SeedRedeemCodeType = 'c' | 'r' | 'd' | 't' | 'p'
type SeedQuota = {
hostLimit: number
packageLimit: number
monthlyTrafficLimit: bigint | null
}
type SeedUserSpec = {
username: string
email: string
role: SeedRole
status: SeedUserStatus
banReason: string | null
avatarStyle: string
quota: SeedQuota
}
type SeedHostSpec = {
ownerUsername: string
name: string
url: string
location: string
countryCode: string
architecture: 'x86_64' | 'aarch64'
status: SeedHostStatus
instanceType: SeedHostInstanceType
natPublicIp: string
natPublicIpv6: string
natBindIp: string
natBindIpv6: string
natPortStart: number
natPortEnd: number
ipAddress: string
ipv6Subnet: string
ipv6Gateway: string
tags: string[]
storageSize: number
cpuAllowanceMax: number
memoryMax: number
caddyEnabled: boolean
}
type SeedStoragePoolSpec = {
name: string
driver: string
purpose: 'instance_data' | 'instance_storage'
description: string
}
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 = {
key: string
ownerUsername: string
name: string
description: string
hostNames: string[]
cpuMax: number
memoryMax: number
diskMax: number
bandwidthMax: number
networkMode: SeedNetworkMode
instanceType: SeedPackageInstanceType
privileged: boolean
nested: boolean
active: boolean
portLimit: number
snapshotLimit: number
backupLimit: number
siteLimit: number
monthlyTrafficLimit: bigint
ioLimitMode: 'throughput' | 'iops'
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[]
}
type GeneratedPackageRecord = {
id: number
spec: SeedPackageSpec
planIdsByName: Map<string, number>
}
type SeedInstanceSpec = {
name: string
incusId: string
userUsername: string
packageKey: string
planName: string
hostName: string
image: string
status: SeedInstanceStatus
createdAt: Date
expiresAt: Date | null
suspendedAt: Date | null
suspendedBy: number | null
suspendReason: string | null
autoRenew: boolean
cloudInitState: string | null
cloudInitSource: string | null
ipv4: string | null
ipv6: string | null
sshPort: number | null
monthlyTrafficUsed: bigint
trafficStatus: SeedTrafficStatus
lastIpv6ReassignAt: Date | null
lastPlanChangeAt: Date | null
lastSyncedAt: Date
}
type SeedBalanceLogSpec = {
userId: number
type: SeedBalanceLogType
amount: number
balanceBefore: number
balanceAfter: number
orderId: string | null
remark: string
}
type GeneratedInstanceRecord = {
id: number
userId: number
hostId: number
packageId: number
packagePlanId: number
spec: SeedInstanceSpec
packageSpec: SeedPackageSpec
plan: SeedPlanSpec
}
type CreatedNotificationChannel = {
id: number
userId: number
type: SeedNotificationChannelType
}
type CreatedAffCode = {
id: number
userId: number
packagePlanId: number | null
code: string
}
const SEED_PREFIX = 'bulkseed'
const SEED_MARK = `[${SEED_PREFIX}]`
const SEED_CODE_PREFIX = SEED_PREFIX.toUpperCase()
const SHARED_PASSWORD = 'BulkSeed123!'
const LOCAL_PASSWORD_SALT = '$2a$10$1234567890123456789012'
const TODAY = new Date()
const avatarStyles = ['bigSmile', 'croodles', 'notionists', 'lorelei']
const countries = ['us', 'sg', 'jp', 'de', 'nl', 'au']
const cities = ['Los Angeles', 'Singapore', 'Tokyo', 'Frankfurt', 'Amsterdam', 'Sydney']
const containerImages = [
'images:ubuntu/24.04/cloud',
'images:debian/12/cloud',
'images:almalinux/9/cloud',
'images:rockylinux/9/cloud'
]
const vmImages = [
'images:ubuntu/24.04/cloud',
'images:debian/12/cloud',
'images:almalinux/9/cloud'
]
const redeemCodeTypes: SeedRedeemCodeType[] = ['c', 'r', 'd', 't', 'p']
const ownerUsers: SeedUserSpec[] = [
createOwnerUser(1, 'admin', 'admin', 24, 40),
createOwnerUser(2, 'apac', 'user', 18, 32),
createOwnerUser(3, 'eu', 'user', 18, 32),
createOwnerUser(4, 'us', 'user', 18, 32),
createOwnerUser(5, 'edge', 'user', 18, 32),
createOwnerUser(6, 'lab', 'user', 18, 32)
]
const customerUsers: SeedUserSpec[] = Array.from({ length: 30 }, (_, index) => {
const id = index + 1
const status: SeedUserStatus = id % 17 === 0 ? 'banned' : 'active'
return {
username: `${SEED_PREFIX}-user-${pad(id, 2)}`,
email: `${SEED_PREFIX}-user-${pad(id, 2)}@incudal.local`,
role: 'user',
status,
banReason: status === 'banned' ? `${SEED_MARK} banned sample user` : null,
avatarStyle: avatarStyles[index % avatarStyles.length],
quota: {
hostLimit: 0,
packageLimit: 0,
monthlyTrafficLimit: bytesFromGb(800)
}
}
})
const allSeedUsers = [...ownerUsers, ...customerUsers]
const hostSpecs: SeedHostSpec[] = [
createHostSpec(1, ownerUsers[0].username, 'official-us-west', 'Los Angeles, US', 'us', 'x86_64', 'both', 'online'),
createHostSpec(2, ownerUsers[0].username, 'official-eu-vm', 'Frankfurt, DE', 'de', 'x86_64', 'vm', 'online'),
createHostSpec(3, ownerUsers[1].username, 'apac-sg-edge', 'Singapore, SG', 'sg', 'x86_64', 'both', 'online'),
createHostSpec(4, ownerUsers[1].username, 'apac-tokyo-arm', 'Tokyo, JP', 'jp', 'aarch64', 'container', 'online'),
createHostSpec(5, ownerUsers[2].username, 'eu-amsterdam', 'Amsterdam, NL', 'nl', 'x86_64', 'both', 'online'),
createHostSpec(6, ownerUsers[2].username, 'eu-madrid-vm', 'Madrid, ES', 'us', 'x86_64', 'vm', 'online'),
createHostSpec(7, ownerUsers[3].username, 'us-ashburn', 'Ashburn, US', 'us', 'x86_64', 'both', 'online'),
createHostSpec(8, ownerUsers[3].username, 'us-dallas-container', 'Dallas, US', 'us', 'x86_64', 'container', 'online'),
createHostSpec(9, ownerUsers[4].username, 'edge-seoul', 'Seoul, KR', 'jp', 'aarch64', 'container', 'online'),
createHostSpec(10, ownerUsers[4].username, 'edge-sydney', 'Sydney, AU', 'au', 'x86_64', 'both', 'online'),
createHostSpec(11, ownerUsers[5].username, 'lab-london', 'London, GB', 'de', 'x86_64', 'both', 'online'),
createHostSpec(12, ownerUsers[5].username, 'lab-warsaw-vm', 'Warsaw, PL', 'nl', 'x86_64', 'vm', 'online')
]
const storagePools: SeedStoragePoolSpec[] = [
{
name: 'default',
driver: 'zfs',
purpose: 'instance_data',
description: `${SEED_MARK} default pool`
},
{
name: 'archive',
driver: 'dir',
purpose: 'instance_storage',
description: `${SEED_MARK} archive pool`
}
]
const packageSpecs = buildPackageSpecs()
const instanceSpecs = buildInstanceSpecs(packageSpecs)
function createOwnerUser(index: number, slug: string, role: SeedRole, hostLimit: number, packageLimit: number): SeedUserSpec {
return {
username: `${SEED_PREFIX}-owner-${slug}`,
email: `${SEED_PREFIX}-owner-${slug}@incudal.local`,
role,
status: 'active',
banReason: null,
avatarStyle: avatarStyles[(index - 1) % avatarStyles.length],
quota: {
hostLimit,
packageLimit,
monthlyTrafficLimit: bytesFromGb(3000)
}
}
}
function createHostSpec(
index: number,
ownerUsername: string,
slug: string,
location: string,
countryCode: string,
architecture: 'x86_64' | 'aarch64',
instanceType: SeedHostInstanceType,
status: SeedHostStatus
): SeedHostSpec {
const natBase = 40 + index
const hex = (0x40 + index).toString(16)
return {
ownerUsername,
name: `${SEED_PREFIX}-host-${pad(index, 2)}-${slug}`,
url: `https://${SEED_PREFIX}-host-${pad(index, 2)}-${slug}.local`,
location,
countryCode,
architecture,
status,
instanceType,
natPublicIp: `198.51.100.${natBase}`,
natPublicIpv6: `2001:db8:feed:${hex}::${index}`,
natBindIp: `10.255.${index}.10`,
natBindIpv6: `fd00:255:${hex}::10`,
natPortStart: 20000 + (index - 1) * 400,
natPortEnd: 20000 + (index - 1) * 400 + 399,
ipAddress: `10.20.${index}.10`,
ipv6Subnet: `2001:db8:${hex}::/64`,
ipv6Gateway: `2001:db8:${hex}::1`,
tags: [SEED_PREFIX, slug, architecture, instanceType],
storageSize: 200 + index * 15,
cpuAllowanceMax: 2200 + index * 180,
memoryMax: 65536 + index * 4096,
caddyEnabled: index % 2 === 0
}
}
function bytesFromGb(gb: number): bigint {
return BigInt(gb) * 1024n * 1024n * 1024n
}
function pad(value: number, digits: number): string {
return String(value).padStart(digits, '0')
}
function daysAgo(days: number, hour = 10): Date {
const date = new Date(TODAY)
date.setUTCDate(date.getUTCDate() - days)
date.setUTCHours(hour, 0, 0, 0)
return date
}
function daysFromNow(days: number, hour = 10): Date {
const date = new Date(TODAY)
date.setUTCDate(date.getUTCDate() + days)
date.setUTCHours(hour, 0, 0, 0)
return date
}
function addMonths(input: Date, months: number): Date {
const output = new Date(input)
output.setUTCMonth(output.getUTCMonth() + months)
return output
}
function roundToStep(value: number, step: number): number {
return Math.max(step, Math.round(value / step) * step)
}
function getBaseRechargeAmount(user: SeedUserSpec, index: number, spend: number): number {
return Math.max(
user.role === 'admin' ? 30000 + index * 500 : 9000 + index * 120,
spend + 2000 + index * 50
)
}
function getGiftAmount(user: SeedUserSpec, index: number): number {
return user.role === 'admin' ? 1500 : 300 + index * 10
}
function getSeedOrderNo(index: number): string {
return `${SEED_PREFIX}-recharge-${pad(index + 1, 3)}`
}
function buildDemoPublicKey(username: string, index: number): string {
const body = Buffer.from(`${SEED_PREFIX}:${username}:${index}`).toString('base64')
return `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI${body.slice(0, 40)} ${username}@seed.local`
}
function buildDemoFingerprint(username: string, index: number): string {
const body = Buffer.from(`${username}:${index}:${SEED_PREFIX}`).toString('base64')
return `SHA256:${body.slice(0, 32)}`
}
function getOwnerHosts(ownerUsername: string): SeedHostSpec[] {
return hostSpecs.filter(host => host.ownerUsername === ownerUsername)
}
function buildPackageSpecs(): SeedPackageSpec[] {
const thirdModes: SeedNetworkMode[] = ['nat_ipv6_nat', 'ipv6_only', 'ipv6_nat']
const packageList: SeedPackageSpec[] = []
ownerUsers.forEach((owner, ownerIndex) => {
const hosts = getOwnerHosts(owner.username)
const containerHosts = hosts.filter(host => host.instanceType !== 'vm')
const vmHosts = hosts.filter(host => host.instanceType !== 'container')
const cpuBase = 220 + ownerIndex * 20
const memoryBase = 8192 + ownerIndex * 1024
const diskBase = 102400 + ownerIndex * 10240
const thirdMode = thirdModes[ownerIndex % thirdModes.length]
packageList.push(
createPackageSpec({
index: packageList.length + 1,
ownerUsername: owner.username,
suffix: 'nat-box',
title: `NAT Box ${ownerIndex + 1}`,
description: `${SEED_MARK} public IPv4 NAT container package`,
hostNames: uniqueNames(containerHosts.map(host => host.name)),
networkMode: 'nat',
instanceType: 'container',
cpuMax: cpuBase,
memoryMax: memoryBase,
diskMax: diskBase,
bandwidthMax: 1200 + ownerIndex * 120,
globalShared: true,
portLimit: 24,
snapshotLimit: 5,
backupLimit: 3,
siteLimit: 4,
monthlyTrafficLimit: bytesFromGb(480 + ownerIndex * 40),
ioLimitMode: ownerIndex % 2 === 0 ? 'throughput' : 'iops',
globalQuotaMultiplier: 1.5
}),
createPackageSpec({
index: packageList.length + 2,
ownerUsername: owner.username,
suffix: 'dual-stack',
title: `Dual Stack ${ownerIndex + 1}`,
description: `${SEED_MARK} IPv4 NAT + routed IPv6 package`,
hostNames: uniqueNames(containerHosts.map(host => host.name)),
networkMode: 'nat_ipv6',
instanceType: 'container',
cpuMax: cpuBase + 30,
memoryMax: memoryBase + 1024,
diskMax: diskBase + 10240,
bandwidthMax: 1400 + ownerIndex * 120,
globalShared: ownerIndex % 2 === 0,
portLimit: 28,
snapshotLimit: 6,
backupLimit: 3,
siteLimit: 5,
monthlyTrafficLimit: bytesFromGb(560 + ownerIndex * 40),
ioLimitMode: 'throughput',
globalQuotaMultiplier: 1.8
}),
createPackageSpec({
index: packageList.length + 3,
ownerUsername: owner.username,
suffix: `ipv6-${thirdMode}`,
title: `IPv6 Mix ${ownerIndex + 1}`,
description: `${SEED_MARK} IPv6-heavy container package`,
hostNames: uniqueNames(containerHosts.map(host => host.name)),
networkMode: thirdMode,
instanceType: 'container',
cpuMax: cpuBase + 10,
memoryMax: memoryBase + 512,
diskMax: diskBase,
bandwidthMax: 1000 + ownerIndex * 100,
globalShared: true,
portLimit: thirdMode === 'ipv6_only' ? 0 : 18,
snapshotLimit: 4,
backupLimit: 2,
siteLimit: 4,
monthlyTrafficLimit: bytesFromGb(360 + ownerIndex * 30),
ioLimitMode: 'throughput',
globalQuotaMultiplier: 1.2
}),
createPackageSpec({
index: packageList.length + 4,
ownerUsername: owner.username,
suffix: 'vm-flex',
title: `VM Flex ${ownerIndex + 1}`,
description: `${SEED_MARK} KVM package for billing and lifecycle screens`,
hostNames: uniqueNames(vmHosts.map(host => host.name)),
networkMode: ownerIndex % 2 === 0 ? 'nat_ipv6' : 'nat',
instanceType: 'vm',
cpuMax: 300 + ownerIndex * 30,
memoryMax: 12288 + ownerIndex * 1024,
diskMax: 153600 + ownerIndex * 10240,
bandwidthMax: 1800 + ownerIndex * 100,
globalShared: ownerIndex % 3 !== 0,
portLimit: 10,
snapshotLimit: 4,
backupLimit: 3,
siteLimit: 1,
monthlyTrafficLimit: bytesFromGb(700 + ownerIndex * 50),
ioLimitMode: 'iops',
globalQuotaMultiplier: 1.0
})
)
})
return packageList
}
function createPackageSpec(input: {
index: number
ownerUsername: string
suffix: string
title: string
description: string
hostNames: string[]
networkMode: SeedNetworkMode
instanceType: SeedPackageInstanceType
cpuMax: number
memoryMax: number
diskMax: number
bandwidthMax: number
globalShared: boolean
portLimit: number
snapshotLimit: number
backupLimit: number
siteLimit: number
monthlyTrafficLimit: bigint
ioLimitMode: 'throughput' | 'iops'
globalQuotaMultiplier: number | null
}): SeedPackageSpec {
const isVm = input.instanceType === 'vm'
const key = `${SEED_PREFIX}-pkg-${pad(input.index, 2)}`
const planNames = isVm ? ['Starter', 'Business', 'Scale'] : ['Nano', 'Plus', 'Prime']
const planScales = isVm ? [0.28, 0.46, 0.68] : [0.18, 0.36, 0.58]
const plans = planNames.map((planName, index) => {
const scale = planScales[index]
const baseCpu = roundToStep(input.cpuMax * scale, isVm ? 20 : 5)
const baseMemory = roundToStep(input.memoryMax * scale, 256)
const baseDisk = roundToStep(input.diskMax * scale, 1024)
return {
name: planName,
description: `${SEED_MARK} ${planName} plan for ${input.title}`,
cpu: baseCpu,
memory: baseMemory,
disk: baseDisk,
portLimit: input.networkMode === 'ipv6_only' ? 0 : Math.max(0, Math.min(input.portLimit, 6 + index * (isVm ? 2 : 6))),
snapshotLimit: Math.min(input.snapshotLimit, 2 + index),
backupLimit: Math.min(input.backupLimit, 1 + Math.floor(index / 1)),
siteLimit: isVm ? Math.min(input.siteLimit, index === 2 ? 1 : 0) : Math.min(input.siteLimit, 1 + index * 2),
swapSize: isVm ? 0 : index === 0 ? 0 : index * 512,
trafficLimit: bytesFromGb((isVm ? 180 : 90) + index * (isVm ? 160 : 80) + input.index * 5),
trafficLimitSpeed: ['2Mbit', '5Mbit', '10Mbit'][index],
price: (isVm ? 2990 : 890) + input.index * (isVm ? 60 : 25) + index * (isVm ? 1800 : 900),
billingCycle: index === 2 && isVm ? 3 : 1,
setupFee: isVm ? index * 300 : 0,
isActive: true,
sortOrder: (index + 1) * 10,
slaGuarantee: [99.5, 99.9, 99.95][index]
}
})
return {
key,
ownerUsername: input.ownerUsername,
name: `${key}-${input.suffix}`,
description: input.description,
hostNames: input.hostNames,
networkMode: input.networkMode,
instanceType: input.instanceType,
cpuMax: input.cpuMax,
memoryMax: input.memoryMax,
diskMax: input.diskMax,
bandwidthMax: input.bandwidthMax,
privileged: !isVm,
nested: !isVm,
active: true,
portLimit: input.portLimit,
snapshotLimit: input.snapshotLimit,
backupLimit: input.backupLimit,
siteLimit: input.siteLimit,
monthlyTrafficLimit: input.monthlyTrafficLimit,
ioLimitMode: input.ioLimitMode,
limitsRead: isVm ? '350MB' : '220MB',
limitsWrite: isVm ? '350MB' : '220MB',
limitsReadIops: isVm ? 2200 : 1100,
limitsWriteIops: isVm ? 2200 : 1100,
limitsIngress: isVm ? '1Gbit' : '600Mbit',
limitsEgress: isVm ? '1Gbit' : '600Mbit',
limitsProcesses: isVm ? 800 : 1200,
limitsCpuPriority: isVm ? 8 : 10,
bootAutostart: true,
bootAutostartPriority: 10 + (input.index % 10),
bootAutostartDelay: 10 + (input.index % 5) * 5,
bootHostShutdownTimeout: isVm ? 60 : 45,
globalShared: input.globalShared,
globalQuotaMultiplier: input.globalQuotaMultiplier,
globalMaxInstances: 12,
allowInstanceDeletion: true,
plans
}
}
function uniqueNames(values: string[]): string[] {
return Array.from(new Set(values))
}
function buildInstanceSpecs(packages: SeedPackageSpec[]): SeedInstanceSpec[] {
return Array.from({ length: 64 }, (_, index) => {
const id = index + 1
const packageSpec = packages[index % packages.length]
const planName = packageSpec.plans[index % packageSpec.plans.length].name
const hostName = packageSpec.hostNames[index % packageSpec.hostNames.length]
const status = pickInstanceStatus(index)
const createdAt = daysAgo(4 + (index % 120), 8 + (index % 6))
const expiresAt = status === 'suspended' ? daysAgo(1 + (index % 4), 9) : daysFromNow(20 + (index % 90), 9)
const ipv4 = ['nat', 'nat_ipv6', 'nat_ipv6_nat'].includes(packageSpec.networkMode)
? `10.${60 + (index % 20)}.${1 + Math.floor(index / 20)}.${20 + (index % 180)}`
: null
const routedIpv6 = packageSpec.networkMode === 'nat_ipv6' || packageSpec.networkMode === 'ipv6_only'
? `2001:db8:${(0x900 + index).toString(16)}::${100 + id}`
: null
const sharedIpv6 = packageSpec.networkMode === 'nat_ipv6_nat' || packageSpec.networkMode === 'ipv6_nat'
? `2001:db8:share::${id}`
: null
const primaryIpv6 = routedIpv6 ?? sharedIpv6
const firstPublicPort = packageSpec.networkMode === 'ipv6_only' ? null : 21000 + index * 6
const trafficStatus: SeedTrafficStatus = index % 11 === 0 ? 'LIMITED' : index % 7 === 0 ? 'WARNING' : 'NORMAL'
const monthlyTrafficUsed = trafficStatus === 'LIMITED'
? bytesFromGb(240 + index * 3)
: trafficStatus === 'WARNING'
? bytesFromGb(180 + index * 2)
: bytesFromGb(70 + index * 2)
return {
name: `${SEED_PREFIX}-inst-${pad(id, 3)}`,
incusId: `${SEED_PREFIX}-incus-${pad(id, 3)}`,
userUsername: customerUsers[(index * 7) % customerUsers.length].username,
packageKey: packageSpec.key,
planName,
hostName,
image: packageSpec.instanceType === 'vm'
? vmImages[index % vmImages.length]
: containerImages[index % containerImages.length],
status,
createdAt,
expiresAt,
suspendedAt: status === 'suspended' ? daysAgo(index % 4, 9) : null,
suspendedBy: status === 'suspended' && index % 2 === 0 ? 1 : null,
suspendReason: status === 'suspended' ? `${SEED_MARK} expired sample` : null,
autoRenew: index % 3 !== 0,
cloudInitState: status === 'error' ? 'error' : 'done',
cloudInitSource: packageSpec.instanceType === 'vm' ? 'cloud-init' : 'lxd-agent',
ipv4,
ipv6: primaryIpv6,
sshPort: firstPublicPort ?? 22,
monthlyTrafficUsed,
trafficStatus,
lastIpv6ReassignAt: packageSpec.networkMode === 'nat_ipv6' && index % 5 === 0 ? daysAgo(index % 3, 7) : null,
lastPlanChangeAt: index % 6 === 0 ? daysAgo(2 + (index % 9), 11) : null,
lastSyncedAt: daysAgo(index % 5, 14)
}
})
}
function pickInstanceStatus(index: number): SeedInstanceStatus {
const mod = index % 20
if (mod === 17) return 'error'
if (mod === 18) return 'suspended'
if (mod === 19 || mod === 14 || mod === 15) return 'stopped'
return 'running'
}
async function ensureLocalDevGuard(): Promise<void> {
const databaseUrl = process.env.DATABASE_URL || ''
const isDevDb = databaseUrl.includes('incudal_dev_password@db:5432/incudal')
if (!isDevDb && process.env.FORCE_BULK_SEED !== '1') {
throw new Error(
`Refusing to run ${SEED_PREFIX} seed outside the local dev database. Set FORCE_BULK_SEED=1 to override.`
)
}
}
async function buildPasswordHash(password: string): Promise<string> {
return bcrypt.hash(password, LOCAL_PASSWORD_SALT)
}
async function syncUsers(passwordHash: string): Promise<Map<string, number>> {
const userIds = new Map<string, number>()
for (const [index, user] of allSeedUsers.entries()) {
const saved = await prisma.user.upsert({
where: { username: user.username },
create: {
username: user.username,
email: user.email,
passwordHash,
role: user.role,
status: user.status,
banReason: user.banReason,
balance: 0,
avatarStyle: user.avatarStyle
},
update: {
email: user.email,
passwordHash,
role: user.role,
status: user.status,
banReason: user.banReason,
avatarStyle: user.avatarStyle
}
})
await prisma.userQuota.upsert({
where: { userId: saved.id },
create: {
userId: saved.id,
hostLimit: user.quota.hostLimit,
packageLimit: user.quota.packageLimit,
monthlyTrafficLimit: user.quota.monthlyTrafficLimit
},
update: {
hostLimit: user.quota.hostLimit,
packageLimit: user.quota.packageLimit,
monthlyTrafficLimit: user.quota.monthlyTrafficLimit
}
})
userIds.set(user.username, saved.id)
}
const seedUserIds = Array.from(userIds.values())
await prisma.loginRecord.deleteMany({
where: {
userId: { in: seedUserIds },
userAgent: { startsWith: `${SEED_PREFIX}/` }
}
})
for (const [index, user] of allSeedUsers.entries()) {
const userId = userIds.get(user.username)!
const firstCountry = countries[index % countries.length]
const secondCountry = countries[(index + 2) % countries.length]
await prisma.loginRecord.createMany({
data: [
{
userId,
ip: `172.19.${10 + (index % 20)}.${20 + (index % 180)}`,
country: firstCountry.toUpperCase(),
region: cities[index % cities.length],
city: cities[index % cities.length],
isp: 'BulkSeedNet',
timezone: 'Asia/Shanghai',
userAgent: `${SEED_PREFIX}/web-chrome`
},
{
userId,
ip: `172.20.${10 + (index % 20)}.${40 + (index % 180)}`,
country: secondCountry.toUpperCase(),
region: cities[(index + 1) % cities.length],
city: cities[(index + 1) % cities.length],
isp: 'BulkSeedMobile',
timezone: 'Asia/Shanghai',
userAgent: `${SEED_PREFIX}/mobile-safari`
}
]
})
}
return userIds
}
async function syncHosts(userIds: Map<string, number>): Promise<Map<string, number>> {
const hostIds = new Map<string, number>()
for (const host of hostSpecs) {
const userId = userIds.get(host.ownerUsername)
if (!userId) {
throw new Error(`Missing owner for host ${host.name}`)
}
const saved = await prisma.host.upsert({
where: {
userId_name: {
userId,
name: host.name
}
},
create: {
userId,
name: host.name,
url: host.url,
location: host.location,
countryCode: host.countryCode,
architecture: host.architecture,
status: host.status,
natPublicIp: host.natPublicIp,
natPublicIpv6: host.natPublicIpv6,
natBindIp: host.natBindIp,
natBindIpv6: host.natBindIpv6,
natPortStart: host.natPortStart,
natPortEnd: host.natPortEnd,
tags: host.tags,
cpuAllowanceMax: host.cpuAllowanceMax,
memoryMax: host.memoryMax,
instanceType: host.instanceType,
ipAddress: host.ipAddress,
storageDriver: 'zfs',
storageType: 'loop',
storageSize: host.storageSize,
ipv6Mode: 1,
ipv6Subnet: host.ipv6Subnet,
ipv6Gateway: host.ipv6Gateway,
ipv6ParentInterface: 'eth0',
enableApi: true,
isInstalled: true,
caddyEnabled: host.caddyEnabled,
caddyUsername: host.caddyEnabled ? `${SEED_PREFIX}-caddy` : null,
caddyPassword: host.caddyEnabled ? `${SEED_PREFIX}-password` : null,
caddyPort: 8444 + (host.natPortStart % 10),
transferEnabled: true,
trafficResetDay: 1 + (host.natPortStart % 20),
announcement: `${SEED_MARK} ${host.location} demo host`,
notifyPurchase: true,
notifyRenew: true,
notifyDestroy: host.instanceType === 'vm',
enableResourcePool: true,
probeUrl: `https://${host.name}.probe.local/health`
},
update: {
url: host.url,
location: host.location,
countryCode: host.countryCode,
architecture: host.architecture,
status: host.status,
natPublicIp: host.natPublicIp,
natPublicIpv6: host.natPublicIpv6,
natBindIp: host.natBindIp,
natBindIpv6: host.natBindIpv6,
natPortStart: host.natPortStart,
natPortEnd: host.natPortEnd,
tags: host.tags,
cpuAllowanceMax: host.cpuAllowanceMax,
memoryMax: host.memoryMax,
instanceType: host.instanceType,
ipAddress: host.ipAddress,
storageDriver: 'zfs',
storageType: 'loop',
storageSize: host.storageSize,
ipv6Mode: 1,
ipv6Subnet: host.ipv6Subnet,
ipv6Gateway: host.ipv6Gateway,
ipv6ParentInterface: 'eth0',
enableApi: true,
isInstalled: true,
caddyEnabled: host.caddyEnabled,
caddyUsername: host.caddyEnabled ? `${SEED_PREFIX}-caddy` : null,
caddyPassword: host.caddyEnabled ? `${SEED_PREFIX}-password` : null,
transferEnabled: true,
announcement: `${SEED_MARK} ${host.location} demo host`,
probeUrl: `https://${host.name}.probe.local/health`
}
})
hostIds.set(host.name, saved.id)
for (const pool of storagePools) {
await prisma.storagePool.upsert({
where: {
hostId_name: {
hostId: saved.id,
name: pool.name
}
},
create: {
hostId: saved.id,
name: pool.name,
driver: pool.driver,
purpose: pool.purpose,
description: pool.description,
config: {
generatedBy: SEED_PREFIX,
host: host.name
}
},
update: {
driver: pool.driver,
purpose: pool.purpose,
description: pool.description,
config: {
generatedBy: SEED_PREFIX,
host: host.name
}
}
})
}
}
return hostIds
}
async function syncPackages(
userIds: Map<string, number>,
hostIds: Map<string, number>
): Promise<Map<string, GeneratedPackageRecord>> {
const packageMap = new Map<string, GeneratedPackageRecord>()
for (const pkg of packageSpecs) {
const userId = userIds.get(pkg.ownerUsername)
if (!userId) {
throw new Error(`Missing package owner ${pkg.ownerUsername}`)
}
const saved = await prisma.package.upsert({
where: {
userId_name: {
userId,
name: pkg.name
}
},
create: {
userId,
name: pkg.name,
description: pkg.description,
cpuMax: pkg.cpuMax,
memoryMax: pkg.memoryMax,
diskMax: pkg.diskMax,
bandwidthMax: pkg.bandwidthMax,
networkMode: pkg.networkMode,
instanceType: pkg.instanceType,
privileged: pkg.privileged,
nested: pkg.nested,
active: pkg.active,
portLimit: pkg.portLimit,
snapshotLimit: pkg.snapshotLimit,
backupLimit: pkg.backupLimit,
siteLimit: pkg.siteLimit,
monthlyTrafficLimit: pkg.monthlyTrafficLimit,
ioLimitMode: pkg.ioLimitMode,
limitsRead: pkg.limitsRead,
limitsWrite: pkg.limitsWrite,
limitsReadIops: pkg.limitsReadIops,
limitsWriteIops: pkg.limitsWriteIops,
limitsIngress: pkg.limitsIngress,
limitsEgress: pkg.limitsEgress,
limitsProcesses: pkg.limitsProcesses,
limitsCpuPriority: pkg.limitsCpuPriority,
bootAutostart: pkg.bootAutostart,
bootAutostartPriority: pkg.bootAutostartPriority,
bootAutostartDelay: pkg.bootAutostartDelay,
bootHostShutdownTimeout: pkg.bootHostShutdownTimeout,
globalShared: pkg.globalShared,
globalQuotaMultiplier: pkg.globalQuotaMultiplier,
globalMaxInstances: pkg.globalMaxInstances,
allowInstanceDeletion: pkg.allowInstanceDeletion
},
update: {
description: pkg.description,
cpuMax: pkg.cpuMax,
memoryMax: pkg.memoryMax,
diskMax: pkg.diskMax,
bandwidthMax: pkg.bandwidthMax,
networkMode: pkg.networkMode,
instanceType: pkg.instanceType,
privileged: pkg.privileged,
nested: pkg.nested,
active: pkg.active,
portLimit: pkg.portLimit,
snapshotLimit: pkg.snapshotLimit,
backupLimit: pkg.backupLimit,
siteLimit: pkg.siteLimit,
monthlyTrafficLimit: pkg.monthlyTrafficLimit,
ioLimitMode: pkg.ioLimitMode,
limitsRead: pkg.limitsRead,
limitsWrite: pkg.limitsWrite,
limitsReadIops: pkg.limitsReadIops,
limitsWriteIops: pkg.limitsWriteIops,
limitsIngress: pkg.limitsIngress,
limitsEgress: pkg.limitsEgress,
limitsProcesses: pkg.limitsProcesses,
limitsCpuPriority: pkg.limitsCpuPriority,
bootAutostart: pkg.bootAutostart,
bootAutostartPriority: pkg.bootAutostartPriority,
bootAutostartDelay: pkg.bootAutostartDelay,
bootHostShutdownTimeout: pkg.bootHostShutdownTimeout,
globalShared: pkg.globalShared,
globalQuotaMultiplier: pkg.globalQuotaMultiplier,
globalMaxInstances: pkg.globalMaxInstances,
allowInstanceDeletion: pkg.allowInstanceDeletion
}
})
await prisma.packageHost.deleteMany({ where: { packageId: saved.id } })
await prisma.packageHost.createMany({
data: pkg.hostNames.map(hostName => {
const hostId = hostIds.get(hostName)
if (!hostId) {
throw new Error(`Missing host ${hostName} for package ${pkg.name}`)
}
return {
packageId: saved.id,
hostId,
storagePoolName: 'default'
}
})
})
for (const plan of pkg.plans) {
await prisma.packagePlan.upsert({
where: {
packageId_name: {
packageId: saved.id,
name: plan.name
}
},
create: {
packageId: saved.id,
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
},
update: {
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
}
})
}
const savedPlans = await prisma.packagePlan.findMany({
where: { packageId: saved.id },
orderBy: { sortOrder: 'asc' }
})
packageMap.set(pkg.key, {
id: saved.id,
spec: pkg,
planIdsByName: new Map(savedPlans.map(plan => [plan.name, plan.id]))
})
}
return packageMap
}
async function syncInstances(
userIds: Map<string, number>,
hostIds: Map<string, number>,
packagesByKey: Map<string, GeneratedPackageRecord>
): Promise<GeneratedInstanceRecord[]> {
const records: GeneratedInstanceRecord[] = []
for (const [index, instance] of instanceSpecs.entries()) {
const userId = userIds.get(instance.userUsername)
const pkg = packagesByKey.get(instance.packageKey)
const hostId = hostIds.get(instance.hostName)
const hostSpec = hostSpecs.find(candidate => candidate.name === instance.hostName)
if (!userId || !pkg || !hostId || !hostSpec) {
throw new Error(`Missing relation while seeding instance ${instance.name}`)
}
const planId = pkg.planIdsByName.get(instance.planName)
const plan = pkg.spec.plans.find(candidate => candidate.name === instance.planName)
if (!planId || !plan) {
throw new Error(`Missing plan ${instance.planName} for ${instance.name}`)
}
const existing = await prisma.instance.findFirst({
where: {
userId,
name: instance.name
}
})
const saved = existing
? await prisma.instance.update({
where: { id: existing.id },
data: buildInstanceData(instance, userId, hostId, hostSpec, pkg.id, planId, pkg.spec, plan, index)
})
: await prisma.instance.create({
data: buildInstanceData(instance, userId, hostId, hostSpec, pkg.id, planId, pkg.spec, plan, index)
})
await syncInstanceChildren(saved.id, userId, hostId, hostSpec, instance, pkg.spec, plan, index)
records.push({
id: saved.id,
userId,
hostId,
packageId: pkg.id,
packagePlanId: planId,
spec: instance,
packageSpec: pkg.spec,
plan
})
}
return records
}
function buildInstanceData(
spec: SeedInstanceSpec,
userId: number,
hostId: number,
hostSpec: SeedHostSpec,
packageId: number,
packagePlanId: number,
pkg: SeedPackageSpec,
plan: SeedPlanSpec,
index: number
) {
return {
incusId: spec.incusId,
name: spec.name,
userId,
hostId,
packageId,
packagePlanId,
storagePoolName: 'default',
image: spec.image,
status: spec.status,
cpu: plan.cpu,
memory: plan.memory,
disk: plan.disk,
ipv4: spec.ipv4,
ipv6: spec.ipv6,
networkMode: pkg.networkMode,
sshPort: pkg.networkMode === 'ipv6_only' ? 22 : hostSpec.natPortStart + 20 + index * 4,
rootPassword: `${SHARED_PASSWORD}!${pad(index + 1, 3)}`,
snapshottedSpecs: {
generatedBy: SEED_PREFIX,
packageKey: pkg.key,
planName: plan.name,
cpu: plan.cpu,
memory: plan.memory,
disk: plan.disk
},
portLimit: plan.portLimit,
snapshotLimit: plan.snapshotLimit,
backupLimit: plan.backupLimit,
siteLimit: plan.siteLimit,
swapEnabled: plan.swapSize > 0,
swapSize: plan.swapSize,
monthlyTrafficLimit: plan.trafficLimit,
monthlyTrafficUsed: spec.monthlyTrafficUsed,
trafficStatus: spec.trafficStatus,
limitsRead: pkg.limitsRead,
limitsWrite: pkg.limitsWrite,
limitsReadIops: pkg.limitsReadIops,
limitsWriteIops: pkg.limitsWriteIops,
limitsIngress: pkg.limitsIngress,
limitsEgress: pkg.limitsEgress,
limitsProcesses: pkg.limitsProcesses,
limitsCpuPriority: pkg.limitsCpuPriority,
bootAutostart: pkg.bootAutostart,
bootAutostartPriority: pkg.bootAutostartPriority,
bootAutostartDelay: pkg.bootAutostartDelay,
bootHostShutdownTimeout: pkg.bootHostShutdownTimeout,
expiresAt: spec.expiresAt,
suspendedAt: spec.suspendedAt,
suspendedBy: spec.suspendedBy,
suspendReason: spec.suspendReason,
billingPrice: plan.price,
billingCycle: plan.billingCycle,
autoRenew: spec.autoRenew,
iconBadgeId: index % 9 === 0 ? 'fire' : null,
autoRenewAttempts: spec.autoRenew ? index % 3 : 0,
lastAutoRenewAttemptAt: spec.autoRenew ? daysAgo(index % 5, 16) : null,
expiryNotifiedAt: spec.status === 'suspended' ? daysAgo(1, 17) : null,
version: index % 5,
lastIpv6ReassignAt: spec.lastIpv6ReassignAt,
lastPlanChangeAt: spec.lastPlanChangeAt,
cloudInitState: spec.cloudInitState,
cloudInitSource: spec.cloudInitSource,
cloudInitLastCheckedAt: spec.lastSyncedAt,
cloudInitCompletedAt: spec.status === 'error' ? null : addMonths(spec.createdAt, 0),
cloudInitManualCompletedAt: spec.status === 'error' ? null : null,
cloudInitManualCompletedBy: null,
createdAt: spec.createdAt,
lastSyncedAt: spec.lastSyncedAt
}
}
async function syncInstanceChildren(
instanceId: number,
userId: number,
hostId: number,
hostSpec: SeedHostSpec,
spec: SeedInstanceSpec,
pkg: SeedPackageSpec,
plan: SeedPlanSpec,
index: number
): Promise<void> {
await prisma.portMapping.deleteMany({ where: { instanceId } })
await prisma.snapshot.deleteMany({ where: { instanceId } })
await prisma.backup.deleteMany({ where: { instanceId } })
await prisma.proxySite.deleteMany({ where: { instanceId } })
await prisma.ipAddress.deleteMany({ where: { instanceId } })
await prisma.ipv6Subnet.deleteMany({ where: { instanceId } })
await prisma.dailyTraffic.deleteMany({ where: { instanceId } })
await prisma.instanceBillingRecord.deleteMany({ where: { instanceId } })
const publicBase = hostSpec.natPortStart + 20 + index * 4
const supportsPortMappings = pkg.networkMode !== 'ipv6_only' && plan.portLimit > 0
if (supportsPortMappings) {
const portSet = pickPortSet(index, pkg.instanceType)
const mappings = portSet.map((privatePort, mappingIndex) => ({
instanceId,
hostId,
protocol: pickProtocol(index, mappingIndex),
publicPort: publicBase + mappingIndex,
privatePort,
remark: `${SEED_MARK} ${mappingIndex === 0 ? 'ssh-or-app' : 'service'}`
}))
await prisma.portMapping.createMany({ data: mappings })
}
const snapshotsToCreate = index % 3 === 0 ? 2 : index % 2 === 0 ? 1 : 0
if (snapshotsToCreate > 0) {
await prisma.snapshot.createMany({
data: Array.from({ length: snapshotsToCreate }, (_, snapshotIndex) => ({
instanceId,
incusName: `${spec.name}/snap-${snapshotIndex + 1}`,
name: `${SEED_PREFIX}-snap-${snapshotIndex + 1}`,
description: `${SEED_MARK} snapshot ${snapshotIndex + 1}`,
stateful: pkg.instanceType === 'vm' && snapshotIndex === 0,
size: 1024 + snapshotIndex * 512
}))
})
}
const backupsToCreate = index % 4 === 0 ? 2 : index % 3 === 0 ? 1 : 0
if (backupsToCreate > 0) {
await prisma.backup.createMany({
data: Array.from({ length: backupsToCreate }, (_, backupIndex) => ({
instanceId,
incusName: `${spec.name}/backup-${backupIndex + 1}`,
name: `${SEED_PREFIX}-backup-${backupIndex + 1}`,
description: `${SEED_MARK} backup ${backupIndex + 1}`,
size: 2048 + backupIndex * 768,
status: backupIndex === backupsToCreate - 1 && index % 8 === 0 ? 'error' : ('ready' as SeedBackupStatus),
expiresAt: daysFromNow(14 + backupIndex * 7, 6)
}))
})
}
if (index % 4 === 0) {
await prisma.backupPolicy.upsert({
where: { instanceId },
create: {
instanceId,
enabled: true,
intervalMinutes: 720,
lastRunAt: daysAgo(1, 2),
nextRunAt: daysFromNow(1, 2)
},
update: {
enabled: true,
intervalMinutes: 720,
lastRunAt: daysAgo(1, 2),
nextRunAt: daysFromNow(1, 2)
}
})
} else {
await prisma.backupPolicy.deleteMany({ where: { instanceId } })
}
if (index % 3 === 0) {
await prisma.snapshotPolicy.upsert({
where: { instanceId },
create: {
instanceId,
enabled: true,
intervalMinutes: 360,
lastRunAt: daysAgo(1, 4),
nextRunAt: daysFromNow(1, 4)
},
update: {
enabled: true,
intervalMinutes: 360,
lastRunAt: daysAgo(1, 4),
nextRunAt: daysFromNow(1, 4)
}
})
} else {
await prisma.snapshotPolicy.deleteMany({ where: { instanceId } })
}
await prisma.trafficSnapshot.upsert({
where: { instanceId },
create: {
instanceId,
rxRaw: 100_000_000n + BigInt(index) * 10_000n,
txRaw: 80_000_000n + BigInt(index) * 9_000n
},
update: {
rxRaw: 100_000_000n + BigInt(index) * 10_000n,
txRaw: 80_000_000n + BigInt(index) * 9_000n
}
})
await prisma.dailyTraffic.createMany({
data: Array.from({ length: 7 }, (_, dayOffset) => ({
instanceId,
date: daysAgo(6 - dayOffset, 0),
rxTotal: 2_000_000_000n + BigInt(index + dayOffset) * 30_000_000n,
txTotal: 1_200_000_000n + BigInt(index + dayOffset) * 20_000_000n
}))
})
const shouldAddIpv6 = pkg.networkMode === 'nat_ipv6' || pkg.networkMode === 'ipv6_only'
if (shouldAddIpv6 && spec.ipv6) {
await prisma.ipAddress.createMany({
data: [
{
instanceId,
address: spec.ipv6,
type: 'inet6',
isPrimary: true,
isCustom: false,
device: 'eth1'
},
...(index % 5 === 0
? [
{
instanceId,
address: `${spec.ipv6.split('::')[0]}::${400 + index}`,
type: 'inet6',
isPrimary: false,
isCustom: true,
device: 'eth1'
}
]
: [])
]
})
}
if (shouldAddIpv6 && spec.ipv6 && index % 6 === 0) {
await prisma.ipv6Subnet.create({
data: {
instanceId,
cidr: `${spec.ipv6.split('::')[0]}:100::/120`,
primaryIp: `${spec.ipv6.split('::')[0]}:100::1`,
device: 'eth1'
}
})
}
const proxyCount = pkg.instanceType === 'container' && plan.siteLimit > 0 && index % 3 !== 0 ? (index % 5 === 0 ? 2 : 1) : 0
if (proxyCount > 0) {
await prisma.proxySite.createMany({
data: Array.from({ length: proxyCount }, (_, siteIndex) => ({
instanceId,
hostId,
domain: `${spec.name}-${siteIndex + 1}.${SEED_PREFIX}.test`,
targetPort: [80, 443, 8080][siteIndex % 3],
httpsEnabled: siteIndex % 2 === 0,
remark: `${SEED_MARK} proxy ${siteIndex + 1}`,
status: pickProxyStatus(index, siteIndex),
enabled: siteIndex % 4 !== 0,
error: index % 9 === 0 && siteIndex === 0 ? `${SEED_MARK} DNS pending` : null
}))
})
}
await prisma.instanceBillingRecord.create({
data: {
instanceId,
userId,
type: 'newPurchase',
amount: plan.price,
months: plan.billingCycle,
periodStart: spec.createdAt,
periodEnd: addMonths(spec.createdAt, plan.billingCycle),
remark: `${SEED_MARK} initial purchase`
}
})
if (index % 6 === 0) {
const renewedStart = addMonths(spec.createdAt, plan.billingCycle)
await prisma.instanceBillingRecord.create({
data: {
instanceId,
userId,
type: 'renew',
amount: plan.price,
months: plan.billingCycle,
periodStart: renewedStart,
periodEnd: addMonths(renewedStart, plan.billingCycle),
remark: `${SEED_MARK} renewal sample`
}
})
}
}
function pickPortSet(index: number, instanceType: SeedPackageInstanceType): number[] {
if (instanceType === 'vm') {
return index % 2 === 0 ? [22, 3389] : [22, 8443]
}
const sets = [
[22, 80],
[22, 443],
[25565, 19132],
[8080, 8443]
]
return sets[index % sets.length]
}
function pickProtocol(index: number, mappingIndex: number): SeedProtocol {
if (mappingIndex === 1 && index % 4 === 2) return 'udp'
return 'tcp'
}
function pickProxyStatus(index: number, siteIndex: number): SeedProxyStatus {
if (index % 9 === 0 && siteIndex === 0) return 'error'
if (index % 4 === 0) return 'pending'
return 'active'
}
async function syncTickets(instances: GeneratedInstanceRecord[]): Promise<void> {
await prisma.ticket.deleteMany({
where: {
subject: { startsWith: SEED_MARK }
}
})
const selected = instances.filter((_, index) => index % 5 === 0).slice(0, 12)
for (const [index, instance] of selected.entries()) {
const hostOwner = await prisma.host.findUniqueOrThrow({
where: { id: instance.hostId },
select: { userId: true }
})
const status = (['open', 'in_progress', 'resolved', 'closed'] as SeedTicketStatus[])[index % 4]
const createdAt = daysAgo(2 + index, 9)
await prisma.ticket.create({
data: {
userId: instance.userId,
hostId: instance.hostId,
instanceId: instance.id,
subject: `${SEED_MARK} ticket-${pad(index + 1, 2)} for ${instance.spec.name}`,
category: ['general', 'billing', 'technical', 'abuse'][index % 4],
priority: (['low', 'normal', 'high', 'urgent'] as SeedTicketPriority[])[index % 4],
status,
createdAt,
resolvedAt: status === 'resolved' || status === 'closed' ? daysAgo(1 + index, 12) : null,
closedAt: status === 'closed' ? daysAgo(index, 15) : null,
messages: {
create: [
{
senderId: instance.userId,
content: `${SEED_MARK} user asks about ${instance.spec.name}`,
isFromOwner: false,
createdAt
},
{
senderId: hostOwner.userId,
content: `${SEED_MARK} host owner replied for ${instance.spec.name}`,
isFromOwner: true,
createdAt: new Date(createdAt.getTime() + 60 * 60 * 1000)
},
...(index % 3 === 0
? [
{
senderId: instance.userId,
content: `${SEED_MARK} follow-up confirmation`,
isFromOwner: false,
createdAt: new Date(createdAt.getTime() + 2 * 60 * 60 * 1000)
}
]
: [])
]
}
}
})
}
}
async function syncBalances(
userIds: Map<string, number>,
instances: GeneratedInstanceRecord[]
): Promise<void> {
const seedUserIds = Array.from(userIds.values())
await prisma.balanceLog.deleteMany({
where: {
userId: { in: seedUserIds },
remark: { startsWith: SEED_MARK }
}
})
const spendByUser = new Map<number, number>()
for (const instance of instances) {
spendByUser.set(instance.userId, (spendByUser.get(instance.userId) || 0) + instance.plan.price)
}
const logs: SeedBalanceLogSpec[] = []
for (const [index, user] of allSeedUsers.entries()) {
const userId = userIds.get(user.username)!
const spend = spendByUser.get(userId) || 0
const baseRecharge = getBaseRechargeAmount(user, index, spend)
const giftAmount = getGiftAmount(user, index)
let balanceBefore = 0
logs.push({
userId,
type: user.role === 'admin' ? 'admin_adjust' : 'recharge',
amount: baseRecharge,
balanceBefore,
balanceAfter: balanceBefore + baseRecharge,
orderId: user.role === 'admin' ? null : getSeedOrderNo(index),
remark: `${SEED_MARK} initial funding`
})
balanceBefore += baseRecharge
logs.push({
userId,
type: 'gift',
amount: giftAmount,
balanceBefore,
balanceAfter: balanceBefore + giftAmount,
orderId: null,
remark: `${SEED_MARK} promo gift`
})
balanceBefore += giftAmount
if (spend > 0) {
logs.push({
userId,
type: 'consume',
amount: -spend,
balanceBefore,
balanceAfter: balanceBefore - spend,
orderId: null,
remark: `${SEED_MARK} instance purchases`
})
balanceBefore -= spend
}
await prisma.user.update({
where: { id: userId },
data: { balance: balanceBefore }
})
}
await prisma.balanceLog.createMany({ data: logs })
}
async function refreshHostAndQuotaUsage(
userIds: Map<string, number>,
hostIds: Map<string, number>,
instances: GeneratedInstanceRecord[]
): Promise<void> {
const hostStats = new Map<number, { cpu: number; memory: number; disk: number; ports: number }>()
for (const hostId of hostIds.values()) {
hostStats.set(hostId, { cpu: 0, memory: 0, disk: 0, ports: 0 })
}
for (const instance of instances) {
const stats = hostStats.get(instance.hostId)
if (!stats) continue
stats.cpu += instance.plan.cpu
stats.memory += instance.plan.memory
stats.disk += instance.plan.disk
}
const portCounts = await prisma.portMapping.groupBy({
by: ['hostId'],
where: {
hostId: { in: Array.from(hostIds.values()) }
},
_count: {
_all: true
}
})
for (const row of portCounts) {
const stats = hostStats.get(row.hostId)
if (stats) {
stats.ports = row._count._all
}
}
for (const [hostName, hostId] of hostIds.entries()) {
const stats = hostStats.get(hostId)!
await prisma.host.update({
where: { id: hostId },
data: {
cpuUsed: stats.cpu,
memoryUsed: stats.memory,
diskUsed: stats.disk,
natPortsUsedCount: stats.ports
}
})
}
const hostCountByOwner = new Map<number, number>()
const packageCountByOwner = new Map<number, number>()
ownerUsers.forEach(owner => {
const ownerId = userIds.get(owner.username)!
hostCountByOwner.set(ownerId, hostSpecs.filter(host => host.ownerUsername === owner.username).length)
packageCountByOwner.set(ownerId, packageSpecs.filter(pkg => pkg.ownerUsername === owner.username).length)
})
for (const user of allSeedUsers) {
const userId = userIds.get(user.username)!
await prisma.userQuota.update({
where: { userId },
data: {
hostUsed: hostCountByOwner.get(userId) || 0,
packageUsed: packageCountByOwner.get(userId) || 0
}
})
}
}
async function printSummary(): Promise<void> {
const [users, hosts, packages, plans, instances, ports, snapshots, backups, sites, tickets] = await Promise.all([
prisma.user.count({ where: { username: { startsWith: SEED_PREFIX } } }),
prisma.host.count({ where: { name: { startsWith: `${SEED_PREFIX}-host-` } } }),
prisma.package.count({ where: { name: { startsWith: `${SEED_PREFIX}-pkg-` } } }),
prisma.packagePlan.count({ where: { package: { name: { startsWith: `${SEED_PREFIX}-pkg-` } } } }),
prisma.instance.count({ where: { name: { startsWith: `${SEED_PREFIX}-inst-` } } }),
prisma.portMapping.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
prisma.snapshot.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
prisma.backup.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
prisma.proxySite.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
prisma.ticket.count({ where: { subject: { startsWith: SEED_MARK } } })
])
console.log(`${SEED_MARK} completed`)
console.log(`${SEED_MARK} users=${users}, hosts=${hosts}, packages=${packages}, plans=${plans}, instances=${instances}`)
console.log(`${SEED_MARK} portMappings=${ports}, snapshots=${snapshots}, backups=${backups}, proxySites=${sites}, tickets=${tickets}`)
console.log(`${SEED_MARK} shared password for generated users: ${SHARED_PASSWORD}`)
}
async function main(): Promise<void> {
await ensureLocalDevGuard()
const passwordHash = await buildPasswordHash(SHARED_PASSWORD)
const userIds = await syncUsers(passwordHash)
const hostIds = await syncHosts(userIds)
const packagesByKey = await syncPackages(userIds, hostIds)
const instances = await syncInstances(userIds, hostIds, packagesByKey)
await syncTickets(instances)
await syncBalances(userIds, instances)
await refreshHostAndQuotaUsage(userIds, hostIds, instances)
await printSummary()
}
void main()
.catch(error => {
console.error(`${SEED_MARK} failed:`, error)
process.exitCode = 1
})
.finally(async () => {
await prisma.$disconnect()
})