Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file.
### Code Improvements

* **Data refresh logging**: CLI now shows specific reason for data refresh ("data is empty" vs "data is outdated") instead of generic "empty or outdated" message
* **AS name display**: ASN names are now displayed using a preferred source hierarchy:
* Priority order: PeeringDB `aka` → PeeringDB `name_long` → PeeringDB `name` → AS2Org `org_name` → AS2Org `name` → Core `name`
* This provides more recognizable, commonly-used AS names from PeeringDB when available
* Affects all commands that display AS names: `inspect`, `as2rel`, `rpki`, `pfx2as`

### Breaking Changes

Expand Down
68 changes: 62 additions & 6 deletions src/database/monocle/as2rel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,16 @@ impl<'a> As2relRepository<'a> {
SELECT
:asn as asn1,
CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END as asn2,
o.org_name as asn2_name,
COALESCE(
NULLIF(p.aka, ''),
NULLIF(p.name_long, ''),
NULLIF(p.name, ''),
NULLIF(ai.org_name, ''),
NULLIF(ai.name, ''),
NULLIF(o.org_name, ''),
NULLIF(o.as_name, ''),
c.name
) as asn2_name,
MAX(CASE WHEN r.rel = 0 THEN r.peers_count ELSE 0 END) as connected_count,
SUM(CASE
WHEN r.asn1 = :asn AND r.rel = 1 THEN r.peers_count
Expand All @@ -242,7 +251,14 @@ impl<'a> As2relRepository<'a> {
ELSE 0
END) as as2_upstream_count
FROM as2rel r
LEFT JOIN as2org_all o ON o.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END
LEFT JOIN asinfo_core c
ON c.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END
LEFT JOIN asinfo_as2org ai
ON ai.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END
LEFT JOIN asinfo_peeringdb p
ON p.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END
LEFT JOIN as2org_all o
ON o.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END
WHERE r.asn1 = :asn OR r.asn2 = :asn
GROUP BY CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END
"#;
Expand Down Expand Up @@ -277,7 +293,16 @@ impl<'a> As2relRepository<'a> {
SELECT
:asn1 as asn1,
:asn2 as asn2,
o.org_name as asn2_name,
COALESCE(
NULLIF(p.aka, ''),
NULLIF(p.name_long, ''),
NULLIF(p.name, ''),
NULLIF(ai.org_name, ''),
NULLIF(ai.name, ''),
NULLIF(o.org_name, ''),
NULLIF(o.as_name, ''),
c.name
) as asn2_name,
MAX(CASE WHEN r.rel = 0 THEN r.peers_count ELSE 0 END) as connected_count,
SUM(CASE
WHEN r.asn1 = :asn1 AND r.rel = 1 THEN r.peers_count
Expand All @@ -290,6 +315,9 @@ impl<'a> As2relRepository<'a> {
ELSE 0
END) as as2_upstream_count
FROM as2rel r
LEFT JOIN asinfo_core c ON c.asn = :asn2
LEFT JOIN asinfo_as2org ai ON ai.asn = :asn2
LEFT JOIN asinfo_peeringdb p ON p.asn = :asn2
LEFT JOIN as2org_all o ON o.asn = :asn2
WHERE (r.asn1 = :asn1 AND r.asn2 = :asn2) OR (r.asn1 = :asn2 AND r.asn2 = :asn1)
"#;
Expand Down Expand Up @@ -618,9 +646,21 @@ impl<'a> As2relRepository<'a> {
SELECT
h.customer_asn,
h.visibility,
o.org_name
COALESCE(
NULLIF(p.aka, ''),
NULLIF(p.name_long, ''),
NULLIF(p.name, ''),
NULLIF(ai.org_name, ''),
NULLIF(ai.name, ''),
NULLIF(o.org_name, ''),
NULLIF(o.as_name, ''),
c.name
) as asn_name
FROM has_target_upstream h
JOIN upstream_counts u ON h.customer_asn = u.customer_asn
LEFT JOIN asinfo_core c ON c.asn = h.customer_asn
LEFT JOIN asinfo_as2org ai ON ai.asn = h.customer_asn
LEFT JOIN asinfo_peeringdb p ON p.asn = h.customer_asn
LEFT JOIN as2org_all o ON o.asn = h.customer_asn
WHERE u.upstream_count = 1
AND h.visibility >= :min_peers
Expand Down Expand Up @@ -855,7 +895,16 @@ impl<'a> As2relRepository<'a> {
SELECT
CASE WHEN r.asn1 < r.asn2 THEN r.asn1 ELSE r.asn2 END as asn1,
CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END as asn2,
o.org_name as asn2_name,
COALESCE(
NULLIF(p.aka, ''),
NULLIF(p.name_long, ''),
NULLIF(p.name, ''),
NULLIF(ai.org_name, ''),
NULLIF(ai.name, ''),
NULLIF(o.org_name, ''),
NULLIF(o.as_name, ''),
c.name
) as asn2_name,
MAX(CASE WHEN r.rel = 0 THEN r.peers_count ELSE 0 END) as connected_count,
SUM(CASE
WHEN r.asn1 < r.asn2 AND r.rel = 1 THEN r.peers_count
Expand All @@ -868,7 +917,14 @@ impl<'a> As2relRepository<'a> {
ELSE 0
END) as as2_upstream_count
FROM as2rel r
LEFT JOIN as2org_all o ON o.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END
LEFT JOIN asinfo_core c
ON c.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END
LEFT JOIN asinfo_as2org ai
ON ai.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END
LEFT JOIN asinfo_peeringdb p
ON p.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END
LEFT JOIN as2org_all o
ON o.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END
WHERE {}
GROUP BY
CASE WHEN r.asn1 < r.asn2 THEN r.asn1 ELSE r.asn2 END,
Expand Down
150 changes: 150 additions & 0 deletions src/database/monocle/asinfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,58 @@ impl<'a> AsinfoRepository<'a> {
result
}

/// Batch lookup of preferred AS names with source preference:
/// peeringdb.name_long/name -> as2org.org_name -> as2org.name -> core.name
pub fn lookup_preferred_names_batch(&self, asns: &[u32]) -> HashMap<u32, String> {
let mut result = HashMap::new();

if asns.is_empty() {
return result;
}

let placeholders: Vec<String> = asns.iter().map(|_| "?".to_string()).collect();
let query = format!(
r#"
SELECT
c.asn,
COALESCE(
NULLIF(p.aka, ''),
NULLIF(p.name_long, ''),
NULLIF(p.name, ''),
NULLIF(a.org_name, ''),
NULLIF(a.name, ''),
c.name
) AS preferred_name
FROM asinfo_core c
LEFT JOIN asinfo_as2org a ON c.asn = a.asn
LEFT JOIN asinfo_peeringdb p ON c.asn = p.asn
WHERE c.asn IN ({})
"#,
placeholders.join(",")
);

if let Ok(mut stmt) = self.conn.prepare(&query) {
let params: Vec<&dyn rusqlite::ToSql> =
asns.iter().map(|a| a as &dyn rusqlite::ToSql).collect();

if let Ok(rows) = stmt.query_map(params.as_slice(), |row| {
Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?))
}) {
for row in rows.flatten() {
result.insert(row.0, row.1);
}
}
}

result
}

/// Lookup preferred AS name for a single ASN
pub fn lookup_preferred_name(&self, asn: u32) -> Option<String> {
let mut result = self.lookup_preferred_names_batch(&[asn]);
result.remove(&asn)
}

/// Batch lookup of org names (from as2org table)
pub fn lookup_orgs_batch(&self, asns: &[u32]) -> HashMap<u32, String> {
let mut result = HashMap::new();
Expand Down Expand Up @@ -1026,6 +1078,104 @@ mod tests {
assert!(names.get(&99999).is_none());
}

#[test]
fn test_lookup_preferred_names_batch() {
let db = setup_test_db();
let repo = AsinfoRepository::new(&db.conn);

let records = vec![
JsonlRecord {
asn: 1,
name: "CORE1".to_string(),
country: "US".to_string(),
as2org: Some(JsonlAs2org {
country: "US".to_string(),
name: "AS2ORG_NAME1".to_string(),
org_id: "ORG1".to_string(),
org_name: "AS2ORG_ORG1".to_string(),
}),
peeringdb: Some(JsonlPeeringdb {
aka: Some("PDB1_AKA".to_string()),
asn: 1,
irr_as_set: None,
name: "PDB1".to_string(),
name_long: Some("PDB1_LONG".to_string()),
website: None,
}),
hegemony: None,
population: None,
},
JsonlRecord {
asn: 2,
name: "CORE2".to_string(),
country: "US".to_string(),
as2org: Some(JsonlAs2org {
country: "US".to_string(),
name: "AS2ORG_NAME2".to_string(),
org_id: "ORG2".to_string(),
org_name: "AS2ORG_ORG2".to_string(),
}),
peeringdb: Some(JsonlPeeringdb {
aka: None,
asn: 2,
irr_as_set: None,
name: "PDB2".to_string(),
name_long: None,
website: None,
}),
hegemony: None,
population: None,
},
JsonlRecord {
asn: 3,
name: "CORE3".to_string(),
country: "US".to_string(),
as2org: Some(JsonlAs2org {
country: "US".to_string(),
name: "AS2ORG_NAME3".to_string(),
org_id: "ORG3".to_string(),
org_name: "AS2ORG_ORG3".to_string(),
}),
peeringdb: None,
hegemony: None,
population: None,
},
JsonlRecord {
asn: 4,
name: "CORE4".to_string(),
country: "US".to_string(),
as2org: Some(JsonlAs2org {
country: "US".to_string(),
name: "AS2ORG_NAME4".to_string(),
org_id: "ORG4".to_string(),
org_name: "".to_string(),
}),
peeringdb: None,
hegemony: None,
population: None,
},
JsonlRecord {
asn: 5,
name: "CORE5".to_string(),
country: "US".to_string(),
as2org: None,
peeringdb: None,
hegemony: None,
population: None,
},
];

repo.store_from_jsonl(&records, "test://source").unwrap();

let names = repo.lookup_preferred_names_batch(&[1, 2, 3, 4, 5, 99999]);
assert_eq!(names.get(&1), Some(&"PDB1_AKA".to_string()));
assert_eq!(names.get(&2), Some(&"PDB2".to_string()));
assert_eq!(names.get(&3), Some(&"AS2ORG_ORG3".to_string()));
assert_eq!(names.get(&4), Some(&"AS2ORG_NAME4".to_string()));
assert_eq!(names.get(&5), Some(&"CORE5".to_string()));
assert!(names.get(&99999).is_none());
}

#[test]
fn test_clear() {
let db = setup_test_db();
Expand Down
Loading