Generate IP address lists automatically

This commit implements auto generation of IP address lists for IP
ranges. The ipcalc PyPI module is used to determine which IP addresses
belong to a given IP range. The IPs are listed in IP range sections and
links to the original pages are added.
This commit is contained in:
Jan Dittberner 2016-04-25 00:17:39 +02:00
parent 334968385b
commit 196f33d3b1

View file

@ -12,11 +12,14 @@ __version__ = '0.1.0'
import re import re
from ipcalc import Network
from docutils import nodes from docutils import nodes
from docutils.parsers.rst import Directive
from sphinx import addnodes from sphinx import addnodes
from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType from sphinx.domains import Domain, ObjType
from sphinx.environment import NoUri
from sphinx.locale import l_ from sphinx.locale import l_
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
@ -27,19 +30,31 @@ def ip_object_anchor(typ, path):
return typ.lower() + '-' + path return typ.lower() + '-' + path
class ip_node(nodes.Inline, nodes.TextElement): pass
class ip_range(nodes.General, nodes.Element): pass
class IPXRefRole(XRefRole): class IPXRefRole(XRefRole):
""" """
Cross referencing role for the IP domain. Cross referencing role for the IP domain.
""" """
def __init__(self, method, **kwargs): def __init__(self, method, **kwargs):
super(IPXRefRole, self).__init__(**kwargs)
self.method = method self.method = method
innernodeclass=None
if method in ('v4', 'v6'):
innernodeclass = ip_node
super(IPXRefRole, self).__init__(
innernodeclass=innernodeclass, **kwargs)
def process_link(self, env, refnode, has_explicit_title, title, target): def process_link(self, env, refnode, has_explicit_title, title, target):
domaindata = env.domaindata['ip']
domaindata[self.method][target] = (target, refnode)
return title, target return title, target
def result_nodes(self, document, env, node, is_ref): def result_nodes(self, document, env, node, is_ref):
try: try:
node['typ'] = self.method
indexnode = addnodes.index() indexnode = addnodes.index()
targetid = 'index-%s' % env.new_serialno('index') targetid = 'index-%s' % env.new_serialno('index')
targetnode = nodes.target('', '', ids=[targetid]) targetnode = nodes.target('', '', ids=[targetid])
@ -50,74 +65,67 @@ class IPXRefRole(XRefRole):
return [node], [e.args[0]] return [node], [e.args[0]]
class IPObject(ObjectDescription): class IPRange(Directive):
has_content = True
typ = NotImplemented
typelabel = NotImplemented
required_arguments = 1 required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = {}
def handle_signature(self, sig, signode): def handle_rangespec(self, node):
ipaddress = sig titlenode = nodes.title()
signode['typ'] = self.typ node.append(titlenode)
signode['path'] = sig titlenode.append(nodes.inline('', self.get_prefix_title()))
signode.append(nodes.inline(self.typelabel, self.typelabel)) titlenode.append(nodes.literal('', self.rangespec))
signode.append(nodes.literal(sig, sig)) ids = ip_object_anchor(self.typ, self.rangespec)
return (ipaddress, self.typ, sig) node['ids'].append(ids)
self.env.domaindata[self.domain][self.typ][ids] = (
self.env.docname,
self.options.get('synopsis', ''))
return ids
def needs_arglist(self): def run(self):
return False if ':' in self.name:
self.domain, self.objtype = self.name.split(':', 1)
else:
self.domain, self.objtype = '', self.name
self.env = self.state.document.settings.env
self.rangespec = self.arguments[0]
node = nodes.section()
name = self.handle_rangespec(node)
self.indexnode = addnodes.index(entries=[
('single', self.get_index_text(), name, '')])
def add_target_and_index(self, name, sig, signode): if self.content:
sigid = ip_object_anchor(*name[1:]) contentnode = nodes.paragraph('')
node.append(contentnode)
self.state.nested_parse(
self.content, self.content_offset, contentnode)
if sigid not in self.state.document.ids: iprange = ip_range()
signode['names'].append(name[0]) node.append(iprange)
signode['ids'].append(sigid) iprange['rangespec'] = self.rangespec
signode['first'] = (not self.names) return [self.indexnode, node]
self.state.document.note_explicit_target(signode)
objects = self.env.domaindata['ip'][self.typ]
if sigid in objects:
self.state_machine.reporter.warning(
'duplicate object description of %s, ' % sigid
+ 'other instance in '
+ self.env.doc2path(objects[sigid][0])
+ ', use: noindex: for one of them',
line=self.lineno
)
objects[sigid] = (
self.env.docname,
self.options.get('synopsis', ''))
idxtext = self.get_index_text(name)
if idxtext:
self.indexnode['entries'].append(
('single', idxtext, sigid, '')
)
def get_index_text(self, name):
idxname = name[0]
return idxname
class IPv4Address(IPObject): class IPv4Range(IPRange):
typ = 'v4'
typelabel = "IPv4 address "
class IPv4Range(IPObject):
typ = 'v4range' typ = 'v4range'
typelabel = "IPv4 range "
def get_prefix_title(self):
return l_('IPv4 address range ')
def get_index_text(self):
return "%s; %s" % (l_('IPv4 range'), self.rangespec)
class IPv6Address(IPObject): class IPv6Range(IPRange):
typ = 'v6'
typelabel = "IPv6 address "
class IPv6Range(IPObject):
typ = 'v6range' typ = 'v6range'
typelabel = "IPv6 range "
def get_prefix_title(self):
return l_('IPv6 address range ')
def get_index_text(self):
return "%s; %s" % (l_('IPv6 range'), self.rangespec)
class IPDomain(Domain): class IPDomain(Domain):
@ -128,31 +136,30 @@ class IPDomain(Domain):
label = 'IP addresses and ranges.' label = 'IP addresses and ranges.'
object_types = { object_types = {
'v4': ObjType(l_('v4'), 'v4'), 'v4': ObjType(l_('v4'), 'v4', 'obj'),
'v6': ObjType(l_('v6'), 'v6'), 'v6': ObjType(l_('v6'), 'v6', 'obj'),
'v4range': ObjType(l_('v4range'), 'v4range'), 'v4range': ObjType(l_('v4range'), 'v4range', 'obj'),
'v6range': ObjType(l_('v6range'), 'v6range'), 'v6range': ObjType(l_('v6range'), 'v6range', 'obj'),
} }
directives = { directives = {
'v4': IPv4Address,
'v6': IPv6Address,
'v4range': IPv4Range, 'v4range': IPv4Range,
'v6range': IPv6Range, 'v6range': IPv6Range,
} }
roles = { roles = {
'v4': IPXRefRole('ip'), 'v4': IPXRefRole('v4'),
'v6': IPXRefRole('ip'), 'v6': IPXRefRole('v6'),
'v4range': IPXRefRole('range'), 'v4range': IPXRefRole('v4range'),
'v6range': IPXRefRole('range'), 'v6range': IPXRefRole('v6range'),
} }
initial_data = { initial_data = {
'v4': {}, 'v4': {},
'v6': {}, 'v6': {},
'v4range': {}, 'v4range': {},
'v6range': {} 'v6range': {},
'ips': [],
} }
def clear_doc(self, docname): def clear_doc(self, docname):
@ -194,6 +201,59 @@ class IPDomain(Domain):
yield (path, path, typ, info[0], anchor, 1) yield (path, path, typ, info[0], anchor, 1)
def process_ips(app, doctree):
env = app.builder.env
domaindata = env.domaindata[IPDomain.name]
for node in doctree.traverse(ip_node):
ip = node.astext()
domaindata['ips'].append({
'docname': env.docname,
'source': node.parent.source or env.doc2path(env.docname),
'lineno': node.parent.line,
'ip': ip,
'typ': node.parent['typ'],
})
node.replace_self(nodes.literal('', ip))
def sort_ip_info(item):
return item['ip']
def process_ip_nodes(app, doctree, fromdocname):
env = app.builder.env
domaindata = env.domaindata[IPDomain.name]
for node in doctree.traverse(ip_range):
content = []
net = Network(node['rangespec'])
for ip_info in sorted(domaindata['ips'], key=sort_ip_info):
if ip_info['ip'] in net:
para = nodes.paragraph()
para += nodes.literal('', ip_info['ip'])
ids = ip_object_anchor(ip_info['typ'], ip_info['ip'])
para['ids'].append(ids)
domaindata[ip_info['typ']][ids] = (fromdocname, '')
newnode = nodes.reference('', '', internal=True)
try:
newnode['refuri'] = app.builder.get_relative_uri(
fromdocname, ip_info['docname'])
except NoUri:
pass
title = env.titles[ip_info['docname']]
innernode = nodes.Text(title.astext())
newnode.append(innernode)
para += nodes.Text(" in ")
para += newnode
content.append(para)
#print(ip_info, 'in', node['rangespec'])
node.replace_self(content)
def setup(app): def setup(app):
app.add_domain(IPDomain) app.add_domain(IPDomain)
app.connect('doctree-read', process_ips)
app.connect('doctree-resolved', process_ip_nodes)
return {'version': __version__} return {'version': __version__}