Automatizacion ATC en BTP: Monitorear la calidad del codigo de forma continua

Kategorie
BTP
Veröffentlicht
Autor
Johannes

Las verificaciones ATC automatizadas son indispensables para la calidad continua del codigo en SAP BTP ABAP Environment. Mientras en el dia a dia de desarrollo ejecutas ATC manualmente en ADT, la API ATC permite la integracion en pipelines CI/CD para Quality Gates automatizados.

Por que automatizar ATC?

La ejecucion manual de verificaciones ATC tiene limites:

AspectoManualAutomatizado
TimingAntes de transporteEn cada commit
ConsistenciaDepende del desarrolladorSiempre los mismos checks
OlvidoPosibleImposible
ReportingPuntualContinuo
Analisis de tendenciasNo posibleDatos historicos
Estandar de equipoDificil de imponerImpuesto automaticamente
+----------------------------------------------------------------+
| Pipeline CI/CD con ATC Quality Gate |
+----------------------------------------------------------------+
| |
| Git Push --> Build --> ABAP Unit --> ATC API --> Deploy |
| Tests Checks |
| | |
| v |
| +--------------+ |
| | Quality | |
| | Gate | |
| | ------------ | |
| | Errors: 0 | |
| | Warnings: <5 | |
| +--------------+ |
| |
+----------------------------------------------------------------+

Requisitos previos

Antes de poder usar la API ATC necesitas:

  1. SAP BTP ABAP Environment (instancia Steampunk)
  2. Communication Arrangement para servicios ADT (SAP_COM_0763)
  3. Communication User con permisos correspondientes
  4. Check Variant para la ejecucion automatizada

Configurar Communication Arrangement

El Communication Arrangement del ABAP Unit Runner aplica tambien para ATC:

+----------------------------------------------------------------+
| Communication Arrangement: SAP_COM_0763 |
+----------------------------------------------------------------+
| |
| Inbound Services (activados): |
| [x] ADT Core Services |
| [x] ADT ABAP Unit |
| [x] ADT ATC (ABAP Test Cockpit) |
| |
| Permisos requeridos: |
| * S_DEVELOP (Leer objetos de desarrollo) |
| * S_ATC_ADM (Administracion ATC) |
| |
+----------------------------------------------------------------+

API ATC en BTP

La API ATC se basa en el framework ADT REST y permite la ejecucion programatica de verificaciones ATC.

Endpoint API

POST https://<system-url>/sap/bc/adt/atc/runs

Formato del Request

<?xml version="1.0" encoding="UTF-8"?>
<atc:run xmlns:atc="http://www.sap.com/adt/atc"
xmlns:adtcore="http://www.sap.com/adt/core"
maximumVerdicts="100">
<objectSets>
<objectSet kind="inclusive">
<adtcore:objectReferences>
<adtcore:objectReference
adtcore:uri="/sap/bc/adt/vit/wb/object_type/devck/object_name/Z_FLIGHT_BOOKING"/>
</adtcore:objectReferences>
</objectSet>
</objectSets>
<checkVariant>
<atc:name>ABAP_CLOUD_READINESS</atc:name>
</checkVariant>
</atc:run>

Parametros importantes del Request

ParametroDescripcionEjemplo
maximumVerdictsMax. cantidad de findings100, 500, 1000
objectReferenceObjeto a verificar (paquete, clase)devck/Z_FLIGHT_BOOKING
checkVariantVariante de checkABAP_CLOUD_READINESS

Ejemplo cURL

Terminal window
# Iniciar verificacion ATC para un paquete
curl -X POST \
"https://my-system.abap.eu10.hana.ondemand.com/sap/bc/adt/atc/runs" \
-u "CICD_USER:password" \
-H "Content-Type: application/vnd.sap.atc.run.v1+xml" \
-H "Accept: application/vnd.sap.atc.run.v1+xml" \
-d @atc_config.xml \
-o atc_results.xml

Definir variantes de check propias

Las variantes de check determinan que verificaciones se ejecutan con que prioridad.

Variantes de check estandar en BTP

