diff --git a/.gitignore b/.gitignore index 71e1def92b37ee1e2a107c9b37e26b2794b7060e..20969d16f10cfe87950112c3dfb1a3a00e4ef5fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ __pycache__/ *.egg-info .coverage coverage.xml -.tox/ +.tox/device_vendor +.vscode \ No newline at end of file diff --git a/geant_service_orchestrator/main.py b/geant_service_orchestrator/main.py index e14ab09627003191a31902f1170dfc71449e7f8c..9ea59ed79b973e8cb0d4dd20d43865f58e9a2f6f 100644 --- a/geant_service_orchestrator/main.py +++ b/geant_service_orchestrator/main.py @@ -2,7 +2,7 @@ from orchestrator import OrchestratorCore from orchestrator.cli.main import app as core_cli from orchestrator.settings import AppSettings import geant_service_orchestrator.products # noqa: F401 -# import workflows +import geant_service_orchestrator.workflows # noqa: F401 app = OrchestratorCore(base_settings=AppSettings()) diff --git a/geant_service_orchestrator/migrations/versions/2023-04-05_857225661207_add_device_workflows.py b/geant_service_orchestrator/migrations/versions/2023-04-05_857225661207_add_device_workflows.py new file mode 100644 index 0000000000000000000000000000000000000000..c4d7d7fed664375254113c0537cdbe2ce6d90beb --- /dev/null +++ b/geant_service_orchestrator/migrations/versions/2023-04-05_857225661207_add_device_workflows.py @@ -0,0 +1,39 @@ +"""add Device workflows. + +Revision ID: 857225661207 +Revises: f4959f32c866 +Create Date: 2023-04-05 09:16:03.725750 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = '857225661207' +down_revision = 'f4959f32c866' +branch_labels = None +depends_on = None + + +from orchestrator.migrations.helpers import create_workflow, delete_workflow + +new_workflows = [ + { + "name": "create_device", + "target": "CREATE", + "description": "Create Device", + "product_type": "Device" + } +] + + +def upgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + create_workflow(conn, workflow) + + +def downgrade() -> None: + conn = op.get_bind() + for workflow in new_workflows: + delete_workflow(conn, workflow["name"]) diff --git a/geant_service_orchestrator/products/product_types/device.py b/geant_service_orchestrator/products/product_types/device.py index a630d2b3378ee2b37b428a239a8e05ddc53c7b42..0b20fa9080744ee682734b83bf0c0866be42c680 100644 --- a/geant_service_orchestrator/products/product_types/device.py +++ b/geant_service_orchestrator/products/product_types/device.py @@ -11,8 +11,8 @@ class DeviceType(strEnum): class DeviceVendor(strEnum): - Juniper = "juniper" - Newvendor = "newvendor" + Juniper = "Juniper" + Newvendor = "Newvendor" class DeviceInactive(SubscriptionModel, is_base=True): diff --git a/geant_service_orchestrator/services/ipam.py b/geant_service_orchestrator/services/ipam.py index 30d2a07b5edbeb4b5fb8ef9b7c45b7954b0737b8..021bcb4480859835a7fe081d294f91fa7f44f33a 100644 --- a/geant_service_orchestrator/services/ipam.py +++ b/geant_service_orchestrator/services/ipam.py @@ -14,8 +14,9 @@ class HostAddresses(BaseSettings): def new_service_networks( - ipam: settings.IPAMParams, service_params: settings.ServiceNetworkParams) -> ServiceNetworks: + oss = settings.load_oss_params() + assert oss.IPAM.INFOBLOX # TODO: load from ipam # cf. https://gitlab.geant.org/goat/gap-jenkins/-/blob/development/service-editor/gap_service_editor/ipam.py#L35-66 # noqa: E501 return ServiceNetworks( @@ -23,8 +24,10 @@ def new_service_networks( v6=ipaddress.IPv6Network('dead:beef::/120')) -def new_host_address(fqdn: str, networks: ServiceNetworks) -> HostAddresses: +def new_device_lo_address() -> HostAddresses: + oss = settings.load_oss_params() + assert oss.IPAM.INFOBLOX # TODO: load from ipam return HostAddresses( - v4=ipaddress.IPv4Address('10.0.0.1'), - v6=ipaddress.IPv6Address('dead:beef::1')) + v4=ipaddress.IPv4Address('10.10.10.10'), + v6=ipaddress.IPv6Address('fc00:798:aa:1::10')) diff --git a/geant_service_orchestrator/services/provisioning_proxy.py b/geant_service_orchestrator/services/provisioning_proxy.py index 96874ed8bb549bc5ab2407cf420a53bcafd827bf..2a3626d3976ad735a82b72759c32f7a3d662f8b9 100644 --- a/geant_service_orchestrator/services/provisioning_proxy.py +++ b/geant_service_orchestrator/services/provisioning_proxy.py @@ -6,7 +6,10 @@ import requests def provision_node( node_subscription_params: DeviceBlock, - pp_params=settings.OSSParams.PROVISIONING_PROXY): + dry_run: bool = False): + oss = settings.load_oss_params() + pp_params = oss.PROVISIONING_PROXY + assert pp_params r = requests.get( f'https://{pp_params.api_base}' f'/api/version', diff --git a/geant_service_orchestrator/workflows/__init__.py b/geant_service_orchestrator/workflows/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d93b338a9c01be9a2e5c3fee0c46f341ef5c98ae --- /dev/null +++ b/geant_service_orchestrator/workflows/__init__.py @@ -0,0 +1,5 @@ +from orchestrator.workflows import LazyWorkflowInstance + +LazyWorkflowInstance("workflows.device.create_device", "create_device") +LazyWorkflowInstance("workflows.device.terminate_device", "terminate_device") +LazyWorkflowInstance("workflows.device.get_facts", "get_facts") diff --git a/geant_service_orchestrator/workflows/device/create_device.py b/geant_service_orchestrator/workflows/device/create_device.py new file mode 100644 index 0000000000000000000000000000000000000000..f8bffe75ebf21421d1f59fbfecf13171e1bf58e4 --- /dev/null +++ b/geant_service_orchestrator/workflows/device/create_device.py @@ -0,0 +1,200 @@ +import ipaddress +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 geant_service_orchestrator.products.product_types import device +from geant_service_orchestrator.services import ipam, provisioning_proxy + + +def initial_input_form_generator(product_name: str) -> FormGenerator: + class CreateDeviceForm(FormPage): + class Config: + title = product_name + + fqdn: str + ts_address: ipaddress.IPv4Address + ts_port: int + device_vendor: device.DeviceVendor + + user_input = yield CreateDeviceForm + + return user_input.dict() + + +@step("Create subscription") +def create_subscription(product: UUIDstr) -> State: + subscription = device.DeviceInactive.from_product_id(product, uuid4()) + + return { + "subscription": subscription, + "subscription_id": subscription.subscription_id, + } + + +@step("Get information from IPAM ") +def get_info_from_ipam(subscription: device.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_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: + country = 'Spain' + city = 'Barcelona' + country_code = 'ES' + latitude = '41.3743' + longitude = '2.1328' + subscription.device.site_country = country + subscription.device.site_city = city + subscription.device.site_country_code = country_code + subscription.device.site_latitude = latitude + subscription.device.site_longitude = longitude + subscription.device.snmp_location = ( + f'{city.upper()},{country.upper()}[{latitude},{longitude}]' + ) + + return {"subscription": subscription} + + +@step("Initialize subscription") +def initialize_subscription( + subscription: device.DeviceInactive, + fqdn: str, + ts_address: ipaddress.IPv4Address, + ts_port: str, + device_vendor: device.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, SubscriptionLifecycle.PROVISIONING + ) + + 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} + + +@workflow( + "Create Device", + initial_input_form=wrap_create_initial_input_form( + 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 + ) diff --git a/geant_service_orchestrator/workflows/device/get_facts.py b/geant_service_orchestrator/workflows/device/get_facts.py new file mode 100644 index 0000000000000000000000000000000000000000..6cc1cbe21da2acb7e90af1a306bf7855e507af30 --- /dev/null +++ b/geant_service_orchestrator/workflows/device/get_facts.py @@ -0,0 +1,60 @@ +from orchestrator.forms import FormPage +from orchestrator.forms.validators import Label +from orchestrator.targets import Target +# from orchestrator.types import SubscriptionLifecycle +from orchestrator.types import InputForm, UUIDstr +from orchestrator.workflow import done, init, step, workflow +# from orchestrator.workflows.steps import ( +# resync, +# set_status, +# store_process_subscription, +# unsync, +# ) +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from products import Device + + +def initial_input_form_generator( + subscription_id: UUIDstr, organisation: UUIDstr +) -> InputForm: + subscription = Device.from_subscription(subscription_id) + + class TerminateForm(FormPage): + are_you_sure: Label = ( + f"Are you sure you want to get facts from \ + {subscription.description}?" + ) + + return TerminateForm + + +@step("Get facts") +def get_facts(subscription_id) -> None: + subscription = Device.from_subscription(subscription_id) + import ansible_runner + + r = ansible_runner.run( + private_data_dir="/opt", + playbook="get_facts.yaml", + inventory=subscription.device.fqdn, + ) + out = r.stdout.read() + out_splitted = out.splitlines() + + return {"output": out_splitted} + + +@workflow( + "Get Facts from Device", + initial_input_form=wrap_modify_initial_input_form( + initial_input_form_generator), + target=Target.SYSTEM, +) +def get_facts_from_device(): + return ( + init + >> get_facts + # >> resync + >> done + ) diff --git a/geant_service_orchestrator/workflows/device/terminate_device.py b/geant_service_orchestrator/workflows/device/terminate_device.py new file mode 100644 index 0000000000000000000000000000000000000000..5c175778ca3e302dd13bf809001087e698fe6d27 --- /dev/null +++ b/geant_service_orchestrator/workflows/device/terminate_device.py @@ -0,0 +1,53 @@ +from orchestrator.forms import FormPage +from orchestrator.forms.validators import Label +from orchestrator.targets import Target +from orchestrator.types import InputForm, SubscriptionLifecycle, UUIDstr +from orchestrator.workflow import done, init, step, workflow +from orchestrator.workflows.steps import ( + resync, + set_status, + store_process_subscription, + unsync, +) +from orchestrator.workflows.utils import wrap_modify_initial_input_form + +from geant_service_orchestrator.products.product_types.device import Device + + +def initial_input_form_generator(subscription_id: UUIDstr) -> InputForm: + subscription = Device.from_subscription(subscription_id) + + class TerminateForm(FormPage): + are_you_sure: Label = ( + f"Are you sure you want to remove {subscription.description}?" + ) # type:ignore + + return TerminateForm + + +def _deprovision_in_user_management_system(fqdn: str) -> str: + pass + + +@step("Deprovision device") +def deprovision_user(subscription: Device) -> None: + # _deprovision_in_user_management_system(subscription.user.user_id) + pass + + +@workflow( + "Terminate device", + initial_input_form=wrap_modify_initial_input_form( + initial_input_form_generator), + target=Target.TERMINATE, +) +def terminate_user(): + return ( + init + >> store_process_subscription(Target.TERMINATE) + >> unsync + >> deprovision_user + >> set_status(SubscriptionLifecycle.TERMINATED) + >> resync + >> done + )