Metadata-Version: 2.4
Name: wg-api-client
Version: 0.2.1
Summary: WireGuard Configuration API Client
Home-page: https://github.com/tiiuae/wg-api-client-lib
Author: TII UAE
Author-email: info@tii.ae
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Networking
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests<3.0.0,>=2.25.0
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# wg-api-client

Python client library and CLI for the WireGuard Configuration
Distribution API. Version 0.2.1.

## Installation

Requires Python 3.9 or later.

```bash
pip install wg-api-client
```

From source:

```bash
git clone https://github.com/tiiuae/wg-api-client-lib.git
cd wg-api-client-lib
pip install -e .
```

Key generation requires `wireguard-tools`:

```bash
# Debian / Ubuntu
sudo apt install -y wireguard-tools
```

Runtime dependency: `requests >=2.25.0, <3.0.0`.

## Configuration

Settings are read from environment variables, CLI flags, or a
config file. No credentials or URLs are hardcoded.

### Environment variables

| Variable            | Required | Description                                    |
| ------------------- | -------- | ---------------------------------------------- |
| `WG_API_URL`        | yes      | API base URL (e.g. `https://host:8080/api/v1`) |
| `WG_API_CREDENTIAL` | yes      | SHA-256 hashed credential                      |
| `WG_API_CA_CERT`    | no       | Path to CA certificate bundle for TLS          |

### Config file

Stored at `~/.wg_api_config` (permissions `0600`). Created on
first successful `auth`. Uses INI format with `[General]` and
`[Auth]` sections.

### .env.example

Copy `.env.example` to `.env` and fill in the values. Generate a
credential hash with:

```bash
echo -n "your-secret" | sha256sum | awk '{print $1}'
```

## CLI usage

```text
wg-api-client [global flags] <command> [command flags]
```

### Global flags

| Flag                          | Env variable        | Description                               |
| ----------------------------- | ------------------- | ----------------------------------------- |
| `--api-url URL`               | `WG_API_URL`        | API base URL                              |
| `--hashed-credential HASH`    | `WG_API_CREDENTIAL` | Hashed credential                         |
| `--ca-cert PATH`              | `WG_API_CA_CERT`    | CA certificate bundle                     |
| `--config-file PATH`          |                     | Config file (default: `~/.wg_api_config`) |
| `--output-format {text,json}` |                     | Output format (default: `text`)           |

### Commands

#### auth

Authenticate and store the JWT token in the config file.

```bash
wg-api-client auth
```

#### get-config

Register a device and write a WireGuard configuration file.

```bash
wg-api-client get-config \
  --cluster-id CLUSTER_ID \
  --role node \
  --output wg.conf
```

| Flag            | Description                                                   |
| --------------- | ------------------------------------------------------------- |
| `--cluster-id`  | Cluster to join (required)                                    |
| `--role`        | Device role: `node`, `controller`, or `uxu` (default: `node`) |
| `--device-id`   | Custom device ID (auto-generated from hardware if omitted)    |
| `--public-key`  | WireGuard public key (generated if omitted)                   |
| `--output`      | Output file path (default: `wg.conf`)                         |
| `--allowed-ips` | Additional allowed IP ranges (repeatable)                     |
| `--table`       | Routing table for the WireGuard interface                     |
| `--listen-port` | Listen port for the WireGuard interface                       |

#### list-devices

List all registered devices (admin).

```bash
wg-api-client list-devices
wg-api-client --output-format json list-devices
```

#### get-device

Get details for a single device (admin).

```bash
wg-api-client get-device DEVICE_ID
```

#### delete-device

Delete a device (admin). Supports `--dry-run`.

```bash
wg-api-client delete-device DEVICE_ID --dry-run
wg-api-client delete-device DEVICE_ID
```

#### delete-all-devices

Delete all devices (admin). Requires `--confirm` or interactive
confirmation. Supports `--dry-run`.

```bash
wg-api-client delete-all-devices --dry-run
wg-api-client delete-all-devices --confirm
```

#### create-cluster

Create a new cluster (admin).

```bash
wg-api-client create-cluster production
wg-api-client --output-format json create-cluster staging
```

#### list-clusters

List all clusters (admin).

```bash
wg-api-client list-clusters
```

#### get-cluster

Get cluster details (admin).

```bash
wg-api-client get-cluster CLUSTER_ID
```

#### delete-cluster

Delete a cluster and all its devices (admin). Supports
`--dry-run`.

```bash
wg-api-client delete-cluster CLUSTER_ID --dry-run
wg-api-client delete-cluster CLUSTER_ID
```

#### get-controller

Get the controller device for a cluster (admin).

```bash
wg-api-client get-controller CLUSTER_ID
```

