Skip to content

Commit 57afb5e

Browse files
authored
Merge pull request #195 from 2-Coatl/feature/fix-workspace
Implementación de registro de usuarios y refactorización de CPython installer
2 parents 0744412 + ba71ac8 commit 57afb5e

16 files changed

Lines changed: 808 additions & 6 deletions
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Install prebuilt CPython fallback script
2+
3+
This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. Maintain this plan according to `.agent/PLANS.md`.
4+
5+
## Purpose / Big Picture
6+
7+
Provide a reproducible fallback script that downloads a prebuilt CPython artifact for Ubuntu 20.04 builders. After implementation, operators can run `./install.sh` (symlinked to the new script) to fetch and install the `cpython-3.12.6-ubuntu20.04-build1.tgz` artifact into `/opt/python-3.12.6`, verifying installation by printing the Python version. This offers an alternative when compiling from source is blocked by missing compilers or expired repositories.
8+
9+
## Progress
10+
11+
- [x] (2024-09-27 12:30Z) Add regression tests describing the expected install script behavior (shebang, wget usage, extraction, verification command); confirmed they fail because the script is missing.
12+
- [x] (2024-09-27 12:45Z) Implement `scripts/install_prebuilt_cpython.sh` to satisfy the new tests with configurable artifact/version variables; targeted pytest suite now passes.
13+
- [x] (2024-09-27 12:55Z) Confirmed bootstrap messaging already highlights the symlink; documented fallback via automated tests and noted unrelated suite failures during full pytest run.
14+
- [x] (2025-11-14 06:12Z) Refactored the install script to source shared logging utilities and adjusted tests to enforce the DRY expectation raised during review feedback.
15+
- [x] (2025-11-14 07:25Z) Added a top-level `scripts/build_cpython.sh` entrypoint that delegates to the infrastructure build script so the Vagrant guidance matches the repository layout; captured coverage with a new pytest module.
16+
17+
## Surprises & Discoveries
18+
19+
- Observation: Full `pytest infrastructure/cpython/tests` currently fails in this repository due to pre-existing missing infrastructure scripts and modules (e.g., validate_build.sh, recovery_plan_parser).
20+
Evidence: pytest output on 2024-09-27 showing 15 failures unrelated to the new install script.
21+
22+
## Decision Log
23+
24+
- Decision: Allow overriding artifact metadata via environment variables instead of CLI flags to keep script usage simple while remaining configurable.
25+
Rationale: Bootstrap and user instructions expect a straightforward command; environment overrides avoid breaking existing symlink workflows yet permit custom builds.
26+
Date/Author: 2024-09-27 / ChatGPT
27+
- Decision: Reuse the repository's shared logging helpers instead of redefining ad-hoc `log_info`/`log_error` functions inside the installer.
28+
Rationale: Aligns with DRY expectations from reviewers and ensures consistent log formatting across infrastructure scripts.
29+
Date/Author: 2025-11-14 / ChatGPT
30+
- Decision: Provide a thin executable wrapper in `scripts/` that defers to `infrastructure/cpython/scripts/build_cpython.sh` instead of duplicating the build logic.
31+
Rationale: Keeps the single source of truth for build operations while satisfying the provisioning instructions that reference `./scripts/build_cpython.sh`.
32+
Date/Author: 2025-11-14 / ChatGPT
33+
34+
## Outcomes & Retrospective
35+
36+
The install script is now covered by regression tests and passes targeted pytest checks. Running the entire infrastructure suite still fails because this workspace lacks several legacy scripts and parser modules referenced by other tests; these gaps predate the new fallback and remain out of scope for this change.
37+
38+
## Context and Orientation
39+
40+
The provisioning flow resides in `infrastructure/cpython/bootstrap.sh`, which already attempts to symlink `install.sh` to `scripts/install_prebuilt_cpython.sh` if the script exists. Currently, that script is absent, so the symlink step issues a warning. Tests for the bootstrap live in `infrastructure/cpython/tests/`. We will add tests ensuring the presence and key steps of the install script, mirroring the user-provided manual commands. The script should:
41+
42+
1. Download the artifact with `wget` into `/tmp/cpython.tgz` (or a configurable temp location).
43+
2. Extract into `/opt/python-<version>` with `sudo tar -xzf ... --strip-components=1`.
44+
3. Print the installed Python version using the extracted interpreter.
45+
46+
We should allow overriding the artifact URL, install prefix, and version/build via environment variables so the script remains maintainable.
47+
48+
## Plan of Work
49+
50+
1. Create a new test module `infrastructure/cpython/tests/test_install_prebuilt_script.py`. The tests will assert that `scripts/install_prebuilt_cpython.sh` exists, is executable, contains a shebang, and references the commands `wget`, `tar -xzf`, and `bin/python3 --version`. Another test will verify that default configuration variables in the script match the documented artifact (version `3.12.6`, build `build1`, Ubuntu `20.04`).
51+
2. Run `pytest infrastructure/cpython/tests/test_install_prebuilt_script.py` and observe failures because the script does not yet exist (Red phase).
52+
3. Implement `scripts/install_prebuilt_cpython.sh` as a Bash script using `set -euo pipefail`, declaring configurable defaults (artifact URL, version, build number, install prefix, temp file). The script should download with retry logic, extract, adjust permissions, and finally execute `/opt/python-<version>/bin/python3 --version`. Ensure the script is executable (`chmod +x`).
53+
4. Optionally update bootstrap tests or script to mention the fallback only if necessary for coverage (e.g., ensure symlink step logs success once script exists). No major bootstrap changes required beyond ensuring the script path resolves.
54+
5. Re-run the targeted pytest module to confirm tests pass (Green). Follow with the full `pytest infrastructure/cpython/tests` suite to ensure no regressions.
55+
6. Refactor if needed for clarity (e.g., shared utility functions in the script), keeping tests green.
56+
57+
## Concrete Steps
58+
59+
1. Working directory: `/workspace/IACT---project`. Command: `pytest infrastructure/cpython/tests/test_install_prebuilt_script.py` (expected failure: file missing).
60+
2. Create `scripts/install_prebuilt_cpython.sh` with executable permissions. Re-run the same pytest command (expected pass).
61+
3. Execute `pytest infrastructure/cpython/tests` to cover the broader suite.
62+
63+
## Validation and Acceptance
64+
65+
Acceptance criteria: Running `pytest infrastructure/cpython/tests` succeeds, and manually invoking `./scripts/install_prebuilt_cpython.sh --dry-run` (if implemented) or `./install.sh` after provisioning downloads and installs the artifact, outputting `Python 3.12.6`. For repository validation, ensure tests assert the presence of commands matching the manual instructions. Documentation or log messages should reflect the new capability.
66+
67+
## Idempotence and Recovery
68+
69+
The script should be safe to re-run: it can skip download if the artifact exists locally (optional) or overwrite by default while backing up existing installations. Tests will not execute the script but only assert its content, maintaining idempotence in the repository state. If the download fails, the script exits non-zero, leaving partial directories removable via `sudo rm -rf /opt/python-<version>`.
70+
71+
## Artifacts and Notes
72+
73+
- Pytest (targeted):
74+
$ pytest infrastructure/cpython/tests/test_install_prebuilt_script.py
75+
... 3 passed in 0.03s
76+
- Pytest (full suite - expected pre-existing failures):
77+
$ pytest infrastructure/cpython/tests
78+
... 15 failed, 47 passed (missing legacy scripts/modules)
79+
80+
## Interfaces and Dependencies
81+
82+
- Shell environment with `wget`, `tar`, and `sudo` available inside the builder VM.
83+
- Artifact naming convention `cpython-<version>-ubuntu20.04-<build>.tgz`. The script should construct URLs accordingly and expose override variables (`ARTIFACT_URL`, `PYTHON_VERSION`, `BUILD_ID`, `INSTALL_PREFIX`).

