Metadata-Version: 2.4
Name: vpseasy
Version: 0.0.2
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)
```

``` python
ci = vps_init('demo-prod', pub_keys)
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 AAAAC3NzaC1lZDI1NTE5AAAAIAcfGCEzt9TJzVOBmlzU4N8LvLKQxUQQ/mIikwArFO1K karthikrajgopal.laxminaarayanan@bain.com
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDM9kAjDCitQvl6oMLab4yK7LgylUV9lo/M/U8uqi5r8LPbo/qia7Oaj+Qo2LSs+Hsesr/mdKNjFle5DRb91k/Ns4GX2V8QyBcKwBK0Davjmj57X+4ZfnmC4HjZ/gzyYe1hV4Vjy4IBE/JSArSP3hI8tCAgN00tNLxJ5AJnYzdgfKpVa+zI54cdrJTPhko12mEyOih62xOWeHT16Y7jGPIaOzPo2YTFM+omwEm7TfyxuRLxaDN0d/ZxlKPi/+vBcuAOItrjA6DJ7lnYwk3cXubCKgHP3uc0MeBBX74S58zpoHlAp6XdePKOoBwam1/aYm+7zq+9GJyDGV25iD9bDwSn+0oNexJFRgQxwFdIwVUk4Iyq6OocUNLX8vrI1Qr6kXIchDVS922LGf+Z5aai89wqaxLLB+U4+dNTzb6zKMtQSMdGrgFZt0N5j/aMuRE5rkfWoeiCT8DmekuxA6NDaF76CYgcIsEaOsCTuNjwrpWBQnQ82r20tfegagT3Y38xBp9PLbFHbM45HkjAsOyT6QqXh8C6XofZJa/QL25uCTHQBBxZCVtYPccs6Sjh4u7zJ3cAH1E7tWgpzwFeBrgBMLIJ0Atwbp4fCqAm1XHyaLaeuVceFk3YXbCUK8DmN0FApkzlxpeCwHQ2ZLUOF5HExYSC2IoHYbYJl8oyhwG8Bwe9hQ== karthik.rajgopal@hotmail.com
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICOEkEHB+WPV42E9izurjWdTrBAHpgDxK5JcdzhkmN7T karthik.rajgopal@hotmail.com
      - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKu2Oi3apJsaZmO3dubl+8ZUA7ivXJdhzILPucA8F2ag karthik.rajgopal@hotmail.com
    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
