aircloud-cli 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aircloud_cli-0.2.0/PKG-INFO +248 -0
- aircloud_cli-0.2.0/README.md +217 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli/main.py +147 -1
- aircloud_cli-0.2.0/aircloud_cli/ssh_config.py +143 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli/ssh_tunnel.py +184 -11
- aircloud_cli-0.2.0/aircloud_cli.egg-info/PKG-INFO +248 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli.egg-info/SOURCES.txt +1 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/pyproject.toml +1 -1
- aircloud_cli-0.1.0/PKG-INFO +0 -130
- aircloud_cli-0.1.0/README.md +0 -99
- aircloud_cli-0.1.0/aircloud_cli.egg-info/PKG-INFO +0 -130
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli/__init__.py +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli/client.py +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli/config.py +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli/container_exec.py +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli.egg-info/dependency_links.txt +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli.egg-info/entry_points.txt +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli.egg-info/requires.txt +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/aircloud_cli.egg-info/top_level.txt +0 -0
- {aircloud_cli-0.1.0 → aircloud_cli-0.2.0}/setup.cfg +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aircloud-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: AirCloud CLI — endpoint management, SSH, and container exec
|
|
5
|
+
Author-email: AirCloud Team <support@aieev.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://aieev.com
|
|
8
|
+
Project-URL: Repository, https://github.com/aieev/aircloud-cli
|
|
9
|
+
Keywords: aircloud,ssh,cli,endpoint,exec
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: System :: Shells
|
|
23
|
+
Classifier: Topic :: Utilities
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Requires-Dist: websockets>=12.0
|
|
27
|
+
Requires-Dist: click>=8.0
|
|
28
|
+
Requires-Dist: httpx>=0.27.0
|
|
29
|
+
Requires-Dist: paramiko>=3.0
|
|
30
|
+
Requires-Dist: cryptography>=3.0
|
|
31
|
+
|
|
32
|
+
# aircloud-cli
|
|
33
|
+
|
|
34
|
+
CLI for the [AirCloud](https://aieev.com) platform — endpoint management,
|
|
35
|
+
SSH access, and container shell exec.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install aircloud-cli
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.9+.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# one-time setup
|
|
49
|
+
aircloud config set api-key <YOUR_API_KEY>
|
|
50
|
+
aircloud config set api-base-url <API_BASE_URL>
|
|
51
|
+
|
|
52
|
+
# inspect endpoints
|
|
53
|
+
aircloud endpoints list
|
|
54
|
+
aircloud endpoints get <endpoint_id>
|
|
55
|
+
|
|
56
|
+
# open a shell in a container
|
|
57
|
+
aircloud ssh <endpoint_id> # public-key auth, sshd-based
|
|
58
|
+
aircloud exec <endpoint_id> # docker exec over WS, no sshd needed
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Commands
|
|
62
|
+
|
|
63
|
+
### `aircloud config`
|
|
64
|
+
|
|
65
|
+
Manage CLI configuration in `~/.aircloud/config.json`.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
aircloud config set api-key <key>
|
|
69
|
+
aircloud config set api-base-url <url>
|
|
70
|
+
aircloud config list
|
|
71
|
+
aircloud config get <key>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `aircloud endpoints`
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
aircloud endpoints list # paged list
|
|
78
|
+
aircloud endpoints get <id> # detail
|
|
79
|
+
aircloud endpoints start <id>
|
|
80
|
+
aircloud endpoints stop <id>
|
|
81
|
+
aircloud endpoints scale <id> --replicas N
|
|
82
|
+
aircloud endpoints patch <id> --command "..." --port 8080 --env KEY=VALUE
|
|
83
|
+
aircloud endpoints replicas <id> # current replicas
|
|
84
|
+
aircloud endpoints logs <id> [--replica-id X] [--start-line N --end-line M]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `aircloud ssh`
|
|
88
|
+
|
|
89
|
+
SSH into a container via WebSocket tunnel using public-key authentication.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
aircloud ssh <endpoint_id> # auto-discover ~/.ssh/id_*
|
|
93
|
+
aircloud ssh <endpoint_id> -i ~/.ssh/aircloud_key
|
|
94
|
+
aircloud ssh <endpoint_id> -r <replica_id> # pin to specific replica
|
|
95
|
+
aircloud ssh <endpoint_id> --tunnel-only # open tunnel only
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Auto-discovers identity files in this order: `~/.ssh/id_ed25519`,
|
|
99
|
+
`~/.ssh/id_ecdsa`, `~/.ssh/id_rsa`. Override with `-i / --identity-file`.
|
|
100
|
+
|
|
101
|
+
The container image must have `sshd` running and consume the
|
|
102
|
+
`AIRCLOUD_AUTHORIZED_KEYS` environment variable (AirCloud-provided template
|
|
103
|
+
images do this by default).
|
|
104
|
+
|
|
105
|
+
### `aircloud ssh-proxy` / `aircloud ssh-config` (VS Code Remote-SSH, scp, git)
|
|
106
|
+
|
|
107
|
+
`aircloud ssh` opens its own tunnel and interactive shell. To use the
|
|
108
|
+
system `ssh`, `scp`, `git`, or **VS Code Remote-SSH** instead — without
|
|
109
|
+
pre-starting a tunnel or opening a local port — register `ssh-proxy` as an
|
|
110
|
+
OpenSSH `ProxyCommand`.
|
|
111
|
+
|
|
112
|
+
`ssh-proxy` bridges its own stdin/stdout to the WebSocket tunnel, so `ssh`
|
|
113
|
+
spawns one automatically per connection and tears it down on disconnect.
|
|
114
|
+
|
|
115
|
+
Install the config block into `~/.ssh/config`:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
aircloud ssh-config <endpoint_id> -i ~/.ssh/id_ed25519 --install
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`--install` writes the block between `# >>> aircloud managed: <id> >>>`
|
|
122
|
+
markers, so it is idempotent (re-running replaces in place, never
|
|
123
|
+
duplicates). It backs up `~/.ssh/config` first, resolves the absolute path
|
|
124
|
+
to `aircloud` (so GUI-launched VS Code finds it regardless of `PATH`), and
|
|
125
|
+
prepends the block above any catch-all `Host *` so its `User` / options win
|
|
126
|
+
under SSH's first-match-wins rule. The installed block:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Host aircloud-<endpoint_id>
|
|
130
|
+
HostName <endpoint_id>
|
|
131
|
+
User root
|
|
132
|
+
IdentityFile ~/.ssh/id_ed25519
|
|
133
|
+
ProxyCommand /abs/path/to/aircloud ssh-proxy %h --stdio
|
|
134
|
+
ServerAliveInterval 30
|
|
135
|
+
ServerAliveCountMax 3
|
|
136
|
+
StrictHostKeyChecking accept-new
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Run without `--install` to print the block for review instead of writing
|
|
140
|
+
it, or with `--uninstall` to remove that endpoint's managed block.
|
|
141
|
+
|
|
142
|
+
Then connect with any SSH-based tool:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
ssh aircloud-<endpoint_id>
|
|
146
|
+
scp file.txt aircloud-<endpoint_id>:/workspace/
|
|
147
|
+
# VS Code: Remote-SSH → Connect to Host → aircloud-<endpoint_id>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`ServerAliveInterval` keeps the session alive past L7 proxy idle timeouts.
|
|
151
|
+
The container image must run `sshd` (same requirement as `aircloud ssh`).
|
|
152
|
+
|
|
153
|
+
Running `aircloud ssh-proxy <endpoint_id> --stdio` directly in a terminal
|
|
154
|
+
only prints the server's SSH banner and then waits — it is a transport for
|
|
155
|
+
an SSH client, not a shell. Drive it through `ssh` / `ProxyCommand`.
|
|
156
|
+
|
|
157
|
+
#### Reconnection / transient relay slowness
|
|
158
|
+
|
|
159
|
+
When the relay is briefly slow (heavy workload on the node), the WebSocket
|
|
160
|
+
handshake can time out (`timed out during opening handshake`). `ssh-proxy`
|
|
161
|
+
retries the connect with exponential backoff so a transient blip is absorbed
|
|
162
|
+
instead of failing the connection. This applies to the **connect phase only**
|
|
163
|
+
— once a session is bridged, a mid-session drop ends it (SSH cannot resume),
|
|
164
|
+
and the client (VS Code, or you) reconnects, which spawns a fresh proxy that
|
|
165
|
+
retries again.
|
|
166
|
+
|
|
167
|
+
Tunables (env, read by `ssh-proxy`):
|
|
168
|
+
|
|
169
|
+
| Variable | Default | Meaning |
|
|
170
|
+
|---|---|---|
|
|
171
|
+
| `AIRCLOUD_SSH_OPEN_TIMEOUT` | `15` | Per-attempt WS handshake timeout (seconds) |
|
|
172
|
+
| `AIRCLOUD_SSH_CONNECT_RETRIES` | `3` | Retry count for transient failures (timeout / 5xx) |
|
|
173
|
+
|
|
174
|
+
Auth/routing failures (3xx/4xx) are not retried — they surface immediately.
|
|
175
|
+
|
|
176
|
+
For VS Code, raise its own connect timeout so a single attempt can span the
|
|
177
|
+
proxy's retries, in `settings.json`:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
"remote.SSH.connectTimeout": 60
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `aircloud exec`
|
|
184
|
+
|
|
185
|
+
Open an interactive shell in the container via `docker exec` over a
|
|
186
|
+
WebSocket. Works on any image — no `sshd` required.
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
aircloud exec <endpoint_id>
|
|
190
|
+
aircloud exec <endpoint_id> -r <replica_id> # pin replica
|
|
191
|
+
aircloud exec <endpoint_id> -c "/bin/sh" # custom command
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `aircloud whoami`
|
|
195
|
+
|
|
196
|
+
Print the identity associated with the current API key.
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
aircloud whoami
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
Editable install — source edits are picked up immediately by the
|
|
205
|
+
`aircloud` command, no rebuild required:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pip install -e .
|
|
209
|
+
aircloud --help
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Build & Release
|
|
213
|
+
|
|
214
|
+
Published to PyPI as `aircloud-cli`. Requires `build` and `twine`
|
|
215
|
+
(`pip install build twine`).
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# 1. bump the version in pyproject.toml (PyPI rejects re-uploading a version)
|
|
219
|
+
|
|
220
|
+
# 2. clean stale artifacts
|
|
221
|
+
rm -rf dist
|
|
222
|
+
|
|
223
|
+
# 3. build sdist + wheel into dist/
|
|
224
|
+
python -m build
|
|
225
|
+
|
|
226
|
+
# 4. upload to PyPI (needs a PyPI API token in ~/.pypirc or entered at the prompt)
|
|
227
|
+
twine upload dist/*
|
|
228
|
+
|
|
229
|
+
# 5. tag and push the release
|
|
230
|
+
git commit -am "release(x.y.z): <summary>"
|
|
231
|
+
git tag vx.y.z
|
|
232
|
+
git push origin main --tags
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Verify the published package in a clean environment:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
python -m venv /tmp/ac && /tmp/ac/bin/pip install "aircloud-cli==x.y.z"
|
|
239
|
+
/tmp/ac/bin/aircloud --help
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Status
|
|
243
|
+
|
|
244
|
+
This is an early functional release. Interfaces may change before `1.0.0`.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
Proprietary. © AIEEV / AirCloud.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# aircloud-cli
|
|
2
|
+
|
|
3
|
+
CLI for the [AirCloud](https://aieev.com) platform — endpoint management,
|
|
4
|
+
SSH access, and container shell exec.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install aircloud-cli
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Requires Python 3.9+.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# one-time setup
|
|
18
|
+
aircloud config set api-key <YOUR_API_KEY>
|
|
19
|
+
aircloud config set api-base-url <API_BASE_URL>
|
|
20
|
+
|
|
21
|
+
# inspect endpoints
|
|
22
|
+
aircloud endpoints list
|
|
23
|
+
aircloud endpoints get <endpoint_id>
|
|
24
|
+
|
|
25
|
+
# open a shell in a container
|
|
26
|
+
aircloud ssh <endpoint_id> # public-key auth, sshd-based
|
|
27
|
+
aircloud exec <endpoint_id> # docker exec over WS, no sshd needed
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### `aircloud config`
|
|
33
|
+
|
|
34
|
+
Manage CLI configuration in `~/.aircloud/config.json`.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
aircloud config set api-key <key>
|
|
38
|
+
aircloud config set api-base-url <url>
|
|
39
|
+
aircloud config list
|
|
40
|
+
aircloud config get <key>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `aircloud endpoints`
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
aircloud endpoints list # paged list
|
|
47
|
+
aircloud endpoints get <id> # detail
|
|
48
|
+
aircloud endpoints start <id>
|
|
49
|
+
aircloud endpoints stop <id>
|
|
50
|
+
aircloud endpoints scale <id> --replicas N
|
|
51
|
+
aircloud endpoints patch <id> --command "..." --port 8080 --env KEY=VALUE
|
|
52
|
+
aircloud endpoints replicas <id> # current replicas
|
|
53
|
+
aircloud endpoints logs <id> [--replica-id X] [--start-line N --end-line M]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `aircloud ssh`
|
|
57
|
+
|
|
58
|
+
SSH into a container via WebSocket tunnel using public-key authentication.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
aircloud ssh <endpoint_id> # auto-discover ~/.ssh/id_*
|
|
62
|
+
aircloud ssh <endpoint_id> -i ~/.ssh/aircloud_key
|
|
63
|
+
aircloud ssh <endpoint_id> -r <replica_id> # pin to specific replica
|
|
64
|
+
aircloud ssh <endpoint_id> --tunnel-only # open tunnel only
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Auto-discovers identity files in this order: `~/.ssh/id_ed25519`,
|
|
68
|
+
`~/.ssh/id_ecdsa`, `~/.ssh/id_rsa`. Override with `-i / --identity-file`.
|
|
69
|
+
|
|
70
|
+
The container image must have `sshd` running and consume the
|
|
71
|
+
`AIRCLOUD_AUTHORIZED_KEYS` environment variable (AirCloud-provided template
|
|
72
|
+
images do this by default).
|
|
73
|
+
|
|
74
|
+
### `aircloud ssh-proxy` / `aircloud ssh-config` (VS Code Remote-SSH, scp, git)
|
|
75
|
+
|
|
76
|
+
`aircloud ssh` opens its own tunnel and interactive shell. To use the
|
|
77
|
+
system `ssh`, `scp`, `git`, or **VS Code Remote-SSH** instead — without
|
|
78
|
+
pre-starting a tunnel or opening a local port — register `ssh-proxy` as an
|
|
79
|
+
OpenSSH `ProxyCommand`.
|
|
80
|
+
|
|
81
|
+
`ssh-proxy` bridges its own stdin/stdout to the WebSocket tunnel, so `ssh`
|
|
82
|
+
spawns one automatically per connection and tears it down on disconnect.
|
|
83
|
+
|
|
84
|
+
Install the config block into `~/.ssh/config`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
aircloud ssh-config <endpoint_id> -i ~/.ssh/id_ed25519 --install
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`--install` writes the block between `# >>> aircloud managed: <id> >>>`
|
|
91
|
+
markers, so it is idempotent (re-running replaces in place, never
|
|
92
|
+
duplicates). It backs up `~/.ssh/config` first, resolves the absolute path
|
|
93
|
+
to `aircloud` (so GUI-launched VS Code finds it regardless of `PATH`), and
|
|
94
|
+
prepends the block above any catch-all `Host *` so its `User` / options win
|
|
95
|
+
under SSH's first-match-wins rule. The installed block:
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
Host aircloud-<endpoint_id>
|
|
99
|
+
HostName <endpoint_id>
|
|
100
|
+
User root
|
|
101
|
+
IdentityFile ~/.ssh/id_ed25519
|
|
102
|
+
ProxyCommand /abs/path/to/aircloud ssh-proxy %h --stdio
|
|
103
|
+
ServerAliveInterval 30
|
|
104
|
+
ServerAliveCountMax 3
|
|
105
|
+
StrictHostKeyChecking accept-new
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Run without `--install` to print the block for review instead of writing
|
|
109
|
+
it, or with `--uninstall` to remove that endpoint's managed block.
|
|
110
|
+
|
|
111
|
+
Then connect with any SSH-based tool:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ssh aircloud-<endpoint_id>
|
|
115
|
+
scp file.txt aircloud-<endpoint_id>:/workspace/
|
|
116
|
+
# VS Code: Remote-SSH → Connect to Host → aircloud-<endpoint_id>
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`ServerAliveInterval` keeps the session alive past L7 proxy idle timeouts.
|
|
120
|
+
The container image must run `sshd` (same requirement as `aircloud ssh`).
|
|
121
|
+
|
|
122
|
+
Running `aircloud ssh-proxy <endpoint_id> --stdio` directly in a terminal
|
|
123
|
+
only prints the server's SSH banner and then waits — it is a transport for
|
|
124
|
+
an SSH client, not a shell. Drive it through `ssh` / `ProxyCommand`.
|
|
125
|
+
|
|
126
|
+
#### Reconnection / transient relay slowness
|
|
127
|
+
|
|
128
|
+
When the relay is briefly slow (heavy workload on the node), the WebSocket
|
|
129
|
+
handshake can time out (`timed out during opening handshake`). `ssh-proxy`
|
|
130
|
+
retries the connect with exponential backoff so a transient blip is absorbed
|
|
131
|
+
instead of failing the connection. This applies to the **connect phase only**
|
|
132
|
+
— once a session is bridged, a mid-session drop ends it (SSH cannot resume),
|
|
133
|
+
and the client (VS Code, or you) reconnects, which spawns a fresh proxy that
|
|
134
|
+
retries again.
|
|
135
|
+
|
|
136
|
+
Tunables (env, read by `ssh-proxy`):
|
|
137
|
+
|
|
138
|
+
| Variable | Default | Meaning |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `AIRCLOUD_SSH_OPEN_TIMEOUT` | `15` | Per-attempt WS handshake timeout (seconds) |
|
|
141
|
+
| `AIRCLOUD_SSH_CONNECT_RETRIES` | `3` | Retry count for transient failures (timeout / 5xx) |
|
|
142
|
+
|
|
143
|
+
Auth/routing failures (3xx/4xx) are not retried — they surface immediately.
|
|
144
|
+
|
|
145
|
+
For VS Code, raise its own connect timeout so a single attempt can span the
|
|
146
|
+
proxy's retries, in `settings.json`:
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
"remote.SSH.connectTimeout": 60
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### `aircloud exec`
|
|
153
|
+
|
|
154
|
+
Open an interactive shell in the container via `docker exec` over a
|
|
155
|
+
WebSocket. Works on any image — no `sshd` required.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
aircloud exec <endpoint_id>
|
|
159
|
+
aircloud exec <endpoint_id> -r <replica_id> # pin replica
|
|
160
|
+
aircloud exec <endpoint_id> -c "/bin/sh" # custom command
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `aircloud whoami`
|
|
164
|
+
|
|
165
|
+
Print the identity associated with the current API key.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
aircloud whoami
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Development
|
|
172
|
+
|
|
173
|
+
Editable install — source edits are picked up immediately by the
|
|
174
|
+
`aircloud` command, no rebuild required:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pip install -e .
|
|
178
|
+
aircloud --help
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Build & Release
|
|
182
|
+
|
|
183
|
+
Published to PyPI as `aircloud-cli`. Requires `build` and `twine`
|
|
184
|
+
(`pip install build twine`).
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# 1. bump the version in pyproject.toml (PyPI rejects re-uploading a version)
|
|
188
|
+
|
|
189
|
+
# 2. clean stale artifacts
|
|
190
|
+
rm -rf dist
|
|
191
|
+
|
|
192
|
+
# 3. build sdist + wheel into dist/
|
|
193
|
+
python -m build
|
|
194
|
+
|
|
195
|
+
# 4. upload to PyPI (needs a PyPI API token in ~/.pypirc or entered at the prompt)
|
|
196
|
+
twine upload dist/*
|
|
197
|
+
|
|
198
|
+
# 5. tag and push the release
|
|
199
|
+
git commit -am "release(x.y.z): <summary>"
|
|
200
|
+
git tag vx.y.z
|
|
201
|
+
git push origin main --tags
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Verify the published package in a clean environment:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
python -m venv /tmp/ac && /tmp/ac/bin/pip install "aircloud-cli==x.y.z"
|
|
208
|
+
/tmp/ac/bin/aircloud --help
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Status
|
|
212
|
+
|
|
213
|
+
This is an early functional release. Interfaces may change before `1.0.0`.
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
Proprietary. © AIEEV / AirCloud.
|
|
@@ -6,9 +6,10 @@ from urllib.parse import urlparse, urlunparse
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
8
|
from aircloud_cli import config
|
|
9
|
+
from aircloud_cli import ssh_config as sshcfg
|
|
9
10
|
from aircloud_cli.client import AirCloudAPIError, AirCloudClient, AirCloudError
|
|
10
11
|
from aircloud_cli.container_exec import start_exec
|
|
11
|
-
from aircloud_cli.ssh_tunnel import start_ssh, start_tunnel
|
|
12
|
+
from aircloud_cli.ssh_tunnel import run_stdio_proxy, start_ssh, start_tunnel
|
|
12
13
|
|
|
13
14
|
DEFAULT_SSH_HOST = "your-aircloud-host.example.com"
|
|
14
15
|
|
|
@@ -437,6 +438,151 @@ def ssh(
|
|
|
437
438
|
)
|
|
438
439
|
|
|
439
440
|
|
|
441
|
+
@cli.command("ssh-proxy")
|
|
442
|
+
@click.argument("endpoint_id")
|
|
443
|
+
@click.option(
|
|
444
|
+
"--stdio",
|
|
445
|
+
is_flag=True,
|
|
446
|
+
default=False,
|
|
447
|
+
help="Bridge stdin/stdout to the tunnel (for use as an ssh ProxyCommand).",
|
|
448
|
+
)
|
|
449
|
+
@click.option(
|
|
450
|
+
"--replica-id",
|
|
451
|
+
"-r",
|
|
452
|
+
default=None,
|
|
453
|
+
help="Pin the tunnel to a specific replica (default: load-balanced)",
|
|
454
|
+
)
|
|
455
|
+
@click.option(
|
|
456
|
+
"--host",
|
|
457
|
+
default=None,
|
|
458
|
+
help="Override host in the derived WS URL (default: inferred from endpoint)",
|
|
459
|
+
)
|
|
460
|
+
@click.option("--api-key", "-k", default=None, help="API key (default: from config)")
|
|
461
|
+
def ssh_proxy(
|
|
462
|
+
endpoint_id: str,
|
|
463
|
+
stdio: bool,
|
|
464
|
+
replica_id: str | None,
|
|
465
|
+
host: str | None,
|
|
466
|
+
api_key: str | None,
|
|
467
|
+
):
|
|
468
|
+
"""Bridge the SSH tunnel over stdin/stdout for use as an ssh ProxyCommand.
|
|
469
|
+
|
|
470
|
+
Designed for ~/.ssh/config so ssh / scp / VS Code Remote-SSH work
|
|
471
|
+
without pre-starting a tunnel or opening a local port:
|
|
472
|
+
|
|
473
|
+
\b
|
|
474
|
+
Host aircloud-myendpoint
|
|
475
|
+
HostName <ENDPOINT_ID>
|
|
476
|
+
User root
|
|
477
|
+
ProxyCommand aircloud ssh-proxy %h --stdio
|
|
478
|
+
ServerAliveInterval 30
|
|
479
|
+
|
|
480
|
+
Then `ssh aircloud-myendpoint` (or VS Code Remote-SSH) connects and
|
|
481
|
+
ssh spawns this proxy automatically. In --stdio mode stdout carries
|
|
482
|
+
raw SSH bytes, so all messages are emitted on stderr.
|
|
483
|
+
"""
|
|
484
|
+
resolved_api_key = api_key or config.get_api_key()
|
|
485
|
+
if not resolved_api_key:
|
|
486
|
+
raise click.ClickException(
|
|
487
|
+
"API key not set. Run: aircloud config set api-key <key>"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
client = _get_client(api_key=resolved_api_key)
|
|
491
|
+
endpoint = client.get_endpoint(endpoint_id)
|
|
492
|
+
serving_url = endpoint.get("serving_endpoint_url")
|
|
493
|
+
if not serving_url:
|
|
494
|
+
raise click.ClickException(
|
|
495
|
+
"Endpoint is missing serving_endpoint_url; cannot build SSH tunnel URL."
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
parsed = urlparse(serving_url)
|
|
499
|
+
scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
500
|
+
netloc = host or parsed.netloc
|
|
501
|
+
path = parsed.path.rstrip("/") + "/ssh/tunnel"
|
|
502
|
+
ws_url = urlunparse((scheme, netloc, path, "", "", ""))
|
|
503
|
+
|
|
504
|
+
click.echo(
|
|
505
|
+
f"aircloud ssh-proxy: tunneling to {endpoint_id} via {netloc}", err=True
|
|
506
|
+
)
|
|
507
|
+
run_stdio_proxy(ws_url, resolved_api_key, replica_id)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@cli.command("ssh-config")
|
|
511
|
+
@click.argument("endpoint_id")
|
|
512
|
+
@click.option("--alias", default=None, help="Host alias (default: aircloud-<endpoint_id>)")
|
|
513
|
+
@click.option("--user", "-u", default="root", show_default=True, help="SSH user")
|
|
514
|
+
@click.option(
|
|
515
|
+
"--identity-file",
|
|
516
|
+
"-i",
|
|
517
|
+
default=None,
|
|
518
|
+
help="Path to SSH private key to pin in the config block",
|
|
519
|
+
)
|
|
520
|
+
@click.option(
|
|
521
|
+
"--install",
|
|
522
|
+
"do_install",
|
|
523
|
+
is_flag=True,
|
|
524
|
+
help="Write the managed block into ~/.ssh/config (idempotent, backs up first)",
|
|
525
|
+
)
|
|
526
|
+
@click.option(
|
|
527
|
+
"--uninstall",
|
|
528
|
+
"do_uninstall",
|
|
529
|
+
is_flag=True,
|
|
530
|
+
help="Remove the managed block for this endpoint from ~/.ssh/config",
|
|
531
|
+
)
|
|
532
|
+
def ssh_config(
|
|
533
|
+
endpoint_id: str,
|
|
534
|
+
alias: str | None,
|
|
535
|
+
user: str,
|
|
536
|
+
identity_file: str | None,
|
|
537
|
+
do_install: bool,
|
|
538
|
+
do_uninstall: bool,
|
|
539
|
+
):
|
|
540
|
+
"""Manage a ~/.ssh/config block that routes ssh/scp/VS Code through ssh-proxy.
|
|
541
|
+
|
|
542
|
+
With no flag the block is printed for review. --install writes it
|
|
543
|
+
idempotently between aircloud markers (backing up the file first and
|
|
544
|
+
using an absolute aircloud path so VS Code Remote-SSH can find it);
|
|
545
|
+
--uninstall removes that managed block.
|
|
546
|
+
"""
|
|
547
|
+
if do_install and do_uninstall:
|
|
548
|
+
raise click.ClickException("Use only one of --install / --uninstall.")
|
|
549
|
+
|
|
550
|
+
host_alias = alias or f"aircloud-{endpoint_id}"
|
|
551
|
+
|
|
552
|
+
if do_uninstall:
|
|
553
|
+
path, removed, backup = sshcfg.uninstall(endpoint_id)
|
|
554
|
+
if removed:
|
|
555
|
+
click.echo(f"Removed aircloud block for {endpoint_id} from {path}")
|
|
556
|
+
if backup:
|
|
557
|
+
click.echo(f"Backup: {backup}")
|
|
558
|
+
else:
|
|
559
|
+
click.echo(f"No managed block for {endpoint_id} found in {path}")
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
aircloud_path = sshcfg.resolve_aircloud_path()
|
|
563
|
+
|
|
564
|
+
if do_install:
|
|
565
|
+
path, backup = sshcfg.install(
|
|
566
|
+
endpoint_id, host_alias, user, identity_file, aircloud_path
|
|
567
|
+
)
|
|
568
|
+
click.echo(f"Installed SSH config block into {path}")
|
|
569
|
+
if backup:
|
|
570
|
+
click.echo(f"Backup: {backup}")
|
|
571
|
+
click.echo(f"Connect: ssh {host_alias}")
|
|
572
|
+
click.echo(f"VS Code: Remote-SSH -> Connect to Host -> {host_alias}")
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
body = sshcfg.render_block(
|
|
576
|
+
endpoint_id, host_alias, user, identity_file, aircloud_path
|
|
577
|
+
)
|
|
578
|
+
click.echo(sshcfg.managed_block(endpoint_id, body).rstrip("\n"))
|
|
579
|
+
click.echo(
|
|
580
|
+
"\n# Append the block above to ~/.ssh/config, or rerun with --install.",
|
|
581
|
+
err=True,
|
|
582
|
+
)
|
|
583
|
+
click.echo(f"# Then: ssh {host_alias}", err=True)
|
|
584
|
+
|
|
585
|
+
|
|
440
586
|
@cli.command("exec")
|
|
441
587
|
@click.argument("endpoint_id")
|
|
442
588
|
@click.option(
|