diff --git a/dashboard/src/components/BackupProviderForm.vue b/dashboard/src/components/BackupProviderForm.vue index 495e72ab5..7c07b38da 100644 --- a/dashboard/src/components/BackupProviderForm.vue +++ b/dashboard/src/components/BackupProviderForm.vue @@ -4,20 +4,26 @@ import { ref, onMounted, watch } from 'vue'; import { Button, InputGroup, SingleSelect, FormGroup, TextInput, Checkbox, PasswordInput, NumberInput } from 'pankow'; import { prettyBinarySize } from 'pankow/utils'; import { BACKUP_FORMATS, STORAGE_PROVIDERS, REGIONS_CONTABO, REGIONS_VULTR, REGIONS_IONOS, REGIONS_OVH, REGIONS_LINODE, REGIONS_SCALEWAY, REGIONS_EXOSCALE, REGIONS_DIGITALOCEAN, REGIONS_HETZNER, REGIONS_WASABI, REGIONS_S3 } from '../constants.js'; +import ProvisionModel from '../models/ProvisionModel.js'; import SystemModel from '../models/SystemModel.js'; import { mountlike, s3like } from '../utils.js'; const provider = defineModel('provider'); const providerConfig = defineModel('providerConfig'); -const formError = defineProps({ +const props = defineProps({ formError: {}, importOnly: { type: Boolean, default: false, - } + }, + provisioning: { + type: Boolean, + default: false, + }, }); const systemModel = SystemModel.create(); +const provisionModel = ProvisionModel.create(); const storageProviders = STORAGE_PROVIDERS.concat([ { name: 'No-op (Only for testing)', value: 'noop' } @@ -65,8 +71,13 @@ function onGcsKeyChange(event) { gcsKeyFileName.value = event.target.files[0].name; } + async function getBlockDevices() { - const [error, result] = await systemModel.blockDevices(); + let error, result; + + if (props.provisioning) [error, result] = await provisionModel.blockDevices(); + else [error, result] = await systemModel.blockDevices(); + if (error) return console.error(error); // amend label for UI @@ -87,6 +98,11 @@ async function getBlockDevices() { } async function getMemory() { + if (props.provisioning) { + maxMemoryLimit.value = 4 * 1024 * 1024 * 1024; + return; + } + const [error, result] = await systemModel.memory(); if (error) return console.error(error); diff --git a/dashboard/src/models/ProvisionModel.js b/dashboard/src/models/ProvisionModel.js index 4239e6709..9f0ff12da 100644 --- a/dashboard/src/models/ProvisionModel.js +++ b/dashboard/src/models/ProvisionModel.js @@ -25,7 +25,7 @@ function create() { } if (error || result.status !== 200) return [error || result]; - return [null, result.body]; + return [null, result.body.devices]; }, async createAdmin(data) { let error, result; diff --git a/dashboard/src/views/RestoreView.vue b/dashboard/src/views/RestoreView.vue index 6f1c17118..cfdc25c87 100644 --- a/dashboard/src/views/RestoreView.vue +++ b/dashboard/src/views/RestoreView.vue @@ -2,7 +2,8 @@ import { ref, onMounted, useTemplateRef } from 'vue'; import { Spinner, Button, SingleSelect, FormGroup, TextInput, Checkbox } from 'pankow'; -import { redirectIfNeeded } from '../utils.js'; +import { REGIONS_CONTABO, REGIONS_VULTR, REGIONS_IONOS, REGIONS_OVH, REGIONS_LINODE, REGIONS_SCALEWAY, REGIONS_WASABI } from '../constants.js'; +import { redirectIfNeeded, mountlike, s3like } from '../utils.js'; import ProvisionModel from '../models/ProvisionModel.js'; import BackupProviderForm from '../components/BackupProviderForm.vue'; @@ -50,8 +51,6 @@ async function waitForRestore () { return console.error(error); } - console.lo('restore', result.restore) - if (!result.restore.active) { if (!result.adminFqdn || result.restore.errorMessage) { // restore reset or errored. start over formError.value.dnsWait = result.restore.errorMessage; @@ -76,48 +75,140 @@ async function onSubmit() { formError.value = {}; if (remotePath.value.indexOf('/') === -1) { - error.value.generic = 'Backup id must include the directory path'; - error.value.remotePath = true; - busy.value = false; - return; + error.value.generic = 'Backup id must include the directory path'; + error.value.remotePath = true; + busy.value = false; + return; } if (remotePath.value.indexOf('box') === -1) { - error.value.generic = 'Backup id must contain "box"'; - error.value.remotePath = true; - busy.value = false; - return; + error.value.generic = 'Backup id must contain "box"'; + error.value.remotePath = true; + busy.value = false; + return; } const version = remotePath.value.match(/_v(\d+.\d+.\d+)/); if (!version) { - formError.value.generic = 'Backup id is missing version information'; - formError.value.remotePath = true; - busy.value = false; - return; + formError.value.generic = 'Backup id is missing version information'; + formError.value.remotePath = true; + busy.value = false; + return; } const data = { - backupConfig: { - provider: provider.value, - format: providerConfig.value.format, - // TODO - }, - remotePath: remotePath.value.replace(/\.tar\.gz(\.enc)?$/, ''), - version: version ? version[1] : '', - ipv4Config: { - provider: ipv4Provider.value, - ip: ipv4Address.value, - ifname: ipv4Interface.value, - }, - ipv6Config: { - provider: ipv6Provider.value, - ip: ipv6Address.value, - ifname: ipv6Interface.value, - }, - skipDnsSetup: skipDnsSetup.value, + backupConfig: { + provider: provider.value, + format: providerConfig.value.format, + }, + remotePath: remotePath.value.replace(/\.tar\.gz(\.enc)?$/, ''), + version: version ? version[1] : '', + ipv4Config: { + provider: ipv4Provider.value, + ip: ipv4Address.value, + ifname: ipv4Interface.value, + }, + ipv6Config: { + provider: ipv6Provider.value, + ip: ipv6Address.value, + ifname: ipv6Interface.value, + }, + skipDnsSetup: skipDnsSetup.value, }; + if (providerConfig.value.encryptionPassword) { + data.backupConfig.encryptedFilenames = providerConfig.value.encryptedFilenames; + data.backupConfig.password = providerConfig.value.encryptionPassword; + } + + if (s3like(provider.value)) { + data.backupConfig.endpoint = providerConfig.value.endpoint; + data.backupConfig.prefix = providerConfig.value.prefix; + data.backupConfig.bucket = providerConfig.value.bucket; + data.backupConfig.accessKeyId = providerConfig.value.accessKeyId; + data.backupConfig.secretAccessKey = providerConfig.value.secretAccessKey; + + if (provider.value === 's3') { + data.backupConfig.region = providerConfig.value.region || undefined; + delete data.endpoint; + } else if (provider.value === 'minio' || provider.value === 's3-v4-compat') { + data.backupConfig.region = providerConfig.value.region || 'us-east-1'; + data.backupConfig.acceptSelfSignedCerts = providerConfig.value.acceptSelfSignedCerts; + data.backupConfig.s3ForcePathStyle = true; + } else if (provider.value === 'exoscale-sos') { + data.backupConfig.region = 'us-east-1'; + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'wasabi') { + data.backupConfig.region = REGIONS_WASABI.find(function (x) { return x.value === data.backupConfig.endpoint; }).region; + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'scaleway-objectstorage') { + data.backupConfig.region = REGIONS_SCALEWAY.find(function (x) { return x.value === data.backupConfig.endpoint; }).region; + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'linode-objectstorage') { + data.backupConfig.region = REGIONS_LINODE.find(function (x) { return x.value === data.backupConfig.endpoint; }).region; + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'ovh-objectstorage') { + data.backupConfig.region = REGIONS_OVH.find(function (x) { return x.value === data.backupConfig.endpoint; }).region; + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'ionos-objectstorage') { + data.backupConfig.region = REGIONS_IONOS.find(function (x) { return x.value === data.backupConfig.endpoint; }).region; + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'vultr-objectstorage') { + data.backupConfig.region = REGIONS_VULTR.find(function (x) { return x.value === data.backupConfig.endpoint; }).region; + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'contabo-objectstorage') { + data.backupConfig.region = REGIONS_CONTABO.find(function (x) { return x.value === data.backupConfig.endpoint; }).region; + data.backupConfig.signatureVersion = 'v4'; + data.backupConfig.s3ForcePathStyle = true; + } else if (provider.value === 'upcloud-objectstorage') { // the UI sets region and endpoint + const m = /^.*\.(.*)\.upcloudobjects.com$/.exec(data.backupConfig.endpoint); + data.backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid + data.backupConfig.signatureVersion = 'v4'; + } else if (provider.value === 'digitalocean-spaces') { + data.backupConfig.region = 'us-east-1'; + } else if (provider.value === 'hetzner-objectstorage') { + data.backupConfig.region = 'us-east-1'; + data.backupConfig.signatureVersion = 'v4'; + } + } else if (mountlike(provider.value)) { + data.backupConfig.prefix = providerConfig.value.prefix; + data.backupConfig.noHardlinks = !providerConfig.value.useHardlinks; + data.backupConfig.mountOptions = {}; + + if (provider.value === 'cifs' || provider.value === 'sshfs' || provider.value === 'nfs') { + data.backupConfig.mountOptions.host = providerConfig.value.mountOptionHost; + data.backupConfig.mountOptions.remoteDir = providerConfig.value.mountOptionRemoteDir; + + if (provider.value === 'cifs') { + data.backupConfig.mountOptions.username = providerConfig.value.mountOptionUsername; + data.backupConfig.mountOptions.password = providerConfig.value.mountOptionPassword; + data.backupConfig.mountOptions.seal = !!providerConfig.value.mountOptionSeal; + data.backupConfig.preserveAttributes = !!providerConfig.value.preserveAttributes; + } else if (provider.value === 'sshfs') { + data.backupConfig.mountOptions.user = providerConfig.value.mountOptionUser; + data.backupConfig.mountOptions.port = parseInt(providerConfig.value.mountOptionPort); + data.backupConfig.mountOptions.privateKey = providerConfig.value.mountOptionPrivateKey; + data.backupConfig.preserveAttributes = true; + } + } else if (provider.value === 'ext4' || provider.value === 'xfs' || provider.value === 'disk') { + data.backupConfig.mountOptions.diskPath = providerConfig.value.mountOptionDiskPath; + data.backupConfig.preserveAttributes = true; + } else if (provider.value === 'mountpoint') { + data.backupConfig.mountPoint = providerConfig.value.mountPoint; + data.backupConfig.chown = !!providerConfig.value.chown; + data.backupConfig.preserveAttributes = !!providerConfig.value.preserveAttributes; + } + } else if (provider.value === 'filesystem') { + data.backupConfig.backupFolder = providerConfig.value.backupFolder; + data.backupConfig.noHardlinks = !providerConfig.value.useHardlinks; + data.backupConfig.preserveAttributes = true; + } else if (provider.value === 'gcs') { + data.backupConfig.bucket = providerConfig.value.bucket; + data.backupConfig.prefix = providerConfig.value.prefix; + data.backupConfig.projectId = providerConfig.value.projectId; + data.backupConfig.credentials = providerConfig.value.credentials; + } + const [error] = await provisionModel.restore(data); if (error) { if (error.status === 424) { @@ -185,11 +276,17 @@ function onUploadBackupConfig() { } onMounted(async () => { - const [error, result] = await provisionModel.status(); + let [error, result] = await provisionModel.status(); if (error) return console.error(error); if (redirectIfNeeded(result, 'restore')) return; // redirected to some other view... + [error, result] = await provisionModel.detectIp(); + if (error) return console.error(error); + + ipv4Provider.value = result.ipv4 ? 'generic' : 'noop'; + ipv6Provider.value = result.ipv6 ? 'generic' : 'noop'; + ready.value = true; }); @@ -231,7 +328,7 @@ onMounted(async () => { - +