Merge pull request #5 from drybjed/dhcp-relay

Various fixes in the debops.dhcpd role
This commit is contained in:
Maciej Delmanowski 2015-03-30 14:15:43 +02:00
commit cff2f573d5
26 changed files with 1663 additions and 450 deletions

View File

@ -1,5 +1,6 @@
--- ---
sudo: True
language: 'python' language: 'python'
python: '2.7' python: '2.7'

8
CHANGES.rst Normal file
View File

@ -0,0 +1,8 @@
Changelog
=========
v0.1.0
------
- First release [drybjed]

View File

@ -2,7 +2,14 @@
[![Travis CI](http://img.shields.io/travis/debops/ansible-dhcpd.svg?style=flat)](http://travis-ci.org/debops/ansible-dhcpd) [![test-suite](http://img.shields.io/badge/test--suite-ansible--dhcpd-blue.svg?style=flat)](https://github.com/debops/test-suite/tree/master/ansible-dhcpd/) [![Ansible Galaxy](http://img.shields.io/badge/galaxy-debops.dhcpd-660198.svg?style=flat)](https://galaxy.ansible.com/list#/roles/1559) [![Travis CI](http://img.shields.io/travis/debops/ansible-dhcpd.svg?style=flat)](http://travis-ci.org/debops/ansible-dhcpd) [![test-suite](http://img.shields.io/badge/test--suite-ansible--dhcpd-blue.svg?style=flat)](https://github.com/debops/test-suite/tree/master/ansible-dhcpd/) [![Ansible Galaxy](http://img.shields.io/badge/galaxy-debops.dhcpd-660198.svg?style=flat)](https://galaxy.ansible.com/list#/roles/1559)
Install and configure [ISC DHCP Server](https://www.isc.org/downloads/dhcp/). `debops.dhcpd` role can be used to configure an [ISC DHCP
Server](https://www.isc.org/downloads/dhcp/) as standalone or in a 2-host
failover configuration. Alternatively, you can configure an DHCP relay on
a host connected to multiple networks which will relay DHCP/BOOTP messages
to your DHCP server.
`dhcp-probe` script will be used to scan the network for unauthorized DHCP
servers and notify administrators if they are found.
### Installation ### Installation
@ -13,7 +20,7 @@ This role requires at least Ansible `v1.7.0`. To install it, run:
### Documentation ### Documentation
More information about `debops.dhcpd` can be found in the More information about `debops.dhcpd` can be found in the
[official debops.dhcpd documentation](http://docs.debops.org/en/latest/ansible/roles/debops.dhcpd.html). [official debops.dhcpd documentation](http://docs.debops.org/en/latest/ansible/roles/ansible-dhcpd/docs/).

View File

@ -1,285 +1,276 @@
--- ---
# Default variables
# =================
# ---- Global ISC DHCP Server configuration ---- # .. contents:: Sections
# :local:
#
# -------------------
# General options
# -------------------
# .. envvar:: dhcpd_mode
#
# What service type to configure on this host:
#
# - ``server``: host is an ISC DHCP server, see ``dhcpd(8)``
#
# - ``relay``: host is an ISC DHCP relay, see dhcrelay(8)
#
# - ``probe``: configure only ``dhcp-probe`` when enabled
#
dhcpd_mode: 'server'
# .. envvar:: dhcpd_ipversion
#
# Internet Protocol version to configure: ``4`` or ``6``
dhcpd_ipversion: '4'
# .. envvar:: dhcpd_base_packages_map
#
# What packages should be installed, depending on mode of operation
dhcpd_base_packages_map:
'server': [ 'isc-dhcp-server' ]
'relay': [ 'isc-dhcp-relay' ]
'probe': []
# --------------------------------
# ISC DHCP Relay configuration
# --------------------------------
# .. envvar:: dhcpd_relay_servers
#
# List of DHCP servers which should receive the relayed packets
dhcpd_relay_servers: [ '{{ ansible_default_ipv4.gateway }}' ]
# .. envvar:: dhcpd_relay_interfaces
#
# List of network interfaces that dhcrelay should listen on
dhcpd_relay_interfaces: []
# .. envvar:: dhcpd_relay_options
#
# Additional dhcrelay options
dhcpd_relay_options: '{{ "-" + dhcpd_ipversion }}'
# ---------------------------------
# ISC DHCP Server configuration
# ---------------------------------
# .. envvar:: dhcpd_server_options
#
# dhcpd(8) options
dhcpd_server_options: '{{ "-" + dhcpd_ipversion }}'
# ---------------------------
# DHCP main configuration
# ---------------------------
# .. envvar:: dhcpd_authoritative
#
# Is this DHCP server authoritative? # Is this DHCP server authoritative?
dhcpd_authoritative: False dhcpd_authoritative: False
# .. envvar:: dhcpd_log_facility
#
# Log facility to use
dhcpd_log_facility: 'local7'
# .. envvar:: dhcpd_interfaces
#
# List of network interfaces to listen on for DHCP requests # List of network interfaces to listen on for DHCP requests
# If this list is empty, Ansible will try to guess correct interfaces # If this list is empty, Ansible will try to guess correct interfaces
# automatically # automatically
dhcpd_interfaces: [] dhcpd_interfaces: []
# Default domain to use
dhcpd_domain: '{{ ansible_domain }}'
# List of default DNS servers. By default, point users to the same host that
# serves them DHCP requests, on default interface. If this host is a router,
# you might need to set DNS server to internal interface IP address.
dhcpd_dns_servers: [ '{{ ansible_default_ipv4.address }}' ]
# .. envvar:: dhcpd_lease_time
#
# Max lease time in hours (default lease time is calculated below) # Max lease time in hours (default lease time is calculated below)
dhcpd_lease_time: 24 dhcpd_lease_time: '24'
# Default global options formatted as a text block
dhcpd_global_options: |
option domain-name "{{ ansible_domain }}";
option domain-name-servers {{ dhcpd_dns_servers | join(' ') }};
default-lease-time {{ (((dhcpd_lease_time / 2) + 6) * 60 * 60)|round|int }};
max-lease-time {{ (dhcpd_lease_time * 60 * 60)|round|int }};
log-facility local7;
# Custom options formatted as a text block # .. envvar:: dhcpd_global_default_lease_time
#
# Default lease time for all IP address leases (18 hours)
dhcpd_global_default_lease_time: '{{ (((dhcpd_lease_time|int / 2) + 6) * 60 * 60)|round|int }}'
# .. envvar:: dhcpd_global_max_lease_time
#
# Maximum lease time for all IP addresses (24 hours)
dhcpd_global_max_lease_time: '{{ (dhcpd_lease_time|int * 60 * 60)|round|int }}'
# ---------------------------
# DHCP advertised options
# ---------------------------
# .. envvar:: dhcpd_auto_options
#
# If enabled, ISC DHCP server will be configured with a set of automatically
# detected options. See ``auto_options.j2`` template for more details.
dhcpd_auto_options: True
# .. envvar:: dhcpd_domain_name
#
# Default host domain to advertise
dhcpd_domain_name: '{{ ansible_domain }}'
# .. envvar:: dhcpd_domain_search
#
# List of additional domains which should be checked when looking for hostnames
dhcpd_domain_search: []
# .. envvar:: dhcpd_nameservers
#
# List of nameservers to advertise by default
# If it's not specified, nameservers from ``/etc/resolv.conf`` will be used
# instead.
dhcpd_nameservers: []
# .. envvar:: dhcpd_options
#
# Custom global options formatted as a text block
dhcpd_options: False dhcpd_options: False
# ---- ISC DHCP Server configuration scopes ---- # ----------------------------------------
# ISC DHCP Server configuration scopes
# ----------------------------------------
# These lists allow you to generate nested configuration scopes in # These lists allow you to generate nested configuration scopes in
# dhcpd.conf. Most of the information about them can be found in dhcpd.conf(5) # dhcpd.conf. Most of the information about them can be found in dhcpd.conf(5)
# manual page. You can create nested configuration using Ansible variable # manual page. You can create nested configuration using Ansible variable
# expansion (examples below). # expansion.
# List of general configuration parameters (work in any configuration scope):
# - comment: '' add a comment to a scope
# - options: | custom options for that scope defined as a text block
# - include: '' path to external file to include in this scope
# List of hosts (works in groups, subnets):
# - hosts: '' or [] list of hosts to configure in that scope; if this is
# a path to a file, dhcpd will include an external file
# in this scope
# List of parameters specific to dhcpd_classes:
# - class: '' class name
# - subclass: this is a hash with expression as key and additional
# options as value in a text block (see example below);
# each match expression must end with a colon to indicate
# hash key; optional
# List of parameters specific to dhcpd_groups:
# - subnets: [] list of subnet scopes to group together
# - groups: [] list of other group scopes to include. No recursion!
# List of parameters specific to dhcpd_shared_networks:
# - name: '' name of shared network
# - subnets: [] list of subnets in a shared network (do not use
# dhcpd_subnets here, because they will be duplicated
# and DHCP server will not start)
# List of parameters specific to dhcpd_subnets:
# - subnet: '' start of a subnet range (ie.: 192.168.1.0)
# - netmask: '' netmask for this subnet (ie.: 255.255.255.0)
# - routers: '' or [] address or list of addresses of gateway for that
# subnet (ie.: 192.168.1.1)
# List of parameters specific to dhcpd_hosts:
# - hostname: '' hostname, without domain part
# - address: '' IP address reserved for that host, optional
# - ethernet: '' Ethernet MAC address of this host, optional
# .. envvar:: dhcpd_keys
#
# List of secret keys used for Dynamic DNS configuration. See
# :ref:`dhcpd_keys` for more details.
dhcpd_keys: [] dhcpd_keys: []
#- key: "secure-key"
# algorithm: "hmac-md5"
# secret: "JFw7jM2/KVU2hIB4xkDSQmHB6JJOLUu4xkzwLNNpR88="
# List of classes
# .. envvar:: dhcpd_zones
#
# List of DNS zones to update with Dynamic DNS configuration. See
# :ref:`dhcpd_zones` for more details.
dhcpd_zones: []
# .. envvar:: dhcpd_classes
#
# List of client classes (see dhcpd.conf(5)). More informaction can be found in
# :ref:`dhcpd_classes`.
dhcpd_classes: [] dhcpd_classes: []
#- class 'example-class'
# subclass:
# 'match1':
# 'match2': |
# # match2 options in a text block;
#- class 'example-empty-class'
# List of groups # .. envvar:: dhcpd_groups
#
# List of configuration scopes groped together. See :ref:`dhcpd_groups` for
# more details.
dhcpd_groups: [] dhcpd_groups: []
#- comment: 'First group'
# hosts: '/etc/dhcp/dhcpd-group1-hosts.conf'
# groups: '{{ dhcpd_group_second }}'
# An example of group nesting
#dhcpd_group_second:
# - comment: 'Second group'
# hosts: '/etc/dhcp/dhcpd-group2-hosts.conf'
# List of shared networks # .. envvar:: dhcpd_shared_networks
#
# List of shared networks grouping specified subnets together. See
# :ref:`dhcpd_shared_networks` for more details.
dhcpd_shared_networks: [] dhcpd_shared_networks: []
#- name: 'shared-net'
# comment: "Local shared network"
# subnets: '{{ dhcpd_subnets_local }}'
# options: |
# default-lease-time 600;
# max-lease-time 900;
# List of subnets not in a shared network # .. envvar:: dhcpd_subnets
dhcpd_subnets: #
- subnet: '{{ ansible_default_ipv4.network }}' # List of subnets not in a shared network. See :ref:`dhcpd_subnets` for more
netmask: '{{ ansible_default_ipv4.netmask }}' # details.
dhcpd_subnets: [ '{{ dhcpd_subnet_default[dhcpd_ipversion] }}' ]
# Default subnet managed automatically
dhcpd_subnet_default:
'4':
subnet: '{{ ansible_default_ipv4.network + "/" + ansible_default_ipv4.netmask }}'
routers: '{{ ansible_default_ipv4.gateway | default("") }}'
comment: 'Generated automatically by Ansible'
'6':
subnet: '{{ ansible_default_ipv6.address + "/" + ansible_default_ipv6.prefix }}'
comment: 'Generated automatically by Ansible' comment: 'Generated automatically by Ansible'
#- subnet: 'dead:be:ef::/64'
# ipv6: True # .. envvar:: dhcpd_hosts
# routers: '10.0.10.1'
# comment: "Example IPv6 subnet"
# options: |
# default-lease-time 300;
# max-lease-time 7200;
# #
#- subnet: '10.0.20.0' # Global list of hosts in DHCP. See ref:`dhcpd_hosts` for more details.
# netmask: '255.255.255.0'
# comment: 'Ignored subnet'
# An example subnets included in a shared network
#dhcpd_subnets_local:
# - subnet: '10.0.30.0'
# netmask: '255.255.255.0'
# routers: [ '10.0.30.1', '10.0.30.2' ]
#
# - subnet: '10.0.40.0'
# netmask: '255.255.255.0'
# routers: '19.0.40.1'
# options: |
# default-lease-time 300;
# max-lease-time 7200;
# pools:
# - comment: "A pool in a subnet"
# range: '10.0.30.10 10.0.30.20'
# Global list of hosts in DHCP
dhcpd_hosts: [] dhcpd_hosts: []
# - hostname: 'examplehost'
# address: '10.0.10.1'
# ethernet: '00:00:00:00:00:00'
# Example global list of hosts read from an external file
#dhcpd_hosts: '/etc/dhcp/dhcpd.hosts.conf'
# List of external files to include # List of external files to include. See :ref:`dhcpd_includes` for more
# details.
dhcpd_includes: [] dhcpd_includes: []
#- '/etc/dhcp/example.conf'
# ---- ISC DHCP failover configuration ----
# # .. envvar:: dhcpd_failovers
# Each 'failover pair' declaration consists of primary and secondary host,
# no more than two nodes failover is currently allowed by isc-dhcpd.
#
# You must specify which failover pair each pool should use by specifying a
# 'failover peer' statement under an 'options' block in each pool declaration.
# e.g:
#
# dhcpd_failovers:
# - failover: "my-failover"
# primary: '10.0.30.1'
# secondary: '10.0.30.2'
# ...
#
# dhcpd_subnets:
# - subnet: ...
# ...
# pools:
# - comment: "My pool with failover"
# range: '10.0.30.10 10.0.30.20'
# options: |
# failover peer "my-failover";
#
# Each failover declaration has a set of an mandatory fields, which is:
# primary: "" Ansible inventory name of a primary DHCP host, if
# you need failover to work on different IP,
# see primary_fo_addr option below.
#
# secondary: "" Ansible inventory name of a secondary DHCP host, if
# you need failover to work on different IP,
# see secondary_fo_addr option below.
#
# Ansible inventory name is either IP ot hostname specified in inventory file.
#
# mclt: 3600 Max Client Lead Time. The maximum amount of time
# that one server can extend a lease for a DHCP
# client beyond the time known by the partner server.
#
# split: [0-255] Specifies the split between the primary and
# secondary for the purposes of load balancing.
# Whenever a client makes a DHCP request, the DHCP
# server runs a hash on the client identification,
# resulting in value from 0 to 255. This is used as
# an index into a 256 bit field. If the bit at that
# index is set, the primary is responsible. If
# the bit at that index is not set, the secondary
# is responsible.
# -- or --
# hba: ([0-9a-f]{2}:){32} Specifies the split between the primary and
# secondary as a bitmap rather than a cutoff, which
# theoretically allows for finer-grained control.
# In practice, there is probably no need for such
# fine-grained control, however.
# max_response_delay: 5 Tells the DHCP server how many seconds may pass
# without receiving a message from its failover peer
# before it assumes that connection has failed.
# This is mandatory according to dhcpd.conf man page.
# max_unacked_updates: 10 Tells the remote DHCP server how many BNDUPD
# messages it can send before it receives a BNDACK
# from the local system.
# This is mandatory according to dhcpd.conf man page.
#
# You must use either 'split' or 'hba' statement. Split has a preference, so
# if it's defined, 'hba' will be omitted by configuration template.
# Optional field are mostly desribed in dhcpd.conf man page:
# port: 647 Specifies port on which primary and secondary
# nodes will listen for failover connection.
# Diffirent ports for primary and secondary is
# currently unsupported.
#
# primary_fo_addr: "" IP/Hostname of a primary DHCP host. This option
# is used if you need failover address be different
# from ansible inventory IP/hostname.
# If omitted, then 'primary' is used.
#
# secondary_fo_addr: "" IP/Hostname of a secondary DHCP host. This option
# is used if you need failover address be different
# from ansible inventory IP/hostname.
# If omitted, then 'secondary' is used.
#
# auto_partner_down: 0 Number of second to start serving partners IPs
# after the partner's failure.
#
# load_balance_max_seconds: 5
# max_lease_misbalance: 15
# max_lease_ownership: 10
# min_balance: 60
# max_balance: 3600
# #
# DHCP failover configuration. See :ref:`dhcpd_failovers` for more details.
dhcpd_failovers: [] dhcpd_failovers: []
## Following is full cluster configuration
#- failover: 'failover-localsubnet'
# primary: '10.0.10.1' # -----------------------------
# primary_fo_addr: '10.5.10.1' # dhcp-probe configuration
# secondary: '10.0.10.2' # -----------------------------
# secondary_fo_addr: '10.5.10.2'
# port: 1337 # .. envvar:: dhcpd_probe
# split: 128 #
# hba: aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa # Enable or disable ``dhcp-probe`` script
# max_response_delay: 5 dhcpd_probe: True
# max_unacked_updates: 10
# load_balance_max_seconds: 5
# auto_partner_down: 0 # .. envvar:: dhcpd_probe_mail_to
# max_lease_misbalance: 15 #
# max_lease_ownership: 10 # List of mail recipients which will receive messages about unauthorized DHCP
# min_balance: 60 # servers. Set to ``[]`` to disable.
# max_balance: 3600 dhcpd_probe_mail_to: [ 'root@{{ ansible_domain }}' ]
#
## Following is minimal cluster configuration
#- failover: 'failover-san' # .. envvar:: dhcpd_probe_page_to
# primary: '10.0.10.1' #
# secondary: '10.0.10.2' # Alternative list of mail recipients which will receive mail messages. Meant
# mclt: 3600 # to be used as a "pager service", you can use ``debops.smstools`` role to
# split: 128 # setup a mail-SMS gateway and send the SMS messages that way.
# max_response_delay: 5 dhcpd_probe_page_to: []
# max_unacked_updates: 10
# .. envvar:: dhcpd_probe_mail_timeout
#
# Number of seconds between to wait between sending new mail messages
dhcpd_probe_mail_timeout: '{{ (20 * 60) }}'
# .. envvar:: dhcpd_probe_page_timeout
#
# Number of seconds between to wait between sending new pager messages
dhcpd_probe_page_timeout: '{{ (20 * 60) }}'
# .. envvar:: dhcpd_probe_legal_servers
#
# List of IP addresses of the host which are authorized DHCP servers.
dhcpd_probe_legal_servers: []
# .. envvar:: dhcpd_probe_options
#
# Additional ``dhcp-probe`` options specified as a YAML text block.
dhcpd_probe_options: ''

1
docs/changelog.rst Normal file
View File

@ -0,0 +1 @@
.. include:: ../CHANGES.rst

21
docs/copyright.rst Normal file
View File

@ -0,0 +1,21 @@
Copyright
=========
::
Copyright (C) 2014 Maciej Delmanowski <drybjed@gmail.com>
Copyright (C) 2014 DebOps Project http://debops.org/
[see Credits for more details]
his program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3, as
published by the Free Software Foundation.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/

16
docs/credits.rst Normal file
View File

@ -0,0 +1,16 @@
Credits
=======
Credits, in chronological order
-------------------------------
* Maciej Delmanowski <drybjed_at_gmail.com>
* creator of the DebOps Project
* current project maintainer
* RedRampage
* Added support for DHCP failover and Dynamic DNS keys

View File

@ -0,0 +1,468 @@
Default variables: configuration
================================
some of ``debops.dhcpd`` default variables have more extensive configuration
than simple strings or lists, here you can find documentation and examples for
them.
.. contents::
:local:
:depth: 1
.. _dhcpd_keys:
dhcpd_keys
----------
This list lets you define symmetric keys used to update dynamic DNS with
information configured using DHCP.
``key``
Name of the key used to select it in specific scope
``algorithm``
Name of the algorithm to use for key encryption
``secret``
Encrypted symmetric key shared between DHCP and DNS servers
``comment``
An optional comment added in the configuration file
Examples::
# Read the secret key from an external file
dhcpd_secret_secure_key: '{{ lookup("password",
secret + "/" + ansible_domain +
"/shared/ddns/keys/secure-key" }}'
dhcpd_keys:
- key: "secure-key"
algorithm: "hmac-md5"
secret: "{{ dhcpd_secret_secure_key }}"
.. _dhcpd_zones:
dhcpd_zones
-----------
This list lets you define DNS zones used to update dynamic DNS with information
configured using DHCP.
``zone``
DNS domain name of a zone, needs to end with a dot (``.``)
``primary``
Address of the primary DNS server serving the specified zone
``key``
Name of the symmetric key used to authorize Dynamic DNS updates of the
specified zone
``comment``
An optional comment added in the configuration file
Examples::
dhcpd_zones:
- zone: "example.org."
primary: "127.0.0.1"
key: "secure-key"
.. _dhcpd_classes:
dhcpd_classes
-------------
Here you can define host classes and custom options for each class.
``class``
Name of the host class
``comment``
Optional comment added in the configuration file
``options``
Text block with options for a particular class scope
``include``
Include an external file
``subclass``
Dict. You can specify matches for a class in two ways:
- a dict key without a value will create a simple match for that host. You
need to specify dict key with colon (``:``) at the end to indicate that
this is a dict key, see examples below
- a dict with a text block as a value will create an extended match scope
with options specified in the text block inside that scope
Examples::
dhcpd_classes:
- class: 'empty-class'
- class: 'allocation-class-1'
options: |
match pick-first-value (option dhcp-client-identifier, hardware);
subclass:
# Simple match
'00:11:22:33:44:55':
# Extended match
'00:11:22:33:22:11': |
option root-path "samsara:/var/diskless/alphapc";
filename "/tftpboot/netbsd.alphapc-diskless";
.. _dhcpd_groups:
dhcpd_groups
------------
Group related configuration together.
``comment``
Optional comment added in the configuration file
``options``
Text block with options for a particular group
``include``
Include an external file
``groups``
Include another group definition of the group in this group. Child group
should be defined in a separate YAML dict. Recursion is not allowed.
``hosts``
List of hosts included in this group. Use the same format as the
``dhcpd_hosts`` list.
``subnets``
List of subnets included in this group. Use the same format as the
``dhcpd_subnets`` list.
Examples::
dhcpd_groups:
- comment: 'First group'
hosts: '/etc/dhcp/dhcpd-group1-hosts.conf'
groups: '{{ dhcpd_group_second }}'
# An example of group nesting
dhcpd_group_second:
- comment: 'Second group'
hosts: '/etc/dhcp/dhcpd-group2-hosts.conf'
.. _dhcpd_shared_networks:
dhcpd_shared_networks
---------------------
List of shared networks which combine specified subnets together.
``name``
Name of a shared network
``comment``
A comment added to this shared network in the configuration
``options``
Custom options in the text block format for this shared network
``include``
Include an external file in this shared network scope
``subnets``
List of subnets included in this shared network. Use the same format as the
``dhcpd_subnets`` list.
Examples::
dhcpd_shared_networks:
- name: 'shared-net'
comment: "Local shared network"
subnets: '{{ dhcpd_subnets_local }}'
options: |
default-lease-time 600;
max-lease-time 900;
dhcpd_subnets_local:
- subnet: '10.0.30.0'
netmask: '255.255.255.0'
routers: [ '10.0.30.1', '10.0.30.2' ]
- subnet: '10.0.40.0'
netmask: '255.255.255.0'
routers: '19.0.40.1'
options: |
default-lease-time 300;
max-lease-time 7200;
pools:
- comment: "A pool in a subnet"
range: '10.0.30.10 10.0.30.20'
.. _dhcpd_subnets:
dhcpd_subnets
-------------
List of subnets included in a specified group.
``subnet``
IP address of the subnet. If it's IPv4, it should be the first IP address in
the subnet, if it's IPv6, it should be specified with the prefix.
``netmask``
If the subnet is IPv4, specify it's netmask in "normal" IP address form, not
the CIDR form.
``ipv6``
Set to ``True`` if managed subnet is IPv6.
``routers``
String (if just one), or list (if many) of IP addresses of the routers for
this subnet
``comment``
A comment added to this subnet in the configuration
``options``
Custom options in the text block format for this subnet
``include``
Include an external file in this subnet scope
``pools``
List of different address pools within specified subnet. Each pool should be
specified as a dict, following keys are recognized:
- ``range``: a string which defines the range of the specific pool, with IP
addresses of the start and end delimited by space
- ``comment``: a comment added to this host in the configuration
- ``options``: custom options in the text block format for this host
- ``include``: include an external file in this pool
Examples::
# List of subnets
dhcpd_subnets: [ '{{ dhcpd_subnet_default }}' ]
dhcpd_subnet_default:
subnet: '{{ ansible_default_ipv4.network }}'
netmask: '{{ ansible_default_ipv4.netmask }}'
comment: 'Generated automatically by Ansible'
# An IPv6 subnet
example_ipv6_subnet:
subnet: 'dead:be:ef::/64'
ipv6: True
routers: 'dead:be:ef::1'
comment: "Example IPv6 subnet"
options: |
default-lease-time 300;
max-lease-time 7200;
.. _dhcpd_hosts:
dhcpd_hosts
-----------
String or list. If string, include an external file with host list in this
place of the configuration. If list, specify a list of dicts describing the
hosts. Each dict can have following keys:
``hostname``
Name of the host
``ethernet``
Ethernet address of this host
``address``
IP address of this host
``comment``
A comment added to this host in the configuration
``options``
Custom options in the text block format for this host
Examples::
# External file with list of hosts
dhcpd_hosts: '/etc/dhcp/dhcp-hosts.conf'
# List of hosts
dhcpd_hosts:
- hostname: 'examplehost'
address: '10.0.10.1'
ethernet: '00:00:00:00:00:00'
.. _dhcpd_includes:
dhcpd_includes
--------------
List of external files to include in DHCP configuration. Use absolute paths for
the files.
Examples::
dhcpd_includes:
- '/etc/dhcp/other-options.conf'
.. _dhcpd_failovers:
dhcpd_failovers
---------------
Each 'failover pair' declaration consists of primary and secondary host,
no more than two nodes failover is currently allowed by ``isc-dhcpd``.
You must specify which failover pair each pool should use by specifying
a 'failover peer' statement under an ``options`` block in each pool
declaration. e.g::
dhcpd_failovers:
- failover: "my-failover"
primary: '10.0.30.1'
secondary: '10.0.30.2'
...
dhcpd_subnets:
- subnet: ...
...
pools:
- comment: "My pool with failover"
range: '10.0.30.10 10.0.30.20'
options: |
failover peer "my-failover";
Each failover declaration has a set of an mandatory fields, which is:
``primary``
Ansible inventory name of a primary DHCP host, if you need failover to work
on different IP, see ``primary_fo_addr`` option below.
``secondary``
Ansible inventory name of a secondary DHCP host, if you need failover to work
on different IP, see secondary_fo_addr option below.
Ansible inventory name is either IP ot hostname specified in inventory file.
``mclt``
Max Client Lead Time. The maximum amount of time that one server can extend
a lease for a DHCP client beyond the time known by the partner server.
Default value: ``3600``
Split configuration between two failover DHCP servers:
``split``
Percentage value between ``0`` and ``255``.
Specifies the split between the primary and secondary servers for the
purposes of load balancing. Whenever a client makes a DHCP request, the DHCP
server runs a hash on the client identification, resulting in value from 0 to
255. This is used as an index into a 256 bit field. If the bit at that index
is set, the primary is responsible. If the bit at that index is not set, the
secondary is responsible. Instead of ``split``, you can use ``hba``.
``hba``
32 character string in the regexp: ``([0-9a-f]{2}:){32}``
Specifies the split between the primary and secondary as a bitmap rather than
a cutoff, which theoretically allows for finer-grained control. In practice,
there is probably no need for such fine-grained control, however.
You must use either 'split' or 'hba' statement. Split has a preference, so
if it's defined, 'hba' will be omitted by configuration template.
``max_response_delay``
Tells the DHCP server how many seconds may pass without receiving a message
from its failover peer before it assumes that connection has failed. This is
mandatory according to ``dhcpd.conf`` man page.
Default value: ``5``
``max_unacked_updates``
Tells the remote DHCP server how many ``BNDUPD`` messages it can send before
it receives a ``BNDACK`` from the local system. This is mandatory according
to ``dhcpd.conf`` man page.
Default value: ``10``
Optional field are mostly desribed in ``dhcpd.conf`` man page:
``port``
Specifies port on which primary and secondary nodes will listen for failover
connection. Diffirent ports for primary and secondary is currently
unsupported.
Default value: ``647``
``primary_fo_addr``
IP/Hostname of a primary DHCP host. This option is used if you need
failover address be different from ansible inventory IP/hostname. If
omitted, then ``primary`` is used.
``secondary_fo_addr``
IP/Hostname of a secondary DHCP host. This option is used if you need
failover address be different from ansible inventory IP/hostname. If
omitted, then ``secondary`` is used.
``auto_partner_down``
Number of seconds to start serving partners IPs after the partner's failure.
Other parameters::
load_balance_max_seconds: 5
max_lease_misbalance: 15
max_lease_ownership: 10
min_balance: 60
max_balance: 3600
Examples::
# Full cluster configuration
dhcpd_failovers:
- failover: 'failover-localsubnet'
primary: '10.0.10.1'
primary_fo_addr: '10.5.10.1'
secondary: '10.0.10.2'
secondary_fo_addr: '10.5.10.2'
port: 1337
split: 128
hba: aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa:aa
max_response_delay: 5
max_unacked_updates: 10
load_balance_max_seconds: 5
auto_partner_down: 0
max_lease_misbalance: 15
max_lease_ownership: 10
min_balance: 60
max_balance: 3600
# Minimal cluster configuration
dhcpd_failovers:
- failover: 'failover-san'
primary: '10.0.10.1'
secondary: '10.0.10.2'
mclt: 3600
split: 128
max_response_delay: 5
max_unacked_updates: 10

18
docs/getting-started.rst Normal file
View File

@ -0,0 +1,18 @@
Getting started
===============
By default ``debops.dhcpd`` installs a DHCP server with some default
configuration. Server will not be authoritative, and will have a default subnet
configuration taken from ``ansible_default_ipv4`` network configuration.
An example playbook which uses ``debops.dhcpd`` role::
---
- name: Manage DHCP server
hosts: debops_dhcpd
roles:
- role: debops.dhcpd
tags: dhcpd

6
docs/guides.rst Normal file
View File

@ -0,0 +1,6 @@
Guides and examples
===================
This section will contain guides for configuring ``debops.dhcpd`` in various
scenarios.

22
docs/index.rst Normal file
View File

@ -0,0 +1,22 @@
debops.dhcpd
============
.. toctree::
:maxdepth: 3
introduction
installation
getting-started
defaults
defaults-configuration
guides
troubleshooting
copyright
credits
changelog
..
Local Variables:
mode: rst
ispell-local-dictionary: "american"
End:

7
docs/installation.rst Normal file
View File

@ -0,0 +1,7 @@
Installation
============
This role requires at least Ansible ``v1.7.0``. To install it, run::
ansible-galaxy install debops.dhcpd

18
docs/introduction.rst Normal file
View File

@ -0,0 +1,18 @@
Introduction
============
``debops.dhcpd`` role can be used to configure an `ISC DHCP Server`_ as
standalone or in a 2-host failover configuration. Alternatively, you can
configure an DHCP relay on a host connected to multiple networks which will
relay DHCP/BOOTP messages to your DHCP server.
``dhcp-probe`` script will be used to scan the network for unauthorized DHCP
servers and notify administrators if they are found.
.. _ISC DHCP Server: https://www.isc.org/downloads/dhcp/
..
Local Variables:
mode: rst
ispell-local-dictionary: "american"
End:

6
docs/troubleshooting.rst Normal file
View File

@ -0,0 +1,6 @@
Troubleshooting
===============
This section will contain information about fixing issues with
``debops.dhcpd`` role.

View File

@ -1,6 +1,23 @@
--- ---
- name: Restart isc-dhcp-server - name: Restart isc-dhcp-server
service: name=isc-dhcp-server state=restarted service:
name: 'isc-dhcp-server'
state: 'restarted'
- name: Restart isc-dhcp-relay
service:
name: 'isc-dhcp-relay'
state: 'restarted'
- name: Restart dhcp-probe
service:
name: 'dhcp-probe'
state: 'stopped'
notify: [ 'Start dhcp-probe' ]
- name: Start dhcp-probe
service:
name: 'dhcp-probe'
state: 'started'

View File

@ -17,5 +17,11 @@ ansigenome_info:
github: 'drybjed' github: 'drybjed'
synopsis: | synopsis: |
Install and configure [ISC DHCP Server](https://www.isc.org/downloads/dhcp/). `debops.dhcpd` role can be used to configure an [ISC DHCP
Server](https://www.isc.org/downloads/dhcp/) as standalone or in a 2-host
failover configuration. Alternatively, you can configure an DHCP relay on
a host connected to multiple networks which will relay DHCP/BOOTP messages
to your DHCP server.
`dhcp-probe` script will be used to scan the network for unauthorized DHCP
servers and notify administrators if they are found.

View File

@ -1,6 +1,8 @@
--- ---
dependencies: [] dependencies:
- role: debops.secret
galaxy_info: galaxy_info:
author: 'Maciej Delmanowski' author: 'Maciej Delmanowski'

28
tasks/dhcp-probe.yml Normal file
View File

@ -0,0 +1,28 @@
---
- name: Create dhcp-probe lib directory
file:
path: '{{ ansible_local.root.lib + "/dhcp-probe" }}'
state: 'directory'
owner: 'root'
group: 'root'
mode: '0755'
- name: Manage dhcp-probe notification scripts
template:
src: 'usr/local/lib/dhcp-probe/{{ item }}.j2'
dest: '{{ ansible_local.root.lib + "/dhcp-probe/" + item }}'
owner: 'root'
group: 'root'
mode: '0755'
with_items: [ 'dhcp_probe_notify2', 'mail-throttled' ]
- name: Configure dhcp-probe
template:
src: 'etc/dhcp_probe.cf.j2'
dest: '/etc/dhcp_probe.cf'
owner: 'root'
group: 'root'
mode: '0644'
notify: [ 'Restart dhcp-probe' ]

View File

@ -1,13 +1,58 @@
--- ---
- name: Install DHCP server packages - name: Configure DHCP relay in debconf
apt: pkg={{ item }} state=latest install_recommends=no debconf:
with_items: [ 'isc-dhcp-server' ] name: 'isc-dhcp-relay'
question: 'isc-dhcp-relay/{{ item.key }}'
vtype: 'string'
value: '{{ item.value }}'
with_dict:
servers: '{{ dhcpd_relay_servers | join(" ") }}'
interfaces: '{{ dhcpd_relay_interfaces | join(" ") }}'
options: '{{ dhcpd_relay_options }}'
register: dhcpd_register_relay_debconf
when: dhcpd_mode == 'relay'
- name: Install DHCP packages
apt:
name: '{{ item }}'
state: 'present'
install_recommends: False
with_flattened:
- dhcpd_base_packages_map[dhcpd_mode]
- [ '{{ "dhcp-probe" if (dhcpd_probe|d() and dhcpd_probe) else [] }}' ]
- name: Reconfigure ISC DHCP relay
command: dpkg-reconfigure --frontend=noninteractive isc-dhcp-relay
notify: [ 'Restart isc-dhcp-relay' ]
when: dhcpd_register_relay_debconf|d() and dhcpd_register_relay_debconf.changed
- name: Get list of nameservers configured in /etc/resolv.conf
shell: grep -E '^nameserver\s' /etc/resolv.conf | awk '{print $2}' | sed -e 'N;s/\n/ /'
register: dhcpd_register_nameservers
changed_when: False
when: dhcpd_mode == 'server'
- name: Convert list of nameservers to Ansible list
set_fact:
dhcpd_runtime_nameservers: "{{ dhcpd_register_nameservers.stdout.split(' ') }}"
when: (dhcpd_register_nameservers is defined and dhcpd_register_nameservers.stdout)
- name: Configure DHCP server - name: Configure DHCP server
template: src={{ item }}.j2 dest=/{{ item }} owner=root group=root mode=0644 template:
src: '{{ item }}.j2'
dest: '/{{ item }}'
owner: 'root'
group: 'root'
mode: '0644'
with_items: [ 'etc/default/isc-dhcp-server', 'etc/dhcp/dhcpd.conf' ] with_items: [ 'etc/default/isc-dhcp-server', 'etc/dhcp/dhcpd.conf' ]
notify: Restart isc-dhcp-server notify: [ 'Restart isc-dhcp-server' ]
when: dhcpd_mode == 'server'
- name: Make sure that IPv6 lease file exists
command: touch /var/lib/dhcp/dhcpd6.leases creates=/var/lib/dhcp/dhcpd6.leases
when: dhcpd_ipversion == '6'
- include: dhcp-probe.yml
when: dhcpd_probe|d() and dhcpd_probe

View File

@ -16,7 +16,7 @@
# Additional options to start dhcpd with. # Additional options to start dhcpd with.
# Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead # Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead
#OPTIONS="" OPTIONS="{{ dhcpd_server_options }}"
# On what interfaces should the DHCP server (dhcpd) serve DHCP requests? # On what interfaces should the DHCP server (dhcpd) serve DHCP requests?
# Separate multiple interfaces with spaces, e.g. "eth0 eth1". # Separate multiple interfaces with spaces, e.g. "eth0 eth1".

View File

@ -0,0 +1,30 @@
{% if dhcpd_domain_name|d() and dhcpd_domain_name %}
{% set dhcpd_tpl_domain_search = [ dhcpd_domain_name ] + dhcpd_domain_search|d([]) %}
option domain-name "{{ dhcpd_domain_name }}";
option domain-search "{{ dhcpd_tpl_domain_search | join('", "') }}";
option dhcp6.domain-search "{{ dhcpd_tpl_domain_search | join('", "') }}";
{% endif %}
{% if dhcpd_nameservers|d() and dhcpd_nameservers %}
{% set dhcpd_tpl_nameservers = dhcpd_nameservers %}
{% elif dhcpd_runtime_nameservers|d() and dhcpd_runtime_nameservers %}
{% set dhcpd_tpl_nameservers = [] %}
{% for server in dhcpd_runtime_nameservers %}
{% if server not in [ '127.0.0.1', '::1' ] %}
{% set _ = dhcpd_tpl_nameservers.append(server) %}
{% endif %}
{% endfor %}
{% endif %}
{% if dhcpd_tpl_nameservers %}
{% if dhcpd_tpl_nameservers | ipv4 %}
option domain-name-servers {{ dhcpd_tpl_nameservers | ipv4 | join(", ") }};
{% endif %}
{% if dhcpd_tpl_nameservers | ipv6 %}
option dhcp6.name-servers {{ dhcpd_tpl_nameservers | ipv6 | join(", ") }};
{% endif %}
{% endif %}
{#
vim: ft=dhcpd
#}

View File

@ -1,192 +1,5 @@
# This file is managed by Ansible, all changes will be lost {% import 'macros.j2' as print with context %}
{% macro print_class(class) %} # {{ ansible_managed }}
{% if class.comment is defined and class.comment %}
# {{ class.comment }}
{% endif %}
class "{{ class.class }}" {
{% if class.options is defined and class.options %}
{{ class.options | indent(8,true) }}
{% endif %}
{% if class.include is defined and class.include %}
include "{{ class.include }}";
{% endif %}
}
{% if class.subclass is defined and class.subclass %}
{% for key, value in class.subclass.iteritems() %}
{% if value is defined and value %}
subclass "{{ class.class }}" "{{ key }}" {
{{ value | indent(8,true) }}
}
{% else %}
subclass "{{ class.class }}" {{ key }};
{% endif %}
{% endfor %}
{% endif %}
{% endmacro %}
{% macro print_group(group) %}
{% if group.comment is defined and group.comment %}
# {{ group.comment }}
{% endif %}
group {
{% if group.options is defined and group.options %}
{{ group.options | indent(8,true) }}
{% endif %}
{% if group.include is defined and group.include %}
include "{{ group.include }}";
{% endif %}
{% if group.groups is defined and group.groups %}
{% for group in group.groups %}
{{ print_group(group) | indent(8, true) }}
{% endfor %}
{% endif %}
{% if group.subnets is defined and group.subnets %}
{% for subnet in group.subnets %}
{{ print_subnet(subnet) | indent(8, true) }}
{% endfor %}
{% endif %}
{% if group.hosts is defined and group.hosts %}
{{ print_hosts(group.hosts) | indent(8, true) }}
{% endif %}
}
{% endmacro %}
{% macro print_subnet(subnet) %}
{% if subnet.comment is defined and subnet.comment %}
# {{ subnet.comment }}
{% endif %}
{% if subnet.ipv6 is defined and subnet.ipv6 %}
subnet6 {{ subnet.subnet }} {
{% else %}
subnet {{ subnet.subnet }} netmask {{ subnet.netmask }} {
{% endif %}
{% if subnet.routers is defined and subnet.routers %}
{% if subnet.routers is string %}
option routers {{ subnet.routers }};
{% else %}
option routers {{ subnet.routers | join(' ') }};
{% endif %}
{% endif %}
{% if subnet.options is defined and subnet.options %}
{{ subnet.options | indent(8,true) }}
{% endif %}
{% if subnet.include is defined and subnet.include %}
include "{{ subnet.include }}";
{% endif %}
{% if subnet.pools is defined and subnet.pools %}
{% for pool in subnet.pools %}
pool {
{% if pool.comment is defined and pool.comment %}
# {{ pool.comment }}
{% endif %}
{% if subnet.ipv6 is defined and subnet.ipv6 %}
range6 {{ pool.range }};
{% else %}
range {{ pool.range }};
{% endif %}
{% if pool.options is defined and pool.options %}
{{ pool.options | indent(16,true) }}
{% endif %}
{% if pool.include is defined and pool.include %}
include "{{ pool.include }}";
{% endif %}
}
{% endfor %}
{% endif %}
{% if subnet.hosts is defined and subnet.hosts %}
{{ print_hosts(subnet.hosts) | indent(8, true) }}
{% endif %}
}
{% endmacro %}
{% macro print_hosts(hosts) %}
{% if hosts is string %}
include "{{ hosts }}";
{% else %}
{% for host in hosts %}
{% if host.comment is defined and host.comment %}
# {{ host.comment }}
{% endif %}
host {{ host.hostname }} {
{% if host.options is defined and host.options %}
{{ host.options | indent(8,true) }}
{% endif %}
{% if host.ethernet is defined and host.ethernet %}
hardware ethernet {{ host.ethernet }};
{% endif %}
{% if host.address is defined and host.address %}
fixed-address {{ host.address }};
{% endif %}
}
{% endfor %}
{% endif %}
{% endmacro %}
{% macro print_failover(failover) %}
{% if failover.comment is defined and failover.comment %}
# {{ failover.comment }}
{% endif %}
failover peer "{{ failover.failover }}" {
{% if failover.primary is defined and failover.primary == inventory_hostname %}
primary;
mclt {{ failover.mclt|default(3600) }};
{% if failover.primary_fo_addr is defined and failover.primary_fo_addr %}
address {{ failover.primary_fo_addr }};
{% else %}
address {{ failover.primary }};
{% endif %}
{% if failover.secondary_fo_addr is defined and failover.secondary_fo_addr %}
peer address {{ failover.secondary_fo_addr }};
{% else %}
peer address {{ failover.secondary }};
{% endif %}
{% if failover.split is defined and failover.split %}
split {{ failover.split }};
{% elif failover.hba is defined and failover.hba %}
hba {{ failover.hba }};
{% endif %}
{% else %}
secondary;
{% if failover.secondary_fo_addr is defined and failover.secondary_fo_addr %}
address {{ failover.secondary_fo_addr }};
{% else %}
address {{ failover.secondary }};
{% endif %}
{% if failover.primary_fo_addr is defined and failover.primary_fo_addr %}
peer address {{ failover.primary_fo_addr }};
{% else %}
peer address {{ failover.primary }};
{% endif %}
{% endif %}
max-response-delay {{ failover.max_response_delay|default(30) }};
max-unacked-updates {{ failover.max_unacked_updates|default(10) }};
{% if failover.load_balance_max_seconds is defined and failover.load_balance_max_seconds %}
load balance max seconds {{ failover.load_balance_max_seconds }};
{% endif %}
{% if failover.max_lease_misbalance is defined and failover.max_lease_misbalance %}
max-lease-misbalance {{ failover.max_lease_misbalance }};
{% endif %}
{% if failover.max_lease_ownership is defined and failover.max_lease_ownership %}
max-lease-ownership {{ failover.max_lease_ownership }};
{% endif %}
{% if failover.min_balance is defined and failover.min_balance %}
min-balance {{ failover.min_balance }};
{% endif %}
{% if failover.max_balance is defined and failover.max_balance %}
max-balance {{ failover.max_balance }};
{% endif %}
{% if failover.auto_partner_down is defined and failover.auto_partner_down %}
auto-partner-down {{ failover.auto_partner_down }};
{% endif %}
}
{% endmacro %}
{% macro print_key(key) %}
{% if key.comment is defined and key.comment %}
# {{ key.comment }}
{% endif %}
key "{{ key.key }}" {
algorithm {{ key.algorithm|default('hmac-md5') }};
secret {{ key.secret }};
}
{% endmacro %}
{% if dhcpd_authoritative is defined and dhcpd_authoritative %} {% if dhcpd_authoritative is defined and dhcpd_authoritative %}
authoritative; authoritative;
@ -195,10 +8,19 @@ authoritative;
not authoritative; not authoritative;
{% endif %} {% endif %}
{% if dhcpd_global_options is defined and dhcpd_global_options %} {% if dhcpd_global_default_lease_time|d() and dhcpd_global_default_lease_time %}
# Global configuration options default-lease-time {{ dhcpd_global_default_lease_time }};
{{ dhcpd_global_options }} {% endif %}
{% if dhcpd_global_max_lease_time|d() and dhcpd_global_max_lease_time %}
max-lease-time {{ dhcpd_global_max_lease_time }};
{% endif %}
{% if dhcpd_log_facility|d() and dhcpd_log_facility %}
log-facility {{ dhcpd_log_facility }};
{% endif %}
{% if dhcpd_auto_options|d() and dhcpd_auto_options %}
{% include 'auto_options.j2' %}
{% endif %} {% endif %}
{% if dhcpd_options is defined and dhcpd_options %} {% if dhcpd_options is defined and dhcpd_options %}
# Configuration options # Configuration options
@ -207,17 +29,22 @@ not authoritative;
{% endif %} {% endif %}
{% if dhcpd_keys is defined and dhcpd_keys %} {% if dhcpd_keys is defined and dhcpd_keys %}
{% for key in dhcpd_keys %} {% for key in dhcpd_keys %}
{{ print_key(key) }} {{ print.key(key) }}
{% endfor %}
{% endif %}
{% if dhcpd_zones is defined and dhcpd_zones %}
{% for zone in dhcpd_zones %}
{{ print.zone(zone) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if dhcpd_classes is defined and dhcpd_classes %} {% if dhcpd_classes is defined and dhcpd_classes %}
{% for class in dhcpd_classes %} {% for class in dhcpd_classes %}
{{ print_class(class) }} {{ print.class(class) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if dhcpd_failovers is defined and dhcpd_failovers %} {% if dhcpd_failovers is defined and dhcpd_failovers %}
{% for failover in dhcpd_failovers %} {% for failover in dhcpd_failovers %}
{{ print_failover(failover) }} {{ print.failover(failover) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if dhcpd_shared_networks is defined and dhcpd_shared_networks %} {% if dhcpd_shared_networks is defined and dhcpd_shared_networks %}
@ -235,7 +62,7 @@ shared-network "{{ network.name }}" {
{% endif %} {% endif %}
{% for subnet in network.subnets %} {% for subnet in network.subnets %}
{{ print_subnet(subnet) | indent(8,true) }} {{ print.subnet(subnet) | indent(8,true) }}
{% endfor %} {% endfor %}
} }
@ -244,16 +71,16 @@ shared-network "{{ network.name }}" {
{% endif %} {% endif %}
{% if dhcpd_groups is defined and dhcpd_groups %} {% if dhcpd_groups is defined and dhcpd_groups %}
{% for group in dhcpd_groups %} {% for group in dhcpd_groups %}
{{ print_group(group) }} {{ print.group(group) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if dhcpd_subnets is defined and dhcpd_subnets %} {% if dhcpd_subnets is defined and dhcpd_subnets %}
{% for subnet in dhcpd_subnets %} {% for subnet in dhcpd_subnets %}
{{ print_subnet(subnet) }} {{ print.subnet(subnet) }}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if dhcpd_hosts is defined and dhcpd_hosts %} {% if dhcpd_hosts is defined and dhcpd_hosts %}
{{ print_hosts(dhcpd_hosts) }} {{ print.hosts(dhcpd_hosts) }}
{% endif %} {% endif %}
{% if dhcpd_includes is defined and dhcpd_includes %} {% if dhcpd_includes is defined and dhcpd_includes %}
{% for include in dhcpd_includes %} {% for include in dhcpd_includes %}

View File

@ -0,0 +1,231 @@
{#
# List of macros for ISC DHCP configuration, IPv4
# ===============================================
#
# ---- Macro: print.class() ----
#}
{% macro class(class) %}
{% if class.comment is defined and class.comment %}
# {{ class.comment }}
{% endif %}
class "{{ class.class }}" {
{% if class.options is defined and class.options %}
{{ class.options | indent(8,true) }}
{% endif %}
{% if class.include is defined and class.include %}
include "{{ class.include }}";
{% endif %}
}
{% if class.subclass is defined and class.subclass %}
{% for key, value in class.subclass.iteritems() %}
{% if value is defined and value %}
subclass "{{ class.class }}" "{{ key }}" {
{{ value | indent(8,true) }}
}
{% else %}
subclass "{{ class.class }}" {{ key }};
{% endif %}
{% endfor %}
{% endif %}
{% endmacro %}
{#
#
# ---- Macro: print.group() ----
#}
{% macro group(group) %}
{% if group.comment is defined and group.comment %}
# {{ group.comment }}
{% endif %}
group {
{% if group.options is defined and group.options %}
{{ group.options | indent(8,true) }}
{% endif %}
{% if group.include is defined and group.include %}
include "{{ group.include }}";
{% endif %}
{% if group.groups is defined and group.groups %}
{% for group in group.groups %}
{{ print.group(group) | indent(8, true) }}
{% endfor %}
{% endif %}
{% if group.subnets is defined and group.subnets %}
{% for subnet in group.subnets %}
{{ print.subnet(subnet) | indent(8, true) }}
{% endfor %}
{% endif %}
{% if group.hosts is defined and group.hosts %}
{{ print.hosts(group.hosts) | indent(8, true) }}
{% endif %}
}
{% endmacro %}
{#
#
# ---- Macro: print.subnet() ----
#}
{% macro subnet(subnet) %}
{% if subnet.comment is defined and subnet.comment %}
# {{ subnet.comment }}
{% endif %}
{% if dhcpd_ipversion is defined and dhcpd_ipversion == '6' %}
subnet6 {{ subnet.subnet | ipaddr('network') + '/' + subnet.subnet | ipaddr('prefix') | string }} {
{% else %}
subnet {{ subnet.subnet | ipaddr('cidr') | ipaddr('network') }} netmask {{ subnet.netmask | default(subnet.subnet | ipaddr('netmask')) }} {
{% endif %}
{% if subnet.routers is defined and subnet.routers %}
{% if subnet.routers is string %}
option routers {{ subnet.routers }};
{% else %}
option routers {{ subnet.routers | join(', ') }};
{% endif %}
{% endif %}
{% if subnet.options is defined and subnet.options %}
{{ subnet.options | indent(8,true) }}
{% endif %}
{% if subnet.include is defined and subnet.include %}
include "{{ subnet.include }}";
{% endif %}
{% if subnet.pools is defined and subnet.pools %}
{% for pool in subnet.pools %}
pool {
{% if pool.comment is defined and pool.comment %}
# {{ pool.comment }}
{% endif %}
{% if dhcpd_ipversion is defined and dhcpd_ipversion == '6' %}
range6 {{ pool.range }};
{% else %}
range {{ pool.range }};
{% endif %}
{% if pool.options is defined and pool.options %}
{{ pool.options | indent(16,true) }}
{% endif %}
{% if pool.include is defined and pool.include %}
include "{{ pool.include }}";
{% endif %}
}
{% endfor %}
{% endif %}
{% if subnet.hosts is defined and subnet.hosts %}
{{ print.hosts(subnet.hosts) | indent(8, true) }}
{% endif %}
}
{% endmacro %}
{#
#
# ---- Macro: print.hosts() ----
#}
{% macro hosts(hosts) %}
{% if hosts is string %}
include "{{ hosts }}";
{% else %}
{% for host in hosts %}
{% if host.comment is defined and host.comment %}
# {{ host.comment }}
{% endif %}
host {{ host.hostname }} {
{% if host.options is defined and host.options %}
{{ host.options | indent(8,true) }}
{% endif %}
{% if host.ethernet is defined and host.ethernet %}
hardware ethernet {{ host.ethernet }};
{% endif %}
{% if host.address is defined and host.address %}
{% if dhcpd_ipversion is defined and dhcpd_ipversion == '6' %}
fixed-address6 {{ host.address }};
{% else %}
fixed-address {{ host.address }};
{% endif %}
{% endif %}
}
{% endfor %}
{% endif %}
{% endmacro %}
{#
#
# ---- Macro: print.failover() ----
#}
{% macro failover(failover) %}
{% if failover.comment is defined and failover.comment %}
# {{ failover.comment }}
{% endif %}
failover peer "{{ failover.failover }}" {
{% if failover.primary is defined and failover.primary == inventory_hostname %}
primary;
mclt {{ failover.mclt|default(3600) }};
{% if failover.primary_fo_addr is defined and failover.primary_fo_addr %}
address {{ failover.primary_fo_addr }};
{% else %}
address {{ failover.primary }};
{% endif %}
{% if failover.secondary_fo_addr is defined and failover.secondary_fo_addr %}
peer address {{ failover.secondary_fo_addr }};
{% else %}
peer address {{ failover.secondary }};
{% endif %}
{% if failover.split is defined and failover.split %}
split {{ failover.split }};
{% elif failover.hba is defined and failover.hba %}
hba {{ failover.hba }};
{% endif %}
{% else %}
secondary;
{% if failover.secondary_fo_addr is defined and failover.secondary_fo_addr %}
address {{ failover.secondary_fo_addr }};
{% else %}
address {{ failover.secondary }};
{% endif %}
{% if failover.primary_fo_addr is defined and failover.primary_fo_addr %}
peer address {{ failover.primary_fo_addr }};
{% else %}
peer address {{ failover.primary }};
{% endif %}
{% endif %}
max-response-delay {{ failover.max_response_delay|default(30) }};
max-unacked-updates {{ failover.max_unacked_updates|default(10) }};
{% if failover.load_balance_max_seconds is defined and failover.load_balance_max_seconds %}
load balance max seconds {{ failover.load_balance_max_seconds }};
{% endif %}
{% if failover.max_lease_misbalance is defined and failover.max_lease_misbalance %}
max-lease-misbalance {{ failover.max_lease_misbalance }};
{% endif %}
{% if failover.max_lease_ownership is defined and failover.max_lease_ownership %}
max-lease-ownership {{ failover.max_lease_ownership }};
{% endif %}
{% if failover.min_balance is defined and failover.min_balance %}
min-balance {{ failover.min_balance }};
{% endif %}
{% if failover.max_balance is defined and failover.max_balance %}
max-balance {{ failover.max_balance }};
{% endif %}
{% if failover.auto_partner_down is defined and failover.auto_partner_down %}
auto-partner-down {{ failover.auto_partner_down }};
{% endif %}
}
{% endmacro %}
{#
#
# ---- Macro: print.key() ----
#}
{% macro key(key) %}
{% if key.comment is defined and key.comment %}
# {{ key.comment }}
{% endif %}
key {{ key.key }} {
algorithm {{ key.algorithm|default('hmac-md5') }};
secret {{ key.secret }};
}
{% endmacro %}
{#
#
# ---- Macro: print.zone() ----
#}
{% macro zone(zone) %}
{% if zone.comment is defined and zone.comment %}
# {{ zone.comment }}
{% endif %}
zone {{ zone.zone }} {
primary {{ zone.primary }};
key {{ zone.key }};
}
{% endmacro %}

View File

@ -0,0 +1,17 @@
# {{ ansible_managed }}
# Send mail messages about unauthorized DHCP servers
alert_program_name2 {{ ansible_local.root.lib + "/dhcp-probe/dhcp_probe_notify2" }}
{% if dhcpd_probe_legal_servers|d() and dhcpd_probe_legal_servers %}
# Legal DHCP servers
{% for address in dhcpd_probe_legal_servers %}
legal_server {{ address }}
{% endfor %}
{% endif %}
{% if dhcpd_probe_options|d() and dhcpd_probe_options %}
# Other options
{{ dhcpd_probe_options }}
{% endif %}

View File

@ -0,0 +1,247 @@
#!/usr/bin/perl -w
# {{ ansible_managed }}
# dhcp_probe_notify2 -p calling_prog_name -I interface_name -i IPaddress -m MACaddress [-y yiaddr]
#
# An external program called by dhcp_probe upon response of a response from an unexpected BootP/DHCP server.
#
# Called via specification of 'alert_program_name2' in /etc/dhcp_probe.cf file.
# This version obeys the syntax provided by the 'alert_program_name2' statement,
# not the syntax provided by the older 'alert_program_name' statement.
#
# Required options:
# -p calling_prog_name the name of the calling program (e.g. 'dhcp_probe')
# -I interface_name the name of the interface on which the unexpected response packet arrived (e.g. 'qfe0')
# -i IPaddress the IP source address of the unexpected response packet (e.g. '192.168.0.1')
# -m MACaddress the Ethernet source address of the unexpected response packet (e.g. '0:1:2:3:4:5')
#
# Optional options:
# -y yiaddr the response packet's non-zero yiaddr value, when it falls within a "Lease Network of Interest" (e.g. '172.16.1.2')
#
# May send email subject to throttling.
# May send page subject to throttling.
#
# You will need to edit the definitions below.
#
# Irwin Tillman
use Sys::Hostname;
use Sys::Syslog qw(:DEFAULT setlogsock);
use Time::HiRes qw(gettimeofday); # from CPAN
use Getopt::Std;
use strict;
#############################################################################
#
# Definitions you may need to edit
my $SYSLOG_FACILITY="daemon"; # name of facility to use if syslogging (e.g. 'daemon')
my $SYSLOG_OPT = 'pid,cons'; # comma-separated syslog options to use if syslogging (e.g. 'pid,cons') , ignored if not syslogging
use vars qw($VERBOSE);
$VERBOSE = 1; # set to true to produce more verbose messages
# We may send one piece of mail this way, subject to frequency throttling.
# Use this set of definitions for a piece of email that is delivered as regular email (not a page).
use vars qw($THROTTLE_MAIL_CMD $THROTTLE_MAIL_TIMEOUT $THROTTLE_MAIL_FROM $THROTTLE_MAIL_RECIPIENT $THROTTLE_MAIL_RECIPIENT_TEST $THROTTLE_MAIL_SUBJECT);
{% if dhcpd_probe_mail_to|d() and dhcpd_probe_mail_to %}
$THROTTLE_MAIL_CMD = "{{ ansible_local.root.lib + '/dhcp-probe/mail-throttled' }}"; # set to "" to disable
{% else %}
$THROTTLE_MAIL_CMD = ""; # set to "" to disable
{% endif %}
$THROTTLE_MAIL_TIMEOUT = {{ dhcpd_probe_mail_timeout | default('600') }}; # seconds
$THROTTLE_MAIL_FROM = "root"; # e.g. "root"
$THROTTLE_MAIL_RECIPIENT = "{{ dhcpd_probe_mail_to | join(' ') | replace('@','\@') }}"; # space-separated email addresses, remember to escape '@' characters
$THROTTLE_MAIL_SUBJECT = "Unexpected BOOTP/DHCP server";
# We may also send another piece of email this way, subject to frequency throttling.
# Use this set of definitions for a piece of email that is delivered to a pager (not as regular email).
use vars qw($THROTTLE_PAGE_CMD $THROTTLE_PAGE_TIMEOUT $THROTTLE_PAGE_RECIPIENT);
{% if dhcpd_probe_page_to|d() and dhcpd_probe_page_to %}
$THROTTLE_PAGE_CMD = "{{ ansible_local.root.lib + '/dhcp-probe/mail-throttled' }}"; # set to "" to disable
{% else %}
$THROTTLE_PAGE_CMD = ""; # set to "" to disable
{% endif %}
$THROTTLE_PAGE_TIMEOUT = {{ dhcpd_probe_page_timeout | default('600') }}; # seconds
$THROTTLE_PAGE_RECIPIENT = "{{ dhcpd_probe_page_to | join(' ') | replace('@','\@') }}"; # space-separated email addresses, remember to escape '@' characters
# End of definitions you may need to edit
#
#############################################################################
(my $prog = $0) =~ s/.*\///;
# init our use of syslog
# setlogsock('unix'); # talk to syslog with UNIX domain socket, not INET domain. XXX causes failure in Solaris 7
openlog($prog, $SYSLOG_OPT, $SYSLOG_FACILITY);
#############################################################################
#
# Parse options and arguments
# We must use getopt() instead of getopts() to avoid throwing an error
# if we are passed an unrecognized option.
# We must silently ignore unrecognized options to be forward compatible with enhancements to dhcp_probe.
use vars qw($opt_i $opt_I $opt_m $opt_p $opt_y);
# &getopt('p:i:I:m:y:');
&getopt('piImy');
# Required options
my $calling_program = $opt_p;
my $ifname = $opt_I;
my $ip_src = $opt_i;
my $ether_src = $opt_m;
#
# Optional options
my $yiaddr = $opt_y || "";
# Enforce presence of required options
unless ($calling_program) {
my_message('LOG_ERR', "${prog}: missing -p calling_program option");
exit 100;
}
unless ($ifname) {
my_message('LOG_ERR', "${prog}: missing -I interface_name option");
exit 101;
}
unless ($ip_src) {
my_message('LOG_ERR', "${prog}: missing -i ip_src_address option");
exit 102;
}
unless ($ether_src) {
my_message('LOG_ERR', "${prog}: missing -m ether_src_address option");
exit 103;
}
# Done parsing options and arguments
#
#############################################################################
# Miscellaneous Initialization
my $hostname = hostname();
my ($seconds, $microseconds) = gettimeofday; # from Time::HiRes on CPAN
my $timestamp = scalar(localtime($seconds));
$timestamp =~ s/(\d\d:\d\d:\d\d)/$1.$microseconds/; # glue microsends to end of seconds
# When we pass a key to THROTTLE_MAIL_CMD, we want the key to also include an indication
# of whether the yiaddr option was specified. (If we didn't include such an indication,
# the following could happen: The first response we detect from a rogue server doesn't
# distribute an "address of concern", so the yiaddr option wasn't specified. We alert
# based on that. The next response we detect from a rogue server does distribute an
# "address of concern", so the yiaddr is specified. Our alert gets throttled since
# the first alert we sent was "recent". But this means the administrator doesn't get
# notified that the rogue distributed an "address of interest". So we need to
# include the state of yiaddr option in the key we pass to THROTTLE_MAIL_CMD,
# to ensure the second response isn't throttled using the same key as the first response.)
#
# We can't just use the value of yiaddr itself as the string to incorporate into the key,
# as that would result in a unqiue key for each distributed IP address. Instead, we
# will use the state of yiaddr (set or unset).
#
# Note this means that when a rogue DHCP server is distributing IP addresses that fall into "Networks of Concern",
# we will very likely send more than one notification for it within each throttle period.
# That's because while some of the responses from the rogue will have a yiaddr within a "Networks of Concern",
# others (for example, DHCPNAK responses) will not. This is unfortunate, but is better than the
# alternative approach (of not taking into account the yiaddr state in the key), since that will
# sometimes cause you to not be alerted at all to the "yiaddr falls into a Network of Concern" situation.
#
# Create a string based on the state of yiaddr, for later incorporation into the key.
my $yiaddr_option_state = $yiaddr ? "yiaddr=set" : "yiaddr=unset";
#############################################################################
if ($THROTTLE_MAIL_CMD) {
# This command suppresses the message if it's sent a message to 'key' within 'throttle_seconds'
# I use the calling program's name and the offender's hardware address as the key.
my $subject_yiaddr_addendum = "";
$subject_yiaddr_addendum = ", YIADDR=$yiaddr" if $yiaddr;
unless (open(THROTTLE_MAIL, "| $THROTTLE_MAIL_CMD -l -k ${calling_program}_mail_${ether_src}_$yiaddr_option_state -t $THROTTLE_MAIL_TIMEOUT -f \"$THROTTLE_MAIL_FROM\" -r \"$THROTTLE_MAIL_RECIPIENT\" -s\"$THROTTLE_MAIL_SUBJECT (MAC=${ether_src}, IP=${ip_src}${subject_yiaddr_addendum})\"")) {
my_message('LOG_ERR', "${prog}: failure trying to send throttled email: can't execute '${THROTTLE_MAIL_CMD}': open(): $!");
exit 20;
}
print THROTTLE_MAIL
$timestamp, "\n",
"\n",
"$calling_program detected an unexpected BOOTP/DHCP server.\n",
"Host=$hostname, interface=${ifname}, IP source=${ip_src}, Ethernet source=${ether_src}\n";
print THROTTLE_MAIL "\nThis means that *there is* a rogue BOOTP/DHCP server operating.\n" if $VERBOSE;
if ($yiaddr) {
print THROTTLE_MAIL
"The server distributed IP address $yiaddr, which falls into a network of special concern.\n";
}
unless (close(THROTTLE_MAIL)) {
my_message('LOG_ERR',
"${prog}: failure trying to send throttled email: error executing '${THROTTLE_MAIL_CMD}': close(): " .
($! ?
"syserr closing pipe: $!"
:
"wait status $? from pipe"
) .
"\n"
);
exit 21;
}
}
if ($THROTTLE_PAGE_CMD) {
# This command suppresses the message if it's sent a message to 'key' within 'throttle_seconds'
# I use the calling program's name and the offender's hardware address as the key.
unless (open(THROTTLE_PAGE, "| $THROTTLE_PAGE_CMD -l -k ${calling_program}_page_${ether_src}_$yiaddr_option_state -t $THROTTLE_PAGE_TIMEOUT -r \"$THROTTLE_PAGE_RECIPIENT\"")) {
my_message('LOG_ERR', "${prog}: failure trying to send throttled page: can't execute '${THROTTLE_PAGE_CMD}': open(): $!\n");
exit 30;
}
print THROTTLE_PAGE "Rogue DHCP server IP=$ip_src MAC=$ether_src seen via host $hostname interface $ifname\n";
print THROTTLE_PAGE "This means *there is* a rogue BOOTP/DHCP server operating.\n" if $VERBOSE;
if ($yiaddr) {
print THROTTLE_PAGE
"Rogue server distributed yiaddr=$yiaddr, a special concern.\n";
}
unless (close(THROTTLE_PAGE)) {
my_message('LOG_ERR',
"${prog}: failure trying to send throttled page: error executing '${THROTTLE_PAGE_CMD}': close(): " .
($! ?
"syserr closing pipe: $!"
:
"wait status $? from pipe"
) .
"\n"
);
exit 31;
}
}
exit 0;
#############################################################################
sub my_message {
# Call with a syslog priority constant and a message string.
# We write the message to syslog, using the specified priority.
# Your message should not contain a newline.
my($priority, $msg) = @_;
syslog($priority, $msg);
return;
}

View File

@ -0,0 +1,173 @@
#!/usr/bin/perl -w
# {{ ansible_managed }}
# $Header: /usr/local/etc/RCS/mail-throttled,v 1.11 2008/12/06 01:50:28 root Exp $
# mail-throttled [-l] [-D dbm_file] -k key -t throttle_seconds [-f from] -r recipient [-d] [-T tie_attempts_max] [-S tie_retry_sleep] [-s subject]
#
# Sends mail body (read from STDIN) to 'recipient', but avoids doing so "too frequently."
#
# You provide a 'key', which is an arbitrary string used to identify this notification.
# You also provide 'throttle_seconds', an integer. If we've sent anything that
# specified this 'key' within the last 'throttle_seconds', we do not send the message.
# Otherwise, we send the message, and the remember that we've sent a message for this 'key'
# at the current time.
#
# This key/timesent tuples are stored on-disk, in a dbm. As a result, the 'key'
# you supply must satisfy the syntactic requirements for dbm keys.
# The caller needs to have permission to read and write this DBM (and create it if
# it does not already exist). If you fail to specify a dbm_file, we'll use a default
# value, which may not be what you want (since the caller might not be able to r/w that
# particular DBM).
# We never clean this dbm. You can safely erase it entirely, if you don't mind losing
# the state, and you know the caller has permission to create a new instance of the DBM.
#
# The 'recipient' should be a valid email address. Naturally, it should not
# be one that will cause any ack or bounce mail to return to us!
# If there are several addresses (delimited by spaces), be sure to quote them as a single arg.
#
# If a subject is specified, be sure to quote it if it contains any spaces or other shell
# metachars.
#
# If -l is specified, then any errors, warnings, or debugging output is written to syslog
# in addition to its usual destination (STDERR). This is helpful if you call this from an
# environment where STDERR may get lost.
#
# Irwin Tillman
use Getopt::Std;
use GDBM_File;
use Errno qw(EAGAIN);
use Sys::Syslog qw(:DEFAULT setlogsock);
use strict;
use warnings;
use vars qw($DBM_FILE_DEFAULT $MAILCMD $MAILCMD_OPTS $FROM_DEFAULT);
$DBM_FILE_DEFAULT = '{{ ansible_local.root.lib + "/dhcp-probe/mail-throttled.gdbm" }}';
$MAILCMD = "/usr/lib/sendmail";
# $MAILCMD_OPTS = "-t -ODeliveryMode=queueonly";
$MAILCMD_OPTS = "-t";
$FROM_DEFAULT = "root";
my $SYSLOG_FACILITY="daemon"; # name of facility to use if syslogging
my $SYSLOG_OPT = 'pid,cons'; # syslog options to use if syslogging, ignored otherwise
my $SYSLOG_PRIORITY = 'LOG_ERR';
# The tie() call sometimes fails with EAGAIN.
# Perhaps that's due to some other process having the DBM open;
# in fact, that may be more likely if the process that calls us may calls us multiple times in quick succession.
# So when tie() fails with EAGAIN, we can sleep and retry some number of times before giving up entirely.
my $TIE_ATTEMPTS_MAX = 3; # number of times to try tie() before giving up
my $TIE_RETRY_SLEEP = 1; # seconds to sleep before retrying tie()
(my $prog = $0) =~ s/.*\///;
use vars qw($opt_f $opt_D $opt_d $opt_k $opt_l $opt_r $opt_s $opt_S $opt_t $opt_T);
&getopts('dD:f:k:lr:s:S:t:T:');
my $debug = $opt_d || "";
my $dbm_file = $opt_D || $DBM_FILE_DEFAULT;
my $key = $opt_k || "";
my $from = $opt_f || $FROM_DEFAULT;
my $recipient = $opt_r || "";
my $throttle_secs = $opt_t || 1;
my $subject = $opt_s || "";
my $also_syslog = $opt_l || "";
my $tie_attempts_max = $opt_T || $TIE_ATTEMPTS_MAX; # we deliberately override if CLI option specifies 0, as that makes no sense
my $tie_retry_sleep = $opt_S || $TIE_RETRY_SLEEP;
if ($also_syslog) {
# init our use of syslog
# setlogsock('unix'); # talk to syslog with UNIX domain socket, not INET domain. XXX causes failure in Solaris 7
openlog($prog, $SYSLOG_OPT, $SYSLOG_FACILITY);
}
my_warn("${prog}:\nkey=$key\nthrottle_secs=$throttle_secs\nfrom=$from\nrecipient=$recipient\ntie_attempts_max=$tie_attempts_max\ntie_retry_sleep=$tie_retry_sleep\nsubject=$subject") if $debug;
# certain options and args are required
&Usage() unless ($key && $throttle_secs && $recipient);
my %last_sent = ();
my $tie_succeeded = 0;
my $tie_attempts_left = $TIE_ATTEMPTS_MAX;
while ($tie_attempts_left--) {
if (tie(%last_sent, 'GDBM_File', $dbm_file, &GDBM_WRCREAT, 0644)) {
$tie_succeeded = 1;
last;
}
# the tie() failed
if ($! == EAGAIN) {
# The failure may be due to a transient problem.
# Retrying may help.
sleep $TIE_RETRY_SLEEP;
next;
} else {
# Some other (presumably more serious) error.
last;
}
}
unless ($tie_succeeded) {
my_warn("${prog}: can't tie ${dbm_file}: $!");
exit 10;
}
my @mailbody = "";
@mailbody = <STDIN>; # read it even if we decide not to send it
my $now = time;
my_warn("now = $now") if $debug;
$last_sent{$key} = 0 unless defined($last_sent{$key}); # so it's defined before we use it in subtraction (placate use strict)
if ($now - $last_sent{$key} >= $throttle_secs) {
my_warn("last_sent = $last_sent{$key}, will send") if $debug;
unless (open(MAIL, "| $MAILCMD $MAILCMD_OPTS -f\"$from\"")) {
my_warn("${prog}: error executing '${MAILCMD}': open(): $!");
exit 20;
}
print MAIL "From: $from\n",
"To: $recipient\n",
($subject ? "Subject: $subject\n" : "") ,
"\n",
@mailbody;
unless (close(MAIL)) {
my_warn("${prog}: error executing '${MAILCMD}': close(): " .
($! ?
"syserror closing pipe: $!"
:
"wait status $? from pipe"
)
);
exit 21;
}
$last_sent{$key} = $now;
} else {
my_warn("last_sent = $last_sent{$key}, suppressing") if $debug;
}
untie %last_sent;
exit 0;
sub Usage {
my_warn("Usage: $prog [-l] [-D dbm_file] -k key -t throttle_seconds [-f from] -r recipient [-T tie_attempts_max] [-S tie_retry_sleep] [-s subject]");
exit 1;
}
sub my_warn {
# Just a wrapper for warn, but with a possible copy to syslog too.
my $msg = shift;
warn $msg, "\n";
syslog($SYSLOG_PRIORITY, $msg) if $also_syslog;
return;
}