#### list-cluster-devices

List all devices in a cluster (admin).

```bash
wg-api-client list-cluster-devices CLUSTER_ID
```

#### get-uxu

Get the UXU device (admin).

```bash
wg-api-client get-uxu
```

#### add-credential

Add a new API credential (admin).

```bash
wg-api-client add-credential \
  --hashed-credential HASH \
  --role admin
```

`--role` accepts `user` (default) or `admin`.

#### health

Check API server health. Does not require authentication.

```bash
wg-api-client health
wg-api-client --output-format json health
```

#### version

Get API server version.

```bash
wg-api-client version
```

#### completion

Generate shell completion scripts.

```bash
# bash
eval "$(wg-api-client completion bash)"

# zsh
eval "$(wg-api-client completion zsh)"
```

## Device roles

| Role         | Description                             | Default |
| ------------ | --------------------------------------- | ------- |
| `node`       | Spoke device, can only reach controller | yes     |
| `controller` | Hub device, one per cluster             | no      |
| `uxu`        | Operator device, global access, unique  | no      |

## Python library

### Basic usage

```python
from wg_api_client import WireGuardAPI, WireGuardHelper
from wg_api_client.unique_id import get_unique_device_id

api = WireGuardAPI(
    api_url="https://vpn.example.com/api/v1",
    hashed_credential="your-sha256-hash",
)

success, message = api.authenticate()

private_key, public_key = WireGuardHelper.generate_keypair()
device_id = get_unique_device_id()

success, config_data = api.request_wireguard_config(
    device_id=device_id,
    role="node",
    public_key=public_key,
    cluster_id="your-cluster-id",
)

if success:
    WireGuardHelper.create_client_config(
        config_data, "wg.conf", private_key=private_key
    )
```

### Factory from environment

```python
from wg_api_client import WireGuardAPI

api = WireGuardAPI.from_env()
```

Reads `WG_API_URL` (required), `WG_API_CREDENTIAL`, and
`WG_API_CA_CERT` from the environment.

### Context manager

```python
with WireGuardAPI.from_env() as api:
    api.authenticate()
    success, devices = api.list_devices()
```

The session is closed automatically on exit.

### Cluster operations

```python
success, data = api.create_cluster("production")
cluster_id = data["id"]

success, clusters = api.list_clusters()
success, devices = api.list_cluster_devices(cluster_id)
success, controller = api.get_cluster_controller(cluster_id)
success, message = api.delete_cluster(cluster_id)
```

### Admin operations

```python
success, devices = api.list_devices()
success, device = api.get_device("device-id")
success, message = api.delete_device("device-id")
success, uxu = api.get_uxu_device()
success, message = api.add_credential("sha256-hash", "admin")
```

## API reference

### WireGuardAPI

Constructor parameters:

| Parameter           | Type          | Default    | Description                       |
| ------------------- | ------------- | ---------- | --------------------------------- |
| `api_url`           | `str`         | (required) | Base URL for the API              |
| `hashed_credential` | `str`         | `None`     | Pre-hashed credential             |
| `token`             | `str`         | `None`     | Existing JWT token                |
| `verify`            | `bool \| str` | `True`     | TLS verify flag or CA bundle path |
| `timeout`           | `int`         | `10`       | Request timeout in seconds        |
| `max_retries`       | `int`         | `3`        | Retries on 502/503/504            |
| `backoff_factor`    | `float`       | `0.5`      | Exponential backoff factor        |

Methods:

| Method                                                              | Returns           | Pagination | Auth  | Description                        |
| ------------------------------------------------------------------- | ----------------- | ---------- | ----- | ---------------------------------- |
| `health_check()`                                                    | `InfoResponse`    | no         | none  | Check server health                |
| `get_version()`                                                     | `InfoResponse`    | no         | none  | Get server version                 |
| `authenticate()`                                                    | `MessageResponse` | no         | cred  | Login, obtain JWT                  |
| `refresh_auth_token()`                                              | `MessageResponse` | no         | token | Refresh JWT                        |
| `ensure_authenticated()`                                            | `bool`            | no         | auto  | Auto-refresh if needed             |
| `request_wireguard_config(device_id, role, public_key, cluster_id)` | `DeviceResponse`  | no         | JWT   | Register device, get config        |
| `list_devices(limit, offset)`                                       | `ListResponse`    | yes        | admin | List all devices                   |
| `get_device(device_id)`                                             | `DeviceResponse`  | no         | admin | Get device details                 |
| `delete_device(device_id)`                                          | `MessageResponse` | no         | admin | Delete a device                    |
| `delete_all_devices()`                                              | `MessageResponse` | no         | admin | Delete all devices                 |
| `create_cluster(name)`                                              | `ClusterResponse` | no         | admin | Create a cluster                   |
| `list_clusters(limit, offset)`                                      | `ListResponse`    | yes        | admin | List clusters                      |
| `get_cluster(cluster_id)`                                           | `ClusterResponse` | no         | admin | Get cluster details                |
| `delete_cluster(cluster_id)`                                        | `MessageResponse` | no         | admin | Delete cluster + devices           |
| `get_cluster_controller(cluster_id)`                                | `DeviceResponse`  | no         | admin | Get cluster controller             |
| `list_cluster_devices(cluster_id, limit, offset)`                   | `ListResponse`    | yes        | admin | List devices in cluster            |
| `get_uxu_device()`                                                  | `DeviceResponse`  | no         | admin | Get UXU device                     |
| `add_credential(hashed_credential, role)`                           | `MessageResponse` | no         | admin | Add API credential                 |
| `from_env()`                                                        | `WireGuardAPI`    | --         | --    | Class method: create from env vars |
| `close()`                                                           | `None`            | --         | --    | Close HTTP session                 |

