Skip to content

Commit

Permalink
Xdg config home support (#50)
Browse files Browse the repository at this point in the history
* update test and coverage scripts

* If the environment variable `XDG_CONFIG_HOME` is set, ~/.config/opn-cli will be used instead of ~/.opn-cli

* add coverage to 100%
  • Loading branch information
andreas-stuerz authored Dec 30, 2023
1 parent ce8926c commit 1c99d2c
Show file tree
Hide file tree
Showing 170 changed files with 8,807 additions and 10,845 deletions.
4 changes: 2 additions & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ omit =
*private*
*/local/*
*venv*
*opnsense_cli/tests/*
*opnsense_cli/fixtures/*
opnsense_cli/tests/*
opnsense_cli/fixtures/*
*output*
/usr/lib/python*

Expand Down
11 changes: 4 additions & 7 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,16 @@ jobs:
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --max-complexity=10 --max-line-length=127 --statistics
scripts/lint
- name: Unit test with coverage
run: |
pytest --exitfirst --verbose --failed-first --cov opnsense_cli --cov-config .coveragerc opnsense_cli
scripts/coverage
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v1
- name: smoke test
run: |
pip install .
mkdir -p /home/runner/.opn-cli
touch /home/runner/.opn-cli/conf.yaml
mkdir -p /home/runner/.config/opn-cli
touch /home/runner/.config/opn-cli/conf.yaml
opn-cli version
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
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ pip install opn-cli
## Usage
Each command and subcommand support the `-h` or `--help option to show help for the current command.
Each command and subcommand support the `-h` or `--help` option to show help for the current command.
The config basedir is `~/.opn-cli/`. If the environment variable `XDG_CONFIG_HOME` is set, `~/.config/opn-cli` will be used instead.
```
$ opn-cli --help
Expand All @@ -113,40 +115,43 @@ Usage: opn-cli [OPTIONS] COMMAND [ARGS]...

You need a valid API key and secret to interact with the API. Open your
browser and go to System->Access->Users and generate or use an existing
Api Key.
Api Key.

See: https://docs.opnsense.org/development/how-tos/api.html#creating-keys.

SSL verify / CA:

If you use ssl verification (--ssl-verify), make sure to specify a valid
ca with --ca <path_to_bundle>.
ca or cert with --ca <path_to_bundle>.

To download the default self-signed cert, open the OPNsense Web Gui and go to
System->Trust->Certificates. Search for the Name: "Web GUI SSL certificate" and
press the "export user cert" button.

To download the default self-signed cert, open the OPNsense Web Gui and go to
System->Trust->Certificates. Search for the Name: "Web GUI SSL certificate" and
press the "export user cert" button.

If you use a ca signed certificate, go to System->Trust->Authorities and
If you use a ca signed certificate, go to System->Trust->Authorities and
press the "export CA cert" button to download the ca.

Save the cert or the ca as ~/.opn-cli/ca.pem.
Save the ca and pass the path to the --ca option.

Configuration:

The base directory for the config is ~/.opn-cli.

If the environment variable XDG_CONFIG_HOME is set, ~/.config/opn-cli will be used instead.

You can set the required options as environment variables. See --help
"[env var: [...]"

Or use a config file passed with -c option.

The configuration cascade from highest precedence to lowest:
The configuration cascade from the highest precedence to lowest:

1. argument & options

2. environment variables

3. config file


Happy automating!

Options:
Expand Down
17 changes: 3 additions & 14 deletions acceptance_tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,7 @@ class CommandResult:
class CliCommandTestCase(TestCase):
@property
def _api_client_args_fixtures(self):
return [
'api_key',
'api_secret',
'https://127.0.0.1:10443/api',
True,
'~/.opn-cli/ca.pem',
60
]
return ["api_key", "api_secret", "https://127.0.0.1:10443/api", True, "~/.opn-cli/ca.pem", 60]

def run_command(self, command) -> str:
"""
Expand All @@ -36,17 +29,13 @@ def run_command(self, command) -> str:
"""
return self.cmd_execute(command)

def cmd_execute(self, command, encoding='UTF-8', raise_exception=False):
def cmd_execute(self, command, encoding="UTF-8", raise_exception=False):
args = shlex.split(command)
p = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = p.communicate()
if error and raise_exception:
raise CommandError(p.returncode, error.decode(encoding))

result = CommandResult(
output.decode(encoding),
error.decode(encoding),
p.returncode
)
result = CommandResult(output.decode(encoding), error.decode(encoding), p.returncode)

return result
15 changes: 2 additions & 13 deletions acceptance_tests/tests/core/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ def setUp(self):

def test_plugin_lifecycle(self):
result = self.run_command("opn-cli plugin installed -o plain -c name,locked")
self.assertIn(
"os-firewall N/A\n"
"os-haproxy N/A\n"
"os-virtualbox N/A\n",
result.stdout
)
self.assertIn("os-firewall N/A\n" "os-haproxy N/A\n" "os-virtualbox N/A\n", result.stdout)

result = self.run_command("opn-cli plugin install os-helloworld")
assert result.exitcode == 0
Expand All @@ -21,13 +16,7 @@ def test_plugin_lifecycle(self):
assert result.exitcode == 0

result = self.run_command("opn-cli plugin installed -o plain -c name,locked")
self.assertIn(
"os-firewall N/A\n"
"os-haproxy N/A\n"
"os-helloworld 1\n"
"os-virtualbox N/A\n",
result.stdout
)
self.assertIn("os-firewall N/A\n" "os-haproxy N/A\n" "os-helloworld 1\n" "os-virtualbox N/A\n", result.stdout)

result = self.run_command("opn-cli plugin unlock os-helloworld")
assert result.exitcode == 0
Expand Down
6 changes: 3 additions & 3 deletions opnsense_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__cli_name__ = 'opn-cli'
__version__ = '1.7.0'
__copyright__ = '(c) by Andreas Stürz'
__cli_name__ = "opn-cli"
__version__ = "1.7.0"
__copyright__ = "(c) by Andreas Stürz"
6 changes: 2 additions & 4 deletions opnsense_cli/api/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from opnsense_cli.api.client import ApiClient


class ApiBase():
class ApiBase:
def __init__(self, api_client: ApiClient):
self._api_client = api_client
self.module = self.MODULE
Expand Down Expand Up @@ -29,9 +29,7 @@ def _api_call(api_function):
def api_response(self, *args, json=None):
api_function(self)
return self._api_client.execute(
*args,
module=self.module, controller=self.controller, method=self.method, command=self.command,
json=json
*args, module=self.module, controller=self.controller, method=self.method, command=self.command, json=json
)

return api_response
22 changes: 11 additions & 11 deletions opnsense_cli/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,31 @@ def _process_response(self, response):

def _get_endpoint_url(self, *args, **kwargs):
endpoint = f"{kwargs['module']}/{kwargs['controller']}/{kwargs['command']}".lower()
endpoint_params = '/'.join(args)
endpoint_params = "/".join(args)
if endpoint_params:
return f"{endpoint}/{endpoint_params}"
return endpoint

def _get(self, endpoint):
req_url = '{}/{}'.format(self._base_url, endpoint)
response = requests.get(req_url, verify=self.ssl_verify_cert,
auth=(self._api_key, self._api_secret),
timeout=self._timeout)
req_url = "{}/{}".format(self._base_url, endpoint)
response = requests.get(
req_url, verify=self.ssl_verify_cert, auth=(self._api_key, self._api_secret), timeout=self._timeout
)
return self._process_response(response)

def _post(self, endpoint, json=None):
req_url = '{}/{}'.format(self._base_url, endpoint)
response = requests.post(req_url, json=json, verify=self.ssl_verify_cert,
auth=(self._api_key, self._api_secret),
timeout=self._timeout)
req_url = "{}/{}".format(self._base_url, endpoint)
response = requests.post(
req_url, json=json, verify=self.ssl_verify_cert, auth=(self._api_key, self._api_secret), timeout=self._timeout
)
return self._process_response(response)

def execute(self, *args, json=None, **kwargs):
endpoint = self._get_endpoint_url(*args, **kwargs)
try:
if kwargs['method'] == 'get':
if kwargs["method"] == "get":
return self._get(endpoint)
elif kwargs['method'] == 'post':
elif kwargs["method"] == "post":
return self._post(endpoint, json)
else:
raise NotImplementedError(f"Unkown HTTP method: {kwargs['method']}")
Expand Down
3 changes: 3 additions & 0 deletions opnsense_cli/api/plugin/firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class FirewallFilter(ApiBase):
"""
Firewall Filter (needs plugin: os-firewall)
"""

@ApiBase._api_call
def add_rule(self, *args, json=None):
self.method = "post"
Expand Down Expand Up @@ -54,6 +55,7 @@ class FirewallAlias(ApiBase):
"""
Firewall Alias Util
"""

@ApiBase._api_call
def export(self, *args):
self.method = "get"
Expand Down Expand Up @@ -91,6 +93,7 @@ class FirewallAliasUtil(ApiBase):
"""
Firewall Alias Util
"""

@ApiBase._api_call
def list(self, *args):
self.method = "get"
Expand Down
18 changes: 9 additions & 9 deletions opnsense_cli/autoloader/click_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ def autoload(self, module_name=None):
opnsense_cli/commands
├── core
│ ├── firewall
│ │ ├── __init__.py (Main @click.group firewall
│ │ ├── alias.py (subgroup alias with commmands from firewall)
│ │ └── rule.py (subgroup rule with commmands from firewall)
│ │ ├── __init__.py (main @click.group firewall)
│ │ ├── alias.py (subgroup alias with commands from firewall)
│ │ └── rule.py (subgroup rule with commands from firewall)
:param module_name: python module name e.g. opnsense_cli.commands.core
:return: click.core.Group
Expand All @@ -33,14 +33,14 @@ def autoload(self, module_name=None):

(root_dir, command_group_dirs, files) = list(os.walk(path))[0]

if '__pycache__' in command_group_dirs:
command_group_dirs.remove('__pycache__')
if "__pycache__" in command_group_dirs:
command_group_dirs.remove("__pycache__")

if not command_group_dirs:
path, file = os.path.split(root_dir)
command_group_dirs = [file]
module_path_components = module_name.split('.')
module_name = '.'.join(module_path_components[0:len(module_path_components) - 1])
module_path_components = module_name.split(".")
module_name = ".".join(module_path_components[0 : len(module_path_components) - 1])

for command_group_dir in command_group_dirs:
command_group_files = list(os.walk(f"{path}/{command_group_dir}"))[0][2]
Expand All @@ -49,7 +49,7 @@ def autoload(self, module_name=None):
import_name = f"{module_name}.{command_group_dir}"
class_name = f"{command_group_dir}"

if command_group_file != '__init__.py':
if command_group_file != "__init__.py":
_subname_ = os.path.splitext(command_group_file)[0]
import_name = f"{module_name}.{command_group_dir}.{_subname_}"
class_name = f"{_subname_}"
Expand All @@ -60,7 +60,7 @@ def autoload(self, module_name=None):
self.loaded_modules.add(module)
self.loaded_classes.append(click_group)

if command_group_file == '__init__.py':
if command_group_file == "__init__.py":
self.click_core_group.add_command(click_group)
else:
click_group.add_command(click_group)
Expand Down
19 changes: 14 additions & 5 deletions opnsense_cli/callbacks/click.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
from opnsense_cli.facades.commands.base import CommandFacade
from opnsense_cli.factories.cli_output_format import CliOutputFormatFactory
from opnsense_cli.formats.base import Format
from opnsense_cli import __cli_name__


def get_default_config_dir():
if "XDG_CONFIG_HOME" in os.environ:
return f"~/.config/{__cli_name__}"
return f"~/.{__cli_name__}"


"""
Click callback methods
Expand All @@ -13,9 +21,10 @@

def defaults_from_configfile(ctx, param, filename):
def dict_from_yaml(path):
with open(path, 'r') as yaml_file:
with open(path, "r") as yaml_file:
data = yaml.load(yaml_file, Loader=yaml.SafeLoader)
return data

options = dict_from_yaml(os.path.expanduser(filename))
ctx.default_map = options

Expand All @@ -34,27 +43,27 @@ def formatter_from_formatter_name(ctx, param, format_name) -> Format:


def bool_as_string(ctx, param, value):
if type(value) == bool:
if isinstance(value, bool):
return str(int(value))
return value


def tuple_to_csv(ctx, param, value):
if param.multiple and not value:
return None
if type(value) == tuple:
if isinstance(value, tuple):
return ",".join(value)
return value


def comma_to_newline(ctx, param, value):
if type(value) == str and "," in value:
if isinstance(value, str) and "," in value:
return value.replace(",", "\n")
return value


def int_as_string(ctx, param, value):
if type(value) == int:
if isinstance(value, int):
return str(value)
return value

Expand Down
Loading

0 comments on commit 1c99d2c

Please sign in to comment.