├── .coveragerc ├── .github ├── FUNDING.yml └── workflows │ ├── build.yaml │ ├── docker.yml │ └── python-publish.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── Vagrantfile ├── acceptance_tests ├── __init__.py ├── base.py └── tests │ ├── __init__.py │ ├── core │ ├── __init__.py │ └── test_plugin.py │ └── plugin │ └── __init__.py ├── acceptance_tests_requirements.txt ├── docker ├── .gitkeep └── .opn-cli │ ├── ca.pem │ └── conf.yaml ├── opnsense_cli ├── __init__.py ├── api │ ├── __init__.py │ ├── base.py │ ├── client.py │ ├── core │ │ ├── __init__.py │ │ ├── configbackup.py │ │ ├── firmware.py │ │ ├── ipsec.py │ │ ├── routes.py │ │ ├── syslog.py │ │ └── unbound.py │ ├── exceptions.py │ ├── plugin │ │ ├── __init__.py │ │ ├── apibackup.py │ │ ├── firewall.py │ │ ├── haproxy.py │ │ ├── nodeexporter.py │ │ └── openvpn.py │ └── tests │ │ ├── __init__.py │ │ └── test_client.py ├── ca.pem ├── cli.py ├── click_addons │ ├── __init__.py │ ├── callbacks.py │ ├── command_autoloader.py │ ├── command_tree.py │ ├── param_type_csv.py │ ├── param_type_int_or_empty.py │ └── tests │ │ ├── __init__.py │ │ ├── test_callbacks.py │ │ ├── test_command_autoloader.py │ │ ├── test_param_type_csv.py │ │ └── test_param_type_int_or_empty.py ├── code_generators │ ├── __init__.py │ ├── base.py │ ├── opn_cli │ │ ├── __init__.py │ │ ├── base.py │ │ ├── command │ │ │ ├── __init__.py │ │ │ ├── codegenerator.py │ │ │ ├── template.py.j2 │ │ │ └── template_vars.py │ │ ├── factories.py │ │ ├── factory_types.py │ │ ├── service │ │ │ ├── __init__.py │ │ │ ├── codegenerator.py │ │ │ ├── template.py.j2 │ │ │ └── template_vars.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── fixtures │ │ │ │ ├── __init__.py │ │ │ │ └── model.xml │ │ │ └── test_click_option_factory.py │ │ └── unit_test │ │ │ ├── __init__.py │ │ │ ├── codegenerator.py │ │ │ ├── template.py.j2 │ │ │ └── template_vars.py │ ├── opnsense_api │ │ ├── __init__.py │ │ ├── codegenerator.py │ │ ├── template.py.j2 │ │ └── template_vars.py │ └── puppet_code │ │ ├── __init__.py │ │ ├── acceptance_test │ │ ├── __init__.py │ │ ├── codegenerator.py │ │ ├── template.rb.j2 │ │ └── template_vars.py │ │ ├── base.py │ │ ├── factories.py │ │ ├── factory_types.py │ │ ├── provider │ │ ├── __init__.py │ │ ├── codegenerator.py │ │ ├── template.rb.j2 │ │ └── template_vars.py │ │ ├── provider_unit_test │ │ ├── __init__.py │ │ ├── codegenerator.py │ │ ├── template.rb.j2 │ │ └── template_vars.py │ │ ├── tests │ │ ├── __init__.py │ │ └── test_puppet_code_fragment_factory.py │ │ ├── type │ │ ├── __init__.py │ │ ├── codegenerator.py │ │ ├── template.rb.j2 │ │ └── template_vars.py │ │ └── type_unit_test │ │ ├── __init__.py │ │ ├── codegenerator.py │ │ ├── template.rb.j2 │ │ └── template_vars.py ├── commands │ ├── __init__.py │ ├── completion │ │ ├── __init__.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_completion.py │ ├── core │ │ ├── __init__.py │ │ ├── configbackup │ │ │ ├── __init__.py │ │ │ ├── backup.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ └── backup_service.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── fixtures │ │ │ │ ├── __init__.py │ │ │ │ └── config.xml.sample │ │ │ │ └── test_configbackup.py │ │ ├── firewall │ │ │ ├── __init__.py │ │ │ ├── alias.py │ │ │ ├── rule.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ ├── firewall_alias_service.py │ │ │ │ └── firewall_rule_service.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── test_firewall.py │ │ │ │ ├── test_firewall_alias.py │ │ │ │ └── test_firewall_rule.py │ │ ├── ipsec │ │ │ ├── __init__.py │ │ │ ├── phase1.py │ │ │ ├── phase2.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ └── ipsec_tunnel_service.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ └── test_ipsec_tunnel.py │ │ ├── openvpn │ │ │ ├── __init__.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ └── test_openvpn_export.py │ │ ├── plugin │ │ │ ├── __init__.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ └── plugin.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ └── test_plugin.py │ │ ├── route │ │ │ ├── __init__.py │ │ │ ├── gateway.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ ├── route_gateway_service.py │ │ │ │ └── route_static_service.py │ │ │ ├── static.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── fixtures │ │ │ │ ├── __init__.py │ │ │ │ └── model_data.json │ │ │ │ ├── test_routes_gateway.py │ │ │ │ └── test_routes_static.py │ │ ├── syslog │ │ │ ├── __init__.py │ │ │ ├── destination.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ ├── syslog_destination_service.py │ │ │ │ └── syslog_stats_service.py │ │ │ ├── stats.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── fixtures │ │ │ │ ├── __init__.py │ │ │ │ └── model_data.json │ │ │ │ ├── test_syslog_destination.py │ │ │ │ └── test_syslog_stats.py │ │ └── unbound │ │ │ ├── __init__.py │ │ │ ├── alias.py │ │ │ ├── domain.py │ │ │ ├── host.py │ │ │ ├── services │ │ │ ├── __init__.py │ │ │ ├── unbound_alias_service.py │ │ │ ├── unbound_domain_service.py │ │ │ └── unbound_host_service.py │ │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── fixtures │ │ │ ├── __init__.py │ │ │ └── model_data.json │ │ │ ├── test_unbound_alias.py │ │ │ ├── test_unbound_domain.py │ │ │ └── test_unbound_host.py │ ├── exceptions.py │ ├── new │ │ ├── __init__.py │ │ ├── api.py │ │ ├── command.py │ │ ├── puppet.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── fixtures │ │ │ ├── __init__.py │ │ │ ├── api │ │ │ │ ├── __init__.py │ │ │ │ ├── core.html │ │ │ │ ├── core_list.html │ │ │ │ ├── plugin.html │ │ │ │ └── plugin_list.html │ │ │ └── opn_cli │ │ │ │ ├── __init__.py │ │ │ │ ├── core_form.xml │ │ │ │ ├── core_model.xml │ │ │ │ ├── plugin_form.xml │ │ │ │ └── plugin_model.xml │ │ │ ├── test_new.py │ │ │ ├── test_new_api.py │ │ │ ├── test_new_command.py │ │ │ └── test_new_puppet.py │ ├── plugin │ │ ├── __init__.py │ │ ├── apibackup │ │ │ ├── __init__.py │ │ │ ├── backup.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ └── api_backup_service.py │ │ │ └── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── fixtures │ │ │ │ ├── __init__.py │ │ │ │ └── config.xml.sample │ │ │ │ └── test_apibackup.py │ │ ├── haproxy │ │ │ ├── __init__.py │ │ │ ├── acl.py │ │ │ ├── action.py │ │ │ ├── backend.py │ │ │ ├── config.py │ │ │ ├── cpu.py │ │ │ ├── errorfile.py │ │ │ ├── frontend.py │ │ │ ├── group.py │ │ │ ├── healthcheck.py │ │ │ ├── lua.py │ │ │ ├── mailer.py │ │ │ ├── mapfile.py │ │ │ ├── resolver.py │ │ │ ├── server.py │ │ │ ├── services │ │ │ │ ├── __init__.py │ │ │ │ ├── base.py │ │ │ │ ├── haproxy_acl_service.py │ │ │ │ ├── haproxy_action_service.py │ │ │ │ ├── haproxy_backend_service.py │ │ │ │ ├── haproxy_config_service.py │ │ │ │ ├── haproxy_cpu_service.py │ │ │ │ ├── haproxy_errorfile_service.py │ │ │ │ ├── haproxy_frontend_service.py │ │ │ │ ├── haproxy_group_service.py │ │ │ │ ├── haproxy_healthcheck_service.py │ │ │ │ ├── haproxy_lua_service.py │ │ │ │ ├── haproxy_mailer_service.py │ │ │ │ ├── haproxy_mapfile_service.py │ │ │ │ ├── haproxy_resolver_service.py │ │ │ │ ├── haproxy_server_service.py │ │ │ │ └── haproxy_user_service.py │ │ │ ├── tests │ │ │ │ ├── __init__.py │ │ │ │ ├── fixtures │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── model_data.json │ │ │ │ ├── test_haproxy.py │ │ │ │ ├── test_haproxy_acl.py │ │ │ │ ├── test_haproxy_action.py │ │ │ │ ├── test_haproxy_backend.py │ │ │ │ ├── test_haproxy_config.py │ │ │ │ ├── test_haproxy_cpu.py │ │ │ │ ├── test_haproxy_errorfile.py │ │ │ │ ├── test_haproxy_frontend.py │ │ │ │ ├── test_haproxy_group.py │ │ │ │ ├── test_haproxy_healthcheck.py │ │ │ │ ├── test_haproxy_lua.py │ │ │ │ ├── test_haproxy_mailer.py │ │ │ │ ├── test_haproxy_mapfile.py │ │ │ │ ├── test_haproxy_resolver.py │ │ │ │ ├── test_haproxy_server.py │ │ │ │ └── test_haproxy_user.py │ │ │ └── user.py │ │ └── nodeexporter │ │ │ ├── __init__.py │ │ │ ├── config.py │ │ │ ├── services │ │ │ ├── __init__.py │ │ │ └── nodeexporter_config_service.py │ │ │ └── tests │ │ │ ├── __init__.py │ │ │ ├── fixtures │ │ │ ├── __init__.py │ │ │ └── model_data.json │ │ │ └── test_nodeexporter_config.py │ ├── service_base.py │ ├── test_base.py │ ├── tree │ │ ├── __init__.py │ │ └── tests │ │ │ ├── __init__.py │ │ │ └── test_tree.py │ └── version │ │ ├── __init__.py │ │ └── tests │ │ ├── __init__.py │ │ └── test_version.py ├── conf.yaml ├── factories.py ├── formatters │ ├── __init__.py │ ├── base.py │ └── cli_output │ │ ├── __init__.py │ │ ├── cli_output_formatter.py │ │ ├── json_type_factory.py │ │ ├── json_types.py │ │ ├── output_format_factory.py │ │ ├── output_formats.py │ │ └── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── test_cols_output_format.py │ │ ├── test_json_filter_output_format.py │ │ ├── test_json_output_format.py │ │ ├── test_json_type.py │ │ ├── test_plain_output_format.py │ │ ├── test_table_output_format.py │ │ └── test_yaml_output_format.py ├── parser │ ├── __init__.py │ ├── base.py │ ├── html_parser.py │ ├── opnsense_api_reference_parser.py │ ├── opnsense_form_parser.py │ ├── opnsense_model_parser.py │ ├── opnsense_module_list_parser.py │ └── xml_parser.py ├── template_engines │ ├── __init__.py │ ├── base.py │ ├── exceptions.py │ ├── jinja2.py │ └── tests │ │ ├── __init__.py │ │ ├── fixtures │ │ ├── __init__.py │ │ └── template_vars.py │ │ └── test_jinja2_template_engine.py ├── test_base.py └── tests │ ├── __init__.py │ └── test_cli.py ├── requirements.txt ├── ruff.toml ├── scripts ├── acceptance_tests ├── changelog ├── coverage ├── create_test_env ├── lint ├── remove_test_env └── unit_tests ├── setup.py └── test_requirements.txt /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | /tmp/* 4 | *private* 5 | */local/* 6 | *venv* 7 | */tests/* 8 | */tests/fixtures/* 9 | test_base.py 10 | */output* 11 | /usr/lib/python* 12 | 13 | [report] 14 | exclude_lines = 15 | if __name__ == .__main__.: 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [andeman]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | pull_request: 7 | branches: [ main ] 8 | jobs: 9 | run: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: [ 3.9 ] 14 | env: 15 | OS: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install wheel 26 | if [ -f test_requirements.txt ]; then pip install -r test_requirements.txt; fi 27 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 28 | - name: Lint with flake8 29 | run: | 30 | scripts/lint 31 | - name: Unit test with coverage 32 | run: | 33 | scripts/coverage 34 | - name: Upload Coverage to Codecov 35 | uses: codecov/codecov-action@v1 36 | - name: smoke test 37 | run: | 38 | pip install . 39 | mkdir -p /home/runner/.config/opn-cli 40 | touch /home/runner/.config/opn-cli/conf.yaml 41 | opn-cli version 42 | opn-cli new command core firewall category --tag categories -m https://raw.githubusercontent.com/opnsense/core/master/src/opnsense/mvc/app/models/OPNsense/Firewall/Category.xml -f https://raw.githubusercontent.com/opnsense/core/master/src/opnsense/mvc/app/controllers/OPNsense/Firewall/forms/categoryEdit.xml 43 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: publish docker image 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | docker: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - 9 | name: Checkout 10 | uses: actions/checkout@v3 11 | - 12 | name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.9' 16 | - 17 | name: Set docker metadata 18 | run: | 19 | echo "docker_image_name=$(python setup.py --name)" >> $GITHUB_ENV 20 | echo "docker_tag_version=$(python setup.py --version)" >> $GITHUB_ENV 21 | - 22 | name: show docker metadata 23 | run: | 24 | echo "${{ env.docker_image_name }}" 25 | echo "${{ env.docker_tag_version }}" 26 | - 27 | name: Login to Docker Hub 28 | uses: docker/login-action@v2 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_TOKEN }} 32 | - 33 | name: Set up QEMU 34 | uses: docker/setup-qemu-action@v2 35 | 36 | - 37 | name: Set up Docker Buildx 38 | uses: docker/setup-buildx-action@v2 39 | 40 | - 41 | name: Build and export to Docker 42 | uses: docker/build-push-action@v3 43 | with: 44 | context: . 45 | load: true 46 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.docker_image_name }}:${{ env.docker_tag_version }} 47 | 48 | - 49 | name: Test 50 | run: | 51 | docker run --rm ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.docker_image_name }}:${{ env.docker_tag_version }} 52 | 53 | - 54 | name: Build and push 55 | uses: docker/build-push-action@v2 56 | with: 57 | context: . 58 | push: true 59 | tags: | 60 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.docker_image_name }}:latest 61 | ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.docker_image_name }}:${{ env.docker_tag_version }} 62 | 63 | - 64 | name: Docker Hub Description 65 | uses: peter-evans/dockerhub-description@v3 66 | with: 67 | username: ${{ secrets.DOCKERHUB_USERNAME }} 68 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 69 | repository: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.docker_image_name }} 70 | 71 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | deploy: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Set up Python 10 | uses: actions/setup-python@v2 11 | with: 12 | python-version: '3.9' 13 | - name: Install dependencies 14 | run: | 15 | python -m pip install --upgrade pip 16 | pip install setuptools wheel twine 17 | - name: Build and publish 18 | env: 19 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 20 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 21 | run: | 22 | python setup.py sdist bdist_wheel 23 | twine upload dist/* 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please use the GitHub issues functionality to report any bugs or requests for new features. Feel free to fork and submit pull requests for potential contributions. 2 | 3 | All contributions must pass all existing tests, new features should provide additional unit/acceptance tests. 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim as builder 2 | 3 | # install dependencies required to build python packages 4 | RUN apt-get update 5 | 6 | # setup venv 7 | ENV VENV="/venv" 8 | ENV PATH="${VENV}/bin:${PATH}" 9 | 10 | # copy build dependencies 11 | COPY opnsense_cli/__init__.py /app/opnsense_cli/__init__.py 12 | COPY MANIFEST.in /app/MANIFEST.in 13 | COPY README.md /app/README.md 14 | COPY setup.py /app/setup.py 15 | COPY requirements.txt /app/requirements.txt 16 | COPY test_requirements.txt /app/test_requirements.txt 17 | 18 | WORKDIR /app 19 | 20 | # install dependencies into builder venv 21 | RUN python -m venv ${VENV} && ${VENV}/bin/pip3 install --upgrade pip setuptools wheel 22 | RUN ${VENV}/bin/pip3 install --no-cache-dir -r requirements.txt 23 | RUN ${VENV}/bin/pip3 install --no-cache-dir -r test_requirements.txt 24 | 25 | 26 | FROM python:3.9-slim as app 27 | 28 | RUN apt-get update \ 29 | # some comfort... 30 | && apt-get install -y procps net-tools vim htop \ 31 | && rm -rf /var/lib/apt/lists/* \ 32 | && apt-get autoremove -y \ 33 | && apt-get clean -y 34 | 35 | # copy dependencies from the builder stage 36 | ENV VENV="/venv" 37 | ENV PATH="${VENV}/bin:$PATH" 38 | COPY --from=builder ${VENV} ${VENV} 39 | 40 | # copy app files 41 | COPY opnsense_cli /app/opnsense_cli 42 | COPY ./acceptance_tests /app/acceptance_tests 43 | COPY ./scripts /app/scripts 44 | COPY MANIFEST.in /app/MANIFEST.in 45 | COPY README.md /app/README.md 46 | COPY .coveragerc /app/.coveragerc 47 | COPY setup.py /app/setup.py 48 | 49 | 50 | # Creates a non-root user and adds permission to access the /app folder 51 | RUN addgroup --system appgroup && useradd appuser -g appgroup -m && chown -R appuser:appgroup /app 52 | 53 | COPY docker/.opn-cli/conf.yaml /home/appuser/.opn-cli/conf.yaml 54 | COPY docker/.opn-cli/ca.pem /home/appuser/.opn-cli/ca.pem 55 | 56 | WORKDIR /app 57 | 58 | # install app 59 | RUN python -m venv ${VENV} && ${VENV}/bin/pip3 install --upgrade --no-cache-dir . 60 | 61 | COPY requirements.txt /app/requirements.txt 62 | COPY test_requirements.txt /app/test_requirements.txt 63 | 64 | # run app 65 | USER appuser 66 | ENTRYPOINT ["opn-cli"] 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, Andreas Stürz 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include opnsense_cli *.j2 2 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = 'andeman/opnsense' 3 | config.vm.box_version = "23.7.10" 4 | config.vm.boot_timeout = 600 5 | 6 | # sepecial configurations for bsd shell / opnsense stuff 7 | config.ssh.sudo_command = "%c" 8 | config.ssh.shell = "/bin/sh" 9 | config.ssh.username = "root" 10 | config.ssh.password = "opnsense" 11 | 12 | config.vm.synced_folder ".", "/vagrant", disabled: true 13 | 14 | config.vm.network :forwarded_port, guest: 22, host: 3333, id: "ssh" 15 | config.vm.network :forwarded_port, guest: 443, host: 10443, auto_correct: true 16 | 17 | config.vm.provider "virtualbox" do |v| 18 | v.memory = 2048 19 | v.cpus = 2 20 | 21 | v.customize ['modifyvm',:id, '--nic1', 'nat', '--nic2', 'intnet'] 22 | end 23 | 24 | $auto_update_script = <<-'SCRIPT' 25 | # auto-update to latest minor version 26 | version_local=$(opnsense-version -v) 27 | version_remote=$(configctl firmware remote | grep -e "^opnsense||" | awk -F '\\|\\|\\|' '{ print $2 }') 28 | echo "installed version: $version_local" 29 | echo "remote version: $version_remote" 30 | if [ "$version_local" != "$version_remote" ]; then 31 | echo "New opnsense version ${version_remote} is available." 32 | echo "Updating..." 33 | configctl firmware flush 34 | configctl firmware update 35 | sleep 1 36 | timeout 2m tail -f /tmp/pkg_upgrade.progress 37 | exit 0 38 | fi 39 | SCRIPT 40 | 41 | config.vm.provision 'shell', inline: $auto_update_script 42 | 43 | end 44 | -------------------------------------------------------------------------------- /acceptance_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/acceptance_tests/__init__.py -------------------------------------------------------------------------------- /acceptance_tests/base.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | import subprocess 3 | from dataclasses import dataclass 4 | from unittest import TestCase 5 | 6 | 7 | class CommandError(Exception): 8 | def __init__(self, code, msg): 9 | message = "Exit code: {} - Error: {}".format(code, msg) 10 | super().__init__(message) 11 | 12 | 13 | @dataclass 14 | class CommandResult: 15 | stdout: str 16 | stderr: str 17 | exitcode: int 18 | 19 | 20 | class CliCommandTestCase(TestCase): 21 | @property 22 | def _api_client_args_fixtures(self): 23 | return ["api_key", "api_secret", "https://127.0.0.1:10443/api", True, "~/.opn-cli/ca.pem", 60] 24 | 25 | def run_command(self, command) -> str: 26 | """ 27 | :param command: The cli command 28 | :return: str 29 | """ 30 | return self.cmd_execute(command) 31 | 32 | def cmd_execute(self, command, encoding="UTF-8", raise_exception=False): 33 | args = shlex.split(command) 34 | p = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 35 | output, error = p.communicate() 36 | if error and raise_exception: 37 | raise CommandError(p.returncode, error.decode(encoding)) 38 | 39 | result = CommandResult(output.decode(encoding), error.decode(encoding), p.returncode) 40 | 41 | return result 42 | -------------------------------------------------------------------------------- /acceptance_tests/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/acceptance_tests/tests/__init__.py -------------------------------------------------------------------------------- /acceptance_tests/tests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/acceptance_tests/tests/core/__init__.py -------------------------------------------------------------------------------- /acceptance_tests/tests/core/test_plugin.py: -------------------------------------------------------------------------------- 1 | from acceptance_tests.base import CliCommandTestCase 2 | 3 | 4 | class TestPluginCommands(CliCommandTestCase): 5 | def setUp(self): 6 | pass 7 | 8 | def test_plugin_lifecycle(self): 9 | result = self.run_command("opn-cli plugin installed -o plain -c name,locked") 10 | self.assertIn("os-firewall N/A\n" "os-haproxy N/A\n" "os-virtualbox N/A\n", result.stdout) 11 | 12 | result = self.run_command("opn-cli plugin install os-helloworld") 13 | assert result.exitcode == 0 14 | 15 | result = self.run_command("opn-cli plugin lock os-helloworld") 16 | assert result.exitcode == 0 17 | 18 | result = self.run_command("opn-cli plugin installed -o plain -c name,locked") 19 | self.assertIn("os-firewall N/A\n" "os-haproxy N/A\n" "os-helloworld 1\n" "os-virtualbox N/A\n", result.stdout) 20 | 21 | result = self.run_command("opn-cli plugin unlock os-helloworld") 22 | assert result.exitcode == 0 23 | 24 | result = self.run_command("opn-cli plugin uninstall os-helloworld") 25 | assert result.exitcode == 0 26 | -------------------------------------------------------------------------------- /acceptance_tests/tests/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/acceptance_tests/tests/plugin/__init__.py -------------------------------------------------------------------------------- /acceptance_tests_requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | ####### acceptance_test_requirements.txt ####### 3 | # 4 | pytest 5 | -------------------------------------------------------------------------------- /docker/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/docker/.gitkeep -------------------------------------------------------------------------------- /docker/.opn-cli/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJDCCAwygAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjjELMAkGA1UEBhMCREUx 3 | EDAOBgNVBAgMB0JhdmFyaWExDzANBgNVBAcMBk11bmljaDEkMCIGA1UECgwbQW5k 4 | cmVhcyBTdMO8cnogSVQtU29sdXRpb25zMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl 5 | eGFtcGxlLmNvbTEUMBIGA1UEAwwLaW50ZXJuYWwtY2EwHhcNMjEwNjE4MDcwNTU1 6 | WhcNMzIwOTA0MDcwNTU1WjCBjjELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0JhdmFy 7 | aWExDzANBgNVBAcMBk11bmljaDEkMCIGA1UECgwbQW5kcmVhcyBTdMO8cnogSVQt 8 | U29sdXRpb25zMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBleGFtcGxlLmNvbTEUMBIG 9 | A1UEAwwLaW50ZXJuYWwtY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 10 | AQC0qagEwZMhQg1KJAN+0RdoseK9ymoicf18UOvKC9gTsViRjgswgHkuMLdJrEMp 11 | 62xYZerLMxiiarGeNIDfft5pzgQ24zZ2kGrOU2uaUmNbbEknV0ORr/Vqgxe3Hufw 12 | RlQ0W+WogChEUJ8zEqSMhcsF4juBze6sKo8zOobX6VqdbbgEwzxoU7veHltrK7if 13 | LXzDZGaaAlnmfVfIP/35SopusktgnJIdr/j/67iTAkSK3oF1AsGpbxv2i0Ff5dYU 14 | f+1lc5tKeOUOx6K+ZYQ028wWDJ+rVQYEhNt1TeobWhQUsagkSMPTQvIZtakKOtsH 15 | tDRTHMr9oOzjBQmrI4qcEWI9AgMBAAGjgYowgYcwNwYJYIZIAYb4QgENBCoWKE9Q 16 | TnNlbnNlIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHQYDVR0OBBYE 17 | FAarfScQ/ktdpoo7h1L78/qWkQ0yMB8GA1UdIwQYMBaAFAarfScQ/ktdpoo7h1L7 18 | 8/qWkQ0yMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADFEhUhtGhLg 19 | j3bSNjl3+qIy6aFzPAsJAAJbJS7BSbgBZLesuMVWOB9LPW0TRN94hGnMBo7CFPhs 20 | YUTWQOx3R2VAJjFeM8unAWYZ+Bs5BMiggd6TGWOxrf8BIG2N5l0VGEmATyQnBen+ 21 | CsqYL9dh3lfnwKn1U4kiNxwMRWZLYismUR+hiWP5xyjoWWqbZ+EEJ42tz83z9qdG 22 | ujFz62+bOH+BPuqM21BiLTKPYR23jHQTlp1gsTn1kBJaKiYXmVGygdfKYZKhJnZy 23 | gdSO3pvQbco79eoHB6pdZs7xzAxwDVD9FklRhZawwRmLAhBhfGt9bw7QQjYVzUCn 24 | JA+UnLuAg6U= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /docker/.opn-cli/conf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | api_key: 3T7LyQbZSXC/WN56qL0LyvLweNICeiTOzZ2JifNAvlrL+BW8Yvx7WSAUS4xvmLM/BE7xVVtv0Mv2QwNm 3 | api_secret: 2mxXt++o5Mmte3sfNJsYxlm18M2t/wAGIAHwmWoe8qc15T5wUrejJQUd/sfXSGnAG2Xk2gqMf8FzHpT2 4 | url: https://host.docker.internal:10443/api 5 | timeout: 60 6 | ssl_verify: true 7 | ca: ~/.opn-cli/ca.pem 8 | 9 | -------------------------------------------------------------------------------- /opnsense_cli/__init__.py: -------------------------------------------------------------------------------- 1 | __cli_name__ = "opn-cli" 2 | __version__ = "1.7.0" 3 | __copyright__ = "(c) by Andreas Stürz" 4 | -------------------------------------------------------------------------------- /opnsense_cli/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/api/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/api/base.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.client import ApiClient 2 | 3 | 4 | class ApiBase: 5 | def __init__(self, api_client: ApiClient): 6 | self._api_client = api_client 7 | self.module = self.MODULE 8 | self.controller = self.CONTROLLER 9 | self._method = None 10 | self._command = None 11 | 12 | @property 13 | def method(self): 14 | return self._method 15 | 16 | @method.setter 17 | def method(self, value): 18 | self._method = value 19 | 20 | @property 21 | def command(self): 22 | return self._command 23 | 24 | @command.setter 25 | def command(self, value): 26 | self._command = value 27 | 28 | def _api_call(api_function): 29 | def api_response(self, *args, json=None): 30 | api_function(self) 31 | return self._api_client.execute( 32 | *args, module=self.module, controller=self.controller, method=self.method, command=self.command, json=json 33 | ) 34 | 35 | return api_response 36 | -------------------------------------------------------------------------------- /opnsense_cli/api/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/api/core/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/api/core/configbackup.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Backup(ApiBase): 5 | MODULE = "core" 6 | CONTROLLER = "backup" 7 | """ 8 | api-backup BackupController 9 | """ 10 | 11 | @ApiBase._api_call 12 | def download(self, *args): 13 | self.method = "get" 14 | self.command = "download" 15 | -------------------------------------------------------------------------------- /opnsense_cli/api/core/firmware.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Firmware(ApiBase): 5 | MODULE = "Core" 6 | CONTROLLER = "Firmware" 7 | """ 8 | FIRMWARE 9 | """ 10 | 11 | @ApiBase._api_call 12 | def info(self, *args): 13 | self.method = "get" 14 | self.command = "info" 15 | 16 | @ApiBase._api_call 17 | def upgradestatus(self, *args): 18 | self.method = "get" 19 | self.command = "upgradestatus" 20 | 21 | """ 22 | PACKAGES 23 | """ 24 | 25 | @ApiBase._api_call 26 | def install(self, *args): 27 | self.method = "post" 28 | self.command = "install" 29 | 30 | @ApiBase._api_call 31 | def reinstall(self, *args): 32 | self.method = "post" 33 | self.command = "reinstall" 34 | 35 | @ApiBase._api_call 36 | def remove(self, *args): 37 | self.method = "post" 38 | self.command = "remove" 39 | 40 | @ApiBase._api_call 41 | def lock(self, *args): 42 | self.method = "post" 43 | self.command = "lock" 44 | 45 | @ApiBase._api_call 46 | def unlock(self, *args): 47 | self.method = "post" 48 | self.command = "unlock" 49 | 50 | @ApiBase._api_call 51 | def details(self, *args): 52 | self.method = "post" 53 | self.command = "details" 54 | -------------------------------------------------------------------------------- /opnsense_cli/api/core/ipsec.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Tunnel(ApiBase): 5 | MODULE = "ipsec" 6 | CONTROLLER = "tunnel" 7 | """ 8 | Ipsec TunnelController 9 | """ 10 | 11 | @ApiBase._api_call 12 | def searchPhase1(self, *args): 13 | self.method = "get" 14 | self.command = "searchPhase1" 15 | 16 | @ApiBase._api_call 17 | def searchPhase2(self, *args): 18 | self.method = "post" 19 | self.command = "searchPhase2" 20 | -------------------------------------------------------------------------------- /opnsense_cli/api/core/routes.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Gateway(ApiBase): 5 | MODULE = "routes" 6 | CONTROLLER = "gateway" 7 | """ 8 | Routes GatewayController 9 | """ 10 | 11 | @ApiBase._api_call 12 | def status(self, *args): 13 | self.method = "get" 14 | self.command = "status" 15 | 16 | 17 | class Routes(ApiBase): 18 | MODULE = "routes" 19 | CONTROLLER = "routes" 20 | """ 21 | Routes RoutesController 22 | """ 23 | 24 | @ApiBase._api_call 25 | def addroute(self, *args): 26 | self.method = "post" 27 | self.command = "addroute" 28 | 29 | @ApiBase._api_call 30 | def delroute(self, *args): 31 | self.method = "post" 32 | self.command = "delroute" 33 | 34 | @ApiBase._api_call 35 | def get(self, *args): 36 | self.method = "get" 37 | self.command = "get" 38 | 39 | @ApiBase._api_call 40 | def reconfigure(self, *args): 41 | self.method = "post" 42 | self.command = "reconfigure" 43 | 44 | @ApiBase._api_call 45 | def setroute(self, *args): 46 | self.method = "post" 47 | self.command = "setroute" 48 | -------------------------------------------------------------------------------- /opnsense_cli/api/core/syslog.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Service(ApiBase): 5 | MODULE = "syslog" 6 | CONTROLLER = "service" 7 | """ 8 | Syslog ServiceController 9 | """ 10 | 11 | @ApiBase._api_call 12 | def reconfigure(self, *args): 13 | self.method = "post" 14 | self.command = "reconfigure" 15 | 16 | @ApiBase._api_call 17 | def stats(self, *args): 18 | self.method = "get" 19 | self.command = "stats" 20 | 21 | 22 | class Settings(ApiBase): 23 | MODULE = "syslog" 24 | CONTROLLER = "settings" 25 | """ 26 | Syslog SettingsController 27 | """ 28 | 29 | @ApiBase._api_call 30 | def addDestination(self, *args): 31 | self.method = "post" 32 | self.command = "addDestination" 33 | 34 | @ApiBase._api_call 35 | def delDestination(self, *args): 36 | self.method = "post" 37 | self.command = "delDestination" 38 | 39 | @ApiBase._api_call 40 | def get(self, *args): 41 | self.method = "get" 42 | self.command = "get" 43 | 44 | @ApiBase._api_call 45 | def setDestination(self, *args): 46 | self.method = "post" 47 | self.command = "setDestination" 48 | -------------------------------------------------------------------------------- /opnsense_cli/api/core/unbound.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Service(ApiBase): 5 | MODULE = "unbound" 6 | CONTROLLER = "service" 7 | """ 8 | Unbound ServiceController 9 | """ 10 | 11 | @ApiBase._api_call 12 | def reconfigure(self, *args): 13 | self.method = "post" 14 | self.command = "reconfigure" 15 | 16 | 17 | class Settings(ApiBase): 18 | MODULE = "unbound" 19 | CONTROLLER = "settings" 20 | """ 21 | Unbound SettingsController 22 | """ 23 | 24 | @ApiBase._api_call 25 | def addDomainOverride(self, *args): 26 | self.method = "post" 27 | self.command = "addDomainOverride" 28 | 29 | @ApiBase._api_call 30 | def addHostAlias(self, *args): 31 | self.method = "post" 32 | self.command = "addHostAlias" 33 | 34 | @ApiBase._api_call 35 | def addHostOverride(self, *args): 36 | self.method = "post" 37 | self.command = "addHostOverride" 38 | 39 | @ApiBase._api_call 40 | def delDomainOverride(self, *args): 41 | self.method = "post" 42 | self.command = "delDomainOverride" 43 | 44 | @ApiBase._api_call 45 | def delHostAlias(self, *args): 46 | self.method = "post" 47 | self.command = "delHostAlias" 48 | 49 | @ApiBase._api_call 50 | def delHostOverride(self, *args): 51 | self.method = "post" 52 | self.command = "delHostOverride" 53 | 54 | @ApiBase._api_call 55 | def get(self, *args): 56 | self.method = "get" 57 | self.command = "get" 58 | 59 | @ApiBase._api_call 60 | def setDomainOverride(self, *args): 61 | self.method = "post" 62 | self.command = "setDomainOverride" 63 | 64 | @ApiBase._api_call 65 | def setHostAlias(self, *args): 66 | self.method = "post" 67 | self.command = "setHostAlias" 68 | 69 | @ApiBase._api_call 70 | def setHostOverride(self, *args): 71 | self.method = "post" 72 | self.command = "setHostOverride" 73 | -------------------------------------------------------------------------------- /opnsense_cli/api/exceptions.py: -------------------------------------------------------------------------------- 1 | from click.exceptions import ClickException 2 | 3 | 4 | class APIException(ClickException): 5 | def __init__(self, *args, status_code=None, resp_body=None, url=None, **kwargs): 6 | self.resp_body = resp_body 7 | self.status_code = status_code 8 | self.url = url 9 | message = { 10 | "API client": kwargs.get("message", resp_body), 11 | } 12 | if url: 13 | message["url"] = url 14 | super(APIException, self).__init__(message, *args) 15 | -------------------------------------------------------------------------------- /opnsense_cli/api/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/api/plugin/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/api/plugin/apibackup.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Backup(ApiBase): 5 | MODULE = "backup" 6 | CONTROLLER = "backup" 7 | """ 8 | api-backup BackupController 9 | """ 10 | 11 | @ApiBase._api_call 12 | def download(self, *args): 13 | self.method = "get" 14 | self.command = "download" 15 | -------------------------------------------------------------------------------- /opnsense_cli/api/plugin/nodeexporter.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class General(ApiBase): 5 | MODULE = "nodeexporter" 6 | CONTROLLER = "general" 7 | """ 8 | Nodeexporter GeneralController 9 | """ 10 | 11 | @ApiBase._api_call 12 | def get(self, *args): 13 | self.method = "get" 14 | self.command = "get" 15 | 16 | @ApiBase._api_call 17 | def set(self, *args): 18 | self.method = "post" 19 | self.command = "set" 20 | 21 | 22 | class Service(ApiBase): 23 | MODULE = "nodeexporter" 24 | CONTROLLER = "service" 25 | """ 26 | Nodeexporter ServiceController 27 | """ 28 | 29 | @ApiBase._api_call 30 | def reconfigure(self, *args): 31 | self.method = "post" 32 | self.command = "reconfigure" 33 | -------------------------------------------------------------------------------- /opnsense_cli/api/plugin/openvpn.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | 4 | class Openvpn(ApiBase): 5 | MODULE = "openvpn" 6 | CONTROLLER = "export" 7 | """ 8 | OPENVPN EXPORT 9 | """ 10 | 11 | @ApiBase._api_call 12 | def accounts(self, *args): 13 | self.method = "get" 14 | self.command = "accounts" 15 | 16 | @ApiBase._api_call 17 | def download(self, *args, json=None): 18 | self.method = "post" 19 | self.command = "download" 20 | 21 | @ApiBase._api_call 22 | def providers(self, *args): 23 | self.method = "get" 24 | self.command = "providers" 25 | 26 | @ApiBase._api_call 27 | def templates(self, *args): 28 | self.method = "get" 29 | self.command = "templates" 30 | -------------------------------------------------------------------------------- /opnsense_cli/api/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/api/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEJDCCAwygAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjjELMAkGA1UEBhMCREUx 3 | EDAOBgNVBAgMB0JhdmFyaWExDzANBgNVBAcMBk11bmljaDEkMCIGA1UECgwbQW5k 4 | cmVhcyBTdMO8cnogSVQtU29sdXRpb25zMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl 5 | eGFtcGxlLmNvbTEUMBIGA1UEAwwLaW50ZXJuYWwtY2EwHhcNMjEwNjE4MDcwNTU1 6 | WhcNMzIwOTA0MDcwNTU1WjCBjjELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0JhdmFy 7 | aWExDzANBgNVBAcMBk11bmljaDEkMCIGA1UECgwbQW5kcmVhcyBTdMO8cnogSVQt 8 | U29sdXRpb25zMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBleGFtcGxlLmNvbTEUMBIG 9 | A1UEAwwLaW50ZXJuYWwtY2EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 10 | AQC0qagEwZMhQg1KJAN+0RdoseK9ymoicf18UOvKC9gTsViRjgswgHkuMLdJrEMp 11 | 62xYZerLMxiiarGeNIDfft5pzgQ24zZ2kGrOU2uaUmNbbEknV0ORr/Vqgxe3Hufw 12 | RlQ0W+WogChEUJ8zEqSMhcsF4juBze6sKo8zOobX6VqdbbgEwzxoU7veHltrK7if 13 | LXzDZGaaAlnmfVfIP/35SopusktgnJIdr/j/67iTAkSK3oF1AsGpbxv2i0Ff5dYU 14 | f+1lc5tKeOUOx6K+ZYQ028wWDJ+rVQYEhNt1TeobWhQUsagkSMPTQvIZtakKOtsH 15 | tDRTHMr9oOzjBQmrI4qcEWI9AgMBAAGjgYowgYcwNwYJYIZIAYb4QgENBCoWKE9Q 16 | TnNlbnNlIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHQYDVR0OBBYE 17 | FAarfScQ/ktdpoo7h1L78/qWkQ0yMB8GA1UdIwQYMBaAFAarfScQ/ktdpoo7h1L7 18 | 8/qWkQ0yMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADFEhUhtGhLg 19 | j3bSNjl3+qIy6aFzPAsJAAJbJS7BSbgBZLesuMVWOB9LPW0TRN94hGnMBo7CFPhs 20 | YUTWQOx3R2VAJjFeM8unAWYZ+Bs5BMiggd6TGWOxrf8BIG2N5l0VGEmATyQnBen+ 21 | CsqYL9dh3lfnwKn1U4kiNxwMRWZLYismUR+hiWP5xyjoWWqbZ+EEJ42tz83z9qdG 22 | ujFz62+bOH+BPuqM21BiLTKPYR23jHQTlp1gsTn1kBJaKiYXmVGygdfKYZKhJnZy 23 | gdSO3pvQbco79eoHB6pdZs7xzAxwDVD9FklRhZawwRmLAhBhfGt9bw7QQjYVzUCn 24 | JA+UnLuAg6U= 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/click_addons/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/click_addons/callbacks.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | 4 | from opnsense_cli.commands.service_base import CommandService 5 | from opnsense_cli.formatters.cli_output.output_format_factory import CliOutputFormatFactory 6 | from opnsense_cli.formatters.cli_output.output_formats import Format 7 | from opnsense_cli import __cli_name__ 8 | 9 | 10 | def get_default_config_dir(): 11 | if "XDG_CONFIG_HOME" in os.environ: 12 | return f"~/.config/{__cli_name__}" 13 | return f"~/.{__cli_name__}" 14 | 15 | 16 | """ 17 | Click callback methods 18 | See: https://click.palletsprojects.com/en/8.0.x/advanced/#parameter-modifications 19 | """ 20 | 21 | 22 | def defaults_from_configfile(ctx, param, filename): 23 | def dict_from_yaml(path): 24 | with open(path, "r") as yaml_file: 25 | data = yaml.load(yaml_file, Loader=yaml.SafeLoader) 26 | return data 27 | 28 | options = dict_from_yaml(os.path.expanduser(filename)) 29 | ctx.default_map = options 30 | 31 | 32 | def expand_path(ctx, param, filename): 33 | return os.path.expanduser(filename) 34 | 35 | 36 | def available_formats(): 37 | return CliOutputFormatFactory._keymap.keys() 38 | 39 | 40 | def formatter_from_formatter_name(ctx, param, format_name) -> Format: 41 | factory = CliOutputFormatFactory(format_name) 42 | return factory.get_class() 43 | 44 | 45 | def bool_as_string(ctx, param, value): 46 | if isinstance(value, bool): 47 | return str(int(value)) 48 | return value 49 | 50 | 51 | def tuple_to_csv(ctx, param, value): 52 | if param.multiple and not value: 53 | return None 54 | if isinstance(value, tuple): 55 | return ",".join(value) 56 | return value 57 | 58 | 59 | def comma_to_newline(ctx, param, value): 60 | if isinstance(value, str) and "," in value: 61 | return value.replace(",", "\n") 62 | return value 63 | 64 | 65 | def int_as_string(ctx, param, value): 66 | if isinstance(value, int): 67 | return str(value) 68 | return value 69 | 70 | 71 | def resolve_linked_names_to_uuids(ctx, param, value): 72 | option_name = param.opts[0].replace("--", "") 73 | resolve_map = ctx.obj.uuid_resolver_map[option_name] 74 | 75 | if value and isinstance(ctx.obj, CommandService): 76 | return ctx.obj.resolve_linked_uuids(resolve_map, value) 77 | return value 78 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/command_tree.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | class _CommandWrapper(object): 5 | def __init__(self, command=None, children=None): 6 | self.command = command 7 | self.children = [] 8 | 9 | @property 10 | def name(self): 11 | return self.command.name 12 | 13 | 14 | def _build_command_tree(click_command): 15 | wrapper = _CommandWrapper(click_command) 16 | 17 | if isinstance(click_command, click.core.Group): 18 | for _, cmd in click_command.commands.items(): 19 | if not getattr(cmd, "hidden", False): 20 | wrapper.children.append(_build_command_tree(cmd)) 21 | 22 | return wrapper 23 | 24 | 25 | def _print_tree(command, depth=0, is_last_item=False, is_last_parent=False): 26 | if depth == 0: 27 | prefix = "" 28 | tree_item = "" 29 | else: 30 | prefix = " " if is_last_parent else "│ " 31 | tree_item = "└── " if is_last_item else "├── " 32 | 33 | root_line = "│ " if is_last_parent else "" 34 | 35 | line = root_line + prefix * (depth - 1) + tree_item + command.name 36 | 37 | click.echo(line) 38 | 39 | for i, child in enumerate(sorted(command.children, key=lambda x: x.name)): 40 | _print_tree(child, depth=(depth + 1), is_last_item=(i == (len(command.children) - 1)), is_last_parent=is_last_item) 41 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/param_type_csv.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | class CsvParamType(click.types.StringParamType): 5 | name = "csv" 6 | 7 | def __repr__(self) -> str: 8 | return "CSV" 9 | 10 | 11 | CSV = CsvParamType() 12 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/param_type_int_or_empty.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | class IntOrEmptyClickParamType(click.ParamType): 5 | name = "int_or_empty" 6 | 7 | def convert(self, value, param, ctx): 8 | if value == "": 9 | return value 10 | try: 11 | return int(value) 12 | except ValueError: 13 | self.fail("%s is not a valid integer" % value, param, ctx) 14 | 15 | 16 | INT_OR_EMPTY = IntOrEmptyClickParamType() 17 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/click_addons/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/click_addons/tests/test_callbacks.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | from opnsense_cli.click_addons.callbacks import get_default_config_dir, bool_as_string, comma_to_newline 4 | from opnsense_cli import __cli_name__ 5 | import os 6 | 7 | 8 | class TestClickCallbacks(unittest.TestCase): 9 | @patch.dict(os.environ, {}, clear=True) 10 | def test_get_default_config_dir(self): 11 | result = get_default_config_dir() 12 | self.assertEqual(result, f"~/.{__cli_name__}") 13 | 14 | @patch.dict(os.environ, {"XDG_CONFIG_HOME": "whatever"}, clear=True) 15 | def test_get_default_config_dir_xgd_home(self): 16 | result = get_default_config_dir() 17 | self.assertEqual(result, f"~/.config/{__cli_name__}") 18 | 19 | def test_bool_as_string_with_bool(self): 20 | result = bool_as_string(None, None, True) 21 | self.assertEqual(result, "1") 22 | 23 | def test_bool_as_string_without_bool(self): 24 | result = bool_as_string(None, None, 5) 25 | self.assertEqual(result, 5) 26 | 27 | def test_comma_to_newline_with_csv_string(self): 28 | result = comma_to_newline(None, None, "item1,item2,item3") 29 | self.assertEqual(result, "item1\nitem2\nitem3") 30 | 31 | def test_comma_to_newline_without_csv_string(self): 32 | result = comma_to_newline(None, None, "item1") 33 | self.assertEqual(result, "item1") 34 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/tests/test_command_autoloader.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from click.core import Command as ClickCommand 3 | from opnsense_cli.click_addons.command_autoloader import ClickCommandAutoloader 4 | from opnsense_cli.cli import cli 5 | from unittest.mock import patch 6 | import os 7 | 8 | 9 | class TestClickCommandAutoloader(TestCase): 10 | def setUp(self): 11 | self._autoloader = ClickCommandAutoloader(cli) 12 | self._script_dir = os.path.dirname(os.path.realpath(__file__)) 13 | self._commands_dir = f"{self._script_dir}/../../opnsense_cli/commands" 14 | 15 | @patch("opnsense_cli.click_addons.command_autoloader.os.walk") 16 | def test_autoload_commands(self, os_walk_mock): 17 | os_walk_mock.return_value = [ 18 | (f"{self._commands_dir}/version", ["__pycache__"], ["__init__.py"]), 19 | (f"{self._commands_dir}/version/__pycache__", [], ["__init__"]), 20 | ] 21 | result = self._autoloader.autoload("opnsense_cli.commands.version") 22 | 23 | self.assertIsInstance(result, ClickCommand) 24 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/tests/test_param_type_csv.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import click 3 | from click.testing import CliRunner 4 | from opnsense_cli.click_addons.param_type_csv import CSV 5 | 6 | 7 | class TestClickParamTypeCsv(unittest.TestCase): 8 | def setUp(self): 9 | @click.command() 10 | @click.option("--csv", type=CSV, help="a String with comma separated values.") 11 | def cvs(csv): 12 | click.echo(f"csv={csv}") 13 | 14 | self._cli_cvs = cvs 15 | 16 | def test_csv_returns_csv(self): 17 | runner = CliRunner() 18 | result = runner.invoke(self._cli_cvs, ["--csv", "a,b,c"]) 19 | print(result.output) 20 | self.assertEqual(result.output, "csv=a,b,c\n") 21 | 22 | def test_csv_has_correct_repr(self): 23 | self.assertEqual(repr(CSV), "CSV") 24 | -------------------------------------------------------------------------------- /opnsense_cli/click_addons/tests/test_param_type_int_or_empty.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import click 3 | from click.testing import CliRunner 4 | from opnsense_cli.click_addons.param_type_int_or_empty import INT_OR_EMPTY 5 | 6 | 7 | class TestClickParamTypeIntOrEmpty(unittest.TestCase): 8 | def setUp(self): 9 | @click.command() 10 | @click.option("--int_or_empty", type=INT_OR_EMPTY, help="Could be an int value or an empty string.") 11 | def cli_int_or_empty(int_or_empty): 12 | click.echo(f"int_or_empty={int_or_empty}") 13 | 14 | self._cli_int_or_empty = cli_int_or_empty 15 | 16 | def test_Int_or_Empty_returns_INT(self): 17 | runner = CliRunner() 18 | result = runner.invoke(self._cli_int_or_empty, ["--int_or_empty", "5"]) 19 | print(result.output) 20 | self.assertEqual(result.output, "int_or_empty=5\n") 21 | 22 | def test_Int_or_Empty_returns_EMPTY_STRING(self): 23 | runner = CliRunner() 24 | result = runner.invoke(self._cli_int_or_empty, ["--int_or_empty", ""]) 25 | print(result.output) 26 | self.assertEqual(result.output, "int_or_empty=\n") 27 | 28 | def test_Int_or_Empty_returns_ERROR(self): 29 | runner = CliRunner() 30 | result = runner.invoke(self._cli_int_or_empty, ["--int_or_empty", "0.5"]) 31 | 32 | self.assertEqual(2, result.exit_code) 33 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from abc import ABC, abstractmethod 3 | 4 | 5 | class CodeGenerator(ABC): 6 | def write_code(self, path): 7 | code = self.get_code() 8 | return self._write_to_file(code, path) 9 | 10 | def _write_to_file(self, content, path): 11 | os.makedirs(os.path.dirname(path), exist_ok=True) 12 | with open(path, "w") as file: 13 | file.writelines(content) 14 | return f"generate new code: {path}" 15 | 16 | def _render_template(self, vars, template): 17 | self._template_engine.vars = vars 18 | self._template_engine.set_template_from_file(template) 19 | return self._template_engine.render() 20 | 21 | def get_code(self): 22 | template_vars = self._get_template_vars() 23 | return self._render_template(template_vars, self._template) 24 | 25 | @abstractmethod 26 | def _get_template_vars(self): 27 | """ " This method should be implemented.""" 28 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/opn_cli/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/base.py: -------------------------------------------------------------------------------- 1 | from bs4 import Tag 2 | 3 | from opnsense_cli.code_generators.base import CodeGenerator 4 | from opnsense_cli.template_engines.base import TemplateEngine 5 | from opnsense_cli.factories import ObjectTypeFromDataFactory 6 | 7 | 8 | class CommandCodeGenerator(CodeGenerator): 9 | def __init__( 10 | self, 11 | tag_content: Tag, 12 | template_engine: TemplateEngine, 13 | option_factory: ObjectTypeFromDataFactory, 14 | template, 15 | group, 16 | command, 17 | model_xml_tag, 18 | module_type, 19 | ): 20 | self._tag_content: Tag = tag_content 21 | self._template_engine = template_engine 22 | self._template = template 23 | self._click_group = group 24 | self._click_command = command 25 | self._click_option_factory = option_factory 26 | self._model_xml_tag = model_xml_tag 27 | self._module_type = module_type 28 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/opn_cli/command/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/command/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.opn_cli.base import CommandCodeGenerator 2 | from opnsense_cli.code_generators.opn_cli.factory_types import ClickOptionCodeFragment 3 | from opnsense_cli.code_generators.opn_cli.command.template_vars import CommandTemplateVars 4 | 5 | 6 | class ClickCommandCodeGenerator(CommandCodeGenerator): 7 | IGNORED_TYPES = ["UniqueIdField"] 8 | IGNORED_TAG_NAMES_CREATE = ["name"] 9 | 10 | def __init__(self, *args): 11 | super().__init__(*args) 12 | self.__help_messages = None 13 | 14 | @property 15 | def _help_messages(self): 16 | if self.__help_messages is None: 17 | return {} 18 | return self.__help_messages 19 | 20 | @_help_messages.setter 21 | def help_messages(self, messages: dict): 22 | self.__help_messages = messages 23 | 24 | def _get_template_vars(self): 25 | click_options_create = [] 26 | click_options_update = [] 27 | column_names = [] 28 | 29 | for tag in self._tag_content.findChildren(recursive=False): 30 | if tag.attrs.get("type") in self.IGNORED_TYPES: 31 | continue 32 | 33 | column_names.append(tag.name) 34 | 35 | click_option_type: ClickOptionCodeFragment = self._click_option_factory.get_type_for_data(tag) 36 | click_option_type.help = self._help_messages.get(tag.name, None) 37 | 38 | create_option_code = self._get_click_option_create_code(tag, click_option_type) 39 | if create_option_code: 40 | click_options_create.append(create_option_code) 41 | 42 | update_option_code = self._get_click_option_update_code(click_option_type) 43 | if update_option_code: 44 | click_options_update.append(update_option_code) 45 | 46 | return CommandTemplateVars( 47 | click_command=self._click_command, 48 | click_group=self._click_group, 49 | click_options_create=click_options_create, 50 | click_options_update=click_options_update, 51 | column_names=column_names, 52 | column_list=repr(column_names), 53 | module_type=self._module_type, 54 | ) 55 | 56 | def _get_click_option_create_code(self, tag, click_option_type: ClickOptionCodeFragment): 57 | if tag.name in self.IGNORED_TAG_NAMES_CREATE: 58 | return None 59 | 60 | return click_option_type.get_code_for_create() 61 | 62 | def _get_click_option_update_code(self, click_option_type: ClickOptionCodeFragment): 63 | return click_option_type.get_code_for_update() 64 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/command/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class CommandTemplateVars: 6 | click_command: str 7 | click_group: str 8 | click_options_create: list 9 | click_options_update: list 10 | column_names: list 11 | column_list: str 12 | module_type: str 13 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/factories.py: -------------------------------------------------------------------------------- 1 | from bs4.element import Tag 2 | from opnsense_cli.code_generators.opn_cli.factory_types import ClickOptionCodeFragment 3 | from opnsense_cli.factories import ObjectTypeFromDataFactory, FactoryException 4 | from opnsense_cli.code_generators.opn_cli.factory_types import ( 5 | ClickBoolean, 6 | ClickText, 7 | ClickInteger, 8 | ClickChoice, 9 | ClickTextLinkedItem, 10 | ) 11 | 12 | 13 | class ClickOptionCodeTypeFactory(ObjectTypeFromDataFactory): 14 | _keymap = { 15 | "BooleanField": ClickBoolean, 16 | "TextField": ClickText, 17 | "IntegerField": ClickInteger, 18 | "OptionField": ClickChoice, 19 | "ModelRelationField": ClickTextLinkedItem, 20 | "CertificateField": ClickText, 21 | "CSVListField": ClickText, 22 | "EmailField": ClickText, 23 | "HostnameField": ClickText, 24 | "NetworkField": ClickText, 25 | "PortField": ClickText, 26 | "JsonKeyValueStoreField": ClickText, 27 | ".\\UnboundDomainField": ClickText, 28 | ".\\UnboundServerField": ClickText, 29 | } 30 | 31 | def _get_class(self, key) -> ClickOptionCodeFragment: 32 | click_option_class = self._keymap.get(key, None) 33 | if not click_option_class: 34 | raise FactoryException(f"Could not find class for {key} in keymap") 35 | 36 | return click_option_class 37 | 38 | def get_type_for_data(self, tag: Tag) -> ClickOptionCodeFragment: 39 | field_type = tag.attrs.get("type", None) 40 | click_option_type_class = self._get_class(field_type) 41 | 42 | return click_option_type_class(tag) 43 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/opn_cli/service/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/service/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.opn_cli.base import CommandCodeGenerator 2 | from opnsense_cli.code_generators.opn_cli.service.template_vars import CommandServiceTemplateVars 3 | from bs4.element import Tag 4 | 5 | 6 | class ClickCommandServiceCodeGenerator(CommandCodeGenerator): 7 | def _get_template_vars(self): 8 | resolver_map = {} 9 | for tag in self._tag_content.findChildren(recursive=False): 10 | resolver_item = self._get_resolver(tag) 11 | 12 | if resolver_item: 13 | resolver_map.update(resolver_item) 14 | 15 | return CommandServiceTemplateVars( 16 | click_command=self._click_command, 17 | click_group=self._click_group, 18 | model_xml_tag=self._model_xml_tag, 19 | resolver_map=resolver_map, 20 | module_type=self._module_type, 21 | ) 22 | 23 | def _get_resolver(self, tag: Tag): 24 | if tag.attrs.get("type") != "ModelRelationField": 25 | return None 26 | 27 | items = tag.find("items").string 28 | template = f"$.{self._click_group}.{items}" + "[{uuids}]." f"{tag.find('display').string}" 29 | 30 | insert_as_key = items.split(".")[1].capitalize() 31 | if tag.find(name="Multiple", text="Y") or tag.find(name="multiple", text="Y"): 32 | insert_as_key = items.split(".")[0].capitalize() 33 | 34 | result = {tag.name: {"template": template, "insert_as_key": insert_as_key}} 35 | 36 | return result 37 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/service/template.py.j2: -------------------------------------------------------------------------------- 1 | {% set service = "{g}{c}Service".format(g=vars.click_group.capitalize(), c=vars.click_command.capitalize()) -%} 2 | from opnsense_cli.commands.exceptions import CommandException 3 | from opnsense_cli.commands.service_base import CommandService 4 | from opnsense_cli.api.{{ vars.module_type }}.{{ vars.click_group }} import Settings, Service 5 | 6 | 7 | class {{ service }}(CommandService): 8 | jsonpath_base = '$.{{ vars.click_group }}.{{ vars.model_xml_tag }}.{{ vars.click_command }}' 9 | uuid_resolver_map = { 10 | {% for key, config in vars.resolver_map.items() -%} 11 | '{{ key }}': {{ config }}, 12 | {% endfor %} 13 | } 14 | 15 | def __init__(self, settings_api: Settings, service_api: Service): 16 | super().__init__() 17 | self._complete_model_data_cache = None 18 | self._settings_api = settings_api 19 | self._service_api = service_api 20 | 21 | def list_{{ vars.click_command }}s(self): 22 | return self._get_{{ vars.click_command }}s_list() 23 | 24 | def show_{{ vars.click_command }}(self, uuid): 25 | {{ vars.click_command }}s = self._get_{{ vars.click_command }}s_list() 26 | {{ vars.click_command }} = next((item for item in {{ vars.click_command }}s if item["uuid"] == uuid), {}) 27 | return {{ vars.click_command }} 28 | 29 | def _get_{{ vars.click_command }}s_list(self): 30 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 31 | 32 | def create_{{ vars.click_command }}(self, json_payload: dict): 33 | result = self._settings_api.add{{ vars.click_command.capitalize() }}(json=json_payload) 34 | self._apply(result) 35 | return result 36 | 37 | def update_{{ vars.click_command }}(self, uuid, json_payload: dict): 38 | result = self._settings_api.set{{ vars.click_command.capitalize() }}(uuid, json=json_payload) 39 | self._apply(result) 40 | return result 41 | 42 | def delete_{{ vars.click_command }}(self, uuid): 43 | result = self._settings_api.del{{ vars.click_command.capitalize() }}(uuid) 44 | self._apply(result) 45 | return result 46 | 47 | def _apply(self, result_admin_action): 48 | if result_admin_action['result'] not in ['saved', 'deleted']: 49 | raise CommandException(result_admin_action) 50 | 51 | result_apply = self._service_api.reconfigure() 52 | 53 | if result_apply['status'] != 'ok': 54 | raise CommandException(f"Apply failed: {result_apply}") 55 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/service/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class CommandServiceTemplateVars: 6 | click_command: str 7 | click_group: str 8 | model_xml_tag: str 9 | resolver_map: dict 10 | module_type: str 11 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/opn_cli/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/opn_cli/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/unit_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/opn_cli/unit_test/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/unit_test/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.opn_cli.base import CommandCodeGenerator 2 | from opnsense_cli.code_generators.opn_cli.unit_test.template_vars import CommandTestTemplateVars 3 | 4 | 5 | class ClickCommandTestCodeGenerator(CommandCodeGenerator): 6 | def _get_template_vars(self): 7 | return CommandTestTemplateVars( 8 | click_command=self._click_command, 9 | click_group=self._click_group, 10 | model_xml_tag=self._model_xml_tag, 11 | module_type=self._module_type, 12 | ) 13 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opn_cli/unit_test/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class CommandTestTemplateVars: 6 | click_command: str 7 | click_group: str 8 | model_xml_tag: str 9 | module_type: str 10 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opnsense_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/opnsense_api/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opnsense_api/codegenerator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from opnsense_cli.template_engines.base import TemplateEngine 3 | from opnsense_cli.code_generators.base import CodeGenerator 4 | from opnsense_cli.code_generators.opnsense_api.template_vars import OpnsenseApiTemplateVars 5 | 6 | 7 | class OpnsenseApiCodeGenerator(CodeGenerator): 8 | def __init__(self, template_engine: TemplateEngine, template, controllers, module_name): 9 | self._template_engine = template_engine 10 | self._template = template 11 | self._controllers = controllers 12 | self._module_name = module_name 13 | 14 | def _get_template_vars(self): 15 | return OpnsenseApiTemplateVars(module_name=self._module_name, controllers=self._controllers) 16 | 17 | def _render_template(self, vars, template): 18 | self._template_engine.vars = vars 19 | self._template_engine.set_template_from_file(template) 20 | return self._template_engine.render() 21 | 22 | def get_code(self): 23 | vars = self._get_template_vars() 24 | return self._render_template(vars, self._template) 25 | 26 | def _write_to_file(self, content, path): 27 | os.makedirs(os.path.dirname(path), exist_ok=True) 28 | with open(path, "w") as file: 29 | file.writelines(content) 30 | return f"generate new code: {path}" 31 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opnsense_api/template.py.j2: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.base import ApiBase 2 | 3 | {% for controller, controller_values in vars.controllers.items() %} 4 | class {{ controller.capitalize() }}(ApiBase): 5 | MODULE = "{{ vars.module_name }}" 6 | CONTROLLER = "{{ controller }}" 7 | """ 8 | {{ vars.module_name.capitalize() }} {{ controller.capitalize() }}Controller 9 | """ 10 | {% for controller_value in controller_values %} 11 | @ApiBase._api_call 12 | def {{ controller_value['command'] }}(self, *args): 13 | self.method = "{{ controller_value['method'].lower() }}" 14 | self.command = "{{ controller_value['command'] }}" 15 | {% endfor %} 16 | {% endfor %} 17 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/opnsense_api/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class OpnsenseApiTemplateVars: 6 | module_name: str 7 | controllers: dict 8 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/puppet_code/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/acceptance_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/puppet_code/acceptance_test/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/acceptance_test/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.puppet_code.acceptance_test.template_vars import PuppetAcceptanceTestTemplateVars 2 | from opnsense_cli.code_generators.puppet_code.base import PuppetCodeGenerator 3 | from opnsense_cli.template_engines.base import TemplateEngine 4 | from opnsense_cli.factories import ObjectTypeFromDataFactory 5 | 6 | 7 | class PuppetAcceptanceTestCodeGenerator(PuppetCodeGenerator): 8 | def __init__( 9 | self, 10 | template_engine: TemplateEngine, 11 | type_factory: ObjectTypeFromDataFactory, 12 | template, 13 | group, 14 | command, 15 | find_uuid_by_column, 16 | create_command_params, 17 | update_command_params, 18 | ): 19 | super().__init__(create_command_params, type_factory, find_uuid_by_column, group, command) 20 | self._template_engine = template_engine 21 | self._template = template 22 | self._update_command_params = update_command_params 23 | 24 | def _get_template_vars(self): 25 | return PuppetAcceptanceTestTemplateVars( 26 | click_command=self._click_command, 27 | click_group=self._click_group, 28 | find_uuid_by_column=self._find_uuid_by_column, 29 | create_item=self._get_code_fragment("TEMPLATE_ACCEPTANCE_TEST_create_item"), 30 | match_item=self._get_code_fragment("TEMPLATE_ACCEPTANCE_TEST_match_item"), 31 | opn_cli_columns=self._get_all_columns(), 32 | ) 33 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/acceptance_test/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class PuppetAcceptanceTestTemplateVars: 7 | click_command: str 8 | click_group: str 9 | find_uuid_by_column: str 10 | create_item: List[str] 11 | match_item: List[str] 12 | opn_cli_columns: List[str] 13 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/base.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.base import CodeGenerator 2 | from opnsense_cli.code_generators.puppet_code.factories import PuppetCodeFragmentFactory 3 | from typing import List 4 | 5 | 6 | class PuppetCodeGenerator(CodeGenerator): 7 | def __init__( 8 | self, 9 | create_command_params, 10 | type_factory: PuppetCodeFragmentFactory, 11 | find_uuid_by_column, 12 | click_group, 13 | click_command, 14 | ): 15 | self._create_command_params = create_command_params 16 | self._type_factory = type_factory 17 | self._find_uuid_by_column = find_uuid_by_column 18 | self._click_group = click_group 19 | self._click_command = click_command 20 | self._ignore_params = ["output", "cols", "help"] 21 | 22 | def _get_code_fragment(self, template_variable_name: str) -> List[str]: 23 | template_variable_name_namevar = f"{template_variable_name}_namevar" 24 | 25 | code_fragments = [] 26 | 27 | for param_line in self._create_command_params: 28 | if param_line["name"] in self._ignore_params: 29 | continue 30 | 31 | code_type = self._type_factory.get_type_for_data( 32 | param_line, 33 | self._find_uuid_by_column, 34 | self._click_group, 35 | self._click_command, 36 | ) 37 | 38 | template = template_variable_name 39 | if self._is_namevar(param_line) and hasattr(code_type, template_variable_name_namevar): 40 | template = template_variable_name_namevar 41 | 42 | code_fragments.append(code_type.get_code_fragment(getattr(code_type, template))) 43 | 44 | return code_fragments 45 | 46 | def _is_namevar(self, param_line): 47 | return param_line["name"] == self._find_uuid_by_column 48 | 49 | def _get_all_columns(self): 50 | columns = [] 51 | for param_line in self._create_command_params: 52 | if param_line["name"] in self._ignore_params: 53 | continue 54 | columns.append(param_line["name"]) 55 | return columns 56 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/factories.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.factories import ObjectTypeFromDataFactory, FactoryException 2 | from opnsense_cli.code_generators.puppet_code.factory_types import ( 3 | PuppetCodeFragment, 4 | PuppetBoolean, 5 | PuppetChoice, 6 | PuppetChoiceMultiple, 7 | PuppetCsv, 8 | PuppetInteger, 9 | PuppetString, 10 | ) 11 | 12 | 13 | class PuppetCodeFragmentFactory(ObjectTypeFromDataFactory): 14 | _keymap = { 15 | "String": PuppetString, 16 | "Bool": PuppetBoolean, 17 | "Choice": PuppetChoice, 18 | "ChoiceMultiple": PuppetChoiceMultiple, 19 | "IntOrEmptyClick": PuppetInteger, 20 | "Csv": PuppetCsv, 21 | } 22 | 23 | def _get_class(self, key) -> PuppetCodeFragment: 24 | click_option_class = self._keymap.get(key, None) 25 | if not click_option_class: 26 | raise FactoryException(f"Could not find class for {key} in keymap") 27 | 28 | return click_option_class 29 | 30 | def get_type_for_data(self, params, find_uuid_by_column, click_group, click_command) -> PuppetCodeFragment: 31 | param_type = params["type"]["param_type"] 32 | if params["multiple"]: 33 | param_type = f"{param_type}Multiple" 34 | 35 | puppet_code_fragment_type_class = self._get_class(param_type) 36 | 37 | return puppet_code_fragment_type_class(params, find_uuid_by_column, click_group, click_command) 38 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/puppet_code/provider/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/provider/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.puppet_code.provider.template_vars import PuppetProviderTemplateVars 2 | from opnsense_cli.code_generators.puppet_code.base import PuppetCodeGenerator 3 | from opnsense_cli.template_engines.base import TemplateEngine 4 | from opnsense_cli.factories import ObjectTypeFromDataFactory 5 | 6 | 7 | class PuppetProviderCodeGenerator(PuppetCodeGenerator): 8 | def __init__( 9 | self, 10 | template_engine: TemplateEngine, 11 | type_factory: ObjectTypeFromDataFactory, 12 | template, 13 | group, 14 | command, 15 | find_uuid_by_column, 16 | create_command_params, 17 | update_command_params, 18 | ): 19 | super().__init__(create_command_params, type_factory, find_uuid_by_column, group, command) 20 | self._template_engine = template_engine 21 | self._template = template 22 | self._update_command_params = update_command_params 23 | 24 | def _get_template_vars(self): 25 | return PuppetProviderTemplateVars( 26 | click_command=self._click_command, 27 | click_group=self._click_group, 28 | find_uuid_by_column=self._find_uuid_by_column, 29 | translate_json_object_to_puppet_resource=self._get_code_fragment( 30 | "TEMPLATE_PROVIDER_translate_json_object_to_puppet_resource" 31 | ), 32 | translate_puppet_resource_to_command_args=self._get_code_fragment( 33 | "TEMPLATE_PROVIDER_translate_puppet_resource_to_command_args" 34 | ), 35 | ) 36 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/provider/template.rb.j2: -------------------------------------------------------------------------------- 1 | {% set provider_class_name = "Opnsense{g}{c}".format(g=vars.click_group.capitalize(), c=vars.click_command.capitalize()) -%} 2 | # frozen_string_literal: true 3 | 4 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'opnsense_provider')) 5 | 6 | # Implementation for the opnsense_haproxy_frontend type using the Resource API. 7 | class Puppet::Provider::{{ provider_class_name }}::{{ provider_class_name }} < Puppet::Provider::OpnsenseProvider 8 | # @return [void] 9 | def initialize 10 | super 11 | @group = '{{ vars.click_group }}' 12 | @command = '{{ vars.click_command }}' 13 | @resource_type = 'list' 14 | @find_uuid_by_column = :{{ vars.find_uuid_by_column }} 15 | @create_key = :{{ vars.find_uuid_by_column }} 16 | end 17 | 18 | # @param [String] device 19 | # @param [Hash] json_object 20 | def _translate_json_object_to_puppet_resource(device, json_object) 21 | { 22 | title: "#{json_object['{{ vars.find_uuid_by_column }}']}@#{device}", 23 | device: device, 24 | uuid: json_object['uuid'], 25 | {% for line in vars.translate_json_object_to_puppet_resource -%} 26 | {{ line }} 27 | {% endfor %} 28 | ensure: 'present', 29 | } 30 | end 31 | 32 | # @param [Integer] mode 33 | # @param [String] id 34 | # @param [Hash] puppet_resource 35 | # @return [Array] 36 | def _translate_puppet_resource_to_command_args(mode, id, puppet_resource) 37 | args = mode == 'create' ? [@group, @command, mode] : [@group, @command, mode, id] 38 | 39 | {% for line in vars.translate_puppet_resource_to_command_args -%} 40 | {{ line }} 41 | {% endfor %} 42 | args 43 | end 44 | # 45 | private :_translate_json_object_to_puppet_resource, :_translate_puppet_resource_to_command_args 46 | end 47 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/provider/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class PuppetProviderTemplateVars: 7 | click_command: str 8 | click_group: str 9 | find_uuid_by_column: str 10 | translate_json_object_to_puppet_resource: List[str] 11 | translate_puppet_resource_to_command_args: List[str] 12 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/provider_unit_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/puppet_code/provider_unit_test/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/provider_unit_test/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.puppet_code.provider_unit_test.template_vars import PuppetProviderUnitTestTemplateVars 2 | from opnsense_cli.code_generators.puppet_code.base import PuppetCodeGenerator 3 | from opnsense_cli.template_engines.base import TemplateEngine 4 | from opnsense_cli.factories import ObjectTypeFromDataFactory 5 | 6 | 7 | class PuppetProviderUnitTestCodeGenerator(PuppetCodeGenerator): 8 | def __init__( 9 | self, 10 | template_engine: TemplateEngine, 11 | type_factory: ObjectTypeFromDataFactory, 12 | template, 13 | group, 14 | command, 15 | find_uuid_by_column, 16 | create_command_params, 17 | update_command_params, 18 | ): 19 | super().__init__(create_command_params, type_factory, find_uuid_by_column, group, command) 20 | self._template_engine = template_engine 21 | self._template = template 22 | self._update_command_params = update_command_params 23 | 24 | def _get_template_vars(self): 25 | return PuppetProviderUnitTestTemplateVars( 26 | click_command=self._click_command, 27 | click_group=self._click_group, 28 | find_uuid_by_column=self._find_uuid_by_column, 29 | json=self._get_code_fragment("TEMPLATE_PROVIDER_UNIT_TEST_json"), 30 | ruby_hash=self._get_code_fragment("TEMPLATE_PROVIDER_UNIT_TEST_ruby_hash"), 31 | ) 32 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/provider_unit_test/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class PuppetProviderUnitTestTemplateVars: 7 | click_command: str 8 | click_group: str 9 | find_uuid_by_column: str 10 | json: List[str] 11 | ruby_hash: List[str] 12 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/puppet_code/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/tests/test_puppet_code_fragment_factory.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.puppet_code.factories import PuppetCodeFragmentFactory 2 | from opnsense_cli.test_base import BaseTestCase 3 | from opnsense_cli.factories import FactoryException 4 | 5 | 6 | class TestPuppetCodeFragmentTypeFactory(BaseTestCase): 7 | def setUp(self): 8 | self._factory = PuppetCodeFragmentFactory() 9 | self._unknown_key = "unknown_key" 10 | 11 | def test_unknown_key(self): 12 | self.assertRaises(FactoryException, self._factory._get_class, self._unknown_key) 13 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/puppet_code/type/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.puppet_code.type.template_vars import PuppetTypeTemplateVars 2 | from opnsense_cli.code_generators.puppet_code.base import PuppetCodeGenerator 3 | from opnsense_cli.template_engines.base import TemplateEngine 4 | from opnsense_cli.factories import ObjectTypeFromDataFactory 5 | 6 | 7 | class PuppetTypeCodeGenerator(PuppetCodeGenerator): 8 | def __init__( 9 | self, 10 | template_engine: TemplateEngine, 11 | type_factory: ObjectTypeFromDataFactory, 12 | template, 13 | group, 14 | command, 15 | find_uuid_by_column, 16 | create_command_params, 17 | update_command_params, 18 | ): 19 | super().__init__(create_command_params, type_factory, find_uuid_by_column, group, command) 20 | self._template_engine = template_engine 21 | self._template = template 22 | self._update_command_params = update_command_params 23 | 24 | def _get_template_vars(self): 25 | return PuppetTypeTemplateVars( 26 | click_command=self._click_command, 27 | click_group=self._click_group, 28 | find_uuid_by_column=self._find_uuid_by_column, 29 | examples=self._get_code_fragment("TEMPLATE_TYPE_example"), 30 | attributes=self._get_code_fragment("TEMPLATE_TYPE_attributes"), 31 | ) 32 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type/template.rb.j2: -------------------------------------------------------------------------------- 1 | {% set provider_name = "opnsense_{g}_{c}".format(g=vars.click_group, c=vars.click_command) -%} 2 | # frozen_string_literal: true 3 | 4 | require 'puppet/resource_api' 5 | 6 | Puppet::ResourceApi.register_type( 7 | name: '{{ provider_name }}', 8 | docs: <<-EOS, 9 | @summary 10 | Manage opnsense {{ vars.click_group }} {{ vars.click_command }} 11 | 12 | @example 13 | {{ provider_name }} { 'example {{ vars.click_group }} {{ vars.click_command }}': 14 | device => 'opnsense-test.device.com', 15 | {% for line in vars.examples -%} 16 | {{ line }} 17 | {% endfor %} 18 | ensure => 'present', 19 | } 20 | 21 | This type provides Puppet with the capabilities to manage opnsense {{ vars.click_group }} {{ vars.click_command }}. 22 | 23 | EOS 24 | features: ['simple_get_filter'], 25 | title_patterns: [ 26 | { 27 | pattern: %r{^(?<{{ vars.find_uuid_by_column }}>.*)@(?.*)$}, 28 | desc: 'Where the {{ vars.find_uuid_by_column }} and the device are provided with a @', 29 | }, 30 | { 31 | pattern: %r{^(?<{{ vars.find_uuid_by_column }}>.*)$}, 32 | desc: 'Where only the {{ vars.find_uuid_by_column }} is provided', 33 | }, 34 | ], 35 | attributes: { 36 | ensure: { 37 | type: 'Enum[present, absent]', 38 | desc: 'Whether this resource should be present or absent on the target system.', 39 | default: 'present', 40 | }, 41 | device: { 42 | type: 'String', 43 | desc: 'The name of the opnsense_device type you want to manage.', 44 | behaviour: :namevar, 45 | }, 46 | uuid: { 47 | type: 'Optional[String]', 48 | desc: 'The uuid of the rule.', 49 | behaviour: :init_only, 50 | }, 51 | {% for line in vars.attributes -%} 52 | {{ line }} 53 | {% endfor %} 54 | }, 55 | ) 56 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class PuppetTypeTemplateVars: 7 | click_command: str 8 | click_group: str 9 | find_uuid_by_column: str 10 | examples: List[str] 11 | attributes: List[str] 12 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type_unit_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/code_generators/puppet_code/type_unit_test/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type_unit_test/codegenerator.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.code_generators.puppet_code.type_unit_test.template_vars import PuppetTypeUnitTestTemplateVars 2 | from opnsense_cli.code_generators.puppet_code.base import PuppetCodeGenerator 3 | from opnsense_cli.template_engines.base import TemplateEngine 4 | from opnsense_cli.code_generators.puppet_code.factories import PuppetCodeFragmentFactory 5 | 6 | 7 | class PuppetTypeUnitTestCodeGenerator(PuppetCodeGenerator): 8 | def __init__( 9 | self, 10 | template_engine: TemplateEngine, 11 | type_factory: PuppetCodeFragmentFactory, 12 | template, 13 | group, 14 | command, 15 | find_uuid_by_column, 16 | create_command_params, 17 | update_command_params, 18 | ): 19 | super().__init__(create_command_params, type_factory, find_uuid_by_column, group, command) 20 | self._template_engine = template_engine 21 | self._template = template 22 | self._update_command_params = update_command_params 23 | 24 | def _get_template_vars(self): 25 | return PuppetTypeUnitTestTemplateVars( 26 | click_command=self._click_command, 27 | click_group=self._click_group, 28 | find_uuid_by_column=self._find_uuid_by_column, 29 | new_resource=self._get_code_fragment("TEMPLATE_TYPE_UNIT_TEST_new_resource"), 30 | accepts_parameter=self._get_code_fragment("TEMPLATE_TYPE_UNIT_TEST_accepts_parameter"), 31 | ) 32 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type_unit_test/template.rb.j2: -------------------------------------------------------------------------------- 1 | {% set provider_name = "opnsense_{g}_{c}".format(g=vars.click_group, c=vars.click_command) -%} 2 | {% set unit_under_test = "{g}_{c}".format(g=vars.click_group, c=vars.click_command) -%} 3 | # frozen_string_literal: true 4 | 5 | require 'spec_helper' 6 | require 'puppet/type/{{ provider_name }}' 7 | 8 | RSpec.describe 'the {{ provider_name }} type' do 9 | it 'loads' do 10 | expect(Puppet::Type.type(:{{ provider_name }})).not_to be_nil 11 | end 12 | 13 | it 'requires a title' do 14 | expect { 15 | Puppet::Type.type(:{{ provider_name }}).new({}) 16 | }.to raise_error(Puppet::Error, 'Title or name must be provided') 17 | end 18 | 19 | context 'example {{ vars.click_group }} {{ vars.click_command }} on opnsense.example.com' do 20 | let(:{{ unit_under_test }}) do 21 | Puppet::Type.type(:{{ provider_name }}).new( 22 | name: 'example {{ vars.click_group }} {{ vars.click_command }}', 23 | device: 'opnsense.example.com', 24 | {% for line in vars.new_resource -%} 25 | {{ line }} 26 | {% endfor %} 27 | ensure: 'present', 28 | ) 29 | end 30 | 31 | {% for line in vars.accepts_parameter -%} 32 | {{ line }} 33 | 34 | {% endfor %} 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /opnsense_cli/code_generators/puppet_code/type_unit_test/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class PuppetTypeUnitTestTemplateVars: 7 | click_command: str 8 | click_group: str 9 | find_uuid_by_column: str 10 | new_resource: List[str] 11 | accepts_parameter: List[str] 12 | -------------------------------------------------------------------------------- /opnsense_cli/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/completion/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | import textwrap 3 | 4 | 5 | @click.command() 6 | def completion(): 7 | """ 8 | Output instructions for shell completion 9 | """ 10 | instructions = """ 11 | Instructions for shell completion: 12 | 13 | See: https://click.palletsprojects.com/en/latest/shell-completion/ 14 | 15 | Bash (invoked every time a shell is started): 16 | echo '# shell completion for opn-cli' >> ~/.bashrc 17 | echo 'eval "$(_OPN_CLI_COMPLETE=bash_source opn-cli)"' >> ~/.bashrc 18 | 19 | Bash (current shell): 20 | _OPN_CLI_COMPLETE=bash_source opn-cli > ~/.opn-cli/opn-cli-complete.bash 21 | source ~/.opn-cli/opn-cli-complete.bash 22 | 23 | Zsh (invoked every time a shell is started): 24 | echo '# shell completion for opn-cli' >> ~/.zshrc 25 | echo 'eval "$(_OPN_CLI_COMPLETE=zsh_source opn-cli)"' >> ~/.zshrc 26 | 27 | Zsh (current shell): 28 | _OPN_CLI_COMPLETE=zsh_source opn-cli >! ~/.opn-cli/opn-cli-complete.zsh 29 | source ~/.opn-cli/opn-cli-complete.zsh 30 | """ 31 | click.echo(textwrap.dedent(instructions)) 32 | -------------------------------------------------------------------------------- /opnsense_cli/commands/completion/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/completion/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/completion/tests/test_completion.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from click.testing import CliRunner 4 | from opnsense_cli.commands.completion import completion 5 | 6 | 7 | class TestCompletionCommands(unittest.TestCase): 8 | def test_version(self): 9 | runner = CliRunner() 10 | result = runner.invoke(completion) 11 | 12 | self.assertEqual(0, result.exit_code) 13 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/configbackup/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def configbackup(**kwargs): 6 | """ 7 | Manage api-backup operations (OPNsense version >= 24.1) 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/configbackup/backup.py: -------------------------------------------------------------------------------- 1 | import click 2 | from opnsense_cli.formatters.cli_output.cli_output_formatter import CliOutputFormatter 3 | from opnsense_cli.click_addons.callbacks import formatter_from_formatter_name, expand_path, available_formats 4 | from opnsense_cli.commands.core.configbackup import configbackup 5 | from opnsense_cli.api.client import ApiClient 6 | from opnsense_cli.api.core.configbackup import Backup 7 | from opnsense_cli.commands.core.configbackup.services.backup_service import ConfigBackupService 8 | 9 | pass_api_client = click.make_pass_decorator(ApiClient) 10 | pass_apibackup_backup_svc = click.make_pass_decorator(ConfigBackupService) 11 | 12 | 13 | @configbackup.group() 14 | @pass_api_client 15 | @click.pass_context 16 | def backup(ctx, api_client: ApiClient, **kwargs): 17 | """ 18 | Manage api-backup 19 | """ 20 | backup_api = Backup(api_client) 21 | ctx.obj = ConfigBackupService(backup_api) 22 | 23 | 24 | @backup.command() 25 | @click.option( 26 | "-p", 27 | "--path", 28 | help="The target path.", 29 | type=click.Path(dir_okay=False), 30 | default="./config.xml", 31 | is_eager=True, 32 | show_default=True, 33 | callback=expand_path, 34 | show_envvar=True, 35 | required=True, 36 | ) 37 | @click.option( 38 | "--output", 39 | "-o", 40 | help="Specifies the Output format.", 41 | default="plain", 42 | type=click.Choice(available_formats()), 43 | callback=formatter_from_formatter_name, 44 | show_default=True, 45 | ) 46 | @click.option( 47 | "--cols", 48 | "-c", 49 | help="Which columns should be printed? Pass empty string (-c " ") to show all columns", 50 | default="status", 51 | show_default=True, 52 | ) 53 | @pass_apibackup_backup_svc 54 | def download(apibackup_backup_svc: ConfigBackupService, **kwargs): 55 | """ 56 | Download config.xml from OPNsense. 57 | """ 58 | result = apibackup_backup_svc.download_backup(kwargs["path"]) 59 | CliOutputFormatter(result, kwargs["output"], kwargs["cols"].split(",")).echo() 60 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/configbackup/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/configbackup/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/configbackup/services/backup_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.core.configbackup import Backup 2 | from opnsense_cli.commands.service_base import CommandService 3 | 4 | 5 | class ConfigBackupService(CommandService): 6 | def __init__(self, backup_api: Backup): 7 | self._backup_api = backup_api 8 | 9 | def download_backup(self, path): 10 | config = self._backup_api.download("this") 11 | self._write_xml_string_to_file(path, config) 12 | return {"status": f"successfully saved to: {path}"} 13 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/configbackup/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/configbackup/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/configbackup/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/configbackup/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/configbackup/tests/test_configbackup.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | import os 3 | from opnsense_cli.commands.core.configbackup.backup import backup 4 | from opnsense_cli.commands.test_base import CommandTestCase 5 | 6 | 7 | class TestApibackupCommands(CommandTestCase): 8 | def setUp(self): 9 | self._setup_fakefs() 10 | self._api_data_fixtures_download = self._read_fixture_file( 11 | "fixtures/config.xml.sample", base_dir=os.path.dirname(__file__) 12 | ) 13 | self._api_client_args_fixtures = ["api_key", "api_secret", "https://127.0.0.1/api", True, "~/.opn-cli/ca.pem", 60] 14 | 15 | @patch("opnsense_cli.commands.core.configbackup.backup.ApiClient.execute") 16 | def test_download(self, api_response_mock): 17 | result = self._opn_cli_command_result( 18 | api_response_mock, 19 | [ 20 | self._api_data_fixtures_download, 21 | ], 22 | backup, 23 | ["download"], 24 | ) 25 | 26 | self.assertIn("successfully saved to: ./config.xml\n", result.output) 27 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/firewall/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def firewall(**kwargs): 6 | """ 7 | Execute firewall operations 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/firewall/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/firewall/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/firewall/services/firewall_alias_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.plugin.firewall import FirewallAlias, FirewallAliasUtil 2 | from opnsense_cli.commands.exceptions import CommandException 3 | from opnsense_cli.commands.service_base import CommandService 4 | 5 | 6 | class FirewallAliasService(CommandService): 7 | def __init__(self, firewall_alias_api: FirewallAlias, firewall_alias_util_api: FirewallAliasUtil): 8 | self._firewall_alias_api = firewall_alias_api 9 | self._firewall_alias_util_api = firewall_alias_util_api 10 | 11 | def show_pf_table(self, alias_name): 12 | return self._firewall_alias_util_api.list(alias_name)["rows"] 13 | 14 | def create_alias(self, json_payload: dict): 15 | result = self._firewall_alias_api.add_item(json=json_payload) 16 | self._apply(result) 17 | return result 18 | 19 | def update_alias(self, alias_name, json_payload: dict): 20 | uuid = self._get_uuid_for_name(alias_name) 21 | result = self._firewall_alias_api.set_item(uuid, json=json_payload) 22 | self._apply(result) 23 | return result 24 | 25 | def delete_alias(self, alias_name): 26 | uuid = self._get_uuid_for_name(alias_name) 27 | result = self._firewall_alias_api.del_item(uuid) 28 | self._apply(result) 29 | return result 30 | 31 | def show_alias(self, alias_name): 32 | uuid = self._get_uuid_for_name(alias_name) 33 | aliases = self._get_alias_list() 34 | alias = next((item for item in aliases if item["uuid"] == uuid), {}) 35 | return alias 36 | 37 | def _get_uuid_for_name(self, name): 38 | try: 39 | return self._firewall_alias_api.get_uuid_for_name(name).get("uuid", None) 40 | except AttributeError: 41 | return "null" 42 | 43 | def _apply(self, result_admin_action): 44 | if result_admin_action["result"] not in ["saved", "deleted"]: 45 | raise CommandException(result_admin_action) 46 | 47 | result_apply = self._firewall_alias_api.reconfigure() 48 | 49 | if result_apply["status"] != "ok": 50 | raise CommandException(f"Apply failed: {result_apply}") 51 | 52 | def list_aliases(self): 53 | return self._get_alias_list() 54 | 55 | def _get_alias_list(self): 56 | aliases = [] 57 | aliases_raw = self._firewall_alias_api.export()["aliases"]["alias"] 58 | 59 | for alias_uuid, alias_data in aliases_raw.items(): 60 | alias_data.update({"uuid": alias_uuid}) 61 | alias_data["content"] = alias_data["content"].replace("\n", ",") 62 | aliases.append(alias_data) 63 | 64 | aliases = self._sort_dict_by_string(aliases, "name") 65 | 66 | return aliases 67 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/firewall/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/firewall/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/firewall/tests/test_firewall.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from click.testing import CliRunner 3 | from opnsense_cli.commands.core.firewall import firewall 4 | 5 | 6 | class TestFirewallCommands(unittest.TestCase): 7 | def test_firewall(self): 8 | runner = CliRunner() 9 | result = runner.invoke(firewall) 10 | 11 | self.assertEqual(0, result.exit_code) 12 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/ipsec/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def ipsec(**kwargs): 6 | """ 7 | Manage Ipsec 8 | """ 9 | 10 | 11 | @ipsec.group() 12 | def tunnel(**kwargs): 13 | """ 14 | Manage ipsec tunnels 15 | """ 16 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/ipsec/phase2.py: -------------------------------------------------------------------------------- 1 | import click 2 | from opnsense_cli.formatters.cli_output.cli_output_formatter import CliOutputFormatter 3 | from opnsense_cli.click_addons.callbacks import formatter_from_formatter_name, available_formats 4 | from opnsense_cli.commands.core.ipsec import tunnel 5 | from opnsense_cli.api.client import ApiClient 6 | from opnsense_cli.api.core.ipsec import Tunnel 7 | from opnsense_cli.commands.core.ipsec.services.ipsec_tunnel_service import IpsecTunnelService 8 | 9 | pass_api_client = click.make_pass_decorator(ApiClient) 10 | pass_ipsec_tunnel_svc = click.make_pass_decorator(IpsecTunnelService) 11 | 12 | 13 | @tunnel.group() 14 | @pass_api_client 15 | @click.pass_context 16 | def phase2(ctx, api_client: ApiClient, **kwargs): 17 | """ 18 | Manage ipsec phase 2 tunnels 19 | """ 20 | tunnel_api = Tunnel(api_client) 21 | ctx.obj = IpsecTunnelService(tunnel_api) 22 | 23 | 24 | @phase2.command() 25 | @click.option( 26 | "--output", 27 | "-o", 28 | help="Specifies the Output format.", 29 | default="table", 30 | type=click.Choice(available_formats()), 31 | callback=formatter_from_formatter_name, 32 | show_default=True, 33 | ) 34 | @click.option( 35 | "--cols", 36 | "-c", 37 | help="Which columns should be printed? Pass empty string (-c " ") to show all columns", 38 | default=("id,uniqid,ikeid,reqid,enabled,protocol,mode,local_subnet,remote_subnet,proposal,description"), 39 | show_default=True, 40 | ) 41 | @pass_ipsec_tunnel_svc 42 | def list(ipsec_tunnel_svc: IpsecTunnelService, **kwargs): 43 | """ 44 | Show all ipsec phase2 tunnels 45 | """ 46 | 47 | result = ipsec_tunnel_svc.list_phase2_tunnels() 48 | 49 | CliOutputFormatter(result, kwargs["output"], kwargs["cols"].split(",")).echo() 50 | 51 | 52 | @phase2.command() 53 | @click.argument("uniqid") 54 | @click.option( 55 | "--output", 56 | "-o", 57 | help="Specifies the Output format.", 58 | default="table", 59 | type=click.Choice(available_formats()), 60 | callback=formatter_from_formatter_name, 61 | show_default=True, 62 | ) 63 | @click.option( 64 | "--cols", 65 | "-c", 66 | help="Which columns should be printed? Pass empty string (-c " ") to show all columns", 67 | default=("id,uniqid,ikeid,reqid,enabled,protocol,mode,local_subnet,remote_subnet,proposal,description"), 68 | show_default=True, 69 | ) 70 | @pass_ipsec_tunnel_svc 71 | def show(ipsec_tunnel_svc: IpsecTunnelService, **kwargs): 72 | """ 73 | Show details for phase 2 tunnel 74 | """ 75 | result = ipsec_tunnel_svc.show_phase2_tunnels(kwargs["uniqid"]) 76 | 77 | CliOutputFormatter(result, kwargs["output"], kwargs["cols"].split(",")).echo() 78 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/ipsec/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/ipsec/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/ipsec/services/ipsec_tunnel_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.core.ipsec import Tunnel 2 | from opnsense_cli.commands.service_base import CommandService 3 | 4 | 5 | class IpsecTunnelService(CommandService): 6 | def __init__(self, tunnel_api: Tunnel): 7 | super().__init__() 8 | self._complete_model_data_cache = None 9 | self._tunnel_api = tunnel_api 10 | 11 | def list_phase1_tunnels(self): 12 | return self._tunnel_api.searchPhase1()["rows"] 13 | 14 | def show_phase1_tunnels(self, ikeid): 15 | phase1_entries = self._tunnel_api.searchPhase1()["rows"] 16 | entry = [item for item in phase1_entries if item["id"] == int(ikeid)] 17 | 18 | return entry 19 | 20 | def list_phase2_tunnels(self): 21 | return self._get_all_phase2_entries() 22 | 23 | def show_phase2_tunnels(self, uniqid): 24 | phase2_entries = self._get_all_phase2_entries() 25 | entry = [item for item in phase2_entries if item["uniqid"] == uniqid] 26 | 27 | return entry 28 | 29 | def _get_all_phase2_entries(self): 30 | results = [] 31 | all_ikeids = self._get_all_ikeids() 32 | 33 | for ikeid in all_ikeids: 34 | entries = self._tunnel_api.searchPhase2(json={"ikeid": ikeid})["rows"] 35 | for entry in entries: 36 | if entry: 37 | results.append(entry) 38 | 39 | return results 40 | 41 | def _get_all_ikeids(self): 42 | phase1_entries = self._tunnel_api.searchPhase1()["rows"] 43 | 44 | return [phase1_entry["id"] for phase1_entry in phase1_entries] 45 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/ipsec/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/ipsec/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/openvpn/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/openvpn/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/plugin/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/plugin/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/plugin/services/plugin.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from opnsense_cli.commands.service_base import CommandService 4 | from opnsense_cli.api.core.firmware import Firmware 5 | from opnsense_cli.commands.exceptions import CommandException 6 | 7 | 8 | class PluginService(CommandService): 9 | def __init__(self, _firmware_api: Firmware, _query_upgrade_status_interval): 10 | self._firmware_api = _firmware_api 11 | self._query_upgrade_status_interval = _query_upgrade_status_interval 12 | 13 | def plugin_list(self): 14 | return self._list_plugins() 15 | 16 | def plugin_installed(self): 17 | all_plugins = self._list_plugins() 18 | installed_plugins = [plugin for plugin in all_plugins if plugin["installed"] == "1"] 19 | return installed_plugins 20 | 21 | def _list_plugins(self): 22 | return self._firmware_api.info()["plugin"] 23 | 24 | def plugin_show(self, name): 25 | return self._firmware_api.details(name) 26 | 27 | def plugin_install(self, name): 28 | self._firmware_api.install(name) 29 | return self.get_last_upgrade_status() 30 | 31 | def plugin_uninstall(self, name): 32 | self._firmware_api.remove(name) 33 | return self.get_last_upgrade_status() 34 | 35 | def plugin_reinstall(self, name): 36 | self._firmware_api.reinstall(name) 37 | return self.get_last_upgrade_status() 38 | 39 | def plugin_lock(self, name): 40 | self._firmware_api.lock(name) 41 | return self.get_last_upgrade_status() 42 | 43 | def plugin_unlock(self, name): 44 | self._firmware_api.unlock(name) 45 | return self.get_last_upgrade_status() 46 | 47 | def get_last_upgrade_status(self): 48 | while True: 49 | log = self._firmware_api.upgradestatus() 50 | self._upgrade_status_error_handling(log) 51 | if log["status"] != "running": 52 | return log 53 | time.sleep(self._query_upgrade_status_interval) 54 | 55 | def _upgrade_status_error_handling(self, log): 56 | if "No packages available to install" in log["log"]: 57 | log["status"] = "error" 58 | raise CommandException(log) 59 | 60 | if "No packages matched for pattern" in log["log"]: 61 | log["status"] = "not found" 62 | 63 | if "is not installed" in log["log"]: 64 | log["status"] = "not found" 65 | 66 | if "***GOT REQUEST TO LOCK***\n***DONE***" in log["log"]: 67 | log["status"] = "not found" 68 | 69 | if "***GOT REQUEST TO UNLOCK***\n***DONE***" in log["log"]: 70 | log["status"] = "not found" 71 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/plugin/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/plugin/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def route(**kwargs): 6 | """ 7 | Manage routes 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/gateway.py: -------------------------------------------------------------------------------- 1 | import click 2 | from opnsense_cli.formatters.cli_output.cli_output_formatter import CliOutputFormatter 3 | from opnsense_cli.click_addons.callbacks import formatter_from_formatter_name, available_formats 4 | from opnsense_cli.commands.core.route import route 5 | from opnsense_cli.api.client import ApiClient 6 | from opnsense_cli.api.core.routes import Gateway 7 | from opnsense_cli.commands.core.route.services.route_gateway_service import RoutesGatewayService 8 | 9 | pass_api_client = click.make_pass_decorator(ApiClient) 10 | pass_routes_gateway_svc = click.make_pass_decorator(RoutesGatewayService) 11 | 12 | 13 | @route.group() 14 | @pass_api_client 15 | @click.pass_context 16 | def gateway(ctx, api_client: ApiClient, **kwargs): 17 | """ 18 | Manage gateway routes 19 | """ 20 | gateway_api = Gateway(api_client) 21 | ctx.obj = RoutesGatewayService(gateway_api) 22 | 23 | 24 | @gateway.command() 25 | @click.option( 26 | "--output", 27 | "-o", 28 | help="Specifies the Output format.", 29 | default="table", 30 | type=click.Choice(available_formats()), 31 | callback=formatter_from_formatter_name, 32 | show_default=True, 33 | ) 34 | @click.option( 35 | "--cols", 36 | "-c", 37 | help="Which columns should be printed? Pass empty string (-c " ") to show all columns", 38 | default=("name,address,status,status_translated,loss,stddev,delay"), 39 | show_default=True, 40 | ) 41 | @pass_routes_gateway_svc 42 | def status(routes_gateway_svc: RoutesGatewayService, **kwargs): 43 | """ 44 | Show gateway states 45 | """ 46 | result = routes_gateway_svc.show_status() 47 | 48 | CliOutputFormatter(result, kwargs["output"], kwargs["cols"].split(",")).echo() 49 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/route/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/services/route_gateway_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.service_base import CommandService 2 | from opnsense_cli.api.core.routes import Gateway 3 | 4 | 5 | class RoutesGatewayService(CommandService): 6 | def __init__(self, gateway_api: Gateway): 7 | super().__init__() 8 | self._gateway_api = gateway_api 9 | 10 | def show_status(self): 11 | return self._gateway_api.status()["items"] 12 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/services/route_static_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.exceptions import CommandException 2 | from opnsense_cli.commands.service_base import CommandService 3 | from opnsense_cli.api.core.routes import Routes, Gateway 4 | 5 | 6 | class RoutesStaticService(CommandService): 7 | jsonpath_base = "$.route.route" 8 | uuid_resolver_map = {} 9 | 10 | def __init__(self, routes_api: Routes, gateway_api: Gateway): 11 | super().__init__() 12 | self._complete_model_data_cache = None 13 | self._settings_api = routes_api 14 | self._gateway_api = gateway_api 15 | 16 | def list_statics(self): 17 | return self._get_statics_list() 18 | 19 | def show_static(self, uuid): 20 | statics = self._get_statics_list() 21 | static = next((item for item in statics if item["uuid"] == uuid), {}) 22 | return static 23 | 24 | def _get_statics_list(self): 25 | return self._api_mutable_model_get( 26 | self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map, sort_by="uuid" 27 | ) 28 | 29 | def create_static(self, json_payload: dict): 30 | result = self._settings_api.addroute(json=json_payload) 31 | self._apply(result) 32 | return result 33 | 34 | def update_static(self, uuid, json_payload: dict): 35 | result = self._settings_api.setroute(uuid, json=json_payload) 36 | self._apply(result) 37 | return result 38 | 39 | def delete_static(self, uuid): 40 | result = self._settings_api.delroute(uuid) 41 | self._apply(result) 42 | return result 43 | 44 | def _apply(self, result_admin_action): 45 | if result_admin_action["result"] not in ["saved", "deleted"]: 46 | raise CommandException(result_admin_action) 47 | 48 | result_apply = self._settings_api.reconfigure() 49 | 50 | if result_apply["status"] != "ok": 51 | raise CommandException(f"Apply failed: {result_apply}") 52 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/route/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/route/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/tests/fixtures/model_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "route": { 3 | "route": { 4 | "7905f696-4692-47aa-b39f-1a8cda5d60c1": { 5 | "network": "10.0.0.98/24", 6 | "gateway": { 7 | "Null4": { 8 | "value": "Null4 - 127.0.0.1", 9 | "selected": 0 10 | }, 11 | "Null6": { 12 | "value": "Null6 - ::1", 13 | "selected": 0 14 | }, 15 | "WAN_DHCP": { 16 | "value": "WAN_DHCP - 10.0.2.2", 17 | "selected": 1 18 | }, 19 | "WAN_DHCP6": { 20 | "value": "WAN_DHCP6 - inet6", 21 | "selected": 0 22 | } 23 | }, 24 | "descr": "Test route 2", 25 | "disabled": "0" 26 | }, 27 | "3943fccd-d727-4081-af2f-af5fa6bfc09d": { 28 | "network": "10.50.2.2/24", 29 | "gateway": { 30 | "Null4": { 31 | "value": "Null4 - 127.0.0.1", 32 | "selected": 1 33 | }, 34 | "Null6": { 35 | "value": "Null6 - ::1", 36 | "selected": 0 37 | }, 38 | "WAN_DHCP": { 39 | "value": "WAN_DHCP - 10.0.2.2", 40 | "selected": 0 41 | }, 42 | "WAN_DHCP6": { 43 | "value": "WAN_DHCP6 - inet6", 44 | "selected": 0 45 | } 46 | }, 47 | "descr": "Test route 1", 48 | "disabled": "0" 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/route/tests/test_routes_gateway.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch, Mock 2 | from opnsense_cli.commands.core.route.gateway import gateway 3 | from opnsense_cli.commands.test_base import CommandTestCase 4 | 5 | 6 | class TestRoutesStaticCommands(CommandTestCase): 7 | def setUp(self): 8 | self._api_data_fixtures_list_EMPTY = {"items": [], "status": "ok"} 9 | self._api_data_fixtures_list = { 10 | "items": [ 11 | { 12 | "name": "WAN_DHCP6", 13 | "address": "~", 14 | "status": "none", 15 | "status_translated": "Online", 16 | "loss": "~", 17 | "stddev": "~", 18 | "delay": "~", 19 | }, 20 | { 21 | "name": "WAN_DHCP", 22 | "address": "10.0.2.2", 23 | "status": "none", 24 | "status_translated": "Online", 25 | "loss": "~", 26 | "stddev": "~", 27 | "delay": "~", 28 | }, 29 | ], 30 | "status": "ok", 31 | } 32 | self._api_client_args_fixtures = ["api_key", "api_secret", "https://127.0.0.1/api", True, "~/.opn-cli/ca.pem", 60] 33 | 34 | @patch("opnsense_cli.commands.core.route.static.ApiClient.execute") 35 | def test_status(self, api_response_mock: Mock): 36 | result = self._opn_cli_command_result( 37 | api_response_mock, 38 | [ 39 | self._api_data_fixtures_list, 40 | ], 41 | gateway, 42 | ["status", "-o", "plain", "-c", "name,address,status,status_translated,loss,stddev,delay"], 43 | ) 44 | 45 | self.assertIn(("WAN_DHCP6 ~ none Online ~ ~ ~\n" "WAN_DHCP 10.0.2.2 none Online ~ ~ ~\n"), result.output) 46 | 47 | @patch("opnsense_cli.commands.core.route.static.ApiClient.execute") 48 | def test_show_EMPTY(self, api_response_mock: Mock): 49 | result = self._opn_cli_command_result( 50 | api_response_mock, 51 | [ 52 | self._api_data_fixtures_list_EMPTY, 53 | ], 54 | gateway, 55 | ["list", "-o", "plain"], 56 | ) 57 | 58 | self.assertIn("", result.output) 59 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/syslog/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def syslog(**kwargs): 6 | """ 7 | Manage syslog 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/syslog/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/syslog/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/syslog/services/syslog_destination_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.exceptions import CommandException 2 | from opnsense_cli.commands.service_base import CommandService 3 | from opnsense_cli.api.core.syslog import Settings, Service 4 | 5 | 6 | class SyslogDestinationService(CommandService): 7 | jsonpath_base = "$.syslog.destinations.destination" 8 | uuid_resolver_map = {} 9 | 10 | def __init__(self, settings_api: Settings, service_api: Service): 11 | super().__init__() 12 | self._complete_model_data_cache = None 13 | self._settings_api = settings_api 14 | self._service_api = service_api 15 | 16 | def list_destinations(self): 17 | return self._get_destinations_list() 18 | 19 | def show_destination(self, uuid): 20 | destinations = self._get_destinations_list() 21 | destination = next((item for item in destinations if item["uuid"] == uuid), {}) 22 | return destination 23 | 24 | def _get_destinations_list(self): 25 | return self._api_mutable_model_get( 26 | self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map, sort_by="uuid" 27 | ) 28 | 29 | def create_destination(self, json_payload: dict): 30 | result = self._settings_api.addDestination(json=json_payload) 31 | self._apply(result) 32 | return result 33 | 34 | def update_destination(self, uuid, json_payload: dict): 35 | result = self._settings_api.setDestination(uuid, json=json_payload) 36 | self._apply(result) 37 | return result 38 | 39 | def delete_destination(self, uuid): 40 | result = self._settings_api.delDestination(uuid) 41 | self._apply(result) 42 | return result 43 | 44 | def _apply(self, result_admin_action): 45 | if result_admin_action["result"] not in ["saved", "deleted"]: 46 | raise CommandException(result_admin_action) 47 | 48 | result_apply = self._service_api.reconfigure() 49 | 50 | if result_apply["status"] != "ok": 51 | raise CommandException(f"Apply failed: {result_apply}") 52 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/syslog/services/syslog_stats_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.service_base import CommandService 2 | from opnsense_cli.api.core.syslog import Service 3 | 4 | 5 | class SyslogStatsService(CommandService): 6 | def __init__(self, service_api: Service): 7 | super().__init__() 8 | self._service_api = service_api 9 | 10 | def show_stats(self, search): 11 | stats_output = self._service_api.stats()["rows"] 12 | 13 | if search: 14 | return self._search_list_of_dicts_by_val(stats_output, search) 15 | 16 | return stats_output 17 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/syslog/stats.py: -------------------------------------------------------------------------------- 1 | import click 2 | from opnsense_cli.formatters.cli_output.cli_output_formatter import CliOutputFormatter 3 | from opnsense_cli.click_addons.callbacks import formatter_from_formatter_name, available_formats 4 | from opnsense_cli.commands.core.syslog import syslog 5 | from opnsense_cli.api.client import ApiClient 6 | from opnsense_cli.api.core.syslog import Service 7 | from opnsense_cli.commands.core.syslog.services.syslog_stats_service import SyslogStatsService 8 | 9 | pass_api_client = click.make_pass_decorator(ApiClient) 10 | pass_syslog_stats_svc = click.make_pass_decorator(SyslogStatsService) 11 | 12 | 13 | @syslog.group() 14 | @pass_api_client 15 | @click.pass_context 16 | def stats(ctx, api_client: ApiClient, **kwargs): 17 | """ 18 | Show syslog stats 19 | """ 20 | service_api = Service(api_client) 21 | ctx.obj = SyslogStatsService(service_api) 22 | 23 | 24 | @stats.command() 25 | @click.option( 26 | "--search", 27 | help=("Search for this string"), 28 | show_default=True, 29 | default=None, 30 | required=False, 31 | ) 32 | @click.option( 33 | "--output", 34 | "-o", 35 | help="Specifies the Output format.", 36 | default="table", 37 | type=click.Choice(available_formats()), 38 | callback=formatter_from_formatter_name, 39 | show_default=True, 40 | ) 41 | @click.option( 42 | "--cols", 43 | "-c", 44 | help="Which columns should be printed? Pass empty string (-c " ") to show all columns", 45 | default=("#,Description,SourceName,SourceId,SourceInstance,State,Type,Number"), 46 | show_default=True, 47 | ) 48 | @pass_syslog_stats_svc 49 | def list(syslog_stats_svc: SyslogStatsService, **kwargs): 50 | """ 51 | Show syslog statistics 52 | """ 53 | result = syslog_stats_svc.show_stats(kwargs["search"]) 54 | 55 | CliOutputFormatter(result, kwargs["output"], kwargs["cols"].split(",")).echo() 56 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/syslog/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/syslog/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/syslog/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/syslog/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/unbound/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def unbound(**kwargs): 6 | """ 7 | Manage Unbound DNS 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/unbound/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/unbound/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/unbound/services/unbound_alias_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.exceptions import CommandException 2 | from opnsense_cli.commands.service_base import CommandService 3 | from opnsense_cli.api.core.unbound import Settings, Service 4 | 5 | 6 | class UnboundAliasService(CommandService): 7 | jsonpath_base = "$.unbound.aliases.alias" 8 | uuid_resolver_map = dict( 9 | host={ 10 | "template": "$.unbound.hosts.host[{uuids}].hostname,domain,rr,mxprio,mx,server", 11 | "insert_as_key": "Host", 12 | "join_by": "|", 13 | }, 14 | ) 15 | 16 | def __init__(self, settings_api: Settings, service_api: Service): 17 | super().__init__() 18 | self._complete_model_data_cache = None 19 | self._settings_api = settings_api 20 | self._service_api = service_api 21 | 22 | def list_aliass(self): 23 | return self._get_aliass_list() 24 | 25 | def show_alias(self, uuid): 26 | aliass = self._get_aliass_list() 27 | alias = next((item for item in aliass if item["uuid"] == uuid), {}) 28 | return alias 29 | 30 | def _get_aliass_list(self): 31 | return self._api_mutable_model_get( 32 | self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map, sort_by="uuid" 33 | ) 34 | 35 | def create_alias(self, json_payload: dict): 36 | result = self._settings_api.addHostAlias(json=json_payload) 37 | self._apply(result) 38 | return result 39 | 40 | def update_alias(self, uuid, json_payload: dict): 41 | result = self._settings_api.setHostAlias(uuid, json=json_payload) 42 | self._apply(result) 43 | return result 44 | 45 | def delete_alias(self, uuid): 46 | result = self._settings_api.delHostAlias(uuid) 47 | self._apply(result) 48 | return result 49 | 50 | def _apply(self, result_admin_action): 51 | if result_admin_action["result"] not in ["saved", "deleted"]: 52 | raise CommandException(result_admin_action) 53 | 54 | result_apply = self._service_api.reconfigure() 55 | 56 | if result_apply["status"] != "ok": 57 | raise CommandException(f"Apply failed: {result_apply}") 58 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/unbound/services/unbound_domain_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.exceptions import CommandException 2 | from opnsense_cli.commands.service_base import CommandService 3 | from opnsense_cli.api.core.unbound import Settings, Service 4 | 5 | 6 | class UnboundDomainService(CommandService): 7 | jsonpath_base = "$.unbound.domains.domain" 8 | uuid_resolver_map = {} 9 | 10 | def __init__(self, settings_api: Settings, service_api: Service): 11 | super().__init__() 12 | self._complete_model_data_cache = None 13 | self._settings_api = settings_api 14 | self._service_api = service_api 15 | 16 | def list_domains(self): 17 | return self._get_domains_list() 18 | 19 | def show_domain(self, uuid): 20 | domains = self._get_domains_list() 21 | domain = next((item for item in domains if item["uuid"] == uuid), {}) 22 | return domain 23 | 24 | def _get_domains_list(self): 25 | return self._api_mutable_model_get( 26 | self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map, sort_by="uuid" 27 | ) 28 | 29 | def create_domain(self, json_payload: dict): 30 | result = self._settings_api.addDomainOverride(json=json_payload) 31 | self._apply(result) 32 | return result 33 | 34 | def update_domain(self, uuid, json_payload: dict): 35 | result = self._settings_api.setDomainOverride(uuid, json=json_payload) 36 | self._apply(result) 37 | return result 38 | 39 | def delete_domain(self, uuid): 40 | result = self._settings_api.delDomainOverride(uuid) 41 | self._apply(result) 42 | return result 43 | 44 | def _apply(self, result_admin_action): 45 | if result_admin_action["result"] not in ["saved", "deleted"]: 46 | raise CommandException(result_admin_action) 47 | 48 | result_apply = self._service_api.reconfigure() 49 | 50 | if result_apply["status"] != "ok": 51 | raise CommandException(f"Apply failed: {result_apply}") 52 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/unbound/services/unbound_host_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.exceptions import CommandException 2 | from opnsense_cli.commands.service_base import CommandService 3 | from opnsense_cli.api.core.unbound import Settings, Service 4 | 5 | 6 | class UnboundHostService(CommandService): 7 | jsonpath_base = "$.unbound.hosts.host" 8 | uuid_resolver_map = {} 9 | 10 | def __init__(self, settings_api: Settings, service_api: Service): 11 | super().__init__() 12 | self._complete_model_data_cache = None 13 | self._settings_api = settings_api 14 | self._service_api = service_api 15 | 16 | def list_hosts(self): 17 | return self._get_hosts_list() 18 | 19 | def show_host(self, uuid): 20 | hosts = self._get_hosts_list() 21 | host = next((item for item in hosts if item["uuid"] == uuid), {}) 22 | return host 23 | 24 | def _get_hosts_list(self): 25 | return self._api_mutable_model_get( 26 | self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map, sort_by="uuid" 27 | ) 28 | 29 | def create_host(self, json_payload: dict): 30 | result = self._settings_api.addHostOverride(json=json_payload) 31 | self._apply(result) 32 | return result 33 | 34 | def update_host(self, uuid, json_payload: dict): 35 | result = self._settings_api.setHostOverride(uuid, json=json_payload) 36 | self._apply(result) 37 | return result 38 | 39 | def delete_host(self, uuid): 40 | result = self._settings_api.delHostOverride(uuid) 41 | self._apply(result) 42 | return result 43 | 44 | def _apply(self, result_admin_action): 45 | if result_admin_action["result"] not in ["saved", "deleted"]: 46 | raise CommandException(result_admin_action) 47 | 48 | result_apply = self._service_api.reconfigure() 49 | 50 | if result_apply["status"] != "ok": 51 | raise CommandException(f"Apply failed: {result_apply}") 52 | -------------------------------------------------------------------------------- /opnsense_cli/commands/core/unbound/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/unbound/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/core/unbound/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/core/unbound/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/exceptions.py: -------------------------------------------------------------------------------- 1 | from click import ClickException 2 | 3 | 4 | class CommandException(ClickException): 5 | pass 6 | -------------------------------------------------------------------------------- /opnsense_cli/commands/new/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def new(**kwargs): 6 | """ 7 | Generate scaffolding code 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/new/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/new/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/fixtures/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/new/tests/fixtures/api/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/fixtures/api/core.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
MethodModuleControllerCommandParameters
POSTcronservicereconfigure 
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
MethodModuleControllerCommandParameters
POSTcronsettingsaddJob 
POSTcronsettingsdelJob$uuid
GETcronsettingsget 
GETcronsettingsgetJob$uuid=null
*cronsettingssearchJobs 
GETcronsettingsset 
POSTcronsettingssetJob$uuid
POSTcronsettingstoggleJob$uuid,$enabled=null
     
