@@ -0,0 +1,195 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: Build & Test
|
||||
runs-on: ssh
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Check toolchain versions
|
||||
run: |
|
||||
node --version
|
||||
npm --version
|
||||
bare --version 2>/dev/null || echo "bare not in PATH (ok)"
|
||||
|
||||
- name: Install root dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install tunnel dependencies
|
||||
run: npm install
|
||||
working-directory: tunnel
|
||||
|
||||
- name: Lint — syntax check tunnel
|
||||
run: node --check tunnel/index.mjs
|
||||
|
||||
- name: Lint — syntax check build scripts
|
||||
run: node --check scripts/build-tunnel.js
|
||||
|
||||
- name: Lint — syntax check connection plugin
|
||||
run: python3 -m py_compile ansible_collections/anisail/anisail/plugins/connection/holesail.py
|
||||
|
||||
- name: Build all platform binaries
|
||||
run: node scripts/build-tunnel.js --all
|
||||
|
||||
- name: Package release zips
|
||||
run: |
|
||||
cd releases
|
||||
for host in darwin-arm64 darwin-x64 linux-arm64 linux-x64 win32-x64; do
|
||||
if [ -f "$host/anisail-tunnel" ]; then
|
||||
zip -j "anisail-tunnel-${host}.zip" "$host/anisail-tunnel"
|
||||
echo "Created anisail-tunnel-${host}.zip"
|
||||
elif [ -f "$host/anisail-tunnel.exe" ]; then
|
||||
zip -j "anisail-tunnel-${host}.zip" "$host/anisail-tunnel.exe"
|
||||
echo "Created anisail-tunnel-${host}.zip"
|
||||
fi
|
||||
done
|
||||
cd ..
|
||||
|
||||
- name: Smoke test — run native binary for current platform
|
||||
run: |
|
||||
echo "=== releases/ layout ==="
|
||||
find releases/ -type f | sort
|
||||
echo ""
|
||||
|
||||
MACHINE=$(uname -m)
|
||||
echo "uname -m: $MACHINE"
|
||||
|
||||
BIN=""
|
||||
for candidate in \
|
||||
"releases/${MACHINE}/anisail-tunnel" \
|
||||
"releases/x86_64/anisail-tunnel" \
|
||||
"releases/aarch64/anisail-tunnel" \
|
||||
"releases/arm64/anisail-tunnel" \
|
||||
"releases/darwin-arm64/anisail-tunnel" \
|
||||
"releases/darwin-x64/anisail-tunnel" \
|
||||
"releases/linux-arm64/anisail-tunnel" \
|
||||
"releases/linux-x64/anisail-tunnel" \
|
||||
"releases/anisail-tunnel"; do
|
||||
if [ -f "$candidate" ]; then
|
||||
BIN="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$BIN" ]; then
|
||||
echo "No runnable binary found — skipping smoke test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Testing binary: $BIN"
|
||||
chmod +x "$BIN"
|
||||
OUTPUT=$(timeout 5 "$BIN" 2>&1 || true)
|
||||
echo "$OUTPUT"
|
||||
|
||||
if echo "$OUTPUT" | grep -q "key.*required"; then
|
||||
echo "PASS: binary started and reported missing key"
|
||||
else
|
||||
echo "FAIL: expected 'key required' message"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: smoke test complete"
|
||||
|
||||
- name: Generate checksums
|
||||
run: |
|
||||
cd releases
|
||||
find . -name "*.zip" | sort | xargs sha256sum > SHA256SUMS.txt 2>/dev/null || true
|
||||
cat SHA256SUMS.txt 2>/dev/null || echo "(no zips)"
|
||||
cd ..
|
||||
|
||||
- name: List artifacts
|
||||
run: |
|
||||
echo "=== All release artifacts ==="
|
||||
find releases/ -type f | sort
|
||||
echo ""
|
||||
find releases/ -type f -exec ls -lh {} \; | awk '{print $5, $9}'
|
||||
|
||||
- name: Publish rolling release
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
|
||||
TAG="latest-main"
|
||||
TITLE="Latest build (main @ ${SHORT_SHA})"
|
||||
COMMIT_MSG=$(echo "${{ github.event.head_commit.message }}" | sed '/^Made-with:/d' | sed '/^$/d' | head -1)
|
||||
BODY="Automated build from main branch.\n\n**Commit:** ${{ github.sha }}\n**Message:** ${COMMIT_MSG}\n\nThis release is updated on every push to main and always contains the latest tunnel binaries."
|
||||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||||
AUTH="Authorization: token ${{ secrets.RELEASE_TOKEN }}"
|
||||
|
||||
REPO_URL=$(git remote get-url origin | sed 's|https://|https://x-token:${{ secrets.RELEASE_TOKEN }}@|')
|
||||
git remote set-url origin "${REPO_URL}"
|
||||
git config user.email "ci@anisail"
|
||||
git config user.name "CI"
|
||||
git tag -f "${TAG}" "${{ github.sha }}"
|
||||
git push origin "refs/tags/${TAG}" --force
|
||||
echo "Tag ${TAG} force-pushed to ${{ github.sha }}"
|
||||
|
||||
EXISTING=$(curl -s -H "$AUTH" "${API}/releases/tags/${TAG}")
|
||||
RELEASE_ID=$(echo "$EXISTING" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{const r=JSON.parse(d);console.log(r.id||'')}catch{console.log('')}})")
|
||||
echo "Existing release ID: $RELEASE_ID"
|
||||
|
||||
if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ] && [ "$RELEASE_ID" != "" ]; then
|
||||
curl -s -X PATCH \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
"${API}/releases/${RELEASE_ID}" \
|
||||
-d "{
|
||||
\"name\": \"${TITLE}\",
|
||||
\"body\": \"${BODY}\",
|
||||
\"prerelease\": true,
|
||||
\"target_commitish\": \"${{ github.sha }}\"
|
||||
}"
|
||||
echo "Updated release ${RELEASE_ID}"
|
||||
|
||||
ASSETS=$(curl -s -H "$AUTH" "${API}/releases/${RELEASE_ID}/assets")
|
||||
echo "$ASSETS" | node -e "
|
||||
let d='';
|
||||
process.stdin.on('data',c=>d+=c).on('end',()=>{
|
||||
try {
|
||||
const assets = JSON.parse(d);
|
||||
if (Array.isArray(assets)) assets.forEach(a => console.log(a.id));
|
||||
} catch(_) {}
|
||||
})" | while read ASSET_ID; do
|
||||
[ -z "$ASSET_ID" ] && continue
|
||||
echo "Deleting asset $ASSET_ID..."
|
||||
curl -s -X DELETE -H "$AUTH" "${API}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
|
||||
done
|
||||
else
|
||||
RELEASE_ID=$(curl -s -X POST \
|
||||
-H "$AUTH" -H "Content-Type: application/json" \
|
||||
"${API}/releases" \
|
||||
-d "{
|
||||
\"tag_name\": \"${TAG}\",
|
||||
\"name\": \"${TITLE}\",
|
||||
\"body\": \"${BODY}\",
|
||||
\"prerelease\": true,
|
||||
\"target_commitish\": \"${{ github.sha }}\"
|
||||
}" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{try{console.log(JSON.parse(d).id)}catch{console.log('')}})")
|
||||
echo "Created release ${RELEASE_ID}"
|
||||
fi
|
||||
|
||||
for FILE in releases/*.zip releases/SHA256SUMS.txt; do
|
||||
[ -f "$FILE" ] || continue
|
||||
NAME=$(basename "$FILE")
|
||||
echo "Uploading $NAME..."
|
||||
curl -s -X POST \
|
||||
-H "$AUTH" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
"${API}/releases/${RELEASE_ID}/assets?name=${NAME}" \
|
||||
--data-binary "@${FILE}"
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "Done — release ${TAG} updated to ${{ github.sha }}"
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output (tunnel binaries and zips; CI publishes these)
|
||||
releases/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs and debug
|
||||
*.log
|
||||
npm-debug.log*
|
||||
.npm
|
||||
|
||||
# Environment / secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Editor / IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@@ -0,0 +1,162 @@
|
||||
# Anisail
|
||||
|
||||
Ansible connection plugin that uses [Holesail](https://github.com/holesail/holesail) P2P tunnels to reach hosts behind NAT, firewalls, or CGNAT — with no port forwarding or static IPs.
|
||||
|
||||
- **On the target**: Run `holesail --live 22` (or your SSH port); copy the `hs://...` URL.
|
||||
- **In inventory**: Set `ansible_connection` and `ansible_holesail_key`.
|
||||
- Ansible starts a local tunnel, connects SSH through it, and runs your playbooks as usual.
|
||||
|
||||
Requires the **anisail-tunnel** binary (Bare-built, shipped in `releases/` or built via `npm run build:tunnel`) and **paramiko** on the control node.
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Install the collection
|
||||
|
||||
From the repo root (or after building the collection artifact):
|
||||
|
||||
```bash
|
||||
ansible-galaxy collection install ansible_collections/anisail/anisail/ --force
|
||||
```
|
||||
|
||||
Or link for development:
|
||||
|
||||
```bash
|
||||
export ANSIBLE_COLLECTIONS_PATHS="$PWD/ansible_collections"
|
||||
# or symlink/copy ansible_collections/anisail into your collections path
|
||||
```
|
||||
|
||||
### 2. Install the tunnel binary
|
||||
|
||||
Either use a prebuilt binary or build it yourself (requires [Node.js](https://nodejs.org/) and `npm`).
|
||||
|
||||
**Option A — Build from source (Bare binary)**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build:tunnel
|
||||
```
|
||||
|
||||
Output: `releases/<platform>/anisail-tunnel` (e.g. `releases/darwin-arm64/anisail-tunnel`). Either add that directory to `PATH` or set `ansible_holesail_tunnel_path` in inventory to the full path.
|
||||
|
||||
**Option B — Use collection releases**
|
||||
|
||||
If you ship the built binary inside the collection (e.g. under `ansible_collections/anisail/anisail/releases/<platform>/anisail-tunnel`), the plugin will find it automatically. Copy from repo `releases/` after building:
|
||||
|
||||
```bash
|
||||
cp -r releases/* ansible_collections/anisail/anisail/releases/
|
||||
```
|
||||
|
||||
**Option C — System PATH**
|
||||
|
||||
Install the binary somewhere on `PATH` and name it `anisail-tunnel`. The plugin will use it if `ansible_holesail_tunnel_path` is not set and no collection-relative binary is found.
|
||||
|
||||
### 3. Install Paramiko (required)
|
||||
|
||||
```bash
|
||||
pip install paramiko
|
||||
```
|
||||
|
||||
## Inventory
|
||||
|
||||
Use the `holesail` connection and set the Holesail key (and optionally port/timeout):
|
||||
|
||||
```yaml
|
||||
# inventory.yml
|
||||
all:
|
||||
hosts:
|
||||
my_device:
|
||||
ansible_connection: anisail.anisail.holesail # or "holesail" if collection is default
|
||||
ansible_holesail_key: "hs://s000..." # from "holesail --live 22" on the target
|
||||
ansible_user: admin # SSH user (optional)
|
||||
# ansible_port: 22 # remote port to tunnel to (default 22)
|
||||
# ansible_holesail_tunnel_path: /path/to/anisail-tunnel
|
||||
# ansible_holesail_ready_timeout: 30 # seconds (default 30)
|
||||
```
|
||||
|
||||
If your playbook sets `collections: [anisail.anisail]`, you can use the short name:
|
||||
|
||||
```yaml
|
||||
ansible_connection: holesail
|
||||
ansible_holesail_key: "hs://s000..."
|
||||
```
|
||||
|
||||
## Target setup
|
||||
|
||||
On each device you want to manage, run Holesail in server mode exposing the SSH port:
|
||||
|
||||
```bash
|
||||
npm i holesail -g
|
||||
holesail --live 22
|
||||
```
|
||||
|
||||
Use the printed URL (e.g. `hs://s000...`) as `ansible_holesail_key` for that host.
|
||||
|
||||
## Playbook example
|
||||
|
||||
```yaml
|
||||
---
|
||||
- name: Run through Holesail tunnel
|
||||
hosts: my_device
|
||||
gather_facts: true
|
||||
tasks:
|
||||
- name: Ping
|
||||
ping:
|
||||
- name: Run a command
|
||||
command: uname -a
|
||||
register: out
|
||||
- name: Print result
|
||||
debug:
|
||||
var: out.stdout
|
||||
```
|
||||
|
||||
Run as usual:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.yml playbook.yml
|
||||
```
|
||||
|
||||
## Connection variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `ansible_holesail_key` | Holesail URL from target (`hs://...`) | (required) |
|
||||
| `ansible_port` | Remote port to tunnel to (e.g. SSH 22) | 22 |
|
||||
| `ansible_holesail_tunnel_path` | Path to anisail-tunnel binary | auto (collection bin/releases or PATH) |
|
||||
| `ansible_holesail_ready_timeout` | Seconds to wait for tunnel ready | 30 |
|
||||
| `ansible_user` | SSH user | play context default |
|
||||
| `ansible_private_key_file` / `ansible_ssh_private_key_file` | SSH key path | — |
|
||||
| `ansible_password` / `ansible_ssh_pass` | SSH password | — |
|
||||
|
||||
## Building the tunnel binary
|
||||
|
||||
From the repo root:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build:tunnel # current platform only
|
||||
npm run build:tunnel:all # all platforms (darwin-arm64, darwin-x64, linux-arm64, linux-x64, win32-x64)
|
||||
```
|
||||
|
||||
Output is under `releases/<platform>/anisail-tunnel` (or `anisail-tunnel.exe` on Windows). The tunnel is a statically built Bare binary; no Node.js is required at runtime.
|
||||
|
||||
## CI and releases
|
||||
|
||||
The repo includes a Gitea Actions workflow (`.gitea/workflows/ci.yml`) that:
|
||||
|
||||
- Runs on push and pull requests to `main`
|
||||
- Installs dependencies, lint-checks the tunnel and connection plugin, and builds tunnel binaries for all platforms (`node scripts/build-tunnel.js --all`)
|
||||
- Packages each binary as `anisail-tunnel-<platform>.zip` and publishes a rolling release on push
|
||||
|
||||
**Required secret:** `RELEASE_TOKEN` — a token with permission to push the `latest-main` tag and create/update releases and upload assets. Configure it in your Gitea repo **Settings → Secrets**.
|
||||
|
||||
The workflow uses the same pattern as [Holesail-Browser CI](https://git.ssh.surf/snxraven/holesail-browser/src/branch/main/.gitea/workflows/ci.yml): force-push tag `latest-main`, create or update a prerelease, and upload all zip artifacts plus `SHA256SUMS.txt`.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## References
|
||||
|
||||
- [Holesail](https://github.com/holesail/holesail) — P2P tunnel (npm, CLI, API)
|
||||
- [Ansible connection plugins](https://docs.ansible.com/ansible/devel/plugins/connection.html)
|
||||
- [Developing network plugins](https://docs.ansible.com/projects/ansible/latest/network/dev_guide/developing_plugins_network.html#network-connection-plugins)
|
||||
@@ -0,0 +1,23 @@
|
||||
# anisail.anisail
|
||||
|
||||
Ansible collection that provides the **holesail** connection plugin for reaching hosts behind NAT/firewalls via [Holesail](https://github.com/holesail/holesail) P2P tunnels.
|
||||
|
||||
## Connection plugin: holesail
|
||||
|
||||
- **Connection type**: `anisail.anisail.holesail` (or `holesail` when the collection is in use)
|
||||
- **Requires**: `ansible_holesail_key` (Holesail URL, e.g. `hs://s000...`), anisail-tunnel binary, and the `paramiko` Python library.
|
||||
|
||||
See the [project README](https://github.com/anisail/anisail) at the repo root for full installation, inventory, and playbook examples.
|
||||
|
||||
## Quick example
|
||||
|
||||
**Inventory:**
|
||||
|
||||
```yaml
|
||||
my_host:
|
||||
ansible_connection: anisail.anisail.holesail
|
||||
ansible_holesail_key: "hs://s000..."
|
||||
ansible_user: admin
|
||||
```
|
||||
|
||||
**Target:** Run `holesail --live 22` and use the printed URL as `ansible_holesail_key`.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
namespace: anisail
|
||||
name: anisail
|
||||
version: 1.0.0
|
||||
readme: README.md
|
||||
description: Ansible connection plugin for Holesail P2P tunnels — reach hosts behind NAT/firewalls with no port forwarding.
|
||||
license: MIT
|
||||
authors:
|
||||
- Anisail
|
||||
repository: https://github.com/anisail/anisail
|
||||
documentation: https://github.com/anisail/anisail
|
||||
keywords:
|
||||
- ansible
|
||||
- connection
|
||||
- holesail
|
||||
- tunnel
|
||||
- p2p
|
||||
- nat
|
||||
|
||||
dependencies: {}
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
# Runtime requirements for anisail.anisail collection
|
||||
requires_ansible: '>=2.14'
|
||||
@@ -0,0 +1 @@
|
||||
# Connection plugins for anisail.anisail
|
||||
@@ -0,0 +1,316 @@
|
||||
# Copyright (c) Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
"""
|
||||
Ansible connection plugin that uses Holesail P2P tunnels to reach hosts behind NAT.
|
||||
Spawns anisail-tunnel binary, reads local port from stdout, then connects SSH over that port.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from ansible.errors import AnsibleConnectionFailure
|
||||
from ansible.plugins.connection import ConnectionBase
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
HAS_PARAMIKO = True
|
||||
except ImportError:
|
||||
HAS_PARAMIKO = False
|
||||
|
||||
DOCUMENTATION = """
|
||||
author: Anisail
|
||||
connection_type: holesail
|
||||
short_description: Connect via Holesail P2P tunnel then SSH
|
||||
description:
|
||||
- Runs an anisail-tunnel binary to create a local TCP tunnel to the target (via hs:// key),
|
||||
then connects SSH to 127.0.0.1:<local_port>. Use when the target is behind NAT/firewall/CGNAT.
|
||||
- On the target, run C(holesail --live 22) (or your SSH port) and set ansible_holesail_key to the printed URL.
|
||||
version_added: '1.0.0'
|
||||
options:
|
||||
key:
|
||||
description: Holesail URL (e.g. hs://s000...) from the target running C(holesail --live <port>).
|
||||
type: string
|
||||
required: true
|
||||
vars:
|
||||
- name: ansible_holesail_key
|
||||
port:
|
||||
description: Remote port to tunnel to (e.g. 22 for SSH).
|
||||
type: int
|
||||
default: 22
|
||||
vars:
|
||||
- name: ansible_port
|
||||
- name: ansible_ssh_port
|
||||
tunnel_path:
|
||||
description: Path to the anisail-tunnel binary. If not set, resolved from collection bin/ or PATH.
|
||||
type: string
|
||||
default: ''
|
||||
vars:
|
||||
- name: ansible_holesail_tunnel_path
|
||||
ready_timeout:
|
||||
description: Seconds to wait for tunnel to be ready.
|
||||
type: int
|
||||
default: 30
|
||||
vars:
|
||||
- name: ansible_holesail_ready_timeout
|
||||
host:
|
||||
description: Hostname/IP (unused; tunnel key defines the target).
|
||||
type: string
|
||||
remote_user:
|
||||
description: User for SSH login.
|
||||
type: string
|
||||
vars:
|
||||
- name: ansible_user
|
||||
- name: ansible_ssh_user
|
||||
private_key_file:
|
||||
description: Path to SSH private key.
|
||||
type: string
|
||||
vars:
|
||||
- name: ansible_private_key_file
|
||||
- name: ansible_ssh_private_key_file
|
||||
password:
|
||||
description: SSH password (if not using key).
|
||||
type: string
|
||||
vars:
|
||||
- name: ansible_password
|
||||
- name: ansible_ssh_pass
|
||||
"""
|
||||
|
||||
|
||||
def _find_tunnel_binary(plugin_path_option, collection_root: str | None) -> str:
|
||||
"""Resolve anisail-tunnel binary path."""
|
||||
if plugin_path_option and os.path.isabs(plugin_path_option) and os.path.isfile(plugin_path_option):
|
||||
return plugin_path_option
|
||||
if plugin_path_option and os.path.isfile(plugin_path_option):
|
||||
return os.path.abspath(plugin_path_option)
|
||||
if collection_root:
|
||||
for sub in ('bin', 'releases'):
|
||||
for name in ('anisail-tunnel', 'anisail-tunnel.exe'):
|
||||
candidate = os.path.join(collection_root, sub, name)
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
import platform as plat
|
||||
system = plat.system().lower()
|
||||
machine = plat.machine().lower()
|
||||
if system == 'darwin':
|
||||
platform = 'darwin-arm64' if machine in ('arm64', 'aarch64') else 'darwin-x64'
|
||||
elif system == 'linux':
|
||||
platform = 'linux-arm64' if machine in ('arm64', 'aarch64') else 'linux-x64'
|
||||
elif system == 'windows':
|
||||
platform = 'win32-x64'
|
||||
else:
|
||||
platform = f"{system}-{machine}"
|
||||
candidate = os.path.join(collection_root, 'releases', platform, 'anisail-tunnel')
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
if platform.startswith('win'):
|
||||
candidate = os.path.join(collection_root, 'releases', platform, 'anisail-tunnel.exe')
|
||||
if os.path.isfile(candidate):
|
||||
return candidate
|
||||
import shutil
|
||||
return shutil.which('anisail-tunnel') or ''
|
||||
|
||||
|
||||
class Connection(ConnectionBase):
|
||||
"""Ansible connection over Holesail tunnel + SSH (Paramiko)."""
|
||||
|
||||
transport = 'holesail'
|
||||
has_pipelining = False
|
||||
|
||||
def __init__(self, play_context, new_stdin, *args, **kwargs):
|
||||
super().__init__(play_context, new_stdin, *args, **kwargs)
|
||||
self._tunnel_process: subprocess.Popen | None = None
|
||||
self._local_port: int | None = None
|
||||
self._paramiko_client: "paramiko.SSHClient | None" = None
|
||||
|
||||
def _connect(self) -> None:
|
||||
if not HAS_PARAMIKO:
|
||||
raise AnsibleConnectionFailure(
|
||||
'The paramiko Python library is required for the holesail connection plugin. '
|
||||
'Install it with: pip install paramiko'
|
||||
)
|
||||
key = self.get_option('key')
|
||||
if not key:
|
||||
raise AnsibleConnectionFailure(
|
||||
'ansible_holesail_key is required for the holesail connection. '
|
||||
'Set it in inventory (e.g. ansible_holesail_key: "hs://s000...").'
|
||||
)
|
||||
remote_port = self.get_option('port') or 22
|
||||
tunnel_path_opt = self.get_option('tunnel_path') or ''
|
||||
ready_timeout_sec = self.get_option('ready_timeout') or 30
|
||||
timeout_ms = ready_timeout_sec * 1000
|
||||
|
||||
collection_root = None
|
||||
try:
|
||||
from ansible.utils.collection_loader import AnsibleCollectionConfig
|
||||
for p in (getattr(AnsibleCollectionConfig, 'collection_paths', None) or []):
|
||||
for base in (p if isinstance(p, (list, tuple)) else [p]):
|
||||
candidate = os.path.join(base, 'anisail', 'anisail')
|
||||
if os.path.isdir(candidate):
|
||||
collection_root = candidate
|
||||
break
|
||||
if collection_root:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not collection_root:
|
||||
try:
|
||||
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
collection_root = os.path.normpath(os.path.join(plugin_dir, '..', '..'))
|
||||
if not os.path.isdir(collection_root):
|
||||
collection_root = None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
binary = _find_tunnel_binary(tunnel_path_opt, collection_root)
|
||||
if not binary:
|
||||
raise AnsibleConnectionFailure(
|
||||
'anisail-tunnel binary not found. Set ansible_holesail_tunnel_path or install the binary '
|
||||
'in the collection bin/ or releases/<platform>/ directory, or have anisail-tunnel in PATH.'
|
||||
)
|
||||
|
||||
argv = [
|
||||
binary,
|
||||
'--key', key,
|
||||
'--remote-port', str(remote_port),
|
||||
'--timeout', str(timeout_ms),
|
||||
]
|
||||
try:
|
||||
self._tunnel_process = subprocess.Popen(
|
||||
argv,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
except OSError as e:
|
||||
raise AnsibleConnectionFailure(f'Failed to start tunnel process: {e}') from e
|
||||
|
||||
line = None
|
||||
try:
|
||||
with self._tunnel_process.stdout:
|
||||
line = self._tunnel_process.stdout.readline()
|
||||
except Exception as e:
|
||||
self._terminate_tunnel()
|
||||
raise AnsibleConnectionFailure(f'Failed to read tunnel port: {e}') from e
|
||||
|
||||
if not line:
|
||||
stderr = b''
|
||||
if self._tunnel_process.stderr:
|
||||
try:
|
||||
stderr = self._tunnel_process.stderr.read()
|
||||
except Exception:
|
||||
pass
|
||||
self._terminate_tunnel()
|
||||
raise AnsibleConnectionFailure(
|
||||
'Tunnel did not output local port. '
|
||||
f'Stderr: {(stderr.decode("utf-8", errors="replace") or "").strip() or "none"}'
|
||||
)
|
||||
|
||||
try:
|
||||
data = json.loads(line.decode('utf-8').strip())
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||
self._terminate_tunnel()
|
||||
raise AnsibleConnectionFailure(f'Invalid tunnel output (expected JSON with local_port): {e}') from e
|
||||
|
||||
local_port = data.get('local_port')
|
||||
if local_port is None:
|
||||
self._terminate_tunnel()
|
||||
raise AnsibleConnectionFailure('Tunnel output missing local_port')
|
||||
|
||||
self._local_port = int(local_port)
|
||||
|
||||
username = self.get_option('remote_user') or self._play_context.remote_user or 'root'
|
||||
pkey = None
|
||||
key_file = self.get_option('private_key_file')
|
||||
if key_file and os.path.isfile(key_file):
|
||||
try:
|
||||
pkey = paramiko.RSAKey.from_private_key_file(key_file)
|
||||
except paramiko.ssh_exception.SSHException:
|
||||
try:
|
||||
pkey = paramiko.Ed25519Key.from_private_key_file(key_file)
|
||||
except paramiko.ssh_exception.SSHException:
|
||||
pass
|
||||
password = self.get_option('password') or self._play_context.password or None
|
||||
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
client.connect(
|
||||
'127.0.0.1',
|
||||
port=self._local_port,
|
||||
username=username,
|
||||
pkey=pkey,
|
||||
password=password,
|
||||
timeout=30,
|
||||
banner_timeout=30,
|
||||
auth_timeout=30,
|
||||
)
|
||||
except Exception as e:
|
||||
self._terminate_tunnel()
|
||||
raise AnsibleConnectionFailure(f'SSH connection to tunnel port failed: {e}') from e
|
||||
|
||||
self._paramiko_client = client
|
||||
|
||||
def _terminate_tunnel(self) -> None:
|
||||
if self._tunnel_process is None:
|
||||
return
|
||||
try:
|
||||
self._tunnel_process.terminate()
|
||||
try:
|
||||
self._tunnel_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._tunnel_process.kill()
|
||||
self._tunnel_process.wait()
|
||||
except Exception:
|
||||
pass
|
||||
self._tunnel_process = None
|
||||
|
||||
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||||
if self._paramiko_client is None:
|
||||
raise AnsibleConnectionFailure('Not connected')
|
||||
try:
|
||||
stdin, stdout, stderr = self._paramiko_client.exec_command(cmd)
|
||||
if in_data:
|
||||
stdin.write(in_data)
|
||||
stdin.channel.shutdown_write()
|
||||
rc = stdout.channel.recv_exit_status()
|
||||
return rc, stdout.read(), stderr.read()
|
||||
except Exception as e:
|
||||
raise AnsibleConnectionFailure(f'exec_command failed: {e}') from e
|
||||
|
||||
def put_file(self, in_path, out_path):
|
||||
if self._paramiko_client is None:
|
||||
raise AnsibleConnectionFailure('Not connected')
|
||||
try:
|
||||
sftp = self._paramiko_client.open_sftp()
|
||||
try:
|
||||
sftp.put(in_path, out_path)
|
||||
finally:
|
||||
sftp.close()
|
||||
except Exception as e:
|
||||
raise AnsibleConnectionFailure(f'put_file failed: {e}') from e
|
||||
|
||||
def get_file(self, in_path, out_path):
|
||||
if self._paramiko_client is None:
|
||||
raise AnsibleConnectionFailure('Not connected')
|
||||
try:
|
||||
sftp = self._paramiko_client.open_sftp()
|
||||
try:
|
||||
sftp.get(in_path, out_path)
|
||||
finally:
|
||||
sftp.close()
|
||||
except Exception as e:
|
||||
raise AnsibleConnectionFailure(f'get_file failed: {e}') from e
|
||||
|
||||
def close(self):
|
||||
if self._paramiko_client:
|
||||
try:
|
||||
self._paramiko_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._paramiko_client = None
|
||||
self._terminate_tunnel()
|
||||
super().close()
|
||||
Generated
+619
@@ -0,0 +1,619 @@
|
||||
{
|
||||
"name": "anisail",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "anisail",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"bare-build": "^0.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
|
||||
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native-b4a": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-b4a": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-addon-resolve": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
|
||||
"integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-module-resolve": "^1.10.0",
|
||||
"bare-semver": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-ansi-escapes": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
|
||||
"integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-stream": "^2.6.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-apk": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-apk/-/bare-apk-0.1.2.tgz",
|
||||
"integrity": "sha512-lu5E7u33snoi3bFqGfVM0HtPm4v6U83zOP14mo2YQGBKWjhGsZS128ToUNdKGoG51QhPs1ltgt2zsBhs0LPOcA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-env": "^3.0.0",
|
||||
"bare-fs": "^4.5.2",
|
||||
"bare-os": "^3.6.2",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-subprocess": "^5.2.1",
|
||||
"require-asset": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-app-image": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-app-image/-/bare-app-image-0.1.1.tgz",
|
||||
"integrity": "sha512-FOmSjy+0bx6Qkztl+n7PHhrqxSnu4ccsokjHPwQUFnDNcAetYlxsLQHquq7B/vJeZiowBVJV+ulrTdwuVBfXpA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"dependencies": {
|
||||
"bare-fs": "^4.5.1",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-subprocess": "^5.1.5",
|
||||
"require-asset": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-assert": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
|
||||
"integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-inspect": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-buffer": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-buffer/-/bare-buffer-3.6.0.tgz",
|
||||
"integrity": "sha512-/maRWEQ2eBkVNMbNFVsq1pHXJYVj4Y3AixwruB24eKZDs5Gtu0fixzvjYmBIuTsBMtVH5Yb27pQO9BhFa+IlIQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"bare": ">=1.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-build": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-build/-/bare-build-0.4.5.tgz",
|
||||
"integrity": "sha512-fGSNL7HfIEtIvwcbZc4TtC8XRxiXAqBoLAEBKPQhTLHxUudF54NUxQDBkdO+qqmUCWPXavjuCI0ZxJm/z/+zoQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-bundle-id": "^1.0.2",
|
||||
"bare-fs": "^4.5.1",
|
||||
"bare-lief": "^0.1.3",
|
||||
"bare-link": "^3.0.0",
|
||||
"bare-module-resolve": "^1.12.0",
|
||||
"bare-module-traverse": "^2.0.0",
|
||||
"bare-os": "^3.6.2",
|
||||
"bare-pack": "^2.0.0",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-subprocess": "^5.1.5",
|
||||
"bare-unpack": "^1.1.3",
|
||||
"bare-url": "^2.3.2",
|
||||
"paparam": "^1.8.0",
|
||||
"require-asset": "^1.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"bare-build": "bin.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-apk": "^0.1.2",
|
||||
"bare-app-image": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-bundle": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
|
||||
"integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-bundle-id": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-bundle-id/-/bare-bundle-id-1.0.2.tgz",
|
||||
"integrity": "sha512-RG/y1J/s6zWmsqUIDtclXh+xxMRTh1jo/10vFL58FKhe9UESchMNkwn0Cz10o5AdA/35WR/KUGoHhIGdyCjQrg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"sodium-native": "^5.0.9"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-bundle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-env": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
|
||||
"integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.5.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
|
||||
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-inspect": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
|
||||
"integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-ansi-escapes": "^2.1.0",
|
||||
"bare-type": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-lief": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/bare-lief/-/bare-lief-0.1.6.tgz",
|
||||
"integrity": "sha512-eNqgZGoHVPGtbkLoDPVyKKEe9uTkXFqOvTevb1iDv3SXv/eU7kMgJoAukSdvFUg7QWNiqd2o/pZwaMy5n3cjNw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"require-addon": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-link": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-link/-/bare-link-3.0.2.tgz",
|
||||
"integrity": "sha512-dtG8B9PFXGMLRb2HJGTsq4jhLPMIq3e8z696f+VIU4eMbWnUOx2TxBvpId/u5yp4oKiapMvyINGEhqEsxtJH4A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-fs": "^4.0.0",
|
||||
"bare-lief": "^0.2.0",
|
||||
"bare-module-resolve": "^1.12.0",
|
||||
"bare-os": "^3.2.0",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-subprocess": "^5.0.2",
|
||||
"bare-url": "^2.0.9",
|
||||
"paparam": "^1.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"bare-link": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-link/node_modules/bare-lief": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-lief/-/bare-lief-0.2.0.tgz",
|
||||
"integrity": "sha512-ya69yPs95HJxSpe5UaNYK2Vzko8DhYS+9cnAM2EhIlzY8J2wWcyMzjABmO4i6ToQwdpvywWgDEs1ud/r5pIjsA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-assert": "^1.2.0",
|
||||
"require-addon": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-module-lexer": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
|
||||
"integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"require-addon": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-module-resolve": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
|
||||
"integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-semver": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-module-traverse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-module-traverse/-/bare-module-traverse-2.0.1.tgz",
|
||||
"integrity": "sha512-1au+Og5p97T9b6Y7xmHZ7KtpW8vEYtz2jC2whmm+YJp46EaHfk26j91MmQhufdkR/8sdK1Q5p+P9A/Y5GrJg7Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-addon-resolve": "^1.5.0",
|
||||
"bare-module-lexer": "^1.4.0",
|
||||
"bare-module-resolve": "^1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
|
||||
"integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bare": ">=1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-pack": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-pack/-/bare-pack-2.0.1.tgz",
|
||||
"integrity": "sha512-zpItExb4Kue1vq97/ZiHNwSBwUGyv3uqhgi1GpT2S7qezkXmc3K/Wx6qkth/7ayS3i+suIn6EGE96EhJxKkCzA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-bundle": "^1.8.3",
|
||||
"bare-bundle-id": "^1.0.0",
|
||||
"bare-fs": "^4.2.1",
|
||||
"bare-module-traverse": "~2.0.0",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-url": "*",
|
||||
"paparam": "^1.5.0",
|
||||
"promaphore": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"bare-pack": "bin.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-url": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-url": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-pipe": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
|
||||
"integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.0.0",
|
||||
"bare-stream": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-semver": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz",
|
||||
"integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz",
|
||||
"integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"streamx": "^2.21.0",
|
||||
"teex": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-events": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-events": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-subprocess": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.2.tgz",
|
||||
"integrity": "sha512-L6oXgQ1aWs25RtG5Ky0bDD06p3RAcVVrDDMWb1DfXpHtyEWxamcyIWUbSykxMTWrpLlmSj6ytbb6yoKehGFfmw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-env": "^3.0.0",
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-os": "^3.0.1",
|
||||
"bare-pipe": "^4.0.0",
|
||||
"bare-url": "^2.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-type": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
|
||||
"integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"bare": ">=1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-unpack": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/bare-unpack/-/bare-unpack-1.1.3.tgz",
|
||||
"integrity": "sha512-b+OFTi74dsMaVQy9w90U6WMqhVBcx5hONJnFbOSfhTefG/9OuY6ZxU3IvybVa5/i3edzboa7rebaMG25K6/kAA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-bundle": "^1.8.3",
|
||||
"bare-fs": "^4.2.3",
|
||||
"bare-path": "^3.0.0",
|
||||
"paparam": "^1.8.5",
|
||||
"promaphore": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"bare-unpack": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
|
||||
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/paparam": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
|
||||
"integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/promaphore": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/promaphore/-/promaphore-1.0.0.tgz",
|
||||
"integrity": "sha512-Eg8401+KJddVvDULkpy8bR964GMX8xMPegL6NdxTeBH2Wa3L86cZlEHizbkFJikr5u+E3wFoR5dLWJ+1OPyEfw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/require-addon": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
|
||||
"integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-addon-resolve": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-asset": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/require-asset/-/require-asset-1.2.1.tgz",
|
||||
"integrity": "sha512-wFFxOxJHL/agpiXNDMzDBzC8hnmjJZYW0wDgolojZZkydS76talLlIH+DU3DWjBxQcu0vair30H7V3C4FcLnSg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-module-resolve": "^1.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sodium-native": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
|
||||
"integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bare-assert": "^1.2.0",
|
||||
"require-addon": "^1.1.0",
|
||||
"which-runtime": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/teex": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
|
||||
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"streamx": "^2.12.5"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
|
||||
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/which-runtime": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
|
||||
"integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "anisail",
|
||||
"version": "1.0.0",
|
||||
"description": "Ansible connection plugin for Holesail P2P tunnels",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build:tunnel": "node scripts/build-tunnel.js",
|
||||
"build:tunnel:all": "node scripts/build-tunnel.js --all"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bare-build": "^0.4.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# For the Ansible connection plugin (control node)
|
||||
paramiko>=2.11.0
|
||||
|
||||
# Optional: to run playbooks
|
||||
# ansible-core>=2.14
|
||||
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Build standalone anisail-tunnel binary using bare-pack + bare-build.
|
||||
* Output: releases/<host>/anisail-tunnel (or .exe on Windows).
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/build-tunnel.js # current host only
|
||||
* node scripts/build-tunnel.js --all # all platforms
|
||||
* node scripts/build-tunnel.js --host darwin-arm64 --host linux-x64
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { execSync } = require('child_process');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const TUNNEL_DIR = path.join(ROOT, 'tunnel');
|
||||
const RELEASES_DIR = path.join(ROOT, 'releases');
|
||||
const ENTRY = path.join(TUNNEL_DIR, 'index.mjs');
|
||||
|
||||
const ALL_HOSTS = [
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-arm64',
|
||||
'linux-x64',
|
||||
'win32-x64'
|
||||
];
|
||||
|
||||
const BUILTINS = [
|
||||
'source-map-support',
|
||||
'dtrace-provider'
|
||||
];
|
||||
|
||||
function getCurrentHost() {
|
||||
const platform = os.platform();
|
||||
const arch = os.arch() === 'arm64' ? 'arm64' : 'x64';
|
||||
return `${platform}-${arch}`;
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const hosts = [];
|
||||
let all = false;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--all') {
|
||||
all = true;
|
||||
} else if (args[i] === '--host' && args[i + 1]) {
|
||||
hosts.push(args[++i]);
|
||||
}
|
||||
}
|
||||
if (all) return ALL_HOSTS;
|
||||
if (hosts.length > 0) return hosts;
|
||||
return [getCurrentHost()];
|
||||
}
|
||||
|
||||
function getPlatformModule(host) {
|
||||
const bareBuildDir = path.dirname(require.resolve('bare-build'));
|
||||
switch (host) {
|
||||
case 'darwin-arm64':
|
||||
case 'darwin-x64':
|
||||
return require(path.join(bareBuildDir, 'lib/platform/apple'));
|
||||
case 'linux-arm64':
|
||||
case 'linux-x64':
|
||||
return require(path.join(bareBuildDir, 'lib/platform/linux'));
|
||||
case 'win32-x64':
|
||||
case 'win32-arm64':
|
||||
return require(path.join(bareBuildDir, 'lib/platform/windows'));
|
||||
default:
|
||||
throw new Error(`Unknown host '${host}'`);
|
||||
}
|
||||
}
|
||||
|
||||
function patchBareBuildSignForLinux() {
|
||||
if (os.platform() === 'darwin') return;
|
||||
try {
|
||||
execSync('which codesign', { stdio: 'ignore' });
|
||||
return;
|
||||
} catch (_) {}
|
||||
const bareBuildDir = path.dirname(require.resolve('bare-build'));
|
||||
const signPath = path.join(bareBuildDir, 'lib/platform/apple/sign.js');
|
||||
if (!fs.existsSync(signPath)) return;
|
||||
const current = fs.readFileSync(signPath, 'utf8');
|
||||
if (current.includes('PATCHED_NO_CODESIGN')) return;
|
||||
fs.writeFileSync(signPath, `// PATCHED_NO_CODESIGN: codesign not available on this platform
|
||||
module.exports = async function sign() {}
|
||||
`);
|
||||
console.log(' Patched bare-build/apple/sign.js (codesign not available)');
|
||||
}
|
||||
|
||||
function normalizeBundleKeysToWindows(bundle) {
|
||||
const Bundle = bundle.constructor;
|
||||
const next = new Bundle();
|
||||
next._id = bundle._id;
|
||||
const keyMap = {};
|
||||
for (const key of bundle.keys()) {
|
||||
const newKey = key.replace(/\//g, '\\');
|
||||
keyMap[key] = newKey;
|
||||
const content = bundle.read(key);
|
||||
const mode = bundle.mode(key);
|
||||
const opts = { mode };
|
||||
if (key === bundle.main) opts.main = true;
|
||||
if (bundle.addons && bundle.addons.includes(key)) opts.addon = true;
|
||||
if (bundle.assets && bundle.assets.includes(key)) opts.asset = true;
|
||||
const res = bundle.resolutions && bundle.resolutions[key];
|
||||
if (res) opts.imports = res;
|
||||
next.write(newKey, content, opts);
|
||||
}
|
||||
for (const [alias, key] of Object.entries(bundle.imports || {})) {
|
||||
next._imports[alias] = keyMap[key] ?? key.replace(/\//g, '\\');
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function patchBundle(bundle) {
|
||||
const bundleKeys = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {});
|
||||
function resolveJsonDiskPath(keyNorm) {
|
||||
const inTunnel = path.join(TUNNEL_DIR, keyNorm);
|
||||
if (fs.existsSync(inTunnel)) return inTunnel;
|
||||
if (keyNorm.startsWith('node_modules' + path.sep) || keyNorm.startsWith('node_modules/')) {
|
||||
const inRoot = path.join(ROOT, keyNorm.replace(/\//g, path.sep));
|
||||
if (fs.existsSync(inRoot)) return inRoot;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
for (const key of bundleKeys) {
|
||||
if (!key.endsWith('.json')) continue;
|
||||
let content = bundle.read(key);
|
||||
if (!content || content.length === 0) {
|
||||
const altKey = key.startsWith('/') ? key.slice(1) : '/' + key.replace(/^\/+/, '');
|
||||
content = bundle.read(altKey);
|
||||
if (content && content.length > 0) bundle.write(key, content);
|
||||
}
|
||||
const isEmpty = !content || content.length === 0;
|
||||
const invalidJson = content && content.length > 0 && (() => {
|
||||
try { JSON.parse(content.toString()); return false; } catch (_) { return true; }
|
||||
})();
|
||||
if (isEmpty || invalidJson) {
|
||||
const keyNorm = key.replace(/^\/+/, '').replace(/\//g, path.sep);
|
||||
const diskPath = resolveJsonDiskPath(keyNorm);
|
||||
if (diskPath) {
|
||||
content = fs.readFileSync(diskPath);
|
||||
bundle.write(key, content);
|
||||
}
|
||||
}
|
||||
if (content && content.length > 0) {
|
||||
const keyNoLead = key.replace(/^\/+/, '');
|
||||
const prefixSlash = 'runtime.bundle/' + keyNoLead;
|
||||
const prefixLead = '/runtime.bundle/' + keyNoLead;
|
||||
if (prefixSlash !== key) bundle.write(prefixSlash, content);
|
||||
if (prefixLead !== key) bundle.write(prefixLead, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function build(hosts) {
|
||||
patchBareBuildSignForLinux();
|
||||
|
||||
if (!fs.existsSync(path.join(TUNNEL_DIR, 'node_modules'))) {
|
||||
console.log('Installing tunnel dependencies...');
|
||||
execSync('npm install', { cwd: TUNNEL_DIR, stdio: 'inherit' });
|
||||
}
|
||||
|
||||
fs.mkdirSync(RELEASES_DIR, { recursive: true });
|
||||
|
||||
const pack = require('bare-pack');
|
||||
const { readModule, listPrefix } = require('bare-pack/fs');
|
||||
const traverse = require('bare-module-traverse');
|
||||
const bundleId = require('bare-bundle-id');
|
||||
|
||||
const pkg = require(path.join(TUNNEL_DIR, 'package.json'));
|
||||
|
||||
console.log('\nBuilding anisail-tunnel v' + pkg.version);
|
||||
console.log('Targets: ' + hosts.join(', '));
|
||||
console.log('Entry: ' + ENTRY);
|
||||
console.log('Output: ' + RELEASES_DIR + '\n');
|
||||
|
||||
const unixHosts = hosts.filter((h) => !h.startsWith('win32'));
|
||||
const winHosts = hosts.filter((h) => h.startsWith('win32'));
|
||||
const hasWindows = winHosts.length > 0;
|
||||
const hasUnix = unixHosts.length > 0;
|
||||
|
||||
async function buildAndEmit(bundleHosts, platformHostsList, normalizeForWindows) {
|
||||
if (bundleHosts.length === 0) return;
|
||||
console.log(' Bundling module graph' + (normalizeForWindows ? ' (Windows bundle)' : '') + '...');
|
||||
let bundle = await pack(
|
||||
pathToFileURL(ENTRY),
|
||||
{
|
||||
hosts: bundleHosts,
|
||||
linked: false,
|
||||
resolve: traverse.resolve.bare,
|
||||
builtins: BUILTINS
|
||||
},
|
||||
readModule,
|
||||
listPrefix
|
||||
);
|
||||
bundle = bundle.unmount(pathToFileURL(TUNNEL_DIR + '/'));
|
||||
patchBundle(bundle);
|
||||
if (normalizeForWindows) {
|
||||
bundle = normalizeBundleKeysToWindows(bundle);
|
||||
console.log(' Normalized bundle keys to Windows path form');
|
||||
}
|
||||
bundle.id = bundleId(bundle).toString('hex');
|
||||
const bundleSize = bundle.toBuffer().length;
|
||||
console.log(' Bundle size: ' + (bundleSize / 1024 / 1024).toFixed(1) + ' MB');
|
||||
|
||||
const groups = new Map();
|
||||
for (const h of platformHostsList) {
|
||||
const platform = getPlatformModule(h);
|
||||
if (!groups.has(platform)) groups.set(platform, []);
|
||||
groups.get(platform).push(h);
|
||||
}
|
||||
for (const [platform, platformHosts] of groups) {
|
||||
console.log(' Building ' + platformHosts.join(', ') + '...');
|
||||
for await (const file of platform(TUNNEL_DIR, bundle, null, {
|
||||
name: 'anisail-tunnel',
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
hosts: platformHosts,
|
||||
out: RELEASES_DIR,
|
||||
standalone: true
|
||||
})) {
|
||||
console.log(' Built: ' + path.relative(ROOT, file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWindows && hasUnix) {
|
||||
await buildAndEmit(unixHosts, unixHosts, false);
|
||||
await buildAndEmit(winHosts, winHosts, true);
|
||||
} else if (hasWindows) {
|
||||
await buildAndEmit(winHosts, winHosts, true);
|
||||
} else {
|
||||
await buildAndEmit(hosts, hosts, false);
|
||||
}
|
||||
|
||||
console.log('\nDone.');
|
||||
}
|
||||
|
||||
const hosts = parseArgs();
|
||||
build(hosts).catch((err) => {
|
||||
console.error('\nBuild failed:', err.message || err);
|
||||
if (err.cause) console.error('Cause:', err.cause);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* anisail-tunnel: Bare-compatible Holesail client that binds a local port and
|
||||
* prints it to stdout for the Ansible connection plugin. Keeps running until
|
||||
* stdin closes or SIGTERM/SIGINT.
|
||||
*
|
||||
* Usage: anisail-tunnel --key <hs://...> [--remote-port 22] [--local-port 0] [--timeout 30000]
|
||||
* Output: exactly one JSON line to stdout: {"local_port": <number>, "ready": true}
|
||||
*/
|
||||
|
||||
import 'bare-process/global';
|
||||
|
||||
const DEFAULT_REMOTE_PORT = 22;
|
||||
const DEFAULT_LOCAL_PORT = 0;
|
||||
const DEFAULT_TIMEOUT_MS = 30000;
|
||||
const LOCAL_HOST = '127.0.0.1';
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let key = null;
|
||||
let remotePort = DEFAULT_REMOTE_PORT;
|
||||
let localPort = DEFAULT_LOCAL_PORT;
|
||||
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--key' && args[i + 1]) {
|
||||
key = args[++i].trim();
|
||||
} else if (args[i] === '--remote-port' && args[i + 1]) {
|
||||
remotePort = parseInt(args[++i], 10) || DEFAULT_REMOTE_PORT;
|
||||
} else if (args[i] === '--local-port' && args[i + 1]) {
|
||||
localPort = parseInt(args[++i], 10);
|
||||
if (isNaN(localPort) || localPort < 0) localPort = DEFAULT_LOCAL_PORT;
|
||||
} else if (args[i] === '--timeout' && args[i + 1]) {
|
||||
timeoutMs = parseInt(args[++i], 10) || DEFAULT_TIMEOUT_MS;
|
||||
}
|
||||
}
|
||||
|
||||
return { key, remotePort, localPort, timeoutMs };
|
||||
}
|
||||
|
||||
function emit(line) {
|
||||
try {
|
||||
process.stdout.write(line + '\n');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const mod = await import('holesail');
|
||||
const Holesail = mod.default || mod;
|
||||
|
||||
const { key, remotePort, localPort, timeoutMs } = parseArgs();
|
||||
|
||||
if (!key || key.length === 0) {
|
||||
const msg = 'anisail-tunnel: --key <hs://...> is required';
|
||||
try {
|
||||
process.stderr.write(msg + '\n');
|
||||
} catch (_) {}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const portToUse = (localPort > 0 ? localPort : 19200 + Math.floor(Math.random() * 1000)) || 19200;
|
||||
|
||||
const readyPromise = new Promise((resolve, reject) => {
|
||||
const t = timeoutMs > 0
|
||||
? setTimeout(() => reject(new Error('Tunnel ready timeout after ' + timeoutMs + 'ms')), timeoutMs)
|
||||
: null;
|
||||
const done = (err, port) => {
|
||||
if (t) clearTimeout(t);
|
||||
if (err) reject(err);
|
||||
else resolve(port);
|
||||
};
|
||||
|
||||
const hs = new Holesail({
|
||||
client: true,
|
||||
key,
|
||||
host: LOCAL_HOST,
|
||||
port: portToUse
|
||||
});
|
||||
|
||||
if (typeof hs.on === 'function') {
|
||||
hs.on('error', (err) => {
|
||||
done(err || new Error('Tunnel error'));
|
||||
});
|
||||
}
|
||||
|
||||
hs.ready()
|
||||
.then(() => {
|
||||
const info = typeof hs.info === 'function' ? hs.info() : (hs.info || {});
|
||||
const boundPort = (info && typeof info.port === 'number')
|
||||
? info.port
|
||||
: portToUse;
|
||||
if (boundPort == null || boundPort < 1 || boundPort > 65535) {
|
||||
done(new Error('Could not determine local tunnel port'));
|
||||
return;
|
||||
}
|
||||
done(null, boundPort);
|
||||
return hs;
|
||||
})
|
||||
.catch(done);
|
||||
|
||||
run._hs = hs;
|
||||
});
|
||||
|
||||
let hs;
|
||||
try {
|
||||
const port = await readyPromise;
|
||||
hs = run._hs;
|
||||
emit(JSON.stringify({ local_port: port, ready: true }));
|
||||
} catch (err) {
|
||||
const msg = (err && err.message) ? err.message : String(err);
|
||||
try {
|
||||
process.stderr.write('anisail-tunnel: ' + msg + '\n');
|
||||
} catch (_) {}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (hs && typeof hs.close === 'function') {
|
||||
try {
|
||||
hs.close();
|
||||
} catch (_) {}
|
||||
hs = null;
|
||||
}
|
||||
process.exitCode = 0;
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGTERM', close);
|
||||
process.on('SIGINT', close);
|
||||
|
||||
process.stdin.on('close', () => {
|
||||
close();
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
close();
|
||||
});
|
||||
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
try {
|
||||
process.stderr.write('anisail-tunnel: ' + (err && err.message ? err.message : String(err)) + '\n');
|
||||
} catch (_) {}
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Generated
+1660
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "anisail-tunnel",
|
||||
"version": "1.0.0",
|
||||
"description": "Bare-compatible Holesail tunnel for Ansible connection plugin",
|
||||
"type": "commonjs",
|
||||
"license": "MIT",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"start": "bare index.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"assert": "npm:bare-node-assert@^1.0.0",
|
||||
"child_process": "npm:bare-subprocess@^5.2.2",
|
||||
"crypto": "npm:bare-node-crypto@^1.0.0",
|
||||
"events": "npm:bare-node-events@^1.0.1",
|
||||
"fs": "npm:bare-node-fs@^1.0.2",
|
||||
"holesail": "^2.4.1",
|
||||
"net": "npm:bare-node-net@^1.0.0",
|
||||
"os": "npm:bare-node-os@^1.0.1",
|
||||
"path": "npm:bare-node-path@^1.0.1",
|
||||
"stream": "npm:bare-node-stream@^1.0.0",
|
||||
"tls": "npm:bare-node-tls@^1.0.0",
|
||||
"util": "npm:bare-node-util@^1.0.0"
|
||||
},
|
||||
"imports": {
|
||||
"child_process": { "bare": "bare-subprocess", "default": "child_process" },
|
||||
"assert": { "bare": "assert", "default": "assert" },
|
||||
"util": { "bare": "util", "default": "util" },
|
||||
"net": { "bare": "net", "default": "net" },
|
||||
"tls": { "bare": "tls", "default": "tls" },
|
||||
"events": { "bare": "events", "default": "events" },
|
||||
"crypto": { "bare": "crypto", "default": "crypto" },
|
||||
"fs": { "bare": "fs", "default": "fs" },
|
||||
"path": { "bare": "path", "default": "path" },
|
||||
"process": { "bare": "bare-process", "default": "process" },
|
||||
"os": { "bare": "os", "default": "os" },
|
||||
"stream": { "bare": "stream", "default": "stream" }
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user