Skip to content

Commit

Permalink
feat(irrexplorer-cli): init
Browse files Browse the repository at this point in the history
  • Loading branch information
kiraum committed Dec 21, 2024
1 parent 3f390db commit 7eff755
Show file tree
Hide file tree
Showing 23 changed files with 1,725 additions and 1 deletion.
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
38 changes: 38 additions & 0 deletions .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Publish to PyPI

on:
push:
tags:
- 'v*'

jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install UV and build tools
run: |
pip install --upgrade uv
uv pip install --system --break-system-packages build
- name: Verify CHANGELOG
run: |
if ! grep -q "## \[$(git describe --tags --abbrev=0 | sed 's/v//')\]" CHANGELOG.md; then
echo "CHANGELOG.md not updated for this version"
exit 1
fi
- name: Build package
run: uv build

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
30 changes: 30 additions & 0 deletions .github/workflows/linter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Test (linter/formatter/coverage)

on: [push]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.13"]

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
pip install --upgrade uv
uv pip sync --system --break-system-packages requirements.lock
- name: Install package
run: |
pip install -e .
- name: Run all linters and formatters
run: |
pre-commit run --all-files
52 changes: 52 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 'v5.0.0'
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-toml
- id: check-added-large-files

- repo: local
hooks:
- id: isort
name: isort
entry: isort
language: system
types: [python]

- id: black
name: black
entry: black
language: python
types_or: [python, pyi]

- id: ruff
name: ruff
entry: ruff check
language: python
types_or: [python, pyi]

- id: mypy
name: mypy
entry: mypy
language: python
types_or: [python, pyi]
require_serial: true

- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
args:
- --rcfile=pyproject.toml

- id: pytest
name: pytest
entry: pytest
language: system
types: [python]
pass_filenames: false
args: ['--cov=irrexplorer_cli', '--cov-fail-under=100', 'tests/']
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Changelog
All notable changes to irrexplorer-cli will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.2] - 2024-12-21
### Added
- Long description from README.md for PyPI package
- Proper package metadata

[0.0.2]: https://github.com/kiraum/irrexplorer-cli/releases/tag/v0.0.2

## [0.0.1] - 2024-12-21
### Added
- Initial development release
- Command-line interface for IRRexplorer.net queries
- Prefix information lookup functionality
- ASN details lookup functionality
- Multiple output formats (json, csv, text)
- Async support for efficient data retrieval
- Integration with IRRexplorer v2 service
- Support for Python 3.13+
- Documentation and usage examples

### Dependencies
- httpx for HTTP requests
- typer for CLI interface
- rich for text formatting

[0.0.1]: https://github.com/kiraum/irrexplorer-cli/releases/tag/v0.0.1
91 changes: 90 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,90 @@
# irrexplorer-cli
# IRRexplorer CLI

A command-line interface to query and explore IRR & BGP data from IRRexplorer.net in real-time.

## Overview

IRRexplorer CLI provides a simple way to access and analyze Internet Routing Registry (IRR) and BGP data through the command line. It interfaces with the IRRexplorer v2 service to help network operators and administrators debug routing data and verify filtering strategies.

## Features

- Query prefix information
- Lookup ASN details
- Real-time data access from IRRexplorer.net
- Easy-to-use command-line interface
- Async support for efficient data retrieval

## Installation

```bash
pip install irrexplorer-cli
```

## Usage

Query Prefix Information
```bash
irrexplorer prefix 192.0.2.0/24
```

Query ASN Information
```bash
irrexplorer asn AS64496
```

The `-f` or `--format` flag allows you to specify the output format:

* `json`: Output results in JSON format
* `csv`: Output results in CSV format
* Default format is human-readable text

## Requirements

* Python 3.13+
* httpx
* typer
* rich

## Development

1. Clone the repository:
```bash
git clone https://github.com/kiraum/irrexplorer-cli.git
```

2. Create/activate venv:
```bash
python3 -m venv venv
. venv/bin/activate
```

3. Install dependencies:
```bash
pip install --upgrade uv
uv pip sync requirements.lock
```

4. Run pre-commit tests before to push:
```bash
pre-commit run --all-files
```

## Data Sources

The CLI tool queries data from IRRexplorer.net, which includes:

* IRR objects and relations (route(6) and as-sets)
* RPKI ROAs and validation status
* BGP origins from DFZ
* RIRstats

## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.

## License

See [LICENSE](LICENSE) file for details.

## Credits

