9 Commits

Author SHA1 Message Date
8b3584fa9e Use server reference as inventory key and add ansible_host var
All checks were successful
Build and Release / Build Windows Exe (push) Successful in 10s
2026-02-06 15:51:30 -05:00
a202e267f7 Capture Production IP from Servers tab and use for flow matching
All checks were successful
Build and Release / Build Windows Exe (push) Successful in 11s
2026-02-06 15:43:06 -05:00
2ccf6c293a Implement to_mgt_ip DNS logic and update port range parsing
All checks were successful
Build and Release / Build Windows Exe (push) Successful in 10s
2026-02-06 15:40:13 -05:00
b6266bea81 Use docker action with entrypoint override
All checks were successful
Build and Release / Build Windows Exe (push) Successful in 11s
2026-02-06 15:28:47 -05:00
284e6b1fbf Set working directory in docker container
Some checks failed
Build and Release / Build Windows Exe (push) Failing after 4s
2026-02-06 15:26:24 -05:00
b3c5f3a6fd Use manual docker run with entrypoint override
Some checks failed
Build and Release / Build Windows Exe (push) Failing after 6s
2026-02-06 15:24:45 -05:00
2634c87dcd Refactor workflow to use container job structure
Some checks failed
Build and Release / Build Windows Exe (push) Failing after 3s
2026-02-06 15:23:22 -05:00
f28af4de7a Fix PyInstaller args and artifact glob
Some checks failed
Build and Release / Build Windows Exe (push) Failing after 3s
2026-02-06 15:21:10 -05:00
7e01ec7073 Fix release artifact path
All checks were successful
Build and Release / Build Windows Exe (push) Successful in 4s
2026-02-06 15:18:36 -05:00
6 changed files with 111 additions and 34 deletions

View File

@@ -9,14 +9,16 @@ jobs:
build:
name: Build Windows Exe
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build with PyInstaller (Wine)
- name: Build with PyInstaller
uses: docker://cdrx/pyinstaller-windows:python3
with:
args: "python -m pip install -r requirements.txt && pyinstaller --onefile --clean --name wif2ansible run.py"
entrypoint: /bin/sh
args: -c "python -m pip install -r requirements.txt && pyinstaller --onefile --clean --name wif2ansible run.py"
- name: Generate Version Tag
id: version
@@ -27,7 +29,7 @@ jobs:
with:
tag_name: ${{ steps.version.outputs.TAG }}
name: Release ${{ steps.version.outputs.TAG }}
files: dist/windows/wif2ansible.exe
files: dist/**/*.exe
draft: false
prerelease: false
env:

View File

@@ -77,9 +77,8 @@ def read_servers(filename: str) -> Dict[str, Server]:
print("Warning: No 'Servers' sheet found.")
return {}
# keywords: reference, platform, ip address, management ip?
# Ruby script looked for: reference, type, alias, platform, middleware
header_keywords = ['reference', 'platform', 'ip address']
# keywords: reference, platform, ip address, management ip, production ip
header_keywords = ['reference', 'platform', 'ip address', 'production ip']
header_row_idx, col_map = find_header_row(target_sheet, header_keywords)
@@ -98,7 +97,8 @@ def read_servers(filename: str) -> Dict[str, Server]:
# Extract data
ref_idx = col_map.get('reference')
plat_idx = col_map.get('platform')
ip_idx = col_map.get('ip address') # Generic IP
ip_idx = col_map.get('ip address') # Generic/Management IP
prod_ip_idx = col_map.get('production ip') # Specific Production IP
# Helper to get value
def get_val(idx):
@@ -111,19 +111,29 @@ def read_servers(filename: str) -> Dict[str, Server]:
continue
plat = get_val(plat_idx) or 'unknown'
ip_raw = get_val(ip_idx)
# Parse Management IP
ip_raw = get_val(ip_idx)
ip_addr = None
if ip_raw:
ips = parse_ip(ip_raw)
if ips:
ip_addr = ips[0] # Take first valid IP
ip_addr = ips[0]
# Parse Production IP
prod_ip_raw = get_val(prod_ip_idx)
prod_ip_addr = None
if prod_ip_raw:
ips = parse_ip(prod_ip_raw)
if ips:
prod_ip_addr = ips[0]
s = Server(
reference=ref,
hostname=ref, # Default hostname to reference
hostname=ref,
platform=plat,
ip_address=ip_addr
ip_address=ip_addr,
production_ip=prod_ip_addr
)
servers[ref] = s

View File

