Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion plist
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/ServiceController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/SnapshotsController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/TunablesController.php
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/forms/backup_local.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/forms/backup_remote.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/forms/hasyncSettings.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/forms/snapshot.xml
/usr/local/opnsense/mvc/app/controllers/OPNsense/Core/forms/tunable.xml
Expand Down Expand Up @@ -678,6 +680,8 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Core/ACL.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Backup.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Backup.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Core/FieldTypes/TunableField.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Firmware.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Firmware.xml
Expand All @@ -688,6 +692,7 @@
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/M1_0_0.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/M1_0_1.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/M1_0_2.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/MHA1_0_0.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/MHA1_0_1.php
/usr/local/opnsense/mvc/app/models/OPNsense/Core/Migrations/TUN1_0_2.php
Expand Down Expand Up @@ -937,6 +942,7 @@
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/clients.volt
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/index.volt
/usr/local/opnsense/mvc/app/views/OPNsense/CaptivePortal/vouchers.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/backup.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/backup_history.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/dashboard.volt
/usr/local/opnsense/mvc/app/views/OPNsense/Core/defaults.volt
Expand Down Expand Up @@ -1385,6 +1391,8 @@
/usr/local/opnsense/scripts/syslog/queryLog.py
/usr/local/opnsense/scripts/syslog/streamLog.py
/usr/local/opnsense/scripts/system/activity.py
/usr/local/opnsense/scripts/system/backup_download.php
/usr/local/opnsense/scripts/system/backup_restore.php
/usr/local/opnsense/scripts/system/bectl.py
/usr/local/opnsense/scripts/system/cert_fetch_local.py
/usr/local/opnsense/scripts/system/certctl.py
Expand Down Expand Up @@ -2522,7 +2530,6 @@
/usr/local/www/crash_reporter.php
/usr/local/www/csrf.inc
/usr/local/www/diag_authentication.php
/usr/local/www/diag_backup.php
/usr/local/www/fbegin.inc
/usr/local/www/firewall_nat_out.php
/usr/local/www/firewall_nat_out_edit.php
Expand Down
12 changes: 11 additions & 1 deletion src/etc/inc/plugins.inc.d/core.inc
Original file line number Diff line number Diff line change
Expand Up @@ -281,9 +281,19 @@ function core_cron()
$jobs[]['autocron'] = ['/usr/local/etc/rc.syshook.d/backup/20-netflow', '0', '*/' . $config['system']['netflowbackup']];
}

$cron_minute = 0;
$cron_hour = 2;

$push_time = $config['system']['backup']['settings']['pushtime'] ?? '';

if (!empty($push_time) && preg_match('/^([0-9]{1,2}):([0-9]{1,2})$/', $push_time, $matches)) {
Comment thread
sopex marked this conversation as resolved.
Outdated
$cron_hour = (int)$matches[1];
$cron_minute = (int)$matches[2];
}

