Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions opt/torero-ui/torero_ui/dashboard/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@

from .models import ServiceExecution, ServiceInfo

logger = logging.getLogger(__name__)
# logger with fallback for initialization issues
def get_logger():
try:
return logging.getLogger(__name__)
except:

# fallback to basic logging if django logging not initialized
logging.basicConfig(level=logging.INFO)
return logging.getLogger(__name__)

logger = get_logger()


class ToreroCliClient:
Expand All @@ -21,6 +31,15 @@ class ToreroCliClient:
def __init__(self) -> None:
self.torero_command = "torero"
self.timeout = getattr(settings, 'TORERO_CLI_TIMEOUT', 30)

# check if torero is available
try:
result = subprocess.run([self.torero_command, "version"],
capture_output=True, text=True, timeout=5, check=False)
if result.returncode != 0:
logger.warning(f"torero command may not be available: {result.stderr}")
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
logger.error(f"torero command not found or not responding: {e}")

def _execute_command(self, args: List[str]) -> Optional[Dict[str, Any]]:
"""execute torero cli command and return parsed json output."""
Expand All @@ -39,9 +58,13 @@ def _execute_command(self, args: List[str]) -> Optional[Dict[str, Any]]:
return json.loads(result.stdout)
except json.JSONDecodeError:
logger.warning(f"failed to parse json from torero output: {result.stdout[:200]}")
logger.warning(f"command was: {' '.join(command)}")
return None
else:
logger.error(f"torero command failed: {' '.join(command)}\nError: {result.stderr}")
logger.error(f"torero command failed: {' '.join(command)}")
logger.error(f"return code: {result.returncode}")
logger.error(f"stdout: {result.stdout[:500] if result.stdout else 'None'}")
logger.error(f"stderr: {result.stderr[:500] if result.stderr else 'None'}")
return None

except subprocess.TimeoutExpired:
Expand All @@ -53,24 +76,33 @@ def _execute_command(self, args: List[str]) -> Optional[Dict[str, Any]]:

def get_services(self) -> List[Dict[str, Any]]:
"""get list of all services from torero cli."""

data = self._execute_command(["get", "services"])
if data and isinstance(data, dict) and "services" in data:
return data["services"] if isinstance(data["services"], list) else []
return []

def get_service_details(self, service_name: str) -> Optional[Dict[str, Any]]:

"""get details for specific service from torero cli."""
return self._execute_command(["describe", "service", service_name])

def execute_service(self, service_name: str, service_type: str, **params: Any) -> Optional[Dict[str, Any]]:
"""execute a service via torero cli."""

# extract operation parameter for opentofu
operation = params.pop('operation', None)

# build command based on service type
if service_type == "ansible-playbook":
command = ["run", "service", "ansible-playbook", service_name]
elif service_type == "python-script":
command = ["run", "service", "python-script", service_name]
elif service_type == "opentofu-plan":
command = ["run", "service", "opentofu-plan", service_name]

# use provided operation or default to apply
op = operation if operation in ['apply', 'destroy'] else 'apply'
command = ["run", "service", "opentofu-plan", op, service_name]
else:
logger.error(f"unsupported service type: {service_type}")
return None
Expand All @@ -79,7 +111,25 @@ def execute_service(self, service_name: str, service_type: str, **params: Any) -
for key, value in params.items():
command.extend([f"--{key}", str(value)])

return self._execute_command(command)
logger.info(f"executing torero command: {' '.join(command)}")

# quick connectivity check before executing
try:
version_check = subprocess.run([self.torero_command, "version"],
capture_output=True, text=True, timeout=5, check=False)
if version_check.returncode != 0:
logger.error(f"torero connectivity check failed before execution: {version_check.stderr}")
return None
except Exception as e:
logger.error(f"torero connectivity check failed: {e}")
return None

result = self._execute_command(command)

if result is None:
logger.error(f"execution failed for service {service_name} (type: {service_type})")

return result


class DataCollectionService:
Expand Down
1 change: 1 addition & 0 deletions opt/torero-ui/torero_ui/dashboard/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
path("api/sync/", views.api_sync_services, name="api_sync"),
path("api/execution/<int:execution_id>/", views.api_execution_details, name="api_execution_details"),
path("api/record-execution/", views.api_record_execution, name="api_record_execution"),
path("api/execute/<str:service_name>/", views.api_execute_service, name="api_execute_service"),
]
77 changes: 75 additions & 2 deletions opt/torero-ui/torero_ui/dashboard/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""views for torero dashboard."""

import json
import logging
from typing import Any, Dict

from django.conf import settings
Expand All @@ -11,7 +12,19 @@
from django.views.generic import TemplateView

from .models import ServiceExecution, ServiceInfo
from .services import DataCollectionService
from .services import DataCollectionService, ToreroCliClient

# logger with fallback for initialization issues
def get_logger():
try:
return logging.getLogger(__name__)
except:

# fallback to basic logging if django logging not initialized
logging.basicConfig(level=logging.INFO)
return logging.getLogger(__name__)

logger = get_logger()


class DashboardView(TemplateView):
Expand Down Expand Up @@ -109,6 +122,7 @@ def api_dashboard_data(request):
@require_http_methods(["POST"])
def api_sync_services(request):
"""api endpoint to trigger service sync."""

try:
data_service = DataCollectionService()
data_service.sync_services()
Expand All @@ -120,6 +134,7 @@ def api_sync_services(request):
@require_http_methods(["GET"])
def api_execution_details(request, execution_id):
"""api endpoint for execution details."""

try:
execution = ServiceExecution.objects.get(id=execution_id)

Expand Down Expand Up @@ -147,6 +162,7 @@ def api_execution_details(request, execution_id):
@require_http_methods(["POST"])
def api_record_execution(request):
"""API endpoint to record execution data from torero-api."""

try:
data = json.loads(request.body)
service_name = data.get('service_name')
Expand Down Expand Up @@ -179,4 +195,61 @@ def api_record_execution(request):
})

except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
return JsonResponse({'error': str(e)}, status=500)


@require_http_methods(["POST"])
def api_execute_service(request, service_name):
"""execute service via torero cli."""

try:
# get service info
service_info = ServiceInfo.objects.get(name=service_name)

# parse request body for additional parameters
operation = None
if request.body:
try:
data = json.loads(request.body)
operation = data.get('operation')
except json.JSONDecodeError:
pass

# execute via cli client
cli_client = ToreroCliClient()
result = cli_client.execute_service(
service_name=service_name,
service_type=service_info.service_type,
operation=operation
)

if result:
return JsonResponse({
'status': 'started',
'message': f'execution started for {service_name}'
})
else:
# provide more detailed error message
error_msg = f'failed to start execution for {service_name} (type: {service_info.service_type})'
logger.error(error_msg)
logger.error(f'cli client returned None for service: {service_name}')

# check if it's a common issue
if not hasattr(settings, 'TORERO_CLI_TIMEOUT'):
logger.warning('TORERO_CLI_TIMEOUT not configured, using default')

return JsonResponse({
'status': 'error',
'message': f'{error_msg} - check server logs for details'
}, status=500)

except ServiceInfo.DoesNotExist:
return JsonResponse({
'status': 'error',
'message': 'service not found'
}, status=404)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': str(e)
}, status=500)
Loading