Add dhcp-validations role

This patch adds the dhcp-validation role created from the two current
DHCP validations (introspection and provisioning).

To run the DHCP Introspection Network check:

  - hosts: undercloud
    include_role:
      name: dhcp-validations
      tasks_from: dhcp-introspection

To run the DHCP Provisioning Network check:

  - hosts: undercloud
    include_role:
      name: dhcp-validations
      tasks_from: dhcp-provisioning

Change-Id: I74561334c645f1a7b0cf59aab4f41f13514bfd45
Implements: blueprint validation-framework
Signed-off-by: Gael Chamoulaud <gchamoul@redhat.com>
This commit is contained in:
Gael Chamoulaud
2019-02-22 14:22:35 +01:00
parent d289816733
commit 98d61e30a2
7 changed files with 340 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
---
- hosts: undercloud
become: true
vars:
metadata:
name: DHCP on the Introspection Network
description: >
An unexpected DHCP server on the network used for node
introspection can cause some nodes to not be inspected.
This validations checks for the DHCP responses on the
interface specified in ironic-inspector.conf.
groups:
- pre-introspection
tasks:
- include_role:
name: dhcp-validations
tasks_from: dhcp-introspection

View File

@@ -0,0 +1,19 @@
---
- hosts: undercloud
become: true
vars:
metadata:
name: DHCP on the Provisioning Network
description: >
An unexpected DHCP server on the provisioning network can
cause problems with deploying the Ironic nodes.
This validation checks for DHCP responses on the undercloud's
provisioning interface (eth1 by default) and fails if there
are any.
groups:
- pre-deployment
tasks:
- include_role:
name: dhcp-validations
tasks_from: dhcp-provisioning

View File

@@ -0,0 +1,2 @@
---
ironic_inspector_conf: "/var/lib/config-data/puppet-generated/ironic_inspector/etc/ironic-inspector/inspector.conf"

View File