api/callcentersite/callcentersite/apps/users/serializers_usuarios.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,80 @@ class SuspenderUsuarioSerializer(serializers.Serializer):
217217
max_length=500,
218218
help_text='Motivo de la suspension',
219219
)
220+
221+
222+
class UserRegistrationSerializer(serializers.ModelSerializer):
223+
"""
224+
Serializer para registro publico de usuarios.
225+
226+
Campos:
227+
- username: str (requerido, unico)
228+
- email: str (requerido, unico, formato email)
229+
- password: str (requerido, minimo 8 caracteres)
230+
- password_confirm: str (requerido, debe coincidir con password)
231+
"""
232+
233+
password = serializers.CharField(
234+
write_only=True,
235+
required=True,
236+
min_length=8,
237+
style={'input_type': 'password'},
238+
help_text='Password del usuario (minimo 8 caracteres)',
239+
)
240+
password_confirm = serializers.CharField(
241+
write_only=True,
242+
required=True,
243+
style={'input_type': 'password'},
244+
help_text='Confirmacion del password',
245+
)
246+
247+
class Meta:
248+
model = User
249+
fields = ['username', 'email', 'password', 'password_confirm']
250+
extra_kwargs = {
251+
'username': {
252+
'required': True,
253+
'allow_blank': False,
254+
},
255+
'email': {
256+
'required': True,
257+
'allow_blank': False,
258+
},
259+
}
260+
261+
def validate_username(self, value: str) -> str:
262+
"""Valida que el username sea unico."""
263+
if User.objects.filter(username=value).exists():
264+
raise serializers.ValidationError(
265+
f'Ya existe un usuario con el username: {value}'
266+
)
267+
return value
268+
269+
def validate_email(self, value: str) -> str:
270+
"""Valida que el email sea unico."""
271+
if User.objects.filter(email=value).exists():
272+
raise serializers.ValidationError(
273+
f'Ya existe un usuario con el email: {value}'
274+
)
275+
return value
276+
277+
def validate(self, data: Dict[str, Any]) -> Dict[str, Any]:
278+
"""Valida que los passwords coincidan."""
279+
if data['password'] != data['password_confirm']:
280+
raise serializers.ValidationError({
281+
'password_confirm': 'Los passwords no coinciden'
282+
})
283+
return data
284+
285+
def create(self, validated_data: Dict[str, Any]) -> User:
286+
"""Crea usuario nuevo con password hasheado."""
287+
# Remove password_confirm as it's not needed for creation
288+
validated_data.pop('password_confirm')
289+
290+
# Create user with hashed password
291+
user = User.objects.create_user(
292+
username=validated_data['username'],
293+
email=validated_data['email'],
294+
password=validated_data['password'],
295+
)
296+
return user

