Skip to content

Commit

Permalink
Fix linter warnings and add github workflows
Browse files Browse the repository at this point in the history
  • Loading branch information
Karlinde committed Mar 16, 2024
1 parent 86bfaf6 commit ba37045
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 29 deletions.
38 changes: 38 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Python package

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_call:

jobs:
build:

runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.8"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
python -m pip install .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude build
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=20 --max-line-length=127 --statistics --exclude build
- name: Test with pytest
run: |
pytest
38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Release Python Package on PyPI

on:
release:
types: [published]

permissions:
contents: read

jobs:
build-test:
uses: ./.github/workflows/python-package.yaml

pypi-publish:
name: Upload release to PyPI
runs-on: ubuntu-latest

needs: build-test

environment:
name: release
url: https://pypi.org/p/zonefilegen

permissions:
id-token: write

steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install build dependencies
run: python -m pip install --upgrade setuptools wheel build
- name: Build package
run: python -m build .
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.2]
### Fixed
- Fixed linter warnings

### Added
- Added github workflows

### Changed
- Consolidated package definition to pyproject.toml only

## [0.1.1]
### Fixed
- Added generation of missing NS records in reverse zones.
Expand Down
2 changes: 1 addition & 1 deletion src/zonefilegen/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.1"
__version__ = "0.1.1"
18 changes: 10 additions & 8 deletions src/zonefilegen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
import zonefilegen.parsing
import zonefilegen.core

# First line of each generated zone file should be a comment with the
# First line of each generated zone file should be a comment with the
# SHA-1 hex digest of the input toml file:
# ; Generated by zonefilegen, INPUT_SHA1: ea03443f2d9f8c580e73d2f8cd1016dc7174bddc
FIRST_LINE_PATTERN = re.compile(r'^\s*;.*INPUT_DIGEST:\s+(?P<digest>[0-9a-f]+)')
SOA_PATTERN = re.compile(r'^.+SOA[ \t]+(?P<mname>[\.\w]+)[ \t]+(?P<rname>[\.\w]+)[ \t]+\(\s*(?P<serial>[0-9]+)', re.MULTILINE)