<<uses>>   model Cron.xml
98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/fixtures/opn_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/new/tests/fixtures/opn_cli/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/fixtures/opn_cli/core_form.xml: -------------------------------------------------------------------------------- 1 |
2 | 3 | category.auto 4 | 5 | checkbox 6 | Automatically added, will be removed when unused 7 | 8 | 9 | category.name 10 | 11 | text 12 | Enter a name for this category. 13 | 14 | 15 | category.color 16 | 17 | text 18 | 19 | pick a color to use. 20 | 21 |
22 | -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/fixtures/opn_cli/core_model.xml: -------------------------------------------------------------------------------- 1 | 2 | //OPNsense/Firewall/Category 3 | 1.0.0 4 | 5 | Firewall categories 6 | 7 | 8 | 9 | 10 | 11 | Y 12 | 13 | 14 | A category with this name already exists. 15 | UniqueConstraint 16 | 17 | 18 | 19 | 20 | 0 21 | 22 | 23 | N 24 | /^([0-9a-fA-F]){6,6}$/u 25 | A valid color code consists of 6 hex digits 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /opnsense_cli/commands/new/tests/test_new.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from click.testing import CliRunner 3 | from opnsense_cli.commands.new import new 4 | 5 | 6 | class TestNewCommands(unittest.TestCase): 7 | def test_new(self): 8 | runner = CliRunner() 9 | result = runner.invoke(new) 10 | 11 | self.assertEqual(0, result.exit_code) 12 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/apibackup/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def apibackup(**kwargs): 6 | """ 7 | Manage api-backup operations (OPNsense version <= 23.7) 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/apibackup/backup.py: -------------------------------------------------------------------------------- 1 | import click 2 | from opnsense_cli.formatters.cli_output.cli_output_formatter import CliOutputFormatter 3 | from opnsense_cli.click_addons.callbacks import formatter_from_formatter_name, expand_path, available_formats 4 | from opnsense_cli.commands.plugin.apibackup import apibackup 5 | from opnsense_cli.api.client import ApiClient 6 | from opnsense_cli.api.plugin.apibackup import Backup 7 | from opnsense_cli.commands.plugin.apibackup.services.api_backup_service import ApibackupBackupService 8 | 9 | pass_api_client = click.make_pass_decorator(ApiClient) 10 | pass_apibackup_backup_svc = click.make_pass_decorator(ApibackupBackupService) 11 | 12 | 13 | @apibackup.group() 14 | @pass_api_client 15 | @click.pass_context 16 | def backup(ctx, api_client: ApiClient, **kwargs): 17 | """ 18 | Manage api-backup 19 | """ 20 | backup_api = Backup(api_client) 21 | ctx.obj = ApibackupBackupService(backup_api) 22 | 23 | 24 | @backup.command() 25 | @click.option( 26 | "-p", 27 | "--path", 28 | help="The target path.", 29 | type=click.Path(dir_okay=False), 30 | default="./config.xml", 31 | is_eager=True, 32 | show_default=True, 33 | callback=expand_path, 34 | show_envvar=True, 35 | required=True, 36 | ) 37 | @click.option( 38 | "--output", 39 | "-o", 40 | help="Specifies the Output format.", 41 | default="plain", 42 | type=click.Choice(available_formats()), 43 | callback=formatter_from_formatter_name, 44 | show_default=True, 45 | ) 46 | @click.option( 47 | "--cols", 48 | "-c", 49 | help="Which columns should be printed? Pass empty string (-c " ") to show all columns", 50 | default="status", 51 | show_default=True, 52 | ) 53 | @pass_apibackup_backup_svc 54 | def download(apibackup_backup_svc: ApibackupBackupService, **kwargs): 55 | """ 56 | Download config.xml from OPNsense. 57 | """ 58 | result = apibackup_backup_svc.download_backup(kwargs["path"]) 59 | CliOutputFormatter(result, kwargs["output"], kwargs["cols"].split(",")).echo() 60 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/apibackup/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/apibackup/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/apibackup/services/api_backup_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.plugin.apibackup import Backup 2 | from opnsense_cli.commands.service_base import CommandService 3 | 4 | 5 | class ApibackupBackupService(CommandService): 6 | def __init__(self, backup_api: Backup): 7 | self._backup_api = backup_api 8 | 9 | def download_backup(self, path): 10 | config = self._backup_api.download("json") 11 | self._write_base64_string_to_zipfile(path, config["content"]) 12 | return {"status": f"successfully saved to: {path}"} 13 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/apibackup/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/apibackup/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/apibackup/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/apibackup/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/apibackup/tests/test_apibackup.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | from opnsense_cli.commands.plugin.apibackup.backup import backup 3 | from opnsense_cli.commands.test_base import CommandTestCase 4 | import base64 5 | import os 6 | 7 | 8 | class TestApibackupCommands(CommandTestCase): 9 | def setUp(self): 10 | self._setup_fakefs() 11 | 12 | self._config_xml = self._read_fixture_file("fixtures/config.xml.sample", base_dir=os.path.dirname(__file__)) 13 | self._api_data_fixtures_download = { 14 | "status": "success", 15 | "filename": "config.xml", 16 | "filetype": "application/xml", 17 | "content": base64.b64encode(bytes(self._config_xml, "utf-8)")), 18 | } 19 | 20 | self._api_client_args_fixtures = ["api_key", "api_secret", "https://127.0.0.1/api", True, "~/.opn-cli/ca.pem", 60] 21 | 22 | @patch("opnsense_cli.commands.plugin.apibackup.backup.ApiClient.execute") 23 | def test_download(self, api_response_mock): 24 | result = self._opn_cli_command_result( 25 | api_response_mock, 26 | [ 27 | self._api_data_fixtures_download, 28 | ], 29 | backup, 30 | ["download"], 31 | ) 32 | 33 | self.assertIn("successfully saved to: ./config.xml\n", result.output) 34 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def haproxy(**kwargs): 6 | """ 7 | Manage haproxy loadbalancer operations 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/haproxy/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/base.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.exceptions import CommandException 2 | from opnsense_cli.commands.service_base import CommandService 3 | 4 | 5 | class HaproxyService(CommandService): 6 | def __init__(self): 7 | super().__init__() 8 | 9 | def _apply(self, result_admin_action=None): 10 | if result_admin_action and result_admin_action["result"] not in ["saved", "deleted"]: 11 | raise CommandException(result_admin_action) 12 | 13 | result_config_test = self._service_api.configtest() 14 | if result_config_test["result"].find("Configuration file is valid") == -1: 15 | raise CommandException(f"Configtest failed: {result_config_test}") 16 | 17 | result_apply = self._service_api.reconfigure() 18 | if result_apply["status"] != "ok": 19 | raise CommandException(f"Apply failed: {result_apply}") 20 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_acl_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyAclService(HaproxyService): 6 | jsonpath_base = "$.haproxy.acls.acl" 7 | uuid_resolver_map = { 8 | "nbsrv_backend": {"template": "$.haproxy.backends.backend[{uuids}].name", "insert_as_key": "BackendNrSrv"}, 9 | "queryBackend": {"template": "$.haproxy.backends.backend[{uuids}].name", "insert_as_key": "BackendQuery"}, 10 | "allowedUsers": {"template": "$.haproxy.users.user[{uuids}].name", "insert_as_key": "Users"}, 11 | "allowedGroups": {"template": "$.haproxy.groups.group[{uuids}].name", "insert_as_key": "Groups"}, 12 | } 13 | 14 | def __init__(self, settings_api: Settings, service_api: Service): 15 | super().__init__() 16 | self._complete_model_data_cache = None 17 | self._settings_api = settings_api 18 | self._service_api = service_api 19 | 20 | def list_acls(self): 21 | return self._get_acls_list() 22 | 23 | def show_acl(self, uuid): 24 | acls = self._get_acls_list() 25 | acl = next((item for item in acls if item["uuid"] == uuid), {}) 26 | return acl 27 | 28 | def _get_acls_list(self): 29 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 30 | 31 | def create_acl(self, json_payload: dict): 32 | result = self._settings_api.addAcl(json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def update_acl(self, uuid, json_payload: dict): 37 | result = self._settings_api.setAcl(uuid, json=json_payload) 38 | self._apply(result) 39 | return result 40 | 41 | def delete_acl(self, uuid): 42 | result = self._settings_api.delAcl(uuid) 43 | self._apply(result) 44 | return result 45 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_action_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyActionService(HaproxyService): 6 | jsonpath_base = "$.haproxy.actions.action" 7 | uuid_resolver_map = { 8 | "linkedAcls": {"template": "$.haproxy.acls.acl[{uuids}].name", "insert_as_key": "Acls"}, 9 | "use_backend": {"template": "$.haproxy.backends.backend[{uuids}].name", "insert_as_key": "Backend"}, 10 | "use_server": {"template": "$.haproxy.servers.server[{uuids}].name", "insert_as_key": "Server"}, 11 | "useBackend": {"template": "$.haproxy.backends.backend[{uuids}].name", "insert_as_key": "Backends"}, 12 | "useServer": {"template": "$.haproxy.servers.server[{uuids}].name", "insert_as_key": "Servers"}, 13 | "map_use_backend_file": {"template": "$.haproxy.mapfiles.mapfile[{uuids}].name", "insert_as_key": "Mapfile"}, 14 | "map_use_backend_default": {"template": "$.haproxy.backends.backend[{uuids}].name", "insert_as_key": "BackendDefault"}, 15 | } 16 | 17 | def __init__(self, settings_api: Settings, service_api: Service): 18 | super().__init__() 19 | self._complete_model_data_cache = None 20 | self._settings_api = settings_api 21 | self._service_api = service_api 22 | 23 | def list_actions(self): 24 | return self._get_actions_list() 25 | 26 | def show_action(self, uuid): 27 | actions = self._get_actions_list() 28 | action = next((item for item in actions if item["uuid"] == uuid), {}) 29 | return action 30 | 31 | def _get_actions_list(self): 32 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 33 | 34 | def create_action(self, json_payload: dict): 35 | result = self._settings_api.addAction(json=json_payload) 36 | self._apply(result) 37 | return result 38 | 39 | def update_action(self, uuid, json_payload: dict): 40 | result = self._settings_api.setAction(uuid, json=json_payload) 41 | self._apply(result) 42 | return result 43 | 44 | def delete_action(self, uuid): 45 | result = self._settings_api.delAction(uuid) 46 | self._apply(result) 47 | return result 48 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_backend_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyBackendService(HaproxyService): 6 | jsonpath_base = "$.haproxy.backends.backend" 7 | uuid_resolver_map = dict( 8 | linkedServers={"template": "$.haproxy.servers.server[{uuids}].name", "insert_as_key": "Servers"}, 9 | linkedResolver={"template": "$.haproxy.resolvers.resolver[{uuids}].name", "insert_as_key": "Resolver"}, 10 | healthCheck={"template": "$.haproxy.healthchecks.healthcheck[{uuids}].name", "insert_as_key": "Healthcheck"}, 11 | linkedMailer={"template": "$.haproxy.mailers.mailer[{uuids}].name", "insert_as_key": "Mailer"}, 12 | basicAuthUsers={"template": "$.haproxy.users.user[{uuids}].name", "insert_as_key": "Users"}, 13 | basicAuthGroups={"template": "$.haproxy.groups.group[{uuids}].name", "insert_as_key": "Groups"}, 14 | linkedActions={"template": "$.haproxy.actions.action[{uuids}].name", "insert_as_key": "Actions"}, 15 | linkedErrorfiles={"template": "$.haproxy.errorfiles.errorfile[{uuids}].name", "insert_as_key": "Errorfiles"}, 16 | ) 17 | 18 | def __init__(self, settings_api: Settings, service_api: Service): 19 | super().__init__() 20 | self._complete_model_data_cache = None 21 | self._settings_api = settings_api 22 | self._service_api = service_api 23 | 24 | def list_backends(self): 25 | return self._get_backends_list() 26 | 27 | def show_backend(self, uuid): 28 | backends = self._get_backends_list() 29 | backend = next((item for item in backends if item["uuid"] == uuid), {}) 30 | return backend 31 | 32 | def _get_backends_list(self): 33 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 34 | 35 | def create_backend(self, json_payload: dict): 36 | result = self._settings_api.addBackend(json=json_payload) 37 | self._apply(result) 38 | return result 39 | 40 | def update_backend(self, uuid, json_payload: dict): 41 | result = self._settings_api.setBackend(uuid, json=json_payload) 42 | self._apply(result) 43 | return result 44 | 45 | def delete_backend(self, uuid): 46 | result = self._settings_api.delBackend(uuid) 47 | self._apply(result) 48 | return result 49 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_config_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.api.plugin.haproxy import Settings 2 | from opnsense_cli.api.plugin.haproxy import Export 3 | from opnsense_cli.api.plugin.haproxy import Service 4 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 5 | 6 | 7 | class HaproxyConfigService(HaproxyService): 8 | def __init__(self, settings_api: Settings, export_api: Export, service_api: Service): 9 | self._settings_api = settings_api 10 | self._export_api = export_api 11 | self._service_api = service_api 12 | 13 | def show_config(self): 14 | return self._export_api.config() 15 | 16 | def test_config(self): 17 | return self._service_api.configtest() 18 | 19 | def apply_config(self): 20 | self._apply() 21 | return {"status": "ok"} 22 | 23 | def show_diff(self): 24 | return self._export_api.diff() 25 | 26 | def download_config(self, path): 27 | config = self._export_api.download("all") 28 | self._write_base64_string_to_zipfile(path, config["content"]) 29 | return {"status": f"successfully saved zip to: {path}"} 30 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_cpu_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyCpuService(HaproxyService): 6 | jsonpath_base = "$.haproxy.cpus.cpu" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_cpus(self): 16 | return self._get_cpus_list() 17 | 18 | def show_cpu(self, uuid): 19 | cpus = self._get_cpus_list() 20 | cpu = next((item for item in cpus if item["uuid"] == uuid), {}) 21 | return cpu 22 | 23 | def _get_cpus_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_cpu(self, json_payload: dict): 27 | result = self._settings_api.addCpu(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_cpu(self, uuid, json_payload: dict): 32 | result = self._settings_api.setCpu(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_cpu(self, uuid): 37 | result = self._settings_api.delCpu(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_errorfile_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyErrorfileService(HaproxyService): 6 | jsonpath_base = "$.haproxy.errorfiles.errorfile" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_errorfiles(self): 16 | return self._get_errorfiles_list() 17 | 18 | def show_errorfile(self, uuid): 19 | errorfiles = self._get_errorfiles_list() 20 | errorfile = next((item for item in errorfiles if item["uuid"] == uuid), {}) 21 | return errorfile 22 | 23 | def _get_errorfiles_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_errorfile(self, json_payload: dict): 27 | result = self._settings_api.addErrorfile(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_errorfile(self, uuid, json_payload: dict): 32 | result = self._settings_api.setErrorfile(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_errorfile(self, uuid): 37 | result = self._settings_api.delErrorfile(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_frontend_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyFrontendService(HaproxyService): 6 | jsonpath_base = "$.haproxy.frontends.frontend" 7 | uuid_resolver_map = dict( 8 | defaultBackend={"template": "$.haproxy.backends.backend[{uuids}].name", "insert_as_key": "Backend"}, 9 | basicAuthUsers={"template": "$.haproxy.users.user[{uuids}].name", "insert_as_key": "Users"}, 10 | basicAuthGroups={"template": "$.haproxy.groups.group[{uuids}].name", "insert_as_key": "Groups"}, 11 | linkedCpuAffinityRules={"template": "$.haproxy.cpus.cpu[{uuids}].name", "insert_as_key": "Cpus"}, 12 | linkedActions={"template": "$.haproxy.actions.action[{uuids}].name", "insert_as_key": "Actions"}, 13 | linkedErrorfiles={"template": "$.haproxy.errorfiles.errorfile[{uuids}].name", "insert_as_key": "Errorfiles"}, 14 | ) 15 | 16 | def __init__(self, settings_api: Settings, service_api: Service): 17 | super().__init__() 18 | self._complete_model_data_cache = None 19 | self._settings_api = settings_api 20 | self._service_api = service_api 21 | 22 | def list_frontends(self): 23 | return self._get_frontends_list() 24 | 25 | def show_frontend(self, uuid): 26 | frontends = self._get_frontends_list() 27 | frontend = next((item for item in frontends if item["uuid"] == uuid), {}) 28 | return frontend 29 | 30 | def _get_frontends_list(self): 31 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 32 | 33 | def create_frontend(self, json_payload: dict): 34 | result = self._settings_api.addFrontend(json=json_payload) 35 | self._apply(result) 36 | return result 37 | 38 | def update_frontend(self, uuid, json_payload: dict): 39 | result = self._settings_api.setFrontend(uuid, json=json_payload) 40 | self._apply(result) 41 | return result 42 | 43 | def delete_frontend(self, uuid): 44 | result = self._settings_api.delFrontend(uuid) 45 | self._apply(result) 46 | return result 47 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_group_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyGroupService(HaproxyService): 6 | jsonpath_base = "$.haproxy.groups.group" 7 | uuid_resolver_map = { 8 | "members": {"template": "$.haproxy.users.user[{uuids}].name", "insert_as_key": "Users"}, 9 | } 10 | 11 | def __init__(self, settings_api: Settings, service_api: Service): 12 | super().__init__() 13 | self._complete_model_data_cache = None 14 | self._settings_api = settings_api 15 | self._service_api = service_api 16 | 17 | def list_groups(self): 18 | return self._get_groups_list() 19 | 20 | def show_group(self, uuid): 21 | groups = self._get_groups_list() 22 | group = next((item for item in groups if item["uuid"] == uuid), {}) 23 | return group 24 | 25 | def _get_groups_list(self): 26 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 27 | 28 | def create_group(self, json_payload: dict): 29 | result = self._settings_api.addGroup(json=json_payload) 30 | self._apply(result) 31 | return result 32 | 33 | def update_group(self, uuid, json_payload: dict): 34 | result = self._settings_api.setGroup(uuid, json=json_payload) 35 | self._apply(result) 36 | return result 37 | 38 | def delete_group(self, uuid): 39 | result = self._settings_api.delGroup(uuid) 40 | self._apply(result) 41 | return result 42 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_healthcheck_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyHealthcheckService(HaproxyService): 6 | jsonpath_base = "$.haproxy.healthchecks.healthcheck" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_healthchecks(self): 16 | return self._get_healthchecks_list() 17 | 18 | def show_healthcheck(self, uuid): 19 | healthchecks = self._get_healthchecks_list() 20 | healthcheck = next((item for item in healthchecks if item["uuid"] == uuid), {}) 21 | return healthcheck 22 | 23 | def _get_healthchecks_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_healthcheck(self, json_payload: dict): 27 | result = self._settings_api.addHealthcheck(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_healthcheck(self, uuid, json_payload: dict): 32 | result = self._settings_api.setHealthcheck(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_healthcheck(self, uuid): 37 | result = self._settings_api.delHealthcheck(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_lua_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyLuaService(HaproxyService): 6 | jsonpath_base = "$.haproxy.luas.lua" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_luas(self): 16 | return self._get_luas_list() 17 | 18 | def show_lua(self, uuid): 19 | luas = self._get_luas_list() 20 | lua = next((item for item in luas if item["uuid"] == uuid), {}) 21 | return lua 22 | 23 | def _get_luas_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_lua(self, json_payload: dict): 27 | result = self._settings_api.addLua(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_lua(self, uuid, json_payload: dict): 32 | result = self._settings_api.setLua(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_lua(self, uuid): 37 | result = self._settings_api.delLua(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_mailer_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyMailerService(HaproxyService): 6 | jsonpath_base = "$.haproxy.mailers.mailer" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_mailers(self): 16 | return self._get_mailers_list() 17 | 18 | def show_mailer(self, uuid): 19 | mailers = self._get_mailers_list() 20 | mailer = next((item for item in mailers if item["uuid"] == uuid), {}) 21 | return mailer 22 | 23 | def _get_mailers_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_mailer(self, json_payload: dict): 27 | result = self._settings_api.addmailer(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_mailer(self, uuid, json_payload: dict): 32 | result = self._settings_api.setmailer(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_mailer(self, uuid): 37 | result = self._settings_api.delmailer(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_mapfile_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyMapfileService(HaproxyService): 6 | jsonpath_base = "$.haproxy.mapfiles.mapfile" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_mapfiles(self): 16 | return self._get_mapfiles_list() 17 | 18 | def show_mapfile(self, uuid): 19 | mapfiles = self._get_mapfiles_list() 20 | mapfile = next((item for item in mapfiles if item["uuid"] == uuid), {}) 21 | return mapfile 22 | 23 | def _get_mapfiles_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_mapfile(self, json_payload: dict): 27 | result = self._settings_api.addMapfile(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_mapfile(self, uuid, json_payload: dict): 32 | result = self._settings_api.setMapfile(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_mapfile(self, uuid): 37 | result = self._settings_api.delMapfile(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_resolver_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyResolverService(HaproxyService): 6 | jsonpath_base = "$.haproxy.resolvers.resolver" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_resolvers(self): 16 | return self._get_resolvers_list() 17 | 18 | def show_resolver(self, uuid): 19 | resolvers = self._get_resolvers_list() 20 | resolver = next((item for item in resolvers if item["uuid"] == uuid), {}) 21 | return resolver 22 | 23 | def _get_resolvers_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_resolver(self, json_payload: dict): 27 | result = self._settings_api.addresolver(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_resolver(self, uuid, json_payload: dict): 32 | result = self._settings_api.setresolver(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_resolver(self, uuid): 37 | result = self._settings_api.delresolver(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_server_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyServerService(HaproxyService): 6 | jsonpath_base = "$.haproxy.servers.server" 7 | uuid_resolver_map = { 8 | "linkedResolver": {"template": "$.haproxy.resolvers.resolver[{uuids}].name", "insert_as_key": "Resolver"}, 9 | } 10 | 11 | def __init__(self, settings_api: Settings, service_api: Service): 12 | super().__init__() 13 | self._complete_model_data_cache = None 14 | self._settings_api = settings_api 15 | self._service_api = service_api 16 | 17 | def list_servers(self): 18 | return self._get_servers_list() 19 | 20 | def show_server(self, uuid): 21 | servers = self._get_servers_list() 22 | server = next((item for item in servers if item["uuid"] == uuid), {}) 23 | return server 24 | 25 | def _get_servers_list(self): 26 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 27 | 28 | def create_server(self, json_payload: dict): 29 | result = self._settings_api.addServer(json=json_payload) 30 | self._apply(result) 31 | return result 32 | 33 | def update_server(self, uuid, json_payload: dict): 34 | result = self._settings_api.setServer(uuid, json=json_payload) 35 | self._apply(result) 36 | return result 37 | 38 | def delete_server(self, uuid): 39 | result = self._settings_api.delServer(uuid) 40 | self._apply(result) 41 | return result 42 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/services/haproxy_user_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.plugin.haproxy.services.base import HaproxyService 2 | from opnsense_cli.api.plugin.haproxy import Settings, Service 3 | 4 | 5 | class HaproxyUserService(HaproxyService): 6 | jsonpath_base = "$.haproxy.users.user" 7 | uuid_resolver_map = {} 8 | 9 | def __init__(self, settings_api: Settings, service_api: Service): 10 | super().__init__() 11 | self._complete_model_data_cache = None 12 | self._settings_api = settings_api 13 | self._service_api = service_api 14 | 15 | def list_users(self): 16 | return self._get_users_list() 17 | 18 | def show_user(self, uuid): 19 | users = self._get_users_list() 20 | user = next((item for item in users if item["uuid"] == uuid), {}) 21 | return user 22 | 23 | def _get_users_list(self): 24 | return self._api_mutable_model_get(self._complete_model_data, self.jsonpath_base, self.uuid_resolver_map) 25 | 26 | def create_user(self, json_payload: dict): 27 | result = self._settings_api.addUser(json=json_payload) 28 | self._apply(result) 29 | return result 30 | 31 | def update_user(self, uuid, json_payload: dict): 32 | result = self._settings_api.setUser(uuid, json=json_payload) 33 | self._apply(result) 34 | return result 35 | 36 | def delete_user(self, uuid): 37 | result = self._settings_api.delUser(uuid) 38 | self._apply(result) 39 | return result 40 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/haproxy/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/haproxy/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/haproxy/tests/test_haproxy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from click.testing import CliRunner 3 | from opnsense_cli.commands.plugin.haproxy import haproxy 4 | 5 | 6 | class TestHaproxyCommands(unittest.TestCase): 7 | def test_haproxy(self): 8 | runner = CliRunner() 9 | result = runner.invoke(haproxy) 10 | 11 | self.assertEqual(0, result.exit_code) 12 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/nodeexporter/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | @click.group() 5 | def nodeexporter(**kwargs): 6 | """ 7 | Manage prometheus exporter for hardware and OS metrics. 8 | """ 9 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/nodeexporter/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/nodeexporter/services/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/nodeexporter/services/nodeexporter_config_service.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.commands.exceptions import CommandException 2 | from opnsense_cli.api.plugin.nodeexporter import General, Service 3 | from opnsense_cli.commands.service_base import CommandService 4 | 5 | 6 | class NodeexporterConfigService(CommandService): 7 | jsonpath_base = "$.general" 8 | uuid_resolver_map = {} 9 | 10 | def __init__(self, settings_api: General, service_api: Service): 11 | super().__init__() 12 | self._complete_model_data_cache = None 13 | self._settings_api = settings_api 14 | self._service_api = service_api 15 | 16 | def show_config(self): 17 | return self._settings_api.get()["general"] 18 | 19 | def edit_config(self, json_payload: dict): 20 | result = self._settings_api.set(json=json_payload) 21 | self._apply(result) 22 | 23 | return result 24 | 25 | def _apply(self, result_admin_action): 26 | if result_admin_action["result"] not in ["saved", "deleted"]: 27 | raise CommandException(result_admin_action) 28 | 29 | result_apply = self._service_api.reconfigure() 30 | 31 | if result_apply["status"] != "ok": 32 | raise CommandException(f"Apply failed: {result_apply}") 33 | -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/nodeexporter/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/nodeexporter/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/nodeexporter/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/plugin/nodeexporter/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/plugin/nodeexporter/tests/fixtures/model_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "enabled": "0", 4 | "listenaddress": "0.0.0.0", 5 | "listenport": "9100", 6 | "cpu": "1", 7 | "exec": "1", 8 | "filesystem": "1", 9 | "loadavg": "1", 10 | "meminfo": "1", 11 | "netdev": "1", 12 | "time": "1", 13 | "devstat": "1", 14 | "interrupts": "0", 15 | "ntp": "0", 16 | "zfs": "0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /opnsense_cli/commands/test_base.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | from click import Group 4 | from click.testing import Result, CliRunner 5 | 6 | from opnsense_cli.api.client import ApiClient 7 | from opnsense_cli.test_base import BaseTestCase 8 | 9 | 10 | class CommandTestCase(BaseTestCase): 11 | def _opn_cli_command_result( 12 | self, api_mock: Mock, api_return_values: list, click_group: Group, click_params: list, catch_exceptions=False 13 | ) -> Result: 14 | """ 15 | :param api_mock: Mock for the API Object, so we can safely test 16 | :param api_return_values: The values for the api mock in order they should be returned. 17 | :param click_group: The click group 18 | :param click_params: The params for the click group command 19 | :param catch_exceptions: Whether exceptions should be caught. 20 | :return: click.testing.Result 21 | """ 22 | api_mock.return_value.headers = {"content-type": "application/octet-stream"} 23 | api_mock.side_effect = api_return_values 24 | client_args = self._api_client_args_fixtures 25 | client = ApiClient(*client_args) 26 | 27 | runner = CliRunner() 28 | return runner.invoke(click_group, click_params, obj=client, catch_exceptions=catch_exceptions) 29 | -------------------------------------------------------------------------------- /opnsense_cli/commands/tree/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | from opnsense_cli.click_addons.command_tree import _build_command_tree, _print_tree 3 | 4 | 5 | @click.command() 6 | @click.pass_context 7 | def tree(ctx): 8 | """ 9 | Show the command tree of your CLI 10 | """ 11 | root_cmd = _build_command_tree(ctx.find_root().command) 12 | _print_tree(root_cmd) 13 | -------------------------------------------------------------------------------- /opnsense_cli/commands/tree/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/tree/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/tree/tests/test_tree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from click.testing import CliRunner 3 | from opnsense_cli.commands.tree import tree 4 | import click 5 | import textwrap 6 | 7 | 8 | class TestTreeCommands(unittest.TestCase): 9 | def test_tree(self): 10 | @click.group(name="root") 11 | def root(): 12 | pass 13 | 14 | @root.command(name="command-one") 15 | def command_one(): 16 | pass 17 | 18 | @root.command(name="command-two") 19 | def command_two(): 20 | pass 21 | 22 | @click.group(name="sub_level1") 23 | def sub_level1(): 24 | pass 25 | 26 | @click.group(name="sub_level2") 27 | def sub_level2(): 28 | pass 29 | 30 | root.add_command(tree) 31 | 32 | root.add_command(sub_level1) 33 | sub_level1.add_command(command_one) 34 | sub_level1.add_command(command_two) 35 | 36 | sub_level1.add_command(sub_level2) 37 | sub_level2.add_command(command_one) 38 | sub_level2.add_command(command_two) 39 | 40 | runner = CliRunner() 41 | result = runner.invoke(root, ["tree"]) 42 | 43 | tree_output = textwrap.dedent( 44 | """\ 45 | root 46 | ├── command-one 47 | ├── command-two 48 | ├── sub_level1 49 | │ ├── command-one 50 | │ ├── command-two 51 | │ └── sub_level2 52 | │ ├── command-one 53 | │ └── command-two 54 | └── tree 55 | """ 56 | ) 57 | 58 | self.assertEqual(tree_output, result.output) 59 | -------------------------------------------------------------------------------- /opnsense_cli/commands/version/__init__.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from opnsense_cli import __cli_name__, __version__ 4 | 5 | 6 | @click.command() 7 | def version(): 8 | """ 9 | Show the CLI version and exit. 10 | """ 11 | click.echo(f"{__cli_name__} v{__version__}") 12 | -------------------------------------------------------------------------------- /opnsense_cli/commands/version/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/commands/version/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/commands/version/tests/test_version.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from click.testing import CliRunner 4 | from opnsense_cli.commands.version import version 5 | from opnsense_cli import __cli_name__, __version__ 6 | 7 | 8 | class TestVersionCommands(unittest.TestCase): 9 | def test_version(self): 10 | runner = CliRunner() 11 | result = runner.invoke(version) 12 | 13 | self.assertIn(f"{__cli_name__} v{__version__}\n", result.output) 14 | self.assertEqual(0, result.exit_code) 15 | -------------------------------------------------------------------------------- /opnsense_cli/conf.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | api_key: 3T7LyQbZSXC/WN56qL0LyvLweNICeiTOzZ2JifNAvlrL+BW8Yvx7WSAUS4xvmLM/BE7xVVtv0Mv2QwNm 3 | api_secret: 2mxXt++o5Mmte3sfNJsYxlm18M2t/wAGIAHwmWoe8qc15T5wUrejJQUd/sfXSGnAG2Xk2gqMf8FzHpT2 4 | url: https://127.0.0.1:10443/api 5 | timeout: 60 6 | ssl_verify: true 7 | ca: ~/.opn-cli/ca.pem 8 | 9 | -------------------------------------------------------------------------------- /opnsense_cli/factories.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class ClassFromKeymapFactory(ABC): 5 | def __init__(self, key): 6 | self._key = key 7 | 8 | @property 9 | @abstractmethod 10 | def _keymap(self) -> dict: 11 | """This property should be implemented.""" 12 | 13 | def get_class(self): 14 | return self._keymap.get(self._key, None) 15 | 16 | 17 | class ObjectTypeFromDataFactory(ABC): 18 | @abstractmethod 19 | def get_type_for_data(self, data): 20 | """This method should be implemented.""" 21 | 22 | 23 | class FactoryException(Exception): 24 | pass 25 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/formatters/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/formatters/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Formatter(ABC): 5 | @abstractmethod 6 | def echo(self): 7 | """This method should be implemented.""" 8 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/formatters/cli_output/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/cli_output_formatter.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.formatters.base import Formatter 2 | from opnsense_cli.formatters.cli_output.output_formats import Format 3 | 4 | 5 | class CliOutputFormatter(Formatter): 6 | def __init__(self, data, format: Format, cols=None): 7 | self._data = data 8 | self._format = format 9 | self._cols = cols 10 | 11 | def echo(self): 12 | format: Format = self._format(self._data, self._cols) 13 | format.echo() 14 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/json_type_factory.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.factories import ObjectTypeFromDataFactory 2 | from opnsense_cli.formatters.cli_output.json_types import JsonType, JsonArray, JsonObj, JsonObjNested 3 | 4 | 5 | class JsonTypeFactory(ObjectTypeFromDataFactory): 6 | def get_type_for_data(self, data) -> JsonType: 7 | if isinstance(data, list): 8 | return JsonArray(data) 9 | 10 | if isinstance(data, dict): 11 | for key, val in data.items(): 12 | if not isinstance(val, dict): 13 | return JsonObj(data) 14 | 15 | return JsonObjNested(data) 16 | 17 | raise NotImplementedError("Type of JSON is unknown.") 18 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/json_types.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class JsonType(ABC): 5 | def __init__(self, json_data: dict): 6 | self._json_data = json_data 7 | 8 | @abstractmethod 9 | def get_filtered_by_columns(self, filter_columns: list) -> list: 10 | result = [] 11 | 12 | for item in self._json_data: 13 | row = [value for name, value in item.items()] 14 | if filter_columns: 15 | row = [str(item[column]) for column in filter_columns] 16 | result.append(row) 17 | return result 18 | 19 | def get_all_columns(self): 20 | result = [] 21 | for row in self._json_data: 22 | result = list(row.keys()) 23 | break 24 | return result 25 | 26 | 27 | class JsonArray(JsonType): 28 | def get_filtered_by_columns(self, filter_columns): 29 | return super().get_filtered_by_columns(filter_columns) 30 | 31 | 32 | class JsonObjNested(JsonType): 33 | def __init__(self, json_data): 34 | json_data_with_id = self.__extract_id_column(json_data) 35 | super().__init__(json_data_with_id) 36 | 37 | def __extract_id_column(self, json_data): 38 | """ 39 | Extract the key of each json row and add it in each json obj with attribute name 40 | """ 41 | result = [] 42 | for item in json_data: 43 | line = {} 44 | line.update({"": item}) 45 | line.update(json_data[item]) 46 | result.append(line) 47 | return result 48 | 49 | def get_filtered_by_columns(self, filter_columns): 50 | return super().get_filtered_by_columns(filter_columns) 51 | 52 | 53 | class JsonObj(JsonType): 54 | def get_filtered_by_columns(self, filter_columns): 55 | filtered_json_data = {column: self._json_data.get(column, "") for column in filter_columns} 56 | result = [str(json_value) for json_value in filtered_json_data.values()] 57 | return [result] 58 | 59 | def get_all_columns(self): 60 | return list(self._json_data.keys()) 61 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/output_format_factory.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.factories import ClassFromKeymapFactory 2 | from opnsense_cli.formatters.cli_output.output_formats import ( 3 | ColsOutputFormat, 4 | JsonFilterOutputFormat, 5 | JsonOutputFormat, 6 | PlainOutputFormat, 7 | TableOutputFormat, 8 | YamlOutputFormat, 9 | ) 10 | 11 | 12 | class CliOutputFormatFactory(ClassFromKeymapFactory): 13 | _keymap = { 14 | "cols": ColsOutputFormat, 15 | "table": TableOutputFormat, 16 | "json": JsonOutputFormat, 17 | "json_filter": JsonFilterOutputFormat, 18 | "plain": PlainOutputFormat, 19 | "yaml": YamlOutputFormat, 20 | } 21 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/formatters/cli_output/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/base.py: -------------------------------------------------------------------------------- 1 | import io 2 | import sys 3 | import unittest 4 | 5 | from opnsense_cli.formatters.cli_output.output_formats import Format 6 | 7 | 8 | class FormatTestCase(unittest.TestCase): 9 | def setUp(self): 10 | self._api_data_json_empty = [] 11 | self._api_data_json_array = [ 12 | { 13 | "name": "os-acme-client", 14 | "version": "2.4", 15 | "comment": "Let's Encrypt client", 16 | "flatsize": "575KiB", 17 | "locked": "N/A", 18 | "license": "BSD2CLAUSE", 19 | "repository": "OPNsense", 20 | "origin": "opnsense/os-acme-client", 21 | "provided": "1", 22 | "installed": "0", 23 | "path": "OPNsense/opnsense/os-acme-client", 24 | "configured": "0", 25 | }, 26 | { 27 | "name": "os-virtualbox", 28 | "version": "1.0_1", 29 | "comment": "VirtualBox guest additions", 30 | "flatsize": "525B", 31 | "locked": "N/A", 32 | "automatic": "N/A", 33 | "license": "BSD2CLAUSE", 34 | "repository": "OPNsense", 35 | "origin": "opnsense/os-virtualbox", 36 | "provided": "1", 37 | "installed": "1", 38 | "path": "OPNsense/opnsense/os-virtualbox", 39 | "configured": "1", 40 | }, 41 | ] 42 | 43 | self._api_data_json_nested = { 44 | "ArchiveOpenVPN": {"name": "Archive", "supportedOptions": ["plain_config", "p12_password"]}, 45 | "PlainOpenVPN": {"name": "File Only", "supportedOptions": ["auth_nocache", "cryptoapi"]}, 46 | "TheGreenBow": {"name": "TheGreenBow", "supportedOptions": []}, 47 | "ViscosityVisz": {"name": "Viscosity (visz)", "supportedOptions": ["plain_config", "random_local_port"]}, 48 | } 49 | 50 | self._api_data_json_obj = { 51 | "enabled": "1", 52 | "name": "zabbix_host", 53 | "type": "host", 54 | "proto": "IPv4", 55 | "counters": "0", 56 | "updatefreq": "0.5", 57 | "content": "www.example.com,www.heise.de", 58 | "description": "Test", 59 | "uuid": "24948d07-8525-4276-b497-108a0c55fcc2", 60 | } 61 | 62 | def _get_format_output(self, format: Format): 63 | capturedOutput = io.StringIO() 64 | sys.stdout = capturedOutput 65 | format.echo() 66 | sys.stdout = sys.__stdout__ 67 | result = capturedOutput.getvalue() 68 | 69 | return result 70 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/test_cols_output_format.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.formatters.cli_output.tests.base import FormatTestCase 2 | from opnsense_cli.formatters.cli_output.output_formats import ColsOutputFormat 3 | 4 | 5 | class TestColsOutputFormat(FormatTestCase): 6 | def test_output_specific_columns(self): 7 | format = ColsOutputFormat(self._api_data_json_array, ["name", "version"]) 8 | 9 | result = self._get_format_output(format) 10 | 11 | self.assertIn("name,version\n", result) 12 | 13 | def test_output_all_columns_with_json_obj(self): 14 | format = ColsOutputFormat(self._api_data_json_obj, [""]) 15 | 16 | result = self._get_format_output(format) 17 | 18 | self.assertIn("enabled,name,type,proto,counters,updatefreq,content,description,uuid\n", result) 19 | 20 | def test_output_all_columns_with_json_array(self): 21 | format = ColsOutputFormat(self._api_data_json_array, [""]) 22 | 23 | result = self._get_format_output(format) 24 | 25 | self.assertIn( 26 | "name,version,comment,flatsize,locked,license,repository,origin,provided,installed,path,configured\n", result 27 | ) 28 | 29 | def test_output_all_columns_with_json_nested(self): 30 | format = ColsOutputFormat(self._api_data_json_nested, [""]) 31 | 32 | result = self._get_format_output(format) 33 | 34 | self.assertIn(",name,supportedOptions\n", result) 35 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/test_json_filter_output_format.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.formatters.cli_output.output_formats import JsonFilterOutputFormat, JsonOutputFormat 2 | from opnsense_cli.formatters.cli_output.tests.base import FormatTestCase 3 | 4 | 5 | class TestJsonFilterOutputFormat(FormatTestCase): 6 | def test_with_json_array(self): 7 | format = JsonFilterOutputFormat(self._api_data_json_array, ["name", "version"]) 8 | 9 | result = self._get_format_output(format) 10 | 11 | self.assertIn( 12 | '[{"name": "os-acme-client", "version": "2.4"}, {"name": "os-virtualbox", "version": "1.0_1"}]\n', result 13 | ) 14 | 15 | def test_with_json_obj(self): 16 | format = JsonFilterOutputFormat(self._api_data_json_obj, ["uuid", "name"]) 17 | 18 | result = self._get_format_output(format) 19 | 20 | self.assertIn('{"uuid": "24948d07-8525-4276-b497-108a0c55fcc2", "name": "zabbix_host"}\n', result) 21 | 22 | def test_with_empty_data(self): 23 | format = JsonOutputFormat(self._api_data_json_empty, ["name", "version"]) 24 | result = self._get_format_output(format) 25 | self.assertIn(("[]"), result) 26 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/test_json_output_format.py: -------------------------------------------------------------------------------- 1 | import json 2 | from opnsense_cli.formatters.cli_output.tests.base import FormatTestCase 3 | from opnsense_cli.formatters.cli_output.output_formats import JsonOutputFormat 4 | 5 | 6 | class TestJsonOutputFormat(FormatTestCase): 7 | def test_with_json_array(self): 8 | format = JsonOutputFormat(self._api_data_json_array, ["name"]) 9 | 10 | result = self._get_format_output(format) 11 | 12 | self.assertIn(f"{json.dumps(self._api_data_json_array)}\n", result) 13 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/test_json_type.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from opnsense_cli.formatters.cli_output.json_types import JsonArray, JsonObjNested, JsonObj 3 | from opnsense_cli.formatters.cli_output.json_type_factory import JsonTypeFactory 4 | 5 | 6 | class TestJsonTypeFactory(unittest.TestCase): 7 | def setUp(self): 8 | self._factory = JsonTypeFactory() 9 | self._should_be_json_obj = [ 10 | {"result": "failed", "validations": {"alias.name": "An alias with this name already exists."}}, 11 | ] 12 | self._should_be_json_array = [ 13 | { 14 | "use_same_key_for_each_example": [ 15 | { 16 | "name": "os-acme-client", 17 | "version": "2.4", 18 | "comment": "Let's Encrypt client", 19 | "flatsize": "575KiB", 20 | "locked": "N/A", 21 | "license": "BSD2CLAUSE", 22 | "repository": "OPNsense", 23 | "origin": "opnsense/os-acme-client", 24 | "provided": "1", 25 | "installed": "0", 26 | "path": "OPNsense/opnsense/os-acme-client", 27 | "configured": "0", 28 | }, 29 | ], 30 | } 31 | ] 32 | self._should_be_json_nested = [ 33 | { 34 | "ArchiveOpenVPN": {"name": "Archive", "supportedOptions": ["plain_config", "p12_password"]}, 35 | "PlainOpenVPN": {"name": "File Only", "supportedOptions": ["auth_nocache", "cryptoapi"]}, 36 | "TheGreenBow": {"name": "TheGreenBow", "supportedOptions": []}, 37 | "ViscosityVisz": {"name": "Viscosity (visz)", "supportedOptions": ["plain_config", "random_local_port"]}, 38 | } 39 | ] 40 | 41 | def test_JsonTypeFactory_returns_JsonObj(self): 42 | for json_data in self._should_be_json_obj: 43 | json_type_obj = self._factory.get_type_for_data(json_data) 44 | self.assertIsInstance(json_type_obj, JsonObj) 45 | 46 | def test_JsonTypeFactory_returns_JsonArray(self): 47 | for json_data in self._should_be_json_array: 48 | json_type_obj = self._factory.get_type_for_data(json_data["use_same_key_for_each_example"]) 49 | self.assertIsInstance(json_type_obj, JsonArray) 50 | 51 | def test_JsonTypeFactory_returns_JsonObjNested(self): 52 | for json_data in self._should_be_json_nested: 53 | json_type_obj = self._factory.get_type_for_data(json_data) 54 | self.assertIsInstance(json_type_obj, JsonObjNested) 55 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/test_plain_output_format.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.formatters.cli_output.output_formats import PlainOutputFormat 2 | from opnsense_cli.formatters.cli_output.tests.base import FormatTestCase 3 | 4 | 5 | class TestPlainOutputFormat(FormatTestCase): 6 | def test_with_json_array(self): 7 | format = PlainOutputFormat(self._api_data_json_array, ["name", "version"]) 8 | format.separator = "|" 9 | 10 | result = self._get_format_output(format) 11 | 12 | self.assertIn("os-acme-client|2.4\n", result) 13 | 14 | def test_with_json_nested(self): 15 | format = PlainOutputFormat(self._api_data_json_nested, ["", "name", "supportedOptions"]) 16 | format.separator = "|" 17 | 18 | result = self._get_format_output(format) 19 | 20 | self.assertIn( 21 | "ArchiveOpenVPN|Archive|['plain_config', 'p12_password']\n" 22 | + "PlainOpenVPN|File Only|['auth_nocache', 'cryptoapi']\n" 23 | + "TheGreenBow|TheGreenBow|[]\n" 24 | + "ViscosityVisz|Viscosity (visz)|['plain_config', 'random_local_port']", 25 | result, 26 | ) 27 | 28 | def test_with_json_obj(self): 29 | format = PlainOutputFormat( 30 | self._api_data_json_obj, 31 | ["uuid", "name", "type", "proto", "counters", "description", "updatefreq", "content", "enabled"], 32 | ) 33 | format.separator = "|" 34 | 35 | result = self._get_format_output(format) 36 | 37 | self.assertIn( 38 | "24948d07-8525-4276-b497-108a0c55fcc2|zabbix_host|host|IPv4|0|Test|0.5|www.example.com,www.heise.de|1\n", result 39 | ) 40 | 41 | def test_with_empty_data(self): 42 | format = PlainOutputFormat(self._api_data_json_empty, ["name", "version"]) 43 | result = self._get_format_output(format) 44 | self.assertIn((""), result) 45 | 46 | def test_not_implemented(self): 47 | format = PlainOutputFormat("Just a String", []) 48 | format.separator = "|" 49 | 50 | self.assertRaises(NotImplementedError, format.echo) 51 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/test_table_output_format.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.formatters.cli_output.tests.base import FormatTestCase 2 | from opnsense_cli.formatters.cli_output.output_formats import TableOutputFormat 3 | 4 | 5 | class TestTableOutputFormat(FormatTestCase): 6 | def test_with_json_array(self): 7 | format = TableOutputFormat(self._api_data_json_array, ["name", "version"]) 8 | result = self._get_format_output(format) 9 | 10 | self.assertIn( 11 | ( 12 | "+----------------+---------+\n" 13 | "| name | version |\n" 14 | "+----------------+---------+\n" 15 | "| os-acme-client | 2.4 |\n" 16 | "| os-virtualbox | 1.0_1 |\n" 17 | "+----------------+---------+\n" 18 | ), 19 | result, 20 | ) 21 | -------------------------------------------------------------------------------- /opnsense_cli/formatters/cli_output/tests/test_yaml_output_format.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.formatters.cli_output.tests.base import FormatTestCase 2 | from opnsense_cli.formatters.cli_output.output_formats import YamlOutputFormat 3 | 4 | 5 | class TestYamlOutputFormat(FormatTestCase): 6 | def test_with_json_array(self): 7 | format = YamlOutputFormat(self._api_data_json_array, ["name", "version"]) 8 | result = self._get_format_output(format) 9 | 10 | self.assertIn("- name: os-acme-client\n version: '2.4'\n- name: os-virtualbox\n version: '1.0_1'\n\n", result) 11 | 12 | def test_with_json_nested(self): 13 | format = YamlOutputFormat(self._api_data_json_nested, ["", "name", "supportedOptions"]) 14 | result = self._get_format_output(format) 15 | 16 | self.assertIn( 17 | ( 18 | "- : ArchiveOpenVPN\n name: Archive\n supportedOptions: '[''plain_config'', ''p12_password'']'\n" 19 | "- : PlainOpenVPN\n name: File Only\n supportedOptions: '[''auth_nocache'', ''cryptoapi'']'\n" 20 | "- : TheGreenBow\n name: TheGreenBow\n supportedOptions: '[]'\n" 21 | "- : ViscosityVisz\n name: Viscosity (visz)\n" 22 | " supportedOptions: '[''plain_config'', ''random_local_port'']'\n\n" 23 | ), 24 | result, 25 | ) 26 | 27 | def test_with_json_obj(self): 28 | format = YamlOutputFormat( 29 | self._api_data_json_obj, 30 | ["uuid", "name", "type", "proto", "counters", "description", "updatefreq", "content", "enabled"], 31 | ) 32 | result = self._get_format_output(format) 33 | 34 | self.assertIn( 35 | ( 36 | "uuid: 24948d07-8525-4276-b497-108a0c55fcc2\nname: zabbix_host\ntype: host\nproto: IPv4\n" 37 | "counters: '0'\ndescription: Test\nupdatefreq: '0.5'\ncontent: www.example.com,www.heise.de\n" 38 | "enabled: '1'\n\n" 39 | ), 40 | result, 41 | ) 42 | 43 | def test_with_empty_data(self): 44 | format = YamlOutputFormat(self._api_data_json_empty, ["name", "version"]) 45 | result = self._get_format_output(format) 46 | self.assertIn(("{}\n\n"), result) 47 | -------------------------------------------------------------------------------- /opnsense_cli/parser/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/parser/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/parser/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Parser(ABC): 5 | @abstractmethod 6 | def _set_content(self): 7 | """This method should be implemented.""" 8 | 9 | @abstractmethod 10 | def _parse_content(self) -> dict: 11 | """This method should be implemented.""" 12 | 13 | def parse(self): 14 | self._set_content() 15 | return self._parse_content() 16 | -------------------------------------------------------------------------------- /opnsense_cli/parser/html_parser.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import requests 3 | from opnsense_cli.parser.base import Parser 4 | 5 | 6 | class HtmlParser(Parser): 7 | def __init__(self, url, tag): 8 | self._url = url 9 | self._tag = tag 10 | 11 | def _set_content(self): 12 | webpage_response = requests.get(self._url, verify=True) 13 | webpage = webpage_response.content 14 | self._content = BeautifulSoup(webpage, "html.parser") 15 | 16 | def _parse_content(self): 17 | tags = self._content.find_all(self._tag) 18 | return tags 19 | -------------------------------------------------------------------------------- /opnsense_cli/parser/opnsense_form_parser.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.parser.xml_parser import XmlParser 2 | 3 | 4 | class OpnsenseFormParser(XmlParser): 5 | def _parse_content(self): 6 | base_tag = super()._parse_content() 7 | return self.get_help_messages_with_id(base_tag) 8 | 9 | def get_help_messages_with_id(self, base_tag): 10 | messages = {} 11 | for field in base_tag.findChildren(recursive=False): 12 | if self._skip_field(field): 13 | continue 14 | 15 | field_id = field.id.string.split(".")[-1] 16 | field_help = field.help.string.replace("'", "\\'") 17 | 18 | messages.update({field_id: field_help}) 19 | 20 | return messages 21 | 22 | def _skip_field(self, field): 23 | if field.find(name="type", text="header"): 24 | return True 25 | 26 | if not field.find(name="id"): 27 | return True 28 | 29 | if not field.find(name="help"): 30 | return True 31 | 32 | return False 33 | -------------------------------------------------------------------------------- /opnsense_cli/parser/opnsense_model_parser.py: -------------------------------------------------------------------------------- 1 | from bs4.element import Tag 2 | from opnsense_cli.parser.xml_parser import XmlParser 3 | 4 | 5 | class OpnsenseModelParser(XmlParser): 6 | def _parse_content(self) -> Tag: 7 | return self._content.find(self._tag, type=None).findChild() 8 | -------------------------------------------------------------------------------- /opnsense_cli/parser/opnsense_module_list_parser.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.parser.html_parser import HtmlParser 2 | 3 | 4 | class OpnsenseModuleListParser(HtmlParser): 5 | def __init__(self, url): 6 | super().__init__(url, "a") 7 | self.module_list = self._list_all_modules() 8 | 9 | def _list_all_modules(self): 10 | super()._set_content() 11 | links = self._content.find_all(self._tag, href=True) 12 | module_list = [] 13 | for link in links: 14 | if link["href"].endswith(".rst"): 15 | url_component_list = link["href"].split("/") 16 | module = self._get_module(url_component_list) 17 | module_list.append(module) 18 | return module_list 19 | 20 | def _get_module(self, url_components): 21 | if len(url_components) > 2: 22 | module_type = url_components[-2] 23 | if module_type == "plugins" or module_type == "core": 24 | return url_components[-1].split(".")[0] 25 | -------------------------------------------------------------------------------- /opnsense_cli/parser/xml_parser.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from bs4.element import Tag 3 | import requests 4 | from opnsense_cli.parser.base import Parser 5 | 6 | 7 | class XmlParser(Parser): 8 | def __init__(self, url, tag): 9 | self._url = url 10 | self._tag = tag 11 | 12 | def _set_content(self): 13 | webpage_response = requests.get(self._url) 14 | webpage = webpage_response.content 15 | self._content = BeautifulSoup(webpage, "xml") 16 | 17 | def _parse_content(self) -> Tag: 18 | tag = self._content.find(self._tag) 19 | return tag 20 | -------------------------------------------------------------------------------- /opnsense_cli/template_engines/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/template_engines/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/template_engines/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | from opnsense_cli.template_engines.exceptions import TemplateEngineException 4 | 5 | 6 | class TemplateEngine(ABC): 7 | def __init__(self, template_basedir): 8 | self.__vars = None 9 | self.__template = None 10 | self.template_basedir = template_basedir 11 | 12 | @property 13 | def template_basedir(self): 14 | return self.__template_basedir 15 | 16 | @template_basedir.setter 17 | def template_basedir(self, dir): 18 | self.__template_basedir = dir 19 | 20 | @property 21 | def template(self): 22 | if not self.__template: 23 | raise TemplateEngineException("missing template") 24 | return self.__template 25 | 26 | @template.setter 27 | def template(self, template): 28 | self.__template = template 29 | 30 | @property 31 | def vars(self): 32 | if not self.__vars: 33 | raise TemplateEngineException("missing template vars") 34 | return self.__vars 35 | 36 | @vars.setter 37 | def vars(self, template_var_obj: dataclass): 38 | self.__vars = template_var_obj 39 | 40 | @abstractmethod 41 | def set_template_from_string(self, template_str: str): 42 | """This method should be implemented.""" 43 | 44 | @abstractmethod 45 | def set_template_from_file(self, file): 46 | """This method should be implemented.""" 47 | 48 | @abstractmethod 49 | def render(self): 50 | """This method should be implemented.""" 51 | -------------------------------------------------------------------------------- /opnsense_cli/template_engines/exceptions.py: -------------------------------------------------------------------------------- 1 | class TemplateEngineException(Exception): 2 | pass 3 | 4 | 5 | class TemplateNotFoundException(TemplateEngineException): 6 | pass 7 | -------------------------------------------------------------------------------- /opnsense_cli/template_engines/jinja2.py: -------------------------------------------------------------------------------- 1 | import os 2 | from jinja2 import Template, Environment, BaseLoader 3 | from opnsense_cli.template_engines.base import TemplateEngine 4 | from opnsense_cli.template_engines.exceptions import TemplateNotFoundException 5 | from jinja2.exceptions import TemplateNotFound, TemplatesNotFound 6 | 7 | 8 | class Jinja2TemplateEngine(TemplateEngine): 9 | def set_template_from_file(self, template, **kwargs): 10 | path = os.path.abspath(os.path.join(self.template_basedir, template)) 11 | try: 12 | template_content = self._read_file(path) 13 | self.set_template_from_string(template_content) 14 | except (TemplateNotFound, TemplatesNotFound, FileNotFoundError): 15 | basedir = os.path.abspath(self.template_basedir) 16 | template_path = os.path.join(basedir, template) 17 | raise TemplateNotFoundException(template_path) 18 | 19 | def set_template_from_string(self, template_str, **kwargs) -> Template: 20 | self.template = Environment(loader=BaseLoader, **kwargs).from_string(template_str) 21 | 22 | def render(self): 23 | return self.template.render(vars=self.vars) 24 | 25 | def _read_file(self, path): 26 | with open(path) as file: 27 | content = file.read() 28 | return content 29 | -------------------------------------------------------------------------------- /opnsense_cli/template_engines/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/template_engines/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/template_engines/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/template_engines/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/template_engines/tests/fixtures/template_vars.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class TemplateTestVars: 6 | my_name: str 7 | -------------------------------------------------------------------------------- /opnsense_cli/template_engines/tests/test_jinja2_template_engine.py: -------------------------------------------------------------------------------- 1 | from opnsense_cli.test_base import BaseTestCase 2 | from opnsense_cli.template_engines.jinja2 import Jinja2TemplateEngine 3 | from opnsense_cli.template_engines.exceptions import TemplateEngineException, TemplateNotFoundException 4 | from opnsense_cli.template_engines.tests.fixtures.template_vars import TemplateTestVars 5 | 6 | 7 | class Test_Jinja2TemplateEngine(BaseTestCase): 8 | def setUp(self): 9 | self._template_basedir = self._get_fixture_path("", "../fixtures/tests/template_engines") 10 | self._engine = Jinja2TemplateEngine(self._template_basedir) 11 | 12 | def test_template_render_OK(self): 13 | self._engine.set_template_from_string("My name: {{ vars.my_name }}") 14 | self._engine.vars = TemplateTestVars(my_name="opn-cli") 15 | result = self._engine.render() 16 | self.assertIn("My name: opn-cli", result) 17 | 18 | def test_template_not_found_ERROR(self): 19 | self.assertRaises(TemplateNotFoundException, self._engine.set_template_from_file, "does_not_exists.py.j2") 20 | 21 | def test_template_missing_ERROR(self): 22 | self.assertRaises(TemplateEngineException, self._engine.render) 23 | 24 | def test_template_vars_missing_ERROR(self): 25 | self._engine.set_template_from_string("a jinja template") 26 | self.assertRaises(TemplateEngineException, self._engine.render) 27 | -------------------------------------------------------------------------------- /opnsense_cli/test_base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | from pyfakefs.fake_filesystem_unittest import TestCase 4 | from unittest.mock import Mock 5 | 6 | import opnsense_cli 7 | 8 | 9 | class BaseTestCase(TestCase): 10 | BASE_DIR = os.path.dirname(opnsense_cli.__file__) 11 | 12 | def _read_json_fixture(self, relative_path, base_dir=BASE_DIR): 13 | file_content = self._read_fixture_file(relative_path, base_dir) 14 | return json.loads(file_content) 15 | 16 | def _read_fixture_file(self, relative_path, base_dir=BASE_DIR): 17 | path = os.path.join(base_dir, relative_path) 18 | file_content = self._read_file(path) 19 | return file_content 20 | 21 | def _read_template_file(self, relative_path, base_dir=BASE_DIR): 22 | path = os.path.join(base_dir, relative_path) 23 | file_content = self._read_file(path) 24 | return file_content 25 | 26 | def _get_fixture_path(self, relative_path, fixture_dir="../fixtures/tests/commands"): 27 | path = os.path.join(os.path.dirname(__file__), fixture_dir, relative_path) 28 | return os.path.abspath(path) 29 | 30 | def _get_output_path(self): 31 | return os.path.abspath(f"{self.BASE_DIR}/../output") 32 | 33 | def _read_file(self, path): 34 | with open(path) as file: 35 | content = file.read() 36 | return content 37 | 38 | def _setup_fakefs(self): 39 | self.setUpPyfakefs() 40 | self.fs.add_real_directory(self.BASE_DIR) 41 | 42 | def _show_fakefs_contents(self): 43 | for file in os.walk("/"): 44 | print(file) 45 | 46 | def _mock_response(self, status=200, content="CONTENT", json_data=None, raise_for_status=None): 47 | """Mock Response obj for request library""" 48 | mock_resp = Mock() 49 | 50 | mock_resp.raise_for_status = Mock() 51 | if raise_for_status: 52 | mock_resp.raise_for_status.side_effect = raise_for_status 53 | 54 | mock_resp.status_code = status 55 | mock_resp.content = content 56 | 57 | if json_data: 58 | mock_resp.json = Mock(return_value=json_data) 59 | 60 | return mock_resp 61 | -------------------------------------------------------------------------------- /opnsense_cli/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreas-stuerz/opn-cli/f795b37314b46e97dfe1b60e58fb7e6b7cfa4477/opnsense_cli/tests/__init__.py -------------------------------------------------------------------------------- /opnsense_cli/tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from unittest import TestCase 3 | from unittest.mock import patch 4 | from click.testing import CliRunner 5 | from opnsense_cli.cli import cli 6 | 7 | 8 | class TestCli(TestCase): 9 | def setUp(self): 10 | self._cli_config = { 11 | "api_key": "xxx", 12 | "api_secret": "yyy", 13 | "url": "https://127.0.0.1/api", 14 | "timeout": 40, 15 | "ssl_verify": True, 16 | "ca": "ca.pem", 17 | "test": "test", 18 | } 19 | 20 | @patch("opnsense_cli.cli.ApiClient.__init__") 21 | def test_cli(self, api_client_mock): 22 | api_client_mock.return_value = None 23 | runner = CliRunner() 24 | with runner.isolated_filesystem(): 25 | with open("config.yaml", "w") as f: 26 | yaml.dump(self._cli_config, f) 27 | 28 | result = runner.invoke(cli, ["-c", "config.yaml", "version"]) 29 | 30 | api_client_mock.assert_called_once_with( 31 | self._cli_config["api_key"], 32 | self._cli_config["api_secret"], 33 | self._cli_config["url"], 34 | self._cli_config["ssl_verify"], 35 | self._cli_config["ca"], 36 | self._cli_config["timeout"], 37 | ) 38 | self.assertEqual(0, result.exit_code) 39 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | ####### requirements.txt ####### 3 | # 4 | . 5 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | # Exclude a variety of commonly ignored directories. 2 | exclude = [ 3 | ".bzr", 4 | ".direnv", 5 | ".eggs", 6 | ".git", 7 | ".git-rewrite", 8 | ".hg", 9 | ".ipynb_checkpoints", 10 | ".mypy_cache", 11 | ".nox", 12 | ".pants.d", 13 | ".pyenv", 14 | ".pytest_cache", 15 | ".pytype", 16 | ".ruff_cache", 17 | ".svn", 18 | ".tox", 19 | ".venv", 20 | ".vscode", 21 | "__pypackages__", 22 | "_build", 23 | "buck-out", 24 | "build", 25 | "dist", 26 | "node_modules", 27 | "site-packages", 28 | "venv", 29 | "output", 30 | ] 31 | 32 | # Same as Black. 33 | line-length = 127 34 | indent-width = 4 35 | 36 | # Assume Python 3.8 37 | target-version = "py38" 38 | 39 | [lint] 40 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. 41 | select = ["E4", "E7", "E9", "F"] 42 | ignore = ["E203"] 43 | 44 | # Allow fix for all enabled rules (when `--fix`) is provided. 45 | fixable = ["ALL"] 46 | unfixable = [] 47 | 48 | # Allow unused variables when underscore-prefixed. 49 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 50 | 51 | [format] 52 | # Like Black, use double quotes for strings. 53 | quote-style = "double" 54 | 55 | # Like Black, indent with spaces, rather than tabs. 56 | indent-style = "space" 57 | 58 | # Like Black, respect magic trailing commas. 59 | skip-magic-trailing-comma = false 60 | 61 | # Like Black, automatically detect the appropriate line ending. 62 | line-ending = "auto" 63 | -------------------------------------------------------------------------------- /scripts/acceptance_tests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | IMAGE_SUFFIX=${1:-} 4 | 5 | ARGS="acceptance_tests" 6 | if [ -n "$2" ]; then 7 | ARGS=$(printf ' %q' "$@") 8 | fi 9 | 10 | scripts/create_test_env 11 | 12 | pytest --exitfirst --verbose --failed-first --ignore=./output --verbose ${ARGS} 13 | 14 | scripts/remove_test_env 15 | 16 | 17 | -------------------------------------------------------------------------------- /scripts/changelog: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | version=${1:-} 3 | user=${2-andeman} 4 | project=${3- opn-cli} 5 | 6 | if [ -z "${version}" ]; then 7 | echo "Please provide the version to release as argument to the script" 8 | echo "$0 v1.0.0" 9 | exit 1 10 | fi 11 | 12 | docker run -it --rm -v "$(pwd)":/usr/local/src/your-app \ 13 | githubchangeloggenerator/github-changelog-generator \ 14 | -t ${CHANGELOG_GITHUB_TOKEN} \ 15 | --user ${user} \ 16 | --project ${project} \ 17 | --future-release ${version} \ 18 | --no-issues 19 | -------------------------------------------------------------------------------- /scripts/coverage: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | MODULE=${1:-opnsense_cli} 3 | MIN_COVERAGE=${2:-100} 4 | if [ -n "$1" ]; then 5 | MODULE=$(printf ' %q' "$@") 6 | fi 7 | 8 | pytest --exitfirst --verbose --failed-first --cov=${MODULE}/ --debug config --cov-report term-missing --cov-fail-under=${MIN_COVERAGE} ${MODULE} --verbosity=5 9 | 10 | -------------------------------------------------------------------------------- /scripts/create_test_env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # install test dependencies 3 | pip3 install -r test_requirements.txt 4 | 5 | # install editable 6 | pip3 install -e . 7 | 8 | # deploy configuration 9 | mv ~/.opn-cli ~/.opn-cli.bak 10 | mkdir -p ~/.opn-cli 11 | cp opnsense_cli/ca.pem ~/.opn-cli/. 12 | cp opnsense_cli/conf.yaml ~/.opn-cli/. 13 | 14 | # setup test device 15 | vagrant up 16 | 17 | # install required opnsense plugins 18 | opn-cli plugin install os-firewall 19 | opn-cli plugin install os-haproxy 20 | opn-cli plugin install os-node_exporter 21 | opn-cli plugin install os-api-backup 22 | 23 | echo "OPNSense URL:" 24 | echo "https://127.0.0.1:10443/" 25 | echo "Login: root" 26 | echo "Pass: opnsense" 27 | echo 28 | -------------------------------------------------------------------------------- /scripts/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | while getopts "fh" arg; do 3 | # shellcheck disable=SC2220 4 | case $arg in 5 | f) 6 | ruff check --fix --statistics 7 | ;; 8 | h) 9 | echo "Usage: $0 [-f] [-h]" 10 | echo " -f: auto fix lint issues" 11 | echo " -h: help" 12 | exit 0 13 | ;; 14 | esac 15 | done 16 | 17 | ruff check --output-format=full 18 | -------------------------------------------------------------------------------- /scripts/remove_test_env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cp -rv ~/.opn-cli.bak ~/.opn-cli 3 | vagrant destroy -f 4 | -------------------------------------------------------------------------------- /scripts/unit_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ARGS="opnsense_cli" 3 | if [ -n "$1" ]; then 4 | ARGS=$(printf ' %q' "$@") 5 | 6 | fi 7 | 8 | pytest --exitfirst --verbose --failed-first --ignore=./output --verbose ${ARGS} 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | from opnsense_cli import __cli_name__, __version__ 5 | 6 | _directory = os.path.abspath(os.path.dirname(__file__)) 7 | with open(os.path.join(_directory, "README.md"), encoding="utf-8") as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name=__cli_name__, 12 | version=__version__, 13 | packages=find_packages(), 14 | description="OPNsense CLI written in python.", 15 | author="Andreas Stürz IT-Solutions", 16 | license="BSD-2-Clause License", 17 | project_urls={ 18 | "Bug Tracker": "https://github.com/andeman/opnsense_cli/issues", 19 | "CI: GitHub Actions Pipelines": "https://github.com/andeman/opnsense_cli/actions", 20 | "Documentation": "https://github.com/andeman/opnsense_cli", 21 | "Source Code": "https://github.com/andeman/opnsense_cli", 22 | }, 23 | long_description=long_description, 24 | long_description_content_type="text/markdown", 25 | install_requires=["click>=8.0.1", "requests", "PTable", "PyYAML", "jsonpath-ng", "beautifulsoup4", "lxml", "Jinja2"], 26 | python_requires=">=3.7", 27 | entry_points=""" 28 | [console_scripts] 29 | opn-cli=opnsense_cli.cli:cli 30 | """, 31 | include_package_data=True, 32 | classifiers=[ 33 | "Development Status :: 4 - Beta", 34 | "Environment :: Console", 35 | "Intended Audience :: Developers", 36 | "License :: OSI Approved :: BSD License", 37 | "Operating System :: POSIX", 38 | "Operating System :: MacOS", 39 | "Operating System :: Unix", 40 | "Operating System :: Microsoft :: Windows", 41 | "Programming Language :: Python :: 3 :: Only", 42 | "Programming Language :: Python :: 3.7", 43 | "Programming Language :: Python :: 3.8", 44 | "Programming Language :: Python :: 3.9", 45 | "Programming Language :: Python :: 3.10", 46 | "Programming Language :: Python :: 3.11", 47 | "Topic :: Software Development :: Libraries :: Python Modules", 48 | ], 49 | ) 50 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | ####### test_requirements.txt ####### 3 | # 4 | pytest 5 | pytest-cov 6 | pyfakefs 7 | pyfakefs 8 | ruff 9 | --------------------------------------------------------------------------------