Support DHCP reservations in networks on client side

This commit is contained in:
Joshua Boniface 2018-09-28 20:31:56 -04:00
parent c66a83e6c5
commit 5d35adb4fc
2 changed files with 401 additions and 103 deletions

View File

@ -581,7 +581,7 @@ def cli_network():
@click.option( @click.option(
'-d', '--description', 'description', '-d', '--description', 'description',
default="", default="",
help='Description of the network.' help='Description of the network. Should not contain whitespace.'
) )
@click.option( @click.option(
'-i', '--ipnet', 'ip_network', '-i', '--ipnet', 'ip_network',
@ -621,7 +621,7 @@ def net_add(vni, description, ip_network, ip_gateway, dhcp_flag):
@click.option( @click.option(
'-d', '--description', 'description', '-d', '--description', 'description',
default=None, default=None,
help='Description of the network.' help='Description of the network. Should not contain whitespace.'
) )
@click.option( @click.option(
'-i', '--ipnet', 'ip_network', '-i', '--ipnet', 'ip_network',
@ -715,6 +715,93 @@ def net_list(limit):
retcode, retmsg = pvc_network.get_list(zk_conn, limit) retcode, retmsg = pvc_network.get_list(zk_conn, limit)
cleanup(retcode, retmsg, zk_conn) cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc network dhcp
###############################################################################
@click.group(name='dhcp', short_help='Manage a PVC virtual network DHCP reservations.', context_settings=CONTEXT_SETTINGS)
def net_dhcp():
"""
Manage host DHCP reservations of a VXLAN network in the PVC cluster.
Note: DHCP reservations are only useful if the network has DHCP enabled.
"""
pass
###############################################################################
# pvc network dhcp add
###############################################################################
@click.command(name='add', short_help='Add a DHCP reservation to a virtual network.')
@click.option(
'-d', '--description', 'description',
default=None,
help='Description of the DHCP reservation; defaults to MACADDR if unspecified. Should not contain whitespace.'
)
@click.argument(
'net'
)
@click.argument(
'ipaddr'
)
@click.argument(
'macaddr'
)
def net_dhcp_add(net, ipaddr, macaddr, description):
"""
Add a new DHCP reservation of IP address IPADDR for MAC address MACADDR to virtual network NET; NET can be either a VNI or description.
"""
zk_conn = pvc_common.startZKConnection(zk_host)
retcode, retmsg = pvc_network.add_dhcp_reservation(zk_conn, net, ipaddr, macaddr, description)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc network dhcp remove
###############################################################################
@click.command(name='remove', short_help='Remove a DHCP reservation from a virtual network.')
@click.argument(
'net'
)
@click.argument(
'reservation'
)
def net_dhcp_remove(net, reservation):
"""
Remove a DHCP reservation RESERVATION from virtual network NET; RESERVATION can be either a MAC address, an IP address, or a description; NET can be either a VNI or description.
"""
zk_conn = pvc_common.startZKConnection(zk_host)
retcode, retmsg = pvc_network.remove_dhcp_reservation(zk_conn, net, reservation)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc network dhcp list
###############################################################################
@click.command(name='list', short_help='List all DHCP reservation objects.')
@click.argument(
'net'
)
@click.argument(
'limit', default=None, required=False
)
def net_dhcp_list(net, limit):
"""
List all DHCP reservations in virtual network NET; optionally only match elements matching regex LIMIT; NET can be either a VNI or description.
"""
zk_conn = pvc_common.startZKConnection(zk_host)
retcode, retmsg = pvc_network.get_list_dhcp_reservations(zk_conn, net, limit)
cleanup(retcode, retmsg, zk_conn)
###############################################################################
# pvc network acl
###############################################################################
@click.group(name='acl', short_help='Manage a PVC virtual network firewall ACL rule.', context_settings=CONTEXT_SETTINGS)
def net_acl():
"""
Manage firewall ACLs of a VXLAN network in the PVC cluster.
"""
pass
@ -813,6 +900,12 @@ cli_network.add_command(net_modify)
cli_network.add_command(net_remove) cli_network.add_command(net_remove)
cli_network.add_command(net_info) cli_network.add_command(net_info)
cli_network.add_command(net_list) cli_network.add_command(net_list)
cli_network.add_command(net_dhcp)
cli_network.add_command(net_acl)
net_dhcp.add_command(net_dhcp_add)
net_dhcp.add_command(net_dhcp_remove)
net_dhcp.add_command(net_dhcp_list)
cli.add_command(cli_node) cli.add_command(cli_node)
cli.add_command(cli_router) cli.add_command(cli_router)

View File

@ -101,23 +101,27 @@ def getNetworkDescription(zk_conn, network):
return net_description return net_description
def getNetworkDHCPReservations(zk_conn, vni): def getNetworkDHCPReservations(zk_conn, vni):
n_dhcp_reservations = zk_conn.get_children('/networks/{}/dhcp_reservations'.format(vni)) # Get a list of VNIs by listing the children of /networks/<vni>/dhcp_reservations
return None dhcp_reservations = sorted(zk_conn.get_children('/networks/{}/dhcp_reservations'.format(vni)))
return dhcp_reservations
def getNetworkFirewallRules(zk_conn, vni): def getNetworkFirewallRules(zk_conn, vni):
n_firewall_rules = zk_conn.get_children('/networks/{}/firewall_rules'.format(vni)) firewall_rules = zk_conn.get_children('/networks/{}/firewall_rules'.format(vni))
return None return None
def getNetworkInformation(zk_conn, vni): def getNetworkInformation(zk_conn, vni):
# Obtain basic information
description = zk_conn.get('/networks/{}'.format(vni))[0].decode('ascii') description = zk_conn.get('/networks/{}'.format(vni))[0].decode('ascii')
ip_network = zk_conn.get('/networks/{}/ip_network'.format(vni))[0].decode('ascii') ip_network = zk_conn.get('/networks/{}/ip_network'.format(vni))[0].decode('ascii')
ip_gateway = zk_conn.get('/networks/{}/ip_gateway'.format(vni))[0].decode('ascii') ip_gateway = zk_conn.get('/networks/{}/ip_gateway'.format(vni))[0].decode('ascii')
dhcp_flag = zk_conn.get('/networks/{}/dhcp_flag'.format(vni))[0].decode('ascii') dhcp_flag = zk_conn.get('/networks/{}/dhcp_flag'.format(vni))[0].decode('ascii')
# Add a human-friendly space
return description, ip_network, ip_gateway, dhcp_flag return description, ip_network, ip_gateway, dhcp_flag
def getDHCPReservationInformation(zk_conn, vni, reservation):
description = zkhandler.readdata(zk_conn, '/networks/{}/dhcp_reservations/{}'.format(vni, reservation))
ip_address = zkhandler.readdata(zk_conn, '/networks/{}/dhcp_reservations/{}/ipv4addr'.format(vni, reservation))
mac_address = zkhandler.readdata(zk_conn, '/networks/{}/dhcp_reservations/{}/macaddr'.format(vni, reservation))
return description, ip_address, mac_address
def formatNetworkInformation(zk_conn, vni, long_output): def formatNetworkInformation(zk_conn, vni, long_output):
description, ip_network, ip_gateway, dhcp_flag = getNetworkInformation(zk_conn, vni) description, ip_network, ip_gateway, dhcp_flag = getNetworkInformation(zk_conn, vni)
@ -139,110 +143,26 @@ def formatNetworkInformation(zk_conn, vni, long_output):
ainformation.append('{}DHCP enabled:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), dhcp_flag_colour, dhcp_flag, colour_off)) ainformation.append('{}DHCP enabled:{} {}{}{}'.format(ansiiprint.purple(), ansiiprint.end(), dhcp_flag_colour, dhcp_flag, colour_off))
if long_output: if long_output:
dhcp_reservations = getNetworkDHCPReservations(zk_conn, vni) dhcp_reservations_list = zk_conn.get_children('/networks/{}/dhcp_reservations'.format(vni))
if dhcp_reservations: if dhcp_reservations_list:
ainformation.append('') ainformation.append('')
ainformation.append('{}Client DHCP reservations:{}'.format(ansiiprint.bold(), ansiiprint.end())) ainformation.append('{}Client DHCP reservations:{}'.format(ansiiprint.bold(), ansiiprint.end()))
ainformation.append('') ainformation.append('')
dhcp_reservations_string = formatDHCPReservationList(zk_conn, vni, dhcp_reservations_list)
firewall_rules = getNetworkFirewallRules(zk_conn, vni) for line in dhcp_reservations_string.split('\n'):
ainformation.append(line)
firewall_rules = zk_conn.get_children('/networks/{}/firewall_rules'.format(vni))
if firewall_rules: if firewall_rules:
ainformation.append('') ainformation.append('')
ainformation.append('{}Network firewall rules:{}'.format(ansiiprint.bold(), ansiiprint.end())) ainformation.append('{}Network firewall rules:{}'.format(ansiiprint.bold(), ansiiprint.end()))
ainformation.append('') ainformation.append('')
formatted_firewall_rules = get_list_firewall_rules(zk_conn, vni)
# Join it all together # Join it all together
information = '\n'.join(ainformation) information = '\n'.join(ainformation)
return information return information
# def formatNetworkList(zk_conn, net_list):
# Direct functions
#
def add_network(zk_conn, vni, description, ip_network, ip_gateway, dhcp_flag):
if description == '':
description = vni
# Check if a network with this VNI already exists
if zk_conn.exists('/networks/{}'.format(vni)):
return False, 'ERROR: A network with VNI {} already exists!'.format(vni)
# Add the new network to Zookeeper
transaction = zk_conn.transaction()
transaction.create('/networks/{}'.format(vni), description.encode('ascii'))
transaction.create('/networks/{}/ip_network'.format(vni), ip_network.encode('ascii'))
transaction.create('/networks/{}/ip_gateway'.format(vni), ip_gateway.encode('ascii'))
transaction.create('/networks/{}/dhcp_flag'.format(vni), str(dhcp_flag).encode('ascii'))
transaction.create('/networks/{}/dhcp_reservations'.format(vni), ''.encode('ascii'))
transaction.create('/networks/{}/firewall_rules'.format(vni), ''.encode('ascii'))
results = transaction.commit()
return True, 'Network "{}" added successfully!'.format(description)
def modify_network(zk_conn, vni, **parameters):
# Add the new network to Zookeeper
transaction = zk_conn.transaction()
if parameters['description'] != None:
transaction.set_data('/networks/{}'.format(vni), parameters['description'].encode('ascii'))
if parameters['ip_network'] != None:
transaction.set_data('/networks/{}/ip_network'.format(vni), parameters['ip_network'].encode('ascii'))
if parameters['ip_gateway'] != None:
transaction.set_data('/networks/{}/ip_gateway'.format(vni), parameters['ip_gateway'].encode('ascii'))
if parameters['dhcp_flag'] != None:
transaction.set_data('/networks/{}/dhcp_flag'.format(vni), str(parameters['dhcp_flag']).encode('ascii'))
results = transaction.commit()
return True, 'Network "{}" modified successfully!'.format(vni)
def remove_network(zk_conn, network):
# Validate and obtain alternate passed value
vni = getNetworkVNI(zk_conn, network)
description = getNetworkDescription(zk_conn, network)
if not vni:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
# Delete the configuration
try:
zk_conn.delete('/networks/{}'.format(vni), recursive=True)
except:
pass
return True, 'Network "{}" removed successfully!'.format(description)
def get_info(zk_conn, network, long_output):
# Validate and obtain alternate passed value
net_vni = getNetworkVNI(zk_conn, network)
if net_vni == None:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
information = formatNetworkInformation(zk_conn, net_vni, long_output)
click.echo(information)
click.echo('')
return True, ''
def get_list(zk_conn, limit):
net_list = []
full_net_list = zk_conn.get_children('/networks')
for net in full_net_list:
description = zkhandler.readdata(zk_conn, '/networks/{}'.format(net))
if limit != None:
try:
# Implcitly assume fuzzy limits
if re.match('\^.*', limit) == None:
limit = '.*' + limit
if re.match('.*\$', limit) == None:
limit = limit + '.*'
if re.match(limit, net) != None:
net_list.append(net)
if re.match(limit, description) != None:
net_list.append(net)
except Exception as e:
return False, 'Regex Error: {}'.format(e)
else:
net_list.append(net)
net_list_output = [] net_list_output = []
description = {} description = {}
ip_network = {} ip_network = {}
@ -260,7 +180,6 @@ def get_list(zk_conn, limit):
else: else:
dhcp_flag_colour[net] = ansiiprint.blue() dhcp_flag_colour[net] = ansiiprint.blue()
# Determine optimal column widths # Determine optimal column widths
# Dynamic columns: node_name, hypervisor, migrated # Dynamic columns: node_name, hypervisor, migrated
net_vni_length = 5 net_vni_length = 5
@ -331,7 +250,293 @@ def get_list(zk_conn, limit):
) )
) )
click.echo(net_list_output_header) output_string = net_list_output_header + '\n' + '\n'.join(sorted(net_list_output))
click.echo('\n'.join(sorted(net_list_output))) return output_string
def formatDHCPReservationList(zk_conn, vni, dhcp_reservations_list):
dhcp_reservation_list_output = []
description = {}
ip_address = {}
mac_address = {}
# Gather information for printing
for dhcp_reservation in dhcp_reservations_list:
# get info
description[dhcp_reservation], ip_address[dhcp_reservation], mac_address[dhcp_reservation] = getDHCPReservationInformation(zk_conn, vni, dhcp_reservation)
# Determine optimal column widths
# Dynamic columns: node_name, hypervisor, migrated
reservation_description_length = 13
reservation_ip_address_length = 13
reservation_mac_address_length = 13
for dhcp_reservation in dhcp_reservations_list:
# description column
_reservation_description_length = len(description[dhcp_reservation]) + 1
if _reservation_description_length > reservation_description_length:
reservation_description_length = _reservation_description_length
# ip_network column
_reservation_ip_address_length = len(ip_address[dhcp_reservation]) + 1
if _reservation_ip_address_length > reservation_ip_address_length:
reservation_ip_address_length = _reservation_ip_address_length
# ip_gateway column
_reservation_mac_address_length = len(mac_address[dhcp_reservation]) + 1
if _reservation_mac_address_length > reservation_mac_address_length:
reservation_mac_address_length = _reservation_mac_address_length
# Format the string (header)
dhcp_reservation_list_output_header = '{bold}\
{reservation_description: <{reservation_description_length}} \
{reservation_ip_address: <{reservation_ip_address_length}} \
{reservation_mac_address: <{reservation_mac_address_length}} \
{end_bold}'.format(
bold=ansiiprint.bold(),
end_bold=ansiiprint.end(),
reservation_description_length=reservation_description_length,
reservation_ip_address_length=reservation_ip_address_length,
reservation_mac_address_length=reservation_mac_address_length,
reservation_description='Description',
reservation_ip_address='IP Address',
reservation_mac_address='MAC Address'
)
for dhcp_reservation in dhcp_reservations_list:
dhcp_reservation_list_output.append('{bold}\
{reservation_description: <{reservation_description_length}} \
{reservation_ip_address: <{reservation_ip_address_length}} \
{reservation_mac_address: <{reservation_mac_address_length}} \
{end_bold}'.format(
bold='',
end_bold='',
reservation_description_length=reservation_description_length,
reservation_ip_address_length=reservation_ip_address_length,
reservation_mac_address_length=reservation_mac_address_length,
reservation_description=description[dhcp_reservation],
reservation_ip_address=ip_address[dhcp_reservation],
reservation_mac_address=mac_address[dhcp_reservation]
)
)
output_string = dhcp_reservation_list_output_header + '\n' + '\n'.join(sorted(dhcp_reservation_list_output))
return output_string
def isValidMAC(macaddr):
allowed = re.compile(r"""
(
^([0-9A-F]{2}[:]){5}([0-9A-F]{2})$
)
""",
re.VERBOSE|re.IGNORECASE)
if allowed.match(macaddr) is None:
return False
else:
return True
def isValidIP(ipaddr):
ip_blocks = str(ipaddr).split(".")
if len(ip_blocks) == 4:
for block in ip_blocks:
# Check if number is digit, if not checked before calling this function
if not block.isdigit():
return False
tmp = int(block)
if 0 > tmp > 255:
return False
return True
return False
#
# Direct functions
#
def add_network(zk_conn, vni, description, ip_network, ip_gateway, dhcp_flag):
if description == '':
description = vni
# Check if a network with this VNI already exists
if zk_conn.exists('/networks/{}'.format(vni)):
return False, 'ERROR: A network with VNI {} already exists!'.format(vni)
# Add the new network to Zookeeper
transaction = zk_conn.transaction()
transaction.create('/networks/{}'.format(vni), description.encode('ascii'))
transaction.create('/networks/{}/ip_network'.format(vni), ip_network.encode('ascii'))
transaction.create('/networks/{}/ip_gateway'.format(vni), ip_gateway.encode('ascii'))
transaction.create('/networks/{}/dhcp_flag'.format(vni), str(dhcp_flag).encode('ascii'))
transaction.create('/networks/{}/dhcp_reservations'.format(vni), ''.encode('ascii'))
transaction.create('/networks/{}/firewall_rules'.format(vni), ''.encode('ascii'))
results = transaction.commit()
return True, 'Network "{}" added successfully!'.format(description)
def modify_network(zk_conn, vni, **parameters):
# Add the new network to Zookeeper
transaction = zk_conn.transaction()
if parameters['description'] != None:
transaction.set_data('/networks/{}'.format(vni), parameters['description'].encode('ascii'))
if parameters['ip_network'] != None:
transaction.set_data('/networks/{}/ip_network'.format(vni), parameters['ip_network'].encode('ascii'))
if parameters['ip_gateway'] != None:
transaction.set_data('/networks/{}/ip_gateway'.format(vni), parameters['ip_gateway'].encode('ascii'))
if parameters['dhcp_flag'] != None:
transaction.set_data('/networks/{}/dhcp_flag'.format(vni), str(parameters['dhcp_flag']).encode('ascii'))
results = transaction.commit()
return True, 'Network "{}" modified successfully!'.format(vni)
def remove_network(zk_conn, network):
# Validate and obtain alternate passed value
vni = getNetworkVNI(zk_conn, network)
description = getNetworkDescription(zk_conn, network)
if not vni:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
# Delete the configuration
try:
zk_conn.delete('/networks/{}'.format(vni), recursive=True)
except:
pass
return True, 'Network "{}" removed successfully!'.format(description)
def add_dhcp_reservation(zk_conn, network, ipaddress, macaddress, description):
# Validate and obtain standard passed value
net_vni = getNetworkVNI(zk_conn, network)
if net_vni == None:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
# Use lowercase MAC format exclusively
macaddress = macaddress.lower()
if not isValidMAC(macaddress):
return False, 'ERROR: MAC address "{}" is not valid! Always use ":" as a separator.'.format(macaddress)
if not isValidIP(ipaddress):
return False, 'ERROR: IP address "{}" is not valid!'.format(macaddress)
if not description:
description = macaddress
if zk_conn.exists('/networks/{}/dhcp_reservations/{}'.format(net_vni, description)):
return False, 'ERROR: A reservation with description {} already exists!'.format(description)
# Add the new network to ZK
try:
zkhandler.writedata(zk_conn, {
'/networks/{}/dhcp_reservations/{}'.format(net_vni, description): description,
'/networks/{}/dhcp_reservations/{}/macaddr'.format(net_vni, description): macaddress,
'/networks/{}/dhcp_reservations/{}/ipv4addr'.format(net_vni, description): ipaddress
})
except Exception as e:
return False, 'ERROR: Failed to write to Zookeeper! Exception: "{}".'.format(e)
return True, 'DHCP reservation "{}" added successfully!'.format(description)
def remove_dhcp_reservation(zk_conn, network, reservation):
# Validate and obtain standard passed value
net_vni = getNetworkVNI(zk_conn, network)
if net_vni == None:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
match_description = ''
# Check if the reservation matches a description, a mac, or an IP address currently in the database
reservation_list = zk_conn.get_children('/networks/{}/dhcp_reservations'.format(net_vni))
for description in reservation_list:
macaddress = zkhandler.readdata(zk_conn, '/networks/{}/dhcp_reservations/{}/macaddr'.format(net_vni, description))
ipaddress = zkhandler.readdata(zk_conn, '/networks/{}/dhcp_reservations/{}/ipv4addr'.format(net_vni, description))
if reservation == description or reservation == macaddress or reservation == ipaddress:
match_description = description
if not match_description:
return False, 'ERROR: No DHCP reservation exists matching "{}"!'.format(reservation)
# Remove the entry from zookeeper
try:
zk_conn.delete('/networks/{}/dhcp_reservations/{}'.format(net_vni, match_description), recursive=True)
except:
return False, 'ERROR: Failed to write to Zookeeper!'
return True, 'DHCP reservation "{}" removed successfully!'.format(match_description)
def get_info(zk_conn, network, long_output):
# Validate and obtain alternate passed value
net_vni = getNetworkVNI(zk_conn, network)
if net_vni == None:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
information = formatNetworkInformation(zk_conn, net_vni, long_output)
click.echo(information)
click.echo('')
return True, '' return True, ''
def get_list(zk_conn, limit):
net_list = []
full_net_list = zk_conn.get_children('/networks')
for net in full_net_list:
description = zkhandler.readdata(zk_conn, '/networks/{}'.format(net))
if limit != None:
try:
# Implcitly assume fuzzy limits
if re.match('\^.*', limit) == None:
limit = '.*' + limit
if re.match('.*\$', limit) == None:
limit = limit + '.*'
if re.match(limit, net) != None:
net_list.append(net)
if re.match(limit, description) != None:
net_list.append(net)
except Exception as e:
return False, 'Regex Error: {}'.format(e)
else:
net_list.append(net)
output_string = formatNetworkList(zk_conn, net_list)
click.echo(output_string)
return True, ''
def get_list_dhcp_reservations(zk_conn, network, limit):
# Validate and obtain alternate passed value
net_vni = getNetworkVNI(zk_conn, network)
if net_vni == None:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
dhcp_reservations_list = []
full_dhcp_reservations_list = zk_conn.get_children('/networks/{}/dhcp_reservations'.format(net_vni))
for dhcp_reservation in full_dhcp_reservations_list:
if limit != None:
try:
# Implcitly assume fuzzy limits
if re.match('\^.*', limit) == None:
limit = '.*' + limit
if re.match('.*\$', limit) == None:
limit = limit + '.*'
if re.match(limit, net) != None:
dhcp_reservations_list.append(dhcp_reservation)
if re.match(limit, description) != None:
dhcp_reservations_list.append(dhcp_reservation)
except Exception as e:
return False, 'Regex Error: {}'.format(e)
else:
dhcp_reservations_list.append(dhcp_reservation)
output_string = formatDHCPReservationList(zk_conn, net_vni, dhcp_reservations_list)
click.echo(output_string)
return True, ''
def get_list_firewall_rules(zk_conn, network):
# Validate and obtain alternate passed value
net_vni = getNetworkVNI(zk_conn, network)
if net_vni == None:
return False, 'ERROR: Could not find network "{}" in the cluster!'.format(network)
firewall_rules = getNetworkFirewallRules(zk_conn, net_vni)
return firewall_rules