Metadata-Version: 2.4
Name: boxctl
Version: 0.2.0
Summary: Manage VirtualBox virtual machines from the command line
Keywords: virtualbox,vbox,vm,cloud-init,vboxmanage
Author: Optersoft, S.L.
Author-email: Optersoft, S.L. <david@optersoft.com>
License-Expression: MIT OR Apache-2.0
License-File: LICENSE-APACHE
License-File: LICENSE-MIT
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://academy.optersoft.com/tool/box
Project-URL: Repository, https://github.com/optersoft/box
Project-URL: Issues, https://github.com/optersoft/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://github.com/optersoft/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 lesson that goes with it is at [xtec.dev/tool/box](https://xtec.dev/tool/box).

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: the current Ubuntu LTS
box create alfa --image ubuntu:24.04   # a specific release
box create alfa --image fedora         # the current Fedora Cloud Base
box create alfa --image coreos         # Fedora CoreOS, stable stream
box images                             # what each of those resolves to today
```

| Image | Source | Provisioning |
|---|---|---|
| `ubuntu` | the release qcow2 from Ubuntu's cloud image index (simplestreams); `ubuntu:<version>` pins one | cloud-init seed ISO |
| `fedora` | the Generic qcow2 from Fedora's release index; `fedora:<version>` pins one | cloud-init seed ISO |
| `coreos` | the VirtualBox OVA from the Fedora CoreOS stable stream | Ignition guest property |

box asks the distro's own index which build is current, checks the download
against the SHA-256 it publishes, and caches it under a name that carries the
build — so a new release is a new download, never a code change. The index is
cached for a day; offline, box uses the last one it saw, and on a first run
with no network at all, a built-in pin of the last known build.

Ubuntu and Fedora machines are built by box itself from the qcow2 (converted
to a VDI; SATA disk, IDE seed DVD, PIIX3 chipset so the NICs are `enp0s3` and
`enp0s8`). CoreOS is imported from its OVA, which is made for VirtualBox.

## 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 (Ubuntu), 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.

To install VirtualBox on Windows unattended, in an Administrator PowerShell:

```powershell
iex (irm https://get.optersoft.com/vbox.ps1)
```

It installs the Visual C++ redistributable VirtualBox requires first — without
it the installer exits 1 having printed nothing — and then reports whether the
host can actually run VMs, which is the thing most likely to be missing on a
cloud or VDI machine.

## 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

Licensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE),
at your option. Unless you state otherwise, any contribution you submit for
inclusion is dual-licensed on those same terms.

Copyright © 2026 Optersoft, S.L.
