'use strict'; /* global $, angular, SECRET_PLACEHOLDER, STORAGE_PROVIDERS, BACKUP_FORMATS, redirectIfNeeded */ /* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_CONTABO, REGIONS_HETZNER */ // create main application module var app = angular.module('Application', ['pascalprecht.translate', 'ngCookies', 'angular-md5', 'ui-notification', 'ui.bootstrap']); app.controller('RestoreController', ['$scope', 'Client', function ($scope, Client) { var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {}); $scope.client = Client; $scope.busy = false; $scope.error = {}; $scope.message = ''; // progress // variables here have to match the import config logic! $scope.provider = ''; $scope.bucket = ''; $scope.prefix = ''; $scope.mountPoint = ''; $scope.accessKeyId = ''; $scope.secretAccessKey = ''; $scope.gcsKey = { keyFileName: '', content: '' }; $scope.region = ''; $scope.endpoint = ''; $scope.backupFolder = ''; $scope.remotePath = ''; $scope.instanceId = ''; $scope.acceptSelfSignedCerts = false; $scope.format = 'tgz'; $scope.advancedVisible = false; $scope.password = ''; $scope.encryptedFilenames = true; $scope.encrypted = false; // only used if a backup config contains that flag $scope.setupToken = ''; $scope.skipDnsSetup = false; $scope.disk = null; $scope.blockDevices = []; $scope.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', user: '', seal: true, port: 22, privateKey: '' }; $scope.$watch('disk', function (newValue) { if (!newValue) return; $scope.mountOptions.diskPath = '/dev/disk/by-uuid/' + newValue.uuid; }); $scope.ipv4Config = { provider: 'generic', ip: '', ifname: '' }; $scope.ipv6Config = { provider: 'generic', ip: '', ifname: '' }; $scope.ipProviders = [ { name: 'Disabled', value: 'noop' }, { name: 'Public IP', value: 'generic' }, { name: 'Static IP Address', value: 'fixed' }, { name: 'Network Interface', value: 'network-interface' } ]; $scope.s3Regions = REGIONS_S3; $scope.wasabiRegions = REGIONS_WASABI; $scope.doSpacesRegions = REGIONS_DIGITALOCEAN; $scope.hetznerRegions = REGIONS_HETZNER; $scope.exoscaleSosRegions = REGIONS_EXOSCALE; $scope.scalewayRegions = REGIONS_SCALEWAY; $scope.linodeRegions = REGIONS_LINODE; $scope.ovhRegions = REGIONS_OVH; $scope.ionosRegions = REGIONS_IONOS; $scope.upcloudRegions = REGIONS_UPCLOUD; $scope.vultrRegions = REGIONS_VULTR; $scope.contaboRegions = REGIONS_CONTABO; $scope.storageProviders = STORAGE_PROVIDERS; $scope.formats = BACKUP_FORMATS; $scope.s3like = function (provider) { return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat' || provider === 'exoscale-sos' || provider === 'digitalocean-spaces' || provider === 'wasabi' || provider === 'scaleway-objectstorage' || provider === 'hetzner-objectstorage' || provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'backblaze-b2' || provider === 'cloudflare-r2' || provider === 'ionos-objectstorage' || provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2' || provider === 'contabo-objectstorage'; }; $scope.mountlike = function (provider) { return provider === 'disk' || provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs'; }; $scope.restore = function () { $scope.error = {}; $scope.busy = true; var backupConfig = { provider: $scope.provider, format: $scope.format, }; if ($scope.password) { backupConfig.password = $scope.password; backupConfig.encryptedFilenames = $scope.encryptedFilenames; } // only set provider specific fields, this will clear them in the db if ($scope.s3like(backupConfig.provider)) { backupConfig.bucket = $scope.bucket; backupConfig.prefix = $scope.prefix; backupConfig.accessKeyId = $scope.accessKeyId; backupConfig.secretAccessKey = $scope.secretAccessKey; if ($scope.endpoint) backupConfig.endpoint = $scope.endpoint; if (backupConfig.provider === 's3') { if ($scope.region) backupConfig.region = $scope.region; delete backupConfig.endpoint; } else if (backupConfig.provider === 'minio' || backupConfig.provider === 's3-v4-compat') { backupConfig.region = backupConfig.region || 'us-east-1'; backupConfig.acceptSelfSignedCerts = $scope.acceptSelfSignedCerts; backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI } else if (backupConfig.provider === 'exoscale-sos') { backupConfig.region = 'us-east-1'; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'wasabi') { backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'scaleway-objectstorage') { backupConfig.region = $scope.scalewayRegions.find(function (x) { return x.value === $scope.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'linode-objectstorage') { backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'ovh-objectstorage') { backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'ionos-objectstorage') { backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'vultr-objectstorage') { backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.endpoint; }).region; backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'contabo-objectstorage') { backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.endpoint; }).region; backupConfig.signatureVersion = 'v4'; backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets) } else if (backupConfig.provider === 'upcloud-objectstorage') { var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint); backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid backupConfig.signatureVersion = 'v4'; } else if (backupConfig.provider === 'digitalocean-spaces') { backupConfig.region = 'us-east-1'; } else if (backupConfig.provider === 'hetzner-objectstorage') { backupConfig.region = 'us-east-1'; backupConfig.signatureVersion = 'v4'; } } else if (backupConfig.provider === 'gcs') { backupConfig.bucket = $scope.bucket; backupConfig.prefix = $scope.prefix; try { var serviceAccountKey = JSON.parse($scope.gcsKey.content); backupConfig.projectId = serviceAccountKey.project_id; backupConfig.credentials = { client_email: serviceAccountKey.client_email, private_key: serviceAccountKey.private_key }; if (!backupConfig.projectId || !backupConfig.credentials || !backupConfig.credentials.client_email || !backupConfig.credentials.private_key) { throw 'fields_missing'; } } catch (e) { $scope.error.generic = 'Cannot parse Google Service Account Key: ' + e.message; $scope.error.gcsKeyInput = true; $scope.busy = false; return; } } else if ($scope.mountlike(backupConfig.provider)) { backupConfig.prefix = $scope.prefix; backupConfig.mountOptions = {}; if (backupConfig.provider === 'cifs' || backupConfig.provider === 'sshfs' || backupConfig.provider === 'nfs') { backupConfig.mountOptions.host = $scope.mountOptions.host; backupConfig.mountOptions.remoteDir = $scope.mountOptions.remoteDir; if (backupConfig.provider === 'cifs') { backupConfig.mountOptions.username = $scope.mountOptions.username; backupConfig.mountOptions.password = $scope.mountOptions.password; backupConfig.mountOptions.seal = $scope.mountOptions.seal; } else if (backupConfig.provider === 'sshfs') { backupConfig.mountOptions.user = $scope.mountOptions.user; backupConfig.mountOptions.port = $scope.mountOptions.port; backupConfig.mountOptions.privateKey = $scope.mountOptions.privateKey; } } else if (backupConfig.provider === 'disk' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') { backupConfig.mountOptions.diskPath = $scope.mountOptions.diskPath; } else if (backupConfig.provider === 'mountpoint') { backupConfig.mountPoint = $scope.mountPoint; } } else if (backupConfig.provider === 'filesystem') { backupConfig.backupFolder = $scope.backupFolder; } if ($scope.remotePath.indexOf('/') === -1) { $scope.error.generic = 'Backup id must include the directory path'; $scope.error.remotePath = true; $scope.busy = false; return; } if ($scope.remotePath.indexOf('box') === -1) { $scope.error.generic = 'Backup id must contain "box"'; $scope.error.remotePath = true; $scope.busy = false; return; } var version = $scope.remotePath.match(/_v(\d+.\d+.\d+)/); if (!version) { $scope.error.generic = 'Backup id is missing version information'; $scope.error.remotePath = true; $scope.busy = false; return; } var data = { backupConfig: backupConfig, remotePath: $scope.remotePath.replace(/\.tar\.gz(\.enc)?$/, ''), version: version ? version[1] : '', ipv4Config: $scope.ipv4Config, ipv6Config: $scope.ipv6Config, skipDnsSetup: $scope.skipDnsSetup, setupToken: $scope.setupToken }; Client.restore(data, function (error) { $scope.busy = false; if (error) { if (error.statusCode === 424) { $scope.error.generic = error.message; if (error.message.indexOf('AWS Access Key Id') !== -1) { $scope.error.accessKeyId = true; $scope.accessKeyId = ''; $scope.configureBackupForm.accessKeyId.$setPristine(); $('#inputConfigureBackupAccessKeyId').focus(); } else if (error.message.indexOf('not match the signature') !== -1 ) { $scope.error.secretAccessKey = true; $scope.secretAccessKey = ''; $scope.configureBackupForm.secretAccessKey.$setPristine(); $('#inputConfigureBackupSecretAccessKey').focus(); } else if (error.message.toLowerCase() === 'access denied') { $scope.error.bucket = true; $scope.bucket = ''; $scope.configureBackupForm.bucket.$setPristine(); $('#inputConfigureBackupBucket').focus(); } else if (error.message.indexOf('ECONNREFUSED') !== -1) { $scope.error.generic = 'Unknown region'; $scope.error.region = true; $scope.configureBackupForm.region.$setPristine(); $('#inputConfigureBackupDORegion').focus(); } else if (error.message.toLowerCase() === 'wrong region') { $scope.error.generic = 'Wrong S3 Region'; $scope.error.region = true; $scope.configureBackupForm.region.$setPristine(); $('#inputConfigureBackupS3Region').focus(); } else { $('#inputConfigureBackupBucket').focus(); } } else { $scope.error.generic = error.message; } return; } waitForRestore(); }); }; function waitForRestore() { $scope.busy = true; Client.getProvisionStatus(function (error, status) { if (!error && !status.restore.active) { // restore finished if (status.restore.errorMessage) { $scope.busy = false; $scope.error.generic = status.restore.errorMessage; } else { // restore worked, redirect to admin page window.location.href = 'https://' + status.adminFqdn + '/'; } return; } if (!error) $scope.message = status.restore.message; setTimeout(waitForRestore, 5000); }); } function readFileLocally(obj, file, fileName) { return function (event) { $scope.$apply(function () { obj[file] = null; obj[fileName] = event.target.files[0].name; var reader = new FileReader(); reader.onload = function (result) { if (!result.target || !result.target.result) return console.error('Unable to read local file'); obj[file] = result.target.result; }; reader.readAsText(event.target.files[0]); }); }; } document.getElementById('gcsKeyFileInput').onchange = readFileLocally($scope.gcsKey, 'content', 'keyFileName'); document.getElementById('backupConfigFileInput').onchange = function (event) { var reader = new FileReader(); reader.onload = function (result) { if (!result.target || !result.target.result) return console.error('Unable to read backup config'); var backupConfig; try { backupConfig = JSON.parse(result.target.result); } catch (e) { console.error('Unable to parse backup config'); return; } $scope.$apply(function () { // we assume property names match here, this does not yet work for gcs keys Object.keys(backupConfig).forEach(function (k) { if (k in $scope) $scope[k] = backupConfig[k]; }); // this allows the config to potentially have a raw password (though our UI sets it to placeholder) if ($scope.mountOptions.password === SECRET_PLACEHOLDER) $scope.mountOptions.password = ''; }); }; reader.readAsText(event.target.files[0]); }; function init() { Client.getProvisionStatus(function (error, status) { if (error) return Client.initError(error, init); if (redirectIfNeeded(status, 'restore')) return; // redirected to some other view... if (status.restore.active) return waitForRestore(); if (status.restore.errorMessage) $scope.error.generic = status.restore.errorMessage; // any previous restore error Client.getProvisionBlockDevices(function (error, result) { if (error) { console.error('Failed to list blockdevices:', error); } else { // only offer non /, /boot or /home disks result = result.filter(function (d) { return d.mountpoint !== '/' && d.mountpoint !== '/home' && d.mountpoint !== '/boot'; }); // only offer xfs and ext4 disks result = result.filter(function (d) { return d.type === 'xfs' || d.type === 'ext4'; }); // amend label for UI result.forEach(function (d) { d.label = d.path; }); } $scope.blockDevices = result; $scope.instanceId = search.instanceId; $scope.setupToken = search.setupToken; Client.detectIp(function (error, ip) { // this is never supposed to error if (!error) $scope.ipv4Config.provider = ip.ipv4 ? 'generic' : 'noop'; if (!error) $scope.ipv6Config.provider = ip.ipv6 ? 'generic' : 'noop'; $scope.initialized = true; }); }); }); } init(); }]);