diff --git a/README.md b/README.md index 9087ce5..699b7b1 100644 --- a/README.md +++ b/README.md @@ -10,31 +10,31 @@ opn-cli - the OPNsense CLI written in python. - [opn-cli](#opn-cli) - * [Install](#install) - * [Configure](#configure) - * [Usage](#usage) - * [docker usage](#docker-usage) - * [Features](#features) - + [Shell Completion](#shell-completion) - + [Output formats](#output-formats) + - [Install](#install) + - [Configure](#configure) + - [Usage](#usage) + - [docker usage](#docker-usage) + - [Features](#features) + - [Shell Completion](#shell-completion) + - [Output formats](#output-formats) - [cols](#cols) - [table](#table) - [json](#json) - - [json_filter](#json-filter) + - [json\_filter](#json_filter) - [plain](#plain) - [yaml](#yaml) - + [Code Generator](#code-generator) + - [Code Generator](#code-generator) - [API code core](#api-code-core) - [API code plugin](#api-code-plugin) - [Core command code](#core-command-code) - [Plugin command code](#plugin-command-code) - [Puppet custom resource type](#puppet-custom-resource-type) - + [Resolving of names to uuids](#resolving-of-names-to-uuids) - * [Commands](#commands) - + [Firewall](#firewall) + - [Resolving of names to uuids](#resolving-of-names-to-uuids) + - [Commands](#commands) + - [Firewall](#firewall) - [Aliases](#aliases) - [Rules](#rules) - + [Haproxy](#haproxy) + - [Haproxy](#haproxy) - [Acl](#acl) - [Action](#action) - [Backend](#backend) @@ -48,30 +48,32 @@ opn-cli - the OPNsense CLI written in python. - [Resolver](#resolver) - [Server](#server) - [User](#user) - + [Ipsec](#ipsec) + - [Ipsec](#ipsec) - [Tunnel phase1](#tunnel-phase1) - [Tunnel phase2](#tunnel-phase2) - + [Routes](#routes) + - [Routes](#routes) - [Static routes](#static-routes) - [Gateway](#gateway) - + [Nodeexporter](#nodeexporter) + - [Nodeexporter](#nodeexporter) - [Config](#config-1) - + [Syslog](#syslog) + - [Syslog](#syslog) - [Syslog destination](#syslog-destination) - [Syslog stats](#syslog-stats) - + [OpenVPN](#openvpn) - + [Plugins](#plugins) - + [Unbound](#unbound) + - [OpenVPN](#openvpn) + - [Plugins](#plugins) + - [Unbound](#unbound) - [host overrides](#host-overrides) - [host alias overrides](#host-alias-overrides) - [domain overrides](#domain-overrides) - [Examples](#examples) - + [Apibackup](#apibackup) - - [Examples](#examples) - * [Development](#development) - + [Setup development environment](#setup-development-environment) - + [Testing](#testing) - + [Contributing](#contributing) + - [Apibackup](#apibackup) + - [Examples](#examples-1) + - [Configbackup](#configbackup) + - [Examples](#examples-2) + - [Development](#development) + - [Setup development environment](#setup-development-environment) + - [Testing](#testing) + - [Contributing](#contributing) ## Install ``` @@ -1038,6 +1040,8 @@ opn-cli unbound domain create --domain rockin.com --server 192.168.56.3 ### Apibackup This feature needs the opnsense plugin os-api-backup. +This plugin was deprecated with the OPNsense 24.1 release. +So if you are running OPNsense 24.1 or higher use the [Configbackup](###configbackup) command. ``` $ opn-cli plugin install os-api-backup @@ -1059,6 +1063,28 @@ $ opn-cli apibackup backup download -p /tmp/config_backup.xml successfully saved to: /tmp/config_backup.xml ``` +### Configbackup +The plugin "os-api-backup" was discontinued in OPNsense Version 24.1, because the core API provides the same functionality. +This command provides the exact same functionality than [Apibackup](###apibackup) but uses the OPNsense Core API-Endpoint. +So if you are running a OPNsense Instance version 24.1 or higher use this command for configuration backups instead of "apibackup". + + +#### Examples + +Download a backup of the OPNsense system configuration to the current directory: + +``` +$ opn-cli configbackup backup download +successfully saved to: ./config.xml +``` + +Or specify a path and filename: + +``` +$ opn-cli configbackup backup download -p /tmp/config_backup.xml +successfully saved to: /tmp/config_backup.xml +``` + ## Development ### Setup development environment diff --git a/opnsense_cli/api/client.py b/opnsense_cli/api/client.py index 173b1af..11838cd 100644 --- a/opnsense_cli/api/client.py +++ b/opnsense_cli/api/client.py @@ -25,8 +25,12 @@ def ssl_verify_cert(self): return self._ssl_verify_cert def _process_response(self, response): + content_type = response.headers["Content-Type"] if response.status_code in HTTP_SUCCESS: - return json.loads(response.text) + if "application/json" in content_type: + return json.loads(response.text) + else: + return response.text else: raise APIException(response=response.status_code, resp_body=response.text, url=response.url) diff --git a/opnsense_cli/api/core/configbackup.py b/opnsense_cli/api/core/configbackup.py new file mode 100644 index 0000000..aea84cf --- /dev/null +++ b/opnsense_cli/api/core/configbackup.py @@ -0,0 +1,14 @@ +from opnsense_cli.api.base import ApiBase + + +class Backup(ApiBase): + MODULE = "core" + CONTROLLER = "backup" + """ + api-backup BackupController + """ + + @ApiBase._api_call + def download(self, *args): + self.method = "get" + self.command = "download" diff --git a/opnsense_cli/commands/core/configbackup/__init__.py b/opnsense_cli/commands/core/configbackup/__init__.py new file mode 100644 index 0000000..f235385 --- /dev/null +++ b/opnsense_cli/commands/core/configbackup/__init__.py @@ -0,0 +1,8 @@ +import click + + +@click.group() +def configbackup(**kwargs): + """ + Manage api-backup operations (OPNsense version >= 24.1) + """ diff --git a/opnsense_cli/commands/core/configbackup/backup.py b/opnsense_cli/commands/core/configbackup/backup.py new file mode 100644 index 0000000..a1720ac --- /dev/null +++ b/opnsense_cli/commands/core/configbackup/backup.py @@ -0,0 +1,59 @@ +import click +from opnsense_cli.formatters.cli_output import CliOutputFormatter +from opnsense_cli.callbacks.click import formatter_from_formatter_name, expand_path, available_formats +from opnsense_cli.commands.core.configbackup import configbackup +from opnsense_cli.api.client import ApiClient +from opnsense_cli.api.core.configbackup import Backup +from opnsense_cli.facades.commands.core.configbackup.backup import ApibackupBackupFacade + +pass_api_client = click.make_pass_decorator(ApiClient) +pass_apibackup_backup_svc = click.make_pass_decorator(ApibackupBackupFacade) + + +@configbackup.group() +@pass_api_client +@click.pass_context +def backup(ctx, api_client: ApiClient, **kwargs): + """ + Manage api-backup + """ + backup_api = Backup(api_client) + ctx.obj = ApibackupBackupFacade(backup_api) + + +@backup.command() +@click.option( + "-p", + "--path", + help="The target path.", + type=click.Path(dir_okay=False), + default="./config.xml", + is_eager=True, + show_default=True, + callback=expand_path, + show_envvar=True, + required=True, +) +@click.option( + "--output", + "-o", + help="Specifies the Output format.", + default="plain", + type=click.Choice(available_formats()), + callback=formatter_from_formatter_name, + show_default=True, +) +@click.option( + "--cols", + "-c", + help="Which columns should be printed? Pass empty string (-c " ") to show all columns", + default="status", + show_default=True, +) +@pass_apibackup_backup_svc +def download(apibackup_backup_svc: ApibackupBackupFacade, **kwargs): + """ + Download config.xml from OPNsense. + """ + result = apibackup_backup_svc.download_backup(kwargs["path"]) + CliOutputFormatter(result, kwargs["output"], kwargs["cols"].split(",")).echo() diff --git a/opnsense_cli/commands/plugin/apibackup/__init__.py b/opnsense_cli/commands/plugin/apibackup/__init__.py index 24df822..11d4b56 100644 --- a/opnsense_cli/commands/plugin/apibackup/__init__.py +++ b/opnsense_cli/commands/plugin/apibackup/__init__.py @@ -4,5 +4,5 @@ @click.group() def apibackup(**kwargs): """ - Manage api-backup operations + Manage api-backup operations (OPNsense version <= 23.7) """ diff --git a/opnsense_cli/facades/commands/base.py b/opnsense_cli/facades/commands/base.py index eb21912..d9c5841 100644 --- a/opnsense_cli/facades/commands/base.py +++ b/opnsense_cli/facades/commands/base.py @@ -105,6 +105,10 @@ def _write_base64_string_to_zipfile(self, path, base64_data): with open(path, "wb") as zipFile: zipFile.write(content) + def _write_xml_string_to_file(self, path, xml_content): + with open(path, "w") as xmlFile: + xmlFile.write(xml_content) + def resolve_linked_uuids(self, resolve_map, resolve_items): uuids = [item for item in resolve_items.split(",") if self.is_uuid(item)] names = [item for item in resolve_items.split(",") if not self.is_uuid(item)] diff --git a/opnsense_cli/facades/commands/core/configbackup/__init__.py b/opnsense_cli/facades/commands/core/configbackup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opnsense_cli/facades/commands/core/configbackup/backup.py b/opnsense_cli/facades/commands/core/configbackup/backup.py new file mode 100644 index 0000000..d611d65 --- /dev/null +++ b/opnsense_cli/facades/commands/core/configbackup/backup.py @@ -0,0 +1,12 @@ +from opnsense_cli.api.core.configbackup import Backup +from opnsense_cli.facades.commands.base import CommandFacade + + +class ApibackupBackupFacade(CommandFacade): + def __init__(self, backup_api: Backup): + self._backup_api = backup_api + + def download_backup(self, path): + config = self._backup_api.download("this") + self._write_xml_string_to_file(path, config) + return {"status": f"successfully saved to: {path}"} diff --git a/opnsense_cli/fixtures/tests/commands/core/configbackup/__init__.py b/opnsense_cli/fixtures/tests/commands/core/configbackup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/opnsense_cli/fixtures/tests/commands/core/configbackup/config.xml.sample b/opnsense_cli/fixtures/tests/commands/core/configbackup/config.xml.sample new file mode 100644 index 0000000..d6a0637 --- /dev/null +++ b/opnsense_cli/fixtures/tests/commands/core/configbackup/config.xml.sample @@ -0,0 +1,384 @@ + + + + opnsense + + + + vfs.read_max + default + + + + net.inet.ip.portrange.first + default + + + + net.inet.tcp.blackhole + default + + + + net.inet.udp.blackhole + default + + + + net.inet.ip.random_id + default + + + + net.inet.ip.sourceroute + default + + + + net.inet.ip.accept_sourceroute + default + + + + net.inet.icmp.log_redirect + default + + + + net.inet.tcp.drop_synfin + default + + + + net.inet6.ip6.redirect + default + + + + net.inet6.ip6.use_tempaddr + default + + + + net.inet6.ip6.prefer_tempaddr + default + + + + net.inet.tcp.syncookies + default + + + + net.inet.tcp.recvspace + default + + + + net.inet.tcp.sendspace + default + + + + net.inet.tcp.delayed_ack + default + + + + net.inet.udp.maxdgram + default + + + + net.link.bridge.pfil_onlyip + default + + + + net.link.bridge.pfil_local_phys + default + + + + net.link.bridge.pfil_member + default + + + + net.link.bridge.pfil_bridge + default + + + + net.link.tap.user_open + default + + + + kern.randompid + default + + + + hw.syscons.kbd_reboot + default + + + + net.inet.tcp.log_debug + default + + + + net.inet.icmp.icmplim + default + + + + net.inet.tcp.tso + default + + + + net.inet.udp.checksum + default + + + + kern.ipc.maxsockbuf + default + + + + vm.pmap.pti + default + + + + hw.ibrs_disable + default + + + + security.bsd.see_other_gids + default + + + + security.bsd.see_other_uids + default + + + + + net.inet.ip.redirect + default + + + + net.inet.icmp.drop_redirect + 1 + + + + net.local.dgram.maxdgram + default + + + + normal + OPNsense + localdomain + 1 + + admins + + system + 1999 + 0 + page-all + + + root + + system + admins + $2y$10$YRVoF4SgskIsrXOvOQjGieB9XqHPRra9R7d80B3BZdbY/j21TwBfS + 0 + + 2000 + 2000 + Etc/UTC + 0.opnsense.pool.ntp.org 1.opnsense.pool.ntp.org 2.opnsense.pool.ntp.org 3.opnsense.pool.ntp.org + + https + + yes + 1 + + 1 + 1 + 1 + 1 + + hadp + hadp + hadp + + monthly + + 1 + 1 + + admins + + -1 + -1 + + + + 1 + mismatch1 + + dhcp + dhcp6 + + + 1 + 1 + + + + 0 + + + 1 + mismatch0 + 192.168.1.1 + 24 + track6 + 64 + + + wan + 0 + + + + + + + 192.168.1.100 + 192.168.1.199 + + + + + 1 + + + + + public + + + + automatic + + + + + pass + inet + + lan + + lan + + + + + + + pass + inet6 + + lan + + lan + + + + + + + + + + + + ICMP + icmp + + + + + TCP + tcp + + + + + HTTP + http + + + / + + 200 + + + + HTTPS + https + + + / + + 200 + + + + SMTP + send + + + + 220 * + + + + + 0.opnsense.pool.ntp.org + + + system_information-container:00000000-col3:show,services_status-container:00000001-col4:show,gateways-container:00000002-col4:show,interface_list-container:00000003-col4:show + 2 + + diff --git a/opnsense_cli/tests/commands/core/test_configbackup.py b/opnsense_cli/tests/commands/core/test_configbackup.py new file mode 100644 index 0000000..6276abd --- /dev/null +++ b/opnsense_cli/tests/commands/core/test_configbackup.py @@ -0,0 +1,24 @@ +from unittest.mock import patch +from opnsense_cli.commands.core.configbackup.backup import backup +from opnsense_cli.tests.commands.base import CommandTestCase + + +class TestApibackupCommands(CommandTestCase): + def setUp(self): + self._setup_fakefs() + + self._api_data_fixtures_download = self._read_fixture_file("core/configbackup/config.xml.sample") + self._api_client_args_fixtures = ["api_key", "api_secret", "https://127.0.0.1/api", True, "~/.opn-cli/ca.pem", 60] + + @patch("opnsense_cli.commands.core.configbackup.backup.ApiClient.execute") + def test_download(self, api_response_mock): + result = self._opn_cli_command_result( + api_response_mock, + [ + self._api_data_fixtures_download, + ], + backup, + ["download"], + ) + + self.assertIn("successfully saved to: ./config.xml\n", result.output)