### WireGuardHelper

| Method                                                                                                    | Description                                                                                                                  |
| --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `generate_keypair()`                                                                                      | Generate a WireGuard private/public key pair. Returns `(private_key, public_key)`.                                           |
| `create_client_config(config_data, output_file, additional_allowed_ips, table, listen_port, private_key)` | Write a WireGuard config file with `0600` permissions. Validates IP format, key length, endpoint format, and path traversal. |

### ConfigManager

| Method                                                 | Description                                    |
| ------------------------------------------------------ | ---------------------------------------------- |
| `ConfigManager(config_file)`                           | Constructor. Default path: `~/.wg_api_config`. |
| `load_config()`                                        | Load config from file.                         |
| `save_config()`                                        | Save config to file with `0600` permissions.   |
| `get_api_url()` / `set_api_url(url)`                   | Read/write API URL.                            |
| `get_hashed_credential()` / `set_hashed_credential(c)` | Read/write credential.                         |
| `get_token()` / `set_token(token)`                     | Read/write JWT token.                          |
| `get_refresh_token()` / `set_refresh_token(token)`     | Read/write refresh token.                      |
| `get_token_expires_at()` / `set_token_expires_at(ts)`  | Read/write token expiry timestamp.             |

## Exception hierarchy

```text
WireGuardAPIError
  +-- AuthenticationError
  +-- APIConnectionError
  +-- TLSError
  +-- APITimeoutError
```

All exceptions accept `message` and `original_error` parameters.
The base `WireGuardAPIError` stores the original exception as
`original_error`.

## Type aliases

Defined in `wg_api_client.types`:

| Alias             | Definition                          | Used by                                                 |
| ----------------- | ----------------------------------- | ------------------------------------------------------- |
| `InfoResponse`    | `Tuple[bool, Dict[str, Any]]`       | `health_check`, `get_version`                           |
| `MessageResponse` | `Tuple[bool, str]`                  | `authenticate`, `delete_device`, `add_credential`, etc. |
| `DeviceResponse`  | `Tuple[bool, Dict[str, Any]]`       | `request_wireguard_config`, `get_device`, etc.          |
| `ListResponse`    | `Tuple[bool, List[Dict[str, Any]]]` | `list_devices`, `list_clusters`, `list_cluster_devices` |
| `ClusterResponse` | `Tuple[bool, Dict[str, Any]]`       | `create_cluster`, `get_cluster`                         |

## Security

- No hardcoded credentials or API URLs.
- Credentials accepted via environment variable (not visible in
  process listings when using env vars).
- TLS verification enabled by default; custom CA bundle supported
  via `--ca-cert` or `WG_API_CA_CERT`.
- Config and key files written atomically with `0600` permissions.
- Private keys passed explicitly through the call chain, not
  stored as class-level state.
- API response data (IP addresses, public keys, endpoints)
  validated before writing config files.
- URL path parameters are percent-encoded.
- SSL errors are classified into specific, actionable messages.
- Retry with exponential backoff on transient HTTP errors
  (502, 503, 504).
- Output path validated against directory traversal.

## Development

Install dev dependencies:

```bash
pip install -r requirements-dev.txt
pip install -e .
```

### Test

```bash
pytest -v
```

### Lint

```bash
black --check wg_api_client/ tests/
isort --check wg_api_client/ tests/
flake8 wg_api_client/ tests/ --max-line-length 120
bandit -r wg_api_client/ -c pyproject.toml
```

CI runs lint, tests (Python 3.9--3.12), build verification, and
publishes to PyPI on version tags.

## License

Apache 2.0