foreach ((new OPNsense\Backup\BackupFactory())->listProviders() as $classname => $provider) {
if ($provider['handle']->isEnabled()) {
$jobs[]['autocron'] = ['/usr/local/sbin/configctl -d system remote backup 3600', 0, 1];
$jobs[]['autocron'] = ['/usr/local/sbin/configctl -d system remote backup 30', $cron_minute, $cron_hour, '*', '*', '*'];
Comment thread
sopex marked this conversation as resolved.
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

/*
* Copyright (C) 2026 Konstantinos Spartalis <cspartalis@potatonetworks.com>
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
Expand Down Expand Up @@ -29,10 +30,12 @@
namespace OPNsense\Core\Api;

use OPNsense\Base\ApiControllerBase;
use OPNsense\Base\UserException;
use OPNsense\Core\ACL;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Core\Shell;
use OPNsense\Backup\Local;

/**
* Class BackupController
Expand Down Expand Up @@ -213,4 +216,211 @@ public function downloadAction($host, $backup = null)
}
}
}

public function getSettingsAction()
{
$mdlBackup = new \OPNsense\Core\Backup();
$nodes = $mdlBackup->getNodes();
return [
'backup' => [
'pushtime' => $nodes['pushtime'],
'backupcount' => $nodes['backupcount'],
]
];
}

public function setSettingsAction()
{
$result = ['status' => 'failed'];
if ($this->request->isPost()) {
$post = $this->request->getPost('backup');
$mdlBackup = new \OPNsense\Core\Backup();

if (isset($post['pushtime'])) {
$mdlBackup->pushtime = trim($post['pushtime']);
}
if (isset($post['backupcount'])) {
$mdlBackup->backupcount = trim($post['backupcount']) === '' ? null : trim($post['backupcount']);
}

$valMsgs = $mdlBackup->performValidation();
if (count($valMsgs) > 0) {
$validations = [];
foreach ($valMsgs as $msg) {
$field = $msg->getField();
if ($field === 'pushtime') {
$validations['backup.pushtime'] = $msg->getMessage();
} else {
$validations['backup.' . $field] = $msg->getMessage();
}
}
return ['status' => 'failed', 'validations' => $validations];
}

$mdlBackup->serializeToConfig();
Config::getInstance()->save('Changed backup settings');

// CRON restart
if (isset($post['pushtime'])) {
$backend = new \OPNsense\Core\Backend();
$backend->configdRun('template reload OPNsense/Cron');
$backend->configdRun('cron restart');
}

$result = ['status' => 'success'];
}
return $result;
}

public function downloadConfigAction()
{
if ($this->request->isPost()) {
$config = Config::getInstance()->object();
$hostname = "OPNsense";
if (isset($config->system->hostname)) {
$hostname = (string)$config->system->hostname . "." . (string)$config->system->domain;
}
$name = "config-" . $hostname . "-" . date("YmdHis") . ".xml";
$tmpfile = tempnam(sys_get_temp_dir(), 'opn_bck_');
$rrd_arg = empty($this->request->getPost('donotbackuprrd')) ? "rrd" : "norrd";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't know this but we were talking about deprecating this part of the backup functionality for a while now to ease the MVC migration while at it.

Copy link
Copy Markdown
Member Author

@sopex sopex Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like rrd :(
Anyway, I like your top secret information. Let me know if there is anything else.
I would also appreciate it if you tried to break the new code :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just the backups into the config.xml really

unfortunately schedules are quite tight at least until after next week

Copy link
Copy Markdown
Member Author

@sopex sopex Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just the backups into the config.xml really

Yes, I understood that but still :)

unfortunately schedules are quite tight at least until after next week

Of course, take your time. In any case, this is in working condition and probably fine by MVC standards. Surely there are improvements to be made, but I will make a small commit today and let it sit fot a bit. Let's see if there is some kind of major feedback that requires rebuilding it from scratch, and if not, I will focus on the minor stuff.

$backend = new Backend();
$response = json_decode(trim($backend->configdpRun('system config export', [$tmpfile, $rrd_arg])), true);

if ($response !== null && $response['status'] === 'success' && file_exists($tmpfile)) {
$data = file_get_contents($tmpfile);
@unlink($tmpfile);

if (!empty($this->request->getPost('encrypt'))) {
$password = $this->request->getPost('encrypt_password');
$crypter = new Local();
$data = $crypter->encrypt($data, $password);
}

$size = strlen($data);
$this->response->setContentType('application/octet-stream');
$this->response->setRawHeader("Content-Disposition: attachment; filename={$name}");
$this->response->setRawHeader("Content-Length: $size");
$this->response->setRawHeader("Pragma: private");
$this->response->setRawHeader("Cache-Control: private, must-revalidate");
$this->response->setContent($data);
return null;
} else if ($response !== null && isset($response['message'])) {
@unlink($tmpfile);
return $response;
}
}
return ['status' => 'failed'];
}

public function restoreAction()
{
if ($this->request->isPost() && isset($_FILES['conffile']) && is_uploaded_file($_FILES['conffile']['tmp_name'])) {
if ((new ACL())->hasPrivilege($this->getUserName(), 'user-config-readonly')) {
return ['status' => 'failed', 'message' => gettext('You do not have sufficient privileges to restore the configuration.')];
}

$data = file_get_contents($_FILES['conffile']['tmp_name']);

if (empty($data)) {
return ['status' => 'failed', 'message' => sprintf(gettext("Warning, could not read file %s"), $_FILES['conffile']['name'])];
}

if (!empty($this->request->getPost('decrypt'))) {
$password = $this->request->getPost('decrypt_password');
$crypter = new Local();
$data = $crypter->decrypt($data, $password);
if (empty($data)) {
return ['status' => 'failed', 'message' => gettext('The uploaded file could not be decrypted.')];
}
}

$post = $this->request->getPost();
$restoreareas = !empty($post['restorearea']) ? $post['restorearea'] : [];
$do_reboot = !empty($post['rebootafterrestore']);

$tmpfile = tempnam(sys_get_temp_dir(), 'opn_bck_');
file_put_contents($tmpfile, $data);

$params = [
'conffile' => $tmpfile,
'restorearea' => $restoreareas,
'rebootafterrestore' => $do_reboot,
'keepconsole' => !empty($post['keepconsole']),
'flush_history' => !empty($post['flush_history'])
];

$paramfile = tempnam(sys_get_temp_dir(), 'opn_bck_par_');
file_put_contents($paramfile, json_encode($params));

$backend = new Backend();
$response = json_decode(trim($backend->configdpRun('system config restore', [$paramfile])), true);

@unlink($tmpfile);
@unlink($paramfile);

if ($response !== null && $response['status'] === 'success') {
if (!empty($response['reboot'])) {
$backend->configdRun('system reboot', true);
}
return $response;
}

return $response ?? ['status' => 'failed', 'message' => gettext("The configuration could not be restored.")];
}
return ['status' => 'failed', 'message' => 'No files uploaded'];
}

public function setupProviderAction($providerName)
{
if ($this->request->isPost()) {
$backupFactory = new \OPNsense\Backup\BackupFactory();
$provider = $backupFactory->getProvider($providerName);
if (!$provider) {
return ['status' => 'failed', 'message' => 'Provider not found.'];
}

$providerSet = array();
$post = $this->request->getPost();

foreach ($provider['handle']->getConfigurationFields() as $field) {
if ($field['type'] == 'file') {
if (isset($_FILES[$field['name']]) && is_uploaded_file($_FILES[$field['name']]['tmp_name'])) {
$providerSet[$field['name']] = file_get_contents($_FILES[$field['name']]['tmp_name']);
} else {
$providerSet[$field['name']] = null;
}
} else {
$providerSet[$field['name']] = isset($post[$field['name']]) ? $post[$field['name']] : '';
}
}

$input_errors = $provider['handle']->setConfiguration($providerSet);

if (count($input_errors) == 0) {
$backend = new Backend();
$backend->configdRun('template reload OPNsense/Cron');
$backend->configdRun('cron restart');

if ($provider['handle']->isEnabled()) {
try {
$filesInBackup = $provider['handle']->backup();
} catch (\Exception $e) {
return ['status' => 'failed', 'message' => $e->getMessage()];
}
if (count($filesInBackup) == 0) {
return ['status' => 'success', 'message' => gettext('Saved settings, but remote backup returned no files.')];
} else {
$msg = gettext("Backup successful. Current file list: ") . implode(", ", $filesInBackup);
return ['status' => 'success', 'message' => $msg];
}
}

return ['status' => 'success', 'message' => gettext("Settings configured.")];
} else {
return ['status' => 'failed', 'message' => implode(", ", $input_errors)];
}
}
return ['status' => 'failed', 'message' => 'Invalid request'];
}
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

/*
* Copyright (C) 2026 Konstantinos Spartalis <cspartalis@potatonetworks.com>
Comment thread
sopex marked this conversation as resolved.
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
Expand Down Expand Up @@ -34,6 +35,40 @@
*/
class BackupController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$backupFactory = new \OPNsense\Backup\BackupFactory();
$this->view->providers = $backupFactory->listProviders();

$this->view->backupLocalForm = $this->getForm("backup_local");
$this->view->backupRemoteForm = $this->getForm("backup_remote");

$this->view->backupSize = \OPNsense\Core\Config::getInstance()->getBackupSize();

$areas = [
'bridges' => gettext('Bridge Devices'),
'gifs' => gettext('GIF Devices'),
'interfaces' => gettext('Interfaces'),
'laggs' => gettext('LAGG Devices'),
'ppps' => gettext('Point-to-Point Devices'),
'rrddata' => gettext('RRD Data'),
'vlans' => gettext('VLAN Devices'),
'wireless' => gettext('Wireless Devices'),
];
Comment on lines +48 to +57
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have to discuss where we want to anchor this

$xmlrpc_options = @json_decode((new \OPNsense\Core\Backend())->configdRun('system xmlrpc options'), true);
if (is_array($xmlrpc_options)) {
foreach ($xmlrpc_options as $area) {
if (!empty($area['section'])) {
$areas[$area['section']] = $area['description'];
}
}
}
natcasesort($areas);
$this->view->areas = $areas;

$this->view->pick('OPNsense/Core/backup');
}

public function historyAction($selected_host = null)
{
$this->view->selected_host = $selected_host;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<form>
<field>
<id>backup.backupcount</id>
<label>Backup Count</label>
<type>text</type>
<help>Enter the number of older configurations to keep in the local backup cache.</help>
</field>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<form>
<field>
<id>backup.pushtime</id>
<label>Push Time</label>
<type>text</type>
<help>This determines the backup time used for the main backup of the day. Additional backups can be set up on the cron page.</help>
</field>
</form>
Loading