Metadata-Version: 2.4
Name: vpseasy
Version: 0.0.4
Summary: create resources, deploy code in prod and dev. supports hetzner and multipass
Project-URL: Repository, https://github.com/vedicreader/vpseasy
Project-URL: Documentation, https://vedicreader.github.io/vpseasy/
Author-email: Karthik <karthik.rajgopal@hotmail.com>
License: Apache-2.0
License-File: LICENSE
Keywords: nbdev
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Requires-Python: >=3.10
Requires-Dist: dockeasy>=0.0.3
Requires-Dist: fastcloudinit>=0.0.5
Requires-Dist: fastcore>=1.12.31
Requires-Dist: hcloud>=2.17.0
Description-Content-Type: text/markdown

# vpseasy


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Install

``` sh
pip install vpseasy
```

``` python
pub_keys = load_pub_keys()
print(f'{len(pub_keys)} key(s) found')
if pub_keys: print(pub_keys[0][:3] + '...')
```

    4 key(s) found
    ssh...

## Cloud-init

[`multi_init()`](https://vedicreader.github.io/vpseasy/core.html#multi_init)
— local Multipass VMs (no UFW).
[`vps_init()`](https://vedicreader.github.io/vpseasy/core.html#vps_init)
— production (UFW, fail2ban, Docker).

``` python
_vm = 'testvm'
mi = multi_init(_vm, docker=False)   # docker=False: skip install+reboot, much faster for local testing
print(mi.yaml)
```

    #cloud-config
    hostname: testvm
    preserve_hostname: false
    packages:
    - curl
    package_update: true
    package_upgrade: true
    disable_root: true
    ssh_pwauth: false
    users:
    - name: deploy
      groups:
      - sudo
      shell: /bin/bash
      sudo:
      - ALL=(ALL) NOPASSWD:ALL
      ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA8djqnLMaZGgYVtiPJCGtDutbid1rnHNuExJolxyXST 71293@MELMAC-71293

``` python
ci = vps_init('demo-prod')
print(ci.yaml)
```

    #cloud-config
    hostname: demo-prod
    preserve_hostname: false
    packages:
    - curl
    - fail2ban
    - unattended-upgrades
    package_update: true
    package_upgrade: true
    disable_root: true
    ssh_pwauth: false
    users:
    - name: deploy
      groups:
      - sudo
      shell: /bin/bash
      sudo:
      - ALL=(ALL) NOPASSWD:ALL
      ssh_authorized_keys:
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGe5xfUhopf+J8VLThytJWd1yO5ad8sWenfkoAtTtj9I 71293@MELMAC-71293
    runcmd:
    - curl -fsSL https://get.docker.com | sh
    - usermod -aG docker deploy
    - systemctl enable --now docker
    - ufw default deny incoming
    - ufw default allow outgoing
    - ufw logging off
    - ufw allow 22/tcp
    - ufw --force enable
    apt:
      conf: 'APT::Periodic::Update-Package-Lists "1";

        APT::Periodic::Download-Upgradeable-Packages "1";

        APT::Periodic::AutocleanInterval "7";

        APT::Periodic::Unattended-Upgrade "0";

        Unattended-Upgrade::Automatic-Reboot "false";

        '
    write_files:
    - path: /etc/logrotate.d/00-cloud-init-global
      owner: root:root
      permissions: '0644'
      content: "/var/log/*.log {\n    weekly\n    rotate 7\n    compress\n    su root adm\n    create\n    missingok\n}\n"
    power_state:
      mode: reboot
      message: Rebooting
      timeout: 1
      condition: true

## Local testing with Multipass

Requires [Multipass](https://multipass.run) installed. Pass
`cloud_init=mi` directly to `mp.launch()`. Use `docker=True` in
[`multi_init()`](https://vedicreader.github.io/vpseasy/core.html#multi_init)
if your app needs Docker pre-installed (adds ~2 min for install +
reboot).

``` python
import tempfile
_app = Path(tempfile.mkdtemp()) / 'myapp'
_app.mkdir()
(_app / 'docker-compose.yml').write_text('services:\n  app:\n    image: nginx:alpine\n')
```

    41

``` python
mp = Multipass()
try: mp.rm(_vm, purge=True)
except: pass
vm = mp.launch(_vm, image='24.04', cpus=1, memory='1G', disk='10G', cloud_init=mi)
ip = mp.ip(vm.name)
print(f'VM at {ip}, key: {vm.key}')
```

    Creating testvm  Configuring testvm  Starting testvm  Waiting for initialization to complete  Launched: testvm
    VM at 192.168.2.56, key: /Users/71293/.ssh/testvm

``` python
deploy_mp(_vm, src=_app)
```

    Resolved SSH key from name slug: /Users/71293/.ssh/testvm

    Warning: Permanently added '192.168.2.56' (ED25519) to the list of known hosts.

    Ensured remote path /srv/app exists and is writable by deploy
    Resolved SSH key from name slug: /Users/71293/.ssh/testvm
    Running rsync: rsync -az --delete -e ssh -o StrictHostKeyChecking=accept-new -i /Users/71293/.ssh/testvm /var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmp8vrta37_/myapp/ deploy@192.168.2.56:/srv/app/
    Rsync completed successfully
    Resolved SSH key from name slug: /Users/71293/.ssh/testvm

``` python
mp.rm(_vm)
```

## Provision and deploy on Hetzner

Set `HCLOUD_TOKEN` in your environment.
[`hetzner_deploy()`](https://vedicreader.github.io/vpseasy/core.html#hetzner_deploy)
provisions the server, waits for cloud-init, and deploys in one call.
It’s idempotent — re-running against an existing server just redeploys.

``` python
hz = Hetzner()
svr = hetzner_deploy('myapp-prod',_app, hz) # hz is not required
print(f'Deployed at {svr.name}, key: {svr.key}')
```

    Server myapp-prod provisioning at 65.21.147.20 ...
    SSH check succeeded: 
    cloud-init status: running
    cloud-init status: unknown
    cloud-init status: unknown
    cloud-init status: unknown
    cloud-init status: unknown
    cloud-init status: running
    cloud-init status: running
    cloud-init status: running
    cloud-init status: running
    cloud-init status: running
    cloud-init status: running
    cloud-init status: unknown
    cloud-init status: unknown
    cloud-init status: unknown
    cloud-init status: done
    Ensured remote path /srv/app exists and is writable by deploy
    Running rsync: rsync -az --delete -e ssh -o StrictHostKeyChecking=accept-new -i /Users/71293/.ssh/myapp-prod /var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmplfmfw3uz/myapp/ deploy@65.21.147.20:/srv/app/
    Rsync completed successfully
    Docker info: Client: Docker Engine - Community
     Version:    29.4.3
     Context:    default
     Debug Mode: false
     Plugins:
      buildx: Docker Buildx (Docker Inc.)
        Version:  v0.33.0
        Path:     /usr/libexec/docker/cli-plugins/docker-buildx
      compose: Docker Compose (Docker Inc.)
        Version:  v5.1.3
        Path:     /usr/libexec/docker/cli-plugins/docker-compose
      model: Docker Model Runner (Docker Inc.)
        Version:  v1.1.37
        Path:     /usr/libexec/docker/cli-plugins/docker-model

    Server:
     Containers: 0
      Running: 0
      Paused: 0
      Stopped: 0
     Images: 0
     Server Version: 29.4.3
     Storage Driver: overlayfs
      driver-type: io.containerd.snapshotter.v1
     Logging Driver: json-file
     Cgroup Driver: systemd
     Cgroup Version: 2
     Plugins:
      Volume: local
      Network: bridge host ipvlan macvlan null overlay
      Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
     CDI spec directories:
      /etc/cdi
      /var/run/cdi
     Swarm: inactive
     Runtimes: runc io.containerd.runc.v2
     Default Runtime: runc
     Init Binary: docker-init
     containerd version: 77c84241c7cbdd9b4eca2591793e3d4f4317c590
     runc version: v1.3.5-0-g488fc13e
     init version: de40ad0
     Security Options:
      apparmor
      seccomp
       Profile: builtin
      cgroupns
     Kernel Version: 6.8.0-111-generic
     Operating System: Ubuntu 24.04.4 LTS
     OSType: linux
     Architecture: x86_64
     CPUs: 2
     Total Memory: 3.73GiB
     Name: myapp-prod
     ID: a8429b9a-7a14-49fa-9ee8-c18e71b015ac
     Docker Root Dir: /var/lib/docker
     Debug Mode: false
     Experimental: false
     Insecure Registries:
      ::1/128
      127.0.0.0/8
     Live Restore Enabled: false
     Firewall Backend: iptables

     Image nginx:alpine Pulling 
     612c0c1df4c5 Pulling fs layer 0B
     aee4e54b3865 Pulling fs layer 0B
     781ff50d2644 Pulling fs layer 0B
     82736a35d0e7 Pulling fs layer 0B
     453da7dbc73e Pulling fs layer 0B
     583599bb7d38 Pulling fs layer 0B
     4a8b0b2a5b19 Pulling fs layer 0B
     6a0ac1617861 Pulling fs layer 0B
     e8fc446e336c Download complete 0B
     6192e1e6a438 Download complete 0B
     6a0ac1617861 Downloading 2.097MB
     781ff50d2644 Download complete 0B
     612c0c1df4c5 Downloading 4.194MB
     6a0ac1617861 Download complete 0B
     aee4e54b3865 Download complete 0B
     453da7dbc73e Download complete 0B
     4a8b0b2a5b19 Download complete 0B
     583599bb7d38 Download complete 0B
     6a0ac1617861 Extracting 1B
     82736a35d0e7 Download complete 0B
     612c0c1df4c5 Downloading 11.53MB
     6a0ac1617861 Extracting 1B
     612c0c1df4c5 Download complete 0B
     6a0ac1617861 Pull complete 0B
     82736a35d0e7 Extracting 1B
     781ff50d2644 Pull complete 0B
     aee4e54b3865 Pull complete 0B
     583599bb7d38 Pull complete 0B
     82736a35d0e7 Pull complete 0B
     453da7dbc73e Pull complete 0B
     4a8b0b2a5b19 Pull complete 0B
     612c0c1df4c5 Extracting 1B
     612c0c1df4c5 Extracting 1B
     612c0c1df4c5 Extracting 1B
     612c0c1df4c5 Extracting 1B
     612c0c1df4c5 Pull complete 0B
     Image nginx:alpine Pulled 
     Network app_default Creating 
     Network app_default Created 
     Container app-app-1 Creating 
     Container app-app-1 Created 
     Container app-app-1 Starting 

    docker compose deployed with build
    Deployed at myapp-prod, key: /Users/71293/.ssh/myapp-prod

     Container app-app-1 Started 

``` python
o = lambda: [s['name'] for s in hz.servers()]
print('servers: ', o())
hz.delete('myapp-prod')
print('Deleted server.')
print('servers', o())
```

    servers:  ['vedicreader-cx32-hel', 'myapp-prod']
    Deleted server.
    servers ['vedicreader-cx32-hel']

## Docker Compose helpers

Any app that dockeasy can build — FastHTML, FastAPI, Go, Rust, Node —
follows the same production Compose shape when deployed behind
Cloudflare Tunnel: an `app` service, a `caddy` reverse proxy, a
`cloudflared` tunnel container, a shared `web` network, and two named
volumes for Caddy state.

[`caddy_stack()`](https://vedicreader.github.io/vpseasy/core.html#caddy_stack)
generates that structure from a domain and any dockeasy Dockerfile
object.
[`vols_to_binds()`](https://vedicreader.github.io/vpseasy/core.html#vols_to_binds)
converts absolute container paths to local bind mounts. The `root=`
argument saves all three files (`Dockerfile`, `docker-compose.yml`,
`Caddyfile`); without it the `Compose` object is returned without
writing anything.

``` python
d = Path(tempfile.mkdtemp())
df = fasthtml_app(pkgs=['sqlite3'], vols=['/app/data'], healthcheck='/health')
c = caddy_stack('myapp.example.com', df, vols=['/app/data'], root=d)
print(c)
```

## Install agent skill

Copies `SKILL.md` to `.agents/skills/vpseasy/` (project-local) and
`~/.claude/skills/vpseasy/` (global Claude Code).

``` python
mv_skill_md(dry_run=True)
```

    Would copy to: ['.agents/skills/vpseasy/SKILL.md', '/Users/71293/.claude/skills/vpseasy/SKILL.md']

## API reference

| Symbol | Description |
|----|----|
| `load_pub_keys(paths=None)` | Read `~/.ssh/id_*.pub` -\> list of strings |
| `gen_key(slug, key_dir=None)` | Generate ed25519 pair -\> `AttrDict(key, pub, pub_str)` |
| `multi_init(hostname, pub_keys, ...)` | Multipass cloud-init YAML -\> `AttrDict(yaml, key)` |
| `vps_init(hostname, pub_keys, ...)` | Production cloud-init YAML -\> `AttrDict(yaml, key)` |
| [`Multipass`](https://vedicreader.github.io/vpseasy/core.html#multipass) | Launch / list / exec / delete local Ubuntu VMs |
| `deploy_mp(name, src, path, build)` | Sync dir + `docker compose up` in Multipass VM |
| [`Hetzner`](https://vedicreader.github.io/vpseasy/core.html#hetzner) | Create / list / delete Hetzner Cloud servers |
| `hetzner_deploy(name, src, ...)` | Full pipeline: provision -\> wait -\> deploy (idempotent) |
| `wait_ssh(host, u, k, tout)` | Poll until SSH accepts connections |
| `wait_ready(host, u, k, tout)` | Poll SSH then cloud-init until done |
| `chk_cloud_init(host, u, k)` | Return `cloud-init status` string |
| `chk_docker(host, u, k)` | Verify Docker daemon running |
| `run_ssh(host, *cmds, ...)` | Run commands over SSH |
| `sync(host, src, path, ...)` | Rsync local dir to remote |
| `deploy(host, src, path, ...)` | [`sync`](https://vedicreader.github.io/vpseasy/core.html#sync) + `docker compose up -d` |
| `vols_to_binds(vols)` | `["/app/data"]` -\> `["./data:/app/data"]` for Compose bind mounts |
| `caddy_stack(domain, df, ...)` | Compose file: app + caddy + cloudflared + web network + caddy volumes |
| `mv_skill_md(dry_run, dir)` | Install agent SKILL.md |
