diff --git a/Dockerfile b/Dockerfile index 646bd9f13042d70535a85b4f669e3cdf36e6b674..40a48076b8af542688b2181b4c4b795c5bf6dd16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,18 @@ FROM python:3.12.7-alpine WORKDIR /app +# Set environment variables for predictable Python behavior and UTF-8 encoding +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 + ARG ARTIFACT_VERSION RUN apk add --no-cache gcc libc-dev libffi-dev curl vim && \ addgroup -S appgroup && adduser -S appuser -G appgroup -h /app -RUN pip install \ +RUN pip install --no-cache-dir \ --pre \ --trusted-host 150.254.211.2 \ --extra-index-url https://150.254.211.2/artifactory/api/pypi/geant-swd-pypi/simple \ @@ -19,7 +25,7 @@ ENV TRANSLATIONS_DIR=/app/gso/translations/ # Copy the shell scripts and ensure scripts do not have Windows line endings and make them executable COPY start-app.sh start-worker.sh start-scheduler.sh /app/ RUN sed -i 's/\r$//' start-app.sh start-worker.sh start-scheduler.sh && \ - chmod 755 start-app.sh start-worker.sh start-scheduler.sh + chmod +x start-app.sh start-worker.sh start-scheduler.sh RUN chown -R appuser:appgroup /app USER appuser diff --git a/gso/__init__.py b/gso/__init__.py index ccbb2d0df7993b2fb35a88781f053de344ed2143..56c9c7d4615be7e6964c95025dab3fc078af40e9 100644 --- a/gso/__init__.py +++ b/gso/__init__.py @@ -17,6 +17,7 @@ import gso.workflows # noqa: F401 from gso.api import router as api_router from gso.auth.oidc import oidc_instance from gso.auth.opa import graphql_opa_instance, opa_instance +from gso.graphql_api.resolvers.customer import custom_subscription_interface from gso.graphql_api.types import GSO_SCALAR_OVERRIDES from gso.settings import load_oss_params @@ -35,7 +36,7 @@ def init_gso_app() -> OrchestratorCore: app.register_authentication(oidc_instance) app.register_authorization(opa_instance) app.register_graphql_authorization(graphql_opa_instance) - app.register_graphql() + app.register_graphql(subscription_interface=custom_subscription_interface) app.include_router(api_router, prefix="/api") if app_settings.EXECUTOR == ExecutorType.WORKER: diff --git a/gso/graphql_api/resolvers/__init__.py b/gso/graphql_api/resolvers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..decfd8226c967a94bf2560b00cb3f62be8a00f10 --- /dev/null +++ b/gso/graphql_api/resolvers/__init__.py @@ -0,0 +1 @@ +"""resolvers module.""" diff --git a/gso/graphql_api/resolvers/customer.py b/gso/graphql_api/resolvers/customer.py new file mode 100644 index 0000000000000000000000000000000000000000..b8187a69ca00f4941ebaa104eab2a98880b74055 --- /dev/null +++ b/gso/graphql_api/resolvers/customer.py @@ -0,0 +1,30 @@ +"""This module contains the resolver for the customer field in the subscription and process types.""" + +import strawberry +from orchestrator.graphql.schemas.customer import CustomerType +from orchestrator.graphql.schemas.process import ProcessType +from orchestrator.graphql.schemas.subscription import SubscriptionInterface +from orchestrator.graphql.utils.override_class import override_class + +from gso.services.partners import get_partner_by_id + + +async def resolve_customer(root: CustomerType) -> CustomerType: + """Resolve the customer field for a subscription or process.""" + partner = get_partner_by_id(root.customer_id) + + return CustomerType( + customer_id=partner.partner_id, + fullname=partner.name, + shortcode=partner.email, + ) + + +customer_field = strawberry.field( + resolver=resolve_customer, + description="Returns customer of a subscription", +) +customer_field.name = "customer" # type: ignore[attr-defined] + +override_class(ProcessType, [customer_field]) # type: ignore[list-item] +custom_subscription_interface = override_class(SubscriptionInterface, [customer_field]) # type: ignore[list-item] diff --git a/gso/migrations/versions/2024-12-18_8a65d0ed588e_add_site_connectivity_check_task.py b/gso/migrations/versions/2024-12-18_8a65d0ed588e_add_site_connectivity_check_task.py new file mode 100644 index 0000000000000000000000000000000000000000..8c3e61e9e66afc45f8fb92c5663b2e41428c8303 --- /dev/null +++ b/gso/migrations/versions/2024-12-18_8a65d0ed588e_add_site_connectivity_check_task.py @@ -0,0 +1,39 @@ +"""Add Site connectivity check task. + +Revision ID: 8a65d0ed588e +Revises: 818d4ffe65df +Create Date: 2024-12-18 14:36:35.886366 + +""" +from uuid import uuid4 + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '8a65d0ed588e' +down_revision = '818d4ffe65df' +branch_labels = None +depends_on = None + +workflow = { + "name": "task_check_site_connectivity", + "target": "SYSTEM", + "description": "Check Site Connectivity", + "workflow_id": uuid4(), +} + + +def upgrade() -> None: + conn = op.get_bind() + conn.execute( + sa.text( + "INSERT INTO workflows VALUES (:workflow_id, :name, :target, :description, now()) ON CONFLICT DO NOTHING" + ), + workflow, + ) + + +def downgrade() -> None: + conn = op.get_bind() + conn.execute(sa.text("DELETE FROM workflows WHERE name = :name"), {"name": workflow["name"]}) diff --git a/gso/translations/en-GB.json b/gso/translations/en-GB.json index c62d1c4d0601eeee8a1c4e0553f7685f2f7b4fbd..4dfe84023f01061ec7c45b0f84740827482172a2 100644 --- a/gso/translations/en-GB.json +++ b/gso/translations/en-GB.json @@ -103,6 +103,7 @@ "task_modify_partners": "Modify partner task", "task_delete_partners": "Delete partner task", "task_clean_old_tasks": "Remove old cleanup tasks", + "task_check_site_connectivity": "Check NETCONF connectivity of a Site", "promote_p_to_pe": "Promote P to PE", "create_layer_2_circuit": "Create Layer 2 Circuit", "modify_layer_2_circuit": "Modify Layer 2 Circuit", diff --git a/gso/workflows/__init__.py b/gso/workflows/__init__.py index cc6725d47cdf05b2a0959920d7f08455987232d3..20e652d4400a387ef852b9751cb27d5037bdbe9f 100644 --- a/gso/workflows/__init__.py +++ b/gso/workflows/__init__.py @@ -107,6 +107,7 @@ LazyWorkflowInstance("gso.workflows.tasks.create_partners", "task_create_partner LazyWorkflowInstance("gso.workflows.tasks.modify_partners", "task_modify_partners") LazyWorkflowInstance("gso.workflows.tasks.delete_partners", "task_delete_partners") LazyWorkflowInstance("gso.workflows.tasks.clean_old_tasks", "task_clean_old_tasks") +LazyWorkflowInstance("gso.workflows.tasks.check_site_connectivity", "task_check_site_connectivity") # Edge port workflows LazyWorkflowInstance("gso.workflows.edge_port.create_edge_port", "create_edge_port") diff --git a/gso/workflows/tasks/check_site_connectivity.py b/gso/workflows/tasks/check_site_connectivity.py new file mode 100644 index 0000000000000000000000000000000000000000..a373d144ce19828133bce7c3be4f9f61e2903a8a --- /dev/null +++ b/gso/workflows/tasks/check_site_connectivity.py @@ -0,0 +1,46 @@ +"""A task for checking site connectivity. + +When a new router is planned to be deployed, it is good practice to verify that the OOB connectivity is functioning +correctly. This task takes a site and a port number as input, and checks whether there is reachability across the OOB +access. +""" + +from orchestrator import workflow +from orchestrator.forms import SubmitFormPage +from orchestrator.targets import Target +from orchestrator.workflow import StepList, begin, done, step +from pydantic import ConfigDict +from pydantic_forms.types import FormGenerator, UUIDstr + +from gso.products.product_types.site import Site +from gso.services.lso_client import LSOState, lso_interaction +from gso.utils.helpers import active_site_selector +from gso.utils.types.ip_address import PortNumber + + +def _initial_input_form_generator() -> FormGenerator: + class CheckSiteConnectivityForm(SubmitFormPage): + model_config = ConfigDict(title="Verify NETCONF connectivity to a Site") + + site: active_site_selector() # type: ignore[valid-type] + port: PortNumber + + user_input = yield CheckSiteConnectivityForm + return user_input.model_dump() + + +@step("Check NETCONF connectivity") +def check_netconf_connectivity(site: UUIDstr, port: PortNumber) -> LSOState: + """Run an Ansible playbook that validates NETCONF connectivity to a Site.""" + site_subscription = Site.from_subscription(site).site + return { + "playbook_name": "gap_ansible/playbooks/check_netconf_connectivity.yaml", + "extra_vars": {"port_number": port}, + "inventory": {"all": {"hosts": {site_subscription.site_ts_address: None}}}, + } + + +@workflow("Check Site Connectivity", _initial_input_form_generator, Target.SYSTEM) +def task_check_site_connectivity() -> StepList: + """Check successful NETCONF connectivity of a Site.""" + return begin >> lso_interaction(check_netconf_connectivity) >> done diff --git a/requirements.txt b/requirements.txt index 97fd1fedbe08dc83073afba7953b608a5fd2a189..bc8fd2b09673f22fef3df8a8dd7356ecd95e2e3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,11 @@ orchestrator-core==2.8.0 -requests==2.31.0 +requests==2.32.3 infoblox-client~=0.6.0 pycountry==23.12.11 pynetbox==7.3.3 celery-redbeat==2.2.0 celery==5.3.6 -azure-identity==1.16.0 +azure-identity==1.19.0 msgraph-sdk==1.2.0 ping3==4.0.8 unidecode==1.3.8 diff --git a/setup.py b/setup.py index bce5bfc27bfdab4845b989f98d31dc31f276530f..3c152ecec8f344a4be312becfce06d38b2843e3f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ from setuptools import find_packages, setup setup( name="geant-service-orchestrator", - version="2.28", + version="2.29", author="GÉANT Orchestration and Automation Team", author_email="goat@geant.org", description="GÉANT Service Orchestrator",