api/callcentersite/callcentersite/apps/users/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
from django.urls import include, path
44
from rest_framework.routers import DefaultRouter
55

6-
from .views_usuarios import UserViewSet
6+
from .views_usuarios import UserRegistrationView, UserViewSet
77

88
router = DefaultRouter()
99
router.register(r'usuarios', UserViewSet, basename='usuario')
1010

1111
urlpatterns = [
1212
path('', include(router.urls)),
13+
# Registro publico de usuarios
14+
path('register/', UserRegistrationView.as_view(), name='user-register'),
1315
# Sistema de permisos granular
1416
path('permisos/', include('callcentersite.apps.users.urls_permisos')),
1517
]

api/callcentersite/callcentersite/apps/users/views_usuarios.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- POST /api/usuarios/:id/suspender/ (action)
1212
- POST /api/usuarios/:id/reactivar/ (action)
1313
- POST /api/usuarios/:id/asignar_grupos/ (action)
14+
- POST /api/register/ (public registration)
1415
1516
Referencia: docs/PLAN_MAESTRO_PRIORIDAD_02.md (Tareas 43-59)
1617
"""
@@ -19,14 +20,16 @@
1920

2021
from django.contrib.auth import get_user_model
2122
from django.core.exceptions import PermissionDenied, ValidationError
22-
from rest_framework import status, viewsets
23+
from rest_framework import generics, status, viewsets
2324
from rest_framework.decorators import action
25+
from rest_framework.permissions import AllowAny
2426
from rest_framework.response import Response
2527

2628
from .serializers_usuarios import (
2729
AsignarGruposSerializer,
2830
SuspenderUsuarioSerializer,
2931
UserListSerializer,
32+
UserRegistrationSerializer,
3033
UserSerializer,
3134
)
3235
from .services_usuarios import UsuarioService
@@ -354,3 +357,42 @@ def asignar_grupos(self, request, pk=None):
354357
{'error': str(e)},
355358
status=status.HTTP_400_BAD_REQUEST,
356359
)
360+
361+
362+
class UserRegistrationView(generics.CreateAPIView):
363+
"""
364+
Vista para registro publico de usuarios.
365+
366+
POST /api/register/
367+
368+
Body:
369+
- username: str (requerido, unico)
370+
- email: str (requerido, unico)
371+
- password: str (requerido, minimo 8 caracteres)
372+
- password_confirm: str (requerido, debe coincidir)
373+
374+
Returns:
375+
- 201 Created: Usuario registrado exitosamente
376+
- 400 Bad Request: Datos invalidos
377+
"""
378+
379+
serializer_class = UserRegistrationSerializer
380+
permission_classes = [AllowAny]
381+
382+
def create(self, request, *args, **kwargs):
383+
"""Registra un nuevo usuario."""
384+
serializer = self.get_serializer(data=request.data)
385+
serializer.is_valid(raise_exception=True)
386+
user = serializer.save()
387+
388+
return Response(
389+
{
390+
'message': 'Usuario registrado exitosamente',
391+
'user': {
392+
'id': user.id,
393+
'username': user.username,
394+
'email': user.email,
395+
}
396+
},
397+
status=status.HTTP_201_CREATED,
398+
)

api/callcentersite/callcentersite/settings/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
"callcentersite.apps.audit",
4545
"callcentersite.apps.dashboard",
4646
"callcentersite.apps.configuration",
47+
"callcentersite.apps.configuracion",
48+
"callcentersite.apps.presupuestos",
49+
"callcentersite.apps.politicas",
50+
"callcentersite.apps.excepciones",
51+
"callcentersite.apps.alertas",
52+
"callcentersite.apps.clientes",
53+
"callcentersite.apps.equipos",
54+
"callcentersite.apps.horarios",
55+
"callcentersite.apps.metricas",
56+
"callcentersite.apps.tickets",
4757
"dora_metrics",
4858
]
4959

0 commit comments

Comments
 (0)