Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b602b579eb | |||
| 7ba588497b | |||
| 75fc311a11 | |||
| 6518831a26 | |||
| b9d50fa717 | |||
| 4bcb891f24 | |||
| 8aab3ff282 | |||
| 0bd1398085 | |||
| 734869eb26 | |||
| 7a39e67a53 | |||
| 11c127c430 | |||
| 661b45ec82 | |||
| 40b9d1802b | |||
| 0e7cef6334 | |||
| 599a9e178a | |||
| d8c4425456 | |||
| 9a9868a964 | |||
| dc05d524bb | |||
| f37b1e8952 | |||
| 90fda36588 | |||
| 4d5f1d1955 | |||
| 9bd09c8b18 | |||
| afdbd1d535 | |||
| 72016cee81 | |||
| c453288a58 | |||
| 61520d9e2b | |||
| 2b452d817b | |||
| a6a2c0d4f7 |
191
ServerBuildSettingsCard.tsx
Normal file
191
ServerBuildSettingsCard.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import { AdminServerContext } from '@/state/admin/server'
|
||||||
|
import { useFlashKey } from '@/util/useFlash'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import updateBuild from '@/api/admin/servers/updateBuild'
|
||||||
|
|
||||||
|
import Button from '@/components/elements/Button'
|
||||||
|
import FlashMessageRender from '@/components/elements/FlashMessageRenderer'
|
||||||
|
import FormCard from '@/components/elements/FormCard'
|
||||||
|
import TextInputForm from '@/components/elements/forms/TextInputForm'
|
||||||
|
|
||||||
|
import AddressesMultiSelectForm from '@/components/admin/servers/AddressesMultiSelectForm'
|
||||||
|
|
||||||
|
|
||||||
|
const ServerBuildSettingsCard = () => {
|
||||||
|
const server = AdminServerContext.useStoreState(state => state.server.data!)
|
||||||
|
const setServer = AdminServerContext.useStoreActions(
|
||||||
|
actions => actions.server.setServer
|
||||||
|
)
|
||||||
|
const { clearFlashes, clearAndAddHttpError } = useFlashKey(
|
||||||
|
`admin.servers.${server.uuid}.settings.hardware.build`
|
||||||
|
)
|
||||||
|
const { t: tStrings } = useTranslation('strings')
|
||||||
|
const { t } = useTranslation('admin.servers.settings')
|
||||||
|
const { t: tIndex } = useTranslation('admin.servers.index')
|
||||||
|
|
||||||
|
const pluckedAddressIds = [
|
||||||
|
...server.limits.addresses.ipv4.map(address => address.id.toString()),
|
||||||
|
...server.limits.addresses.ipv6.map(address => address.id.toString()),
|
||||||
|
]
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
cpu: z.preprocess(Number, z.number().min(1)),
|
||||||
|
memory: z.preprocess(Number, z.number().min(16)),
|
||||||
|
disk: z.preprocess(Number, z.number().min(1)),
|
||||||
|
addressIds: z.array(z.preprocess(Number, z.number())),
|
||||||
|
snapshotLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
backupLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
bandwidthLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
rateLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
bandwidthUsage: z.preprocess(Number, z.number().min(0)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
cpu: server.limits.cpu.toString(),
|
||||||
|
memory: (server.limits.memory / 1048576).toString(),
|
||||||
|
disk: (server.limits.disk / 1048576).toString(),
|
||||||
|
addressIds: pluckedAddressIds,
|
||||||
|
snapshotLimit: server.limits.snapshots?.toString() ?? '',
|
||||||
|
backupLimit: server.limits.backups?.toString() ?? '',
|
||||||
|
bandwidthLimit: server.limits.bandwidth
|
||||||
|
? (server.limits.bandwidth / 1048576).toString()
|
||||||
|
: '',
|
||||||
|
rateLimit: server.limits.rateLimit?.toString() ?? '',
|
||||||
|
bandwidthUsage: (server.usages.bandwidth / 1048576).toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const submit = async (_data: any) => {
|
||||||
|
const {
|
||||||
|
memory,
|
||||||
|
disk,
|
||||||
|
snapshotLimit,
|
||||||
|
backupLimit,
|
||||||
|
bandwidthLimit,
|
||||||
|
rateLimit,
|
||||||
|
bandwidthUsage,
|
||||||
|
...data
|
||||||
|
} = _data as z.infer<typeof schema>
|
||||||
|
clearFlashes()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newServer = await updateBuild(server.uuid, {
|
||||||
|
memory: memory * 1048576,
|
||||||
|
disk: disk * 1048576,
|
||||||
|
snapshotLimit: snapshotLimit !== '' ? snapshotLimit : null,
|
||||||
|
backupLimit: backupLimit !== '' ? backupLimit : null,
|
||||||
|
bandwidthLimit:
|
||||||
|
bandwidthLimit !== '' ? bandwidthLimit * 1048576 : null,
|
||||||
|
rateLimit: rateLimit !== '' ? Number(rateLimit) : null,
|
||||||
|
bandwidthUsage: bandwidthUsage * 1048576,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
setServer(newServer)
|
||||||
|
|
||||||
|
form.reset({
|
||||||
|
cpu: data.cpu.toString(),
|
||||||
|
memory: memory.toString(),
|
||||||
|
disk: disk.toString(),
|
||||||
|
addressIds: data.addressIds.map(id => id.toString()),
|
||||||
|
snapshotLimit: snapshotLimit.toString() ?? '',
|
||||||
|
backupLimit: backupLimit.toString() ?? '',
|
||||||
|
bandwidthLimit:
|
||||||
|
bandwidthLimit !== '' ? bandwidthLimit.toString() : '',
|
||||||
|
rateLimit: rateLimit !== '' ? rateLimit.toString() : '',
|
||||||
|
bandwidthUsage: bandwidthUsage.toString(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
clearAndAddHttpError(error as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormCard className='w-full'>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(submit)}>
|
||||||
|
<FormCard.Body>
|
||||||
|
<FormCard.Title>{t('build.title')}</FormCard.Title>
|
||||||
|
<div className='space-y-3 mt-3'>
|
||||||
|
<FlashMessageRender
|
||||||
|
byKey={`admin.servers.${server.uuid}.settings.hardware.build`}
|
||||||
|
/>
|
||||||
|
<TextInputForm name='cpu' label={tStrings('cpu')} />
|
||||||
|
<TextInputForm
|
||||||
|
name='memory'
|
||||||
|
label={`${tStrings('memory')} (MiB)`}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='disk'
|
||||||
|
label={`${tStrings('disk')} (MiB)`}
|
||||||
|
/>
|
||||||
|
<AddressesMultiSelectForm nodeId={server.nodeId} />
|
||||||
|
<TextInputForm
|
||||||
|
name='snapshotLimit'
|
||||||
|
label={tIndex('snapshot_limit')}
|
||||||
|
placeholder={'Leave blank for no limit'}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='backupLimit'
|
||||||
|
label={tIndex('backup_limit')}
|
||||||
|
placeholder={'Leave blank for no limit'}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='bandwidthLimit'
|
||||||
|
label={`${tIndex('bandwidth_limit')} (MiB)`}
|
||||||
|
placeholder={
|
||||||
|
tIndex('limit_placeholder') ??
|
||||||
|
'Leave blank for no limit'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='rateLimit'
|
||||||
|
label={`${tIndex('rate_limit')} (MiB/s)`}
|
||||||
|
placeholder={
|
||||||
|
tIndex('limit_placeholder') ??
|
||||||
|
'Leave blank for no limit'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='bandwidthUsage'
|
||||||
|
label={`${tIndex('bandwidth_usage')} (MiB)`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormCard.Body>
|
||||||
|
<FormCard.Footer>
|
||||||
|
<Button
|
||||||
|
loading={form.formState.isSubmitting}
|
||||||
|
disabled={!form.formState.isDirty}
|
||||||
|
type='submit'
|
||||||
|
variant='filled'
|
||||||
|
color='success'
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
{tStrings('save')}
|
||||||
|
</Button>
|
||||||
|
</FormCard.Footer>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</FormCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerBuildSettingsCard
|
||||||
313
Speed/CreateServerModal.tsx
Normal file
313
Speed/CreateServerModal.tsx
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
import { useFlashKey } from '@/util/useFlash'
|
||||||
|
import usePagination from '@/util/usePagination'
|
||||||
|
import { hostname, password, usKeyboardCharacters } from '@/util/validation'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import createServer from '@/api/admin/servers/createServer'
|
||||||
|
import { ServerResponse } from '@/api/admin/servers/getServers'
|
||||||
|
import useServersSWR from '@/api/admin/servers/useServersSWR'
|
||||||
|
|
||||||
|
import FlashMessageRender from '@/components/elements/FlashMessageRenderer'
|
||||||
|
import Modal from '@/components/elements/Modal'
|
||||||
|
import CheckboxForm from '@/components/elements/forms/CheckboxForm'
|
||||||
|
import TextInputForm from '@/components/elements/forms/TextInputForm'
|
||||||
|
|
||||||
|
import AddressesMultiSelectForm from '@/components/admin/servers/AddressesMultiSelectForm'
|
||||||
|
import NodesSelectForm from '@/components/admin/servers/NodesSelectForm'
|
||||||
|
import TemplatesSelectForm from '@/components/admin/servers/TemplatesSelectForm'
|
||||||
|
import UsersSelectForm from '@/components/admin/servers/UsersSelectForm'
|
||||||
|
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nodeId?: number
|
||||||
|
userId?: number
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateServerModal = ({ nodeId, userId, open, onClose }: Props) => {
|
||||||
|
const [page] = usePagination()
|
||||||
|
const { mutate } = useServersSWR({
|
||||||
|
nodeId,
|
||||||
|
userId,
|
||||||
|
page,
|
||||||
|
query: '',
|
||||||
|
include: ['node', 'user'],
|
||||||
|
})
|
||||||
|
const { clearFlashes, clearAndAddHttpError } = useFlashKey(
|
||||||
|
'admin.servers.create'
|
||||||
|
)
|
||||||
|
const { t } = useTranslation('admin.servers.index')
|
||||||
|
const { t: tStrings } = useTranslation('strings')
|
||||||
|
|
||||||
|
const schemaWithCreateVm = z.object({
|
||||||
|
name: z.string().max(40).nonempty(),
|
||||||
|
nodeId: z.preprocess(Number, z.number()),
|
||||||
|
userId: z.preprocess(Number, z.number()),
|
||||||
|
vmid: z.union([
|
||||||
|
z.preprocess(Number, z.number().int().min(100).max(999999999)),
|
||||||
|
z.literal(''),
|
||||||
|
]),
|
||||||
|
hostname: hostname().max(191).nonempty(),
|
||||||
|
addressIds: z.array(z.preprocess(Number, z.number())),
|
||||||
|
cpu: z.preprocess(Number, z.number().min(1)),
|
||||||
|
memory: z.preprocess(Number, z.number().min(16)),
|
||||||
|
disk: z.preprocess(Number, z.number().min(1)),
|
||||||
|
snapshotLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
backupLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
bandwidthLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
rateLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
accountPassword: password(usKeyboardCharacters()).nonempty(),
|
||||||
|
shouldCreateServer: z.literal(true),
|
||||||
|
startOnCompletion: z.boolean(),
|
||||||
|
templateUuid: z.string().nonempty(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const schemaWithoutCreatingVm = z.object({
|
||||||
|
name: z.string().max(40).nonempty(),
|
||||||
|
nodeId: z.preprocess(Number, z.number()),
|
||||||
|
userId: z.preprocess(Number, z.number()),
|
||||||
|
vmid: z.union([
|
||||||
|
z.preprocess(Number, z.number().int().min(100).max(999999999)),
|
||||||
|
z.literal(''),
|
||||||
|
]),
|
||||||
|
hostname: hostname().max(191).nonempty(),
|
||||||
|
addressIds: z.array(z.preprocess(Number, z.number())),
|
||||||
|
cpu: z.preprocess(Number, z.number().min(1)),
|
||||||
|
memory: z.preprocess(Number, z.number().min(16)),
|
||||||
|
disk: z.preprocess(Number, z.number().min(1)),
|
||||||
|
snapshotLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
backupLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
bandwidthLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
rateLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
accountPassword: password(usKeyboardCharacters()).optional(),
|
||||||
|
shouldCreateServer: z.literal(false),
|
||||||
|
startOnCompletion: z.boolean(),
|
||||||
|
templateUuid: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const schema = z.discriminatedUnion('shouldCreateServer', [
|
||||||
|
schemaWithCreateVm,
|
||||||
|
schemaWithoutCreatingVm,
|
||||||
|
])
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
nodeId: nodeId?.toString() ?? '',
|
||||||
|
userId: userId?.toString() ?? '',
|
||||||
|
vmid: '',
|
||||||
|
hostname: '',
|
||||||
|
addressIds: [],
|
||||||
|
cpu: '',
|
||||||
|
memory: '',
|
||||||
|
disk: '',
|
||||||
|
snapshotLimit: '0',
|
||||||
|
backupLimit: '',
|
||||||
|
bandwidthLimit: '',
|
||||||
|
rateLimit: '',
|
||||||
|
accountPassword: '',
|
||||||
|
shouldCreateServer: true,
|
||||||
|
startOnCompletion: false,
|
||||||
|
templateUuid: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const watchShouldCreateServer = form.watch('shouldCreateServer')
|
||||||
|
const watchNodeId = form.watch('nodeId')
|
||||||
|
|
||||||
|
const submit = async (_data: any) => {
|
||||||
|
const {
|
||||||
|
vmid,
|
||||||
|
cpu,
|
||||||
|
memory,
|
||||||
|
disk,
|
||||||
|
snapshotLimit,
|
||||||
|
backupLimit,
|
||||||
|
bandwidthLimit,
|
||||||
|
rateLimit,
|
||||||
|
addressIds,
|
||||||
|
accountPassword,
|
||||||
|
...data
|
||||||
|
} = _data as z.infer<typeof schema>
|
||||||
|
clearFlashes()
|
||||||
|
try {
|
||||||
|
const server = await createServer({
|
||||||
|
...data,
|
||||||
|
vmid: vmid !== '' ? vmid : null,
|
||||||
|
limits: {
|
||||||
|
cpu,
|
||||||
|
memory: memory * 1048576,
|
||||||
|
disk: disk * 1048576,
|
||||||
|
snapshots: snapshotLimit !== '' ? snapshotLimit : null,
|
||||||
|
backups: backupLimit !== '' ? backupLimit : null,
|
||||||
|
bandwidth:
|
||||||
|
bandwidthLimit !== '' ? bandwidthLimit * 1048576 : null,
|
||||||
|
rateLimit: rateLimit !== '' ? Number(rateLimit) : null,
|
||||||
|
addressIds,
|
||||||
|
},
|
||||||
|
accountPassword: accountPassword ? accountPassword : null,
|
||||||
|
})
|
||||||
|
|
||||||
|
mutate(data => {
|
||||||
|
if (!data) return data
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
items: [server, ...data.items],
|
||||||
|
} as ServerResponse
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
handleClose()
|
||||||
|
} catch (e) {
|
||||||
|
clearAndAddHttpError(e as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
clearFlashes()
|
||||||
|
form.reset()
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={handleClose}>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>{t('create_modal.title')}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(submit)}>
|
||||||
|
<Modal.Body>
|
||||||
|
<FlashMessageRender
|
||||||
|
className='mb-5'
|
||||||
|
byKey={'admin.servers.create'}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name={'name'}
|
||||||
|
label={tStrings('display_name')}
|
||||||
|
/>
|
||||||
|
{nodeId ? null : <NodesSelectForm />}
|
||||||
|
{userId ? null : <UsersSelectForm />}
|
||||||
|
<TextInputForm
|
||||||
|
name={'vmid'}
|
||||||
|
label={'VMID'}
|
||||||
|
placeholder={
|
||||||
|
t('vmid_placeholder') ??
|
||||||
|
'Leave blank for random VMID'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name={'hostname'}
|
||||||
|
label={tStrings('hostname')}
|
||||||
|
/>
|
||||||
|
<AddressesMultiSelectForm
|
||||||
|
disabled={watchNodeId === ''}
|
||||||
|
/>
|
||||||
|
<div className={'grid grid-cols-2 gap-3'}>
|
||||||
|
<TextInputForm
|
||||||
|
name={'cpu'}
|
||||||
|
label={tStrings('cpu')}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name={'memory'}
|
||||||
|
label={`${tStrings('memory')} (MiB)`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TextInputForm
|
||||||
|
name={'disk'}
|
||||||
|
label={`${tStrings('disk')} (MiB)`}
|
||||||
|
/>
|
||||||
|
<div className={'grid grid-cols-2 gap-3'}>
|
||||||
|
<TextInputForm
|
||||||
|
name={'backupLimit'}
|
||||||
|
label={t('backup_limit')}
|
||||||
|
placeholder={
|
||||||
|
t('limit_placeholder') ??
|
||||||
|
'Leave blank for no limit'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name={'bandwidthLimit'}
|
||||||
|
label={`${t('bandwidth_limit')} (MiB)`}
|
||||||
|
placeholder={
|
||||||
|
t('limit_placeholder') ??
|
||||||
|
'Leave blank for no limit'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<TextInputForm
|
||||||
|
name={'rateLimit'}
|
||||||
|
label={`${t('rate_limit')} (MiB/s)`}
|
||||||
|
placeholder={
|
||||||
|
t('limit_placeholder') ??
|
||||||
|
'Leave blank for no limit'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name={'accountPassword'}
|
||||||
|
label={tStrings('system_os_password')}
|
||||||
|
type={'password'}
|
||||||
|
/>
|
||||||
|
<CheckboxForm
|
||||||
|
name={'shouldCreateServer'}
|
||||||
|
label={t('should_create_vm')}
|
||||||
|
className={'mt-3 relative'}
|
||||||
|
/>
|
||||||
|
<TemplatesSelectForm
|
||||||
|
disabled={
|
||||||
|
!watchShouldCreateServer || watchNodeId === ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<CheckboxForm
|
||||||
|
name={'startOnCompletion'}
|
||||||
|
label={t('start_server_after_installing')}
|
||||||
|
className={'mt-3 relative'}
|
||||||
|
/>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Actions>
|
||||||
|
<Modal.Action type='button' onClick={handleClose}>
|
||||||
|
{tStrings('cancel')}
|
||||||
|
</Modal.Action>
|
||||||
|
<Modal.Action
|
||||||
|
type='submit'
|
||||||
|
loading={form.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
{tStrings('create')}
|
||||||
|
</Modal.Action>
|
||||||
|
</Modal.Actions>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreateServerModal
|
||||||
260
Speed/NetworkService.php
Normal file
260
Speed/NetworkService.php
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Convoy\Services\Servers;
|
||||||
|
|
||||||
|
use Convoy\Data\Server\Deployments\CloudinitAddressConfigData;
|
||||||
|
use Convoy\Data\Server\Eloquent\ServerAddressesData;
|
||||||
|
use Convoy\Data\Server\MacAddressData;
|
||||||
|
use Convoy\Enums\Network\AddressType;
|
||||||
|
use Convoy\Models\Address;
|
||||||
|
use Convoy\Models\Server;
|
||||||
|
use Convoy\Repositories\Eloquent\AddressRepository;
|
||||||
|
use Convoy\Repositories\Proxmox\Server\ProxmoxCloudinitRepository;
|
||||||
|
use Convoy\Repositories\Proxmox\Server\ProxmoxConfigRepository;
|
||||||
|
use Convoy\Repositories\Proxmox\Server\ProxmoxFirewallRepository;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use function collect;
|
||||||
|
use function is_null;
|
||||||
|
|
||||||
|
class NetworkService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AddressRepository $repository,
|
||||||
|
private ProxmoxFirewallRepository $firewallRepository,
|
||||||
|
private CloudinitService $cloudinitService,
|
||||||
|
private ProxmoxCloudinitRepository $cloudinitRepository,
|
||||||
|
private ProxmoxConfigRepository $allocationRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteIpset(Server $server, string $name)
|
||||||
|
{
|
||||||
|
$this->firewallRepository->setServer($server);
|
||||||
|
|
||||||
|
$addresses = array_column($this->firewallRepository->getLockedIps($name), 'cidr');
|
||||||
|
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$this->firewallRepository->unlockIp($name, $address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->firewallRepository->deleteIpset($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearIpsets(Server $server): void
|
||||||
|
{
|
||||||
|
$this->firewallRepository->setServer($server);
|
||||||
|
|
||||||
|
$ipSets = array_column($this->firewallRepository->getIpsets(), 'name');
|
||||||
|
|
||||||
|
foreach ($ipSets as $ipSet) {
|
||||||
|
$this->deleteIpset($server, $ipSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lockIps(Server $server, array $addresses, string $ipsetName): void
|
||||||
|
{
|
||||||
|
$this->firewallRepository->setServer($server);
|
||||||
|
|
||||||
|
$this->firewallRepository->createIpset($ipsetName);
|
||||||
|
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$this->firewallRepository->lockIp($ipsetName, $address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMacAddresses(Server $server, bool $eloquent = true, bool $proxmox = false): MacAddressData
|
||||||
|
{
|
||||||
|
if ($eloquent) {
|
||||||
|
$addresses = $this->getAddresses($server);
|
||||||
|
|
||||||
|
$eloquentMacAddress = $addresses->ipv4->first(
|
||||||
|
)?->mac_address ?? $addresses->ipv6->first()?->mac_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($proxmox) {
|
||||||
|
$config = $this->cloudinitRepository->setServer($server)->getConfig();
|
||||||
|
|
||||||
|
$proxmoxMacAddress = null;
|
||||||
|
if (preg_match(
|
||||||
|
"/\b[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\b/su",
|
||||||
|
Arr::get($config, 'net0', ''),
|
||||||
|
$matches,
|
||||||
|
)) {
|
||||||
|
$proxmoxMacAddress = $matches[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MacAddressData::from([
|
||||||
|
'eloquent' => $eloquentMacAddress ?? null,
|
||||||
|
'proxmox' => $proxmoxMacAddress ?? null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAddresses(Server $server): ServerAddressesData
|
||||||
|
{
|
||||||
|
return ServerAddressesData::from([
|
||||||
|
'ipv4' => array_values(
|
||||||
|
$server->addresses->where('type', AddressType::IPV4->value)->toArray(),
|
||||||
|
),
|
||||||
|
'ipv6' => array_values(
|
||||||
|
$server->addresses->where('type', AddressType::IPV6->value)->toArray(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncSettings(Server $server): void
|
||||||
|
{
|
||||||
|
$macAddresses = $this->getMacAddresses($server, true, true);
|
||||||
|
$addresses = $this->getAddresses($server);
|
||||||
|
|
||||||
|
$this->clearIpsets($server);
|
||||||
|
$this->cloudinitService->updateIpConfig($server, CloudinitAddressConfigData::from([
|
||||||
|
'ipv4' => $addresses->ipv4->first()?->toArray(),
|
||||||
|
'ipv6' => $addresses->ipv6->first()?->toArray(),
|
||||||
|
]));
|
||||||
|
$this->lockIps(
|
||||||
|
$server,
|
||||||
|
array_unique(Arr::flatten($server->addresses()->get(['address'])->toArray())),
|
||||||
|
'ipfilter-net0',
|
||||||
|
);
|
||||||
|
$this->firewallRepository->setServer($server)->updateOptions([
|
||||||
|
'enable' => true,
|
||||||
|
'ipfilter' => true,
|
||||||
|
'policy_in' => 'ACCEPT',
|
||||||
|
'policy_out' => 'ACCEPT',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$macAddress = $macAddresses->eloquent ?? $macAddresses->proxmox;
|
||||||
|
|
||||||
|
$this->allocationRepository->setServer($server)->update(
|
||||||
|
['net0' => "virtio={$macAddress},bridge={$server->node->network},firewall=1"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateRateLimit(Server $server, ?float $mebibytes = null): void
|
||||||
|
{
|
||||||
|
$macAddresses = $this->getMacAddresses($server, true, true);
|
||||||
|
$macAddress = $macAddresses->eloquent ?? $macAddresses->proxmox;
|
||||||
|
$rawConfig = $this->allocationRepository->setServer($server)->getConfig();
|
||||||
|
$networkConfig = collect($rawConfig)->where('key', '=', 'net0')->first();
|
||||||
|
|
||||||
|
if (is_null($networkConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsedConfig = $this->parseConfig($networkConfig['value']);
|
||||||
|
|
||||||
|
// List of possible models
|
||||||
|
$models = ['e1000', 'e1000-82540em', 'e1000-82544gc', 'e1000-82545em', 'e1000e', 'i82551', 'i82557b', 'i82559er', 'ne2k_isa', 'ne2k_pci', 'pcnet', 'rtl8139', 'virtio', 'vmxnet3'];
|
||||||
|
|
||||||
|
// Update the model with the new MAC address
|
||||||
|
$modelFound = false;
|
||||||
|
foreach ($parsedConfig as $item) {
|
||||||
|
if (in_array($item->key, $models)) {
|
||||||
|
$item->value = $macAddress;
|
||||||
|
$modelFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no model key exists, add the default model with the MAC address
|
||||||
|
if (!$modelFound) {
|
||||||
|
$parsedConfig[] = (object) ['key' => 'virtio', 'value' => $macAddress];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or create the bridge value
|
||||||
|
$bridgeFound = false;
|
||||||
|
foreach ($parsedConfig as $item) {
|
||||||
|
if ($item->key === 'bridge') {
|
||||||
|
$item->value = $server->node->network;
|
||||||
|
$bridgeFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$bridgeFound) {
|
||||||
|
$parsedConfig[] = (object) ['key' => 'bridge', 'value' => $server->node->network];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update or create the firewall key
|
||||||
|
$firewallFound = false;
|
||||||
|
foreach ($parsedConfig as $item) {
|
||||||
|
if ($item->key === 'firewall') {
|
||||||
|
$item->value = 1;
|
||||||
|
$firewallFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$firewallFound) {
|
||||||
|
$parsedConfig[] = (object) ['key' => 'firewall', 'value' => 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the rate limit
|
||||||
|
if (is_null($mebibytes)) {
|
||||||
|
// Remove the 'rate' key if $mebibytes is null
|
||||||
|
$parsedConfig = array_filter($parsedConfig, fn ($item) => $item->key !== 'rate');
|
||||||
|
} else {
|
||||||
|
// Add or update the 'rate' key
|
||||||
|
$rateUpdated = false;
|
||||||
|
foreach ($parsedConfig as $item) {
|
||||||
|
if ($item->key === 'rate') {
|
||||||
|
$item->value = $mebibytes;
|
||||||
|
$rateUpdated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$rateUpdated) {
|
||||||
|
$parsedConfig[] = (object) ['key' => 'rate', 'value' => $mebibytes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the configuration string
|
||||||
|
$newConfig = implode(',', array_map(fn ($item) => "{$item->key}={$item->value}", $parsedConfig));
|
||||||
|
|
||||||
|
// Update the Proxmox configuration
|
||||||
|
$this->allocationRepository->setServer($server)->update(['net0' => $newConfig]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseConfig(string $config): array
|
||||||
|
{
|
||||||
|
// Split components by commas
|
||||||
|
$components = explode(',', $config);
|
||||||
|
|
||||||
|
// Array to hold the parsed objects
|
||||||
|
$parsedObjects = [];
|
||||||
|
|
||||||
|
foreach ($components as $component) {
|
||||||
|
// Split each component into key and value
|
||||||
|
[$key, $value] = explode('=', $component);
|
||||||
|
|
||||||
|
// Create an associative array (or object) for key-value pairs
|
||||||
|
$parsedObjects[] = (object) ['key' => $key, 'value' => $value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parsedObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateAddresses(Server $server, array $addressIds): void
|
||||||
|
{
|
||||||
|
$currentAddresses = $server->addresses()->get()->pluck('id')->toArray();
|
||||||
|
|
||||||
|
$addressesToAdd = array_diff($addressIds, $currentAddresses);
|
||||||
|
$addressesToRemove = array_filter(
|
||||||
|
$currentAddresses,
|
||||||
|
fn ($id) => !in_array($id, $addressIds),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($addressesToAdd)) {
|
||||||
|
$this->repository->attachAddresses($server, $addressesToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($addressesToRemove)) {
|
||||||
|
Address::query()
|
||||||
|
->where('server_id', $server->id)
|
||||||
|
->whereIn('id', $addressesToRemove)
|
||||||
|
->update(['server_id' => null]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,11 +38,11 @@ class Server extends Model
|
|||||||
'cpu' => 'required|numeric|min:1',
|
'cpu' => 'required|numeric|min:1',
|
||||||
'memory' => 'required|numeric|min:16777216',
|
'memory' => 'required|numeric|min:16777216',
|
||||||
'disk' => 'required|numeric|min:1',
|
'disk' => 'required|numeric|min:1',
|
||||||
'bandwidth_usage' => 'sometimes|numeric|min:0',
|
'bandwidth_usage' => 'nullable|numeric|min:0',
|
||||||
'snapshot_limit' => 'present|nullable|integer|min:0',
|
'snapshot_limit' => 'nullable|integer|min:0',
|
||||||
'backup_limit' => 'present|nullable|integer|min:0',
|
'backup_limit' => 'nullable|integer|min:0',
|
||||||
'bandwidth_limit' => 'present|nullable|integer|min:0',
|
'bandwidth_limit' => 'nullable|integer|min:0',
|
||||||
'rate_limit' => 'sometimes|nullable|numeric|min:0',
|
'rate_limit' => 'nullable|numeric|min:0',
|
||||||
'hydrated_at' => 'nullable|date',
|
'hydrated_at' => 'nullable|date',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
175
Speed/ServerBuildSettingsCard.tsx
Normal file
175
Speed/ServerBuildSettingsCard.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { AdminServerContext } from '@/state/admin/server'
|
||||||
|
import { useFlashKey } from '@/util/useFlash'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
import updateBuild from '@/api/admin/servers/updateBuild'
|
||||||
|
|
||||||
|
import Button from '@/components/elements/Button'
|
||||||
|
import FlashMessageRender from '@/components/elements/FlashMessageRenderer'
|
||||||
|
import FormCard from '@/components/elements/FormCard'
|
||||||
|
import TextInputForm from '@/components/elements/forms/TextInputForm'
|
||||||
|
|
||||||
|
import AddressesMultiSelectForm from '@/components/admin/servers/AddressesMultiSelectForm'
|
||||||
|
|
||||||
|
|
||||||
|
const ServerBuildSettingsCard = () => {
|
||||||
|
const server = AdminServerContext.useStoreState(state => state.server.data!)
|
||||||
|
const setServer = AdminServerContext.useStoreActions(
|
||||||
|
actions => actions.server.setServer
|
||||||
|
)
|
||||||
|
const { clearFlashes, clearAndAddHttpError } = useFlashKey(
|
||||||
|
`admin.servers.${server.uuid}.settings.hardware.build`
|
||||||
|
)
|
||||||
|
const { t: tStrings } = useTranslation('strings')
|
||||||
|
const { t } = useTranslation('admin.servers.settings')
|
||||||
|
const { t: tIndex } = useTranslation('admin.servers.index')
|
||||||
|
|
||||||
|
const pluckedAddressIds = [
|
||||||
|
...server.limits.addresses.ipv4.map(address => address.id.toString()),
|
||||||
|
...server.limits.addresses.ipv6.map(address => address.id.toString()),
|
||||||
|
]
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
cpu: z.preprocess(Number, z.number().min(1)),
|
||||||
|
memory: z.preprocess(Number, z.number().min(16)),
|
||||||
|
disk: z.preprocess(Number, z.number().min(1)),
|
||||||
|
addressIds: z.array(z.preprocess(Number, z.number())),
|
||||||
|
snapshotLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
backupLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
bandwidthLimit: z.union([
|
||||||
|
z.literal(''),
|
||||||
|
z.preprocess(Number, z.number().min(0)),
|
||||||
|
]),
|
||||||
|
bandwidthUsage: z.preprocess(Number, z.number().min(0)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
cpu: server.limits.cpu.toString(),
|
||||||
|
memory: (server.limits.memory / 1048576).toString(),
|
||||||
|
disk: (server.limits.disk / 1048576).toString(),
|
||||||
|
addressIds: pluckedAddressIds,
|
||||||
|
snapshotLimit: server.limits.snapshots?.toString() ?? '',
|
||||||
|
backupLimit: server.limits.backups?.toString() ?? '',
|
||||||
|
bandwidthLimit: server.limits.bandwidth
|
||||||
|
? (server.limits.bandwidth / 1048576).toString()
|
||||||
|
: '',
|
||||||
|
bandwidthUsage: (server.usages.bandwidth / 1048576).toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const submit = async (_data: any) => {
|
||||||
|
const {
|
||||||
|
memory,
|
||||||
|
disk,
|
||||||
|
snapshotLimit,
|
||||||
|
backupLimit,
|
||||||
|
bandwidthLimit,
|
||||||
|
bandwidthUsage,
|
||||||
|
...data
|
||||||
|
} = _data as z.infer<typeof schema>
|
||||||
|
clearFlashes()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newServer = await updateBuild(server.uuid, {
|
||||||
|
memory: memory * 1048576,
|
||||||
|
disk: disk * 1048576,
|
||||||
|
snapshotLimit: snapshotLimit !== '' ? snapshotLimit : null,
|
||||||
|
backupLimit: backupLimit !== '' ? backupLimit : null,
|
||||||
|
bandwidthLimit:
|
||||||
|
bandwidthLimit !== '' ? bandwidthLimit * 1048576 : null,
|
||||||
|
bandwidthUsage: bandwidthUsage * 1048576,
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
|
||||||
|
setServer(newServer)
|
||||||
|
|
||||||
|
form.reset({
|
||||||
|
cpu: data.cpu.toString(),
|
||||||
|
memory: memory.toString(),
|
||||||
|
disk: disk.toString(),
|
||||||
|
addressIds: data.addressIds.map(id => id.toString()),
|
||||||
|
snapshotLimit: snapshotLimit.toString() ?? '',
|
||||||
|
backupLimit: backupLimit.toString() ?? '',
|
||||||
|
bandwidthLimit:
|
||||||
|
bandwidthLimit !== '' ? bandwidthLimit.toString() : '',
|
||||||
|
bandwidthUsage: bandwidthUsage.toString(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
clearAndAddHttpError(error as any)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormCard className='w-full'>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(submit)}>
|
||||||
|
<FormCard.Body>
|
||||||
|
<FormCard.Title>{t('build.title')}</FormCard.Title>
|
||||||
|
<div className='space-y-3 mt-3'>
|
||||||
|
<FlashMessageRender
|
||||||
|
byKey={`admin.servers.${server.uuid}.settings.hardware.build`}
|
||||||
|
/>
|
||||||
|
<TextInputForm name='cpu' label={tStrings('cpu')} />
|
||||||
|
<TextInputForm
|
||||||
|
name='memory'
|
||||||
|
label={`${tStrings('memory')} (MiB)`}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='disk'
|
||||||
|
label={`${tStrings('disk')} (MiB)`}
|
||||||
|
/>
|
||||||
|
<AddressesMultiSelectForm nodeId={server.nodeId} />
|
||||||
|
<TextInputForm
|
||||||
|
name='snapshotLimit'
|
||||||
|
label={tIndex('snapshot_limit')}
|
||||||
|
placeholder={'Leave blank for no limit'}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='backupLimit'
|
||||||
|
label={tIndex('backup_limit')}
|
||||||
|
placeholder={'Leave blank for no limit'}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='bandwidthLimit'
|
||||||
|
label={`${tIndex('bandwidth_limit')} (MiB)`}
|
||||||
|
placeholder={
|
||||||
|
tIndex('limit_placeholder') ??
|
||||||
|
'Leave blank for no limit'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TextInputForm
|
||||||
|
name='bandwidthUsage'
|
||||||
|
label={`${tIndex('bandwidth_usage')} (MiB)`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormCard.Body>
|
||||||
|
<FormCard.Footer>
|
||||||
|
<Button
|
||||||
|
loading={form.formState.isSubmitting}
|
||||||
|
disabled={!form.formState.isDirty}
|
||||||
|
type='submit'
|
||||||
|
variant='filled'
|
||||||
|
color='success'
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
{tStrings('save')}
|
||||||
|
</Button>
|
||||||
|
</FormCard.Footer>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</FormCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ServerBuildSettingsCard
|
||||||
163
Speed/ServerController.php
Normal file
163
Speed/ServerController.php
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Convoy\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use Convoy\Enums\Server\Status;
|
||||||
|
use Convoy\Enums\Server\SuspensionAction;
|
||||||
|
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
|
||||||
|
use Convoy\Http\Controllers\ApiController;
|
||||||
|
use Convoy\Http\Requests\Admin\Servers\Settings\UpdateBuildRequest;
|
||||||
|
use Convoy\Http\Requests\Admin\Servers\Settings\UpdateGeneralInfoRequest;
|
||||||
|
use Convoy\Http\Requests\Admin\Servers\StoreServerRequest;
|
||||||
|
use Convoy\Models\Filters\FiltersServerByAddressPoolId;
|
||||||
|
use Convoy\Models\Filters\FiltersServerWildcard;
|
||||||
|
use Convoy\Models\Server;
|
||||||
|
use Convoy\Services\Servers\CloudinitService;
|
||||||
|
use Convoy\Services\Servers\NetworkService;
|
||||||
|
use Convoy\Services\Servers\ServerCreationService;
|
||||||
|
use Convoy\Services\Servers\ServerDeletionService;
|
||||||
|
use Convoy\Services\Servers\ServerSuspensionService;
|
||||||
|
use Convoy\Services\Servers\SyncBuildService;
|
||||||
|
use Convoy\Transformers\Admin\ServerBuildTransformer;
|
||||||
|
use Illuminate\Database\ConnectionInterface;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Spatie\QueryBuilder\AllowedFilter;
|
||||||
|
use Spatie\QueryBuilder\QueryBuilder;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||||
|
|
||||||
|
class ServerController extends ApiController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ConnectionInterface $connection,
|
||||||
|
private ServerDeletionService $deletionService,
|
||||||
|
private NetworkService $networkService,
|
||||||
|
private ServerSuspensionService $suspensionService,
|
||||||
|
private ServerCreationService $creationService,
|
||||||
|
private CloudinitService $cloudinitService,
|
||||||
|
private SyncBuildService $buildModificationService,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$servers = QueryBuilder::for(Server::query())
|
||||||
|
->with(['addresses', 'user', 'node'])
|
||||||
|
->defaultSort('-id')
|
||||||
|
->allowedFilters(
|
||||||
|
[
|
||||||
|
AllowedFilter::custom(
|
||||||
|
'*', new FiltersServerWildcard(),
|
||||||
|
),
|
||||||
|
AllowedFilter::custom(
|
||||||
|
'address_pool_id',
|
||||||
|
new FiltersServerByAddressPoolId(),
|
||||||
|
),
|
||||||
|
AllowedFilter::exact('node_id'),
|
||||||
|
AllowedFilter::exact('user_id'),
|
||||||
|
'name',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
->paginate(min($request->query('per_page', 50), 100))->appends(
|
||||||
|
$request->query(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return fractal($servers, new ServerBuildTransformer())->parseIncludes($request->include)
|
||||||
|
->respond();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, Server $server)
|
||||||
|
{
|
||||||
|
$server->load(['addresses', 'user', 'node']);
|
||||||
|
|
||||||
|
return fractal($server, new ServerBuildTransformer())->parseIncludes($request->include)
|
||||||
|
->respond();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreServerRequest $request)
|
||||||
|
{
|
||||||
|
$server = $this->creationService->handle($request->validated());
|
||||||
|
|
||||||
|
$server->load(['addresses', 'user', 'node']);
|
||||||
|
|
||||||
|
return fractal($server, new ServerBuildTransformer())->parseIncludes(['user', 'node'])
|
||||||
|
->respond();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateGeneralInfoRequest $request, Server $server)
|
||||||
|
{
|
||||||
|
$this->connection->transaction(function () use ($request, $server) {
|
||||||
|
if ($request->hostname !== $server->hostname && !empty($request->hostname)) {
|
||||||
|
try {
|
||||||
|
$this->cloudinitService->updateHostname($server, $request->hostname);
|
||||||
|
} catch (ProxmoxConnectionException) {
|
||||||
|
throw new ServiceUnavailableHttpException(
|
||||||
|
message: "Server {$server->uuid} failed to sync hostname.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$server->update($request->validated());
|
||||||
|
});
|
||||||
|
|
||||||
|
$server->load(['addresses', 'user', 'node']);
|
||||||
|
|
||||||
|
return fractal($server, new ServerBuildTransformer())->parseIncludes(['user', 'node'])
|
||||||
|
->respond();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateBuild(UpdateBuildRequest $request, Server $server)
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
// Handle address_ids separately
|
||||||
|
$addressIds = $validated['address_ids'] ?? null;
|
||||||
|
unset($validated['address_ids']);
|
||||||
|
|
||||||
|
// Ensure rate_limit is always present in update data to avoid validation errors
|
||||||
|
if (!array_key_exists('rate_limit', $validated)) {
|
||||||
|
$validated['rate_limit'] = $server->rate_limit ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server with validated data, skip model validation as request validation is already done
|
||||||
|
$server->skipValidation()->update($validated);
|
||||||
|
|
||||||
|
$this->networkService->updateAddresses($server, $addressIds ?? []);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->buildModificationService->handle($server);
|
||||||
|
} catch (ProxmoxConnectionException $e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
$server->load(['addresses', 'user', 'node']);
|
||||||
|
|
||||||
|
return fractal($server, new ServerBuildTransformer())->parseIncludes(['user', 'node'])
|
||||||
|
->respond();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function suspend(Server $server)
|
||||||
|
{
|
||||||
|
$this->suspensionService->toggle($server);
|
||||||
|
|
||||||
|
return $this->returnNoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unsuspend(Server $server)
|
||||||
|
{
|
||||||
|
$this->suspensionService->toggle($server, SuspensionAction::UNSUSPEND);
|
||||||
|
|
||||||
|
return $this->returnNoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Request $request, Server $server)
|
||||||
|
{
|
||||||
|
$this->connection->transaction(function () use ($server, $request) {
|
||||||
|
$server->update(['status' => Status::DELETING->value]);
|
||||||
|
|
||||||
|
$this->deletionService->handle($server, $request->input('no_purge', false));
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->returnNoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Speed/ServerUsagesSyncService.php
Normal file
49
Speed/ServerUsagesSyncService.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Convoy\Services\Nodes;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Convoy\Models\Node;
|
||||||
|
use Convoy\Models\Server;
|
||||||
|
use Convoy\Enums\Server\MetricTimeframe;
|
||||||
|
use Convoy\Repositories\Proxmox\Server\ProxmoxMetricsRepository;
|
||||||
|
use Convoy\Exceptions\Repository\Proxmox\ProxmoxConnectionException;
|
||||||
|
|
||||||
|
class ServerUsagesSyncService
|
||||||
|
{
|
||||||
|
public function __construct(private ProxmoxMetricsRepository $repository)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(Node $node)
|
||||||
|
{
|
||||||
|
$servers = $node->servers;
|
||||||
|
|
||||||
|
$servers->each(function (Server $server) {
|
||||||
|
try {
|
||||||
|
$metrics = $this->repository->setServer($server)->getMetrics(MetricTimeframe::HOUR);
|
||||||
|
|
||||||
|
$bandwidth = $server->bandwidth_usage;
|
||||||
|
$endingDate = $server->hydrated_at ? Carbon::parse($server->hydrated_at) : Carbon::now()->firstOfMonth();
|
||||||
|
|
||||||
|
foreach ($metrics as $metric) {
|
||||||
|
if (Carbon::createFromTimestamp($metric['time'])->gt($endingDate)) {
|
||||||
|
// we multiply it by 60 seconds because each metric is
|
||||||
|
// recorded every 1 minute but the values like netin and
|
||||||
|
// netout are in bytes/sec
|
||||||
|
$bandwidth += (int) $metric['netin'] * 60 + (int) $metric['netout'] * 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($bandwidth > 0) {
|
||||||
|
$server->skipValidation()->update([
|
||||||
|
'bandwidth_usage' => $bandwidth,
|
||||||
|
'hydrated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (ProxmoxConnectionException $e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,16 @@ use Illuminate\Validation\Validator;
|
|||||||
*/
|
*/
|
||||||
class StoreServerRequest extends BaseApiRequest
|
class StoreServerRequest extends BaseApiRequest
|
||||||
{
|
{
|
||||||
|
protected function prepareForValidation()
|
||||||
|
{
|
||||||
|
// Map camelCase to snake_case for rateLimit
|
||||||
|
$this->merge([
|
||||||
|
'limits' => [
|
||||||
|
'rate_limit' => $this->input('limits.rateLimit'),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$rules = Server::getRules();
|
$rules = Server::getRules();
|
||||||
|
|||||||
@ -10,6 +10,14 @@ use Convoy\Http\Requests\BaseApiRequest;
|
|||||||
|
|
||||||
class UpdateBuildRequest extends BaseApiRequest
|
class UpdateBuildRequest extends BaseApiRequest
|
||||||
{
|
{
|
||||||
|
protected function prepareForValidation()
|
||||||
|
{
|
||||||
|
// Map camelCase to snake_case for rateLimit
|
||||||
|
$this->merge([
|
||||||
|
'rate_limit' => $this->input('rateLimit'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$server = $this->parameter('server', Server::class);
|
$server = $this->parameter('server', Server::class);
|
||||||
|
|||||||
@ -9,11 +9,24 @@ convoy/app/Services/Servers/ServerCreationService.php
|
|||||||
|
|
||||||
convoy/app/Services/Servers/SyncBuildService.php
|
convoy/app/Services/Servers/SyncBuildService.php
|
||||||
|
|
||||||
|
convoy/app/Http/Controllers/Admin/ServerController.php
|
||||||
|
|
||||||
|
convoy/app/Services/Nodes/ServerUsagesSyncService.php
|
||||||
|
|
||||||
|
#FRONT END REBUILD
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
#DATA BASE Migration (Convoy)
|
#DATA BASE Migration (Convoy)
|
||||||
php artisan migrate
|
php artisan migrate
|
||||||
|
|
||||||
#USE DOCKER (Convoy)
|
#DATA BASE Migration USE DOCKER (Convoy)
|
||||||
docker compose exec workspace bash -c "php artisan migrate"
|
docker compose exec workspace bash -c "php artisan migrate"
|
||||||
|
docker compose exec workspace bash -c "php artisan cache:clear"
|
||||||
|
docker compose exec workspace bash -c "php artisan config:clear"
|
||||||
|
docker compose exec workspace bash -c "php artisan route:clear"
|
||||||
|
docker compose exec workspace bash -c "php artisan horizon:terminate"
|
||||||
|
docker compose exec workspace bash -c "php artisan queue:restart"
|
||||||
|
|
||||||
#RESTART (Convoy)
|
#RESTART (Convoy)
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|||||||
43
Speed/updateBuild.ts
Normal file
43
Speed/updateBuild.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { rawDataToAdminServer } from '@/api/admin/servers/getServer'
|
||||||
|
import http from '@/api/http'
|
||||||
|
|
||||||
|
interface UpdateServerBuildParameters {
|
||||||
|
cpu: number
|
||||||
|
memory: number
|
||||||
|
disk: number
|
||||||
|
addressIds: number[]
|
||||||
|
snapshotLimit: number | null
|
||||||
|
backupLimit: number | null
|
||||||
|
bandwidthLimit: number | null
|
||||||
|
rateLimit: number | null
|
||||||
|
bandwidthUsage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBuild = async (
|
||||||
|
serverUuid: string,
|
||||||
|
{
|
||||||
|
addressIds,
|
||||||
|
snapshotLimit,
|
||||||
|
backupLimit,
|
||||||
|
bandwidthLimit,
|
||||||
|
rateLimit,
|
||||||
|
bandwidthUsage,
|
||||||
|
...params
|
||||||
|
}: UpdateServerBuildParameters
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
data: { data },
|
||||||
|
} = await http.patch(`/api/admin/servers/${serverUuid}/settings/build`, {
|
||||||
|
address_ids: addressIds,
|
||||||
|
snapshot_limit: snapshotLimit,
|
||||||
|
backup_limit: backupLimit,
|
||||||
|
bandwidth_limit: bandwidthLimit,
|
||||||
|
rate_limit: rateLimit,
|
||||||
|
bandwidth_usage: bandwidthUsage,
|
||||||
|
...params,
|
||||||
|
})
|
||||||
|
|
||||||
|
return rawDataToAdminServer(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default updateBuild
|
||||||
Loading…
x
Reference in New Issue
Block a user