@@ -1,5 +1,6 @@
from typing import List, Dict, Any
from .models import Server, Flow
from .network import to_mgt_ip
def generate_inventory(servers: Dict[str, Server], flows: List[Flow]) -> Dict[str, Any]:
"""
@@ -19,9 +20,13 @@ def generate_inventory(servers: Dict[str, Server], flows: List[Flow]) -> Dict[st
for s in servers.values():
if s.ip_address:
ip_to_server[s.ip_address] = s
# also index by hostname/reference potentially?
# ip_to_server[s.reference] = s
# But flows ususally have IPs.
if s.production_ip:
ip_to_server[s.production_ip] = s
# Also index by reference/hostname for DNS matches
if s.reference:
ip_to_server[s.reference.lower()] = s
if s.hostname:
ip_to_server[s.hostname.lower()] = s
inventory_hosts = {}
@@ -34,18 +39,30 @@ def generate_inventory(servers: Dict[str, Server], flows: List[Flow]) -> Dict[st
server = ip_to_server.get(flow.source_ip)
if not server:
# Try finding by looking if source matches any server's reference/hostname?
# Unlikely for IPs.
drop_count += 1
if drop_count <= 5: # Debug spam limit
print(f"Dropping flow {flow.flow_id}: Source {flow.source_ip} not found in Servers tab.")
continue
# Try DNS resolution (Public IP -> Management FQDN)
mgt_dns = to_mgt_ip(flow.source_ip)
if mgt_dns:
# mgt_dns might be "server.ds.gc.ca".
# Our keys might be "server" or "server.ds.gc.ca" or IPs
# Try exact match
server = ip_to_server.get(mgt_dns.lower())
# If not found, try shortname?
if not server:
short = mgt_dns.split('.')[0]
server = ip_to_server.get(short.lower())
if not server:
drop_count += 1
if drop_count <= 5: # Debug spam limit
print(f"Dropping flow {flow.flow_id}: Source {flow.source_ip} (Mgt: {mgt_dns}) not found in Servers tab.")
continue
match_count += 1
# Prepare host entry if new
# We use the IP as the key in inventory 'hosts'
host_key = server.ip_address
# We use the Reference/Hostname as the key in inventory 'hosts'
host_key = server.reference or server.hostname or server.ip_address
if host_key not in inventory_hosts:
host_vars = server.get_ansible_vars()

View File

@@ -6,6 +6,7 @@ class Server:
reference: str
hostname: str # This might be same as reference
ip_address: Optional[str] = None
production_ip: Optional[str] = None
platform: str = 'unknown' # e.g. 'Windows', 'Linux'
def get_ansible_vars(self) -> Dict[str, Any]:
@@ -22,6 +23,9 @@ class Server:
# Default ssh is usually fine, but being explicit doesn't hurt
pass
if self.ip_address:
vars['ansible_host'] = self.ip_address
return vars
@dataclass

50
wif2ansible/network.py Normal file
View File

@@ -0,0 +1,50 @@
import socket
from typing import Optional
def get_hostname(ip: str) -> Optional[str]:
try:
# Python's equivalent to Resolv.getname(ip)
# returns (hostname, aliaslist, ipaddrlist)
return socket.gethostbyaddr(ip)[0]
except socket.error:
return None
def get_ip(hostname: str) -> Optional[str]:
try:
return socket.gethostbyname(hostname)
except socket.error:
return None
def to_mgt_ip(name_or_ip: str) -> Optional[str]:
"""
Mimics the Ruby script's to_mgt_ip logic:
1. Reverse lookup IP to get FQDN.
2. Construct management FQDN ({host}.ds.gc.ca or .pre-ds.gc.ca).
3. Resolve that management FQDN to an IP.
4. Return the Management FQDN if successful.
"""
# In Ruby script, input 'name' is often an IP address from the WIF source column.
# Step 1: Reverse Lookup
fqdn = get_hostname(name_or_ip)
if not fqdn:
# If input is already a name, use it? Ruby script assumes it gets a name from Resolv.getname(ip)
# If name_or_ip is NOT an IP, gethostbyaddr might fail or behave differently.
# But if it's already a name, we can try using it.
fqdn = name_or_ip
short_name = fqdn.split('.')[0]
# Step 2 & 3: Try suffixes
suffixes = ['.ds.gc.ca', '.pre-ds.gc.ca']
for suffix in suffixes:
mgt_dns = short_name + suffix
resolved_ip = get_ip(mgt_dns)
if resolved_ip:
# Ruby: return mgt_dns if mgt_ip.to_s.length > 4
return mgt_dns
# print(f"Warning: {name_or_ip} could not be resolved to a management address.")
return None

View File

@@ -43,18 +43,12 @@ def parse_ports(port_str: str) -> List[int]:
if range_match:
start, end = map(int, range_match.groups())
if start <= end:
# Limitation: adding huge ranges might blow up inventory size
# but for Ansible 'ports' list it's better to be explicit or use range syntax.
# For now, let's keep it expanded if small, or maybe just keeps the start/end?
# Ruby script logic: expanded it.
# We'll limit expansion to avoid DOSing ourselves.
if end - start < 1000:
ports.update(range(start, end + 1))
else:
# Fallback: just add start and end to avoid massive lists?
# Or maybe ansible allows ranges?
# Usually we list ports. Let's expand for now.
ports.update(range(start, end + 1))
# User Request: "only add the first, last, and middle port"
ports.add(start)
ports.add(end)
if end - start > 1:
middle = start + (end - start) // 2
ports.add(middle)
continue
# Single port