diff --git a/requirements.txt b/requirements.txt index 719a1d2fc226e3c9b9b41a5e606c192edf14bfeb..f2f76e1e9eb2ab7fc19646125e129eefff059a7e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,16 @@ jsonschema requests sqlalchemy alembic + +httpx +fastapi +pydantic +uvicorn[standard] + mysqlclient -click junos-eznc pytest +responses sphinx sphinx-rtd-theme diff --git a/resource_management/__init__.py b/resource_management/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1589417bb4e9d55b64017dcb53a4a653fe75d8cf 100644 --- a/resource_management/__init__.py +++ b/resource_management/__init__.py @@ -0,0 +1,58 @@ +""" +automatically invoked app factory +""" +import logging + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from resource_management import environment +from resource_management import config + +from resource_management.routes import default +from resource_management.routes import interfaces + + +def create_app(): + """ + overrides default settings with those found + in the file read from env var SETTINGS_FILENAME + + :return: a new flask app instance + """ + + app = FastAPI() + # app = FastAPI(dependencies=[Depends(get_query_token)]) + + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + app.include_router( + default.router, + prefix='/api', + # tags="default"], + # dependencies=[Depends(get_token_header)], + # responses={418: {"description": "I'm a teapot"}}, + ) + + app.include_router( + interfaces.router, + prefix='/api/interfaces', + # tags=["trunk"], + # dependencies=[Depends(get_token_header)], + # responses={418: {"description": "I'm a teapot"}}, + ) + + # test that config params are available and can be loaded + config.load() + + logging.info('FastAPI app initialized') + + environment.setup_logging() + + return app diff --git a/resource_management/cli.py b/resource_management/cli.py deleted file mode 100644 index 9936f42f77a25058ba6fab427fca934e220c034a..0000000000000000000000000000000000000000 --- a/resource_management/cli.py +++ /dev/null @@ -1,127 +0,0 @@ -""" - -script name [TODO] -==================== - -TODO: add a console script for this - -.. code-block:: bash - - Usage: script name [OPTIONS] - - notes: - first call init_db - (dns=mysql_dsn(params from config)) - then create - a session (as in unit test) - ... and perform the db queries - - Options: - --config FILENAME config filename [required] - --fqdn TEXT config filename [required] - --help Show this message and exit. - -The configuration filename must be formatted according -to this schema: - - .. asjson:: - resource_management.config.CONFIG_SCHEMA - - -""" -import json -import click -import logging -from jsonschema import validate, ValidationError - -from resource_management.hardware.juniper \ - import load_router_ports, LINE_CARDS_LIST_SCHEMA -from resource_management import db, config, environment -from resource_management.db import model - -environment.setup_logging() -logger = logging.getLogger(__name__) - - -def _save_router_info(fqdn, fpcs): - - with db.session_scope() as session: - - node = model.Node(fqdn=fqdn) - session.add(node) - - for _line_card in fpcs: - - line_card_record = model.LineCard( - model=_line_card['model'], - slot=_line_card['slot'], - serial=_line_card['serial'], - description=_line_card['description'], - node=node) - session.add(line_card_record) - - for port in _line_card['ports']: - - port_record = model.Port( - pic=port['pic'], - position=port['position'], - interface=port.get('interface', None), - cable=port.get('cable', None), - line_card=line_card_record) - session.add(port_record) - - for speed in port.get('speeds', []): - session.add( - model.PortSpeedCapability( - speed=speed, - port=port_record)) - - logger.info(f'saved router information: {fqdn}') - - -def _validate_config(_unused_ctx, _unused_param, file): - try: - params = config.load(file) - logger.info("Loaded params from config file: {0}".format(params)) - return params - except json.JSONDecodeError: - raise click.BadParameter('config file is not valid json') - except ValidationError as e: - raise click.BadParameter(e.message) - - -@click.command() -@click.option( - '--config', - required=True, - type=click.File('r'), - help='config filename', - callback=_validate_config) -@click.option( - '--fqdn', - required=True, - type=click.STRING, - help='config filename') -def cli(config, fqdn): - """ - notes: - - first call init_db(dns=mysql_dsn(params from config)) - - then create a session (as in unit test test_db_model.py) - - ... and perform the db queries - """ - - line_cards = list(load_router_ports(fqdn, config['ssh'])) - # sanity check, shouldn't fail ... otherwise die badly - validate(line_cards, LINE_CARDS_LIST_SCHEMA) - - mysql_config = config["mysql"] - dsn = db.mysql_dsn( - username=mysql_config["username"], - password=mysql_config["password"], - hostname=mysql_config["hostname"], - db_name=mysql_config["dbname"]) - - db.init_db_model(dsn) - - _save_router_info(fqdn, line_cards) - - -if __name__ == '__main__': - cli() diff --git a/resource_management/config.py b/resource_management/config.py index 041b26c9dc2e4e1ba66e5ddf391603ff72d114cb..67f9df25153fa00fc0597fa65fb3d5a321856ebd 100644 --- a/resource_management/config.py +++ b/resource_management/config.py @@ -1,21 +1,11 @@ import json import jsonschema +import os CONFIG_SCHEMA = { '$schema': 'http://json-schema.org/draft-07/schema#', 'definitions': { - 'database-credentials': { - 'type': 'object', - 'properties': { - 'hostname': {'type': 'string'}, - 'dbname': {'type': 'string'}, - 'username': {'type': 'string'}, - 'password': {'type': 'string'} - }, - 'required': ['hostname', 'dbname', 'username', 'password'], - 'additionalProperties': False - }, 'ssh-credentials': { 'type': 'object', 'properties': { @@ -41,15 +31,15 @@ CONFIG_SCHEMA = { 'type': 'object', 'properties': { - 'mysql': {'$ref': '#/definitions/database-credentials'}, + 'db': {'type': 'string'}, 'ssh': {'$ref': '#/definitions/ssh-credentials'} }, - 'required': ['mysql', 'ssh'], + 'required': ['db', 'ssh'], 'additionalProperties': False } -def load(f): +def load_from_file(f): """ Loads, validates and returns configuration parameters. @@ -63,3 +53,9 @@ def load(f): config = json.loads(f.read()) jsonschema.validate(config, CONFIG_SCHEMA) return config + + +def load(): + assert 'SETTINGS_FILENAME' in os.environ + with open(os.environ['SETTINGS_FILENAME']) as f: + return load_from_file(f) diff --git a/resource_management/db/__init__.py b/resource_management/db/__init__.py index 6b3d5f7d10462f13c5eaa4842e20972853b1e155..661cc76cc4d2782824633e99f7657199429a790b 100644 --- a/resource_management/db/__init__.py +++ b/resource_management/db/__init__.py @@ -39,6 +39,8 @@ def init_db_model(dsn): # cf. https://docs.sqlalchemy.org/en # /latest/orm/extensions/automap.html engine = create_engine( - dsn, pool_size=10, max_overflow=0, pool_recycle=3600) + dsn, pool_size=10, pool_recycle=3600) + # engine = create_engine( + # dsn, pool_size=10, max_overflow=0, pool_recycle=3600) _SESSION_MAKER = sessionmaker(bind=engine) diff --git a/resource_management/db/model.py b/resource_management/db/model.py index 75599c45513e97d75ea906764f7f2f41abdad345..4203d874debb734dc58298affb2d93046354c59e 100644 --- a/resource_management/db/model.py +++ b/resource_management/db/model.py @@ -18,52 +18,72 @@ logger = logging.getLogger(__name__) base_schema: Any = declarative_base() -class Node(base_schema): - __tablename__ = 'nodes' +class Router(base_schema): + __tablename__ = 'routers' id = Column('id', Integer, primary_key=True) fqdn = Column('fqdn', String, nullable=False) - line_cards = relationship( - "LineCard", - back_populates="node", + + physical = relationship( + "PhysicalInterface", + back_populates="router", + cascade="all, delete, delete-orphan") + + lags = relationship( + "LAG", + back_populates="router", cascade="all, delete, delete-orphan") -class LineCard(base_schema): - __tablename__ = 'line_cards' +class LAG(base_schema): + __tablename__ = 'lags' id = Column('id', Integer, primary_key=True) - slot = Column('slot', Integer, nullable=False) - model = Column('model', String, nullable=False) - serial = Column('serial', String, nullable=False) - description = Column('description', String, nullable=False) - node_id = Column('node_id', Integer, ForeignKey('nodes.id')) - node = relationship('Node', back_populates='line_cards') - ports = relationship( - 'Port', - back_populates='line_card', + name = Column('name', String, nullable=False) + + router_id = Column( + 'router_id', + Integer, + ForeignKey('routers.id'), + nullable=False) + router = relationship('Router', back_populates='lags') + + physical = relationship( + "PhysicalInterface", + back_populates="lag", cascade="all, delete, delete-orphan") -class Port(base_schema): - __tablename__ = 'ports' +class PhysicalInterface(base_schema): + __tablename__ = 'physical_interfaces' id = Column('id', Integer, primary_key=True) - pic = Column('pic', Integer, nullable=False) - position = Column('position', Integer, nullable=False) - interface = Column('interface', Integer, nullable=True) - cable = Column('cable', Integer, nullable=True) - line_card_id = Column( - 'line_card_id', Integer, ForeignKey('line_cards.id')) - line_card = relationship('LineCard', back_populates='ports') - - speed_capabilities = relationship( - 'PortSpeedCapability', - back_populates='port', + name = Column('name', String, nullable=False) + + router_id = Column( + 'router_id', + Integer, + ForeignKey('routers.id'), + nullable=False) + router = relationship('Router', back_populates='physical') + + lag_id = Column( + 'lag_id', + Integer, + ForeignKey('lags.id'), + nullable=True) + lag = relationship('LAG', back_populates='physical') + + logical = relationship( + 'LogicalInterface', + back_populates='physical', cascade="all, delete, delete-orphan") -class PortSpeedCapability(base_schema): - __tablename__ = 'port_speed_capabilities' +class LogicalInterface(base_schema): + __tablename__ = 'logical_interfaces' id = Column('id', Integer, primary_key=True) - speed = Column('speed', String, nullable=False) - port_id = Column( - 'port_id', Integer, ForeignKey('ports.id')) - port = relationship('Port', back_populates='speed_capabilities') + name = Column('name', String, nullable=False) + physical_id = Column( + 'physical_id', + Integer, + ForeignKey('physical_interfaces.id'), + nullable=False) + physical = relationship('PhysicalInterface', back_populates='logical') diff --git a/resource_management/hardware/juniper.py b/resource_management/juniper.py similarity index 98% rename from resource_management/hardware/juniper.py rename to resource_management/juniper.py index 460f4e4c0de169048265fecb69e439bb80959049..4c3eb4dc5f0e277b303d6b382b6d153cdb33a511 100644 --- a/resource_management/hardware/juniper.py +++ b/resource_management/juniper.py @@ -383,13 +383,14 @@ def load_installed_ethernet_ports(router: 'jnpr.junos.Device'): some routers in our network don't return int for mtu from that command, and the pyez view has some validation that fails - for each physical port on the router, yields a dict formatted like: - - { - 'name': str, # interface name + returns a dict formatted as: + { + physical interface name: { 'admin': bool, # admin status ('up' == True) 'oper': bool, # operational status ('up' == True) - } + } + } + """ catalog_spec = { @@ -413,12 +414,13 @@ def load_installed_ethernet_ports(router: 'jnpr.junos.Device'): catalog = loader.load(catalog_spec) PhysicalInterfacesTable = catalog['PhysicalInterfacesTable'] + interfaces = {} for ifc in PhysicalInterfacesTable(router).get(): - yield { - 'name': ifc.interface, + interfaces[ifc.interface] = { 'admin': ifc.admin == 'up', 'oper': ifc.admin == 'up' } + return interfaces def load_router_ports(hostname, ssh_config, port=830): diff --git a/resource_management/router_interfaces.py b/resource_management/router_interfaces.py new file mode 100644 index 0000000000000000000000000000000000000000..d0cbc8811520ea0083b8c91e3b4c4bd40f7ba2a0 --- /dev/null +++ b/resource_management/router_interfaces.py @@ -0,0 +1,69 @@ +import logging + +from resource_management import db, config, juniper +from resource_management.db import model + +logger = logging.getLogger(__name__) + + +def load_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() + + with juniper.router( + hostname=fqdn, + port=830, # TODO: make this configurable? + ssh_config=params['ssh']) as dev: + + aggregates = juniper.load_aggregates(dev) + 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 + + with db.session_scope() as session: + + router_record = model.Router(fqdn=fqdn) + session.add(router_record) + + # add all the lag records + # keep a map for referencing below when adding physical interfaces + agg_records = {} + for a in aggregates.keys(): + record = model.LAG( + name=a, + router=router_record) + session.add(record) + agg_records[a] = record + + # add all the physical interface records + # keep a map for referencing below when adding logical interfaces + physical_records = {} + for physical_name, _ in physical.items(): + record = model.PhysicalInterface( + name=physical_name, + router=router_record) + lag_name = _find_lag_name(physical_name) + if lag_name: + record.lag = agg_records[lag_name] + physical_records[physical_name] = record + session.add(record) + + for physical_name, logical_names in logical.items(): + for ln in logical_names: + record = model.LogicalInterface( + name=ln, + physical=physical_records[physical_name]) + session.add(record) + + session.commit() diff --git a/resource_management/hardware/__init__.py b/resource_management/routes/__init__.py similarity index 100% rename from resource_management/hardware/__init__.py rename to resource_management/routes/__init__.py diff --git a/resource_management/routes/default.py b/resource_management/routes/default.py new file mode 100644 index 0000000000000000000000000000000000000000..5231ffe5af71071993d5fa7d4af392f484fade57 --- /dev/null +++ b/resource_management/routes/default.py @@ -0,0 +1,23 @@ +import pkg_resources + +from fastapi import APIRouter +import pydantic + +API_VERSION = '0.1' +VERSION_STRING = pydantic.constr(regex=r'\d+\.\d+') + +router = APIRouter() + + +class Version(pydantic.BaseModel): + api: VERSION_STRING + module: VERSION_STRING + + +@router.get('/version') +def version() -> Version: + return { + 'api': API_VERSION, + 'module': + pkg_resources.get_distribution('resource-management').version + } diff --git a/resource_management/routes/interfaces.py b/resource_management/routes/interfaces.py new file mode 100644 index 0000000000000000000000000000000000000000..8a480c3b49c2d63a5bd8b618281d4efde9999492 --- /dev/null +++ b/resource_management/routes/interfaces.py @@ -0,0 +1,117 @@ +from fastapi import APIRouter, HTTPException +import pydantic + +from resource_management import db +from resource_management import config +from resource_management.db import model +from resource_management import router_interfaces + +router = APIRouter() + + +class InterfaceCounts(pydantic.BaseModel): + total: int + available: int + + +class InterfacesSummary(pydantic.BaseModel): + fqdn: str + lag: int # number of defined lag interfaces + physical: InterfaceCounts + + +class NextLAG(pydantic.BaseModel): + fqdn: str + name: str + + +class NextPhysicalInterface(pydantic.BaseModel): + fqdn: str + lag: str + name: str + + +@router.post('/import/{fqdn}') +async def load_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: + router = session.query(model.Router).filter_by(fqdn=fqdn).one() + num_lags = len(router.lags) + num_physical = len(router.physical) + num_available_physical = sum(1 for p in router.physical if p.lag) + + return { + 'fqdn': fqdn, + 'lag': num_lags, + 'physical': { + 'total': num_physical, + 'available': num_available_physical + } + } + + +@router.post('/next-lag/{fqdn}') +async def reserve_next_lag(fqdn: str) -> NextLAG: + """ + compute the next available lag name for the given router + + TODO: _next_lag_name is a placeholder for + whatever logic turns out to be right + """ + + params = config.load() + db.init_db_model(params['db']) + + with db.session_scope() as session: + records = 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 + + return { + 'fqdn': fqdn, + 'name': _next_lag_name() + } + + +@router.post('/next-physical/{fqdn}/{lag_name}') +async def reserve_physical_bundle_member( + fqdn: str, lag_name: str) -> NextPhysicalInterface: + """ + compute the next available lag name for the given router + + TODO: _find_available_physical is a placeholder for + whatever logic turns out to be right (e.g. speeds, etc) + """ + + params = config.load() + db.init_db_model(params['db']) + + with db.session_scope() as session: + router = session.query(model.Router).filter_by(fqdn=fqdn).one() + + def _find_available_physical(): + for ifc in router.physical: + if not ifc.lag: + return ifc.name + raise HTTPException( + status_code=404, + detail=f'no available physical ports for "{lag_name}"') + + return { + 'fqdn': fqdn, + 'lag': lag_name, + 'name': _find_available_physical() + } diff --git a/test/conftest.py b/test/conftest.py index f768395899ac0096c64c1e16eda57a4dbbedc284..07d1714ac144a4298437e6d46860ab3e8117af0f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -12,7 +12,10 @@ from ncclient.manager import Manager, make_device_handler from ncclient.transport import SSHSession from ncclient.xml_ import NCElement +from fastapi.testclient import TestClient + from resource_management.db import model +import resource_management RPC_DATA_DIR = os.path.join(os.path.dirname(__file__), 'rpc-data') HOSTNAMES = [ @@ -76,6 +79,11 @@ HOSTNAMES = [ def resources_db(): # cf. https://stackoverflow.com/a/33057675 + # engine = create_engine( + # 'sqlite://', + # connect_args={'check_same_thread': False}, + # poolclass=StaticPool, + # echo=False) engine = create_engine( 'sqlite://', connect_args={'check_same_thread': False}, @@ -86,8 +94,9 @@ def resources_db(): with patch( 'resource_management.db._SESSION_MAKER', - sessionmaker(bind=engine)): - yield # wait until caller context ends + new=sessionmaker(bind=engine)): + with patch('resource_management.db.init_db_model'): + yield # wait until caller context ends class _namespace(object): @@ -101,12 +110,7 @@ def pytest_generate_tests(metafunc): @pytest.fixture def config_data(router_name): return { - 'mysql': { - 'username': 'bogus-user', - 'password': 'bogus-password', - 'hostname': 'bogus hostname', - 'dbname': 'bogus database name' - }, + 'db': 'sqlite://', 'ssh': { 'username': 'another-bogus-user', 'private-key': 'bogus private key filename' @@ -115,19 +119,12 @@ def config_data(router_name): @pytest.fixture -def config_filename(config_data, router_name): - # for os indepence see: - # https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file - # Generate a random temporary file name - file_name = os.path.join(tempfile.gettempdir(), os.urandom(24).hex()) - # Ensure the file is created - open(file_name, "x").close() - # Open the file in the given mode - tempFile = open(file_name, 'w') - - tempFile .write(json.dumps(config_data)) - tempFile .flush() - yield tempFile .name +def config_file(config_data, router_name): + with tempfile.NamedTemporaryFile(mode='w') as f: + f.write(json.dumps(config_data)) + f.flush() + os.environ['SETTINGS_FILENAME'] = f.name + yield f.name @pytest.fixture @@ -195,3 +192,9 @@ def mocked_router(netconf_rpc_replies): # it needs to be here ... with patch('jnpr.junos.device.Device.close'): yield # wait here until parent context ends + + +@pytest.fixture +def client(config_file): + app = resource_management.create_app() + yield TestClient(app) # wait here until calling context ends diff --git a/test/test_cli.py b/test/test_cli.py deleted file mode 100644 index d3f8ca9b247bcc3e2b5752f29a5d64fffbd14c31..0000000000000000000000000000000000000000 --- a/test/test_cli.py +++ /dev/null @@ -1,16 +0,0 @@ -from unittest.mock import patch -from click.testing import CliRunner -from resource_management.cli import cli - - -@patch('resource_management.db.init_db_model') -@patch('resource_management.cli.load_router_ports') -@patch('resource_management.cli._save_router_info') -def test_cli_happy_flow( - _unused1, _unused2, _unused3, - router_name, config_filename): - runner = CliRunner() - result = runner.invoke(cli, [ - '--config', config_filename, - '--fqdn', router_name]) - assert result.exit_code == 0 diff --git a/test/test_config.py b/test/test_config.py index 72ac321ed2878404399e53a90756fe7fd1f79a1f..2c89593260c32adfb0d5b84eaf4b0e44bc5af65c 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -4,15 +4,13 @@ import json from resource_management import config -def test_config_with_file(config_filename): - # trivial sanity check on the config fixture - with open(config_filename) as f: - params = config.load(f) - assert params +def test_config_with_file(config_file): + params = config.load() + assert params def test_config(config_data): with io.StringIO(json.dumps(config_data)) as f: f.seek(0) # rewind file position to the beginning - params = config.load(f) + params = config.load_from_file(f) assert params diff --git a/test/test_db_model.py b/test/test_db_model.py deleted file mode 100644 index 7f9138b880423d435510e79b5df0b38872381ff1..0000000000000000000000000000000000000000 --- a/test/test_db_model.py +++ /dev/null @@ -1,72 +0,0 @@ -from resource_management.db import model -from resource_management.db import session_scope -from resource_management.cli import _save_router_info -from resource_management.hardware.juniper import load_router_ports - -# -# def test_model_sanity_check(resources_db, router_name): -# -# test_node = [randint(1, 50) for _ in range(randint(1, 50))] -# -# with session_scope() as session: -# -# node = model.Node(fqdn=router_name) -# session.add(node) -# -# for position, num_ports in enumerate(test_node): -# line_card = model.LineCard( -# model=f'XYZ-{position}', position=position, node=node) -# session.add(line_card) -# for idx in range(num_ports): -# port = model.Port( -# name=str(idx), line_card=line_card, speed=1000) -# session.add(port) -# -# # new session - previous rows should have been committed -# with session_scope() as session: -# node = session.query(model.Node) \ -# .filter(model.Node.fqdn == router_name).one() -# assert len(node.line_cards) == len(test_node) -# for expected_num_ports, line_card in zip(test_node, node.line_cards): -# assert expected_num_ports == len(line_card.ports) - - -def test_save_router_info(resources_db, router_name, mocked_router): - - ssh = { - 'username': 'bogus', - 'private-key': 'no file' - } - - fpcs = list(load_router_ports(hostname=router_name, ssh_config=ssh)) - _save_router_info(fqdn=router_name, fpcs=fpcs) - - fpc_slot_dict = {f['slot']: f['ports'] for f in fpcs} - - with session_scope() as session: - node = session.query(model.Node) \ - .filter(model.Node.fqdn == router_name).one() - expected_fpc_slots = set(fpc_slot_dict.keys()) - - fpc_slots = {fpc.slot for fpc in node.line_cards} - assert expected_fpc_slots == fpc_slots - - for fpc in node.line_cards: - expected_ports = { - (p['pic'], p['position']) for p in fpc_slot_dict[fpc.slot]} - db_ports = {(p.pic, p.position) for p in fpc.ports} - assert expected_ports == db_ports - - -# TODO: do this in a tmp container ... -# from resource_management.db import mysql_dsn -# from resource_management.migrations import migration_utils -# -# -# def test_migration(): -# dsn = mysql_dsn( -# username='dummy', -# password='dummy-pass', -# hostname='localhost', -# db_name='resources_test') -# migration_utils.upgrade(dsn=dsn) diff --git a/test/test_interfaces_routes.py b/test/test_interfaces_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..6ffc443aae121e94605d38562f41f650d7fc8a76 --- /dev/null +++ b/test/test_interfaces_routes.py @@ -0,0 +1,41 @@ +import jsonschema + +from resource_management.routes.default import Version +from resource_management.routes import interfaces +from resource_management import router_interfaces + + +def test_bad_method(client): + rv = client.post('/api/version') + assert rv.status_code == 405 + + +def test_version_request(client): + rv = client.get('/api/version') + assert rv.status_code == 200 + jsonschema.validate(rv.json(), Version.schema()) + + +def test_update_router_interfaces( + client, resources_db, mocked_router, router_name): + rv = client.post(f'/api/interfaces/import/{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) + + rv = client.post(f'/api/interfaces/next-lag/{router_name}') + assert rv.status_code == 200 + jsonschema.validate(rv.json(), interfaces.NextLAG.schema()) + + +def test_next_physical(client, resources_db, mocked_router, router_name): + + router_interfaces.load_router_interfaces(router_name) + + rv = client.post(f'/api/interfaces/next-physical/{router_name}/ae123123') + assert rv.status_code == 200 + jsonschema.validate(rv.json(), interfaces.NextPhysicalInterface.schema()) diff --git a/test/test_parse_router.py b/test/test_parse_router.py index ca4ae63cc27e592b82b3de86855eb5247bdf14b9..e6723d5f454de67d3a1b29c8d25266b17725ae41 100644 --- a/test/test_parse_router.py +++ b/test/test_parse_router.py @@ -3,7 +3,9 @@ import re from jsonschema import validate from lxml import etree -from resource_management.hardware import juniper +from resource_management import router_interfaces, juniper +from resource_management import db +from resource_management.db import model def _remove_ns(xml): @@ -47,9 +49,9 @@ def test_router_ports(mocked_router, netconf_rpc_replies): == {f['slot'] for f in fpcs} -def test_new_load_interfaces(mocked_router, netconf_rpc_replies): +def test_no_db_load_interfaces(mocked_router): """ - renewed design for this app: no chassis info for now + new design for this app: no chassis info for now """ ssh = { @@ -67,9 +69,26 @@ def test_new_load_interfaces(mocked_router, netconf_rpc_replies): # print(len(config)) # just sanity check there are non-empty responses - physical = list(juniper.load_installed_ethernet_ports(dev)) + physical = juniper.load_installed_ethernet_ports(dev) assert physical logical = juniper.load_logical_interfaces(dev) assert logical aggregates = juniper.load_aggregates(dev) assert aggregates + + +def test_load_interfaces( + mocked_router, resources_db, config_file, router_name): + """ + new design for this app: no chassis info for now + """ + + router_interfaces.load_router_interfaces(router_name) + + with db.session_scope() as session: + router = session.query(model.Router).filter_by(fqdn=router_name).one() + assert router + assert router.physical + assert router.lags + assert all(_l.physical for _l in router.lags) + assert any(_p.logical for _p in router.physical)