--- /dev/null
+dhcp-server
+===========
+
+This role can be used to install, update, and manage a DHCP server running on CentOS 7.
+
+Notes
++++++
+
+This role is heavily modified to be primarily useful for our test labs that only have two or three subnets. See https://wiki.sepia.ceph.com/doku.php?id=services:networking.
+
+This role checks for firewalld and iptables. It will configure firewalld unless iptables is running. It **does not** configure iptables. At the time the role was created, our DHCP server was running other services and its iptables was already heavily modified and configured. This reason, along with firewalld being the default in CentOS 7, is why iptables configuration is skipped.
+
+Variables
++++++++++
+This role basically has two required and two optional variables:
+
++----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **Required Variables** |
++---------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+|:: | This list will be used to populate the global ``/etc/dhcpd.conf``. You can add additional keys and values. Just make sure they follow the syntax required for dhcpd.conf. |
+| | |
+| dhcp_global_options: | |
+| - ddns-update-style: none | Here's the dhcpd_ man page. |
+| - default-lease-time: 43200 | |
+| - max-lease-time: 172800 | |
+| - one-lease-per-client: "true" | |
+| | |
++---------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+|:: | This is large dictionary that gets parsed out into individual dhcpd config files. Each top-level key (``front`` and ``ipmi`` in the example) will get its own dhcp conf file created. The example shown to the left is our actual ``dhcp_subnets`` dictionary. |
+| | |
+| dhcp_subnets: | |
+| front: | Under each subnet, ``cidr``, ``ipvar``, and ``macvar`` are required. ``ipvar`` and ``macvar`` tell the Jinja2 template which IP address and MAC address should be used for each host in each subnet config file. |
+| cidr: 172.21.0.0/20 | |
+| ipvar: ip | Here's a line from our Ansible inventory host file |
+| macvar: mac | |
+| domain_name: front.sepia.ceph.com | ``smithi001.front.sepia.ceph.com mac=0C:C4:7A:BD:15:E8 ip=172.21.15.1 ipmi=172.21.47.1 bmc=0C:C4:7A:6E:21:A7`` |
+| domain_search: | |
+| - front.sepia.ceph.com | This will result in a static IP entry for smithi001-front with IP 172.21.15.1 and MAC 0C:C4:7A:BD:15:E8 in ``dhcpd.front.conf`` and a smithi001-ipmi entry with IP 172.21.47.1 with MAC 0C:C4:7A:6E:21:A7 in ``dhcpd.ipmi.conf``. |
+| - sepia.ceph.com | |
+| domain_name_server: | The ``next_server`` and ``filename`` values can be overridden by ansible group or host. See below. |
+| - 172.21.0.1 | |
+| - 172.21.0.2 | All the other keys are optional. |
+| routers: 172.21.15.254 | |
+| next_server: 172.21.0.11 | |
+| filename: "/pxelinux.0" | |
+| classes: | |
+| virtual: "match if substring(hardware, 0, 4) = 01:52:54:00" | |
+| lxc: "match if substring(hardware, 0, 4) = 01:52:54:ff" | |
+| pools: | |
+| virtual: | |
+| range: 172.21.10.20 172.21.10.250 | |
+| unknown_clients: | |
+| range: | |
+| - 172.21.11.0 172.21.11.19 | |
+| - 172.21.13.170 172.21.13.250 | |
+| next_server: 172.21.0.11 | |
+| filename: "/pxelinux.0" | |
+| lxc: | |
+| range: 172.21.14.1 172.21.14.200 | |
+| ipmi: | |
+| cidr: 172.21.32.0/20 | |
+| ipvar: ipmi | |
+| macvar: bmc | |
+| domain_name: ipmi.sepia.ceph.com | |
+| domain_search: | |
+| - ipmi.sepia.ceph.com | |
+| - sepia.ceph.com | |
+| domain_name_servers: | |
+| - 172.21.0.1 | |
+| - 172.21.0.2 | |
+| routers: 172.21.47.254 | |
+| next_server: 172.21.0.11 | |
+| filename: "/pxelinux.0" | |
+| pools: | |
+| unknown_clients: | |
+| range: 172.21.43.1 172.21.43.100 | |
+| next_server: 172.21.0.11 | |
+| filename: "/pxelinux.0" | |
+| | |
++---------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| **Optional Variables** |
++---------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| ``dhcp_next_server: 1.2.3.4`` | This is your PXE/TFTP server's IP address. This will **override** the subnet's ``next_server`` defined in the ``dhcp_subnets`` dictionary. It can be defined in your Ansible inventory in a couple ways: |
+| | |
+| | #. In ``ansible/inventory/group_vars/group.yml`` if some hosts should use a different PXE server |
+| | #. In your inventory ``hosts`` file on a per-host basis. See Ansible's docs_ on variable precedence. |
++---------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+| ``dhcp_filename: "/pxelinux.0"`` | Same rules as above. This is the TFTP filename the DHCP server should instruct DHCP clients to download. |
++---------------------------------------------------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
+
+.. _docs: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable
+.. _dhcpd: https://linux.die.net/man/8/dhcpd
--- /dev/null
+---
+- name: Install/update packages
+ yum:
+ name: dhcp
+ state: latest
+ register: dhcp_yum_transaction
+
+- name: Check for firewalld
+ command: firewall-cmd --state
+ register: firewalld_state
+ ignore_errors: true
+
+- name: Check for iptables
+ command: systemctl status iptables
+ register: iptables_state
+ ignore_errors: true
+
+- name: Make sure firewalld is running
+ service:
+ name: firewalld
+ state: started
+ enabled: yes
+ when: iptables_state.rc != 0
+
+- name: Configure firewalld
+ firewalld:
+ service: dhcp
+ state: enabled
+ permanent: true
+ immediate: yes
+ when: iptables_state.rc != 0
+
+- name: Write global dhcpd.conf
+ template:
+ src: dhcpd.conf.j2
+ dest: /etc/dhcp/dhcpd.conf
+ backup: yes
+ register: dhcp_global_config
+
+- name: Write each subnet config
+ template:
+ src: dhcpd.subnet.conf.j2
+ dest: "/etc/dhcp/dhcpd.{{ item }}.conf"
+ backup: yes
+ with_items: "{{ dhcp_subnets }}"
+ register: dhcp_subnet_config
+
+- name: Test new config
+ command: dhcpd -t -cf /etc/dhcp/dhcpd.conf
+ register: dhcpd_config_test_result
+ when: dhcp_global_config|changed or dhcp_subnet_config|changed
+
+- name: Restart dhcpd
+ service:
+ name: dhcpd
+ state: restarted
+ when:
+ - dhcpd_config_test_result is defined
+ - dhcpd_config_test_result.rc == 0
--- /dev/null
+{% for subnet, subnet_item in dhcp_subnets.iteritems() %}
+{% if subnet == item %}
+subnet {{ subnet_item.cidr | ipaddr('network') }} netmask {{ subnet_item.cidr | ipaddr('netmask') }} {
+ {% if subnet_item.domain_name is defined -%}
+ option domain-name "{{ subnet_item.domain_name }}";
+ {% endif -%}
+ {% if subnet_item.domain_search is defined -%}
+ option domain-search "{{ subnet_item.domain_search|join('", "') }}";
+ {% endif -%}
+ {% if subnet_item.domain_name_servers is defined -%}
+ option domain-name-servers {{ subnet_item.domain_name_servers|join(', ') }};
+ {% endif -%}
+ {% if subnet_item.routers is defined -%}
+ option routers {{ subnet_item.routers }};
+ {% endif -%}
+ {% if subnet_item.next_server is defined -%}
+ next-server {{ subnet_item.next_server }};
+ {% endif -%}
+ {% if subnet_item.filename is defined -%}
+ filename "{{ subnet_item.filename }}";
+ {% endif %}
+
+ {% if subnet_item.classes is defined -%}
+ {% for class_name, class_string in subnet_item.classes.items() -%}
+ class "{{ class_name }}" {
+ {{ class_string }};
+ }
+
+ {% endfor -%}
+ {%- endif -%}
+
+ {% if subnet_item.pools is defined -%}
+ {% for pool, pool_value in subnet_item.pools.items() -%}
+ pool {
+ {% if pool == "unknown_clients" -%}
+ allow unknown-clients;
+ {% else -%}
+ allow members of "{{ pool }}";
+ {% endif -%}
+ {% if pool_value.range is string -%}
+ range {{ pool_value.range }};
+ {% else -%}
+ range {{ pool_value.range|join(';\n range ') }};
+ {% endif -%}
+ {% if pool_value.next_server is defined -%}
+ next-server {{ pool_value.next_server }};
+ {% endif -%}
+ {% if pool_value.filename is defined -%}
+ filename "{{ pool_value.filename }}";
+ {% endif -%}
+ }
+
+ {% endfor -%}
+ {%- endif -%}
+
+ {% for host in groups['all'] | sort | unique -%}
+ {% if hostvars[host][subnet_item.macvar] is defined -%}
+ host {{ host.split('.')[0] }}-{{ subnet }} {
+ {% if hostvars[host]['dhcp_next_server'] is defined -%}
+ next-server {{ hostvars[host]['dhcp_next_server'] }};
+ filename "{{ hostvars[host]['dhcp_filename'] }}";
+ {% endif -%}
+ hardware ethernet {{ hostvars[host][subnet_item.macvar] }};
+ fixed-address {{ hostvars[host][subnet_item.ipvar] }};
+ }
+ {% endif -%}
+ {% endfor -%}
+} # end subnet
+{% endif %}
+{% endfor %}