From 5d35adb4fcd3c9af8961eff30fc35c5246fee618 Mon Sep 17 00:00:00 2001 From: Joshua Boniface Date: Fri, 28 Sep 2018 20:31:56 -0400 Subject: [PATCH] Support DHCP reservations in networks on client side --- cli-client/pvc.py | 97 ++++++- client-common/client_lib/network.py | 407 +++++++++++++++++++++------- 2 files changed, 401 insertions(+), 103 deletions(-) diff --git a/cli-client/pvc.py b/cli-client/pvc.py index c02ddd6e..8074f4e8 100755 --- a/cli-client/pvc.py +++ b/cli-client/pvc.py @@ -581,7 +581,7 @@ def cli_network(): @click.option( '-d', '--description', 'description', default="", - help='Description of the network.' + help='Description of the network. Should not contain whitespace.' ) @click.option( '-i', '--ipnet', 'ip_network', @@ -621,7 +621,7 @@ def net_add(vni, description, ip_network, ip_gateway, dhcp_flag): @click.option( '-d', '--description', 'description', default=None, - help='Description of the network.' + help='Description of the network. Should not contain whitespace.' ) @click.option( '-i', '--ipnet', 'ip_network', @@ -715,6 +715,93 @@ def net_list(limit): retcode, retmsg = pvc_network.get_list(zk_conn, limit) 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_info) 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_router) diff --git a/client-common/client_lib/network.py b/client-common/client_lib/network.py index 53737afa..180cb1d9 100644 --- a/client-common/client_lib/network.py +++ b/client-common/client_lib/network.py @@ -101,23 +101,27 @@ def getNetworkDescription(zk_conn, network): return net_description def getNetworkDHCPReservations(zk_conn, vni): - n_dhcp_reservations = zk_conn.get_children('/networks/{}/dhcp_reservations'.format(vni)) - return None + # Get a list of VNIs by listing the children of /networks//dhcp_reservations + dhcp_reservations = sorted(zk_conn.get_children('/networks/{}/dhcp_reservations'.format(vni))) + return dhcp_reservations 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 def getNetworkInformation(zk_conn, vni): - # Obtain basic information description = zk_conn.get('/networks/{}'.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') 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 +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): 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)) if long_output: - dhcp_reservations = getNetworkDHCPReservations(zk_conn, vni) - if dhcp_reservations: + dhcp_reservations_list = zk_conn.get_children('/networks/{}/dhcp_reservations'.format(vni)) + if dhcp_reservations_list: ainformation.append('') ainformation.append('{}Client DHCP reservations:{}'.format(ansiiprint.bold(), ansiiprint.end())) ainformation.append('') - - firewall_rules = getNetworkFirewallRules(zk_conn, vni) + dhcp_reservations_string = formatDHCPReservationList(zk_conn, vni, dhcp_reservations_list) + 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: ainformation.append('') ainformation.append('{}Network firewall rules:{}'.format(ansiiprint.bold(), ansiiprint.end())) ainformation.append('') + formatted_firewall_rules = get_list_firewall_rules(zk_conn, vni) # Join it all together information = '\n'.join(ainformation) return information -# -# 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) - +def formatNetworkList(zk_conn, net_list): net_list_output = [] description = {} ip_network = {} @@ -260,7 +180,6 @@ def get_list(zk_conn, limit): else: dhcp_flag_colour[net] = ansiiprint.blue() - # Determine optimal column widths # Dynamic columns: node_name, hypervisor, migrated net_vni_length = 5 @@ -331,7 +250,293 @@ def get_list(zk_conn, limit): ) ) - click.echo(net_list_output_header) - click.echo('\n'.join(sorted(net_list_output))) + output_string = net_list_output_header + '\n' + '\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, '' + +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