Metadata-Version: 2.4
Name: boxctl
Version: 0.1.0
Summary: Manage VirtualBox virtual machines from the command line
Keywords: virtualbox,vbox,vm,cloud-init,vboxmanage
Author: David de Mingo
Author-email: David de Mingo <david@xtec.dev>
License-Expression: AGPL-3.0-only
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Education
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Systems Administration
Classifier: Topic :: Utilities
Requires-Dist: cryptography>=42.0
Requires-Dist: httpx>=0.27
Requires-Dist: pycdlib>=1.14
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Requires-Python: >=3.11
Project-URL: Homepage, https://gitlab.com/xtec/tool/box
Project-URL: Repository, https://gitlab.com/xtec/tool/box
Project-URL: Issues, https://gitlab.com/xtec/tool/box/-/issues
Description-Content-Type: text/markdown

# box

Manage **VirtualBox** virtual machines from the command line.

`box` imports an Ubuntu cloud image as a VirtualBox machine, seeds it with
cloud-init (a passwordless-sudo `box` user, your SSH key, a static host-only
address) and gets you a shell in it — one command per lifecycle step, no
`VBoxManage` incantations.

It is a Python port of the `box vbox` provider from
[gitlab.com/xtec/box](https://gitlab.com/xtec/box), packaged on its own so it
installs with `uv`/`pip` instead of a Rust toolchain. Sibling of
[`wslx`](https://gitlab.com/xtec/tool/wsl), which ports the WSL provider the
same way. Machines created by either tool are interchangeable: same config
directory, same cloud-init, same `box` user, same SSH key.

## Install

```console
uv tool install boxctl
```

or run it without installing:

```console
uvx --from boxctl box list
```

`pip install boxctl` works too. Python 3.11+.

The distribution is named **`boxctl`** because `box` is taken on PyPI; the
command it installs is **`box`**. A `boxctl` alias is installed alongside it in
case something else on your `PATH` already answers to `box`.

## Use

```console
box create alfa           # import a new Ubuntu machine named alfa
box ssh alfa              # start it if needed, wait for sshd, open a shell
box list                  # every machine box made, with state and address
box stop alfa             # ACPI power button, wait for poweroff
box start alfa -c 4 -m 4096   # boot headless with 4 CPUs and 4 GB RAM
box resize alfa 40        # grow the disk to 40 GB, and the partition inside it
box delete alfa           # power off, unregister, delete the disks
```

`create`, `start`, `stop` and `delete` take any number of names:

```console
box create alfa beta gamma
```

## Images

```console
box create alfa --image ubuntu   # default
box create alfa --image coreos
box create alfa --image fedora
```

| Image | Source | Provisioning |
|---|---|---|
| `ubuntu` | Ubuntu 22.04 cloud OVA, downloaded and cached | cloud-init seed ISO |
| `coreos` | Fedora CoreOS 37 OVA, downloaded and cached | Ignition guest property |
| `fedora` | `<config>/ova/fedora-37.ova`, **supplied by you** | none |

Fedora Cloud publishes no VirtualBox appliance, so there is nothing for box to
download and no network layout it can rely on; drop your own OVA at that path
and box will import it as-is.

## What a machine looks like

- user **`box`**, password `password`, passwordless `sudo`, your
  `~/.ssh/id_ed25519.pub` in `authorized_keys`
- a NAT adapter for outbound traffic (`enp0s3`) and a host-only adapter
  (`enp0s8`) with a **static address**, so `box ssh` works without DHCP leases
- hostname set to the machine name
- snapd disabled, floppy controller blacklisted, cloud-init disabled after the
  first boot

### Addresses

Host-only addresses are allocated by box from `192.168.56.15` upward and stored
in the machine's own VirtualBox extra data, so they survive reboots. The range
stops at `.100`: VirtualBox's host-only interface is `.1` and its DHCP server
hands out `.101`–`.254`.

## Where things live

| What | Where |
|---|---|
| Machines (`.vbox`, disks, `seed.iso`) | `~/.config/box/virtualbox/<name>/` |
| Downloaded OVA appliances | `~/.config/box/cache/` |
| Locally supplied appliances | `~/.config/box/ova/` |
| SSH key pair | `~/.ssh/id_ed25519` |

On Windows the config directory is `%APPDATA%\box` instead. `box delete`
removes a machine's folder; the SSH key and the OVA cache are left alone.

`box list` only shows machines box created — it owns a directory per machine,
and that listing is what it walks. A directory whose machine VirtualBox no
longer knows about (deleted from the GUI, say) is pruned as a side effect.

## Requirements

`VBoxManage` on `PATH` — on Windows box also honours the installer's
`VBOX_MSI_INSTALL_PATH`. `box ssh` and `box resize` additionally need an `ssh`
client. The package installs and imports anywhere so it can be developed and
tested on a machine with no VirtualBox at all.

## Development

```console
uv sync --all-groups
uv run pytest
uv run ruff check
```

The test suite needs neither VirtualBox nor a network: everything that shells
out is covered through its pure parts — `VBoxManage` output parsing, host
address allocation, cloud-init and Ignition rendering, seed ISO structure, key
generation — with `subprocess` stubbed at the boundary.

## Differences from the Rust `box vbox`

Behaviour is otherwise a faithful port.

- `box stop` gives up with an error instead of pressing the ACPI button
  forever when a guest ignores it.
- `VBoxManage` output parsing keeps values containing `=` (`rec_screen_opts`),
  which the Rust port's regexes silently dropped.
- The generated private key always uses LF line endings, on Windows too. The
  Rust port wrote CRLF and then had to `sed` the CRs out again after copying
  the key into a machine.
- The netplan file box writes declares `version: 2`, which netplan requires.
- The seed ISO is built with `pycdlib` rather than a hand-rolled ISO 9660
  writer, and carries Joliet and Rock Ridge so the guest sees `user-data`
  rather than its 8.3 alias.
- `box ip <name>` is new; `box update` is gone, since `uv tool upgrade boxctl`
  does that job.

## License

AGPL-3.0-only. © David de Mingo.
