# Fusion
Remote task runner using Erlang distribution over SSH. Zero dependencies.
Fusion connects to remote servers via SSH, sets up port tunnels for Erlang distribution, bootstraps a remote BEAM node, and lets you run Elixir code on it. Think Ansible/Chef but for Elixir - push modules and execute functions on remote machines without pre-installing your application.
## Requirements
- Elixir ~> 1.18 / OTP 28+
- Remote server with Elixir/Erlang installed
- SSH access (key-based or password via `sshpass`)
## Installation
Add `fusion` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:fusion, "~> 0.2.0"}
]
end
```
## Usage
Your local BEAM must be started as a distributed node:
```bash
iex --sname myapp@localhost -S mix
```
Then connect and run code remotely:
```elixir
# Define the target
target = %Fusion.Target{
host: "10.0.1.5",
port: 22,
username: "deploy",
auth: {:key, "~/.ssh/id_ed25519"}
}
# Connect (sets up tunnels, bootstraps remote BEAM, joins cluster)
{:ok, manager} = Fusion.NodeManager.start_link(target)
{:ok, remote_node} = Fusion.NodeManager.connect(manager)
# Run code remotely (MFA form)
{:ok, 3} = Fusion.run(remote_node, Kernel, :+, [1, 2])
# Run system commands on the remote
{:ok, {hostname, 0}} = Fusion.run(remote_node, System, :cmd, ["hostname", []])
# Push and run your own modules (dependencies are resolved automatically)
{:ok, result} = Fusion.run(remote_node, MyApp.Worker, :process, [data])
# Disconnect and clean up
Fusion.NodeManager.disconnect(manager)
```
### Automatic Dependency Resolution
When you run `MyApp.Worker` remotely, Fusion automatically pushes all project modules that `Worker` references (struct usage, function calls, etc.). You don't need to manually track the dependency chain.
```elixir
# This pushes MyApp.Worker AND any project modules it depends on
{:ok, result} = Fusion.run(remote_node, MyApp.Worker, :do_work, [])
# You can also push explicitly
Fusion.TaskRunner.push_module(remote_node, MyApp.Worker)
Fusion.TaskRunner.push_modules(remote_node, [MyApp.Config, MyApp.Utils])
```
Standard library modules (Kernel, Enum, String, etc.) are already on the remote and don't need pushing.
## How It Works
### 1. SSH Tunnel Setup
Fusion creates 3 SSH tunnels between local and remote:
```
Local Machine Remote Server
───────────── ─────────────
┌─── Reverse ────┐
Local node port ◄┘ tunnel #1 └── Remote can reach local node
┌─── Forward ────┐
localhost:port ──┘ tunnel #2 └► Remote node's dist port
┌─── Reverse ────┐
Local EPMD ◄─┘ tunnel #3 └── Remote registers with local EPMD
(port 4369)
```
### 2. Remote BEAM Bootstrap
Starts Elixir on the remote via SSH with carefully configured flags:
- `ERL_EPMD_PORT=<tunneled>` - routes EPMD registration through tunnel #3 back to local EPMD
- `--sname worker@localhost` - uses `@localhost` because all traffic goes through localhost-bound tunnels
- `--cookie <local_cookie>` - matches the local cluster's cookie
- `--erl "-kernel inet_dist_listen_min/max <port>"` - pins distribution port to match tunnel #2
### 3. Transparent Connection
Since the remote registered with the *local* EPMD, `Node.connect/1` works as if the remote node were local. All distribution traffic is routed through the SSH tunnels.
### 4. Code Pushing
Module bytecode is transferred via Erlang distribution:
1. Read `.beam` binary locally with `:code.get_object_code/1`
2. Parse BEAM atoms table to find non-stdlib dependencies
3. Push each dependency recursively (bottom-up)
4. Load on remote with `:code.load_binary/3`
5. Execute via `:erpc.call/4`
## Testing
```bash
# Unit tests (no external dependencies)
mix test
# Docker integration tests (requires Docker)
cd test/docker && ./run.sh start
elixir --sname fusion_test@localhost -S mix test --include external
# Stop the test container
cd test/docker && ./run.sh stop
```
### Test Tiers
- **Tier 1 (Unit)** - Doctests and pure logic tests. No network, no SSH.
- **Tier 2 (Integration)** - Tests against localhost SSH. Skips gracefully if not configured.
- **Tier 3 (External)** - End-to-end tests against a Docker container with SSH + Elixir. Requires `./run.sh start`.
## Architecture
```
Fusion (public API)
├── TaskRunner - Remote code execution + module pushing + dependency resolution
├── NodeManager - GenServer: tunnel setup, BEAM bootstrap, connection lifecycle
├── Target - SSH connection configuration struct
├── TunnelSupervisor - DynamicSupervisor for tunnel processes
├── Net - Port generation, EPMD utilities
├── Connector - SSH connection GenServer
├── SshPortTunnel - SSH port tunnel process wrapper
├── PortRelay - Port relay process wrapper
├── UdpTunnel - UDP tunnel process wrapper
└── Utilities
├── Ssh - SSH command string generation
├── Exec - OS process execution (Port/System.cmd)
├── Erl - Erlang CLI command builder
└── Bash/Socat/Netcat/Netstat/Telnet - CLI tool wrappers
```
## License
MIT