VarianteDescripcionRecomendado para
ABAP_CLOUD_READINESSVerifica compatibilidad CloudMigracion a ABAP Cloud
DEFAULTTodos los checks estandarCalidad general
SAP_CPSAP Cloud Platform ChecksDesarrollo BTP

Obtener variante de check via API

Terminal window
# Consultar variantes de check disponibles
curl -X GET \
"https://my-system.abap.eu10.hana.ondemand.com/sap/bc/adt/atc/customizing/variants" \
-u "CICD_USER:password" \
-H "Accept: application/vnd.sap.atc.customizing.v1+xml"

Crear variante de check propia

En SAP BTP ABAP Environment creas variantes de check a traves de la app Fiori Maintain ATC Check Variants:

+----------------------------------------------------------------+
| Check Variant: Z_CI_PIPELINE |
+----------------------------------------------------------------+
| |
| Descripcion: Check Variant para pipeline CI/CD |
| |
| +------------------------------------------------------------+ |
| | Check | Prioridad | Activo | |
| +--------------------------------+-----------+---------------+ |
| | SLIN_CHECK (Syntax Check) | 1 | [x] | |
| | SEC_CHECK (Security) | 1 | [x] | |
| | PERF_CHECK (Performance) | 2 | [x] | |
| | CONV_CHECK (Conventions) | 3 | [x] | |
| | CLOUD_CHECK (Cloud Readiness) | 1 | [x] | |
| | UNIT_TEST (Unit Tests) | 2 | [ ] | |
| +--------------------------------+-----------+---------------+ |
| |
| Configuracion de prioridades: |
| * Prioridad 1 (Error) -> Build falla |
| * Prioridad 2 (Warning) -> Build con advertencia |
| * Prioridad 3 (Info) -> Solo protocolado |
| |
+----------------------------------------------------------------+

Categorias de check en detalle

" Las categorias de check mas importantes para ABAP Cloud:
" 1. Cloud Readiness Checks
" - Uso de APIs no liberadas
" - Accesos directos a DB (SELECT en tablas estandar)
" - Uso de sentencias Dynpro
" 2. Security Checks
" - Riesgos de SQL Injection
" - Cross-Site Scripting (XSS)
" - Authority Checks faltantes
" - Credenciales hardcodeadas
" 3. Performance Checks
" - Evitar SELECT *
" - SELECT en bucles
" - Clausulas WHERE faltantes
" - LOOP AT sin alternativa READ TABLE
" 4. Naming Convention Checks
" - Prefijo Z/Y para objetos custom
" - Directrices de nombres para variables
" - Asignacion a paquete

Configurar ejecucion automatica

Proceso ATC en dos pasos

La API ATC trabaja de forma asincrona - inicias un run y consultas los resultados:

+-----------------------------------------------------------------+
| Workflow API ATC |
+-----------------------------------------------------------------+
| |
| 1. POST /sap/bc/adt/atc/runs |
| +-- Request: Configuracion de check |
| +-- Response: Run-ID (Header Location) |
| |
| 2. Polling: GET /sap/bc/adt/atc/runs/{run-id} |
| +-- Status: running -> esperar |
| +-- Status: finished -> obtener resultados |
| |
| 3. GET /sap/bc/adt/atc/runs/{run-id}/results |
| +-- Response: Findings como XML |
| |
+-----------------------------------------------------------------+

Script Bash completo para ejecucion ATC

