kayobe/ansible/roles/ip-allocation/library/ip_allocation.py
Pierre Riteau 697e74e41a Stop allocating network and broadcast addresses
When an allocation pool range is not defined by the operator, we should
not include network and broadcast addresses in the list of IP addresses
to give to hosts.

Change-Id: Id6e14286b5eb2b767a515e7edfc56741fb8d2c78
Story: 2006267
Task: 35958
2019-08-02 14:08:34 +02:00

202 lines
6.5 KiB
Python

#!/usr/bin/python
# Copyright (c) 2017 StackHPC Ltd.
#
# 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.
DOCUMENTATION = """
module: ip_allocation
short_description: Allocate an IP address for a host from a pool
author: Mark Goddard (mark@stackhpc.com)
options:
- option-name: net_name
description: Name of the network
required: True
type: string
- option-name: hostname
description: Name of the host
required: True
type: string
- option-name: cidr
description: IP Network in CIDR format
required: True
type: string
- option-name: allocation_pool_start
description: First address of the pool from which to allocate
required: False
type: string
- option-name: allocation_pool_end
description: Last address of the pool from which to allocate
required: False
type: string
- option-name: allocation_file
description: >
Path to a file in which to store the allocations. Will be created if it
does not exist.
required: True
type: string
requirements:
- netaddr
- PyYAML
"""
EXAMPLES = """
- name: Ensure host has an IP address
ip_allocation:
net_name: my-network
hostname: my-host
cidr: 10.0.0.0/24
allocation_pool_start: 10.0.0.1
allocation_pool_end: 10.0.0.254
allocation_file: /path/to/allocation/file.yml
"""
RETURN = """
ip:
description: The allocated IP address
returned: success
type: string
sample: 10.0.0.1
"""
from ansible.module_utils.basic import *
import sys
# Store a list of import errors to report to the user.
IMPORT_ERRORS=[]
try:
import netaddr
except Exception as e:
IMPORT_ERRORS.append(e)
try:
import yaml
except Exception as e:
IMPORT_ERRORS.append(e)
def read_allocations(module):
"""Read IP address allocations from the allocation file."""
filename = module.params['allocation_file']
try:
with open(filename, 'r') as f:
content = yaml.safe_load(f)
except IOError as e:
if e.errno == errno.ENOENT:
# Ignore ENOENT - we will create the file.
return {}
module.fail_json(msg="Failed to open allocation file %s for reading" % filename)
except yaml.YAMLError as e:
module.fail_json(msg="Failed to parse allocation file %s as YAML" % filename)
if content is None:
# If the file is empty, yaml.safe_load() will return None.
content = {}
return content
def write_allocations(module, allocations):
"""Write IP address allocations to the allocation file."""
filename = module.params['allocation_file']
try:
with open(filename, 'w') as f:
yaml.dump(allocations, f, default_flow_style=False)
except IOError as e:
module.fail_json(msg="Failed to open allocation file %s for writing" % filename)
except yaml.YAMLError as e:
module.fail_json(msg="Failed to dump allocation file %s as YAML" % filename)
def update_allocation(module, allocations):
"""Allocate an IP address on a network for a host.
:param module: AnsibleModule instance
:param allocations: Existing IP address allocations
"""
net_name = module.params['net_name']
hostname = module.params['hostname']
cidr = module.params['cidr']
allocation_pool_start = module.params['allocation_pool_start']
allocation_pool_end = module.params['allocation_pool_end']
network = netaddr.IPNetwork(cidr)
result = {
'changed': False,
}
object_name = "%s_ips" % net_name
net_allocations = allocations.setdefault(object_name, {})
invalid_allocations = {hn: ip for hn, ip in net_allocations.items()
if netaddr.IPAddress(ip) not in network}
if invalid_allocations:
module.fail_json(msg="Found invalid existing allocations in network %s: %s" %
(network,
", ".join("%s: %s" % (hn, ip)
for hn, ip in invalid_allocations.items())))
if hostname not in net_allocations:
result['changed'] = True
allocated_ips = netaddr.IPSet(net_allocations.values())
if allocation_pool_start and allocation_pool_end:
allocation_pool = netaddr.IPRange(allocation_pool_start, allocation_pool_end)
allocation_pool = netaddr.IPSet(allocation_pool)
else:
allocation_pool = netaddr.IPSet([network])
if network.prefixlen != 32:
reserved_ips = [network.network, network.broadcast]
allocation_pool -= netaddr.IPSet(reserved_ips)
free_ips = allocation_pool - allocated_ips
for free_cidr in free_ips.iter_cidrs():
ip = free_cidr[0]
break
else:
module.fail_json(msg="No unallocated IP addresses for %s in %s" % (hostname, net_name))
free_ips.remove(ip)
net_allocations[hostname] = str(ip)
result['ip'] = net_allocations[hostname]
return result
def allocate(module):
"""Allocate an IP address for a host, updating the allocation file."""
allocations = read_allocations(module)
result = update_allocation(module, allocations)
if result['changed'] and not module.check_mode:
write_allocations(module, allocations)
return result
def main():
module = AnsibleModule(
argument_spec=dict(
net_name=dict(required=True, type='str'),
hostname=dict(required=True, type='str'),
cidr=dict(required=True, type='str'),
allocation_pool_start=dict(required=False, type='str'),
allocation_pool_end=dict(required=False, type='str'),
allocation_file=dict(required=True, type='str'),
),
supports_check_mode=True,
)
# Fail if there were any exceptions when importing modules.
if IMPORT_ERRORS:
module.fail_json(msg="Import errors: %s" %
", ".join([repr(e) for e in IMPORT_ERRORS]))
try:
results = allocate(module)
except Exception as e:
module.fail_json(msg="Failed to allocate IP address: %s" % repr(e))
else:
module.exit_json(**results)
if __name__ == '__main__':
main()