xenfra-sdk 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- xenfra_sdk/engine.py +46 -22
- xenfra_sdk/resources/deployments.py +12 -5
- xenfra_sdk/templates/docker-compose.yml.j2 +3 -9
- {xenfra_sdk-0.2.0.dist-info → xenfra_sdk-0.2.1.dist-info}/METADATA +1 -1
- {xenfra_sdk-0.2.0.dist-info → xenfra_sdk-0.2.1.dist-info}/RECORD +6 -6
- {xenfra_sdk-0.2.0.dist-info → xenfra_sdk-0.2.1.dist-info}/WHEEL +1 -1
xenfra_sdk/engine.py
CHANGED
|
@@ -162,7 +162,7 @@ class InfraEngine:
|
|
|
162
162
|
# 3. Stream logs
|
|
163
163
|
ip_address = droplet.ip_address
|
|
164
164
|
with self._get_connection(ip_address) as conn:
|
|
165
|
-
conn.run("cd /root/app && docker
|
|
165
|
+
conn.run("cd /root/app && docker compose logs -f app", pty=True)
|
|
166
166
|
|
|
167
167
|
def get_account_balance(self) -> dict:
|
|
168
168
|
"""
|
|
@@ -244,6 +244,9 @@ class InfraEngine:
|
|
|
244
244
|
repo_url: Optional[str] = None,
|
|
245
245
|
is_dockerized: bool = True,
|
|
246
246
|
db_session: Session = None,
|
|
247
|
+
port: int = 8000,
|
|
248
|
+
command: str = None,
|
|
249
|
+
database: str = None,
|
|
247
250
|
**kwargs,
|
|
248
251
|
):
|
|
249
252
|
"""A stateful, blocking orchestrator for deploying a new server."""
|
|
@@ -260,7 +263,10 @@ class InfraEngine:
|
|
|
260
263
|
"email": email,
|
|
261
264
|
"domain": domain,
|
|
262
265
|
"repo_url": repo_url,
|
|
263
|
-
|
|
266
|
+
"port": port,
|
|
267
|
+
"command": command,
|
|
268
|
+
"database": database,
|
|
269
|
+
**kwargs, # Pass any additional config
|
|
264
270
|
}
|
|
265
271
|
files = dockerizer.generate_templated_assets(context)
|
|
266
272
|
for file in files:
|
|
@@ -282,8 +288,8 @@ class InfraEngine:
|
|
|
282
288
|
region=region,
|
|
283
289
|
image=image,
|
|
284
290
|
size_slug=size,
|
|
285
|
-
ssh_keys=[ssh_key],
|
|
286
|
-
|
|
291
|
+
ssh_keys=[ssh_key.id],
|
|
292
|
+
user_data=cloud_init_script,
|
|
287
293
|
tags=["xenfra"],
|
|
288
294
|
)
|
|
289
295
|
droplet.create()
|
|
@@ -324,22 +330,36 @@ class InfraEngine:
|
|
|
324
330
|
if not conn or not conn.is_connected:
|
|
325
331
|
raise DeploymentError("Could not establish SSH connection.", stage="Polling")
|
|
326
332
|
|
|
333
|
+
logger(" - [DEBUG] Entering SSH context for Phase 5 polling...")
|
|
327
334
|
with conn:
|
|
328
335
|
last_log_line = 0
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
336
|
+
logger(" - Polling server setup log (/root/setup.log)...")
|
|
337
|
+
for i in range(120): # 20-minute timeout
|
|
338
|
+
# Heartbeat
|
|
339
|
+
if i % 3 == 0: # Every 30 seconds
|
|
340
|
+
logger(f" - Phase 5 Heartbeat: Waiting for setup completion ({i+1}/120)...")
|
|
341
|
+
|
|
342
|
+
# Check for completion with timeout
|
|
343
|
+
try:
|
|
344
|
+
check_result = conn.run("test -f /root/setup_complete", warn=True, hide=True, timeout=10)
|
|
345
|
+
if check_result.ok:
|
|
346
|
+
logger(" - Cloud-init setup complete.")
|
|
347
|
+
break
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger(f" - [Warning] Status check failed: {e}. Retrying...")
|
|
334
350
|
|
|
335
351
|
# Tail the setup log for visibility
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
352
|
+
try:
|
|
353
|
+
log_result = conn.run(f"tail -n +{last_log_line + 1} /root/setup.log 2>/dev/null", warn=True, hide=True, timeout=10)
|
|
354
|
+
if log_result.ok and log_result.stdout.strip():
|
|
355
|
+
new_lines = log_result.stdout.strip().split("\n")
|
|
356
|
+
for line in new_lines:
|
|
357
|
+
if line.strip():
|
|
358
|
+
logger(f" [Server Setup] {line.strip()}")
|
|
359
|
+
last_log_line += len(new_lines)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
# Log doesn't exist yet or tail failed
|
|
362
|
+
pass
|
|
343
363
|
|
|
344
364
|
time.sleep(10)
|
|
345
365
|
else:
|
|
@@ -363,16 +383,20 @@ class InfraEngine:
|
|
|
363
383
|
stage="Code Upload",
|
|
364
384
|
)
|
|
365
385
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
386
|
+
# Use rsync for efficient local folder upload
|
|
387
|
+
private_key_path = str(Path.home() / ".ssh" / "id_rsa")
|
|
388
|
+
rsync_cmd = f'rsync -avz --exclude=".git" --exclude=".venv" --exclude="__pycache__" -e "ssh -i {private_key_path} -o StrictHostKeyChecking=no" . root@{ip_address}:/root/app/'
|
|
389
|
+
logger(f" - Uploading local code via rsync...")
|
|
390
|
+
result = subprocess.run(rsync_cmd, shell=True, capture_output=True, text=True)
|
|
391
|
+
if result.returncode != 0:
|
|
392
|
+
raise DeploymentError(f"rsync failed: {result.stderr}", stage="Code Upload")
|
|
369
393
|
logger(" - Code upload complete.")
|
|
370
394
|
|
|
371
395
|
# === 7. FINAL DEPLOY STAGE ===
|
|
372
396
|
if is_dockerized:
|
|
373
397
|
logger("\n[bold blue]PHASE 7: BUILDING AND DEPLOYING CONTAINERS[/bold blue]")
|
|
374
398
|
with self._get_connection(ip_address) as conn:
|
|
375
|
-
result = conn.run("cd /root/app && docker
|
|
399
|
+
result = conn.run("cd /root/app && docker compose up -d --build", hide=True)
|
|
376
400
|
if result.failed:
|
|
377
401
|
raise DeploymentError(f"docker-compose failed: {result.stderr}", stage="Deploy")
|
|
378
402
|
logger(" - Docker containers are building in the background...")
|
|
@@ -393,7 +417,7 @@ class InfraEngine:
|
|
|
393
417
|
with self._get_connection(ip_address) as conn:
|
|
394
418
|
# Check if running
|
|
395
419
|
if is_dockerized:
|
|
396
|
-
ps_result = conn.run("cd /root/app && docker
|
|
420
|
+
ps_result = conn.run("cd /root/app && docker compose ps", hide=True)
|
|
397
421
|
running = "running" in ps_result.stdout
|
|
398
422
|
else:
|
|
399
423
|
ps_result = conn.run("ps aux | grep -v grep | grep python", hide=True)
|
|
@@ -432,7 +456,7 @@ class InfraEngine:
|
|
|
432
456
|
else:
|
|
433
457
|
# On failure, get logs and destroy droplet
|
|
434
458
|
with self._get_connection(ip_address) as conn:
|
|
435
|
-
logs = conn.run("cd /root/app && docker
|
|
459
|
+
logs = conn.run("cd /root/app && docker compose logs", hide=True).stdout
|
|
436
460
|
raise DeploymentError(
|
|
437
461
|
f"Application failed to become healthy in time. Logs:\n{logs}",
|
|
438
462
|
stage="Verification",
|
|
@@ -97,7 +97,7 @@ class DeploymentsManager(BaseManager):
|
|
|
97
97
|
except Exception as e:
|
|
98
98
|
raise XenfraError(f"Failed to get logs for deployment {deployment_id}: {e}")
|
|
99
99
|
|
|
100
|
-
def create_stream(self, project_name: str, git_repo: str, branch: str, framework: str, region: str = None, size_slug: str = None, is_dockerized: bool = True) -> Iterator[dict]:
|
|
100
|
+
def create_stream(self, project_name: str, git_repo: str, branch: str, framework: str, region: str = None, size_slug: str = None, is_dockerized: bool = True, port: int = None, command: str = None, database: str = None) -> Iterator[dict]:
|
|
101
101
|
"""
|
|
102
102
|
Creates a new deployment with real-time SSE log streaming.
|
|
103
103
|
|
|
@@ -110,6 +110,10 @@ class DeploymentsManager(BaseManager):
|
|
|
110
110
|
framework: Framework type (fastapi, flask, django)
|
|
111
111
|
region: DigitalOcean region (optional)
|
|
112
112
|
size_slug: DigitalOcean droplet size (optional)
|
|
113
|
+
is_dockerized: Whether to use Docker (optional)
|
|
114
|
+
port: Application port (optional, default 8000)
|
|
115
|
+
command: Start command (optional, auto-detected if not provided)
|
|
116
|
+
database: Database type (optional, e.g. 'postgres')
|
|
113
117
|
|
|
114
118
|
Yields:
|
|
115
119
|
dict: SSE events with 'event' and 'data' fields
|
|
@@ -133,6 +137,12 @@ class DeploymentsManager(BaseManager):
|
|
|
133
137
|
payload["size_slug"] = size_slug
|
|
134
138
|
if is_dockerized is not None:
|
|
135
139
|
payload["is_dockerized"] = is_dockerized
|
|
140
|
+
if port:
|
|
141
|
+
payload["port"] = port
|
|
142
|
+
if command:
|
|
143
|
+
payload["command"] = command
|
|
144
|
+
if database:
|
|
145
|
+
payload["database"] = database
|
|
136
146
|
|
|
137
147
|
try:
|
|
138
148
|
# Use httpx to stream the SSE response
|
|
@@ -150,11 +160,8 @@ class DeploymentsManager(BaseManager):
|
|
|
150
160
|
streaming_api_url = os.getenv("XENFRA_STREAMING_API_URL")
|
|
151
161
|
if streaming_api_url:
|
|
152
162
|
base_url = streaming_api_url
|
|
153
|
-
elif self._client.api_url == "https://api.xenfra.tech":
|
|
154
|
-
# Production: use non-proxied streaming subdomain
|
|
155
|
-
base_url = "https://stream.xenfra.tech"
|
|
156
163
|
else:
|
|
157
|
-
# Local/dev: use regular API URL
|
|
164
|
+
# Local/dev/production: use regular API URL
|
|
158
165
|
base_url = self._client.api_url
|
|
159
166
|
|
|
160
167
|
url = f"{base_url}/deployments/stream"
|
|
@@ -6,17 +6,13 @@ services:
|
|
|
6
6
|
build: .
|
|
7
7
|
ports:
|
|
8
8
|
- "{{ port | default(8000) }}:{{ port | default(8000) }}"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
command: {{ command }}
|
|
12
|
-
{% if database == 'postgres' %}
|
|
9
|
+
command: {{ command | default('uvicorn main:app --host 0.0.0.0 --port 8000') }}
|
|
10
|
+
{% if database == 'postgres' or database == 'postgresql' %}
|
|
13
11
|
depends_on:
|
|
14
12
|
- db
|
|
15
13
|
environment:
|
|
16
14
|
- DATABASE_URL=postgresql://{{ db_user | default('user') }}:{{ db_password | default('password') }}@db:5432/{{ db_name | default('appdb') }}
|
|
17
|
-
{% endif %}
|
|
18
15
|
|
|
19
|
-
{% if database == 'postgres' %}
|
|
20
16
|
db:
|
|
21
17
|
image: postgres:15-alpine
|
|
22
18
|
volumes:
|
|
@@ -25,9 +21,7 @@ services:
|
|
|
25
21
|
- POSTGRES_USER={{ db_user | default('user') }}
|
|
26
22
|
- POSTGRES_PASSWORD={{ db_password | default('password') }}
|
|
27
23
|
- POSTGRES_DB={{ db_name | default('appdb') }}
|
|
28
|
-
{% endif %}
|
|
29
24
|
|
|
30
25
|
volumes:
|
|
31
|
-
{% if database == 'postgres' %}
|
|
32
26
|
postgres_data:
|
|
33
|
-
|
|
27
|
+
{% endif %}
|
|
@@ -9,7 +9,7 @@ xenfra_sdk/db/models.py,sha256=oNT9UhYzr_SgzDJdPu5fF6IiiztWjm2tHZzpIAYVJ4Y,706
|
|
|
9
9
|
xenfra_sdk/db/session.py,sha256=ul6tFBBDKU8UErJsZ-g7b8cryLr9-yA6jGY3d8fD15Q,853
|
|
10
10
|
xenfra_sdk/dependencies.py,sha256=k7tiy_5dayr_iQx_i2klGeNjGWhxfivGVC38SpvQvzE,1250
|
|
11
11
|
xenfra_sdk/dockerizer.py,sha256=Fml-1QpUtQqslHYG12igxDvGEpYjx1E9qp0oWbBsofk,3962
|
|
12
|
-
xenfra_sdk/engine.py,sha256=
|
|
12
|
+
xenfra_sdk/engine.py,sha256=ACle-suwDNu-KY2Sv3W86aXP5-HFHdJ6vnpK4w5TSk8,20820
|
|
13
13
|
xenfra_sdk/exceptions.py,sha256=FaKctVJNMFIw09G8Lm2yd_DHhSngGcyh2j_BTgXMM60,496
|
|
14
14
|
xenfra_sdk/mcp_client.py,sha256=GPsVJtWT87G7k_GxePATLfCqFkYFFNIbpW5AsZi4Bhk,5931
|
|
15
15
|
xenfra_sdk/models.py,sha256=-QkHFAnimEQA1hZ70YcordlbKWbO9mgzDUdO03qg5BI,7278
|
|
@@ -18,14 +18,14 @@ xenfra_sdk/privacy.py,sha256=ksGf5L9PVtRP-xZS3T-Gj7MKfexTqIMgbFLoYkIESOE,5662
|
|
|
18
18
|
xenfra_sdk/recipes.py,sha256=jSFlPLIze3rKpBaeFKNVkWESQzBXWGkoSwSH1jwx0Vs,973
|
|
19
19
|
xenfra_sdk/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
xenfra_sdk/resources/base.py,sha256=C6BuZfhR-oU5ecHSfkGG6ZLU6MHGXUyCWoy1F-yTIf8,84
|
|
21
|
-
xenfra_sdk/resources/deployments.py,sha256=
|
|
21
|
+
xenfra_sdk/resources/deployments.py,sha256=0IS54LyoLLCB6h25a2jgM2jk3tvYmb2Auf49p11C2nU,9751
|
|
22
22
|
xenfra_sdk/resources/intelligence.py,sha256=Y11K6_iXfm2QKTbH1vUmt45MifLoVtZtlHEkqbzmTzs,3418
|
|
23
23
|
xenfra_sdk/resources/projects.py,sha256=EsCVXmqkhWl_Guz_8WDQDi3kAm1Wyg1rjXcyAigPD6E,3712
|
|
24
24
|
xenfra_sdk/security.py,sha256=Px887RRb1BUDXaPUrxmQITJ1mHyOyupCJqEDZ78F7Tk,1240
|
|
25
25
|
xenfra_sdk/templates/Dockerfile.j2,sha256=GXc0JiaF-HsxTQS15Gs2fcvsIhA1EHnwapdFVitUWS0,689
|
|
26
26
|
xenfra_sdk/templates/cloud-init.sh.j2,sha256=fxQP2GVAAVVvDAqlOVH3eNmeKSTFQ2soErQQRD5RX3U,3072
|
|
27
|
-
xenfra_sdk/templates/docker-compose.yml.j2,sha256=
|
|
27
|
+
xenfra_sdk/templates/docker-compose.yml.j2,sha256=93AvztUyUMgYbdycB9EHow1qCkeNmQ2NR2G9FD5zh5A,840
|
|
28
28
|
xenfra_sdk/utils.py,sha256=d8eCjjV32QwqoJa759CEcETnnsjG5qVKDLQ84yYtlus,3898
|
|
29
|
-
xenfra_sdk-0.2.
|
|
30
|
-
xenfra_sdk-0.2.
|
|
31
|
-
xenfra_sdk-0.2.
|
|
29
|
+
xenfra_sdk-0.2.1.dist-info/WHEEL,sha256=eycQt0QpYmJMLKpE3X9iDk8R04v2ZF0x82ogq-zP6bQ,79
|
|
30
|
+
xenfra_sdk-0.2.1.dist-info/METADATA,sha256=-nLfpSQm2a5dz_hhGafkZ1OVyVX9ZU6IRCmQKGMu5es,3980
|
|
31
|
+
xenfra_sdk-0.2.1.dist-info/RECORD,,
|