#!/bin/bash
# run_atc.sh - Ejecutar verificacion ATC en BTP
set -e
# Configuracion
SYSTEM_URL="${ABAP_ENDPOINT}"
USER="${ABAP_USER}"
PASSWORD="${ABAP_PASSWORD}"
PACKAGE="${1:-Z_FLIGHT_BOOKING}"
CHECK_VARIANT="${2:-ABAP_CLOUD_READINESS}"
MAX_VERDICTS="${3:-500}"
echo "=== Starting ATC Check ==="
echo "Package: $PACKAGE"
echo "Variant: $CHECK_VARIANT"
# 1. Iniciar ATC Run
cat > atc_request.xml << EOF
<?xml version="1.0" encoding="UTF-8"?>
<atc:run xmlns:atc="http://www.sap.com/adt/atc"
xmlns:adtcore="http://www.sap.com/adt/core"
maximumVerdicts="$MAX_VERDICTS">
<objectSets>
<objectSet kind="inclusive">
<adtcore:objectReferences>
<adtcore:objectReference
adtcore:uri="/sap/bc/adt/vit/wb/object_type/devck/object_name/$PACKAGE"/>
</adtcore:objectReferences>
</objectSet>
</objectSets>
<checkVariant>
<atc:name>$CHECK_VARIANT</atc:name>
</checkVariant>
</atc:run>
EOF
# Iniciar run y extraer header Location
RESPONSE=$(curl -s -D - \
-X POST \
"$SYSTEM_URL/sap/bc/adt/atc/runs" \
-u "$USER:$PASSWORD" \
-H "Content-Type: application/vnd.sap.atc.run.v1+xml" \
-H "Accept: application/vnd.sap.atc.run.v1+xml" \
-d @atc_request.xml)
RUN_ID=$(echo "$RESPONSE" | grep -i "location:" | sed 's/.*runs\///' | tr -d '\r\n')
if [ -z "$RUN_ID" ]; then
echo "::error::Failed to start ATC run"
echo "$RESPONSE"
exit 1
fi
echo "ATC Run started: $RUN_ID"
# 2. Esperar finalizacion (Polling)
MAX_ATTEMPTS=60
ATTEMPT=0
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
STATUS_RESPONSE=$(curl -s \
-X GET \
"$SYSTEM_URL/sap/bc/adt/atc/runs/$RUN_ID" \
-u "$USER:$PASSWORD" \
-H "Accept: application/vnd.sap.atc.run.v1+xml")
STATUS=$(echo "$STATUS_RESPONSE" | grep -oP 'status="\K[^"]+' || echo "unknown")
echo "Status: $STATUS (Attempt $((ATTEMPT+1))/$MAX_ATTEMPTS)"
if [ "$STATUS" = "finished" ]; then
break
fi
sleep 5
ATTEMPT=$((ATTEMPT+1))
done
if [ "$STATUS" != "finished" ]; then
echo "::error::ATC run timed out"
exit 1
fi
# 3. Obtener resultados
curl -s \
-X GET \
"$SYSTEM_URL/sap/bc/adt/atc/runs/$RUN_ID/results" \
-u "$USER:$PASSWORD" \
-H "Accept: application/vnd.sap.atc.worklist.v1+xml" \
-o atc_results.xml
echo "=== ATC Results saved to atc_results.xml ==="

Evaluar resultados y reportar

Formato del Response ATC

<?xml version="1.0" encoding="UTF-8"?>
<atc:worklist xmlns:atc="http://www.sap.com/adt/atc"
xmlns:adtcore="http://www.sap.com/adt/core"
timestamp="2026-02-15T10:30:00Z"
usedCheckVariant="Z_CI_PIPELINE">
<objects>
<object adtcore:uri="/sap/bc/adt/oo/classes/zcl_flight_booking"
adtcore:name="ZCL_FLIGHT_BOOKING"
adtcore:type="CLAS">
<findings>
<finding checkId="CL_CI_CRITICAL_API_USE"
checkTitle="Non-released API usage"
messageId="001"
messageTitle="Usage of non-released API detected"
priority="1"
location="/sap/bc/adt/oo/classes/zcl_flight_booking/source/main#start=45,10">
<atc:quickfixes count="1"/>
</finding>
<finding checkId="CL_CI_SEC_INJECTION"
checkTitle="SQL Injection Risk"
messageId="002"
messageTitle="Potential SQL injection vulnerability"
priority="1"
location="/sap/bc/adt/oo/classes/zcl_flight_booking/source/main#start=78,5">
<atc:quickfixes count="0"/>
</finding>
<finding checkId="CL_CI_CONV_NAMING"
checkTitle="Naming Convention"
messageId="003"
messageTitle="Variable naming does not follow convention"
priority="3"
location="/sap/bc/adt/oo/classes/zcl_flight_booking/source/main#start=12,8">
<atc:quickfixes count="1"/>
</finding>
</findings>
</object>
</objects>
<statistics>
<totalFindings>3</totalFindings>
<findingsByPriority>
<priority1>2</priority1>
<priority2>0</priority2>
<priority3>1</priority3>
</findingsByPriority>
</statistics>
</atc:worklist>

