xenfra-sdk 0.1.1__py3-none-any.whl → 0.1.3__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/__init__.py +21 -21
- xenfra_sdk/cli/main.py +226 -226
- xenfra_sdk/client.py +4 -3
- xenfra_sdk/client_with_hooks.py +4 -2
- xenfra_sdk/config.py +26 -26
- xenfra_sdk/db/models.py +24 -27
- xenfra_sdk/db/session.py +30 -30
- xenfra_sdk/dependencies.py +39 -38
- xenfra_sdk/dockerizer.py +87 -87
- xenfra_sdk/engine.py +411 -388
- xenfra_sdk/exceptions.py +19 -19
- xenfra_sdk/mcp_client.py +154 -154
- xenfra_sdk/models.py +182 -170
- xenfra_sdk/patterns.json +13 -13
- xenfra_sdk/privacy.py +153 -151
- xenfra_sdk/recipes.py +25 -25
- xenfra_sdk/resources/base.py +3 -3
- xenfra_sdk/resources/deployments.py +89 -85
- xenfra_sdk/resources/intelligence.py +95 -105
- xenfra_sdk/resources/projects.py +5 -10
- xenfra_sdk/security.py +41 -41
- xenfra_sdk/templates/Dockerfile.j2 +25 -25
- xenfra_sdk/templates/cloud-init.sh.j2 +68 -68
- xenfra_sdk/templates/docker-compose.yml.j2 +33 -33
- xenfra_sdk/utils.py +2 -4
- {xenfra_sdk-0.1.1.dist-info → xenfra_sdk-0.1.3.dist-info}/METADATA +92 -92
- xenfra_sdk-0.1.3.dist-info/RECORD +31 -0
- {xenfra_sdk-0.1.1.dist-info → xenfra_sdk-0.1.3.dist-info}/WHEEL +1 -1
- xenfra_sdk-0.1.1.dist-info/RECORD +0 -31
|
@@ -1,105 +1,95 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Intelligence resource manager for Xenfra SDK.
|
|
3
|
-
Provides AI-powered deployment diagnosis and codebase analysis.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
from ..
|
|
9
|
-
from ..
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
logs: str,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
If provided, AI will
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if
|
|
51
|
-
payload["
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
response =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
f"IntelligenceManager.analyze_codebase response: status={response.status_code}"
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Safe JSON parsing
|
|
100
|
-
data = safe_json_parse(response)
|
|
101
|
-
return CodebaseAnalysisResponse(**data)
|
|
102
|
-
except XenfraAPIError:
|
|
103
|
-
raise
|
|
104
|
-
except Exception as e:
|
|
105
|
-
raise XenfraError(f"Failed to analyze codebase: {e}")
|
|
1
|
+
"""
|
|
2
|
+
Intelligence resource manager for Xenfra SDK.
|
|
3
|
+
Provides AI-powered deployment diagnosis and codebase analysis.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from ..exceptions import XenfraAPIError, XenfraError
|
|
9
|
+
from ..models import CodebaseAnalysisResponse, DiagnosisResponse
|
|
10
|
+
from ..utils import safe_json_parse
|
|
11
|
+
from .base import BaseManager
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IntelligenceManager(BaseManager):
|
|
17
|
+
"""
|
|
18
|
+
Manager for AI-powered intelligence operations.
|
|
19
|
+
|
|
20
|
+
Provides:
|
|
21
|
+
- Deployment failure diagnosis (Zen Nod)
|
|
22
|
+
- Codebase analysis for zero-config init (Zen Init)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def diagnose(
|
|
26
|
+
self, logs: str, package_manager: str | None = None, dependency_file: str | None = None
|
|
27
|
+
) -> DiagnosisResponse:
|
|
28
|
+
"""
|
|
29
|
+
Diagnose deployment failure from logs using AI.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
logs: The deployment logs to analyze
|
|
33
|
+
package_manager: Optional package manager context (uv, pip, poetry, npm, etc.)
|
|
34
|
+
If provided, AI will target this manager's dependency file
|
|
35
|
+
dependency_file: Optional dependency file context (pyproject.toml, requirements.txt, etc.)
|
|
36
|
+
If provided, AI will suggest patches for this file
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
DiagnosisResponse with diagnosis, suggestion, and optional patch
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
XenfraAPIError: If the API request fails
|
|
43
|
+
XenfraError: If parsing the response fails
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
# Build request payload
|
|
47
|
+
payload = {"logs": logs}
|
|
48
|
+
if package_manager:
|
|
49
|
+
payload["package_manager"] = package_manager
|
|
50
|
+
if dependency_file:
|
|
51
|
+
payload["dependency_file"] = dependency_file
|
|
52
|
+
|
|
53
|
+
response = self._client._request("POST", "/intelligence/diagnose", json=payload)
|
|
54
|
+
|
|
55
|
+
logger.debug(f"IntelligenceManager.diagnose response: status={response.status_code}")
|
|
56
|
+
|
|
57
|
+
# Safe JSON parsing
|
|
58
|
+
data = safe_json_parse(response)
|
|
59
|
+
return DiagnosisResponse(**data)
|
|
60
|
+
except XenfraAPIError:
|
|
61
|
+
raise
|
|
62
|
+
except Exception as e:
|
|
63
|
+
raise XenfraError(f"Failed to diagnose logs: {e}")
|
|
64
|
+
|
|
65
|
+
def analyze_codebase(self, code_snippets: dict[str, str]) -> CodebaseAnalysisResponse:
|
|
66
|
+
"""
|
|
67
|
+
Analyze codebase to detect framework, dependencies, and deployment config.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
code_snippets: Dictionary of filename -> content
|
|
71
|
+
e.g., {"main.py": "...", "requirements.txt": "..."}
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
CodebaseAnalysisResponse with detected configuration
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
XenfraAPIError: If the API request fails
|
|
78
|
+
XenfraError: If parsing the response fails
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
response = self._client._request(
|
|
82
|
+
"POST", "/intelligence/analyze-codebase", json={"code_snippets": code_snippets}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
logger.debug(
|
|
86
|
+
f"IntelligenceManager.analyze_codebase response: status={response.status_code}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Safe JSON parsing
|
|
90
|
+
data = safe_json_parse(response)
|
|
91
|
+
return CodebaseAnalysisResponse(**data)
|
|
92
|
+
except XenfraAPIError:
|
|
93
|
+
raise
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise XenfraError(f"Failed to analyze codebase: {e}")
|
xenfra_sdk/resources/projects.py
CHANGED
|
@@ -24,7 +24,9 @@ class ProjectsManager(BaseManager):
|
|
|
24
24
|
projects = safe_get_json_field(data, "projects", [])
|
|
25
25
|
|
|
26
26
|
if not isinstance(projects, list):
|
|
27
|
-
raise XenfraError(
|
|
27
|
+
raise XenfraError(
|
|
28
|
+
f"Expected 'projects' to be a list, got {type(projects).__name__}"
|
|
29
|
+
)
|
|
28
30
|
|
|
29
31
|
return [ProjectRead(**p) for p in projects]
|
|
30
32
|
except XenfraAPIError:
|
|
@@ -58,10 +60,7 @@ class ProjectsManager(BaseManager):
|
|
|
58
60
|
raise XenfraError(f"Failed to get project {project_id}: {e}")
|
|
59
61
|
|
|
60
62
|
def create(
|
|
61
|
-
self,
|
|
62
|
-
name: str,
|
|
63
|
-
region: str = "nyc3",
|
|
64
|
-
size_slug: str = "s-1vcpu-1gb"
|
|
63
|
+
self, name: str, region: str = "nyc3", size_slug: str = "s-1vcpu-1gb"
|
|
65
64
|
) -> ProjectRead:
|
|
66
65
|
"""Create a new project.
|
|
67
66
|
|
|
@@ -78,11 +77,7 @@ class ProjectsManager(BaseManager):
|
|
|
78
77
|
XenfraError: If there's a network or parsing error.
|
|
79
78
|
"""
|
|
80
79
|
try:
|
|
81
|
-
payload = {
|
|
82
|
-
"name": name,
|
|
83
|
-
"region": region,
|
|
84
|
-
"size_slug": size_slug
|
|
85
|
-
}
|
|
80
|
+
payload = {"name": name, "region": region, "size_slug": size_slug}
|
|
86
81
|
logger.debug(f"ProjectsManager.create payload: {payload}")
|
|
87
82
|
response = self._client._request("POST", "/projects/", json=payload)
|
|
88
83
|
# Safe JSON parsing
|
xenfra_sdk/security.py
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
# src/xenfra_sdk/security.py
|
|
2
|
-
"""
|
|
3
|
-
Security utilities for the Xenfra SDK.
|
|
4
|
-
Provides token encryption/decryption for storing OAuth credentials.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
from typing import Optional
|
|
9
|
-
|
|
10
|
-
from cryptography.fernet import Fernet
|
|
11
|
-
|
|
12
|
-
# --- Configuration from Environment ---
|
|
13
|
-
# These should be set in the service's environment
|
|
14
|
-
ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", "")
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _get_fernet() -> Optional[Fernet]:
|
|
18
|
-
"""Get Fernet instance for encryption/decryption."""
|
|
19
|
-
if not ENCRYPTION_KEY:
|
|
20
|
-
return None
|
|
21
|
-
try:
|
|
22
|
-
return Fernet(ENCRYPTION_KEY.encode())
|
|
23
|
-
except Exception:
|
|
24
|
-
return None
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# --- Token Encryption ---
|
|
28
|
-
def encrypt_token(token: str) -> str:
|
|
29
|
-
"""Encrypts a token using Fernet symmetric encryption."""
|
|
30
|
-
fernet = _get_fernet()
|
|
31
|
-
if fernet is None:
|
|
32
|
-
raise ValueError("ENCRYPTION_KEY environment variable is not set or invalid")
|
|
33
|
-
return fernet.encrypt(token.encode()).decode()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def decrypt_token(encrypted_token: str) -> str:
|
|
37
|
-
"""Decrypts a token."""
|
|
38
|
-
fernet = _get_fernet()
|
|
39
|
-
if fernet is None:
|
|
40
|
-
raise ValueError("ENCRYPTION_KEY environment variable is not set or invalid")
|
|
41
|
-
return fernet.decrypt(encrypted_token.encode()).decode()
|
|
1
|
+
# src/xenfra_sdk/security.py
|
|
2
|
+
"""
|
|
3
|
+
Security utilities for the Xenfra SDK.
|
|
4
|
+
Provides token encryption/decryption for storing OAuth credentials.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from cryptography.fernet import Fernet
|
|
11
|
+
|
|
12
|
+
# --- Configuration from Environment ---
|
|
13
|
+
# These should be set in the service's environment
|
|
14
|
+
ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", "")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_fernet() -> Optional[Fernet]:
|
|
18
|
+
"""Get Fernet instance for encryption/decryption."""
|
|
19
|
+
if not ENCRYPTION_KEY:
|
|
20
|
+
return None
|
|
21
|
+
try:
|
|
22
|
+
return Fernet(ENCRYPTION_KEY.encode())
|
|
23
|
+
except Exception:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- Token Encryption ---
|
|
28
|
+
def encrypt_token(token: str) -> str:
|
|
29
|
+
"""Encrypts a token using Fernet symmetric encryption."""
|
|
30
|
+
fernet = _get_fernet()
|
|
31
|
+
if fernet is None:
|
|
32
|
+
raise ValueError("ENCRYPTION_KEY environment variable is not set or invalid")
|
|
33
|
+
return fernet.encrypt(token.encode()).decode()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def decrypt_token(encrypted_token: str) -> str:
|
|
37
|
+
"""Decrypts a token."""
|
|
38
|
+
fernet = _get_fernet()
|
|
39
|
+
if fernet is None:
|
|
40
|
+
raise ValueError("ENCRYPTION_KEY environment variable is not set or invalid")
|
|
41
|
+
return fernet.decrypt(encrypted_token.encode()).decode()
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
# Dockerfile template for Python web applications
|
|
2
|
-
FROM {{ python_version | default('python:3.11-slim') }}
|
|
3
|
-
|
|
4
|
-
WORKDIR /app
|
|
5
|
-
|
|
6
|
-
# Install uv, our preferred package manager
|
|
7
|
-
RUN apt-get update && apt-get install -y curl && \
|
|
8
|
-
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
|
9
|
-
apt-get remove -y curl && \
|
|
10
|
-
apt-get clean && \
|
|
11
|
-
rm -rf /var/lib/apt/lists/*
|
|
12
|
-
|
|
13
|
-
COPY requirements.txt .
|
|
14
|
-
|
|
15
|
-
# Install dependencies
|
|
16
|
-
RUN /root/.cargo/bin/uv pip install --system --no-cache -r requirements.txt
|
|
17
|
-
|
|
18
|
-
COPY . .
|
|
19
|
-
|
|
20
|
-
# Expose the application port
|
|
21
|
-
EXPOSE {{ port | default(8000) }}
|
|
22
|
-
|
|
23
|
-
# The command to run the application will be in docker-compose.yml
|
|
24
|
-
# This allows for more flexibility
|
|
25
|
-
|
|
1
|
+
# Dockerfile template for Python web applications
|
|
2
|
+
FROM {{ python_version | default('python:3.11-slim') }}
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
|
|
6
|
+
# Install uv, our preferred package manager
|
|
7
|
+
RUN apt-get update && apt-get install -y curl && \
|
|
8
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
|
9
|
+
apt-get remove -y curl && \
|
|
10
|
+
apt-get clean && \
|
|
11
|
+
rm -rf /var/lib/apt/lists/*
|
|
12
|
+
|
|
13
|
+
COPY requirements.txt .
|
|
14
|
+
|
|
15
|
+
# Install dependencies
|
|
16
|
+
RUN /root/.cargo/bin/uv pip install --system --no-cache -r requirements.txt
|
|
17
|
+
|
|
18
|
+
COPY . .
|
|
19
|
+
|
|
20
|
+
# Expose the application port
|
|
21
|
+
EXPOSE {{ port | default(8000) }}
|
|
22
|
+
|
|
23
|
+
# The command to run the application will be in docker-compose.yml
|
|
24
|
+
# This allows for more flexibility
|
|
25
|
+
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
export DEBIAN_FRONTEND=noninteractive
|
|
3
|
-
LOG="/root/setup.log"
|
|
4
|
-
touch $LOG
|
|
5
|
-
|
|
6
|
-
echo "--------------------------------" >> $LOG
|
|
7
|
-
echo "🧘 XENFRA: Context-Aware Boot" >> $LOG
|
|
8
|
-
echo "--------------------------------" >> $LOG
|
|
9
|
-
|
|
10
|
-
# Create App Directory
|
|
11
|
-
mkdir -p /root/app
|
|
12
|
-
cd /root/app
|
|
13
|
-
|
|
14
|
-
# --- AGGRESSIVE FIX: KILL BACKGROUND UPDATES ---
|
|
15
|
-
echo "⚔️ [0/6] Stopping Background Updates..." >> $LOG
|
|
16
|
-
systemctl stop unattended-upgrades.service || true
|
|
17
|
-
systemctl stop apt-daily.service || true
|
|
18
|
-
systemctl stop apt-daily-upgrade.service || true
|
|
19
|
-
systemctl kill --kill-who=all apt-daily.service || true
|
|
20
|
-
systemctl kill --kill-who=all apt-daily-upgrade.service || true
|
|
21
|
-
|
|
22
|
-
# Force remove locks if they exist
|
|
23
|
-
rm -f /var/lib/dpkg/lock*
|
|
24
|
-
rm -f /var/lib/apt/lists/lock
|
|
25
|
-
rm -f /var/cache/apt/archives/lock
|
|
26
|
-
dpkg --configure -a || true
|
|
27
|
-
# -----------------------------------------------
|
|
28
|
-
|
|
29
|
-
# 1. System Updates
|
|
30
|
-
echo "🔄 [1/5] Refreshing Package Lists..." >> $LOG
|
|
31
|
-
apt-get update
|
|
32
|
-
apt-get install -y python3-pip git curl
|
|
33
|
-
|
|
34
|
-
# 2. Install Docker & Compose
|
|
35
|
-
echo "🐳 [2/5] Installing Docker..." >> $LOG
|
|
36
|
-
apt-get install -y docker.io || (curl -fsSL https://get.docker.com | sh)
|
|
37
|
-
echo "🎶 [3/5] Installing Docker Compose..." >> $LOG
|
|
38
|
-
apt-get install -y docker-compose-v2
|
|
39
|
-
|
|
40
|
-
# --- DOCKERIZED DEPLOYMENT ---
|
|
41
|
-
echo "📦 [4/5] Installing Caddy..." >> $LOG
|
|
42
|
-
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
|
|
43
|
-
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
44
|
-
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
|
45
|
-
apt-get update
|
|
46
|
-
apt-get install -y caddy
|
|
47
|
-
|
|
48
|
-
{% if domain %}
|
|
49
|
-
# Dynamically generate Caddyfile content
|
|
50
|
-
echo "🔒 Writing Caddyfile for {{ domain }}..." >> $LOG
|
|
51
|
-
cat << EOF > /etc/caddy/Caddyfile
|
|
52
|
-
{{ domain }}:80, {{ domain }}:443 {
|
|
53
|
-
reverse_proxy localhost:{{ port | default(8000) }}
|
|
54
|
-
tls {{ email }}
|
|
55
|
-
}
|
|
56
|
-
EOF
|
|
57
|
-
{% endif %}
|
|
58
|
-
|
|
59
|
-
{% if domain %}
|
|
60
|
-
echo "🚀 [5/5] Starting Caddy..." >> $LOG
|
|
61
|
-
systemctl restart caddy
|
|
62
|
-
{% else %}
|
|
63
|
-
echo "✅ [5/5] Skipping Caddy start (no domain specified)." >> $LOG
|
|
64
|
-
{% endif %}
|
|
65
|
-
|
|
66
|
-
# Finish
|
|
67
|
-
echo "✅ SETUP SCRIPT COMPLETE" >> $LOG
|
|
68
|
-
touch /root/setup_complete
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
3
|
+
LOG="/root/setup.log"
|
|
4
|
+
touch $LOG
|
|
5
|
+
|
|
6
|
+
echo "--------------------------------" >> $LOG
|
|
7
|
+
echo "🧘 XENFRA: Context-Aware Boot" >> $LOG
|
|
8
|
+
echo "--------------------------------" >> $LOG
|
|
9
|
+
|
|
10
|
+
# Create App Directory
|
|
11
|
+
mkdir -p /root/app
|
|
12
|
+
cd /root/app
|
|
13
|
+
|
|
14
|
+
# --- AGGRESSIVE FIX: KILL BACKGROUND UPDATES ---
|
|
15
|
+
echo "⚔️ [0/6] Stopping Background Updates..." >> $LOG
|
|
16
|
+
systemctl stop unattended-upgrades.service || true
|
|
17
|
+
systemctl stop apt-daily.service || true
|
|
18
|
+
systemctl stop apt-daily-upgrade.service || true
|
|
19
|
+
systemctl kill --kill-who=all apt-daily.service || true
|
|
20
|
+
systemctl kill --kill-who=all apt-daily-upgrade.service || true
|
|
21
|
+
|
|
22
|
+
# Force remove locks if they exist
|
|
23
|
+
rm -f /var/lib/dpkg/lock*
|
|
24
|
+
rm -f /var/lib/apt/lists/lock
|
|
25
|
+
rm -f /var/cache/apt/archives/lock
|
|
26
|
+
dpkg --configure -a || true
|
|
27
|
+
# -----------------------------------------------
|
|
28
|
+
|
|
29
|
+
# 1. System Updates
|
|
30
|
+
echo "🔄 [1/5] Refreshing Package Lists..." >> $LOG
|
|
31
|
+
apt-get update
|
|
32
|
+
apt-get install -y python3-pip git curl
|
|
33
|
+
|
|
34
|
+
# 2. Install Docker & Compose
|
|
35
|
+
echo "🐳 [2/5] Installing Docker..." >> $LOG
|
|
36
|
+
apt-get install -y docker.io || (curl -fsSL https://get.docker.com | sh)
|
|
37
|
+
echo "🎶 [3/5] Installing Docker Compose..." >> $LOG
|
|
38
|
+
apt-get install -y docker-compose-v2
|
|
39
|
+
|
|
40
|
+
# --- DOCKERIZED DEPLOYMENT ---
|
|
41
|
+
echo "📦 [4/5] Installing Caddy..." >> $LOG
|
|
42
|
+
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
|
|
43
|
+
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
44
|
+
curl -LsSf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
|
|
45
|
+
apt-get update
|
|
46
|
+
apt-get install -y caddy
|
|
47
|
+
|
|
48
|
+
{% if domain %}
|
|
49
|
+
# Dynamically generate Caddyfile content
|
|
50
|
+
echo "🔒 Writing Caddyfile for {{ domain }}..." >> $LOG
|
|
51
|
+
cat << EOF > /etc/caddy/Caddyfile
|
|
52
|
+
{{ domain }}:80, {{ domain }}:443 {
|
|
53
|
+
reverse_proxy localhost:{{ port | default(8000) }}
|
|
54
|
+
tls {{ email }}
|
|
55
|
+
}
|
|
56
|
+
EOF
|
|
57
|
+
{% endif %}
|
|
58
|
+
|
|
59
|
+
{% if domain %}
|
|
60
|
+
echo "🚀 [5/5] Starting Caddy..." >> $LOG
|
|
61
|
+
systemctl restart caddy
|
|
62
|
+
{% else %}
|
|
63
|
+
echo "✅ [5/5] Skipping Caddy start (no domain specified)." >> $LOG
|
|
64
|
+
{% endif %}
|
|
65
|
+
|
|
66
|
+
# Finish
|
|
67
|
+
echo "✅ SETUP SCRIPT COMPLETE" >> $LOG
|
|
68
|
+
touch /root/setup_complete
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# docker-compose.yml template
|
|
2
|
-
version: '3.8'
|
|
3
|
-
|
|
4
|
-
services:
|
|
5
|
-
app:
|
|
6
|
-
build: .
|
|
7
|
-
ports:
|
|
8
|
-
- "{{ port | default(8000) }}:{{ port | default(8000) }}"
|
|
9
|
-
volumes:
|
|
10
|
-
- .:/app
|
|
11
|
-
command: {{ command }}
|
|
12
|
-
{% if database == 'postgres' %}
|
|
13
|
-
depends_on:
|
|
14
|
-
- db
|
|
15
|
-
environment:
|
|
16
|
-
- DATABASE_URL=postgresql://{{ db_user | default('user') }}:{{ db_password | default('password') }}@db:5432/{{ db_name | default('appdb') }}
|
|
17
|
-
{% endif %}
|
|
18
|
-
|
|
19
|
-
{% if database == 'postgres' %}
|
|
20
|
-
db:
|
|
21
|
-
image: postgres:15-alpine
|
|
22
|
-
volumes:
|
|
23
|
-
- postgres_data:/var/lib/postgresql/data/
|
|
24
|
-
environment:
|
|
25
|
-
- POSTGRES_USER={{ db_user | default('user') }}
|
|
26
|
-
- POSTGRES_PASSWORD={{ db_password | default('password') }}
|
|
27
|
-
- POSTGRES_DB={{ db_name | default('appdb') }}
|
|
28
|
-
{% endif %}
|
|
29
|
-
|
|
30
|
-
volumes:
|
|
31
|
-
{% if database == 'postgres' %}
|
|
32
|
-
postgres_data:
|
|
33
|
-
{% endif %}
|
|
1
|
+
# docker-compose.yml template
|
|
2
|
+
version: '3.8'
|
|
3
|
+
|
|
4
|
+
services:
|
|
5
|
+
app:
|
|
6
|
+
build: .
|
|
7
|
+
ports:
|
|
8
|
+
- "{{ port | default(8000) }}:{{ port | default(8000) }}"
|
|
9
|
+
volumes:
|
|
10
|
+
- .:/app
|
|
11
|
+
command: {{ command }}
|
|
12
|
+
{% if database == 'postgres' %}
|
|
13
|
+
depends_on:
|
|
14
|
+
- db
|
|
15
|
+
environment:
|
|
16
|
+
- DATABASE_URL=postgresql://{{ db_user | default('user') }}:{{ db_password | default('password') }}@db:5432/{{ db_name | default('appdb') }}
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
{% if database == 'postgres' %}
|
|
20
|
+
db:
|
|
21
|
+
image: postgres:15-alpine
|
|
22
|
+
volumes:
|
|
23
|
+
- postgres_data:/var/lib/postgresql/data/
|
|
24
|
+
environment:
|
|
25
|
+
- POSTGRES_USER={{ db_user | default('user') }}
|
|
26
|
+
- POSTGRES_PASSWORD={{ db_password | default('password') }}
|
|
27
|
+
- POSTGRES_DB={{ db_name | default('appdb') }}
|
|
28
|
+
{% endif %}
|
|
29
|
+
|
|
30
|
+
volumes:
|
|
31
|
+
{% if database == 'postgres' %}
|
|
32
|
+
postgres_data:
|
|
33
|
+
{% endif %}
|
xenfra_sdk/utils.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tomllib # Python 3.11+
|
|
3
|
-
from typing import Any, Dict
|
|
3
|
+
from typing import Any, Dict
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
@@ -92,9 +92,7 @@ def safe_json_parse(response: httpx.Response) -> Dict[str, Any]:
|
|
|
92
92
|
if "application/json" not in content_type:
|
|
93
93
|
# Try to get error text for better error messages
|
|
94
94
|
error_text = response.text[:500] if response.text else "Unknown error"
|
|
95
|
-
raise XenfraError(
|
|
96
|
-
f"Expected JSON response, got {content_type}. Response: {error_text}"
|
|
97
|
-
)
|
|
95
|
+
raise XenfraError(f"Expected JSON response, got {content_type}. Response: {error_text}")
|
|
98
96
|
|
|
99
97
|
try:
|
|
100
98
|
return response.json()
|