@@ -0,0 +1,239 @@
#!/usr/bin/env python
# Copyright 2017 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import fcntl
import socket
import struct
import sys
import threading
import time
ETH_P_IP = 0x0800
SIOCGIFHWADDR = 0x8927
dhcp_servers = []
interfaces_addresses = {}
class DHCPDiscover(object):
def __init__(self, interface):
self.interface = interface
self.mac = interfaces_addresses[interface]
self.socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW)
def bind(self):
self.socket.bind((self.interface, 0))
def send(self):
packet = self.packet()
self.bind()
self.socket.send(packet)
def close_socket(self):
self.socket.close()
def packet(self):
return self.ethernet_header() \
+ self.ip_header() \
+ self.udp_header() \
+ self.dhcp_discover_payload()
def ethernet_header(self):
return struct.pack('!6s6sH',
b'\xff\xff\xff\xff\xff\xff', # Dest HW address
self.mac, # Source HW address
ETH_P_IP) # EtherType - IPv4
def ip_header(self, checksum=None):
# 0 1 2 3
# 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# |Version| IHL |Type of Service| Total Length |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Identification |Flags| Fragment Offset |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Time to Live | Protocol | Header Checksum |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Source Address |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Destination Address |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
# | Options | Padding |
# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
if checksum is None:
checksum = self.ip_checksum()
return struct.pack('!BBHHHBBHI4s',
(4 << 4) + 5, # IPv4 + 20 bytes header length
0, # TOS
272, # Total Length
1, # Id
0, # Flags & Fragment Offset
64, # TTL
socket.IPPROTO_UDP,
checksum,
0, # Source
socket.inet_aton('255.255.255.255')) # Destination
def ip_checksum(self):
generated_checksum = self._checksum(self.ip_header(checksum=0))
return socket.htons(generated_checksum)
def udp_header(self, checksum=None):
# 0 7 8 15 16 23 24 31
# +--------+--------+--------+--------+
# | Source | Destination |
# | Port | Port |
# +--------+--------+--------+--------+
# | | |
# | Length | Checksum |
# +--------+--------+--------+--------+
if checksum is None:
checksum = self.udp_checksum()
return struct.pack('!HHHH',
68,
67,
252,
checksum)
def udp_checksum(self):
pseudo_header = self.ip_pseudo_header()
generated_checksum = self._checksum(pseudo_header +
self.udp_header(checksum=0) +
self.dhcp_discover_payload())
return socket.htons(generated_checksum)
def ip_pseudo_header(self):
# 0 7 8 15 16 23 24 31
# +--------+--------+--------+--------+
# | source address |
# +--------+--------+--------+--------+
# | destination address |
# +--------+--------+--------+--------+
# | zero |protocol| UDP length |
# +--------+--------+--------+--------+
return struct.pack('!I4sBBH',
0,
socket.inet_aton('255.255.255.255'),
0,
socket.IPPROTO_UDP,
252) # Length
def dhcp_discover_payload(self):
return struct.pack('!BBBBIHHIIII6s10s67s125s4s3s1s',
1, # Message Type - Boot Request
1, # Hardware Type - Ethernet
6, # HW Address Length
0, # Hops
0, # Transaction ID
0, # Seconds elapsed
0, # Bootp flags
0, # Client IP Address
0, # Your IP Address
0, # Next server IP Address
0, # Relay Agent IP Address
self.mac, # Client MAC address
b'\x00' * 10, # Client HW address padding
b'\x00' * 67, # Server host name not given
b'\x00' * 125, # Boot file name not given
b'\x63\x82\x53\x63', # Magic Cookie
b'\x35\x01\x01', # DHCP Message Type = Discover
b'\xff' # Option End
)
def _checksum(self, msg):
s = 0
for i in range(0, len(msg), 2):
w = ord(msg[i]) + (ord(msg[i + 1]) << 8)
s = s + w
s = (s >> 16) + (s & 0xffff)
s = s + (s >> 16)
s = ~s & 0xffff
return s
def get_hw_addresses(interfaces):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for interface in interfaces:
info = fcntl.ioctl(s.fileno(),
SIOCGIFHWADDR,
struct.pack('256s', interface[:15]))
interfaces_addresses[interface] = info[18:24]
s.close()
def inspect_frame(data):
eth_type = struct.unpack('!H', data[12:14])[0]
protocol = ord(data[23])
src_port = struct.unpack('!H', data[34:36])[0]
dst_port = struct.unpack('!H', data[36:38])[0]
msg_type = ord(data[42])
# Make sure we got a DHCP Offer
if eth_type == ETH_P_IP \
and protocol == socket.IPPROTO_UDP \
and src_port == 67 \
and dst_port == 68 \
and msg_type == 2: # DHCP Boot Reply
server_ip_address = '.'.join(["%s" % ord(m) for m in
data[26:30]])
server_hw_address = ":".join(["%02x" % ord(m) for m in
data[6:12]])
dhcp_servers.append([server_ip_address, server_hw_address])
def wait_for_dhcp_offers(interfaces, timeout):
listening_socket = socket.socket(socket.PF_PACKET, socket.SOCK_RAW,
socket.htons(ETH_P_IP))
listening_socket.settimeout(timeout)
allowed_macs = interfaces_addresses.values()
end_of_time = time.time() + timeout
try:
while time.time() < end_of_time:
data = listening_socket.recv(1024)
dst_mac = struct.unpack('!6s', data[0:6])[0]
if dst_mac in allowed_macs:
inspect_frame(data)
except socket.timeout:
pass
listening_socket.close()
def main():
interfaces = sys.argv[1:]
timeout = 5
get_hw_addresses(interfaces)
listening_thread = threading.Thread(target=wait_for_dhcp_offers,
args=[interfaces, timeout])
listening_thread.start()
for interface in interfaces:
dhcp_discover = DHCPDiscover(interface)
dhcp_discover.send()
dhcp_discover.close_socket()
listening_thread.join()
if dhcp_servers:
sys.stderr.write('Found {} DHCP servers:'.format(len(dhcp_servers)))
for ip, mac in dhcp_servers:
sys.stderr.write("\n* {} ({})".format(ip, mac))
sys.exit(1)
else:
print("No DHCP servers found.")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,28 @@
galaxy_info:
author: TripleO Validations Team
company: Red Hat
license: Apache
min_ansible_version: 2.4
platforms:
- name: CentOS
versions:
- 7
- name: RHEL
versions:
- 7
categories:
- cloud
- baremetal
- system
galaxy_tags: []
# List tags for your role here, one per line. A tag is a keyword that describes
# and categorizes the role. Users find roles by searching for tags. Be sure to
# remove the '[]' above, if you add tags to this list.
#
# NOTE: A tag is limited to a single word comprised of alphanumeric characters.
# Maximum 20 tags per role.
dependencies: []

View File

@@ -0,0 +1,20 @@
---
- name: Look up the introspection interface
become: True
ini:
path: "{{ ironic_inspector_conf }}"
section: iptables
key: dnsmasq_interface
register: interface
- name: Look up the introspection interface from the deprecated option
become: True
ini:
path: "{{ ironic_inspector_conf }}"
section: firewall
key: dnsmasq_interface
register: interface_deprecated
- name: Look for rogue DHCP servers
script: files/rogue_dhcp.py {{ interface.value or interface_deprecated.value or 'br-ctlplane' }}
changed_when: False

View File

@@ -0,0 +1,14 @@
---
- name: Get the path of tripleo undercloud config file
hiera: name="tripleo_undercloud_conf_file"
- name: Gather undercloud.conf values
ini:
path: "{{ tripleo_undercloud_conf_file }}"
section: DEFAULT
key: local_interface
ignore_missing_file: True
register: local_interface
- name: Look for DHCP responses
script: files/rogue_dhcp.py {{ local_interface.value|default('eth1') }}