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:
18
playbooks/dhcp-introspection.yaml
Normal file
18
playbooks/dhcp-introspection.yaml
Normal 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
|
||||
19
playbooks/dhcp-provisioning.yaml
Normal file
19
playbooks/dhcp-provisioning.yaml
Normal 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
|
||||
2
roles/dhcp-validations/defaults/main.yml
Normal file
2
roles/dhcp-validations/defaults/main.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
ironic_inspector_conf: "/var/lib/config-data/puppet-generated/ironic_inspector/etc/ironic-inspector/inspector.conf"
|
||||
239
roles/dhcp-validations/files/rogue_dhcp.py
Executable file
239
roles/dhcp-validations/files/rogue_dhcp.py
Executable 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()
|
||||
28
roles/dhcp-validations/meta/main.yml
Normal file
28
roles/dhcp-validations/meta/main.yml
Normal 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: []
|
||||
20
roles/dhcp-validations/tasks/dhcp-introspection.yaml
Normal file
20
roles/dhcp-validations/tasks/dhcp-introspection.yaml
Normal 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
|
||||
14
roles/dhcp-validations/tasks/dhcp-provisioning.yaml
Normal file
14
roles/dhcp-validations/tasks/dhcp-provisioning.yaml
Normal 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') }}
|
||||
Reference in New Issue
Block a user