Metadata-Version: 2.4
Name: vpseasy
Version: 0.0.7
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! -->

``` 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 AAAAC3NzaC1lZDI1NTE5AAAAIG5vF0hxKfho9gZ9nWIp5GIq+UDkZTQ+/v1lgzp+bk5K 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 AAAAC3NzaC1lZDI1NTE5AAAAIH+XjqpWlA8Zcct/3Py1OasAupD8py5/oUlxI4359V8z 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 95.216.194.42 ...
    SSH to host 95.216.194.42 check succeeded
    cloud-init status: running
    cloud-init status: running
    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: 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/tmpo9ofr56s/myapp/ deploy@95.216.194.42:/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: 16846d84-4424-459d-925b-2df55ed21703
     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
    docker-compose check output: /srv/app/docker-compose.yml
    docker compose ran with build → 
    Deployed at myapp-prod, key: /Users/71293/.ssh/myapp-prod

``` 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)
```

    services:
      app:
        build: .
        volumes:
        - ./data:/app/data
        env_file:
        - .env
        restart: unless-stopped
        networks:
        - web
      caddy:
        image: caddy:2
        depends_on:
        - app
        volumes:
        - /private/var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmp_dtwwgkg/Caddyfile:/etc/caddy/Caddyfile
        - caddy_data:/data
        - caddy_config:/config
        networks:
        - web
        restart: unless-stopped
      cloudflared:
        image: cloudflare/cloudflared:latest
        command: tunnel --no-autoupdate run --url http://caddy
        environment:
        - TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}
        networks:
        - web
        restart: unless-stopped
    networks:
      web: null
    volumes:
      caddy_data: null
      caddy_config: null

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