diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py index 385191637d32de97c028d5953e25afbe1e13980b..3d19684eb2b83044a40a23a69c72a6a2b65351ec 100644 --- a/gso/workflows/router/create_router.py +++ b/gso/workflows/router/create_router.py @@ -10,16 +10,16 @@ from orchestrator.workflow import StepList, conditional, done, init, step, workf from orchestrator.workflows.steps import resync, set_status, store_process_subscription from orchestrator.workflows.utils import wrap_create_initial_input_form from pydantic import validator -from services.crm import customer_selector from gso.products.product_blocks.router import RouterRole, RouterVendor, generate_fqdn from gso.products.product_types.router import RouterInactive, RouterProvisioning from gso.products.product_types.site import Site from gso.services import infoblox, provisioning_proxy, subscriptions +from gso.services.crm import customer_selector from gso.services.netbox_client import NetboxClient from gso.services.provisioning_proxy import pp_interaction +from gso.utils.functions import iso_from_ipv4 from gso.utils.types.ip_port import PortNumber -from utils.functions import iso_from_ipv4 def _site_selector() -> Choice: diff --git a/test/conftest.py b/test/conftest.py index 2bb17b7a4baf57e34ca19bfadcb70f0790cc5786..c9859e8c5fed66a2fd9e2256d2f8bf720c79b639 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -47,6 +47,12 @@ class FakerProvider(BaseProvider): return ipaddress.IPv6Network(str(network) + "/64") + def tt_number(self) -> str: + random_date = self.generator.date(pattern="%Y%m%d") + random_int = self.generator.random_int(min=10000000, max=99999999) + + return f"TT#{random_date}{random_int}" + @pytest.fixture(scope="session") def faker() -> Faker: diff --git a/test/fixtures.py b/test/fixtures.py index 24864c5a96b9f4b26b2e2e648c63f12a26f1f1f4..1dcd2d790918be9bfed466ebc8edd17c19c4e692 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -32,7 +32,7 @@ def site_subscription_factory(faker): site_ts_address=None, ) -> UUIDstr: description = description or "Site Subscription" - site_name = site_name or faker.name() + site_name = site_name or faker.domain_word() site_city = site_city or faker.city() site_country = site_country or faker.country() site_country_code = site_country_code or faker.country_code() diff --git a/test/workflows/__init__.py b/test/workflows/__init__.py index 9704dceec41bfc7644dfcfe5d81e6918bfd0ad55..899f7aa2e9f41688f63cbac52d3903eec6d85076 100644 --- a/test/workflows/__init__.py +++ b/test/workflows/__init__.py @@ -201,7 +201,7 @@ def run_workflow(workflow_key: str, input_data: State | list[State]) -> tuple[WF def resume_workflow( - process: ProcessStat, step_log: list[tuple[Step, WFProcess]], input_data: State + process: ProcessStat, step_log: list[tuple[Step, WFProcess]], input_data: State | list[State] ) -> tuple[WFProcess, list]: # ATTENTION!! This code needs to be as similar as possible to `server.services.processes.resume_process` # The main differences are: we use a different step log function, and we don't run in a separate thread diff --git a/test/workflows/router/__init__.py b/test/workflows/router/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/workflows/router/test_create_router.py b/test/workflows/router/test_create_router.py new file mode 100644 index 0000000000000000000000000000000000000000..8c818b4e9cf041e43ae226880a5a95ee28c4a6dc --- /dev/null +++ b/test/workflows/router/test_create_router.py @@ -0,0 +1,231 @@ +from unittest.mock import patch + +import pytest +from infoblox_client import objects + +from gso.products import ProductType, Site +from gso.products.product_blocks.router import RouterRole, RouterVendor +from gso.products.product_types.router import Router +from gso.services.crm import customer_selector, get_customer_by_name +from gso.services.subscriptions import get_product_id_by_name +from test.workflows import ( + assert_aborted, + assert_complete, + assert_suspended, + extract_state, + resume_workflow, + run_workflow, +) + + +@pytest.fixture +def router_input_form_data(site_subscription_factory, faker): + router_site = site_subscription_factory() + + return { + "tt_number": faker.tt_number(), + "customer": getattr(customer_selector(), get_customer_by_name("GÉANT")["id"]), + "router_site": router_site, + "hostname": faker.pystr(), + "ts_port": faker.pyint(), + "router_vendor": faker.random_choices(elements=(RouterVendor.NOKIA, RouterVendor.JUNIPER), length=1)[0], + "router_role": faker.random_choices(elements=(RouterRole.P, RouterRole.PE, RouterRole.AMT), length=1)[0], + "is_ias_connected": True, + } + + +def _user_accept_and_assert_suspended(process_stat, step_log, extra_data=None): + extra_data = extra_data or {} + result, step_log = resume_workflow(process_stat, step_log, extra_data) + assert_suspended(result) + + return result, step_log + + +@pytest.mark.workflow +@patch("gso.workflows.router.create_router.provisioning_proxy.provision_router") +@patch("gso.workflows.router.create_router.NetBoxClient.create_device") +@patch("gso.workflows.router.create_router.infoblox.hostname_available") +@patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr") +@patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn") +@patch("gso.workflows.router.create_router.infoblox.allocate_v6_network") +@patch("gso.workflows.router.create_router.infoblox.allocate_v4_network") +@patch("gso.workflows.router.create_router.infoblox.allocate_host") +def test_create_router_success( + mock_allocate_host, + mock_allocate_v4_network, + mock_allocate_v6_network, + mock_find_host_by_fqdn, + mock_find_network_by_cidr, + mock_hostname_available, + mock_netbox_create_device, + mock_provision_router, + router_input_form_data, + faker, +): + # Set up mock return values + mock_site = Site.from_subscription(router_input_form_data["router_site"]).site + mock_v4 = faker.ipv4() + mock_v4_net = faker.ipv4_network() + mock_v6 = faker.ipv6() + mock_fqdn = ( + f"{router_input_form_data['hostname']}.{mock_site.site_name.lower()}." + f"{mock_site.site_country_code.lower()}.geant.net" + ) + mock_hostname_available.return_value = True + mock_allocate_host.return_value = str(mock_v4), str(mock_v6) + mock_allocate_v4_network.return_value = mock_v4_net + mock_allocate_v6_network.return_value = faker.ipv6_network() + mock_find_host_by_fqdn.return_value = objects.HostRecord( + connector=None, + aliases=[mock_fqdn], + comment=faker.sentence(), + ipv4addrs=[ + objects.IPv4( + ipv4addr=str(mock_v4), + configure_for_dhcp=False, + mac="00:00:00:00:00:00", + ip=str(mock_v4), + host=f"lo0.{mock_fqdn}", + ) + ], + name=mock_fqdn, + ) + mock_find_network_by_cidr.return_value = objects.NetworkV4( + connector=None, + comment=faker.sentence(), + network=str(mock_v4_net), + network_view="default", + cidr=str(mock_v4_net), + ) + + # Run workflow + product_id = get_product_id_by_name(ProductType.ROUTER) + initial_router_data = [{"product": product_id}, router_input_form_data] + result, process_stat, step_log = run_workflow("create_router", initial_router_data) + assert_suspended(result) + + lso_return = { + "pp_run_results": { + "status": "ok", + "job_id": faker.uuid4(), + "output": "parsed_output", + "return_code": 0, + }, + "confirm": "ACCEPTED", + } + + result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, lso_return) + result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, [{}, {}]) + result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, lso_return) + result, step_log = resume_workflow(process_stat, step_log, [{}, {}]) + + assert_complete(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Router.from_subscription(subscription_id) + + assert "active" == subscription.status + assert subscription.description == f"Router {mock_fqdn}" + + assert mock_provision_router.call_count == 2 + assert mock_netbox_create_device.call_count == 1 + assert mock_find_host_by_fqdn.call_count == 1 + assert mock_find_network_by_cidr.call_count == 3 + + +@pytest.mark.workflow +@patch("gso.workflows.router.create_router.provisioning_proxy.provision_router") +@patch("gso.workflows.router.create_router.NetBoxClient.create_device") +@patch("gso.workflows.router.create_router.infoblox.hostname_available") +@patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr") +@patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn") +@patch("gso.workflows.router.create_router.infoblox.allocate_v6_network") +@patch("gso.workflows.router.create_router.infoblox.allocate_v4_network") +@patch("gso.workflows.router.create_router.infoblox.allocate_host") +def test_create_router_lso_failure( + mock_allocate_host, + mock_allocate_v4_network, + mock_allocate_v6_network, + mock_find_host_by_fqdn, + mock_find_network_by_cidr, + mock_hostname_available, + mock_netbox_create_device, + mock_provision_router, + router_input_form_data, + faker, +): + # Set up mock return values + mock_site = Site.from_subscription(router_input_form_data["router_site"]).site + mock_v4 = faker.ipv4() + mock_v4_net = faker.ipv4_network() + mock_v6 = faker.ipv6() + mock_fqdn = ( + f"{router_input_form_data['hostname']}.{mock_site.site_name.lower()}." + f"{mock_site.site_country_code.lower()}.geant.net" + ) + mock_hostname_available.return_value = True + mock_allocate_host.return_value = str(mock_v4), str(mock_v6) + mock_allocate_v4_network.return_value = mock_v4_net + mock_allocate_v6_network.return_value = faker.ipv6_network() + mock_find_host_by_fqdn.return_value = objects.HostRecord( + connector=None, + aliases=[mock_fqdn], + comment=faker.sentence(), + ipv4addrs=[ + objects.IPv4( + ipv4addr=str(mock_v4), + configure_for_dhcp=False, + mac="00:00:00:00:00:00", + ip=str(mock_v4), + host=f"lo0.{mock_fqdn}", + ) + ], + name=mock_fqdn, + ) + mock_find_network_by_cidr.return_value = objects.NetworkV4( + connector=None, + comment=faker.sentence(), + network=str(mock_v4_net), + network_view="default", + cidr=str(mock_v4_net), + ) + + # Run workflow + product_id = get_product_id_by_name(ProductType.ROUTER) + initial_router_data = [{"product": product_id}, router_input_form_data] + result, process_stat, step_log = run_workflow("create_router", initial_router_data) + assert_suspended(result) + + lso_return = { + "pp_run_results": { + "status": "failure", + "job_id": faker.uuid4(), + "output": "parsed_output", + "return_code": 1, + }, + "confirm": "ACCEPTED", + } + + attempts = 3 + for _ in range(attempts - 1): + result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, lso_return) + result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, [{}, {}]) + + result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, lso_return) + result, step_log = resume_workflow(process_stat, step_log, [{}, {}]) + + assert_aborted(result) + + state = extract_state(result) + subscription_id = state["subscription_id"] + subscription = Router.from_subscription(subscription_id) + + assert "provisioning" == subscription.status + assert subscription.description == f"Router {mock_fqdn}" + + assert mock_provision_router.call_count == attempts + assert mock_netbox_create_device.call_count == 0 + assert mock_find_host_by_fqdn.call_count == 0 + assert mock_find_network_by_cidr.call_count == 0