diff --git a/resource_management/db/model.py b/resource_management/db/model.py index 4203d874debb734dc58298affb2d93046354c59e..b8e3c54bd5456e1182a2cbf66ada2db37036fdd0 100644 --- a/resource_management/db/model.py +++ b/resource_management/db/model.py @@ -5,10 +5,11 @@ Resources Database This database holds information about router port resources """ +import enum import logging from typing import Any -from sqlalchemy import Column, ForeignKey, Integer, String +import sqlalchemy as sa from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship @@ -18,10 +19,16 @@ logger = logging.getLogger(__name__) base_schema: Any = declarative_base() +class AvalabilityStates(enum.Enum): + AVAILABLE = enum.auto() + USED = enum.auto() + RESERVED = enum.auto() + + class Router(base_schema): __tablename__ = 'routers' - id = Column('id', Integer, primary_key=True) - fqdn = Column('fqdn', String, nullable=False) + id = sa.Column('id', sa.Integer, primary_key=True) + fqdn = sa.Column('fqdn', sa.String, nullable=False) physical = relationship( "PhysicalInterface", @@ -36,13 +43,18 @@ class Router(base_schema): class LAG(base_schema): __tablename__ = 'lags' - id = Column('id', Integer, primary_key=True) - name = Column('name', String, nullable=False) + id = sa.Column('id', sa.Integer, primary_key=True) + name = sa.Column('name', sa.String, nullable=False) + + availability = sa.Column( + 'availability', + sa.Enum(*[a.name for a in AvalabilityStates]), + nullable=False) - router_id = Column( + router_id = sa.Column( 'router_id', - Integer, - ForeignKey('routers.id'), + sa.Integer, + sa.ForeignKey('routers.id'), nullable=False) router = relationship('Router', back_populates='lags') @@ -54,20 +66,25 @@ class LAG(base_schema): class PhysicalInterface(base_schema): __tablename__ = 'physical_interfaces' - id = Column('id', Integer, primary_key=True) - name = Column('name', String, nullable=False) + id = sa.Column('id', sa.Integer, primary_key=True) + name = sa.Column('name', sa.String, nullable=False) + + availability = sa.Column( + 'availability', + sa.Enum(*[a.name for a in AvalabilityStates]), + nullable=False) - router_id = Column( + router_id = sa.Column( 'router_id', - Integer, - ForeignKey('routers.id'), + sa.Integer, + sa.ForeignKey('routers.id'), nullable=False) router = relationship('Router', back_populates='physical') - lag_id = Column( + lag_id = sa.Column( 'lag_id', - Integer, - ForeignKey('lags.id'), + sa.Integer, + sa.ForeignKey('lags.id'), nullable=True) lag = relationship('LAG', back_populates='physical') @@ -79,11 +96,11 @@ class PhysicalInterface(base_schema): class LogicalInterface(base_schema): __tablename__ = 'logical_interfaces' - id = Column('id', Integer, primary_key=True) - name = Column('name', String, nullable=False) - physical_id = Column( + id = sa.Column('id', sa.Integer, primary_key=True) + name = sa.Column('name', sa.String, nullable=False) + physical_id = sa.Column( 'physical_id', - Integer, - ForeignKey('physical_interfaces.id'), + sa.Integer, + sa.ForeignKey('physical_interfaces.id'), nullable=False) physical = relationship('PhysicalInterface', back_populates='logical') diff --git a/resource_management/router_interfaces.py b/resource_management/router_interfaces.py index d0cbc8811520ea0083b8c91e3b4c4bd40f7ba2a0..5ef8d6c16ca43c67a92d721f91faaa55bef70347 100644 --- a/resource_management/router_interfaces.py +++ b/resource_management/router_interfaces.py @@ -6,12 +6,10 @@ from resource_management.db import model logger = logging.getLogger(__name__) -def load_router_interfaces(fqdn: str): +def load_new_router_interfaces(fqdn: str): """ load all interface info from the router and update the db with the current information - - TODO: merge, not just create new """ params = config.load() @@ -25,11 +23,19 @@ def load_router_interfaces(fqdn: str): physical = juniper.load_installed_ethernet_ports(dev) logical = juniper.load_logical_interfaces(dev) - def _find_lag_name(interface_name): - for name, interfaces in aggregates.items(): - if interface_name in interfaces: - return name - return None + _inverted_aggregate_map = {} + for lag_name, physical_interfaces in aggregates.items(): + for pn in physical_interfaces: + _inverted_aggregate_map[pn] = lag_name + + def _interface_availability(ifc_name): + if physical[ifc_name]['oper']: + return model.AvalabilityStates.USED.name + if ifc_name in _inverted_aggregate_map: + return model.AvalabilityStates.USED.name + if ifc_name in logical: + return model.AvalabilityStates.USED.name + return model.AvalabilityStates.AVAILABLE.name with db.session_scope() as session: @@ -38,13 +44,15 @@ def load_router_interfaces(fqdn: str): # add all the lag records # keep a map for referencing below when adding physical interfaces - agg_records = {} + lag_records = {} for a in aggregates.keys(): record = model.LAG( name=a, + # if present, then used + availability=model.AvalabilityStates.USED.name, router=router_record) session.add(record) - agg_records[a] = record + lag_records[a] = record # add all the physical interface records # keep a map for referencing below when adding logical interfaces @@ -52,10 +60,11 @@ def load_router_interfaces(fqdn: str): for physical_name, _ in physical.items(): record = model.PhysicalInterface( name=physical_name, + availability=_interface_availability(physical_name), router=router_record) - lag_name = _find_lag_name(physical_name) + lag_name = _inverted_aggregate_map.get(physical_name, None) if lag_name: - record.lag = agg_records[lag_name] + record.lag = lag_records[lag_name] physical_records[physical_name] = record session.add(record) diff --git a/resource_management/routes/interfaces.py b/resource_management/routes/interfaces.py index 8a480c3b49c2d63a5bd8b618281d4efde9999492..51a26a912e6c431395a8ceea0afd9463bffe95f1 100644 --- a/resource_management/routes/interfaces.py +++ b/resource_management/routes/interfaces.py @@ -1,6 +1,8 @@ from fastapi import APIRouter, HTTPException import pydantic +import sqlalchemy as sa + from resource_management import db from resource_management import config from resource_management.db import model @@ -8,6 +10,8 @@ from resource_management import router_interfaces router = APIRouter() +FIRST_LAG_INDEX = 0 + class InterfaceCounts(pydantic.BaseModel): total: int @@ -31,13 +35,21 @@ class NextPhysicalInterface(pydantic.BaseModel): name: str -@router.post('/import/{fqdn}') -async def load_router_interfaces(fqdn: str) -> InterfacesSummary: +@router.post('/initialize-router/{fqdn}') +async def load_new_router_interfaces(fqdn: str) -> InterfacesSummary: params = config.load() db.init_db_model(params['db']) - router_interfaces.load_router_interfaces(fqdn) + with db.session_scope() as session: + num_routers = session.query(sa.func.count(model.Router.fqdn)) \ + .filter_by(fqdn=fqdn).scalar() + if num_routers != 0: + raise HTTPException( + status_code=400, + detail=f'router "{fqdn}" already exists') + + router_interfaces.load_new_router_interfaces(fqdn) with db.session_scope() as session: router = session.query(model.Router).filter_by(fqdn=fqdn).one() @@ -55,6 +67,13 @@ async def load_router_interfaces(fqdn: str) -> InterfacesSummary: } +@router.post('/reconcile-router/{fqdn}') +async def reconcile_current_router_interfaces(fqdn: str): + raise HTTPException( + status_code=501, + detail='not implemented') + + @router.post('/next-lag/{fqdn}') async def reserve_next_lag(fqdn: str) -> NextLAG: """ @@ -68,22 +87,29 @@ async def reserve_next_lag(fqdn: str) -> NextLAG: db.init_db_model(params['db']) with db.session_scope() as session: - records = session.query(model.LAG.name) \ + router_record = session.query(model.Router.id) \ + .filter_by(fqdn=fqdn).one() + lag_rows = session.query(model.LAG.name) \ .join(model.Router).filter_by(fqdn=fqdn).all() - names = set(r[0] for r in records) - - def _next_lag_name(): - index = 1 - while True: - candidate = f'ae{index}' - if candidate not in names: - return candidate - index += 1 + lag_names = set(r[0] for r in lag_rows) + + def _next_lag_name(): + index = FIRST_LAG_INDEX + while True: + candidate_name = f'ae{index}' + if candidate_name not in lag_names: + new_lag_record = model.LAG( + name=candidate_name, + router_id=router_record[0], + availability=model.AvalabilityStates.RESERVED.name) + session.add(new_lag_record) + return candidate_name + index += 1 - return { - 'fqdn': fqdn, - 'name': _next_lag_name() - } + return { + 'fqdn': fqdn, + 'name': _next_lag_name() + } @router.post('/next-physical/{fqdn}/{lag_name}') @@ -104,8 +130,12 @@ async def reserve_physical_bundle_member( def _find_available_physical(): for ifc in router.physical: - if not ifc.lag: + if ifc.availability \ + == model.AvalabilityStates.AVAILABLE.name: + ifc.availability = model.AvalabilityStates.RESERVED.name + session.add(ifc) return ifc.name + raise HTTPException( status_code=404, detail=f'no available physical ports for "{lag_name}"') diff --git a/test/test_interfaces_routes.py b/test/test_interfaces_routes.py index 6ffc443aae121e94605d38562f41f650d7fc8a76..ed0158d337778a411dcae2570f76c3919f82e9a3 100644 --- a/test/test_interfaces_routes.py +++ b/test/test_interfaces_routes.py @@ -3,6 +3,10 @@ import jsonschema from resource_management.routes.default import Version from resource_management.routes import interfaces from resource_management import router_interfaces +from resource_management import db +from resource_management.db import model + +import pytest def test_bad_method(client): @@ -18,24 +22,57 @@ def test_version_request(client): def test_update_router_interfaces( client, resources_db, mocked_router, router_name): - rv = client.post(f'/api/interfaces/import/{router_name}') + rv = client.post(f'/api/interfaces/initialize-router/{router_name}') assert rv.status_code == 200 jsonschema.validate(rv.json(), interfaces.InterfacesSummary.schema()) def test_next_lag(client, resources_db, mocked_router, router_name): - router_interfaces.load_router_interfaces(router_name) + router_interfaces.load_new_router_interfaces(router_name) rv = client.post(f'/api/interfaces/next-lag/{router_name}') assert rv.status_code == 200 - jsonschema.validate(rv.json(), interfaces.NextLAG.schema()) + response = rv.json() + jsonschema.validate(response, interfaces.NextLAG.schema()) + + with db.session_scope() as session: + row = session.query(model.LAG). \ + filter_by(name=response['name']). \ + join(model.Router). \ + filter_by(fqdn=router_name).one() + assert row.availability == model.AvalabilityStates.RESERVED.name def test_next_physical(client, resources_db, mocked_router, router_name): - router_interfaces.load_router_interfaces(router_name) + if '.lab.' in router_name: + pytest.skip('not all lab routers have available ports') + if router_name.startswith('rt'): + pytest.skip('not all rt* have available ports') + + router_interfaces.load_new_router_interfaces(router_name) + + with db.session_scope() as session: + rows = session.query(model.LAG.name). \ + join(model.Router). \ + filter_by(fqdn=router_name).all() + lag_name = rows[0][0] - rv = client.post(f'/api/interfaces/next-physical/{router_name}/ae123123') + rv = client.post(f'/api/interfaces/next-physical/{router_name}/{lag_name}') assert rv.status_code == 200 - jsonschema.validate(rv.json(), interfaces.NextPhysicalInterface.schema()) + + response = rv.json() + jsonschema.validate(response, interfaces.NextPhysicalInterface.schema()) + + assert response['fqdn'] == router_name + assert response['lag'] == lag_name + + with db.session_scope() as session: + ifc_row = session.query(model.PhysicalInterface). \ + filter_by(name=response['name']). \ + join(model.Router). \ + filter_by(fqdn=router_name). \ + join(model.LAG). \ + filter_by(name=lag_name).one() + assert ifc_row.availability == model.AvalabilityStates.RESERVED.name diff --git a/test/test_parse_router.py b/test/test_parse_router.py index e6723d5f454de67d3a1b29c8d25266b17725ae41..ffe6157f9a4a80bba0cfb5a22b133a62a0d61aa9 100644 --- a/test/test_parse_router.py +++ b/test/test_parse_router.py @@ -83,7 +83,7 @@ def test_load_interfaces( new design for this app: no chassis info for now """ - router_interfaces.load_router_interfaces(router_name) + router_interfaces.load_new_router_interfaces(router_name) with db.session_scope() as session: router = session.query(model.Router).filter_by(fqdn=router_name).one()