Skip to content

Introduce prefix allocation #2611

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ def create_vm_response(instance, vm)
'id' => instance.uuid,
'az' => instance.availability_zone,
'ips' => vm&.ips || [],
'ips_cidr' => vm&.ips_cidr || [],
'vm_created_at' => vm&.created_at&.utc&.iso8601,
}
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ def create_instance_plan(stemcell)

compilation_network = @deployment_plan.network(@deployment_plan.compilation.network_name)
reservation = DesiredNetworkReservation.new_dynamic(instance.model, compilation_network)
@logger.debug("Creating new dynamic reservation #{reservation.inspect} for instance '#{instance}' and compile instance group '#{compile_instance_group}'")
desired_instance = DeploymentPlan::DesiredInstance.new(compile_instance_group)
instance_plan = DeploymentPlan::InstancePlan.new(
existing_instance: instance.model,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,36 @@ def self.parse(network_spec, availability_zones, logger)
if network_spec.has_key?('subnets')
validate_network_has_no_key_while_subnets_present('dns', name, network_spec)
validate_network_has_no_key_while_subnets_present('cloud_properties', name, network_spec)
validate_network_has_no_key_while_subnets_present('prefix', name, network_spec)

subnets = network_spec['subnets'].map do |subnet_properties|
name_servers = name_server_parser.parse(subnet_properties['name'], subnet_properties)
cloud_properties = safe_property(subnet_properties, 'cloud_properties', class: Hash, default: {})
prefix = safe_property(subnet_properties, 'prefix', class: Hash, default: {})
if prefix.empty? || prefix.nil?
prefix = 32
end
subnet_availability_zones = parse_availability_zones(subnet_properties, availability_zones, name)
DynamicNetworkSubnet.new(name_servers, cloud_properties, subnet_availability_zones)
DynamicNetworkSubnet.new(name_servers, cloud_properties, subnet_availability_zones, prefix)
end
else
cloud_properties = safe_property(network_spec, 'cloud_properties', class: Hash, default: {})
prefix = safe_property(network_spec, 'prefix', class: Hash, default: {})
if prefix.empty? || prefix.nil?
prefix = 32
end
name_servers = name_server_parser.parse(network_spec['name'], network_spec)
subnets = [DynamicNetworkSubnet.new(name_servers, cloud_properties, nil)]
subnets = [DynamicNetworkSubnet.new(name_servers, cloud_properties, nil, prefix)]
end

new(name, subnets, logger)
unless subnets.empty?
prefix = subnets.first.prefix
else
prefix = 32
end


new(name, subnets, logger, prefix)
end

def self.validate_network_has_no_key_while_subnets_present(key, name, network_spec)
Expand Down Expand Up @@ -77,9 +93,10 @@ def self.check_validity_of_availability_zone(availability_zone, availability_zon
end
end

def initialize(name, subnets, logger)
def initialize(name, subnets, prefix, logger)
super(name, logger)
@subnets = subnets
@prefix = prefix
end

attr_reader :subnets
Expand All @@ -94,7 +111,7 @@ def initialize(name, subnets, logger)
def network_settings(reservation, default_properties = Network::REQUIRED_DEFAULTS, availability_zone = nil)
unless reservation.dynamic?
raise NetworkReservationWrongType,
"IP '#{format_ip(reservation.ip)}' on network '#{reservation.network.name}' does not belong to dynamic pool"
"IP '#{reservation.ip}' on network '#{reservation.network.name}' does not belong to dynamic pool"
end

if availability_zone.nil?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
module Bosh::Director
module DeploymentPlan
class DynamicNetworkSubnet
def initialize(dns, cloud_properties, availability_zone_names)
def initialize(dns, cloud_properties, availability_zone_names, prefix)
@dns = dns
@cloud_properties = cloud_properties
@availability_zone_names = availability_zone_names.nil? ? nil : availability_zone_names
@prefix = prefix
end

attr_reader :dns, :cloud_properties, :availability_zone_names
attr_reader :dns, :cloud_properties, :availability_zone_names, :prefix
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def self.create_from_db(instance_model, deployment, logger)
# Dynamic network reservations are not saved in DB, recreating from instance spec
instance_model.spec.fetch('networks', []).each do |network_name, network_config|
next unless network_config['type'] == 'dynamic'
reservations.add_existing(instance_model, deployment, network_name, network_config['ip'], network_config['type'])
reservations.add_existing(instance_model, deployment, network_name, Bosh::Director::IpAddrOrCidr.new("#{network_config['ip']}/32"), network_config['type'])
end
end

Expand Down Expand Up @@ -75,17 +75,17 @@ def find_network(deployment, cidr_ip, network_name, instance_model)
ip_in_subnet = network.subnets.find { |snet| snet.is_reservable?(cidr_ip) }
next unless ip_in_subnet

@logger.debug("Registering existing reservation with IP '#{format_ip(cidr_ip)}' for instance '#{instance_model}'"\
@logger.debug("Registering existing reservation with IP '#{format_cidr_ip(cidr_ip)}' for instance '#{instance_model}'"\
"on network '#{network.name}'")
return network
end
elsif network_match_on_name # dynamic and static vip
@logger.debug("Registering existing reservation with IP '#{format_ip(cidr_ip)}' for instance '#{instance_model}'"\
@logger.debug("Registering existing reservation with IP '#{format_cidr_ip(cidr_ip)}' for instance '#{instance_model}'"\
"on network '#{network_name}'")
return network_match_on_name
end

@logger.debug("Failed to find network #{network_name} or a network with valid subnets for #{format_ip(cidr_ip)},"\
@logger.debug("Failed to find network #{network_name} or a network with valid subnets for #{format_cidr_ip(cidr_ip)},"\
'reservation will be marked as obsolete')
Network.new(network_name, nil)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def reserve_manual(reservation)

filter_subnet_by_instance_az(reservation).each do |subnet|
if (ip = @ip_repo.allocate_dynamic_ip(reservation, subnet))
@logger.debug("Reserving dynamic IP '#{ip}' for manual network '#{reservation.network.name}'")
@logger.debug("Reserving dynamic IP '#{ip.to_cidr_s}' for manual network '#{reservation.network.name}'")
reservation.resolve_ip(ip)
reservation.resolve_type(:dynamic)
break
Expand All @@ -81,7 +81,7 @@ def reserve_manual(reservation)
@logger.debug("Reserving #{reservation.desc} for manual network '#{reservation.network.name}'")

if (subnet = reservation.network.find_subnet_containing(reservation.ip))
if subnet.restricted_ips.include?(reservation.ip)
if subnet.restricted_ips.include?(reservation.ip.to_i)
message = "Failed to reserve IP '#{format_ip(reservation.ip)}' for network '#{subnet.network_name}': IP belongs to reserved range"
@logger.error(message)
raise Bosh::Director::NetworkReservationIpReserved, message
Expand All @@ -99,7 +99,8 @@ def reserve_manual_with_subnet(reservation, subnet)
@ip_repo.add(reservation)

subnet_az_names = subnet.availability_zone_names.to_a.join(', ')
if subnet.static_ips.include?(reservation.ip)

if subnet.static_ips.include?(reservation.ip.to_i)
reservation.resolve_type(:static)
@logger.debug("Found subnet with azs '#{subnet_az_names}' for #{format_ip(reservation.ip)}. Reserved as static network reservation.")
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class IpRepo
include Bosh::Director::IpUtil
class IpFoundInDatabaseAndCanBeRetried < StandardError; end
class NoMoreIPsAvailableAndStopRetrying < StandardError; end
class PrefixOutOfRange < StandardError; end

def initialize(logger)
@logger = Bosh::Director::TaggedLogger.new(logger, 'network-configuration')
Expand All @@ -11,7 +12,7 @@ def initialize(logger)
def delete(ip)
ip_or_cidr = Bosh::Director::IpAddrOrCidr.new(ip)

ip_address = Bosh::Director::Models::IpAddress.first(address_str: ip_or_cidr.to_i.to_s)
ip_address = Bosh::Director::Models::IpAddress.first(address_str: ip_or_cidr.to_cidr_s)

if ip_address
@logger.debug("Releasing ip '#{ip_or_cidr}'")
Expand All @@ -22,7 +23,7 @@ def delete(ip)
end

def add(reservation)
ip_or_cidr = Bosh::Director::IpAddrOrCidr.new(reservation.ip)
ip_or_cidr = reservation.ip

reservation_type = reservation.network.ip_type(ip_or_cidr)

Expand All @@ -34,6 +35,7 @@ def add(reservation)
)

reservation.resolve_type(reservation_type)

@logger.debug("Reserved ip '#{ip_or_cidr}' for #{reservation.network.name} as #{reservation_type}")
end

Expand All @@ -50,8 +52,8 @@ def allocate_dynamic_ip(reservation, subnet)
retry
end

@logger.debug("Allocated dynamic IP '#{ip_address}' for #{reservation.network.name}")
ip_address.to_i
@logger.debug("Allocated dynamic IP '#{ip_address.to_cidr_s}' for #{reservation.network.name}")
ip_address
end

def allocate_vip_ip(reservation, subnet)
Expand All @@ -74,59 +76,91 @@ def allocate_vip_ip(reservation, subnet)

private

def all_ip_addresses
Bosh::Director::Models::IpAddress.select(:address_str).all.map { |a| a.address }
end

def try_to_allocate_dynamic_ip(reservation, subnet)
addresses_in_use = Set.new(all_ip_addresses)

first_range_address = subnet.range.to_range.first.to_i - 1
first_range_address = Bosh::Director::IpAddrOrCidr.new(subnet.range.to_range.first.to_i - 1)

addresses_we_cant_allocate = addresses_in_use
addresses_we_cant_allocate << first_range_address

addresses_we_cant_allocate.merge(subnet.restricted_ips.to_a) unless subnet.restricted_ips.empty?
addresses_we_cant_allocate.merge(subnet.static_ips.to_a) unless subnet.static_ips.empty?
addr = find_first_available_address(addresses_we_cant_allocate, first_range_address)
ip_address = Bosh::Director::IpAddrOrCidr.new(addr)
addresses_we_cant_allocate.merge(subnet.restricted_ips.map { |int_ip| Bosh::Director::IpAddrOrCidr.new(int_ip)}) unless subnet.restricted_ips.empty?
addresses_we_cant_allocate.merge(subnet.static_ips.map { |int_ip| Bosh::Director::IpAddrOrCidr.new(int_ip)}) unless subnet.static_ips.empty?

unless subnet.range == ip_address || subnet.range.include?(ip_address)
raise NoMoreIPsAvailableAndStopRetrying
if subnet.range.ipv6?
addresses_we_cant_allocate.delete_if { |ipaddr| ipaddr.ipv4? }
else
addresses_we_cant_allocate.delete_if { |ipaddr| ipaddr.ipv6? }
end

save_ip(ip_address, reservation, false)
prefix = subnet.prefix

ip_address
ip_address_cidr = find_next_available_ip(addresses_we_cant_allocate, first_range_address, prefix)

if !(subnet.range == ip_address_cidr || subnet.range.include?(ip_address_cidr)) ||
subnet.range.to_range.last.to_i < (ip_address_cidr.to_i + ip_address_cidr.count)
raise NoMoreIPsAvailableAndStopRetrying
end

save_ip(ip_address_cidr.to_cidr_s, reservation, false)

ip_address_cidr
end

def find_first_available_address(addresses_we_cant_allocate, first_address)
last_address_we_cant_use = addresses_we_cant_allocate
.to_a
.reject { |a| a < first_address }
.sort
.find { |a| !addresses_we_cant_allocate.include?(a + 1) }
last_address_we_cant_use + 1
def find_next_available_ip(ip_entries, first_range_address, prefix)
filtered_ips = ip_entries.sort_by { |ip| ip.to_i }.reject { |ip| ip.to_i < first_range_address.to_i } #remove ips that are below subnet range

current_ip = Bosh::Director::IpAddrOrCidr.new(first_range_address.to_i + 1)
found = false

while found == false
current_prefix = Bosh::Director::IpAddrOrCidr.new("#{current_ip}/#{prefix}")

if filtered_ips.any? { |ip| current_prefix.include?(ip) }
filtered_ips.reject! { |ip| ip.to_i < current_prefix.to_i }
actual_ip_prefix = filtered_ips.first.count
if actual_ip_prefix > current_prefix.count
current_ip = Bosh::Director::IpAddrOrCidr.new(current_ip.to_i + actual_ip_prefix)
else
current_ip = Bosh::Director::IpAddrOrCidr.new(current_ip.to_i + current_prefix.count)
end
else
found_cidr = current_prefix
found = true
end
end

found_cidr
end

def try_to_allocate_vip_ip(reservation, subnet)
addresses_in_use = Set.new(all_ip_addresses)
addresses_in_use = Set.new(all_ip_addresses.map { |ip| ip.to_i })

if Bosh::Director::IpAddrOrCidr.new(subnet.static_ips.first.to_i).ipv6?
prefix = 128
else
prefix = 32
end

available_ips = subnet.static_ips - addresses_in_use

raise NoMoreIPsAvailableAndStopRetrying if available_ips.empty?

ip_address = Bosh::Director::IpAddrOrCidr.new(available_ips.first)
ip_address = Bosh::Director::IpAddrOrCidr.new("#{Bosh::Director::IpAddrOrCidr.new(available_ips.first)}/#{prefix}")

save_ip(ip_address, reservation, false)
save_ip(ip_address.to_cidr_s, reservation, false)

ip_address
end

def all_ip_addresses
Bosh::Director::Models::IpAddress.select(:address_str).all.map { |a| a.address_str.to_i }
end

def reserve_with_instance_validation(instance_model, ip, reservation, is_static)
# try to save IP first before validating its instance to prevent race conditions
save_ip(ip, reservation, is_static)
save_ip(ip.to_cidr_s, reservation, is_static)
rescue IpFoundInDatabaseAndCanBeRetried
ip_address = Bosh::Director::Models::IpAddress.first(address_str: ip.to_i.to_s)
ip_address = Bosh::Director::Models::IpAddress.first(address_str: ip.to_cidr_s)

retry unless ip_address

Expand Down Expand Up @@ -156,17 +190,19 @@ def validate_instance_and_update_reservation_type(instance_model, ip, ip_address
end

def save_ip(ip, reservation, is_static)
@logger.debug("Adding IP Address: #{ip} from reservation: #{reservation}")
ip_address = Bosh::Director::Models::IpAddress.new(
address_str: ip.to_i.to_s,
address_str: ip,
network_name: reservation.network.name,
task_id: Bosh::Director::Config.current_job.task_id,
static: is_static,
)
)
reservation.instance_model.add_ip_address(ip_address)
rescue Sequel::ValidationFailed, Sequel::DatabaseError => e
error_message = e.message.downcase
@logger.debug("ERROR!!! #{error_message}")
if error_message.include?('unique') || error_message.include?('duplicate')
raise IpFoundInDatabaseAndCanBeRetried
raise IpFoundInDatabaseAndCanBeRetried, e.inspect
else
raise e
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,31 @@ def self.parse(network_spec, availability_zones, logger)
managed = Config.network_lifecycle_enabled? && safe_property(network_spec, 'managed', default: false)
subnet_specs = safe_property(network_spec, 'subnets', class: Array)
subnets = []
prefix = nil
subnet_specs.each do |subnet_spec|
new_subnet = ManualNetworkSubnet.parse(name, subnet_spec, availability_zones, managed)
subnets.each do |subnet|
raise NetworkOverlappingSubnets, "Network '#{name}' has overlapping subnets" if subnet.overlaps?(new_subnet)
if prefix.nil?
prefix = subnet.prefix
elsif prefix != subnet.prefix
raise NetworkPrefixSizesDiffer, "Network '#{name}' has subnets that define different prefixes"
end
end
subnets << new_subnet
end
validate_all_subnets_use_azs(subnets, name)
new(name, subnets, logger, managed)
new(name, subnets, prefix, logger, managed)
end

def managed?
@managed
end

def initialize(name, subnets, logger, managed = false)
def initialize(name, subnets, prefix, logger, managed = false)
super(name, TaggedLogger.new(logger, 'network-configuration'))
@subnets = subnets
@prefix = prefix
@managed = managed
end

Expand All @@ -46,15 +53,21 @@ def network_settings(reservation, default_properties = REQUIRED_DEFAULTS, availa
"Can't generate network settings without an IP"
end

ip_or_cidr = Bosh::Director::IpAddrOrCidr.new(reservation.ip)
ip_or_cidr = reservation.ip
subnet = find_subnet_containing(reservation.ip)

unless subnet
raise NetworkReservationInvalidIp, "Provided IP '#{ip_or_cidr}' does not belong to any subnet"
end

unless subnet.prefix.to_i == ip_or_cidr.prefix.to_i
raise NetworkReservationInvalidPrefix, "Subnet Prefix #{subnet.prefix} and ip reservation prefix #{ip_or_cidr.prefix} do not match"
end

config = {
"type" => "manual",
"ip" => ip_or_cidr.to_s,
"prefix" => ip_or_cidr.prefix.to_s,
"netmask" => subnet.netmask,
"cloud_properties" => subnet.cloud_properties
}
Expand Down
Loading