Script Python para parsing de resultados

parse_atc_results.py
import xml.etree.ElementTree as ET
import sys
import json
from dataclasses import dataclass, asdict
from typing import List
@dataclass
class Finding:
check_id: str
check_title: str
message: str
priority: int
object_name: str
location: str
has_quickfix: bool
def parse_atc_results(xml_file: str) -> dict:
tree = ET.parse(xml_file)
root = tree.getroot()
ns = {
'atc': 'http://www.sap.com/adt/atc',
'adtcore': 'http://www.sap.com/adt/core'
}
findings: List[Finding] = []
priority_counts = {1: 0, 2: 0, 3: 0}
for obj in root.findall('.//atc:object', ns):
obj_name = obj.get('{http://www.sap.com/adt/core}name', 'Unknown')
for finding in obj.findall('.//atc:finding', ns):
priority = int(finding.get('priority', 3))
priority_counts[priority] = priority_counts.get(priority, 0) + 1
quickfixes = finding.find('atc:quickfixes', ns)
has_quickfix = quickfixes is not None and int(quickfixes.get('count', 0)) > 0
findings.append(Finding(
check_id=finding.get('checkId', ''),
check_title=finding.get('checkTitle', ''),
message=finding.get('messageTitle', ''),
priority=priority,
object_name=obj_name,
location=finding.get('location', ''),
has_quickfix=has_quickfix
))
return {
'total': len(findings),
'errors': priority_counts.get(1, 0),
'warnings': priority_counts.get(2, 0),
'info': priority_counts.get(3, 0),
'findings': [asdict(f) for f in findings]
}
def print_report(results: dict, error_threshold: int = 0, warning_threshold: int = 5):
print("=" * 60)
print("ATC QUALITY REPORT")
print("=" * 60)
print(f"Total Findings: {results['total']}")
print(f" Errors (P1): {results['errors']}")
print(f" Warnings (P2): {results['warnings']}")
print(f" Info (P3): {results['info']}")
print("=" * 60)
# Findings agrupados por prioridad
for priority in [1, 2, 3]:
priority_findings = [f for f in results['findings'] if f['priority'] == priority]
if priority_findings:
print(f"\nPriority {priority} Findings:")
print("-" * 40)
for f in priority_findings:
print(f" [{f['check_id']}] {f['object_name']}")
print(f" {f['message']}")
quickfix = "[OK] Quickfix disponible" if f['has_quickfix'] else ""
print(f" {f['location']} {quickfix}")
print("\n" + "=" * 60)
# Verificar thresholds
exit_code = 0
if results['errors'] > error_threshold:
print(f"::error::Found {results['errors']} error(s) (threshold: {error_threshold})")
exit_code = 1
if results['warnings'] > warning_threshold:
print(f"::warning::Found {results['warnings']} warning(s) (threshold: {warning_threshold})")
return exit_code
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python parse_atc_results.py <xml_file> [--json]")
sys.exit(1)
results = parse_atc_results(sys.argv[1])
if '--json' in sys.argv:
print(json.dumps(results, indent=2))
else:
exit_code = print_report(results)
sys.exit(exit_code)

Integracion con GitHub/GitLab

Workflow GitHub Actions