def gen_zone(zone: zonefilegen.core.Zone, output_dir: pathlib.Path, soa_dict: dict, input_digest: str):
out_filepath: pathlib.Path = output_dir / f"{zone.name}zone"
logging.info(f"Generating zone file {out_filepath}")
Expand All @@ -26,22 +27,23 @@ def gen_zone(zone: zonefilegen.core.Zone, output_dir: pathlib.Path, soa_dict: di
if first_line_matches:
old_digest = first_line_matches.group('digest')
else:
logging.error(f"Existing zone file {out_filepath} was not generated by this tool. Aborting.")
logging.error(f"Existing zone file {out_filepath} was not generated by this tool. Aborting.")
exit(1)

if soa_matches:
old_serial = soa_matches.group('serial')
else:
logging.warning(f"Didn't find or recognize SOA record in existing zone file {out_filepath}. Serial number will be reset.")

logging.warning(f"Didn't find or recognize SOA record in existing zone file {out_filepath}. Serial number will"
" be reset.")

if old_serial and old_digest:
if old_digest != input_digest:
serial_number = (int(old_serial) + 1) % pow(2, 32)
serial_number = (int(old_serial) + 1) % pow(2, 32)
logging.info(f"Changes detected, updating serial to {serial_number}")
else:
serial_number = int(old_serial)
logging.info(f"No changes detected, serial remains at {serial_number}")

if serial_number is None:
serial_number = 1
soa_rec = zonefilegen.generation.build_soa_record(soa_dict, serial_number)
Expand All @@ -54,6 +56,7 @@ def gen_zone(zone: zonefilegen.core.Zone, output_dir: pathlib.Path, soa_dict: di
for rec in zone.records:
f.write(rec.to_line() + '\n')


def generate():
parser = argparse.ArgumentParser(description='Generate DNS zone files.')
parser.add_argument('input_file', type=pathlib.Path, help='Input file in TOML format')
Expand All @@ -67,8 +70,7 @@ def generate():
log_level = logging.DEBUG
elif args.verbose > 0:
log_level = logging.INFO
logging.basicConfig(format='%(levelname)s: %(message)s',level=log_level)

logging.basicConfig(format='%(levelname)s: %(message)s', level=log_level)

(fwd_zone, reverse_zones, soa_dict, input_digest) = zonefilegen.parsing.parse_toml_file(args.input_file)

Expand Down
9 changes: 4 additions & 5 deletions src/zonefilegen/core.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from typing import List

RECORD_CLASSES = [
'IN',
'CH',
'HS',
]

RECORD_TYPES = [
RECORD_TYPES = [
'A',
'NS',
'MD',
Expand Down Expand Up @@ -95,14 +93,15 @@
'DLV',
]


class ResourceRecord():
def __init__(self):
self.name = None
self.ttl = None
self.record_class = None
self.record_type = None
self.data = None

def to_line(self):
ttl_str = str(self.ttl) if self.ttl else ''
record_class_str = str(self.record_class) if self.record_class else ''
Expand All @@ -114,7 +113,7 @@ def __init__(self, name: str, default_ttl: int):
self.name = name
self.default_ttl = default_ttl
self.records = []

def generate_origin(self):
return f"$ORIGIN {self.name}"

Expand Down
21 changes: 14 additions & 7 deletions src/zonefilegen/generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import zonefilegen.core
import zonefilegen.parsing


def build_soa_record(soa_dict: dict, serial_number: int) -> zonefilegen.core.ResourceRecord:
"""
Constructs a special SOA record with a specified serial number
Expand All @@ -21,7 +22,11 @@ def build_soa_record(soa_dict: dict, serial_number: int) -> zonefilegen.core.Res
f"{soa_dict['negative_caching_ttl']})"
return soa_rec

def build_reverse_zone(network, ptr_candidates: Tuple, default_ttl: int, ns_records: List[Tuple[str, int, str]]) -> zonefilegen.core.Zone:

def build_reverse_zone(network,
ptr_candidates: Tuple,
default_ttl: int,
ns_records: List[Tuple[str, int, str]]) -> zonefilegen.core.Zone:
"""
Checks a set of addresses if they are part of a network and
include them as PTR records in a reverse zone for that network in such case.
Expand Down Expand Up @@ -52,39 +57,41 @@ def build_reverse_zone(network, ptr_candidates: Tuple, default_ttl: int, ns_reco
rev_zone.records.append(rec)
return rev_zone


def build_fwd_zone(zone_name: str, rrset_dict: dict, default_ttl: int) -> zonefilegen.core.Zone:
"""
Builds a forward DNS zone from a set of records
"""
fwd_zone = zonefilegen.core.Zone(zone_name, default_ttl)

for rrset in rrset_dict:
if rrset['name'].endswith('.'):
name = rrset['name']
elif rrset['name'] == '@':
name = zone_name
else:
name = f"{rrset['name']}.{zone_name}"

if type(rrset['data']) is list:
record_data_arr = rrset['data']
else:
record_data_arr = [rrset['data']]

for record_data in record_data_arr:
record = zonefilegen.core.ResourceRecord()
record.name = name
record.record_type = rrset['type']
if not record.record_type in zonefilegen.core.RECORD_TYPES:
if record.record_type not in zonefilegen.core.RECORD_TYPES:
logging.critical(f"Invalid type in record for {record.name} : {record.record_type}")
exit(1)
if 'ttl' in rrset:
record.ttl = rrset['ttl']
record.record_class = 'IN'
record.data = record_data

fwd_zone.records.append(record)
return fwd_zone


def generate_header(digest: str):
return f"; Generated by zonefilegen, INPUT_DIGEST: {digest}"
return f"; Generated by zonefilegen, INPUT_DIGEST: {digest}"
25 changes: 17 additions & 8 deletions src/zonefilegen/parsing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import ipaddress
import hashlib
import ipaddress
import logging
import pathlib
from typing import List, Tuple
Expand All @@ -10,6 +9,7 @@
import zonefilegen.core
import zonefilegen.generation


def get_rev_zone_name(network) -> str:
"""
Cuts off the first few blocks of a reverse pointer for a network address
Expand All @@ -27,6 +27,7 @@ def get_rev_zone_name(network) -> str:
blocks_to_cut = int((address_len - network.prefixlen) / divisor)
return '.'.join(network.network_address.reverse_pointer.split('.')[blocks_to_cut:None]) + '.'


def get_rev_ptr_name(address, prefix_len) -> str:
"""
Cuts off the last few blocks of a reverse pointer for an address
Expand All @@ -44,17 +45,19 @@ def get_rev_ptr_name(address, prefix_len) -> str:
blocks_to_cut = int(((address_len - prefix_len) / divisor))
return '.'.join(address.reverse_pointer.split('.')[None:blocks_to_cut])


def parse_toml_file(input_file_path: pathlib.Path) -> Tuple[zonefilegen.core.Zone, List[zonefilegen.core.Zone], dict, str]:
"""
Parses a toml file with DNS records and generates one forward zone and one or
more reverse zones. Additionally, a dict with info about the SOA record and a digest of the source file is returned for embedding
in the generated files, to detect when serial number needs to be updated.
Parses a toml file with DNS records and generates one forward zone and one
or more reverse zones. Additionally, a dict with info about the SOA record
and a digest of the source file is returned for embedding in the generated
files, to detect when serial number needs to be updated.
"""
with open(input_file_path, 'r') as f:
data = toml.load(f)
f.seek(0)
file_digest = hashlib.sha1(f.read().encode()).hexdigest()

fwd_zone = zonefilegen.generation.build_fwd_zone(data['origin'], data['rrset'], data['default_ttl'])

ipv4_ptr_candidates = []
Expand All @@ -76,12 +79,18 @@ def parse_toml_file(input_file_path: pathlib.Path) -> Tuple[zonefilegen.core.Zon
if network.prefixlen % 8 != 0:
logging.fatal("IPv4 network prefix must be divisible by 8")
exit(1)
reverse_zones.append(zonefilegen.generation.build_reverse_zone(network, ipv4_ptr_candidates, data['default_ttl'], rev_ns_records))
reverse_zones.append(zonefilegen.generation.build_reverse_zone(network,
ipv4_ptr_candidates,
data['default_ttl'],
rev_ns_records))

elif type(network) is ipaddress.IPv6Network:
if network.prefixlen % 4 != 0:
logging.fatal("IPv6 network prefix must be divisible by 4")
exit(1)
reverse_zones.append(zonefilegen.generation.build_reverse_zone(network, ipv6_ptr_candidates, data['default_ttl'], rev_ns_records))
reverse_zones.append(zonefilegen.generation.build_reverse_zone(network,
ipv6_ptr_candidates,
data['default_ttl'],
rev_ns_records))

return (fwd_zone, reverse_zones, data['soa'], file_digest)
return (fwd_zone, reverse_zones, data['soa'], file_digest)

0 comments on commit ba37045

Please sign in to comment.