Metadata-Version: 2.4
Name: netcup-scp-cli
Version: 1.0.0
Summary: CLI for netcup SCP (Server Control Panel) REST API
Author: Pavel Pikta
License: MIT
Project-URL: Homepage, https://github.com/pavelpikta/netcup-cli
Project-URL: Repository, https://github.com/pavelpikta/netcup-cli
Project-URL: Issues, https://github.com/pavelpikta/netcup-cli/issues
Project-URL: Changelog, https://github.com/pavelpikta/netcup-cli/blob/main/CHANGELOG.md
Project-URL: Documentation, https://forum.netcup.de/netcup-anwendungen/scp-server-control-panel/scp-server-control-panel-rest-api/
Keywords: netcup,scp,server-control-panel,cli,api
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Requires-Dist: click>=8.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: responses>=0.25.0; extra == "dev"
Dynamic: license-file

# Netcup Server Control Panel (SCP) CLI

**Command-line interface for the Netcup Server Control Panel (SCP) REST API.**

Manage servers, rDNS, snapshots, rescue systems, firewall policies, SSH keys, VLANs, and more from the terminal. Uses OAuth2 device-code flow; credentials are stored locally and refreshed automatically.

| | |
| --- | --- |
| **Python** | 3.10+ |
| **License** | MIT |
| **API** | [SCP REST API](https://www.servercontrolpanel.de) · OpenAPI 2026.0703.095128 |

> **Why `netcup` and not `scp`?** The command is named `netcup` to avoid clashing with the standard Unix **scp** (secure copy) command.

---

## Table of contents

- [Overview](#overview)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Authentication](#authentication)
- [Command reference](#command-reference)
- [Configuration](#configuration)
- [Output and exit codes](#output-and-exit-codes)
- [Troubleshooting](#troubleshooting)
- [API compatibility](#api-compatibility)
- [Project structure](#project-structure)
- [Getting help](#getting-help)
- [License](#license)

---

## Overview

netcup CLI talks to the [netcup SCP REST API](https://www.servercontrolpanel.de). It supports:

- **Servers** — List, get, power on/off, set hostname/nickname; manage disks, interfaces, firewall, ISO, snapshots, rescue system, metrics, logs, image setup, guest agent, storage optimization.
- **rDNS** — Get, set, and delete reverse DNS for IPv4 and IPv6.
- **Tasks** — List (with filters), get, and cancel async tasks.
- **Users** — Current user info, get/update user; manage failover IPs, firewall policies, SSH keys, VLANs, user images/ISOs, logs.
- **VLANs** — Get VLAN by ID (standalone).
- **Maintenance** — Ping API and maintenance window info.

All commands that need a **user context** (e.g. `users failoverips list`) use the **current user** from your token by default; override with `--user-id <id>` when needed.

---

## Requirements

- **Python 3.10** or newer
- Network access to `https://www.servercontrolpanel.de`
- A netcup SCP account (device-code login in the browser)

---

## Installation

### From PyPI (when published)

```bash
pip install netcup-scp-cli
```

The PyPI distribution is **`netcup-scp-cli`** (the names `netcup-cli` and `netcupctl` are already taken). The installed command is still **`netcup`**; the Python import package is **`netcup_scp_cli`**.

### From source (development or unreleased)

```bash
cd netcup-cli
pip install -e .
```

After installation, the `netcup` command is available.

**Development (lint, format, test)** — using [uv](https://docs.astral.sh/uv/) (recommended):

```bash
uv sync --all-extras          # create venv + install deps including dev
uv run ruff check src tests   # lint
uv run ruff format src tests  # format
uv run ruff check src tests --fix  # auto-fix lint
uv run pytest                 # tests
```

Without uv: `pip install -e ".[dev]"` then run `ruff` and `pytest` as above.

```bash
netcup --help
```

### Without installing (run as module)

```bash
cd netcup-cli
pip install -r requirements.txt
PYTHONPATH=src python -m netcup_scp_cli --help
```

### Optional: use pipx (isolated environment)

```bash
pipx install netcup-scp-cli
# or from a local clone:
pipx install -e path/to/netcup-cli
```

---

## Quick start

1. **Log in** (opens browser for device code):

   ```bash
   netcup auth login
   ```

   Complete the flow in the browser; the CLI stores a refresh token under `~/.config/netcup-scp-cli/`.

2. **Check API**:

   ```bash
   netcup maintenance ping
   # OK
   ```

3. **List servers**:

   ```bash
   netcup servers list
   ```

4. **Inspect one server**:

   ```bash
   netcup servers get <server_id>
   ```

5. **Power control**:

   ```bash
   netcup servers power <server_id> on
   netcup servers power <server_id> off --option POWEROFF
   ```

6. **rDNS**:

   ```bash
   netcup rdns ipv4 get 1.2.3.4
   netcup rdns ipv4 set 1.2.3.4 host.example.com
   ```

7. **Current user**:

   ```bash
   netcup users info
   ```

For the full command set, see [Command reference](#command-reference).

---

## Authentication

- **Method:** OAuth2 **device code** flow (no password in the CLI).
- **Stored credential:** Only the **refresh token** is saved; access tokens are obtained on demand and not written to disk.
- **Location:** `~/.config/netcup-scp-cli/credentials` (or `$XDG_CONFIG_HOME/netcup-scp-cli/credentials` if set). File format: `{"refresh_token": "…"}`.
- **Lifetime:** Refresh token remains valid as long as it is used at least once every 30 days (per SCP API).

**Commands:**

| Command | Description |
|--------|-------------|
| `netcup auth login` | Start device-code login; optionally `--no-save` to avoid storing the token. |
| `netcup auth logout` | Remove the stored credentials file. |
| `netcup auth revoke` | Revoke the current refresh token on the server and delete the local file. |
| `netcup auth show` | Print the path of the credentials file and whether it exists. |

**Security:**

- The credentials file should be readable only by the current user (the CLI sets mode `0600` when possible).
- Do not share the credentials file or the refresh token.
- Use `netcup auth revoke` if the token may have been exposed.

---

## Command reference

### Global options

- `--help`, `-h` — Show help for the command or group.
- `--version` — Show netcup CLI version.

---

### `netcup auth`

| Command | Description |
|--------|-------------|
| `auth login [--no-save]` | Log in via device code; store refresh token unless `--no-save`. |
| `auth logout` | Remove stored credentials. |
| `auth revoke` | Revoke refresh token and remove credentials. |
| `auth show` | Show credentials file path and existence. |

---

### `netcup servers`

| Command | Arguments / options | Description |
|--------|--------------------|-------------|
| `servers list` | `[--limit N] [--offset N] [--ip IP] [--name NAME] [-q QUERY] [--sort FIELD]` | List servers (optional filters; `--sort` repeatable: `name`, `nickname`, prefix `-` for desc). |
| `servers get <server_id>` | `[--no-live]` | Get one server; `--no-live` skips live info. |
| `servers power <server_id> on\|off` | `[--option POWERCYCLE\|RESET\|POWEROFF]` | Power on/off or power cycle. |
| `servers set-hostname <server_id> <hostname>` | | Set server hostname. |
| `servers set-nickname <server_id> <nickname>` | | Set server nickname. |

**Disks:** `servers disks <server_id>`

| Subcommand | Arguments | Description |
|------------|-----------|-------------|
| `list` | `<server_id>` | List disks. |
| `get` | `<server_id> <disk_name>` | Get one disk. |
| `supported-drivers` | `<server_id>` | List supported storage drivers. |
| `set-driver` | `<server_id> VIRTIO\|VIRTIO_SCSI\|IDE\|SATA` | Set disk driver. |
| `format` | `<server_id> <disk_name>` | Format disk (data loss). |

**Interfaces:** `servers interfaces <server_id>`

| Subcommand | Arguments / options | Description |
|------------|---------------------|-------------|
| `list` | `<server_id> [--no-rdns]` | List interfaces. |
| `get` | `<server_id> <mac> [--no-rdns]` | Get interface by MAC. |
| `create` | `<server_id> --vlan-id ID --driver DRIVER` | Create interface (VLAN); driver: VIRTIO, E1000, E1000E, RTL8139, VMXNET3. |
| `update` | `<server_id> <mac> --body JSON` | Update interface (e.g. driver). |
| `delete` | `<server_id> <mac>` | Delete interface. |

**Firewall (per interface):** `servers firewall <server_id> <mac>`

| Subcommand | Description |
|------------|-------------|
| `get [--consistency-check]` | Get firewall config. |
| `put --body JSON` | Set firewall (copiedPolicies, userPolicies, active). |
| `reapply` | Reapply firewall. |
| `restore-copied-policies` | Restore copied policies. |

**ISO:** `servers iso <server_id>`

| Subcommand | Options | Description |
|------------|---------|-------------|
| `get` | | Get attached ISO. |
| `attach` | `--iso-id ID` or `--user-iso NAME`; `[--boot-cdrom]` | Attach ISO. |
| `detach` | | Detach ISO. |
| `images` | | List available ISO images for server. |

**Snapshots:** `servers snapshots <server_id>`

| Subcommand | Arguments / options | Description |
|------------|---------------------|-------------|
| `list` | `<server_id>` | List snapshots. |
| `get` | `<server_id> <name>` | Get one snapshot. |
| `create` | `<server_id> --name NAME [--description DESC] [--disk-name NAME] [--online]` | Create snapshot. |
| `delete` | `<server_id> <name>` | Delete snapshot. |
| `export` | `<server_id> <name>` | Export snapshot. |
| `revert` | `<server_id> <name>` | Revert to snapshot. |
| `dryrun` | `<server_id> [--disk-name NAME] [--online]` | Check if snapshot creation is possible. |

**Rescue system:** `servers rescue <server_id>`

| Subcommand | Description |
|------------|-------------|
| `get` | Get rescue system status. |
| `activate` | Activate rescue system. |
| `deactivate` | Deactivate rescue system. |

**Metrics:** `servers metrics <server_id>`

| Subcommand | Options | Description |
|------------|---------|-------------|
| `cpu` | `[--hours N]` | CPU metrics. |
| `disk` | `[--hours N]` | Disk metrics. |
| `network` | `[--hours N]` | Network metrics. |
| `packet` | `[--hours N]` | Network packet metrics. |

**Other server commands**

| Command | Arguments / options | Description |
|--------|---------------------|-------------|
| `servers logs list` | `<server_id> [--limit N] [--offset N]` | Server logs. |
| `servers guest-agent get` | `<server_id>` | Guest agent data. |
| `servers image flavours` | `<server_id>` | Image flavours for setup. |
| `servers image setup` | `<server_id> --body JSON` | Setup image (JSON body per API). |
| `servers user-image setup` | `<server_id> --name NAME [--disk-name NAME] [--email-notification]` | Setup user image. |
| `servers storage-optimization start` | `<server_id> [--disks NAME...] [--start-after]` | Start storage optimization. |

---

### `netcup rdns`

| Command | Example |
|--------|---------|
| `rdns ipv4 get <ip>` | `netcup rdns ipv4 get 1.2.3.4` |
| `rdns ipv4 set <ip> <rdns>` | `netcup rdns ipv4 set 1.2.3.4 host.example.com` |
| `rdns ipv4 delete <ip>` | |
| `rdns ipv6 get <ip>` | |
| `rdns ipv6 set <ip> <rdns>` | |
| `rdns ipv6 delete <ip>` | |

---

### `netcup tasks`

| Command | Options | Description |
|--------|---------|-------------|
| `tasks list` | `[--limit N] [--offset N] [-q QUERY] [--server-id ID] [--state STATE]` | List tasks. |
| `tasks get <uuid>` | | Get one task. |
| `tasks cancel <uuid>` | | Cancel task. |

---

### `netcup users`

| Command | Options | Description |
|--------|---------|-------------|
| `users info` | | Current user (from token). |
| `users get <user_id>` | | Get user by ID. |
| `users update <user_id> --body JSON` | | Update user (UserSave schema). |

All commands below support **`--user-id <id>`**; if omitted, the current user is used.

**Failover IPs:** `users failoverips v4|v6`

| Subcommand | Example |
|------------|---------|
| `v4 list` | `[--ip IP] [--server-id ID]` |
| `v4 route <id> <server_id>` | Route failover IPv4 to server. |
| `v6 list` | Same options as v4. |
| `v6 route <id> <server_id>` | Route failover IPv6 to server. |

**Firewall policies:** `users firewall-policies`

| Subcommand | Options |
|------------|---------|
| `list` | `[--limit N] [--offset N] [-q QUERY]` |
| `get <policy_id>` | `[--with-servers-count]` |
| `create --body JSON` | JSON: name, optional description, rules. |
| `update <policy_id> --body JSON` | |
| `delete <policy_id>` | |

**SSH keys:** `users ssh-keys`

| Subcommand | Options |
|------------|---------|
| `list` | |
| `add --name NAME --key KEY` | Add public key. |
| `delete <key_id>` | |

**VLANs:** `users vlans`

| Subcommand | Options |
|------------|---------|
| `list` | `[--server-id ID]` |
| `get <vlan_id>` | |
| `update <vlan_id> <name>` | |

**User images (S3):** `users images`

| Subcommand | Description |
|------------|-------------|
| `list` | List user images. |
| `delete <key>` | Delete image by key. |
| `download-url <key>` | Get presigned download URL. |
| `upload <key> <path>` | Upload local file (`--multipart` / `--no-multipart`, `--part-size`). |

**User ISOs (S3):** `users isos`

| Subcommand | Description |
|------------|-------------|
| `list` | List user ISOs. |
| `delete <key>` | Delete ISO by key. |
| `download-url <key>` | Get presigned download URL. |
| `upload <key> <path>` | Upload local file (`--multipart` / `--no-multipart`, `--part-size`). |

**User logs:** `users logs list [--limit N] [--offset N]`

---

### `netcup vlans`

| Command | Description |
|--------|-------------|
| `vlans get <vlan_id>` | Get VLAN by ID (no user scope). |

---

### `netcup maintenance`

| Command | Description |
|--------|-------------|
| `maintenance ping` | Check API availability (returns OK). |
| `maintenance info` | Maintenance window information. |

---

## Configuration

### Credentials

- **Path:** `~/.config/netcup-scp-cli/credentials` (Linux/macOS). On Windows, `%APPDATA%\netcup-scp-cli\credentials` is not used by default; the CLI uses `$XDG_CONFIG_HOME` if set, otherwise `~/.config`.
- **Override directory:** Set `XDG_CONFIG_HOME` (e.g. `export XDG_CONFIG_HOME=$HOME/.config`).
- **Format:** JSON with a single key: `{"refresh_token": "…"}`. Do not edit manually unless you know the token value.

### API endpoints (hardcoded)

The CLI uses these base URLs (not configurable via env or file):

- **Auth:** `https://www.servercontrolpanel.de/realms/scp/protocol/openid-connect`
- **API root:** `https://www.servercontrolpanel.de/scp-core/api`
- **API v1:** `https://www.servercontrolpanel.de/scp-core/api/v1`

To point at another environment, you would need to change the code in `netcup_scp_cli/config.py`.

---

## Output and exit codes

- **Success:** Exit code `0`. List/get commands print JSON to stdout (pretty-printed).
- **Failure:** Exit code `1`. Error message is printed to stderr (and often in red when run in a TTY).
- **JSON:** All API responses that return JSON are printed as-is (indented). Use `jq` for further processing if needed, e.g. `netcup servers list | jq '.[].name'`.

---

## Troubleshooting

### `API error: 406`

- **Cause:** The server could not satisfy the `Accept` header (e.g. endpoint returns `text/plain` but client asked for `application/json`).
- **Relevant command:** `maintenance ping` — fixed in current version by requesting `Accept: text/plain` for that call. Ensure you use the latest code.

### `No stored credentials. Run: netcup auth login`

- **Cause:** No refresh token found in the credentials file.
- **Fix:** Run `netcup auth login` and complete the browser flow. Ensure the credentials file exists at the path shown by `netcup auth show`.

### `Token refresh failed: 401` or `403`

- **Cause:** Refresh token expired or was revoked (e.g. after 30 days of no use, or revoked in SCP account).
- **Fix:** Run `netcup auth login` again. If needed, revoke old access in SCP: Account → Applications → scp → Remove access, then log in again.

### `Access denied on device authorization`

- **Cause:** You declined the authorization in the browser or closed the page before approving.
- **Fix:** Run `netcup auth login` again and approve the application in the browser.

### `Device code expired`

- **Cause:** You did not complete the browser step within the validity window (e.g. 600 seconds).
- **Fix:** Run `netcup auth login` again and complete the flow promptly.

### `API error: 404` / `422` / `503`

- **Cause:** Resource not found, validation error, or service temporarily unavailable (e.g. maintenance).
- **Action:** Check IDs and request body; for 503, retry later or check maintenance info: `netcup maintenance info`.

---

## API compatibility

- **Spec:** The CLI is built against the SCP REST API as described in the OpenAPI spec (version **2026.0703.095128** in the bundled `openapi.json`).
- **Coverage:** Most endpoints from the spec are implemented (servers, rDNS, tasks, users, failover IPs, firewall policies, SSH keys, VLANs, images/ISOs including S3 upload, logs, maintenance). Intentionally skipped: `GET /openapi` (use bundled snapshot) and `POST /openapi/mcp` (explore-only; no CLI wrapper). Coverage is guarded by `tests/test_openapi_coverage.py`.
- **Breaking changes:** If the API introduces breaking changes, the CLI may need updates. Check the [netcup SCP API forum](https://forum.netcup.de/netcup-anwendungen/scp-server-control-panel/scp-server-control-panel-rest-api/) and release notes.

### OpenAPI spec file

The file **`openapi.json`** in the project root is a snapshot of the spec fetched from:

```bash
curl -s -X GET 'https://www.servercontrolpanel.de/scp-core/api/v1/openapi' -H 'accept: application/json' -o openapi.json
```

You can refresh it to align with the latest API. The CLI does not read this file at runtime; it is for reference and for the OpenAPI coverage tests.

### SCP OpenAPI MCP endpoint (Miscellaneous)

The bundled spec documents **`POST /v1/openapi/mcp`** (tag **Miscellaneous**; some explorers label it like `#/Miscellaneous/post_api_v1_openapi_mcp`). Summary: **SCP OpenAPI Spec MCP Server**.

**Purpose (from the spec):** SCP (Server Control Panel) API Explorer — OpenAPI specification explorer for the netcup SCP REST API. It provides documentation, schema definitions, and guidance for building SCP API clients and scripts. **It does not execute SCP API calls directly.**

**Full URL:** `https://www.servercontrolpanel.de/scp-core/api/v1/openapi/mcp`

Use this with an MCP-capable client if you want explore-only access to the API surface. It uses the same **`https://www.servercontrolpanel.de/scp-core/api/v1`** base as other authenticated routes; this CLI does not ship a wrapper for the MCP protocol (JSON-RPC message exchange over HTTP).

---

## Project structure

```
netcup-cli/                    # git repo (local clone)
├── pyproject.toml          # Package metadata (PyPI name: netcup-scp-cli)
├── requirements.txt        # requests, click
├── README.md               # This file
├── openapi.json            # Snapshot of SCP OpenAPI spec
├── .github/workflows/ci.yml
├── tests/                  # OpenAPI coverage + unit tests
└── src/netcup_scp_cli/
    ├── __init__.py         # Version
    ├── config.py           # URLs, credential path
    ├── auth.py             # Device code, refresh, revoke, load/save credentials
    ├── client.py           # HTTP client (Bearer token, Accept header)
    ├── exceptions.py       # SCPError, AuthError, APIError, ConfigError
    ├── output.py           # JSON formatting
    ├── api/                # API layer (one module per resource)
    │   ├── base.py
    │   ├── s3_upload.py    # Shared S3 multipart / single-shot upload helper
    │   ├── maintenance.py
    │   ├── rdns.py
    │   ├── servers.py
    │   ├── servers_disks.py
    │   ├── servers_interfaces.py
    │   ├── servers_iso.py
    │   ├── servers_snapshots.py
    │   ├── servers_rescue.py
    │   ├── servers_metrics.py
    │   ├── servers_logs.py
    │   ├── servers_image.py
    │   ├── servers_guest.py
    │   ├── servers_gpu.py
    │   ├── servers_storage.py
    │   ├── tasks.py
    │   ├── users.py
    │   ├── user_failoverips.py
    │   ├── user_firewall_policies.py
    │   ├── user_ssh_keys.py
    │   ├── user_vlans.py
    │   ├── user_images.py
    │   ├── user_isos.py
    │   └── user_logs.py
    └── cli/                # Click commands
        ├── main.py         # Entry point, group registration
        ├── helpers.py      # user_id resolution
        ├── auth_cmd.py
        ├── servers_cmd.py
        ├── servers_disks_cmd.py
        ├── servers_interfaces_cmd.py
        ├── servers_iso_cmd.py
        ├── servers_snapshots_cmd.py
        ├── servers_rescue_cmd.py
        ├── servers_metrics_cmd.py
        ├── servers_misc_cmd.py
        ├── rdns_cmd.py
        ├── tasks_cmd.py
        ├── users_cmd.py
        ├── users_resources_cmd.py
        ├── vlans_cmd.py
        └── maintenance_cmd.py
```

---

## Getting help

- **General:** `netcup --help`
- **Command group:** `netcup servers --help`, `netcup users --help`, etc.
- **Specific command:** `netcup servers power --help`, `netcup rdns ipv4 set --help`
- **Version:** `netcup --version`

---

## License

MIT. See the [LICENSE](LICENSE) file in the project repository for the full license text.

---

## Links

- [SCP (Server Control Panel)](https://www.servercontrolpanel.de)
- [netcup SCP REST API (Forum)](https://forum.netcup.de/netcup-anwendungen/scp-server-control-panel/scp-server-control-panel-rest-api/)
