From 568656d2bd0341aacc74a936e28315b06f1881ca Mon Sep 17 00:00:00 2001 From: Feng Pan Date: Fri, 22 Apr 2016 18:49:07 -0400 Subject: Add python parsing library for network settings file. Changes: - Implements network_settings.yaml file parsing in python. - Adds support for both IPv4 and IPv6 in network_settings.yaml - Adds support for api_network in network_settings.yaml - Removes bash library functions for network related functions. - Adds dependency to python34-yaml for apex-common package. Note that support for ipv6 and api_network is not complete yet. Proper configuriration of network environment and nic template files will be added later. Change-Id: I087f725dabedfef109c9de1f58ce2611da647e87 Signed-off-by: Feng Pan --- build/opnfv-apex-common.spec | 12 +- ci/deploy.sh | 127 +--------------- lib/common-functions.sh | 320 +--------------------------------------- lib/python/apex-python-utils.py | 67 +++++++++ lib/python/apex/__init__.py | 2 + lib/python/apex/ip_utils.py | 228 ++++++++++++++++++++++++++-- lib/python/apex/net_env.py | 242 ++++++++++++++++++++++++++++++ 7 files changed, 542 insertions(+), 456 deletions(-) create mode 100755 lib/python/apex-python-utils.py create mode 100644 lib/python/apex/net_env.py diff --git a/build/opnfv-apex-common.spec b/build/opnfv-apex-common.spec index 89fb4030..56f0f305 100644 --- a/build/opnfv-apex-common.spec +++ b/build/opnfv-apex-common.spec @@ -11,7 +11,7 @@ Source0: opnfv-apex-common.tar.gz BuildArch: noarch BuildRequires: python-docutils python34-devel Requires: openstack-tripleo opnfv-apex-sdn opnfv-apex-undercloud openvswitch qemu-kvm bridge-utils libguestfs-tools -Requires: initscripts net-tools iputils iproute iptables python34 +Requires: initscripts net-tools iputils iproute iptables python34 python34-yaml %description Scripts for OPNFV deployment using RDO Manager @@ -39,12 +39,14 @@ install config/deploy/os-onos-nofeature-ha.yaml %{buildroot}%{_sysconfdir}/opnfv install config/deploy/os-opencontrail-nofeature-ha.yaml %{buildroot}%{_sysconfdir}/opnfv-apex/os-opencontrail-nofeature-ha.yaml install config/network/network_settings.yaml %{buildroot}%{_sysconfdir}/opnfv-apex/network_settings.yaml -mkdir -p %{buildroot}%{_var}/opt/opnfv/lib/ +mkdir -p %{buildroot}%{_var}/opt/opnfv/lib/python/apex install lib/common-functions.sh %{buildroot}%{_var}/opt/opnfv/lib/ install lib/utility-functions.sh %{buildroot}%{_var}/opt/opnfv/lib/ +install lib/python/apex-python-utils.py %{buildroot}%{_var}/opt/opnfv/lib/python/ mkdir -p %{buildroot}%{python3_sitelib}/apex/ install lib/python/apex/__init__.py %{buildroot}%{python3_sitelib}/apex/ install lib/python/apex/ip_utils.py %{buildroot}%{python3_sitelib}/apex/ +install lib/python/apex/net_env.py %{buildroot}%{python3_sitelib}/apex/ mkdir -p %{buildroot}%{_var}/opt/opnfv/lib/installer/onos/ install lib/installer/onos/onos_gw_mac_update.sh %{buildroot}%{_var}/opt/opnfv/lib/installer/onos/ @@ -64,7 +66,11 @@ install config/inventory/pod_example_settings.yaml %{buildroot}%{_docdir}/opnfv/ %attr(755,root,root) %{_bindir}/opnfv-util %{_var}/opt/opnfv/lib/common-functions.sh %{_var}/opt/opnfv/lib/utility-functions.sh +%{_var}/opt/opnfv/lib/python/apex-python-utils.py +%{_var}/opt/opnfv/lib/python/apex-python-utils.pyo +%{_var}/opt/opnfv/lib/python/apex-python-utils.pyc %{python3_sitelib}/apex/ip_utils.py +%{python3_sitelib}/apex/net_env.py %{python3_sitelib}/apex/__init__.py %{python3_sitelib}/apex/__pycache__/* %{_var}/opt/opnfv/lib/installer/onos/onos_gw_mac_update.sh @@ -84,6 +90,8 @@ install config/inventory/pod_example_settings.yaml %{buildroot}%{_docdir}/opnfv/ %doc %{_docdir}/opnfv/inventory.yaml.example %changelog +* Fri Apr 22 2016 Feng Pan - 3.0-3 +- Adds python network setting parsing lib. * Fri Apr 15 2016 Feng Pan - 3.0-2 - Adds python ip utility lib. * Mon Apr 11 2016 Tim Rozet - 3.0-1 diff --git a/ci/deploy.sh b/ci/deploy.sh index ebcde335..cd76558c 100755 --- a/ci/deploy.sh +++ b/ci/deploy.sh @@ -101,126 +101,13 @@ parse_setting_value() { } ##parses network settings yaml into globals parse_network_settings() { - local required_network_settings="cidr" - local common_optional_network_settings="usable_ip_range" - local admin_network_optional_settings="provisioner_ip dhcp_range introspection_range" - local public_network_optional_settings="floating_ip_range gateway provisioner_ip" - local nic_value cidr - - eval $(parse_yaml ${NETSETS}) - for network in ${OPNFV_NETWORK_TYPES}; do - if [[ $(eval echo \${${network}_enabled}) == 'true' ]]; then - enabled_network_list+="${network} " - elif [ "${network}" == 'admin_network' ]; then - echo -e "${red}ERROR: You must enable admin_network and configure it explicitly or use auto-detection${reset}" - exit 1 - elif [[ "${network}" == 'public_network' && "$net_isolation_enabled" == "TRUE" ]]; then - echo -e "${red}ERROR: You must enable public_network and configure it explicitly or use auto-detection${reset}" + if output=$(python3.4 -B $CONFIG/lib/python/apex-python-utils.py parse_net_settings -n $NETSETS -i $net_isolation_enabled); then + eval "$output" + echo -e "${blue}${output}${reset}" + else exit 1 - else - echo -e "${blue}INFO: Network: ${network} is disabled, will collapse into admin_network" - fi - done + fi - # check for enabled network values - for enabled_network in ${enabled_network_list}; do - # detect required settings first to continue - echo -e "${blue}INFO: Detecting Required settings for: ${enabled_network}${reset}" - for setting in ${required_network_settings}; do - eval "setting_value=\${${enabled_network}_${setting}}" - if [ -z "${setting_value}" ]; then - # if setting is missing we try to autodetect - eval "nic_value=\${${enabled_network}_bridged_interface}" - if [ -n "$nic_value" ]; then - setting_value=$(eval find_${setting} ${nic_value}) - if [ -n "$setting_value" ]; then - eval "${enabled_network}_${setting}=${setting_value}" - echo -e "${blue}INFO: Auto-detection: ${enabled_network}_${setting}: ${setting_value}${reset}" - else - echo -e "${red}ERROR: Auto-detection failed: ${setting} not found using interface: ${nic_value}${reset}" - exit 1 - fi - else - echo -e "${red}ERROR: Required setting: ${setting} not found, and bridge interface not provided\ -for Auto-detection${reset}" - exit 1 - fi - else - echo -e "${blue}INFO: ${enabled_network}_${setting}: ${setting_value}${reset}" - fi - done - echo -e "${blue}INFO: Detecting Common settings for: ${enabled_network}${reset}" - # detect optional common settings - # these settings can be auto-generated if missing - for setting in ${common_optional_network_settings}; do - eval "setting_value=\${${enabled_network}_${setting}}" - if [ -z "${setting_value}" ]; then - if [ -n "$nic_value" ]; then - setting_value=$(eval find_${setting} ${nic_value}) - else - setting_value='' - echo -e "${blue}INFO: Skipping Auto-detection, NIC not specified for ${enabled_network}. Attempting Auto-generation...${reset}" - fi - if [ -n "$setting_value" ]; then - eval "${enabled_network}_${setting}=${setting_value}" - echo -e "${blue}INFO: Auto-detection: ${enabled_network}_${setting}: ${setting_value}${reset}" - else - # if Auto-detection fails we can auto-generate with CIDR - eval "cidr=\${${enabled_network}_cidr}" - if [ -n "$cidr" ]; then - echo -e "${blue}INFO: Auto-generating: ${setting}${reset}" - setting_value=$(eval generate_${setting} ${cidr}) - else - setting_value='' - echo -e "${red}ERROR: Auto-generation failed: required parameter CIDR missing for network ${enabled_network}${reset}" - fi - if [ -n "$setting_value" ]; then - eval "${enabled_network}_${setting}=${setting_value}" - echo -e "${blue}INFO: Auto-generated: ${enabled_network}_${setting}: ${setting_value}${reset}" - else - echo -e "${red}ERROR: Auto-generation failed: ${setting} not found${reset}" - exit 1 - fi - fi - else - echo -e "${blue}INFO: ${enabled_network}_${setting}: ${setting_value}${reset}" - fi - done - echo -e "${blue}INFO: Detecting Network Specific settings for: ${enabled_network}${reset}" - # detect network specific settings - for setting in $(eval echo \${${enabled_network}_optional_settings}); do - eval "setting_value=\${${enabled_network}_${setting}}" - if [ -z "${setting_value}" ]; then - if [ -n "$nic_value" ]; then - setting_value=$(eval find_${setting} ${nic_value}) - else - setting_value='' - echo -e "${blue}INFO: Skipping Auto-detection, NIC not specified for ${enabled_network}. Attempting Auto-generation...${reset}" - fi - if [ -n "$setting_value" ]; then - eval "${enabled_network}_${setting}=${setting_value}" - echo -e "${blue}INFO: Auto-detection: ${enabled_network}_${setting}: ${setting_value}${reset}" - else - eval "cidr=\${${enabled_network}_cidr}" - if [ -n "$cidr" ]; then - setting_value=$(eval generate_${setting} ${cidr}) - else - setting_value='' - echo -e "${red}ERROR: Auto-generation failed: required parameter CIDR missing for network ${enabled_network}${reset}" - fi - if [ -n "$setting_value" ]; then - eval "${enabled_network}_${setting}=${setting_value}" - echo -e "${blue}INFO: Auto-generated: ${enabled_network}_${setting}: ${setting_value}${reset}" - else - echo -e "${red}ERROR: Auto-generation failed: ${setting} not found${reset}" - exit 1 - fi - fi - else - echo -e "${blue}INFO: ${enabled_network}_${setting}: ${setting_value}${reset}" - fi - done - done } ##parses deploy settings yaml into globals and options array ##params: none @@ -686,7 +573,7 @@ function configure_network_environment { sed -i '/EC2MetadataIp/c\\ EC2MetadataIp: '${admin_network_provisioner_ip}'' $1 # check for private network - if [[ ! -z "$private_network_enabled" && "$private_network_enabled" == "true" ]]; then + if [[ ! -z "$private_network_enabled" && "$private_network_enabled" == "True" ]]; then sed -i 's#^.*Network::Tenant.*$# OS::TripleO::Network::Tenant: '${tht_dir}'/tenant.yaml#' $1 sed -i 's#^.*Controller::Ports::TenantPort:.*$# OS::TripleO::Controller::Ports::TenantPort: '${tht_dir}'/ports/tenant.yaml#' $1 sed -i 's#^.*Compute::Ports::TenantPort:.*$# OS::TripleO::Compute::Ports::TenantPort: '${tht_dir}'/ports/tenant.yaml#' $1 @@ -700,7 +587,7 @@ function configure_network_environment { fi # check for storage network - if [[ ! -z "$storage_network_enabled" && "$storage_network_enabled" == "true" ]]; then + if [[ ! -z "$storage_network_enabled" && "$storage_network_enabled" == "True" ]]; then sed -i 's#^.*Network::Storage:.*$# OS::TripleO::Network::Storage: '${tht_dir}'/storage.yaml#' $1 sed -i 's#^.*Network::Ports::StorageVipPort:.*$# OS::TripleO::Network::Ports::StorageVipPort: '${tht_dir}'/ports/storage.yaml#' $1 sed -i 's#^.*Controller::Ports::StoragePort:.*$# OS::TripleO::Controller::Ports::StoragePort: '${tht_dir}'/ports/storage.yaml#' $1 diff --git a/lib/common-functions.sh b/lib/common-functions.sh index 32ee6bcc..e7041ac4 100644 --- a/lib/common-functions.sh +++ b/lib/common-functions.sh @@ -2,9 +2,6 @@ # Common Functions used by OPNFV Apex # author: Tim Rozet (trozet@redhat.com) -#python ip_gen command -ip_gen="python3.4 -B -m apex.ip_utils generate_ip_range" - ##converts subnet mask to prefix ##params: subnet mask function prefix2mask { @@ -17,324 +14,11 @@ function prefix2mask { ##find ip of interface ##params: interface name function find_ip { - ip addr show $1 | grep -Eo '^\s+inet\s+[\.0-9]+' | awk '{print $2}' -} - -##finds subnet of ip and netmask -##params: ip, netmask -function find_subnet { - IFS=. read -r i1 i2 i3 i4 <<< "$1" - IFS=. read -r m1 m2 m3 m4 <<< "$2" - printf "%d.%d.%d.%d\n" "$((i1 & m1))" "$((i2 & m2))" "$((i3 & m3))" "$((i4 & m4))" -} - -##verify subnet has at least n IPs -##params: subnet mask, n IPs -function verify_subnet_size { - IFS=. read -r i1 i2 i3 i4 <<< "$1" - num_ips_required=$2 - - ##this function assumes you would never need more than 254 - ##we check here to make sure - if [ "$num_ips_required" -ge 254 ]; then - echo -e "\n\n${red}ERROR: allocating more than 254 IPs is unsupported...Exiting${reset}\n\n" - return 1 - fi - - ##we just return if 3rd octet is not 255 - ##because we know the subnet is big enough - if [ "$i3" -ne 255 ]; then - return 0 - elif [ $((254-$i4)) -ge "$num_ips_required" ]; then - return 0 - else - echo -e "\n\n${red}ERROR: Subnet is too small${reset}\n\n" - return 1 - fi -} - -##finds last usable ip (broadcast minus 1) of a subnet from an IP and netmask -## Warning: This function only works for IPv4 at the moment. -##params: ip, netmask -function find_last_ip_subnet { - IFS=. read -r i1 i2 i3 i4 <<< "$1" - IFS=. read -r m1 m2 m3 m4 <<< "$2" - IFS=. read -r s1 s2 s3 s4 <<< "$((i1 & m1)).$((i2 & m2)).$((i3 & m3)).$((i4 & m4))" - printf "%d.%d.%d.%d\n" "$((255 - $m1 + $s1))" "$((255 - $m2 + $s2))" "$((255 - $m3 + $s3))" "$((255 - $m4 + $s4 - 1))" -} - -##increments subnet by a value -##params: ip, value -##assumes low value -function increment_subnet { - IFS=. read -r i1 i2 i3 i4 <<< "$1" - printf "%d.%d.%d.%d\n" "$i1" "$i2" "$i3" "$((i4 | $2))" -} - -##finds netmask of interface -##params: interface -##returns long format 255.255.x.x -function find_netmask { - ifconfig $1 | grep -Eo 'netmask\s+[\.0-9]+' | awk '{print $2}' -} - -##finds short netmask of interface -##params: interface -##returns short format, ex: /21 -function find_short_netmask { - echo "/$(ip addr show $1 | grep -Eo '^\s+inet\s+[\/\.0-9]+' | awk '{print $2}' | cut -d / -f2)" -} - -##increments next IP -##params: ip -##assumes a /24 subnet -function next_ip { - baseaddr="$(echo $1 | cut -d. -f1-3)" - lsv="$(echo $1 | cut -d. -f4)" - if [ "$lsv" -ge 254 ]; then - return 1 - fi - ((lsv++)) - echo $baseaddr.$lsv -} - -##subtracts a value from an IP address -##params: last ip, ip_count -##assumes ip_count is less than the last octect of the address -subtract_ip() { - IFS=. read -r i1 i2 i3 i4 <<< "$1" - ip_count=$2 - if [ $i4 -lt $ip_count ]; then - echo -e "\n\n${red}ERROR: Can't subtract $ip_count from IP address $1 Exiting${reset}\n\n" - exit 1 - fi - printf "%d.%d.%d.%d\n" "$i1" "$i2" "$i3" "$((i4 - $ip_count ))" -} - -##check if IP is in use -##params: ip -##ping ip to get arp entry, then check arp -function is_ip_used { - ping -c 5 $1 > /dev/null 2>&1 - arp -n | grep "$1 " | grep -iv incomplete > /dev/null 2>&1 -} - -##find next usable IP -##params: ip -function next_usable_ip { - new_ip=$(next_ip $1) - while [ "$new_ip" ]; do - if ! is_ip_used $new_ip; then - echo $new_ip - return 0 - fi - new_ip=$(next_ip $new_ip) - done - return 1 -} - -##increment ip by value -##params: ip, amount to increment by -##increment_ip $next_private_ip 10 -function increment_ip { - baseaddr="$(echo $1 | cut -d. -f1-3)" - lsv="$(echo $1 | cut -d. -f4)" - incrval=$2 - lsv=$((lsv+incrval)) - if [ "$lsv" -ge 254 ]; then - return 1 - fi - echo $baseaddr.$lsv -} - -##finds gateway on system -##params: interface to validate gateway on (optional) -##find_gateway em1 -function find_gateway { - local gw gw_interface - if [ -z "$1" ]; then - return 1 - fi - gw=$(ip route | grep default | awk '{print $3}') - gw_interface=$(ip route get $gw | awk '{print $3}') - if [ -n "$1" ]; then - if [ "$gw_interface" == "$1" ]; then - echo ${gw} - fi - fi -} - -##finds subnet in CIDR notation for interface -##params: interface to find CIDR -function find_cidr { - local cidr network ip netmask short_mask - if [ -z "$1" ]; then - return 1 - fi - ip=$(find_ip $1) - netmask=$(find_netmask $1) - if [[ -z "$ip" || -z "$netmask" ]]; then - return 1 - fi - network=$(find_subnet ${ip} ${netamsk}) - short_mask=$(find_short_netmask $1) - if [[ -z "$network" || -z "$short_mask" ]]; then - return 1 - fi - cidr="${subnet}'\'${short_mask}" - echo ${cidr} -} - -##finds block of usable IP addresses for an interface -##simply returns at the moment the correct format -##after first 20 IPs, and leave 20 IPs at end of subnet (for floating ips, etc) -##params: interface to find IP -function find_usable_ip_range { - local interface_ip subnet_mask first_block_ip last_block_ip - if [ -z "$1" ]; then - return 1 - fi - interface_ip=$(find_ip $1) - subnet_mask=$(find_netmask $1) - if [[ -z "$interface_ip" || -z "$subnet_mask" ]]; then - return 1 - fi - interface_ip=$(increment_ip ${interface_ip} 20) - first_block_ip=$(next_usable_ip ${interface_ip}) - if [ -z "$first_block_ip" ]; then - return 1 - fi - last_block_ip=$(find_last_ip_subnet ${interface_ip} ${subnet_mask}) - if [ -z "$last_block_ip" ]; then - return 1 - else - last_block_ip=$(subtract_ip ${last_block_ip} 21) - echo "${first_block_ip},${last_block_ip}" - fi - -} - -##generates usable IP range in correct format based on CIDR -##A block of 20 IP addresses are reserved at beginning of address space. -##A block of 22 IP addresses are reserved at end of address space, this includes -##the broadcast IP address. -##In a /24 IPv4 CIDR, this results in .1-20 as as .234-255 being excluded. -##params: cidr -function generate_usable_ip_range { - if [ -z "$1" ]; then - return 1 - fi - echo $($ip_gen $1 21 -23) -} - - -##find the undercloud IP address -##finds first usable IP on subnet -##params: interface -function find_provisioner_ip { - local interface_ip - if [ -z "$1" ]; then - return 1 - fi - interface_ip=$(find_ip $1) - if [ -z "$interface_ip" ]; then - return 1 - fi - echo $(increment_ip ${interface_ip} 1) -} - -##generates undercloud IP address based on CIDR -##params: cidr -function generate_provisioner_ip { - if [ -z "$1" ]; then - return 1 - fi - echo $($ip_gen $1 1 1) -} - - -##finds the dhcp range available via interface -##uses first 8 IPs, after 2nd IP -##params: interface -function find_dhcp_range { - local dhcp_range_start dhcp_range_end interface_ip - if [ -z "$1" ]; then - return 1 - fi - interface_ip=$(find_ip $1) - if [ -z "$interface_ip" ]; then - return 1 - fi - dhcp_range_start=$(increment_ip ${interface_ip} 2) - dhcp_range_end=$(increment_ip ${dhcp_range_start} 8) - echo "${dhcp_range_start},${dhcp_range_end}" -} - -##generates the dhcp range available via CIDR -##uses first 8 IPs, after 1st IP -##params: cidr -function generate_dhcp_range { - if [ -z "$1" ]; then - return 1 - fi - echo $($ip_gen $1 2 10) -} - -##finds the introspection range available via interface -##uses 8 IPs, after the first 10 IPs -##params: interface -function find_introspection_range { - local inspect_range_start inspect_range_end interface_ip - if [ -z "$1" ]; then - return 1 - fi - interface_ip=$(find_ip $1) - if [ -z "$interface_ip" ]; then - return 1 - fi - inspect_range_start=$(increment_ip ${interface_ip} 10) - inspect_range_end=$(increment_ip ${inspect_range_start} 8) - echo "${inspect_range_start},${inspect_range_end}" -} - -##generate the introspection range available via CIDR -##uses 8 IPs, after the first 10 IPs -##params: cidr -function generate_introspection_range { - if [ -z "$1" ]; then - return 1 - fi - echo $($ip_gen $1 11 19) -} - -##finds the floating ip range available via interface -##uses last 20 IPs of a subnet, minus last IP -##params: interface -function find_floating_ip_range { - local float_range_start float_range_end interface_ip subnet_mask - if [ -z "$1" ]; then - return 1 - fi - interface_ip=$(find_ip $1) - subnet_mask=$(find_netmask $1) - if [[ -z "$interface_ip" || -z "$subnet_mask" ]]; then + if [[ -z "$1" ]]; then return 1 fi - float_range_end=$(find_last_ip_subnet ${interface_ip} ${subnet_mask}) - float_range_end=$(subtract_ip ${float_range_end} 1) - float_range_start=$(subtract_ip ${float_range_end} 19) - echo "${float_range_start},${float_range_end}" -} -##generate the floating range available via CIDR -##uses last 20 IPs of subnet, minus last 2 IPs. -##In a /24 IPv4 CIDR, this would result in floating ip range of .234-253 -##params: cidr -function generate_floating_ip_range { - if [ -z "$1" ]; then - return 1 - fi - echo $($ip_gen $1 -22 -3) + python3.4 -B $CONFIG/lib/python/apex-python-utils.py find_ip -i $1 } ##attach interface to OVS and set the network config correctly diff --git a/lib/python/apex-python-utils.py b/lib/python/apex-python-utils.py new file mode 100755 index 00000000..802e8571 --- /dev/null +++ b/lib/python/apex-python-utils.py @@ -0,0 +1,67 @@ +############################################################################## +# Copyright (c) 2016 Feng Pan (fpan@redhat.com) +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +import argparse +import sys +import apex +import logging +import os + +def parse_net_settings(settings_args): + settings = apex.NetworkSettings(settings_args.path, + settings_args.network_isolation) + settings.dump_bash() + + +def find_ip(int_args): + interface = apex.ip_utils.get_interface(int_args.interface, + int_args.address_family) + if interface: + print(interface.ip) + + +parser = argparse.ArgumentParser() +parser.add_argument('--DEBUG', action='store_true', default=False, + help="Turn on debug messages") +subparsers = parser.add_subparsers() + +net_settings = subparsers.add_parser('parse_net_settings', + help='Parse network settings file') +net_settings.add_argument('-n', '--path', default='network_settings.yaml', + help='path to network settings file') +net_settings.add_argument('-i', '--network_isolation', type=bool, default=True, + help='network isolation') +net_settings.set_defaults(func=parse_net_settings) + +get_int_ip = subparsers.add_parser('find_ip', + help='Find interface ip') +get_int_ip.add_argument('-i', '--interface', required=True, + help='Interface name') +get_int_ip.add_argument('-af', '--address_family', default=4, type=int, + choices=[4, 6], + help='IP Address family') +get_int_ip.set_defaults(func=find_ip) + +args = parser.parse_args(sys.argv[1:]) +if args.DEBUG: + logging.basicConfig(level=logging.DEBUG) +else: + apex_log_filename = '/var/log/apex/apex.log' + os.makedirs(os.path.dirname(apex_log_filename), exist_ok=True) + logging.basicConfig(filename=apex_log_filename, + format='%(asctime)s %(levelname)s: %(message)s', + datefmt='%m/%d/%Y %I:%M:%S %p', + level=logging.DEBUG) + +if hasattr(args, 'func'): + args.func(args) +else: + parser.print_help() + exit(1) diff --git a/lib/python/apex/__init__.py b/lib/python/apex/__init__.py index 0c0ae6c6..88b066b2 100644 --- a/lib/python/apex/__init__.py +++ b/lib/python/apex/__init__.py @@ -7,3 +7,5 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## + +from .net_env import NetworkSettings diff --git a/lib/python/apex/ip_utils.py b/lib/python/apex/ip_utils.py index 680ce7e0..d7099db5 100644 --- a/lib/python/apex/ip_utils.py +++ b/lib/python/apex/ip_utils.py @@ -1,4 +1,3 @@ - ############################################################################## # Copyright (c) 2016 Feng Pan (fpan@redhat.com) and others. # @@ -10,6 +9,130 @@ import ipaddress +import subprocess +import re +import logging + + +def get_ip_range(start_offset=None, count=None, end_offset=None, + cidr=None, interface=None): + """ + Generate IP range for a network (cidr) or an interface. + + If CIDR is provided, it will take precedence over interface. In this case, + The entire CIDR IP address space is considered usable. start_offset will be + calculated from the network address, and end_offset will be calculated from + the last address in subnet. + + If interface is provided, the interface IP will be used to calculate + offsets: + - If the interface IP is in the first half of the address space, + start_offset will be calculated from the interface IP, and end_offset + will be calculated from end of address space. + - If the interface IP is in the second half of the address space, + start_offset will be calculated from the network address in the address + space, and end_offset will be calculated from the interface IP. + + 2 of start_offset, end_offset and count options must be provided: + - If start_offset and end_offset are provided, a range from start_offset + to end_offset will be returned. + - If count is provided, a range from either start_offset to (start_offset + +count) or (end_offset-count) to end_offset will be returned. The + IP range returned will be of size . + Both start_offset and end_offset must be greater than 0. + + Returns IP range in the format of "first_addr,second_addr" or exception + is raised. + """ + if cidr: + if count and start_offset and not end_offset: + start_index = start_offset + end_index = start_offset + count -1 + elif count and end_offset and not start_offset: + end_index = -1 - end_offset + start_index = -1 - end_index - count + 1 + elif start_offset and end_offset and not count: + start_index = start_offset + end_index = -1 - end_offset + else: + raise IPUtilsException("Argument error: must pass in exactly 2 of" + "start_offset, end_offset and count") + + start_ip = cidr[start_index] + end_ip = cidr[end_index] + network = cidr + elif interface: + network = interface.network + number_of_addr = network.num_addresses + if interface.ip < network[int(number_of_addr / 2)]: + if count and start_offset and not end_offset: + start_ip = interface.ip + start_offset + end_ip = start_ip + count - 1 + elif count and end_offset and not start_offset: + end_ip = network[-1 - end_offset] + start_ip = end_ip - count + 1 + elif start_offset and end_offset and not count: + start_ip = interface.ip + start_offset + end_ip = network[-1 - end_offset] + else: + raise IPUtilsException( + "Argument error: must pass in exactly 2 of" + "start_offset, end_offset and count") + else: + if count and start_offset and not end_offset: + start_ip = network[start_offset] + end_ip = start_ip + count -1 + elif count and end_offset and not start_offset: + end_ip = interface.ip - end_offset + start_ip = end_ip - count + 1 + elif start_offset and end_offset and not count: + start_ip = network[start_offset] + end_ip = interface.ip - end_offset + else: + raise IPUtilsException( + "Argument error: must pass in exactly 2 of" + "start_offset, end_offset and count") + + else: + raise IPUtilsException("Must pass in cidr or interface to generate" + "ip range") + + range_result = _validate_ip_range(start_ip, end_ip, network) + if range_result: + ip_range = "{},{}".format(start_ip, end_ip) + return ip_range + else: + raise IPUtilsException("Invalid IP range: {},{} for network {}" + .format(start_ip, end_ip, network)) + + +def get_ip(offset, cidr=None, interface=None): + """ + Returns an IP in a network given an offset. + + Either cidr or interface must be provided, cidr takes precedence. + + If cidr is provided, offset is calculated from network address. + If interface is provided, offset is calculated from interface IP. + + offset can be positive or negative, but the resulting IP address must also + be contained in the same subnet, otherwise an exception will be raised. + + returns a IP address object. + """ + if cidr: + ip = cidr[0 + offset] + network = cidr + elif interface: + ip = interface.ip + offset + network = interface.network + else: + raise IPUtilsException("Must pass in cidr or interface to generate IP") + + if ip not in network: + raise IPUtilsException("IP {} not in network {}".format(ip, network)) + else: + return str(ip) def generate_ip_range(args): @@ -22,7 +145,8 @@ def generate_ip_range(args): start_position: starting index, default to first address in subnet (1) end_position: ending index, default to last address in subnet (-1) - Returns IP range in string format. A single IP is returned if start and end IPs are identical. + Returns IP range in string format. A single IP is returned if start and + end IPs are identical. """ cidr = ipaddress.ip_network(args.CIDR) (start_index, end_index) = (args.start_position, args.end_position) @@ -32,23 +156,95 @@ def generate_ip_range(args): return ','.join(sorted([str(cidr[start_index]), str(cidr[end_index])])) -def main(): - import argparse - import sys +def get_interface(nic, address_family=4): + """ + Returns interface object for a given NIC name in the system + + Only global address will be returned at the moment. + + Returns interface object if an address is found for the given nic, + otherwise returns None. + """ + if not nic.strip(): + logging.error("empty nic name specified") + return None + output = subprocess.getoutput("ip -{} addr show {} scope global" + .format(address_family, nic)) + if address_family == 4: + pattern = re.compile("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}") + elif address_family == 6: + pattern = re.compile("([0-9a-f]{0,4}:){2,7}[0-9a-f]{0,4}/\d{1,3}") + else: + raise IPUtilsException("Invalid address family: {}" + .format(address_family)) + match = re.search(pattern, output) + if match: + logging.info("found interface {} ip: {}".format(nic, match.group())) + return ipaddress.ip_interface(match.group()) + else: + logging.info("interface ip not found! ip address output:\n{}" + .format(output)) + return None + + +def find_gateway(interface): + """ + Validate gateway on the system + + Ensures that the provided interface object is in fact configured as default + route on the system. + + Returns gateway IP (reachable from interface) if default route is found, + otherwise returns None. + """ + + address_family = interface.version + output = subprocess.getoutput("ip -{} route".format(address_family)) - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers() + pattern = re.compile("default\s+via\s+(\S+)\s+") + match = re.search(pattern, output) - parser_gen_ip_range = subparsers.add_parser('generate_ip_range', help='Generate IP Range given CIDR') - parser_gen_ip_range.add_argument('CIDR', help='Network in CIDR notation') - parser_gen_ip_range.add_argument('start_position', type=int, help='Starting index') - parser_gen_ip_range.add_argument('end_position', type=int, help='Ending index') - parser_gen_ip_range.set_defaults(func=generate_ip_range) + if match: + gateway_ip = match.group(1) + reverse_route_output = subprocess.getoutput("ip route get {}" + .format(gateway_ip)) + pattern = re.compile("{}.+src\s+{}".format(gateway_ip, interface.ip)) + if not re.search(pattern, reverse_route_output): + logging.warning("Default route doesn't match interface specified: " + "{}".format(reverse_route_output)) + return None + else: + return gateway_ip + else: + logging.warning("Can't find gateway address on system") + return None + + +def _validate_ip_range(start_ip, end_ip, cidr): + """ + Validates an IP range is in good order and the range is part of cidr. + + Returns True if validation succeeds, False otherwise. + """ + ip_range = "{},{}".format(start_ip, end_ip) + if end_ip <= start_ip: + logging.warning("IP range {} is invalid: end_ip should be greater than " + "starting ip".format(ip_range)) + return False + if start_ip not in ipaddress.ip_network(cidr): + logging.warning('start_ip {} is not in network {}' + .format(start_ip, cidr)) + return False + if end_ip not in ipaddress.ip_network(cidr): + logging.warning('end_ip {} is not in network {}'.format(end_ip, cidr)) + return False - args = parser.parse_args(sys.argv[1:]) - print(args.func(args)) + return True -if __name__ == '__main__': - main() +class IPUtilsException(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return self.value diff --git a/lib/python/apex/net_env.py b/lib/python/apex/net_env.py new file mode 100644 index 00000000..ec46fe28 --- /dev/null +++ b/lib/python/apex/net_env.py @@ -0,0 +1,242 @@ +############################################################################## +# Copyright (c) 2016 Feng Pan (fpan@redhat.com) and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + + +import yaml +import logging +import ipaddress +from . import ip_utils + + +ADMIN_NETWORK = 'admin_network' +PRIVATE_NETWORK = 'private_network' +PUBLIC_NETWORK = 'public_network' +STORAGE_NETWORK = 'storage_network' +API_NETWORK = 'api_network' +OPNFV_NETWORK_TYPES = [ADMIN_NETWORK, PRIVATE_NETWORK, PUBLIC_NETWORK, + STORAGE_NETWORK, API_NETWORK] + + +class NetworkSettings: + """ + This class parses APEX network settings yaml file into an object. It + generates or detects all missing fields for deployment. + + The resulting object will be used later to generate network environment file + as well as configuring post deployment networks. + + Currently the parsed object is dumped into a bash global definition file + for deploy.sh consumption. This object will later be used directly as + deployment script move to python. + """ + def __init__(self, filename, network_isolation): + with open(filename, 'r') as network_settings_file: + self.settings_obj = yaml.load(network_settings_file) + self.network_isolation = network_isolation + self.enabled_network_list = [] + self._validate_input() + + def _validate_input(self): + """ + Validates the network settings file and populates all fields. + + NetworkSettingsException will be raised if validation fails. + """ + if ADMIN_NETWORK not in self.settings_obj or \ + self.settings_obj[ADMIN_NETWORK].get('enabled') != True: + raise NetworkSettingsException("You must enable admin_network " + "and configure it explicitly or " + "use auto-detection") + if self.network_isolation and \ + (PUBLIC_NETWORK not in self.settings_obj or + self.settings_obj[PUBLIC_NETWORK].get('enabled') != True): + raise NetworkSettingsException("You must enable public_network " + "and configure it explicitly or " + "use auto-detection") + + for network in OPNFV_NETWORK_TYPES: + if network in self.settings_obj: + if self.settings_obj[network].get('enabled') == True: + logging.info("{} enabled".format(network)) + self._config_required_settings(network) + self._config_ip_range(network=network, + setting='usable_ip_range', + start_offset=21, end_offset=21) + self._config_optional_settings(network) + self.enabled_network_list.append(network) + else: + logging.info("{} disabled, will collapse with " + "admin_network".format(network)) + else: + logging.info("{} is not in specified, will collapse with " + "admin_network".format(network)) + + def _config_required_settings(self, network): + """ + Configures either CIDR or bridged_interface setting + + cidr takes precedence if both cidr and bridged_interface are specified + for a given network. + + When using bridged_interface, we will detect network setting on the + given NIC in the system. The resulting config in settings object will + be an ipaddress.network object, replacing the NIC name. + """ + cidr = self.settings_obj[network].get('cidr') + nic_name = self.settings_obj[network].get('bridged_interface') + + if cidr: + cidr = ipaddress.ip_network(self.settings_obj[network]['cidr']) + self.settings_obj[network]['cidr'] = cidr + logging.info("{}_cidr: {}".format(network, cidr)) + return 0 + elif nic_name: + # If cidr is not specified, we need to know if we should find + # IPv6 or IPv4 address on the interface + if self.settings_obj[network].get('ipv6') == True: + address_family = 6 + else: + address_family = 4 + nic_interface = ip_utils.get_interface(nic_name, address_family) + if nic_interface: + self.settings_obj[network]['bridged_interface'] = nic_interface + logging.info("{}_bridged_interface: {}". + format(network, nic_interface)) + return 0 + else: + raise NetworkSettingsException("Auto detection failed for {}: " + "Unable to find valid ip for " + "interface {}" + .format(network, nic_name)) + + else: + raise NetworkSettingsException("Auto detection failed for {}: " + "either bridge_interface or cidr " + "must be specified" + .format(network)) + + def _config_ip_range(self, network, setting, start_offset=None, + end_offset=None, count=None): + """ + Configures IP range for a given setting. + + If the setting is already specified, no change will be made. + + The spec for start_offset, end_offset and count are identical to + ip_utils.get_ip_range. + """ + ip_range = self.settings_obj[network].get(setting) + interface = self.settings_obj[network].get('bridged_interface') + + if not ip_range: + cidr = self.settings_obj[network].get('cidr') + ip_range = ip_utils.get_ip_range(start_offset=start_offset, + end_offset=end_offset, + count=count, + cidr=cidr, + interface=interface) + self.settings_obj[network][setting] = ip_range + + logging.info("{}_{}: {}".format(network, setting, ip_range)) + + def _config_ip(self, network, setting, offset): + """ + Configures IP for a given setting. + + If the setting is already specified, no change will be made. + + The spec for offset is identical to ip_utils.get_ip + """ + ip = self.settings_obj[network].get(setting) + interface = self.settings_obj[network].get('bridged_interface') + + if not ip: + cidr = self.settings_obj[network].get('cidr') + ip = ip_utils.get_ip(offset, cidr, interface) + self.settings_obj[network][setting] = ip + + logging.info("{}_{}: {}".format(network, setting, ip)) + + def _config_optional_settings(self, network): + """ + Configures optional settings: + - admin_network: + - provisioner_ip + - dhcp_range + - introspection_range + - public_network: + - provisioner_ip + - floating_ip + - gateway + """ + if network == ADMIN_NETWORK: + self._config_ip(network, 'provisioner_ip', 1) + self._config_ip_range(network=network, setting='dhcp_range', + start_offset=2, count=9) + self._config_ip_range(network=network, + setting='introspection_range', + start_offset=11, count=9) + elif network == PUBLIC_NETWORK: + self._config_ip(network, 'provisioner_ip', 1) + self._config_ip_range(network=network, + setting='floating_ip', + end_offset=2, count=20) + self._config_gateway(network) + + def _config_gateway(self, network): + """ + Configures gateway setting for a given network. + + If cidr is specified, we always use the first address in the address + space for gateway. Otherwise, we detect the system gateway. + """ + gateway = self.settings_obj[network].get('gateway') + interface = self.settings_obj[network].get('bridged_interface') + + if not gateway: + cidr = self.settings_obj[network].get('cidr') + if cidr: + gateway = ip_utils.get_ip(1, cidr) + else: + gateway = ip_utils.find_gateway(interface) + + if gateway: + self.settings_obj[network]['gateway'] = gateway + else: + raise NetworkSettingsException("Failed to set gateway") + + logging.info("{}_gateway: {}".format(network, gateway)) + + + def dump_bash(self, path=None): + """ + Prints settings for bash consumption. + + If optional path is provided, bash string will be written to the file + instead of stdout. + """ + bash_str = '' + for network in self.enabled_network_list: + for key, value in self.settings_obj[network].items(): + bash_str += "{}_{}={}\n".format(network, key, value) + bash_str += "enabled_network_list='{}'\n" \ + .format(' '.join(self.enabled_network_list)) + if path: + with open(path, 'w') as file: + file.write(bash_str) + else: + print(bash_str) + + +class NetworkSettingsException(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value -- cgit 1.2.3-korg