diff --git a/.gitignore b/.gitignore index 6f706bf96b555012f29447cf06e44530ac2393f1..ab0739bc5673861de5f36164e4232746b0b53ebe 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ __pycache__/ coverage.xml .tox/device_vendor .vscode -venv \ No newline at end of file +venv +oss-params.json diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 98ee26d5e8212038c0b972f01a2f78d0a75aedfd..a13415dd26ddb0a58ad7eb65abcfd191c2e40de2 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -1,4 +1,7 @@ { + "GENERAL": { + "public_hostname": "https://gap.geant.org" + }, "RESOURCE_MANAGER_API_PREFIX": "http://localhost:44444", "IPAM": { "INFOBLOX": { @@ -18,8 +21,9 @@ } }, "PROVISIONING_PROXY": { - "api_base": "http://localhost:44444", + "scheme": "https", + "api_base": "localhost:44444", "auth": "Bearer <token>", "api_version": 1123 } -} \ No newline at end of file +} diff --git a/gso/services/provisioning_proxy.py b/gso/services/provisioning_proxy.py index ec282b84bba0fcd3e39d0729fd3760312de9d1c7..1189647133fde96d469c71f32f188bdc186fa8b0 100644 --- a/gso/services/provisioning_proxy.py +++ b/gso/services/provisioning_proxy.py @@ -1,24 +1,93 @@ import logging -from gso.products.product_types.device \ - import DeviceBlock +import requests +from orchestrator import inputstep +from orchestrator.config.assignee import Assignee +from orchestrator.forms import FormPage, ReadOnlyField +from orchestrator.forms.validators import Accept, Label, LongText +from orchestrator.types import UUIDstr, State + from gso import settings -# import requests +from gso.products.product_types.device import DeviceProvisioning logger = logging.getLogger(__name__) def provision_node( - node_subscription_params: DeviceBlock, + node_subscription_params: DeviceProvisioning, + process_id: UUIDstr, dry_run: bool = True): oss = settings.load_oss_params() pp_params = oss.PROVISIONING_PROXY assert pp_params - logger.debug(f'[disabled] provisioning node {node_subscription_params}') + device_params = node_subscription_params.device + callback_url = f'{settings.load_oss_params().GENERAL.public_hostname}' \ + f'/api/processes/{process_id}/resume' + logger.debug(f'[disabled] provisioning node {device_params}') + + parameters = { + 'callback': callback_url, + 'dry_run': dry_run, + 'device': { + 'fqdn': device_params.fqdn, + 'lo_address': { + 'v4': str(device_params.lo_ipv4_address), + 'v6': str(device_params.lo_ipv6_address) + }, + 'lo_iso_address': device_params.lo_iso_address, + 'si_ipv4_network': str(device_params.si_ipv4_network), + 'ias_lt_network': { + 'v4': str(device_params.ias_lt_ipv4_network), + 'v6': str(device_params.ias_lt_ipv6_network) + }, + 'site_country_code': device_params.site_country_code, + 'site_city': device_params.site_city, + 'site_latitude': device_params.site_latitude, + 'site_longitude': device_params.site_longitude, + 'snmp_location': device_params.snmp_location, + 'device_type': node_subscription_params.device_type, + 'device_vendor': node_subscription_params.device_vendor, + 'ts_address': device_params.ts_address, + 'ts_port': device_params.ts_port + } + } + + post_request = requests.post( + f'{pp_params.scheme}://{pp_params.api_base}' + f'/api/device', + json=parameters) + post_request.raise_for_status() + + +@inputstep('Await provisioning proxy results', assignee=Assignee('SYSTEM')) +def await_pp_results() -> State: + class ProvisioningResultPage(FormPage): + class Config: + title = 'Do NOT click on confirm in this step!' + + warning_label: Label = 'This step relies on an external service to ' \ + 'send an update to the orchestrator, do not ' \ + 'interfere with this process please.' + pp_run_results: dict = {'state': 'not_ready'} + confirm: Accept = Accept('INCOMPLETE') + + result_page = yield ProvisioningResultPage + + return result_page.dict() + + +@inputstep('Confirm provisioning proxy results', assignee=Assignee('SYSTEM')) +def confirm_pp_results(state: State) -> State: + class ConfirmRunPage(FormPage): + class Config: + title = 'Execution completed, please confirm the results.' + + run_status: str = ReadOnlyField(state['pp_run_results']['status']) + run_results: LongText = ReadOnlyField( + f"{state['pp_run_results']['output']}") + confirm: Accept = Accept('INCOMPLETE') + + yield ConfirmRunPage - # r = requests.get( - # f'https://{pp_params.api_base}' - # f'/api/version', - # params=node_subscription_params.dict()) - # r.raise_for_status() + return state diff --git a/gso/settings.py b/gso/settings.py index 0846e5f2ae2f983f7216666df34a8388e0697853..11cebf9d63dcf14633cae189df201ab295d31532 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -4,6 +4,10 @@ import os from pydantic import BaseSettings +class GeneralParams(BaseSettings): + public_hostname: str + + class InfoBloxParams(BaseSettings): scheme: str wapi_version: str @@ -34,12 +38,14 @@ class IPAMParams(BaseSettings): class ProvisioningProxyParams(BaseSettings): + scheme: str api_base: str auth: str # FIXME: unfinished api_version: int class OSSParams(BaseSettings): + GENERAL: GeneralParams IPAM: IPAMParams RESOURCE_MANAGER_API_PREFIX: str # api prefix PROVISIONING_PROXY: ProvisioningProxyParams diff --git a/gso/workflows/device/create_device.py b/gso/workflows/device/create_device.py index d3e0bfd0055f56688cfc01a32b0697e852dee0d8..c59c2866b679520fbd91e227ac829136bc0408c2 100644 --- a/gso/workflows/device/create_device.py +++ b/gso/workflows/device/create_device.py @@ -3,16 +3,18 @@ from uuid import uuid4 from orchestrator.forms import FormPage from orchestrator.targets import Target -from orchestrator.workflow import inputstep -from orchestrator.forms.validators import Accept from orchestrator.types import FormGenerator, State from orchestrator.types import SubscriptionLifecycle, UUIDstr from orchestrator.workflow import done, init, step, workflow from orchestrator.workflows.steps import resync, set_status from orchestrator.workflows.steps import store_process_subscription from orchestrator.workflows.utils import wrap_create_initial_input_form -from gso.products.product_types import device -# from gso.services import ipam, provisioning_proxy + +from gso.products.product_types.device import DeviceVendor, DeviceInactive, \ + DeviceProvisioning +from gso.services import provisioning_proxy +from gso.services.provisioning_proxy import confirm_pp_results, \ + await_pp_results def initial_input_form_generator(product_name: str) -> FormGenerator: @@ -23,40 +25,41 @@ def initial_input_form_generator(product_name: str) -> FormGenerator: fqdn: str ts_address: ipaddress.IPv4Address ts_port: int - device_vendor: device.DeviceVendor + device_vendor: DeviceVendor user_input = yield CreateDeviceForm return user_input.dict() -@step("Create subscription") +@step('Create subscription') def create_subscription(product: UUIDstr) -> State: - subscription = device.DeviceInactive.from_product_id(product, uuid4()) + subscription = DeviceInactive.from_product_id(product, uuid4()) return { - "subscription": subscription, - "subscription_id": subscription.subscription_id, + 'subscription': subscription, + 'subscription_id': subscription.subscription_id, } -@step("Get information from IPAM ") -def get_info_from_ipam(subscription: device.DeviceInactive) -> State: +@step('Get information from IPAM') +def get_info_from_ipam(subscription: DeviceInactive) -> State: # lo = ipam.new_device_lo_address() # subscription.device.lo_ipv4_address = lo.v4 # subscription.device.lo_ipv6_address = lo.v6 # TODO: get info about how these should be generated - subscription.device.lo_ipv4_address = "10.10.10.10" - subscription.device.lo_ipv6_address = "fc00:798:10::10" - subscription.device.lo_iso_address = "49.51e5.0001.0620.4009.6047.00" - subscription.device.si_ipv4_network = "192.168.0.0/31" - subscription.device.ias_lt_ipv4_network = "192.168.1.0/31" - subscription.device.ias_lt_ipv6_network = "fc00:798:1::150/126" - return {"subscription": subscription} + subscription.device.lo_ipv4_address = '10.10.10.10' + subscription.device.lo_ipv6_address = 'fc00:798:10::10' + subscription.device.lo_iso_address = '49.51e5.0001.0620.4009.6047.00' + subscription.device.si_ipv4_network = '192.168.0.0/31' + subscription.device.ias_lt_ipv4_network = '192.168.1.0/31' + subscription.device.ias_lt_ipv6_network = 'fc00:798:1::150/126' + + return {'subscription': subscription} -@step("get information about SNMP") -def get_snmp_info(subscription: device.DeviceInactive) -> State: +@step('Get information about SNMP') +def get_snmp_info(subscription: DeviceInactive) -> State: country = 'Spain' city = 'Barcelona' country_code = 'ES' @@ -71,132 +74,74 @@ def get_snmp_info(subscription: device.DeviceInactive) -> State: f'{city.upper()},{country.upper()}[{latitude},{longitude}]' ) - return {"subscription": subscription} + return {'subscription': subscription} -@step("Initialize subscription") +@step('Initialize subscription') def initialize_subscription( - subscription: device.DeviceInactive, - fqdn: str, - ts_address: ipaddress.IPv4Address, - ts_port: str, - device_vendor: device.DeviceVendor + subscription: DeviceInactive, + fqdn: str, + ts_address: ipaddress.IPv4Address, + ts_port: str, + device_vendor: DeviceVendor ) -> State: subscription.device.fqdn = fqdn subscription.device.ts_address = str(ts_address) subscription.device.ts_port = str(ts_port) subscription.device_vendor = device_vendor - subscription.description = f"Device {fqdn} type \ - ({subscription.device_type})" - subscription = device.DeviceProvisioning.from_other_lifecycle( + subscription.description = f'Device {fqdn} ' \ + f'({subscription.device_type})' + subscription = DeviceProvisioning.from_other_lifecycle( subscription, SubscriptionLifecycle.PROVISIONING ) - return {"subscription": subscription} + return {'subscription': subscription} -@step("Provision device [DRY RUN]") -def provision_device_dry( - subscription: device.DeviceProvisioning, - fqdn: str, - ts_address: str, - ts_port: str -) -> State: - # import ansible_runner - # - # r = ansible_runner.run( - # private_data_dir="/opt/geant-gap-ansible", - # playbook="base_config.yaml", - # inventory=subscription.device.fqdn, - # extravars={ - # "lo_ipv4_address": str(subscription.device.lo_ipv4_address), - # "lo_ipv6_address": str(subscription.device.lo_ipv6_address), - # "lo_iso_address": subscription.device.lo_iso_address, - # "snmp_location": subscription.device.snmp_location, - # "si_ipv4_network": str(subscription.device.si_ipv4_network), - # "lt_ipv4_network": str(subscription.device.ias_lt_ipv4_network), - # "lt_ipv6_network": str(subscription.device.ias_lt_ipv6_network), - # "site_country_code": subscription.device.site_country_code, - # "verb": "deploy", - # }, - # ) - # out = r.stdout.read() - # out_splitted = out.splitlines() - # # if r.rc != 0: - # # raise ValueError("Ansible has failed") - # return {"dry_run_output": out_splitted, "return_code": r.rc} - # provisioning_proxy.provision_node( - # node_subscription_params=subscription, - # dry_run=True) - # TODO: figure out what to return when we are suspending & waiting - # for the provisioning-proxy to call back - return {"return_code": 0} - - -@inputstep("Confirm step", assignee="CHANGES") -def confirm_step() -> FormGenerator: - class ConfirmForm(FormPage): - confirm: Accept - - user_input = yield ConfirmForm - - return {"confirm": user_input.confirm} - - -@step("Provision device [FOR REAL]") -def provision_device_real( - subscription: device.DeviceProvisioning, - fqdn: str, - ts_address: str, - ts_port: str -) -> State: - # import ansible_runner - # - # r = ansible_runner.run( - # private_data_dir="/opt/geant-gap-ansible", - # playbook="base_config.yaml", - # inventory=subscription.device.fqdn, - # extravars={ - # "lo_ipv4_address": str(subscription.device.lo_ipv4_address), - # "lo_ipv6_address": str(subscription.device.lo_ipv6_address), - # "lo_iso_address": subscription.device.lo_iso_address, - # "snmp_location": subscription.device.snmp_location, - # "si_ipv4_network": str(subscription.device.si_ipv4_network), - # "lt_ipv4_network": str(subscription.device.ias_lt_ipv4_network), - # "lt_ipv6_network": str(subscription.device.ias_lt_ipv6_network), - # "site_country_code": subscription.device.site_country_code, - # "verb": "deploy", - # }, - # ) - # out = r.stdout.read() - # out_splitted = out.splitlines() - # - # return {"real_run_output": out_splitted, "return_code": r.rc} - # provisioning_proxy.provision_node( - # node_subscription_params=subscription) - # TODO: figure out what to return when we are suspending & waiting - # for the provisioning-proxy to call back - return {"return_code": 0} +@step('Provision device [DRY RUN]') +def provision_device_dry(subscription: DeviceProvisioning, + process_id: UUIDstr) -> State: + provisioning_proxy.provision_node( + subscription, + process_id + ) + + return {'subscription': subscription} + + +@step('Provision device [FOR REAL]') +def provision_device_real(subscription: DeviceProvisioning, + process_id: UUIDstr) -> State: + provisioning_proxy.provision_node( + subscription, + process_id, + False # No dry run this time, run it for real + ) + + return {'subscription': subscription} @workflow( - "Create Device", + 'Create device', initial_input_form=wrap_create_initial_input_form( - initial_input_form_generator), + initial_input_form_generator), target=Target.CREATE, ) def create_device(): return ( - init - >> create_subscription - >> store_process_subscription(Target.CREATE) - >> get_info_from_ipam - >> get_snmp_info - >> initialize_subscription - >> provision_device_dry - >> confirm_step - >> provision_device_real - >> set_status(SubscriptionLifecycle.ACTIVE) - >> resync - >> done + init + >> create_subscription + >> store_process_subscription(Target.CREATE) + >> get_info_from_ipam + >> get_snmp_info + >> initialize_subscription + >> provision_device_dry + >> await_pp_results + >> confirm_pp_results + >> provision_device_real + >> await_pp_results + >> confirm_pp_results + >> set_status(SubscriptionLifecycle.ACTIVE) + >> resync + >> done ) diff --git a/tox.ini b/tox.ini index 7973b25a04d0a5a1e560b7fe9fe03fea63887bc3..dd02509de947f75dab25e672ed28a3801dc3b28d 100644 --- a/tox.ini +++ b/tox.ini @@ -15,4 +15,3 @@ commands = # coverage report --fail-under 80 coverage report flake8 -