From 79baaa609bb39651dbeba9fe1d3cabf9e2832e9e Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Fri, 27 Mar 2015 10:50:21 +0100 Subject: [PATCH 01/17] Convert role tasks to YAML format --- handlers/main.yml | 5 +++-- tasks/main.yml | 18 ++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/handlers/main.yml b/handlers/main.yml index d2105ac..7a85b53 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -1,6 +1,7 @@ --- - name: Restart isc-dhcp-server - service: name=isc-dhcp-server state=restarted - + service: + name: 'isc-dhcp-server' + state: 'restarted' diff --git a/tasks/main.yml b/tasks/main.yml index 4a919b6..b18e5c9 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,13 +1,19 @@ --- -- name: Install DHCP server packages - apt: pkg={{ item }} state=latest install_recommends=no +- name: Install DHCP packages + apt: + name: '{{ item }}' + state: 'present' + install_recommends: False with_items: [ 'isc-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' ] - notify: Restart isc-dhcp-server - - + notify: [ 'Restart isc-dhcp-server' ] From 93cae1c116abcafd391d044691115c9d3af60a44 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Fri, 27 Mar 2015 14:29:04 +0100 Subject: [PATCH 02/17] Request sudo access on Travis-CI --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0538788..0385d30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ --- +sudo: True language: 'python' python: '2.7' From 9b0ad40354d592ec364e6d1e93a338720ade1437 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Fri, 27 Mar 2015 14:32:44 +0100 Subject: [PATCH 03/17] Add support for ISC DHCP Relay You can enable installation of DHCP relay instead of the server via 'dhcpd_mode' variable. By default, relays send the packets to their network gateway, but that might not be what you really want; however selection of correct DHCP server can be performed by specifying it in 'dhcpd_relay_servers' list. --- defaults/main.yml | 23 +++++++++++++++++++++++ handlers/main.yml | 5 +++++ tasks/main.yml | 21 ++++++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/defaults/main.yml b/defaults/main.yml index c9aaffd..60c9e72 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,5 +1,28 @@ --- +# 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) +dhcpd_mode: 'relay' + +# What packages should be installed, depending on mode of operation +dhcpd_base_packages_map: + 'server': [ 'isc-dhcp-server' ] + 'relay': [ 'isc-dhcp-relay' ] + + +# ---- ISC DHCP Relay configuration ---- + +# List of DHCP servers which should receive the relayed packets +dhcpd_relay_servers: [ '{{ ansible_default_ipv4.gateway }}' ] + +# List of network interfaces that dhcrelay should listen on +dhcpd_relay_interfaces: [] + +# Additional dhcrelay options +dhcpd_relay_options: '-4' + + # ---- Global ISC DHCP Server configuration ---- # Is this DHCP server authoritative? diff --git a/handlers/main.yml b/handlers/main.yml index 7a85b53..a021695 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -5,3 +5,8 @@ name: 'isc-dhcp-server' state: 'restarted' +- name: Restart isc-dhcp-relay + service: + name: 'isc-dhcp-relay' + state: 'restarted' + diff --git a/tasks/main.yml b/tasks/main.yml index b18e5c9..b23321e 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,11 +1,29 @@ --- +- name: Configure DHCP relay in debconf + debconf: + 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_items: [ 'isc-dhcp-server' ] + with_items: dhcpd_base_packages_map[dhcpd_mode] + +- 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: Configure DHCP server template: @@ -16,4 +34,5 @@ mode: '0644' with_items: [ 'etc/default/isc-dhcp-server', 'etc/dhcp/dhcpd.conf' ] notify: [ 'Restart isc-dhcp-server' ] + when: dhcpd_mode == 'server' From 277e2df11ccc0a6e3af41da7c90d0075b65f1b83 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 28 Mar 2015 23:31:49 +0100 Subject: [PATCH 04/17] Add 'debops.secret' role to list of dependencies Secrets are used to store confidential key information used for Dynamic DNS. --- meta/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/meta/main.yml b/meta/main.yml index e94cef3..96eaff6 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,6 +1,8 @@ --- -dependencies: [] +dependencies: + + - role: debops.secret galaxy_info: author: 'Maciej Delmanowski' From 80f1f887863bc0322c85de4d9aa28955cfb39061 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sat, 28 Mar 2015 23:34:25 +0100 Subject: [PATCH 05/17] Reorganize role documentation --- CHANGES.rst | 8 + defaults/main.yml | 321 +++++++---------------- docs/changelog.rst | 1 + docs/copyright.rst | 21 ++ docs/credits.rst | 16 ++ docs/defaults-configuration.rst | 439 ++++++++++++++++++++++++++++++++ docs/getting-started.rst | 18 ++ docs/guides.rst | 6 + docs/index.rst | 22 ++ docs/installation.rst | 7 + docs/introduction.rst | 15 ++ docs/troubleshooting.rst | 6 + 12 files changed, 652 insertions(+), 228 deletions(-) create mode 100644 CHANGES.rst create mode 100644 docs/changelog.rst create mode 100644 docs/copyright.rst create mode 100644 docs/credits.rst create mode 100644 docs/defaults-configuration.rst create mode 100644 docs/getting-started.rst create mode 100644 docs/guides.rst create mode 100644 docs/index.rst create mode 100644 docs/installation.rst create mode 100644 docs/introduction.rst create mode 100644 docs/troubleshooting.rst diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..71a8bc5 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,8 @@ +Changelog +========= + +v0.1.0 +------ + +- First release [drybjed] + diff --git a/defaults/main.yml b/defaults/main.yml index 60c9e72..de90538 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,49 +1,95 @@ --- +# Default variables +# ================= +# .. 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) -dhcpd_mode: 'relay' +# +# - ``server``: host is an ISC DHCP server, see ``dhcpd(8)`` +# +# - ``relay``: host is an ISC DHCP relay, see dhcrelay(8) +# +dhcpd_mode: 'server' + +# .. 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' ] -# ---- ISC DHCP Relay configuration ---- +# -------------------------------- +# 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: '-4' -# ---- Global ISC DHCP Server configuration ---- +# ---------------------------------------- +# Global ISC DHCP Server configuration +# ---------------------------------------- +# .. envvar:: dhcpd_authoritative +# # Is this DHCP server authoritative? dhcpd_authoritative: False + +# .. envvar:: dhcpd_interfaces +# # List of network interfaces to listen on for DHCP requests # If this list is empty, Ansible will try to guess correct interfaces # automatically dhcpd_interfaces: [] + +# .. envvar:: dhcpd_domain +# # Default domain to use dhcpd_domain: '{{ ansible_domain }}' + +# .. envvar:: dhcpd_dns_servers +# # 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) dhcpd_lease_time: 24 + +# .. envvar:: dhcpd_global_options +# # Default global options formatted as a text block dhcpd_global_options: | option domain-name "{{ ansible_domain }}"; @@ -52,257 +98,76 @@ dhcpd_global_options: | max-lease-time {{ (dhcpd_lease_time * 60 * 60)|round|int }}; log-facility local7; + +# .. envvar:: dhcpd_options +# # Custom options formatted as a text block dhcpd_options: False -# ---- ISC DHCP Server configuration scopes ---- +# ---------------------------------------- +# ISC DHCP Server configuration scopes +# ---------------------------------------- # 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) # manual page. You can create nested configuration using Ansible variable -# expansion (examples below). - -# 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 +# expansion. +# .. envvar:: dhcpd_keys +# +# List of secret keys used for Dynamic DNS configuration. See +# :ref:`dhcpd_keys` for more details. dhcpd_keys: [] - #- key: "secure-key" - # algorithm: "hmac-md5" - # secret: "JFw7jM2/KVU2hIB4xkDSQmHB6JJOLUu4xkzwLNNpR88=" -# List of classes + +# .. envvar:: dhcpd_classes +# +# List of client classes (see dhcpd.conf(5)). More informaction can be found in +# :ref:`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: [] - #- 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: [] - #- 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 -dhcpd_subnets: - - subnet: '{{ ansible_default_ipv4.network }}' - netmask: '{{ ansible_default_ipv4.netmask }}' - comment: 'Generated automatically by Ansible' - - #- subnet: 'dead:be:ef::/64' - # ipv6: True - # routers: '10.0.10.1' - # comment: "Example IPv6 subnet" - # options: | - # default-lease-time 300; - # max-lease-time 7200; - # - #- subnet: '10.0.20.0' - # 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' ] +# .. envvar:: dhcpd_subnets # -# - 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' +# List of subnets not in a shared network. See :ref:`dhcpd_subnets` for more +# details. +dhcpd_subnets: [ '{{ dhcpd_subnet_default }}' ] + +# Default subnet managed automatically +dhcpd_subnet_default: + subnet: '{{ ansible_default_ipv4.network }}' + netmask: '{{ ansible_default_ipv4.netmask }}' + comment: 'Generated automatically by Ansible' -# Global list of hosts in DHCP +# .. envvar:: dhcpd_hosts +# +# Global list of hosts in DHCP. See ref:`dhcpd_hosts` for more details. 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: [] - #- '/etc/dhcp/example.conf' -# ---- ISC DHCP failover configuration ---- -# -# 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 + +# .. envvar:: dhcpd_failovers # +# DHCP failover configuration. See :ref:`dhcpd_failovers` for more details. dhcpd_failovers: [] - ## Following is full cluster configuration - #- 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 - # - ## Following is minimal cluster configuration - #- 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 diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..d9e113e --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGES.rst diff --git a/docs/copyright.rst b/docs/copyright.rst new file mode 100644 index 0000000..742b240 --- /dev/null +++ b/docs/copyright.rst @@ -0,0 +1,21 @@ +Copyright +========= + +:: + + Copyright (C) 2014 Maciej Delmanowski + 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/ + diff --git a/docs/credits.rst b/docs/credits.rst new file mode 100644 index 0000000..0f6b51a --- /dev/null +++ b/docs/credits.rst @@ -0,0 +1,16 @@ +Credits +======= + +Credits, in chronological order +------------------------------- + +* Maciej Delmanowski + + * creator of the DebOps Project + + * current project maintainer + +* RedRampage + + * Added support for DHCP failover and Dynamic DNS keys + diff --git a/docs/defaults-configuration.rst b/docs/defaults-configuration.rst new file mode 100644 index 0000000..761615f --- /dev/null +++ b/docs/defaults-configuration.rst @@ -0,0 +1,439 @@ +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_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 + diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 0000000..163d87c --- /dev/null +++ b/docs/getting-started.rst @@ -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 + diff --git a/docs/guides.rst b/docs/guides.rst new file mode 100644 index 0000000..742b1f7 --- /dev/null +++ b/docs/guides.rst @@ -0,0 +1,6 @@ +Guides and examples +=================== + +This section will contain guides for configuring ``debops.dhcpd`` in various +scenarios. + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..e6a8372 --- /dev/null +++ b/docs/index.rst @@ -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: diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..84578e1 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,7 @@ +Installation +============ + +This role requires at least Ansible ``v1.7.0``. To install it, run:: + + ansible-galaxy install debops.dhcpd + diff --git a/docs/introduction.rst b/docs/introduction.rst new file mode 100644 index 0000000..f40b673 --- /dev/null +++ b/docs/introduction.rst @@ -0,0 +1,15 @@ +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 network which will +relay DHCP/BOOTP messages to your DHCP server. + +.. _ISC DHCP Server: https://www.isc.org/downloads/dhcp/ + +.. + Local Variables: + mode: rst + ispell-local-dictionary: "american" + End: diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst new file mode 100644 index 0000000..e83d254 --- /dev/null +++ b/docs/troubleshooting.rst @@ -0,0 +1,6 @@ +Troubleshooting +=============== + +This section will contain information about fixing issues with +``debops.dhcpd`` role. + From 3c4a1e80bbd9ee12baedf79584f006afb80b933a Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 00:25:58 +0100 Subject: [PATCH 06/17] Add variable for dhcpd(8) server options --- defaults/main.yml | 16 +++++++++++++--- templates/etc/default/isc-dhcp-server.j2 | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/defaults/main.yml b/defaults/main.yml index de90538..7092798 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -50,9 +50,19 @@ dhcpd_relay_interfaces: [] dhcpd_relay_options: '-4' -# ---------------------------------------- -# Global ISC DHCP Server configuration -# ---------------------------------------- +# --------------------------------- +# ISC DHCP Server configuration +# --------------------------------- + +# .. envvar:: dhcpd_server_options +# +# dhcpd(8) options +dhcpd_server_options: '-4' + + +# --------------------------- +# DHCP main configuration +# --------------------------- # .. envvar:: dhcpd_authoritative # diff --git a/templates/etc/default/isc-dhcp-server.j2 b/templates/etc/default/isc-dhcp-server.j2 index 430430e..69347d7 100644 --- a/templates/etc/default/isc-dhcp-server.j2 +++ b/templates/etc/default/isc-dhcp-server.j2 @@ -16,7 +16,7 @@ # Additional options to start dhcpd with. # 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? # Separate multiple interfaces with spaces, e.g. "eth0 eth1". From 48c658bbdf58573c8d6115f88bbc68edfdd44f60 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 21:11:40 +0200 Subject: [PATCH 07/17] Add 'dhcpd_ipversion' variable This variable indicates which Internet Protocol version should be enabled in DHCP server, because ISC DHCP server supports only 1 version active at a time. --- defaults/main.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/defaults/main.yml b/defaults/main.yml index 7092798..f0b7171 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -20,6 +20,12 @@ 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 @@ -47,7 +53,7 @@ dhcpd_relay_interfaces: [] # .. envvar:: dhcpd_relay_options # # Additional dhcrelay options -dhcpd_relay_options: '-4' +dhcpd_relay_options: '{{ "-" + dhcpd_ipversion }}' # --------------------------------- @@ -57,7 +63,7 @@ dhcpd_relay_options: '-4' # .. envvar:: dhcpd_server_options # # dhcpd(8) options -dhcpd_server_options: '-4' +dhcpd_server_options: '{{ "-" + dhcpd_ipversion }}' # --------------------------- @@ -156,13 +162,16 @@ dhcpd_shared_networks: [] # # List of subnets not in a shared network. See :ref:`dhcpd_subnets` for more # details. -dhcpd_subnets: [ '{{ dhcpd_subnet_default }}' ] +dhcpd_subnets: [ '{{ dhcpd_subnet_default[dhcpd_ipversion] }}' ] # Default subnet managed automatically dhcpd_subnet_default: - subnet: '{{ ansible_default_ipv4.network }}' - netmask: '{{ ansible_default_ipv4.netmask }}' - comment: 'Generated automatically by Ansible' + '4': + subnet: '{{ ansible_default_ipv4.network + "/" + ansible_default_ipv4.netmask }}' + comment: 'Generated automatically by Ansible' + '6': + subnet: '{{ ansible_default_ipv6.address + "/" + ansible_default_ipv6.prefix }}' + comment: 'Generated automatically by Ansible' # .. envvar:: dhcpd_hosts From f34a60a7cce01d0baedc7f8183b257913f486f98 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 21:13:24 +0200 Subject: [PATCH 08/17] Make sure that /var/lib/dhcp/dhcpd6.leases exists --- tasks/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tasks/main.yml b/tasks/main.yml index b23321e..1758046 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -36,3 +36,7 @@ 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' + From 04abdbb3c738cf5861f5c3e00cc8c186370169dc Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 21:24:30 +0200 Subject: [PATCH 09/17] Move macro functions to separate file --- templates/etc/dhcp/dhcpd.conf.j2 | 205 ++--------------------------- templates/etc/dhcp/macros.j2 | 218 +++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 196 deletions(-) create mode 100644 templates/etc/dhcp/macros.j2 diff --git a/templates/etc/dhcp/dhcpd.conf.j2 b/templates/etc/dhcp/dhcpd.conf.j2 index 3049878..e619b1b 100644 --- a/templates/etc/dhcp/dhcpd.conf.j2 +++ b/templates/etc/dhcp/dhcpd.conf.j2 @@ -1,192 +1,5 @@ -# This file is managed by Ansible, all changes will be lost -{% macro print_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(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 %} +{% import 'macros.j2' as print with context %} +# {{ ansible_managed }} {% if dhcpd_authoritative is defined and dhcpd_authoritative %} authoritative; @@ -207,17 +20,17 @@ not authoritative; {% endif %} {% if dhcpd_keys is defined and dhcpd_keys %} {% for key in dhcpd_keys %} -{{ print_key(key) }} +{{ print.key(key) }} {% endfor %} {% endif %} {% if dhcpd_classes is defined and dhcpd_classes %} {% for class in dhcpd_classes %} -{{ print_class(class) }} +{{ print.class(class) }} {% endfor %} {% endif %} {% if dhcpd_failovers is defined and dhcpd_failovers %} {% for failover in dhcpd_failovers %} -{{ print_failover(failover) }} +{{ print.failover(failover) }} {% endfor %} {% endif %} {% if dhcpd_shared_networks is defined and dhcpd_shared_networks %} @@ -235,7 +48,7 @@ shared-network "{{ network.name }}" { {% endif %} {% for subnet in network.subnets %} -{{ print_subnet(subnet) | indent(8,true) }} +{{ print.subnet(subnet) | indent(8,true) }} {% endfor %} } @@ -244,16 +57,16 @@ shared-network "{{ network.name }}" { {% endif %} {% if dhcpd_groups is defined and dhcpd_groups %} {% for group in dhcpd_groups %} -{{ print_group(group) }} +{{ print.group(group) }} {% endfor %} {% endif %} {% if dhcpd_subnets is defined and dhcpd_subnets %} {% for subnet in dhcpd_subnets %} -{{ print_subnet(subnet) }} +{{ print.subnet(subnet) }} {% endfor %} {% endif %} {% if dhcpd_hosts is defined and dhcpd_hosts %} -{{ print_hosts(dhcpd_hosts) }} +{{ print.hosts(dhcpd_hosts) }} {% endif %} {% if dhcpd_includes is defined and dhcpd_includes %} {% for include in dhcpd_includes %} diff --git a/templates/etc/dhcp/macros.j2 b/templates/etc/dhcp/macros.j2 new file mode 100644 index 0000000..096cb12 --- /dev/null +++ b/templates/etc/dhcp/macros.j2 @@ -0,0 +1,218 @@ +{# +# 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 %} From 7dfe3855ca5305e5e5c72153bc0e107fa7a722e7 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 22:56:46 +0200 Subject: [PATCH 10/17] Reorganize DHCP options - 'dhcpd_global_options' text block has been split into individual variables; - if list of nameservers is not specified, debops.dhcpd role will advertise nameservers set in '/etc/resolv.conf' file; - default set of options can be enabled or disabled using a bool variable 'dhcpd_auto_options'. The options have been moved to a separate configuration template included in main one; --- defaults/main.yml | 72 ++++++++++++++++++++---------- tasks/main.yml | 11 +++++ templates/etc/dhcp/auto_options.j2 | 26 +++++++++++ templates/etc/dhcp/dhcpd.conf.j2 | 15 +++++-- 4 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 templates/etc/dhcp/auto_options.j2 diff --git a/defaults/main.yml b/defaults/main.yml index f0b7171..c942695 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -76,6 +76,12 @@ dhcpd_server_options: '{{ "-" + dhcpd_ipversion }}' 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 @@ -84,40 +90,58 @@ dhcpd_authoritative: False dhcpd_interfaces: [] -# .. envvar:: dhcpd_domain -# -# Default domain to use -dhcpd_domain: '{{ ansible_domain }}' - - -# .. envvar:: dhcpd_dns_servers -# -# 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) -dhcpd_lease_time: 24 +dhcpd_lease_time: '24' -# .. envvar:: dhcpd_global_options +# .. envvar:: dhcpd_global_default_lease_time # -# 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; +# 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 options formatted as a text block +# Custom global options formatted as a text block dhcpd_options: False diff --git a/tasks/main.yml b/tasks/main.yml index 1758046..26d80d7 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -25,6 +25,17 @@ 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 template: src: '{{ item }}.j2' diff --git a/templates/etc/dhcp/auto_options.j2 b/templates/etc/dhcp/auto_options.j2 new file mode 100644 index 0000000..63ef653 --- /dev/null +++ b/templates/etc/dhcp/auto_options.j2 @@ -0,0 +1,26 @@ +{% if dhcpd_domain_name|d() and dhcpd_domain_name %} +option domain-name "{{ dhcpd_domain_name }}"; + +{% endif %} +{% if dhcpd_domain_search|d() and dhcpd_domain_search %} +option domain-search "{{ dhcpd_domain_search | join('", "') }}"; +option dhcp6.domain-search "{{ dhcpd_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 = dhcpd_runtime_nameservers %} +{% 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 +#} diff --git a/templates/etc/dhcp/dhcpd.conf.j2 b/templates/etc/dhcp/dhcpd.conf.j2 index e619b1b..17b1902 100644 --- a/templates/etc/dhcp/dhcpd.conf.j2 +++ b/templates/etc/dhcp/dhcpd.conf.j2 @@ -8,10 +8,19 @@ authoritative; not authoritative; {% endif %} -{% if dhcpd_global_options is defined and dhcpd_global_options %} -# Global configuration options -{{ dhcpd_global_options }} +{% if dhcpd_global_default_lease_time|d() and dhcpd_global_default_lease_time %} +default-lease-time {{ dhcpd_global_default_lease_time }}; +{% 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 %} {% if dhcpd_options is defined and dhcpd_options %} # Configuration options From f5fec411deb1fc69b8f1c1667147e7328af4376c Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 23:34:58 +0200 Subject: [PATCH 11/17] Add IPv4 default router if found in Ansible facts --- defaults/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/defaults/main.yml b/defaults/main.yml index c942695..a399fe1 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -192,6 +192,7 @@ dhcpd_subnets: [ '{{ dhcpd_subnet_default[dhcpd_ipversion] }}' ] 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 }}' From e2dde9fe012367314c778c610f20640ed0811f0c Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 23:36:16 +0200 Subject: [PATCH 12/17] Set domain-search options by default --- templates/etc/dhcp/auto_options.j2 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/etc/dhcp/auto_options.j2 b/templates/etc/dhcp/auto_options.j2 index 63ef653..72eb0cf 100644 --- a/templates/etc/dhcp/auto_options.j2 +++ b/templates/etc/dhcp/auto_options.j2 @@ -1,10 +1,9 @@ {% 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 }}"; -{% endif %} -{% if dhcpd_domain_search|d() and dhcpd_domain_search %} -option domain-search "{{ dhcpd_domain_search | join('", "') }}"; -option dhcp6.domain-search "{{ dhcpd_domain_search | join('", "') }}"; +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 %} From 8504950c0c87e9ed494e0a09ec463be32fb00eb7 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 23:36:45 +0200 Subject: [PATCH 13/17] Correctly format list of IPv4 routers --- templates/etc/dhcp/macros.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/etc/dhcp/macros.j2 b/templates/etc/dhcp/macros.j2 index 096cb12..6fa05cf 100644 --- a/templates/etc/dhcp/macros.j2 +++ b/templates/etc/dhcp/macros.j2 @@ -77,7 +77,7 @@ subnet {{ subnet.subnet | ipaddr('cidr') | ipaddr('network') }} netmask {{ subne {% if subnet.routers is string %} option routers {{ subnet.routers }}; {% else %} - option routers {{ subnet.routers | join(' ') }}; + option routers {{ subnet.routers | join(', ') }}; {% endif %} {% endif %} {% if subnet.options is defined and subnet.options %} From e8d7c97876360801521ea982e5b7067eb5351f93 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Sun, 29 Mar 2015 23:41:24 +0200 Subject: [PATCH 14/17] Filter list of autogenerated nameservers If somehow '127.0.0.1' from 'dnsmasq' got into the list of nameservers in '/etc/resolv.conf', filter it out of the list of advertised nameservers to avoid confusion. --- templates/etc/dhcp/auto_options.j2 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/etc/dhcp/auto_options.j2 b/templates/etc/dhcp/auto_options.j2 index 72eb0cf..7c0a656 100644 --- a/templates/etc/dhcp/auto_options.j2 +++ b/templates/etc/dhcp/auto_options.j2 @@ -9,7 +9,12 @@ option dhcp6.domain-search "{{ dhcpd_tpl_domain_search | join('", "') }}"; {% 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 = 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 %} From 337b1e749dc46cdcb95b44deecae872cd0db114e Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Mon, 30 Mar 2015 08:56:44 +0200 Subject: [PATCH 15/17] Update documentation and README --- README.md | 8 ++++++-- docs/introduction.rst | 2 +- meta/ansigenome.yml | 6 +++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 39bab7a..9475ad9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ [![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. ### Installation @@ -13,7 +17,7 @@ This role requires at least Ansible `v1.7.0`. To install it, run: ### Documentation 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/). diff --git a/docs/introduction.rst b/docs/introduction.rst index f40b673..e762637 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -3,7 +3,7 @@ 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 network which will +configure an DHCP relay on a host connected to multiple networks which will relay DHCP/BOOTP messages to your DHCP server. .. _ISC DHCP Server: https://www.isc.org/downloads/dhcp/ diff --git a/meta/ansigenome.yml b/meta/ansigenome.yml index 4977157..c661d35 100644 --- a/meta/ansigenome.yml +++ b/meta/ansigenome.yml @@ -17,5 +17,9 @@ ansigenome_info: github: 'drybjed' 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. From 5014dd864269021b23c576fec27723bed5962e31 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Mon, 30 Mar 2015 13:34:53 +0200 Subject: [PATCH 16/17] Add 'dhcp-probe' support dhcp-probe script is used to scan the network for unauthorized DHCP servers. --- README.md | 3 + defaults/main.yml | 52 ++++ docs/introduction.rst | 3 + handlers/main.yml | 11 + meta/ansigenome.yml | 4 +- tasks/dhcp-probe.yml | 28 ++ tasks/main.yml | 7 +- templates/etc/dhcp_probe.cf.j2 | 17 ++ .../lib/dhcp-probe/dhcp_probe_notify2.j2 | 247 ++++++++++++++++++ .../local/lib/dhcp-probe/mail-throttled.j2 | 173 ++++++++++++ 10 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 tasks/dhcp-probe.yml create mode 100644 templates/etc/dhcp_probe.cf.j2 create mode 100755 templates/usr/local/lib/dhcp-probe/dhcp_probe_notify2.j2 create mode 100755 templates/usr/local/lib/dhcp-probe/mail-throttled.j2 diff --git a/README.md b/README.md index 9475ad9..6ee647b 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ 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 This role requires at least Ansible `v1.7.0`. To install it, run: diff --git a/defaults/main.yml b/defaults/main.yml index a399fe1..85c8864 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -17,6 +17,8 @@ # # - ``relay``: host is an ISC DHCP relay, see dhcrelay(8) # +# - ``probe``: configure only ``dhcp-probe`` when enabled +# dhcpd_mode: 'server' @@ -32,6 +34,7 @@ dhcpd_ipversion: '4' dhcpd_base_packages_map: 'server': [ 'isc-dhcp-server' ] 'relay': [ 'isc-dhcp-relay' ] + 'probe': [] # -------------------------------- @@ -215,3 +218,52 @@ dhcpd_includes: [] # DHCP failover configuration. See :ref:`dhcpd_failovers` for more details. dhcpd_failovers: [] + +# ----------------------------- +# dhcp-probe configuration +# ----------------------------- + +# .. envvar:: dhcpd_probe +# +# Enable or disable ``dhcp-probe`` script +dhcpd_probe: True + + +# .. envvar:: dhcpd_probe_mail_to +# +# List of mail recipients which will receive messages about unauthorized DHCP +# servers. Set to ``[]`` to disable. +dhcpd_probe_mail_to: [ 'root@{{ ansible_domain }}' ] + + +# .. envvar:: dhcpd_probe_page_to +# +# Alternative list of mail recipients which will receive mail messages. Meant +# to be used as a "pager service", you can use ``debops.smstools`` role to +# setup a mail-SMS gateway and send the SMS messages that way. +dhcpd_probe_page_to: [] + + +# .. 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: '' + diff --git a/docs/introduction.rst b/docs/introduction.rst index e762637..78e230a 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -6,6 +6,9 @@ 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/ .. diff --git a/handlers/main.yml b/handlers/main.yml index a021695..db77015 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -10,3 +10,14 @@ 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' + diff --git a/meta/ansigenome.yml b/meta/ansigenome.yml index c661d35..e78e1f6 100644 --- a/meta/ansigenome.yml +++ b/meta/ansigenome.yml @@ -22,4 +22,6 @@ ansigenome_info: 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. diff --git a/tasks/dhcp-probe.yml b/tasks/dhcp-probe.yml new file mode 100644 index 0000000..19bb000 --- /dev/null +++ b/tasks/dhcp-probe.yml @@ -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' ] + diff --git a/tasks/main.yml b/tasks/main.yml index 26d80d7..2381fc3 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -18,7 +18,9 @@ name: '{{ item }}' state: 'present' install_recommends: False - with_items: dhcpd_base_packages_map[dhcpd_mode] + 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 @@ -51,3 +53,6 @@ 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 + diff --git a/templates/etc/dhcp_probe.cf.j2 b/templates/etc/dhcp_probe.cf.j2 new file mode 100644 index 0000000..802b770 --- /dev/null +++ b/templates/etc/dhcp_probe.cf.j2 @@ -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 %} diff --git a/templates/usr/local/lib/dhcp-probe/dhcp_probe_notify2.j2 b/templates/usr/local/lib/dhcp-probe/dhcp_probe_notify2.j2 new file mode 100755 index 0000000..80acece --- /dev/null +++ b/templates/usr/local/lib/dhcp-probe/dhcp_probe_notify2.j2 @@ -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; +} diff --git a/templates/usr/local/lib/dhcp-probe/mail-throttled.j2 b/templates/usr/local/lib/dhcp-probe/mail-throttled.j2 new file mode 100755 index 0000000..686643e --- /dev/null +++ b/templates/usr/local/lib/dhcp-probe/mail-throttled.j2 @@ -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 = ; # 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; +} From 0e0d53bbc8ba05c005f048ab4f0ef96484db09f6 Mon Sep 17 00:00:00 2001 From: Maciej Delmanowski Date: Mon, 30 Mar 2015 13:58:19 +0200 Subject: [PATCH 17/17] Add 'dhcpd_zones' list variable --- defaults/main.yml | 7 +++++++ docs/defaults-configuration.rst | 29 +++++++++++++++++++++++++++++ templates/etc/dhcp/dhcpd.conf.j2 | 5 +++++ templates/etc/dhcp/macros.j2 | 15 ++++++++++++++- 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/defaults/main.yml b/defaults/main.yml index 85c8864..85eda6f 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -164,6 +164,13 @@ dhcpd_options: False dhcpd_keys: [] +# .. 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 diff --git a/docs/defaults-configuration.rst b/docs/defaults-configuration.rst index 761615f..3355182 100644 --- a/docs/defaults-configuration.rst +++ b/docs/defaults-configuration.rst @@ -43,6 +43,35 @@ Examples:: 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 diff --git a/templates/etc/dhcp/dhcpd.conf.j2 b/templates/etc/dhcp/dhcpd.conf.j2 index 17b1902..8228928 100644 --- a/templates/etc/dhcp/dhcpd.conf.j2 +++ b/templates/etc/dhcp/dhcpd.conf.j2 @@ -32,6 +32,11 @@ log-facility {{ dhcpd_log_facility }}; {{ print.key(key) }} {% endfor %} {% endif %} +{% if dhcpd_zones is defined and dhcpd_zones %} +{% for zone in dhcpd_zones %} +{{ print.zone(zone) }} +{% endfor %} +{% endif %} {% if dhcpd_classes is defined and dhcpd_classes %} {% for class in dhcpd_classes %} {{ print.class(class) }} diff --git a/templates/etc/dhcp/macros.j2 b/templates/etc/dhcp/macros.j2 index 6fa05cf..ae28e60 100644 --- a/templates/etc/dhcp/macros.j2 +++ b/templates/etc/dhcp/macros.j2 @@ -211,8 +211,21 @@ failover peer "{{ failover.failover }}" { {% if key.comment is defined and key.comment %} # {{ key.comment }} {% endif %} -key "{{ key.key }}" { +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 %}