.github/workflows/atc-quality-gate.yml
name: ATC Quality Gate
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
PACKAGE: Z_FLIGHT_BOOKING
CHECK_VARIANT: ABAP_CLOUD_READINESS
ERROR_THRESHOLD: 0
WARNING_THRESHOLD: 5
jobs:
atc-check:
name: ATC Quality Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Create ATC Configuration
run: |
cat > atc_config.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<atc:run xmlns:atc="http://www.sap.com/adt/atc"
xmlns:adtcore="http://www.sap.com/adt/core"
maximumVerdicts="500">
<objectSets>
<objectSet kind="inclusive">
<adtcore:objectReferences>
<adtcore:objectReference
adtcore:uri="/sap/bc/adt/vit/wb/object_type/devck/object_name/${{ env.PACKAGE }}"/>
</adtcore:objectReferences>
</objectSet>
</objectSets>
<checkVariant>
<atc:name>${{ env.CHECK_VARIANT }}</atc:name>
</checkVariant>
</atc:run>
EOF
- name: Start ATC Run
id: start-atc
run: |
RESPONSE=$(curl -s -D - \
-X POST \
"${{ secrets.ABAP_ENDPOINT }}/sap/bc/adt/atc/runs" \
-u "${{ secrets.ABAP_USER }}:${{ secrets.ABAP_PASSWORD }}" \
-H "Content-Type: application/vnd.sap.atc.run.v1+xml" \
-H "Accept: application/vnd.sap.atc.run.v1+xml" \
-d @atc_config.xml)
RUN_ID=$(echo "$RESPONSE" | grep -i "location:" | sed 's/.*runs\///' | tr -d '\r\n')
if [ -z "$RUN_ID" ]; then
echo "::error::Failed to start ATC run"
exit 1
fi
echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT
echo "Started ATC run: $RUN_ID"
- name: Wait for ATC Completion
run: |
RUN_ID="${{ steps.start-atc.outputs.run_id }}"
MAX_ATTEMPTS=60
for i in $(seq 1 $MAX_ATTEMPTS); do
STATUS=$(curl -s \
-X GET \
"${{ secrets.ABAP_ENDPOINT }}/sap/bc/adt/atc/runs/$RUN_ID" \
-u "${{ secrets.ABAP_USER }}:${{ secrets.ABAP_PASSWORD }}" \
-H "Accept: application/vnd.sap.atc.run.v1+xml" \
| grep -oP 'status="\K[^"]+' || echo "unknown")
echo "Status: $STATUS (Attempt $i/$MAX_ATTEMPTS)"
if [ "$STATUS" = "finished" ]; then
break
fi
sleep 5
done
if [ "$STATUS" != "finished" ]; then
echo "::error::ATC run timed out"
exit 1
fi
- name: Fetch ATC Results
run: |
RUN_ID="${{ steps.start-atc.outputs.run_id }}"
curl -s \
-X GET \
"${{ secrets.ABAP_ENDPOINT }}/sap/bc/adt/atc/runs/$RUN_ID/results" \
-u "${{ secrets.ABAP_USER }}:${{ secrets.ABAP_PASSWORD }}" \
-H "Accept: application/vnd.sap.atc.worklist.v1+xml" \
-o atc_results.xml
- name: Parse and Evaluate Results
id: parse
run: |
python << 'PYTHON_SCRIPT'
import xml.etree.ElementTree as ET
import os
tree = ET.parse('atc_results.xml')
root = tree.getroot()
ns = {'atc': 'http://www.sap.com/adt/atc', 'adtcore': 'http://www.sap.com/adt/core'}
errors = warnings = info = 0
for finding in root.findall('.//atc:finding', ns):
priority = int(finding.get('priority', 3))
if priority == 1:
errors += 1
elif priority == 2:
warnings += 1
else:
info += 1
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f"errors={errors}\n")
f.write(f"warnings={warnings}\n")
f.write(f"info={info}\n")
f.write(f"total={errors + warnings + info}\n")
print(f"Errors: {errors}, Warnings: {warnings}, Info: {info}")
# Verificar Quality Gate
if errors > int(os.environ.get('ERROR_THRESHOLD', 0)):
print(f"::error::Quality Gate failed: {errors} error(s) found")
exit(1)
PYTHON_SCRIPT
- name: Upload ATC Results
uses: actions/upload-artifact@v4
with:
name: atc-results
path: atc_results.xml
- name: Create Summary
if: always()
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
## ATC Quality Gate Results
| Metric | Value | Threshold |
|--------|-------|-----------|
| Errors (P1) | ${{ steps.parse.outputs.errors }} | ${{ env.ERROR_THRESHOLD }} |
| Warnings (P2) | ${{ steps.parse.outputs.warnings }} | ${{ env.WARNING_THRESHOLD }} |
| Info (P3) | ${{ steps.parse.outputs.info }} | - |
| **Total** | ${{ steps.parse.outputs.total }} | |
**Status:** ${{ steps.parse.outputs.errors == 0 && '[OK] Passed' || '[X] Failed' }}
EOF