mp = Multipass()
mp.rm(_vm,purge=True)   # idempotent cleanup
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}')
deploy_mp(_vm, src='./myapp')
mp.rm(_vm)
```

    Creating testvm  Configuring testvm  Starting testvm  Waiting for initialization to complete  

    launch failed: The following errors occurred:
    timed out waiting for initialization to complete

    CalledProcessError: Command '['multipass', 'launch', '24.04', '-n', 'testvm', '-c', '1', '-m', '1G', '-d', '10G', '--cloud-init', '-']' returned non-zero exit status 2.
    [31m---------------------------------------------------------------------------[39m
    [31mCalledProcessError[39m                        Traceback (most recent call last)
    [36mCell[39m[36m [39m[32mIn[6][39m[32m, line 4[39m
    [32m      1[39m [38;5;66;03m#| eval: False[39;00m
    [32m      2[39m mp = Multipass()
    [32m      3[39m mp.rm(_vm,purge=[38;5;28;01mTrue[39;00m)   [38;5;66;03m# idempotent cleanup[39;00m
    [32m----> [39m[32m4[39m vm = mp.launch(_vm, image=[33m'24.04'[39m, cpus=[32m1[39m, memory=[33m'1G'[39m, disk=[33m'10G'[39m, cloud_init=mi)
    [32m      5[39m ip = mp.ip(vm.name)
    [32m      6[39m print(f'VM at {ip}, key: {vm.key}')
    [32m      7[39m deploy_mp(_vm, src=[33m'./myapp'[39m)

    [36mFile [39m[32m~/code/personal/orgs/vpseasy/vpseasy/core.py:29[39m, in [36mMultipass.launch[39m[34m(self, name, image, cpus, memory, disk, cloud_init, mounts)[39m
    [32m     27[39m [38;5;28;01mfor[39;00m hp, vp [38;5;129;01min[39;00m (mounts [38;5;129;01mor[39;00m {}).items(): args += [[33m'[39m[33m--mount[39m[33m'[39m, [33mf[39m[33m'[39m[38;5;132;01m{[39;00mhp[38;5;132;01m}[39;00m[33m:[39m[38;5;132;01m{[39;00mvp[38;5;132;01m}[39;00m[33m'[39m]
    [32m     28[39m [38;5;28;01mif[39;00m cloud_init: args += [[33m'[39m[33m--cloud-init[39m[33m'[39m, [33m'[39m[33m-[39m[33m'[39m]
    [32m---> [39m[32m29[39m [30;43msubprocess[39;49m[30;43m.[39;49m[30;43mrun[39;49m[30;43m([39;49m[30;43margs[39;49m[30;43m,[39;49m[30;43m [39;49m[30;43minput[39;49m[30;43m=[39;49m[30;43mcloud_init[39;49m[30;43m.[39;49m[30;43myaml[39;49m[30;43m [39;49m[30;43;01mif[39;49;00m[30;43m [39;49m[30;43mcloud_init[39;49m[30;43m [39;49m[30;43;01melse[39;49;00m[30;43m [39;49m[30;43;01mNone[39;49;00m[30;43m,[39;49m[30;43m [39;49m[30;43mtext[39;49m[30;43m=[39;49m[30;43;01mTrue[39;49;00m[30;43m,[39;49m[30;43m [39;49m[30;43mcheck[39;49m[30;43m=[39;49m[30;43;01mTrue[39;49;00m[30;43m)[39;49m
    [32m     30[39m [38;5;28;01mreturn[39;00m AttrDict(name=name, key=cloud_init.key [38;5;28;01mif[39;00m cloud_init [38;5;28;01melse[39;00m [38;5;28;01mNone[39;00m)

    [36mFile [39m[32m~/Library/Application Support/uv/python/cpython-3.13.1-macos-aarch64-none/lib/python3.13/subprocess.py:577[39m, in [36mrun[39m[34m(input, capture_output, timeout, check, *popenargs, **kwargs)[39m
    [32m    575[39m     retcode = process.poll()
    [32m    576[39m     [38;5;28;01mif[39;00m check [38;5;129;01mand[39;00m retcode:
    [32m--> [39m[32m577[39m         [38;5;28;01mraise[39;00m CalledProcessError(retcode, process.args,
    [32m    578[39m                                  output=stdout, stderr=stderr)
    [32m    579[39m [38;5;28;01mreturn[39;00m CompletedProcess(process.args, retcode, stdout, stderr)

    [31mCalledProcessError[39m: Command '['multipass', 'launch', '24.04', '-n', 'testvm', '-c', '1', '-m', '1G', '-d', '10G', '--cloud-init', '-']' returned non-zero exit status 2.

## Provision on Hetzner

Set `HCLOUD_TOKEN` in your environment.
[`vps_init()`](https://vedicreader.github.io/vpseasy/core.html#vps_init)
auto-generates an SSH key pair when `pub_keys=None`.

``` python
hz = Hetzner()                              # reads HCLOUD_TOKEN
ci = vps_init('myapp-prod', pub_keys)
svr = hz.create('myapp-prod', cloud_init=ci, ssh_keys=hz.key_names(), location='hel1')
print(f'Provisioning at {svr.ip}')
```

## Deploy

[`wait_ssh()`](https://vedicreader.github.io/vpseasy/core.html#wait_ssh)
blocks until SSH is up.
[`deploy()`](https://vedicreader.github.io/vpseasy/core.html#deploy)
rsyncs your Compose stack and brings it up.

``` python
wait_ssh(svr.ip, tout=300)
assert chk_cloud_init(svr.ip) == 'done'
assert chk_docker(svr.ip)
deploy('./myapp', svr.ip)
```

## 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)   # preview; pass dry_run=False to actually install
```

## 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 |
| `wait_ssh(host, u, k, tout)` | Poll until SSH accepts connections |
| `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` |
| `mv_skill_md(dry_run, dir)` | Install agent SKILL.md |
