Contributing to LWS
On this page
Bug reports, fixes, documentation and new commands are all welcome. This page covers what you need to know to get a change merged.
Reporting a problem
Open an issue with the command
you ran, its full output, your Proxmox VE version and whether you use
use_local_only. Remove passwords and API keys from anything you paste.
Setting up a checkout
# Fork the repository on GitHub, then:
git clone https://github.com/YOUR_USERNAME/lws.git
cd lws
git remote add upstream https://github.com/fabriziosalmi/lws.git
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt # includes pytest, pytest-cov and pytest-mock
pip install ruff==0.15.6 # the version CI uses
LWS supports Python 3.10 to 3.14. The floor is declared once, in
pyproject.toml; tests/test_python_support.py fails if the CI matrix or the
Dockerfile disagree with it.
Checks a pull request has to pass
CI runs on every pull request:
| Check | Command | Blocks the merge |
|---|---|---|
| Tests, Python 3.10 to 3.14 | pytest |
Yes |
| Lint: real errors | ruff check --select=E9,F63,F7,F82 . |
Yes |
| Lint: full rule set | ruff check . |
No, advisory: lws.py and api.py carry older findings |
| Static analysis | slopless (.github/workflows/slopless.yml) |
On errors |
| Documentation site | Jekyll build and link check, when docs/ changes |
Yes |
Run the first two before pushing:
ruff check --select=E9,F63,F7,F82 .
pytest
pytest.ini already adds coverage for lws_core and api and an HTML report
in htmlcov/. The suite is fully mocked: it needs no Proxmox host, SSH access
or Docker. Only the markers unit, integration, slow, ssh and proxmox
are allowed (--strict-markers).
Writing code
Commands
Every command lives in lws.py; lws_commands/ is an empty placeholder. A
new command follows the pattern of the existing ones:
@lxc.command('example')
@click.argument('instance_ids', nargs=-1, callback=_validate_pattern(_VMID_RE, "instance id"))
@click.option('--region', '--location', default='eu-south-1', help="Region in which to operate. Default to eu-south-1")
@click.option('--az', '--node', default='az1', help="Availability zone (Proxmox host) to target. Default to az1")
def example(instance_ids, region, az):
"""Short description shown in --help."""
...
- Validate every value that reaches a remote command. OpenSSH joins the
arguments into one string for the host’s shell, so a value that is safe as
a local argument can still inject a command remotely. Use
_validate_patternwith one of the patterns at the top oflws.py, or_validate_ip_or_cidr, as a click callback. - Run remote commands through
run_proxmox_commandfromlws_core, souse_local_onlyis honoured and the SSH options and password handling stay in one place. - Exit non-zero on failure (
sys.exit(1)). The REST API turns the exit code into the HTTP status, so a failure that exits 0 reaches API clients as a success.
API endpoints
api.py runs lws.py as a subprocess:
@app.route('/api/v1/lxc/instances/<instance_id>/example', methods=['POST'])
@require_api_key
def example_endpoint(instance_id):
data = request.get_json(silent=True) or {}
stdout, stderr, rc = run_lws_command(['lxc', 'example', instance_id], data)
return format_response(stdout, stderr, rc)
run_lws_command(command_parts, data=None, consumed_keys=None) turns the
remaining keys of data into --key value options. Pass consumed_keys for
values you already placed in command_parts, so they are not sent twice.
Non-numeric <instance_id> path segments are rejected before the handler
runs.
Tests
Tests live in tests/ and mock subprocess and SSH. Do not depend on a
config.yaml in the working directory: the file is not tracked. Write one to
tmp_path and change into it, as tests/test_config.py does:
def test_load_config_from_cwd(tmp_path, monkeypatch, sample_config):
(tmp_path / "config.yaml").write_text(yaml.dump(sample_config))
monkeypatch.chdir(tmp_path)
assert load_config()["regions"]
Documentation
The site in docs/ is built by GitHub Pages with Jekyll; docs/README.md
explains its layout and how to preview it. When you change a command, update
docs/_pages/cli-reference.md and, for the API, docs/_pages/api-reference.md.
Two tests keep the documentation honest:
tests/test_docs_examples.pyparses everylwscommand in the documentation and the README with the real CLI. An example with a wrong option or size name fails the suite.tests/test_docs_site.pychecks that every page is indocs/_data/navigation.ymland has a title and description, and that the version on the site matchespyproject.toml.
Commits and pull requests
Commit titles follow a type: summary form, as in the history: fix:,
docs:, ci:, build(deps):, chore:, release:, with a scope where it
helps (fix(api):). Explain in the body why the change is needed.
fix(api): refuse to start without a real API key
An empty api_key left every endpoint unauthenticated...
- Branch from an up-to-date
main:git fetch upstream && git checkout -b fix/short-name upstream/main. - Keep each pull request to one change, with tests for new behaviour.
- Describe what changed and how you checked it. There is no pull request template.
- Pull requests are squash-merged; the title becomes the commit title.
Security issues
SECURITY.md describes how to report a vulnerability. Avoid posting working
exploit details in a public issue.
Conduct
The project follows its Code of Conduct.
License
Contributions are released under the MIT License, like the rest of the project.