Pipeline GitLab CI/CD

.gitlab-ci.yml
stages:
- quality
variables:
PACKAGE: Z_FLIGHT_BOOKING
CHECK_VARIANT: ABAP_CLOUD_READINESS
ERROR_THRESHOLD: "0"
atc-quality-gate:
stage: quality
image: python:3.11-slim
before_script:
- apt-get update && apt-get install -y curl
script:
# Iniciar ATC Run
- |
RESPONSE=$(curl -s -D - \
-X POST \
"${ABAP_ENDPOINT}/sap/bc/adt/atc/runs" \
-u "${ABAP_USER}:${ABAP_PASSWORD}" \
-H "Content-Type: application/vnd.sap.atc.run.v1+xml" \
-H "Accept: application/vnd.sap.atc.run.v1+xml" \
-d "<atc:run xmlns:atc=\"http://www.sap.com/adt/atc\"
xmlns:adtcore=\"http://www.sap.com/adt/core\"
maximumVerdicts=\"500\">
<objectSets>
<objectSet kind=\"inclusive\">
<adtcore:objectReferences>
<adtcore:objectReference
adtcore:uri=\"/sap/bc/adt/vit/wb/object_type/devck/object_name/${PACKAGE}\"/>
</adtcore:objectReferences>
</objectSet>
</objectSets>
<checkVariant>
<atc:name>${CHECK_VARIANT}</atc:name>
</checkVariant>
</atc:run>")
RUN_ID=$(echo "$RESPONSE" | grep -i "location:" | sed 's/.*runs\///' | tr -d '\r\n')
echo "ATC Run ID: $RUN_ID"
# Esperar finalizacion
- |
for i in $(seq 1 60); do
STATUS=$(curl -s \
"${ABAP_ENDPOINT}/sap/bc/adt/atc/runs/${RUN_ID}" \
-u "${ABAP_USER}:${ABAP_PASSWORD}" \
-H "Accept: application/vnd.sap.atc.run.v1+xml" \
| grep -oP 'status="\K[^"]+' || echo "unknown")
echo "Attempt $i: Status = $STATUS"
[ "$STATUS" = "finished" ] && break
sleep 5
done
# Obtener resultados
- |
curl -s \
"${ABAP_ENDPOINT}/sap/bc/adt/atc/runs/${RUN_ID}/results" \
-u "${ABAP_USER}:${ABAP_PASSWORD}" \
-H "Accept: application/vnd.sap.atc.worklist.v1+xml" \
-o atc_results.xml
# Verificar resultados
- |
python3 << 'EOF'
import xml.etree.ElementTree as ET
import sys
import os
tree = ET.parse('atc_results.xml')
root = tree.getroot()
ns = {'atc': 'http://www.sap.com/adt/atc'}
errors = sum(1 for f in root.findall('.//atc:finding', ns) if f.get('priority') == '1')
print(f"Found {errors} error(s)")
if errors > int(os.environ.get('ERROR_THRESHOLD', 0)):
sys.exit(1)
EOF
artifacts:
paths:
- atc_results.xml
reports:
junit: atc_results.xml
when: always
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"

Mejores practicas

AspectoRecomendacion
Check VariantVariante propia para CI/CD con prioridades claras
Error-Threshold0 para Priority 1 (no se permiten errores)
Warning-ThresholdEstablecer de forma realista, reducir gradualmente
TimeoutMax. 5 minutos para paquetes grandes
QuickfixesMostrar en reporte, no aplicar automaticamente
Trend-TrackingProtocolar findings a lo largo del tiempo
Estandares de equipoAcordar variante de check en el equipo
Manejo de excepcionesDocumentar excepciones justificadas

Troubleshooting

ProblemaPosible causaSolucion
401 UnauthorizedCredenciales incorrectasVerificar user/password
403 ForbiddenPermiso ATC faltanteVerificar S_ATC_ADM
404 Not FoundPaquete/variante no existeVerificar nombres
TimeoutPaquete muy grandeDividir en paquetes mas pequenios
Sin FindingsCheck variant vaciaActivar checks en variante
Run permanece “running”Sistema sobrecargadoReintentar mas tarde

Recursos adicionales