This tool interfaces with IRRexplorer v2, a project maintained by Stichting NLNOG and DashCare BV.
Empty file added irrexplorer_cli/__init__.py
Empty file.
123 changes: 123 additions & 0 deletions irrexplorer_cli/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""Helper functions for the CLI."""

import ipaddress
import re
from typing import Any, Dict, List

from irrexplorer_cli.models import PrefixInfo, PrefixResult


def validate_prefix_format(prefix_input: str) -> bool:
"""Validate IPv4 or IPv6 prefix format."""
try:
ipaddress.ip_network(prefix_input)
return True
except ValueError:
return False


def validate_asn_format(asn_input: str) -> bool:
"""Validate ASN format."""
asn_pattern = r"^(?:AS|as)?(\d+)$"
match = re.match(asn_pattern, asn_input)
if match:
asn_number = int(match.group(1))
return 0 <= asn_number <= 4294967295
return False


def format_prefix_result(result: PrefixInfo, prefix_type: str) -> str:
"""Format a single prefix result for CSV output."""
prefix_result = PrefixResult(
prefix=result.prefix,
categoryOverall=result.categoryOverall,
rir=result.rir,
rpkiRoutes=result.rpkiRoutes,
bgpOrigins=result.bgpOrigins,
irrRoutes=result.irrRoutes,
messages=result.messages,
)

rpki_status = "NOT_FOUND"
if prefix_result.rpkiRoutes:
rpki_status = prefix_result.rpkiRoutes[0].rpkiStatus

bgp_origins = "|".join(str(asn) for asn in prefix_result.bgpOrigins)

irr_routes = []
for db, routes in prefix_result.irrRoutes.items():
for route in routes:
irr_routes.append(f"{db}:AS{route.asn}:{route.rpkiStatus}")
irr_routes_str = "|".join(irr_routes)

messages = "|".join(msg.text for msg in prefix_result.messages)

return (
f"{prefix_type},{prefix_result.prefix},{prefix_result.categoryOverall},"
f"{prefix_result.rir},{rpki_status},{bgp_origins},{irr_routes_str},{messages}"
)


def format_direct_origins(as_number: str, results: Dict[str, List[Dict[str, Any]]]) -> None:
"""Format and print direct origin prefixes."""
for pfx_dict in results.get("directOrigin", []):
pfx = PrefixInfo(**pfx_dict)
rpki_status = "NOT_FOUND"
if pfx.rpkiRoutes:
rpki_status = pfx.rpkiRoutes[0].rpkiStatus

bgp_origins = "|".join(str(as_number) for as_number in pfx.bgpOrigins)
irr_routes = []
for db, routes in pfx.irrRoutes.items():
for route in routes:
irr_routes.append(f"{db}:AS{route.asn}:{route.rpkiStatus}")
irr_routes_str = "|".join(irr_routes)
messages = "|".join(msg.text for msg in pfx.messages)

print(
f"\nDIRECT,{as_number},{pfx.prefix},{pfx.categoryOverall},{pfx.rir},"
f"{rpki_status},{bgp_origins},{irr_routes_str},{messages}",
end="",
)


def format_overlapping_prefixes(as_number: str, results: Dict[str, List[Dict[str, Any]]]) -> None:
"""Format and print overlapping prefixes."""
for pfx_dict in results.get("overlaps", []):
pfx = PrefixInfo(**pfx_dict)
rpki_status = "NOT_FOUND"
if pfx.rpkiRoutes:
rpki_status = pfx.rpkiRoutes[0].rpkiStatus

bgp_origins = "|".join(str(as_number) for as_number in pfx.bgpOrigins)
irr_routes = []
for db, routes in pfx.irrRoutes.items():
for route in routes:
irr_routes.append(f"{db}:AS{route.asn}:{route.rpkiStatus}")
irr_routes_str = "|".join(irr_routes)
messages = "|".join(msg.text for msg in pfx.messages)

print(
f"\nOVERLAP,{as_number},{pfx.prefix},{pfx.categoryOverall},{pfx.rir},"
f"{rpki_status},{bgp_origins},{irr_routes_str},{messages}",
end="",
)


def format_as_sets(as_number: str, sets_data: Dict[str, Dict[str, List[str]]]) -> None:
"""Format and print AS sets."""
if sets_data and sets_data.get("setsPerIrr"):
for irr, sets in sets_data["setsPerIrr"].items():
for as_set in sets:
print(f"\nSET,{as_number},{as_set},{irr},N/A,N/A,N/A,N/A,N/A", end="")


async def find_least_specific_prefix(direct_overlaps: List[PrefixInfo]) -> str | None:
"""Find the least specific prefix from the overlaps."""
least_specific = None
for info in direct_overlaps:
if "/" in info.prefix:
_, mask = info.prefix.split("/")
if least_specific is None or int(mask) < int(least_specific.split("/")[1]):
least_specific = info.prefix
return least_specific
Loading

0 comments on commit 7eff755

Please sign in to comment.