Skip to content
Snippets Groups Projects
Commit 1ec676bf authored by Karel van Klink's avatar Karel van Klink :smiley_cat:
Browse files

Merge branch 'feature/fix-pipeline' into 'develop'

Attempt at fixing the CI/CD pipeline

See merge request !13
parents 6c9b41e5 b5ae598c
No related branches found
No related tags found
1 merge request!13Attempt at fixing the CI/CD pipeline
[MAIN]
extension-pkg-whitelist=pydantic
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
# Note that it does not contain TODO, only the default FIXME and XXX
notes=FIXME,
XXX
......@@ -2,5 +2,6 @@
"collection": {
"name": "organisation.collection",
"version": "1.2.3.4.5.6"
}
},
"ansible_private_data_dir": "(optional) /some/absolute/path"
}
......@@ -28,8 +28,10 @@ CONFIG_SCHEMA = {
'type': 'object',
'properties': {
'collection': {'$ref': '#/definitions/galaxy-collection-details'},
'ansible_private_data_dir': {'type': 'string'}
},
'required': ['collection'],
'optional': ['ansible_private_data_dir'],
'additionalProperties': False
}
......
"""
Module that gathers common API responses and data models.
"""
import enum
import logging
import threading
import uuid
# from typing import Optional
import ansible_runner
from pydantic import BaseModel
import requests
from pydantic import BaseModel
from lso import config
logger = logging.getLogger(__name__)
# enum.StrEnum is only available in python 3.11
class PlaybookJobStatus(str, enum.Enum):
"""
Enumerator for status codes of a playbook job that is running.
"""
#: All is well.
OK = 'ok'
#: We're not OK.
ERROR = 'error'
class PlaybookLaunchResponse(BaseModel):
"""
Running a playbook gives this response.
:param status:
:type status: PlaybookJobStatus
:param job_id:
:type job_id: str, optional
:param info:
:type info: str, optional
"""
#: Status of a Playbook job.
status: PlaybookJobStatus
#: The ID assigned to a job.
job_id: str = ''
#: Information on a job.
info: str = ''
def playbook_launch_success(job_id: str) -> PlaybookLaunchResponse:
"""
Return a :class:`PlaybookLaunchResponse` for the successful start of a
playbook execution.
:return PlaybookLaunchResponse: A playbook launch response that is
successful.
"""
return PlaybookLaunchResponse(status=PlaybookJobStatus.OK, job_id=job_id)
def playbook_launch_error(reason: str) -> PlaybookLaunchResponse:
"""
Return a :class:`PlaybookLaunchResponse` for the erroneous start of a
playbook execution.
:return PlaybookLaunchResponse: A playbook launch response that is
unsuccessful.
"""
return PlaybookLaunchResponse(status=PlaybookJobStatus.ERROR, info=reason)
......@@ -37,12 +72,23 @@ def _run_playbook_proc(
playbook: str,
extra_vars: dict,
inventory: str,
callback: str,
private_data_dir: str = '/opt/geant-gap-ansible', # TODO???
callback: str
):
r = ansible_runner.run(
private_data_dir=private_data_dir,
"""
Internal function for running a playbook.
:param job_id: Identifier of the job that is executed.
:type job_id: str
:param playbook: Name of a playbook.
:type playbook: str
:param extra_vars: Extra variables passed to the Ansible playbook
:type extra_vars: dict
:param callback: Callback URL to POST to when execution is completed.
:type callback: str
"""
ansible_playbook_run = ansible_runner.run(
private_data_dir=config.load().get('ansible_private_data_dir', None),
playbook=playbook,
inventory=inventory,
extravars=extra_vars)
......@@ -51,10 +97,10 @@ def _run_playbook_proc(
# TODO: NAT-151
payload = {
'job_id': job_id,
'output': str(r.stdout.read()),
'return_code': int(r.rc)
'output': str(ansible_playbook_run.stdout.read()),
'return_code': int(ansible_playbook_run.rc)
}
requests.post(callback, json=payload)
requests.post(callback, json=payload, timeout=10000)
def run_playbook(
......@@ -62,9 +108,26 @@ def run_playbook(
extra_vars: dict,
inventory: str,
callback: str) -> PlaybookLaunchResponse:
"""
Run an Ansible playbook against a specified inventory.
:param playbook: name of the playbook that is executed.
:type playbook: str
:param extra_vars: Any extra vars needed for the playbook to run.
:type extra_vars: dict
:param inventory: The inventory that the playbook is executed against.
:type inventory: str
:param callback: Callback URL where the playbook should send a status
update when execution is completed. This is used for WFO to continue
with the next step in a workflow.
:type callback: str
:return: Result of playbook launch, this could either be successful or
unsuccessful.
:rtype: :class:`PlaybookLaunchResponse`
"""
job_id = str(uuid.uuid4())
t = threading.Thread(
thread = threading.Thread(
target=_run_playbook_proc,
kwargs={
'job_id': job_id,
......@@ -73,6 +136,6 @@ def run_playbook(
'extra_vars': extra_vars,
'callback': callback
})
t.start()
thread.start()
return playbook_launch_success(job_id=job_id) # TODO: handle real id's
......@@ -13,35 +13,117 @@ router = APIRouter()
class InterfaceAddress(pydantic.BaseModel):
"""
Set of one IPv4 and one IPv6 address.
:param v4: IPv4 address
:type v4: ipaddress.IPv4Address, optional
:param v6: IPv6 address
:type v6: ipaddress.IPv6Address, optional
"""
#: IPv4 address.
v4: Optional[ipaddress.IPv4Address] = None
#: IPv6 address.
v6: Optional[ipaddress.IPv6Address] = None
class InterfaceNetwork(pydantic.BaseModel):
"""
Set of one IPv4 and one IPv6 subnet, should be given in CIDR notation.
:param v4: IPv4 subnet
:type v4: ipaddress.IPv4Network, optional
:param v6: IPv6 subnet
:type v6: ipaddress.IPv6Network, optional
"""
#: IPv4 subnet.
v4: Optional[ipaddress.IPv4Network] = None
#: IPv6 subnet.
v6: Optional[ipaddress.IPv6Network] = None
class DeviceParams(pydantic.BaseModel):
fqdn: str # TODO: add some validation
"""
Parameters of an API call that deploys a new device in the network.
This device can either be a router or a switch, from the different vendors
that are supported.
:param fqdn:
:type fqdn: str
:param lo_address:
:type lo_address: :class:`InterfaceAddress`
:param lo_iso_address:
:type lo_iso_address: str
:param si_ipv4_network:
:type si_ipv4_network: ipaddress.IPv4Network
:param ias_lt_network:
:type ias_lt_network: :class:`InterfaceNetwork`
:param site_country_code:
:type site_country_code: str
:param site_city:
:type site_city: str
:param site_latitude:
:type site_latitude: str
:type site_longitude:
:type site_longitude: str
:param device_type:
:type device_type: str
:param device_vendor:
:type device_vendor: str
"""
#: FQDN of a device, TODO: add some validation
fqdn: str
#: Loopback interface address of a device, should be an
#: :class:`InterfaceAddress` object.
lo_address: InterfaceAddress
#: Loopback ISO address.
lo_iso_address: str
#: SI IPv4 network, as an `ipaddress.IPv4Network`.
si_ipv4_network: ipaddress.IPv4Network
#: IAS LT network, stored as an :class:`InterfaceNetwork`.
ias_lt_network: InterfaceNetwork
#: Country code where the device is located.
site_country_code: str
#: City where the device is located.
site_city: str
#: Latitude of the device site.
site_latitude: str
#: Longitude of the device site.
site_longitude: str
snmp_location: str
#: Type of device, either ``router`` or ``switch``.
device_type: str
#: The device vendor, for specific configuration.
device_vendor: str
class NodeProvisioningParams(pydantic.BaseModel):
"""
Parameters for node provisioning
:param callback:
:type callback: pydantic.HttpUrl
:param device:
:type device: :class:`DeviceParams`
:param ansible_host:
:type ansible_host: ipaddress.IPv4Address or ipaddress.IPv6Address
:param ansible_port:
:type ansible_port: int
:param dry_run:
:type dry_run: bool, optional
"""
#: Callback URL that is reported back to WFO, this will allow for the
#: workflow to continue once the playbook has been executed.
callback: pydantic.HttpUrl # TODO: NAT-151
#: Parameters for the new device.
device: DeviceParams
#: Host address that ansible should point to, most likely a terminal
#: server.
ansible_host: ipaddress.IPv4Address | ipaddress.IPv6Address
#: Similar to the ``ansible_host``, but for a port number.
ansible_port: int
#: Whether this playbook execution should be a dry run, or run for real.
#: defaults to ``True`` for obvious reasons, also making it an optional
#: parameter.
dry_run: Optional[bool] = True
......@@ -52,8 +134,10 @@ async def provision_node(params: NodeProvisioningParams) \
Launches a playbook to provision a new node.
The response will contain either a job id or error information.
:param params: NodeProvisioningParams
:return: PlaybookLaunchResponse
:param params: Parameters for provisioning a new node
:type params: :class:`NodeProvisioningParams`
:return: Response from the Ansible runner, including a run ID.
:rtype: :class:`PlaybookLaunchResponse`
"""
extra_vars = {
'lo_ipv4_address': str(params.device.lo_address.v4),
......@@ -63,7 +147,10 @@ async def provision_node(params: NodeProvisioningParams) \
'ias_lt_ipv4_network': str(params.device.ias_lt_network.v4),
'ias_lt_ipv6_network': str(params.device.ias_lt_network.v6),
'site_country_code': params.device.site_country_code,
'snmp_location': params.device.snmp_location,
'snmp_location': f'{params.device.site_city},'
f'{params.device.site_country_code}'
f'[{params.device.site_latitude},'
f'{params.device.site_longitude}]',
'site_city': params.device.site_city,
'site_latitude': params.device.site_latitude,
'site_longitude': params.device.site_longitude,
......
......@@ -17,6 +17,8 @@ def test_nominal_node_provisioning(client):
params = {
'callback': callback_url,
'ansible_host': '127.0.0.1',
'ansible_port': 22,
'device': {
'fqdn': 'bogus.fqdn.org',
'lo_address': {'v4': '1.2.3.4', 'v6': '2001:db8::1'},
......@@ -24,12 +26,18 @@ def test_nominal_node_provisioning(client):
'snmp_location': 'city,country[1.2,3.4]',
'si_ipv4_network': '1.2.3.0/24',
'ias_lt_network': {'v4': '1.2.3.0/24', 'v6': '2001:db8::/64'},
'site_country_code': 'abcdefg'
'site_country_code': 'XX',
'site_city': 'NOWHERE',
'site_latitude': '0.000',
'site_longitude': '0.000',
'device_type': 'router',
'device_vendor': 'vendor',
'dry_run': 'True'
}
}
with patch('lso.routes.common.ansible_runner.run') as _run:
rv = client.post('/api/device', json=params)
rv = client.post('/api/device/', json=params)
assert rv.status_code == 200
response = rv.json()
# wait a second for the run thread to finish
......
......@@ -5,8 +5,6 @@ exclude = obsolete,.tox,venv
passenv = XDG_CACHE_HOME,USE_COMPOSE
deps =
coverage
flake8
vale
pylint
-r requirements.txt
......@@ -17,9 +15,6 @@ commands =
coverage html
coverage report --fail-under 80
pylint lso --fail-under 9.5
flake8
python docs/dump-openapi-spec.py
sphinx-apidoc lso lso/app.py -o docs/source -d 2 -f
vale --config=docs/vale/.vale.ini sync
vale --config=docs/vale/.vale.ini docs/source/*.rst lso/*.py
sphinx-build -b html docs/source docs/build
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment