[cloud] ec2_group fix CIDR with host bits set - fixes #25403 (#29605)

* WIP adds network subnetting functions

* adds functions to convert between netmask and masklen
* adds functions to verify netmask and masklen
* adds function to dtermine network and subnet from address / mask pair

* network_common: add a function to get the first 48 bits in a IPv6 address.

ec2_group: only use network bits of a CIDR.

* Add tests for CIDRs with host bits set.

* ec2_group: add warning if CIDR isn't the networking address.

* Fix pep8.

* Improve wording.

* fix import for network utils

* Update tests to use pytest instead of unittest

* add test for to_ipv6_network()

* Fix PEP8
This commit is contained in:
Sloane Hertel
2017-12-20 14:57:47 -05:00
committed by Ryan Brown
parent 5f215337c9
commit d877c146ab
4 changed files with 385 additions and 101 deletions

View File

@@ -31,8 +31,11 @@ import operator
import socket
from itertools import chain
from struct import pack
from socket import inet_aton, inet_ntoa
from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils.six.moves import zip
from ansible.module_utils.basic import AnsibleFallbackNotFound
try:
@@ -45,6 +48,7 @@ except ImportError:
OPERATORS = frozenset(['ge', 'gt', 'eq', 'neq', 'lt', 'le'])
ALIASES = frozenset([('min', 'ge'), ('max', 'le'), ('exactly', 'eq'), ('neq', 'ne')])
VALID_MASKS = [2**8 - 2**i for i in range(0, 9)]
def to_list(val):
@@ -426,3 +430,106 @@ class Template:
if marker in data:
return True
return False
def is_netmask(val):
parts = str(val).split('.')
if not len(parts) == 4:
return False
for part in parts:
try:
if int(part) not in VALID_MASKS:
raise ValueError
except ValueError:
return False
return True
def is_masklen(val):
try:
return 0 <= int(val) <= 32
except ValueError:
return False
def to_bits(val):
""" converts a netmask to bits """
bits = ''
for octet in val.split('.'):
bits += bin(int(octet))[2:].zfill(8)
return str
def to_netmask(val):
""" converts a masklen to a netmask """
if not is_masklen(val):
raise ValueError('invalid value for masklen')
bits = 0
for i in range(32 - int(val), 32):
bits |= (1 << i)
return inet_ntoa(pack('>I', bits))
def to_masklen(val):
""" converts a netmask to a masklen """
if not is_netmask(val):
raise ValueError('invalid value for netmask: %s' % val)
bits = list()
for x in val.split('.'):
octet = bin(int(x)).count('1')
bits.append(octet)
return sum(bits)
def to_subnet(addr, mask, dotted_notation=False):
""" coverts an addr / mask pair to a subnet in cidr notation """
try:
if not is_masklen(mask):
raise ValueError
cidr = int(mask)
mask = to_netmask(mask)
except ValueError:
cidr = to_masklen(mask)
addr = addr.split('.')
mask = mask.split('.')
network = list()
for s_addr, s_mask in zip(addr, mask):
network.append(str(int(s_addr) & int(s_mask)))
if dotted_notation:
return '%s %s' % ('.'.join(network), to_netmask(cidr))
return '%s/%s' % ('.'.join(network), cidr)
def to_ipv6_network(addr):
""" IPv6 addresses are eight groupings. The first three groupings (48 bits) comprise the network address. """
# Split by :: to identify omitted zeros
ipv6_prefix = addr.split('::')[0]
# Get the first three groups, or as many as are found + ::
found_groups = []
for group in ipv6_prefix.split(':'):
found_groups.append(group)
if len(found_groups) == 3:
break
if len(found_groups) < 3:
found_groups.append('::')
# Concatenate network address parts
network_addr = ''
for group in found_groups:
if group != '::':
network_addr += str(group)
network_addr += str(':')
# Ensure network address ends with ::
if not network_addr.endswith('::'):
network_addr += str(':')
return network_addr

View File

@@ -289,6 +289,7 @@ from ansible.module_utils.ec2 import camel_dict_to_snake_dict
from ansible.module_utils.ec2 import HAS_BOTO3
from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list, compare_aws_tags
from ansible.module_utils.ec2 import AWSRetry
from ansible.module_utils.network.common.utils import to_ipv6_network, to_subnet
import traceback
try:
@@ -521,7 +522,22 @@ def update_rules_description(module, client, rule_type, group_id, ip_permissions
def authorize_ip(type, changed, client, group, groupRules,
ip, ip_permission, module, rule, ethertype):
# If rule already exists, don't later delete it
for thisip in ip:
for this_ip in ip:
split_addr = this_ip.split('/')
if len(split_addr) == 2:
# this_ip is a IPv4 or IPv6 CIDR that may or may not have host bits set
# Get the network bits.
try:
thisip = to_subnet(split_addr[0], split_addr[1])
except ValueError:
thisip = to_ipv6_network(split_addr[0]) + "/" + split_addr[1]
if thisip != this_ip:
module.warn("One of your CIDR addresses ({0}) has host bits set. To get rid of this warning, "
"check the network mask and make sure that only network bits are set: {1}.".format(this_ip, thisip))
else:
thisip = this_ip
rule_id = make_rule_key(type, rule, group['GroupId'], thisip)
if rule_id in groupRules: