diff --git a/src/bosh-director/db/migrations/20250618102610_migrate_ip_address_representation_from_integer_to_cidr_notation.rb b/src/bosh-director/db/migrations/20250618102610_migrate_ip_address_representation_from_integer_to_cidr_notation.rb new file mode 100644 index 00000000000..2425ec83c0e --- /dev/null +++ b/src/bosh-director/db/migrations/20250618102610_migrate_ip_address_representation_from_integer_to_cidr_notation.rb @@ -0,0 +1,27 @@ +Sequel.migration do + up do + alter_table(:ip_addresses) do + add_column :nic_group, Integer, null: true + end + + from(:ip_addresses).each do |row| + integer_representation = row[:address_str].to_i + + cidr_notation = Bosh::Director::IpAddrOrCidr.new(integer_representation).to_s + + from(:ip_addresses).where(id: row[:id]).update(address_str: cidr_notation) + end + end + down do + alter_table(:ip_addresses) do + drop_column :nic_group + end + from(:ip_addresses).each do |row| + cidr_notation = row[:address_str] + + ip_addr = Bosh::Director::IpAddrOrCidr.new(cidr_notation) + integer_representation = ip_addr.to_i + from(:ip_addresses).where(id: row[:id]).update(address_str: integer_representation) + end + end +end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/compilation_instance_pool.rb b/src/bosh-director/lib/bosh/director/deployment_plan/compilation_instance_pool.rb index 333218c53f4..f03b4d458e4 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/compilation_instance_pool.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/compilation_instance_pool.rb @@ -188,7 +188,8 @@ def create_instance_plan(stemcell) end compilation_network = @deployment_plan.network(@deployment_plan.compilation.network_name) - reservation = DesiredNetworkReservation.new_dynamic(instance.model, compilation_network) + reservation = DesiredNetworkReservation.new_dynamic(instance.model, compilation_network, nil) + @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, diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network.rb b/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network.rb index e80045480b1..2fb341afff7 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network.rb @@ -13,6 +13,7 @@ def self.parse(network_spec, availability_zones, logger) validate_network_has_no_key('az', name, network_spec) validate_network_has_no_key('azs', name, network_spec) validate_network_has_no_key('managed', name, network_spec) + validate_network_has_no_key('prefix', name, network_spec) if network_spec.has_key?('subnets') validate_network_has_no_key_while_subnets_present('dns', name, network_spec) @@ -21,16 +22,21 @@ def self.parse(network_spec, availability_zones, logger) 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: Integer, default: Network::IPV4_DEFAULT_PREFIX_SIZE) + raise NetworkInvalidProperty, "Prefix property is not supported for dynamic networks." unless prefix == Network::IPV4_DEFAULT_PREFIX_SIZE 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: {}) + # We need to set the IPv4 default value (dynamic networks only support IPv4) + prefix = Network::IPV4_DEFAULT_PREFIX_SIZE + 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) + new(name, subnets, prefix, logger) end def self.validate_network_has_no_key_while_subnets_present(key, name, network_spec) @@ -77,9 +83,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 @@ -94,7 +101,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? diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network_subnet.rb b/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network_subnet.rb index bcd6bb04f34..1770bdf2b22 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network_subnet.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/dynamic_network_subnet.rb @@ -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.to_s end - attr_reader :dns, :cloud_properties, :availability_zone_names + attr_reader :dns, :cloud_properties, :availability_zone_names, :prefix end end end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_networks_parser.rb b/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_networks_parser.rb index 37652b33a20..492787f81b8 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_networks_parser.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_networks_parser.rb @@ -33,12 +33,13 @@ def parse_networks(instance_group_spec, instance_group_name, manifest_networks) network_specs.map do |network_spec| network_name = safe_property(network_spec, 'name', class: String) default_for = safe_property(network_spec, 'default', class: Array, default: []) + nic_group = safe_property(network_spec, 'nic_group', class: String, default: nil) static_ips = parse_static_ips(network_spec['static_ips'], instance_group_name) deployment_network = look_up_deployment_network(manifest_networks, instance_group_name, network_name) deployment_network.validate_reference_from_job!(network_spec, instance_group_name) - JobNetwork.new(network_name, static_ips, default_for, deployment_network) + JobNetwork.new(network_name, static_ips, default_for, deployment_network, nic_group) end end @@ -56,9 +57,9 @@ def parse_static_ips(static_ips_raw, instance_group_name) if static_ips_raw static_ips = [] each_ip(static_ips_raw) do |ip| - if static_ips.include?(ip) + if ip_in_array?(ip, static_ips) raise JobInvalidStaticIPs, - "Instance group '#{instance_group_name}' specifies static IP '#{format_ip(ip)}' more than once" + "Instance group '#{instance_group_name}' specifies static IP '#{ip}' more than once" end static_ips.push(ip) diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_spec_parser.rb b/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_spec_parser.rb index e0e74b44319..069851616ae 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_spec_parser.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/instance_group_spec_parser.rb @@ -5,7 +5,6 @@ module DeploymentPlan class InstanceGroupSpecParser include ValidationHelper include Bosh::Common::Template::PropertyHelper - include IpUtil MANUAL_LINK_KEYS = %w[instances properties address].freeze diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/instance_network_reservations.rb b/src/bosh-director/lib/bosh/director/deployment_plan/instance_network_reservations.rb index de51af3e03b..873e1eef239 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/instance_network_reservations.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/instance_network_reservations.rb @@ -2,7 +2,7 @@ module Bosh::Director module DeploymentPlan class InstanceNetworkReservations include Enumerable - include IpUtil + extend IpUtil def self.create_from_db(instance_model, deployment, logger) reservations = new(logger) @@ -17,14 +17,15 @@ def self.create_from_db(instance_model, deployment, logger) deployment, ip_address.network_name, ip_address.address, - 'not-dynamic') + 'not-dynamic', + ip_address.nic_group) end unless instance_model.spec.nil? # 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, to_ipaddr(network_config['ip']), network_config['type'], network_config['nic_group']) end end @@ -54,9 +55,9 @@ def delete(reservation) @reservations.delete(reservation) end - def add_existing(instance_model, deployment, network_name, ip, existing_network_type) + def add_existing(instance_model, deployment, network_name, ip, existing_network_type, nic_group) network = find_network(deployment, ip, network_name, instance_model) - reservation = ExistingNetworkReservation.new(instance_model, network, ip, existing_network_type) + reservation = ExistingNetworkReservation.new(instance_model, network, ip, existing_network_type, nic_group) deployment.ip_provider.reserve_existing_ips(reservation) @reservations << reservation end @@ -75,17 +76,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 '#{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 '#{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 #{cidr_ip},"\ 'reservation will be marked as obsolete') Network.new(network_name, nil) end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/instance_plan.rb b/src/bosh-director/lib/bosh/director/deployment_plan/instance_plan.rb index b2bdcb9c9cd..481d49f1bd5 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/instance_plan.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/instance_plan.rb @@ -219,7 +219,7 @@ def configuration_changed? end def remove_obsolete_network_plans_for_ips(ips) - network_plans.delete_if { |plan| ips.include?(plan.reservation.ip.to_s) } + network_plans.delete_if { |plan| ips.include?(plan.reservation.ip.base_addr) } end def release_obsolete_network_plans(ip_provider) diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_provider.rb b/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_provider.rb index b4de47f1aad..366709f698b 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_provider.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_provider.rb @@ -81,8 +81,8 @@ 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) - message = "Failed to reserve IP '#{format_ip(reservation.ip)}' for network '#{subnet.network_name}': IP belongs to reserved range" + if ip_in_array?(reservation.ip, subnet.restricted_ips) + message = "Failed to reserve IP '#{reservation.ip}' for network '#{subnet.network_name}': IP belongs to reserved range" @logger.error(message) raise Bosh::Director::NetworkReservationIpReserved, message end @@ -90,7 +90,7 @@ def reserve_manual(reservation) reserve_manual_with_subnet(reservation, subnet) else raise NetworkReservationIpOutsideSubnet, - "Provided static IP '#{format_ip(reservation.ip)}' does not belong to any subnet in network '#{reservation.network.name}'" + "Provided static IP '#{reservation.ip}' does not belong to any subnet in network '#{reservation.network.name}'" end end end @@ -99,12 +99,13 @@ 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 ip_in_array?(reservation.ip, subnet.static_ips) reservation.resolve_type(:static) - @logger.debug("Found subnet with azs '#{subnet_az_names}' for #{format_ip(reservation.ip)}. Reserved as static network reservation.") + @logger.debug("Found subnet with azs '#{subnet_az_names}' for #{reservation.ip}. Reserved as static network reservation.") else reservation.resolve_type(:dynamic) - @logger.debug("Found subnet with azs '#{subnet_az_names}' for #{format_ip(reservation.ip)}. Reserved as dynamic network reservation.") + @logger.debug("Found subnet with azs '#{subnet_az_names}' for #{reservation.ip}. Reserved as dynamic network reservation.") end end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_repo.rb b/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_repo.rb index 8d37093a18a..a9e1f840951 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_repo.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/ip_provider/ip_repo.rb @@ -3,15 +3,16 @@ 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') end def delete(ip) - ip_or_cidr = Bosh::Director::IpAddrOrCidr.new(ip) + ip_or_cidr = to_ipaddr(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_s) if ip_address @logger.debug("Releasing ip '#{ip_or_cidr}'") @@ -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) @@ -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 @@ -51,7 +53,7 @@ def allocate_dynamic_ip(reservation, subnet) end @logger.debug("Allocated dynamic IP '#{ip_address}' for #{reservation.network.name}") - ip_address.to_i + ip_address end def allocate_vip_ip(reservation, subnet) @@ -69,64 +71,94 @@ def allocate_vip_ip(reservation, subnet) end @logger.debug("Allocated vip IP '#{ip_address}' for #{reservation.network.name}") - ip_address.to_i + ip_address end 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 = to_ipaddr(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) unless subnet.restricted_ips.empty? + addresses_we_cant_allocate.merge(subnet.static_ips) 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) + ip_address_cidr = find_next_available_ip(addresses_we_cant_allocate, first_range_address, subnet.prefix) - ip_address + if !(subnet.range == ip_address_cidr || subnet.range.include?(ip_address_cidr)) + raise NoMoreIPsAvailableAndStopRetrying + end + + save_ip(ip_address_cidr, 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(addresses_we_cant_allocate, first_range_address, prefix) + # Remove IPs that are below subnet range + filtered_ips = addresses_we_cant_allocate.sort_by { |ip| ip.to_i }.reject { |ip| ip.to_i < first_range_address.to_i } + + current_ip = to_ipaddr(first_range_address.to_i + 1) + found = false + + while found == false + current_prefix = to_ipaddr("#{current_ip.base_addr}/#{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 = to_ipaddr(current_ip.to_i + actual_ip_prefix) + else + current_ip = to_ipaddr(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 to_ipaddr(subnet.static_ips.first.to_i).ipv6? + prefix = Bosh::Director::DeploymentPlan::Network::IPV6_DEFAULT_PREFIX_SIZE + else + prefix = Bosh::Director::DeploymentPlan::Network::IPV4_DEFAULT_PREFIX_SIZE + end - available_ips = subnet.static_ips - addresses_in_use + available_ips = subnet.static_ips.map(&:to_i).to_set - addresses_in_use raise NoMoreIPsAvailableAndStopRetrying if available_ips.empty? - ip_address = Bosh::Director::IpAddrOrCidr.new(available_ips.first) + ip_address = to_ipaddr("#{to_ipaddr(available_ips.first).base_addr}/#{prefix}") save_ip(ip_address, 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) 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_s) retry unless ip_address @@ -156,17 +188,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.to_s, network_name: reservation.network.name, task_id: Bosh::Director::Config.current_job.task_id, static: is_static, - ) + nic_group: reservation.nic_group, + ) reservation.instance_model.add_ip_address(ip_address) rescue Sequel::ValidationFailed, Sequel::DatabaseError => e error_message = e.message.downcase if error_message.include?('unique') || error_message.include?('duplicate') - raise IpFoundInDatabaseAndCanBeRetried + raise IpFoundInDatabaseAndCanBeRetried, e.inspect else raise e end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/job_network.rb b/src/bosh-director/lib/bosh/director/deployment_plan/job_network.rb index 534f05e876c..e18261f1f8b 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/job_network.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/job_network.rb @@ -1,13 +1,14 @@ module Bosh::Director module DeploymentPlan class JobNetwork - attr_reader :name, :static_ips, :deployment_network + attr_reader :name, :static_ips, :deployment_network, :nic_group - def initialize(name, static_ips, default_for, deployment_network) + def initialize(name, static_ips, default_for, deployment_network, nic_group) @name = name @static_ips = static_ips @default_for = default_for @deployment_network = deployment_network + @nic_group = nic_group&.to_i end def availability_zones diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/manual_network.rb b/src/bosh-director/lib/bosh/director/deployment_plan/manual_network.rb index 26c76cd5964..93887b1f7ab 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/manual_network.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/manual_network.rb @@ -13,24 +13,32 @@ 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 != subnet.prefix + raise NetworkPrefixSizesDiffer, "Network '#{name}' has subnets that define different prefixes" + end + end + if prefix.nil? + prefix = new_subnet.prefix 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 @@ -46,15 +54,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, + "ip" => ip_or_cidr.base_addr, + "prefix" => ip_or_cidr.prefix.to_s, "netmask" => subnet.netmask, "cloud_properties" => subnet.cloud_properties } @@ -63,14 +77,19 @@ def network_settings(reservation, default_properties = REQUIRED_DEFAULTS, availa config["default"] = default_properties.sort end + nic_group = reservation.nic_group + if nic_group + config["nic_group"] = nic_group.to_s + end + config["dns"] = subnet.dns if subnet.dns - config["gateway"] = subnet.gateway.to_s if subnet.gateway + config["gateway"] = subnet.gateway.base_addr if subnet.gateway config end def ip_type(cidr_ip) static_ips = @subnets.map { |subnet| subnet.static_ips.to_a }.flatten - static_ips.include?(cidr_ip.to_i) ? :static : :dynamic + ip_in_array?(cidr_ip, static_ips) ? :static : :dynamic end def find_az_names_for_ip(ip) diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/manual_network_subnet.rb b/src/bosh-director/lib/bosh/director/deployment_plan/manual_network_subnet.rb index 7d282155b07..60d9d079f5a 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/manual_network_subnet.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/manual_network_subnet.rb @@ -5,7 +5,7 @@ class ManualNetworkSubnet < Subnet extend IpUtil attr_reader :network_name, :name, :dns, - :availability_zone_names, :netmask_bits + :availability_zone_names, :netmask_bits, :prefix attr_accessor :cloud_properties, :range, :gateway, :restricted_ips, :static_ips, :netmask @@ -17,15 +17,17 @@ def self.parse(network_name, subnet_spec, availability_zones, managed = false) ignore_missing_gateway = Bosh::Director::Config.ignore_missing_gateway gateway_property = safe_property(subnet_spec, 'gateway', class: String, optional: ignore_missing_gateway || managed) reserved_property = safe_property(subnet_spec, 'reserved', optional: true) + prefix = safe_property(subnet_spec, 'prefix', optional: true) restricted_ips = Set.new static_ips = Set.new + static_cidrs = Set.new if managed && !range_property range_property, gateway_property, reserved_property = parse_properties_from_database(network_name, sn_name) end if range_property - range = Bosh::Director::IpAddrOrCidr.new(range_property) + range = to_ipaddr(range_property) if range.count <= 1 raise NetworkInvalidRange, "Invalid network range '#{range_property}', " \ @@ -33,10 +35,10 @@ def self.parse(network_name, subnet_spec, availability_zones, managed = false) end netmask = range.netmask - broadcast = range.to_range.last + broadcast = range.last if gateway_property - gateway = Bosh::Director::IpAddrOrCidr.new(gateway_property) + gateway = to_ipaddr(gateway_property) invalid_gateway(network_name, 'must be a single IP') unless gateway.count == 1 invalid_gateway(network_name, 'must be inside the range') unless range.include?(gateway) invalid_gateway(network_name, "can't be the network id") if gateway == range @@ -45,13 +47,13 @@ def self.parse(network_name, subnet_spec, availability_zones, managed = false) static_property = safe_property(subnet_spec, 'static', optional: true) - restricted_ips.add(gateway.to_i) if gateway - restricted_ips.add(range.to_i) - restricted_ips.add(broadcast.to_i) + restricted_ips.add(gateway) if gateway + restricted_ips.add(range.first) + restricted_ips.add(broadcast) - each_ip(reserved_property) do |ip| + each_ip(reserved_property, false) do |ip| unless range.include?(ip) - raise NetworkReservedIpOutOfRange, "Reserved IP '#{format_ip(ip)}' is out of " \ + raise NetworkReservedIpOutOfRange, "Reserved IP '#{ip}' is out of " \ "network '#{network_name}' range" end @@ -64,16 +66,44 @@ def self.parse(network_name, subnet_spec, availability_zones, managed = false) end end - each_ip(static_property) do |ip| - if restricted_ips.include?(ip) - raise NetworkStaticIpOutOfRange, "Static IP '#{to_ipaddr(ip)}' is in network '#{network_name}' reserved range" + restricted_ips.reject! do |ip| + restricted_ips.any? do |other_ip| + includes = other_ip.include?(ip) rescue false + includes && other_ip.prefix < ip.prefix + end + end + + each_ip(static_property, false) do |ip| + if ip_in_array?(ip, restricted_ips) + raise NetworkStaticIpOutOfRange, "Static IP '#{ip}' is in network '#{network_name}' reserved range" end unless range.include?(ip) - raise NetworkStaticIpOutOfRange, "Static IP '#{to_ipaddr(ip)}' is out of network '#{network_name}' range" + raise NetworkStaticIpOutOfRange, "Static IP '#{ip}' is out of network '#{network_name}' range" + end + + static_cidrs.add(ip) + end + + if prefix.nil? + if range.ipv6? + prefix = Network::IPV6_DEFAULT_PREFIX_SIZE + else + prefix = Network::IPV4_DEFAULT_PREFIX_SIZE + end + else + if range.prefix > prefix.to_i + raise NetworkPrefixSizeTooBig, "Prefix size '#{prefix}' is larger than range prefix '#{range.prefix}'" end + end - static_ips.add(ip) + static_cidrs.each do |static_cidr| + static_cidr.each_base_addr(prefix) do |base_addr_int| + if static_cidr.include?(base_addr_int) + static_ips.add(to_ipaddr(base_addr_int)) + end + break if static_cidr.last.to_i <= base_addr_int + end end end @@ -95,10 +125,11 @@ def self.parse(network_name, subnet_spec, availability_zones, managed = false) static_ips, sn_name, netmask_bits, + prefix ) end - def initialize(network_name, range, gateway, name_servers, cloud_properties, netmask, availability_zone_names, restricted_ips, static_ips, subnet_name = nil, netmask_bits = nil) + def initialize(network_name, range, gateway, name_servers, cloud_properties, netmask, availability_zone_names, restricted_ips, static_ips, subnet_name = nil, netmask_bits = nil, prefix = nil) @network_name = network_name @name = subnet_name @netmask_bits = netmask_bits @@ -110,6 +141,7 @@ def initialize(network_name, range, gateway, name_servers, cloud_properties, net @availability_zone_names = availability_zone_names @restricted_ips = restricted_ips @static_ips = static_ips + @prefix = prefix.to_s end def overlaps?(subnet) @@ -123,7 +155,13 @@ def overlaps?(subnet) end def is_reservable?(ip) - range.include?(ip) && !restricted_ips.include?(ip.to_i) + restricted_ips.each do | restricted_ip | + return false if restricted_ip.include?(ip) + rescue IPAddr::InvalidAddressError # when ip versions are not the same + return false + end + + range.include?(ip.to_range.first) && range.include?(ip.to_range.last) end def self.parse_properties_from_database(network_name, subnet_name) diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/network.rb b/src/bosh-director/lib/bosh/director/deployment_plan/network.rb index f970fc75d2e..ac2c48f27fb 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/network.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/network.rb @@ -7,6 +7,8 @@ class Network REQUIRED_DEFAULTS = %w(dns gateway).sort OPTIONAL_DEFAULTS = %w(addressable).sort + IPV4_DEFAULT_PREFIX_SIZE = 32 + IPV6_DEFAULT_PREFIX_SIZE = 128 # @return [String] network name attr_accessor :name @@ -87,6 +89,10 @@ def has_azs?(az_names) def availability_zones @subnets.map(&:availability_zone_names).flatten.uniq end + + def prefix # for now the prefix should be considered the same for all subnets + @subnets.first.prefix + end end end end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/network_parser/name_servers_parser.rb b/src/bosh-director/lib/bosh/director/deployment_plan/network_parser/name_servers_parser.rb index 5b6a103c65b..6b3b0307759 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/network_parser/name_servers_parser.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/network_parser/name_servers_parser.rb @@ -2,6 +2,7 @@ module Bosh::Director module DeploymentPlan module NetworkParser class NameServersParser + include IpUtil include ValidationHelper def parse(network, subnet_properties) @@ -12,13 +13,13 @@ def parse(network, subnet_properties) if dns_spec servers = [] dns_spec.each do |dns| - dns = Bosh::Director::IpAddrOrCidr.new(dns) + dns = to_ipaddr(dns) unless dns.count == 1 raise NetworkInvalidDns, "Invalid DNS for network '#{network}': must be a single IP" end - servers << dns.to_string + servers << dns.base_addr end end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/planner.rb b/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/planner.rb index c38834b0f5e..0226cc34060 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/planner.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/planner.rb @@ -6,13 +6,13 @@ def initialize(logger) end def network_plan_with_dynamic_reservation(instance_plan, job_network) - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_plan.instance.model, job_network.deployment_network) + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_plan.instance.model, job_network.deployment_network, job_network.nic_group) @logger.debug("Creating new dynamic reservation #{reservation} for instance '#{instance_plan.instance}'") Plan.new(reservation: reservation) end def network_plan_with_static_reservation(instance_plan, job_network, static_ip) - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_plan.instance.model, job_network.deployment_network, static_ip) + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_plan.instance.model, job_network.deployment_network, static_ip, job_network.nic_group) @logger.debug("Creating new static reservation #{reservation} for instance '#{instance_plan.instance}'") Plan.new(reservation: reservation) end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/reservation_reconciler.rb b/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/reservation_reconciler.rb index 22996950388..af310b3667c 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/reservation_reconciler.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/reservation_reconciler.rb @@ -9,6 +9,7 @@ def initialize(instance_plan, logger) def reconcile(existing_reservations) unplaced_existing_reservations = Set.new(existing_reservations) existing_network_plans = [] + desired_reservations = @instance_plan.network_plans.map(&:reservation) reconciled_reservations = [] @@ -22,6 +23,7 @@ def reconcile(existing_reservations) end if desired_reservation + @logger.debug( "For desired reservation #{desired_reservation} found existing reservation " \ "on the same network #{existing_reservation}", @@ -35,7 +37,7 @@ def reconcile(existing_reservations) unplaced_existing_reservations.delete(existing_reservation) - if existing_reservation.network != desired_reservation.network + if existing_reservation.network != desired_reservation.network || desired_reservation.nic_group != existing_reservation.nic_group existing_reservation = switch_existing_reservation_network(desired_reservation, existing_reservation) end @@ -86,6 +88,7 @@ def switch_existing_reservation_network(desired_reservation, existing_reservatio existing_reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic( existing_reservation.instance_model, desired_reservation.network, + desired_reservation.nic_group, ) existing_reservation.resolve_ip(existing_reservation_ip) existing_reservation @@ -96,6 +99,8 @@ def reservation_contains_assigned_address?(existing_reservation, desired_reserva return false if desired_reservation.network.is_a?(DynamicNetwork) || existing_reservation.network.is_a?(DynamicNetwork) + return false if desired_reservation.network.prefix != existing_reservation.network.prefix + desired_reservation.network.subnets.any? do |subnet| if existing_reservation.instance_model.availability_zone != '' && !subnet.availability_zone_names.nil? next unless subnet.availability_zone_names.include?(existing_reservation.instance_model.availability_zone) diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/vip_planner.rb b/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/vip_planner.rb index 5ce76499fc2..793df4cceea 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/vip_planner.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/network_planner/vip_planner.rb @@ -1,74 +1,77 @@ -module Bosh::Director::DeploymentPlan - module NetworkPlanner - class VipPlanner - def initialize(network_planner, logger) - @network_planner = network_planner - @logger = logger - end +module Bosh::Director + module DeploymentPlan + module NetworkPlanner + class VipPlanner + include IpUtil + def initialize(network_planner, logger) + @network_planner = network_planner + @logger = logger + end - def add_vip_network_plans(instance_plans, vip_networks) - vip_networks.each do |vip_network| - static_ips = vip_network.static_ips.nil? ? [] : vip_network.static_ips.dup + def add_vip_network_plans(instance_plans, vip_networks) + vip_networks.each do |vip_network| + static_ips = vip_network.static_ips.nil? ? [] : vip_network.static_ips.dup - if !static_ips.empty? && vip_network.deployment_network.globally_allocate_ip? - raise( - Bosh::Director::NetworkReservationVipMisconfigured, - 'IPs cannot be specified in both the instance group and the cloud config', - ) - end + if !static_ips.empty? && vip_network.deployment_network.globally_allocate_ip? + raise( + Bosh::Director::NetworkReservationVipMisconfigured, + 'IPs cannot be specified in both the instance group and the cloud config', + ) + end - if vip_network.deployment_network.globally_allocate_ip? - create_global_networks_plans(instance_plans, vip_network) - else - create_instance_defined_network_plans(instance_plans, vip_network, static_ips) + if vip_network.deployment_network.globally_allocate_ip? + create_global_networks_plans(instance_plans, vip_network) + else + create_instance_defined_network_plans(instance_plans, vip_network, static_ips) + end end end - end - private + private - def create_global_networks_plans(instance_plans, vip_network) - instance_plans.each do |instance_plan| - instance_plan.network_plans << @network_planner.network_plan_with_dynamic_reservation(instance_plan, vip_network) + def create_global_networks_plans(instance_plans, vip_network) + instance_plans.each do |instance_plan| + instance_plan.network_plans << @network_planner.network_plan_with_dynamic_reservation(instance_plan, vip_network) + end end - end - def create_instance_defined_network_plans(instance_plans, vip_network, static_ips) - unplaced_instance_plans = [] + def create_instance_defined_network_plans(instance_plans, vip_network, static_ips) + unplaced_instance_plans = [] + + instance_plans.each do |plan| + static_ip = get_instance_static_ip(plan.existing_instance, vip_network.name, static_ips) + if static_ip + plan.network_plans << @network_planner.network_plan_with_static_reservation(plan, vip_network, static_ip) + else + unplaced_instance_plans << plan + end + end - instance_plans.each do |plan| - static_ip = get_instance_static_ip(plan.existing_instance, vip_network.name, static_ips) - if static_ip + unplaced_instance_plans.each do |plan| + static_ip = static_ips.shift plan.network_plans << @network_planner.network_plan_with_static_reservation(plan, vip_network, static_ip) - else - unplaced_instance_plans << plan end end - unplaced_instance_plans.each do |plan| - static_ip = static_ips.shift - plan.network_plans << @network_planner.network_plan_with_static_reservation(plan, vip_network, static_ip) - end - end + def get_instance_static_ip(existing_instance, network_name, static_ips) + return unless existing_instance - def get_instance_static_ip(existing_instance, network_name, static_ips) - return unless existing_instance + existing_instance_ip = find_ip_for_network(existing_instance, network_name) - existing_instance_ip = find_ip_for_network(existing_instance, network_name) + return unless existing_instance_ip && ip_in_array?(existing_instance_ip, static_ips) - return unless existing_instance_ip && static_ips.include?(existing_instance_ip) + static_ips.delete(existing_instance_ip) - static_ips.delete(existing_instance_ip) + existing_instance_ip + end - existing_instance_ip - end + def find_ip_for_network(existing_instance, network_name) + ip_address = existing_instance.ip_addresses.find do |ip| + ip.network_name == network_name + end - def find_ip_for_network(existing_instance, network_name) - ip_address = existing_instance.ip_addresses.find do |ip| - ip.network_name == network_name + ip_address&.address end - - ip_address&.address end end end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/networks_to_static_ips.rb b/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/networks_to_static_ips.rb index 994ce32a727..8667555696d 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/networks_to_static_ips.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/networks_to_static_ips.rb @@ -16,10 +16,10 @@ def self.create(job_networks, desired_azs, job_name) subnets = job_network.deployment_network.subnets job_network.static_ips.each do |static_ip| - subnet_for_ip = subnets.find { |subnet| subnet.static_ips.include?(static_ip) } + subnet_for_ip = subnets.find { |subnet| ip_in_array?(static_ip, subnet.static_ips) } if subnet_for_ip.nil? raise InstanceGroupNetworkInstanceIpMismatch, - "Instance group '#{job_name}' with network '#{job_network.name}' declares static ip '#{format_ip(static_ip)}', " + + "Instance group '#{job_name}' with network '#{job_network.name}' declares static ip '#{static_ip}', " + "which belongs to no subnet" end az_names = subnet_for_ip.availability_zone_names.nil? ? [nil] : subnet_for_ip.availability_zone_names @@ -59,7 +59,7 @@ def validate_ips_are_in_desired_azs(desired_azs) if non_desired_ip_to_az raise JobStaticIpsFromInvalidAvailabilityZone, - "Instance group '#{@job_name}' declares static ip '#{format_ip(non_desired_ip_to_az.ip)}' which does not belong to any of the instance group's availability zones." + "Instance group '#{@job_name}' declares static ip '#{non_desired_ip_to_az.ip}' which does not belong to any of the instance group's availability zones." end end end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker.rb b/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker.rb index 6a0cd6173c8..da699931f48 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker.rb @@ -52,7 +52,7 @@ def validate_ignored_instances_networks(existing_instance_models) existing_instance_model.ip_addresses.each do |ip_address| ignored_vm_network = @job_networks.select { |n| n.name == ip_address.network_name }.first - if !ignored_vm_network.static_ips.include?(ip_address.address) + if !ip_in_array?(ip_address.address, ignored_vm_network.static_ips) raise DeploymentIgnoredInstancesModification, "In instance group '#{@job_name}', an attempt was made to remove a static ip"+ ' that is used by an ignored instance. This operation is not allowed.' end @@ -133,7 +133,7 @@ def create_network_plan_with_az(instance_plan, network, instance_plans) 'Failed to distribute static IPs to satisfy existing instance reservations' end - @logger.debug("Claiming IP '#{format_ip(static_ip_to_azs.ip)}' on network #{network.name} and az '#{desired_instance.availability_zone}' for instance '#{instance}'") + @logger.debug("Claiming IP '#{static_ip_to_azs.ip}' on network #{network.name} and az '#{desired_instance.availability_zone}' for instance '#{instance}'") @networks_to_static_ips.claim_in_az(static_ip_to_azs.ip, desired_instance.availability_zone) @network_planner.network_plan_with_static_reservation(instance_plan, network, static_ip_to_azs.ip) @@ -141,6 +141,7 @@ def create_network_plan_with_az(instance_plan, network, instance_plans) def create_instance_plan_based_on_existing_ips(desired_instances, existing_instance_model) instance_plan = nil + @job_networks.each do |network| next unless network.static? instance_ips_on_network = find_instance_ips_on_network(existing_instance_model, network) @@ -148,8 +149,9 @@ def create_instance_plan_based_on_existing_ips(desired_instances, existing_insta instance_ips_on_network.each do |instance_ip| ip_address = instance_ip.address + # Instance is using IP in static IPs list, we have to use this instance - @logger.debug("Existing instance '#{instance_name(existing_instance_model)}' is using static IP '#{format_ip(ip_address)}' on network '#{network.name}'") + @logger.debug("Existing instance '#{instance_name(existing_instance_model)}' is using static IP '#{ip_address}' on network '#{network.name}'") if instance_plan.nil? desired_instance = desired_instances.shift instance_plan = create_existing_instance_plan_with_az(desired_instance, existing_instance_model, network, ip_address) @@ -176,7 +178,7 @@ def create_instance_plan_based_on_existing_ips(desired_instances, existing_insta end def find_instance_ips_on_network(existing_instance_model, network) - existing_instance_model.ip_addresses.select { |ip_address| network.static_ips.include?(ip_address.address) } + existing_instance_model.ip_addresses.select { |ip_address| ip_in_array?(ip_address.address, network.static_ips) } end def already_has_instance_plan?(existing_instance_model, instance_plans) @@ -192,13 +194,14 @@ def create_existing_instance_plan_with_az(desired_instance, existing_instance_mo end def assign_az_based_on_ip(desired_instance, existing_instance_model, network, ip_address) - ip_az_names = @networks_to_static_ips.find_by_network_and_ip(network, ip_address).az_names + ip_az_names = @networks_to_static_ips.find_by_network_and_ip(network, ip_address.to_i).az_names + if ip_az_names.include?(existing_instance_model.availability_zone) az_name = existing_instance_model.availability_zone @logger.debug("Instance '#{instance_name(existing_instance_model)}' belongs to az '#{az_name}' that is in subnet az list, reusing instance az.") else raise Bosh::Director::NetworkReservationError, - "Existing instance '#{instance_name(existing_instance_model)}' is using IP '#{format_ip(ip_address)}' in availability zone '#{existing_instance_model.availability_zone}'" + "Existing instance '#{instance_name(existing_instance_model)}' is using IP '#{ip_address}' in availability zone '#{existing_instance_model.availability_zone}'" end desired_instance.az = to_az(az_name) end @@ -246,6 +249,7 @@ def create_existing_instance_plan(desired_instance, existing_instance_model) def create_network_plan_with_ip(instance_plan, network, ip_address) instance_az = instance_plan.desired_instance.az instance_az_name = instance_az.nil? ? nil : instance_az.name + ip_az_names = @networks_to_static_ips.find_by_network_and_ip(network, ip_address).az_names if ip_az_names.include?(instance_az_name) instance_plan.network_plans << @network_planner.network_plan_with_static_reservation(instance_plan, network, ip_address) diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/stages/create_network.rb b/src/bosh-director/lib/bosh/director/deployment_plan/stages/create_network.rb index 64c6df56864..6e75cc4aabc 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/stages/create_network.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/stages/create_network.rb @@ -120,7 +120,7 @@ def create_subnet(subnet, network_model, rollback) network_address_properties = network_create_results[1] network_cloud_properties = network_create_results[2] - range = subnet.range ? subnet.range.to_cidr_s : network_address_properties['range'] + range = subnet.range ? subnet.range.to_s : network_address_properties['range'] gw = subnet.gateway ? subnet.gateway : network_address_properties['gateway'] reserved_ips = network_address_properties.fetch('reserved', []) @@ -145,25 +145,25 @@ def fetch_cpi_input(subnet, az_cloud_props) 'cloud_properties' => {}, } cpi_input['cloud_properties'] = az_cloud_props.merge(subnet.cloud_properties) if subnet.cloud_properties - cpi_input['range'] = subnet.range.to_cidr_s if subnet.range - cpi_input['gateway'] = subnet.gateway.to_s if subnet.gateway + cpi_input['range'] = subnet.range.to_s if subnet.range + cpi_input['gateway'] = subnet.gateway.base_addr if subnet.gateway cpi_input['netmask_bits'] = subnet.netmask_bits if subnet.netmask_bits cpi_input end def populate_subnet_properties(subnet, db_subnet) subnet.cloud_properties = JSON.parse(db_subnet.cloud_properties) - subnet.range = Bosh::Director::IpAddrOrCidr.new(db_subnet.range) - subnet.gateway = Bosh::Director::IpAddrOrCidr.new(db_subnet.gateway) + subnet.range = to_ipaddr(db_subnet.range) + subnet.gateway = to_ipaddr(db_subnet.gateway) subnet.netmask = subnet.range.netmask - subnet.restricted_ips.add(subnet.gateway.to_i) if subnet.gateway - subnet.restricted_ips.add(subnet.range.to_i) - subnet.restricted_ips.add(subnet.range.to_range.last.to_i) + subnet.restricted_ips.add(subnet.gateway) if subnet.gateway + subnet.restricted_ips.add(subnet.range) + subnet.restricted_ips.add(subnet.range.to_range.last) each_ip(JSON.parse(db_subnet.reserved)) do |ip| unless subnet.range.include?(ip) raise NetworkReservedIpOutOfRange, - "Reserved IP '#{to_ipaddr(ip)}' is out of subnet '#{subnet.name}' range" + "Reserved IP '#{ip}' is out of subnet '#{subnet.name}' range" end subnet.restricted_ips.add(ip) end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/vip_network.rb b/src/bosh-director/lib/bosh/director/deployment_plan/vip_network.rb index 4e1125523c1..afc55cd4075 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/vip_network.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/vip_network.rb @@ -6,7 +6,7 @@ class VipNetwork < NetworkWithSubnets # @return [Hash] Network cloud properties attr_reader :cloud_properties - attr_reader :subnets + attr_reader :subnets, :prefix def self.parse(network_spec, availability_zones, logger) name = safe_property(network_spec, 'name', class: String) @@ -15,8 +15,14 @@ def self.parse(network_spec, availability_zones, logger) DeploymentPlan::VipNetworkSubnet.parse(subnet_spec, name, availability_zones) end + unless subnets.empty? + prefix = subnets.first.prefix + else + prefix = Network::IPV4_DEFAULT_PREFIX_SIZE + end + cloud_properties = safe_property(network_spec, 'cloud_properties', class: Hash, default: {}) - new(name, cloud_properties, subnets, logger) + new(name, cloud_properties, subnets, prefix, logger) end ## @@ -25,10 +31,11 @@ def self.parse(network_spec, availability_zones, logger) # @param [Hash] network_spec parsed from the cloud config # @param [VipNetworkSubnet] vip network subnets parsed from the cloud config # @param [Logger] logger - def initialize(name, cloud_properties, subnets, logger) + def initialize(name, cloud_properties, subnets, prefix, logger) super(name, logger) @cloud_properties = cloud_properties @subnets = subnets + @prefix = prefix @logger = TaggedLogger.new(logger, 'network-configuration') end @@ -45,7 +52,7 @@ def network_settings(reservation, default_properties = REQUIRED_DEFAULTS, _avail { 'type' => 'vip', - 'ip' => Bosh::Director::IpAddrOrCidr.new(reservation.ip).to_s, + 'ip' => to_ipaddr(reservation.ip).base_addr, 'cloud_properties' => @cloud_properties, } end @@ -69,7 +76,7 @@ def has_azs?(*) end def find_az_names_for_ip(ip) - subnet = @subnets.find { |sn| sn.static_ips.include?(ip) } + subnet = @subnets.find { |sn| ip_in_array?(ip, sn.static_ips) } subnet.availability_zone_names if subnet end diff --git a/src/bosh-director/lib/bosh/director/deployment_plan/vip_network_subnet.rb b/src/bosh-director/lib/bosh/director/deployment_plan/vip_network_subnet.rb index 50e4f608127..9eb681d1228 100644 --- a/src/bosh-director/lib/bosh/director/deployment_plan/vip_network_subnet.rb +++ b/src/bosh-director/lib/bosh/director/deployment_plan/vip_network_subnet.rb @@ -4,11 +4,16 @@ class VipNetworkSubnet < Subnet extend ValidationHelper extend IpUtil - attr_reader :static_ips, :availability_zone_names + attr_reader :static_ips, :availability_zone_names, :prefix def self.parse(subnet_spec, network_name, azs) static_ips = Set.new + prefix = safe_property(subnet_spec, 'prefix', optional: true) + if prefix.nil? + prefix = Network::IPV4_DEFAULT_PREFIX_SIZE + end + static_property = safe_property(subnet_spec, 'static', class: Array, default: []) each_ip(static_property) do |ip| static_ips.add(ip) @@ -16,16 +21,17 @@ def self.parse(subnet_spec, network_name, azs) availability_zone_names = parse_availability_zones(subnet_spec, network_name, azs) - new(static_ips, availability_zone_names) + new(static_ips, availability_zone_names, prefix) end - def initialize(static_ips, availability_zone_names) + def initialize(static_ips, availability_zone_names, prefix) @static_ips = static_ips @availability_zone_names = availability_zone_names + @prefix = prefix.to_s end def is_reservable?(ip) - @static_ips.include?(ip) + VipNetworkSubnet.ip_in_array?(ip, @static_ips) end end end diff --git a/src/bosh-director/lib/bosh/director/errors.rb b/src/bosh-director/lib/bosh/director/errors.rb index 00e2699d8ba..4756d9904ce 100644 --- a/src/bosh-director/lib/bosh/director/errors.rb +++ b/src/bosh-director/lib/bosh/director/errors.rb @@ -165,6 +165,7 @@ def self.err(error_code, response_code = BAD_REQUEST) NetworkReservationIpOutsideSubnet = err(130012) NetworkReservationIpReserved = err(130013) NetworkReservationVipMisconfigured = err(130014) + NetworkReservationInvalidPrefix = err(130015) # Manifest parsing: instance group section InstanceGroupMissingRelease = err(140001) @@ -224,6 +225,9 @@ def self.err(error_code, response_code = BAD_REQUEST) NetworkInvalidIpRangeFormat = err(160010) NetworkDeletingUnorphanedError = err(160011) NetworkNotFoundError = err(16012) + NetworkPrefixSizeTooBig = err(16013) + NetworkPrefixSizesDiffer = err(16014) + NetworkPrefixStaticIpNotBaseAddress = err(16015) # ResourcePool ResourcePoolUnknownNetwork = err(170001) diff --git a/src/bosh-director/lib/bosh/director/ip_addr_or_cidr.rb b/src/bosh-director/lib/bosh/director/ip_addr_or_cidr.rb index 4a9f4154821..89bd69b28e1 100644 --- a/src/bosh-director/lib/bosh/director/ip_addr_or_cidr.rb +++ b/src/bosh-director/lib/bosh/director/ip_addr_or_cidr.rb @@ -3,8 +3,9 @@ module Bosh module Director class IpAddrOrCidr - delegate :==, :include?, :ipv4?, :ipv6?, :netmask, :to_i, :to_range, :to_string, to: :@ipaddr - alias :to_s :to_string + include Comparable + + delegate :==, :include?, :ipv4?, :ipv6?, :netmask, :mask, :to_i, :to_range, :prefix, :succ, :<=>, to: :@ipaddr def initialize(ip_or_cidr) @ipaddr = @@ -13,18 +14,76 @@ def initialize(ip_or_cidr) elsif ip_or_cidr.kind_of?(Integer) IPAddr.new(ip_or_cidr, inet_type_for(ip_or_cidr)) else - IPAddr.new(ip_or_cidr) + begin + IPAddr.new(ip_or_cidr) + rescue IPAddr::InvalidAddressError => e + raise e, "Invalid IP or CIDR format: #{ip_or_cidr}" + end end end + def each_base_addr(prefix_length) + if @ipaddr.ipv4? + bits = 32 + elsif @ipaddr.ipv6? + bits = 128 + end + step_size = 2**(bits - prefix_length.to_i) + base_addr_int = @ipaddr.to_i + + first_base_addr_int = Bosh::Director::IpAddrOrCidr.new("#{@ipaddr}/#{prefix_length}").to_i + + if base_addr_int != first_base_addr_int + base_addr_int += step_size + end + + while base_addr_int <= @ipaddr.to_range.last.to_i + yield base_addr_int + base_addr_int += step_size + end + end + + def eql?(other) + self == other + end + + def hash + @ipaddr.hash + end + def count (@ipaddr.to_range.last.to_i - @ipaddr.to_range.first.to_i) + 1 end - def to_cidr_s + def to_s "#{@ipaddr}/#{@ipaddr.prefix}" end + def base_addr + @ipaddr.to_s + end + + def to_range + @ipaddr.to_range + end + + def succ + next_ip = @ipaddr.succ + Bosh::Director::IpAddrOrCidr.new(next_ip.to_i) + end + + def <=>(other) + @ipaddr.to_i <=> other.to_i + end + + def last + Bosh::Director::IpAddrOrCidr.new(@ipaddr.to_range.last.to_i) + end + + def first + Bosh::Director::IpAddrOrCidr.new(@ipaddr.to_range.first.to_i) + end + private def max_addresses diff --git a/src/bosh-director/lib/bosh/director/ip_util.rb b/src/bosh-director/lib/bosh/director/ip_util.rb index 62c67f667c4..c735235489d 100644 --- a/src/bosh-director/lib/bosh/director/ip_util.rb +++ b/src/bosh-director/lib/bosh/director/ip_util.rb @@ -1,61 +1,104 @@ module Bosh::Director module IpUtil - def each_ip(range_string_or_strings) + def each_ip(range_string_or_strings, expanded = true) [range_string_or_strings].flatten.compact.each do |range_string| - string_to_range(range_string).each do |ip| - yield ip + string_to_range(range_string, expanded).each do |ip_or_range| + yield ip_or_range end end rescue ArgumentError => e raise NetworkInvalidIpRangeFormat, e.message end - def ip_to_i(ip) - to_ipaddr(ip).to_i - end - def to_ipaddr(ip) Bosh::Director::IpAddrOrCidr.new(ip) end - # @param [Integer] ip Integer IP representation - # @return [String] Human-readable IP representation - def format_ip(ip) - to_ipaddr(ip).to_s - end - def ip_address?(ip) - ip_address = Bosh::Director::IpAddrOrCidr.new(ip) + ip_address = to_ipaddr(ip) ip_address.ipv4? || ip_address.ipv6? rescue return false end + def ip_in_array?(ip_to_check, ip_objects_array) + ip_to_check = to_ipaddr(ip_to_check) + + ip_objects_array.any? do |ip_object| + ip_object.include?(ip_to_check) + rescue IPAddr::InvalidAddressError + false + end + end + private - def string_to_range(range_string) + def string_to_range(range_string, expanded) parts = range_string.split('-').map { |part| part.strip } - unless [1,2].include?(parts.length) + if parts.size == 1 + if expanded + cidr_range = to_ipaddr(parts[0]).to_range + first_ip = to_ipaddr(cidr_range.first.to_i) + last_ip = to_ipaddr(cidr_range.last.to_i) + (first_ip .. last_ip) + else + [to_ipaddr(parts[0])] + end + elsif parts.size == 2 + first_ip = to_ipaddr(parts[0]) + last_ip = to_ipaddr(parts[1]) + unless first_ip.count == 1 && last_ip.count == 1 + raise NetworkInvalidIpRangeFormat, "Invalid IP range format: #{range_string}" + end + if expanded + (first_ip .. last_ip) + else + ip_range_to_cidr_list(first_ip, last_ip) + end + else raise NetworkInvalidIpRangeFormat, "Invalid IP range format: #{range_string}" end + end - if parts.size == 1 - cidr_range = Bosh::Director::IpAddrOrCidr.new(parts[0]).to_range - first_ip = cidr_range.first - last_ip = cidr_range.last + def ip_range_to_cidr_list(first_ip, last_ip) + cidr_blocks = [] - elsif parts.size == 2 - first_ip = Bosh::Director::IpAddrOrCidr.new(parts[0]) - last_ip = Bosh::Director::IpAddrOrCidr.new(parts[1]) + current_ip = first_ip - unless first_ip.count == 1 && last_ip.count == 1 - raise NetworkInvalidIpRangeFormat, "Invalid IP range format: #{range_string}" + while current_ip.to_i <= last_ip.to_i + mask = current_ip.ipv4? ? 32 : 128 + + while mask >= 0 + potential_subnet = to_ipaddr("#{current_ip.base_addr}/#{mask}") + + first_ip_in_range = potential_subnet.first + last_ip_in_range = potential_subnet.last + + if first_ip_in_range == current_ip && last_ip_in_range == last_ip + cidr_blocks << potential_subnet + current_ip = potential_subnet.last.succ + break + end + + if first_ip_in_range < current_ip || ( first_ip_in_range == current_ip && last_ip_in_range >= last_ip ) + previous_mask = mask + 1 + found_subnet = to_ipaddr("#{current_ip.base_addr}/#{previous_mask}") + cidr_blocks << found_subnet + current_ip = found_subnet.last.succ + break + end + + mask -= 1 + end + + if current_ip.to_i > last_ip.to_i + break end end + cidr_blocks - (first_ip.to_i .. last_ip.to_i) end end end diff --git a/src/bosh-director/lib/bosh/director/metrics_collector.rb b/src/bosh-director/lib/bosh/director/metrics_collector.rb index 91b600b17ab..0c94c941639 100644 --- a/src/bosh-director/lib/bosh/director/metrics_collector.rb +++ b/src/bosh-director/lib/bosh/director/metrics_collector.rb @@ -173,7 +173,9 @@ def calculate_network_metrics(network) network.subnets.each do |subnet| total_static += subnet.static_ips.size - total_restricted += subnet.restricted_ips.size + subnet.restricted_ips.each do |ip_addr_or_cidr| + total_restricted += ip_addr_or_cidr.count + end total_available += subnet.range.count end diff --git a/src/bosh-director/lib/bosh/director/models/ip_address.rb b/src/bosh-director/lib/bosh/director/models/ip_address.rb index ecc441816a8..cae019c7952 100644 --- a/src/bosh-director/lib/bosh/director/models/ip_address.rb +++ b/src/bosh-director/lib/bosh/director/models/ip_address.rb @@ -1,55 +1,60 @@ -module Bosh::Director::Models - class IpAddress < Sequel::Model(Bosh::Director::Config.db) - many_to_one :instance - many_to_one :vm - many_to_one :orphaned_vm - - def validate - raise 'No instance or orphaned VM associated with IP' if instance_id.nil? && orphaned_vm_id.nil? - raise 'IP address cannot have both instance id and orphaned VM id' if !instance_id.nil? && !orphaned_vm_id.nil? - validates_presence :instance_id, allow_nil: true - validates_presence :orphaned_vm_id, allow_nil: true - validates_presence :task_id - validates_presence :address_str - validates_unique :address_str - raise 'Invalid type for address_str column' unless address_str.is_a?(String) - end +module Bosh::Director + module Models + class IpAddress < Sequel::Model(Bosh::Director::Config.db) + include IpUtil - def before_create - self.created_at ||= Time.now - end + many_to_one :instance + many_to_one :vm + many_to_one :orphaned_vm - def info - [ - "#{instance.deployment.name}.#{instance.job}/#{instance.index}", - network_name, - "#{Bosh::Director::IpAddrOrCidr.new(address_str.to_i)} (#{type})" - ].join(' - ') - end + def validate + raise 'No instance or orphaned VM associated with IP' if instance_id.nil? && orphaned_vm_id.nil? + raise 'IP address cannot have both instance id and orphaned VM id' if !instance_id.nil? && !orphaned_vm_id.nil? + validates_presence :instance_id, allow_nil: true + validates_presence :orphaned_vm_id, allow_nil: true + validates_presence :task_id + validates_presence :address_str + validates_unique :address_str + raise 'Invalid type for address_str column' unless address_str.is_a?(String) + end - def formatted_ip - Bosh::Director::IpAddrOrCidr.new(address).to_s - end + def before_create + self.created_at ||= Time.now + end - def type - static ? 'static' : 'dynamic' - end + def info + [ + "#{instance.deployment.name}.#{instance.job}/#{instance.index}", + network_name, + "#{to_ipaddr(address_str)} (#{type})" + ].join(' - ') + end - def address - unless address_str.match?(/\A\d+\z/) - info_display = '' - begin - info_display = info - rescue StandardError - info_display = 'missing_info' + def formatted_ip + address.to_s + end + + def type + static ? 'static' : 'dynamic' + end + + def address + unless address_str.include?('/') || address_str.match?(/\A\d+\z/) + info_display = '' + begin + info_display = info + rescue StandardError + info_display = 'missing_info' + end + raise "Unexpected address '#{address_str}' (#{info_display})" end - raise "Unexpected address '#{address_str}' (#{info_display})" + + to_ipaddr(address_str) end - address_str.to_i - end - def to_s - info + def to_s + info + end end end end diff --git a/src/bosh-director/lib/bosh/director/models/vm.rb b/src/bosh-director/lib/bosh/director/models/vm.rb index a7465642fc9..004000f869d 100644 --- a/src/bosh-director/lib/bosh/director/models/vm.rb +++ b/src/bosh-director/lib/bosh/director/models/vm.rb @@ -19,6 +19,16 @@ def network_spec=(spec) end def ips + ips_cidr.map do |cidr_ip| + if ( cidr_ip.include?(':') && cidr_ip.include?("/#{Bosh::Director::DeploymentPlan::Network::IPV6_DEFAULT_PREFIX_SIZE}") ) || ( cidr_ip.include?('.') && cidr_ip.include?("/#{Bosh::Director::DeploymentPlan::Network::IPV4_DEFAULT_PREFIX_SIZE}") ) + cidr_ip.split('/')[0] + else + cidr_ip + end + end + end + + def ips_cidr manual_or_vip_ips.concat(dynamic_ips).uniq end @@ -29,7 +39,18 @@ def manual_or_vip_ips end def dynamic_ips - network_spec.map { |_, network| network['ip'] } + network_spec.map do |_, network| + ip = network['ip'] + prefix = network['prefix'].to_s + + if prefix.empty? + prefix = ip.include?(':') ? + Bosh::Director::DeploymentPlan::Network::IPV6_DEFAULT_PREFIX_SIZE : + Bosh::Director::DeploymentPlan::Network::IPV4_DEFAULT_PREFIX_SIZE + end + + "#{ip}/#{prefix}" + end end end end diff --git a/src/bosh-director/lib/bosh/director/network_reservation.rb b/src/bosh-director/lib/bosh/director/network_reservation.rb index f1f99702dda..d4d3d84a367 100644 --- a/src/bosh-director/lib/bosh/director/network_reservation.rb +++ b/src/bosh-director/lib/bosh/director/network_reservation.rb @@ -1,11 +1,12 @@ module Bosh::Director class NetworkReservation - attr_reader :ip, :instance_model, :network, :type + attr_reader :ip, :instance_model, :network, :type, :nic_group - def initialize(instance_model, network) + def initialize(instance_model, network, nic_group) @instance_model = instance_model @network = network @ip = nil + @nic_group = nic_group&.to_i end def static? @@ -15,20 +16,14 @@ def static? def dynamic? type == :dynamic end - - private - - def formatted_ip - IpAddrOrCidr.new(@ip).to_s if @ip - end end class ExistingNetworkReservation < NetworkReservation attr_reader :network_type, :obsolete - def initialize(instance_model, network, ip, network_type) - super(instance_model, network) - @ip = IpAddrOrCidr.new(ip).to_i if ip + def initialize(instance_model, network, ip, network_type, nic_group = nil) + super(instance_model, network, nic_group) + @ip = IpAddrOrCidr.new(ip) if ip @network_type = network_type @obsolete = network.instance_of? Bosh::Director::DeploymentPlan::Network end @@ -38,48 +33,49 @@ def resolve_type(type) end def desc - "existing reservation#{@ip.nil? ? '' : " with IP '#{formatted_ip}' for instance #{@instance_model}"}" + "existing reservation#{@ip.nil? ? '' : " with IP '#{@ip}' for instance #{@instance_model}"}" end def to_s - "{ip=#{formatted_ip}, network=#{@network.name}, instance=#{@instance_model}, type=#{type}}" + "{ip=#{@ip}, network=#{@network.name}, instance=#{@instance_model}, type=#{type}, nic_group=#{@nic_group}}" end end class DesiredNetworkReservation < NetworkReservation - def self.new_dynamic(instance_model, network) - new(instance_model, network, nil, :dynamic) + def self.new_dynamic(instance_model, network, nic_group = nil) + new(instance_model, network, nil, :dynamic, nic_group) end - def self.new_static(instance_model, network, ip) - new(instance_model, network, ip, :static) + def self.new_static(instance_model, network, ip, nic_group = nil) + cidr_ip = "#{IpAddrOrCidr.new(ip).base_addr}/#{network.prefix}" + new(instance_model, network, cidr_ip, :static, nic_group) end - def initialize(instance_model, network, ip, type) - super(instance_model, network) - @ip = IpAddrOrCidr.new(ip).to_i if ip + def initialize(instance_model, network, ip, type, nic_group) + super(instance_model, network, nic_group) + @ip = resolve_ip(ip) if ip @type = type end def resolve_ip(ip) - @ip = IpAddrOrCidr.new(ip).to_i + @ip = IpAddrOrCidr.new(ip) end def resolve_type(type) if @type != type raise NetworkReservationWrongType, - "IP '#{formatted_ip}' on network '#{@network.name}' does not belong to #{@type} pool" + "IP '#{@ip}' on network '#{@network.name}' does not belong to #{@type} pool" end @type = type end def desc - "#{type} reservation with IP '#{formatted_ip}' for instance #{@instance_model}" + "#{type} reservation with IP '#{@ip}' for instance #{@instance_model}" end def to_s - "{type=#{type}, ip=#{formatted_ip}, network=#{@network.name}, instance=#{@instance_model}}" + "{type=#{type}, ip=#{@ip}, network=#{@network.name}, instance=#{@instance_model}, nic_group=#{@nic_group}}" end end end diff --git a/src/bosh-director/lib/clouds/dummy.rb b/src/bosh-director/lib/clouds/dummy.rb index 949cae9ea0c..cad9917c82e 100644 --- a/src/bosh-director/lib/clouds/dummy.rb +++ b/src/bosh-director/lib/clouds/dummy.rb @@ -95,7 +95,7 @@ def create_vm(agent_id, stemcell_id, cloud_properties, networks, disk_cids, env) elsif cloud_properties['az_name'] ip_address = cmd.ip_address_for_az(cloud_properties['az_name']) else - ip_address = IPAddr.new(rand(0..IPAddr::IN4MASK), Socket::AF_INET).to_string + ip_address = IPAddr.new(rand(0..IPAddr::IN4MASK), Socket::AF_INET).to_s end if ip_address diff --git a/src/bosh-director/spec/factories.rb b/src/bosh-director/spec/factories.rb index c0ebfa335e3..4cb0101ab5d 100644 --- a/src/bosh-director/spec/factories.rb +++ b/src/bosh-director/spec/factories.rb @@ -23,9 +23,10 @@ name { 'job-network-name' } static_ips { [] } default_for { [] } + nic_group { nil } association :deployment_network, factory: :deployment_plan_manual_network, strategy: :build - initialize_with { new(name, static_ips, default_for, deployment_network) } + initialize_with { new(name, static_ips, default_for, deployment_network, nic_group) } end factory :deployment_plan_instance_group, class: Bosh::Director::DeploymentPlan::InstanceGroup do @@ -170,9 +171,10 @@ factory :models_ip_address, class: Bosh::Director::Models::IpAddress do sequence(:network_name) { |i| "ip-address-network-name-#{i}" } sequence(:task_id) { |i| "ip-address-task-id-#{i}" } - address_str { IPAddr.new(Random.rand(IPAddr::IN4MASK.to_i), Socket::AF_INET).to_i.to_s } + address_str { Bosh::Director::IpAddrOrCidr.new(Random.rand(IPAddr::IN4MASK)).to_s } static { false } created_at { Time.now } + nic_group { nil } association :instance, factory: :models_instance, strategy: :create end diff --git a/src/bosh-director/spec/unit/bosh/director/api/controllers/deployments_controller_spec.rb b/src/bosh-director/spec/unit/bosh/director/api/controllers/deployments_controller_spec.rb index ea7469d0b56..a81ff5513a6 100644 --- a/src/bosh-director/spec/unit/bosh/director/api/controllers/deployments_controller_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/api/controllers/deployments_controller_spec.rb @@ -1356,7 +1356,7 @@ def manifest_with_errand(deployment_name='errand') 'state' => 'started', 'uuid' => "instance-#{i}", 'variable_set_id' => (Models::VariableSet.create(deployment: deployment).id), - 'spec' => {'networks' => {'network1' => {'ip' => "#{i}.#{i}.#{i}.#{i}"}}}, + 'spec' => {'networks' => {'network1' => {'ip' => "#{i}.#{i}.#{i}.#{i}", 'prefix' => '32'}}}, } instance_params['availability_zone'] = 'az0' if i == 0 @@ -1368,7 +1368,7 @@ def manifest_with_errand(deployment_name='errand') 'cid' => "cid-#{i}-#{j}", 'instance_id' => instance.id, 'created_at' => time, - 'network_spec' => {'network1' => {'ip' => "#{i}.#{i}.#{j}.#{j}"}}, + 'network_spec' => {'network1' => {'ip' => "#{i}.#{i}.#{j}.#{j}", 'prefix' => '32'}}, } vm = Models::Vm.create(vm_params) @@ -1453,7 +1453,7 @@ def manifest_with_errand(deployment_name='errand') ip_addresses_params = { 'instance_id' => instance.id, 'task_id' => i.to_s, - 'address_str' => ip_to_i("1.2.#{i}.#{j}").to_s, + 'address_str' => ("1.2.#{i}.#{j}/32"), 'vm_id' => vm.id, } Models::IpAddress.create(ip_addresses_params) @@ -1485,6 +1485,69 @@ def manifest_with_errand(deployment_name='errand') end end + it 'returns ip addresses with prefix if prefix differs from 32 or 128 for a vm' do + 9.times do |i| + instance_params = { + 'deployment_id' => deployment.id, + 'job' => "job-#{i}", + 'index' => i, + 'state' => 'started', + 'uuid' => "instance-#{i}", + 'variable_set_id' => (Models::VariableSet.create(deployment: deployment).id) + } + + instance_params['availability_zone'] = 'az0' if i == 0 + instance_params['availability_zone'] = 'az1' if i == 1 + instance = Models::Instance.create(instance_params) + 2.times do |j| + vm_params = { + 'agent_id' => "agent-#{i}-#{j}", + 'cid' => "cid-#{i}-#{j}", + 'instance_id' => instance.id, + 'created_at' => time, + } + + vm = Models::Vm.create(vm_params) + + if j == 0 + instance.active_vm = vm + end + + ip_addresses_params = { + 'instance_id' => instance.id, + 'task_id' => i.to_s, + 'address_str' => "1.2.#{i}.#{j*16}/28", + 'vm_id' => vm.id, + } + Models::IpAddress.create(ip_addresses_params) + end + end + + get '/test_deployment/vms' + + expect(last_response.status).to eq(200) + body = JSON.parse(last_response.body) + expect(body.size).to eq(18) + + body.sort_by { |instance| instance['agent_id'] }.each_with_index do |instance_with_vm, i| + instance_idx = i / 2 + vm_by_instance = i % 2 + vm_is_active = vm_by_instance == 0 + expect(instance_with_vm).to eq( + 'agent_id' => "agent-#{instance_idx}-#{vm_by_instance}", + 'job' => "job-#{instance_idx}", + 'index' => instance_idx, + 'cid' => "cid-#{instance_idx}-#{vm_by_instance}", + 'id' => "instance-#{instance_idx}", + 'active' => vm_is_active, + 'az' => { 0 => 'az0', 1 => 'az1', nil => nil }[instance_idx], + 'ips' => ["1.2.#{instance_idx}.#{vm_by_instance*16}/28"], + 'vm_created_at' => time.utc.iso8601, + 'permanent_nats_credentials' => false, + ) + end + end + it 'returns network spec ip addresses' do 15.times do |i| instance_params = { @@ -1504,7 +1567,7 @@ def manifest_with_errand(deployment_name='errand') 'cid' => "cid-#{i}", 'instance_id' => instance.id, 'created_at' => time, - 'network_spec' => {'network1' => {'ip' => "1.2.3.#{i}"}}, + 'network_spec' => {'network1' => {'ip' => "1.2.3.#{i}", 'prefix' => '32'}}, } vm = Models::Vm.create(vm_params) @@ -1558,8 +1621,8 @@ def manifest_with_errand(deployment_name='errand') 'created_at' => time, 'instance_id' => instance.id, 'network_spec' => { - 'network1' => { 'ip' => network_spec_ip }, - 'network2' => { 'ip' => vip }, + 'network1' => { 'ip' => network_spec_ip, 'prefix' => '32' }, + 'network2' => { 'ip' => vip, 'prefix' => '32'}, }, } @@ -1567,7 +1630,7 @@ def manifest_with_errand(deployment_name='errand') instance.active_vm = vm ip_addresses_params = { - 'address_str' => ip_to_i(vip).to_s, + 'address_str' => "#{vip}/32", 'instance_id' => instance.id, 'task_id' => '1', 'vm_id' => vm.id, @@ -1587,7 +1650,7 @@ def manifest_with_errand(deployment_name='errand') 'cid' => 'cid', 'id' => 'instance-id', 'index' => 0, - 'ips' => [vip, network_spec_ip], + 'ips' => ["#{vip}", "#{network_spec_ip}"], 'job' => 'job', 'vm_created_at' => time.utc.iso8601, 'permanent_nats_credentials' => false, @@ -1700,14 +1763,7 @@ def manifest_with_errand(deployment_name='errand') instance_params['availability_zone'] = 'az0' if i == 0 instance_params['availability_zone'] = 'az1' if i == 1 - instance = Models::Instance.create(instance_params) - - ip_addresses_params = { - 'instance_id' => instance.id, - 'task_id' => "#{i}", - 'address_str' => ip_to_i("1.2.3.#{i}").to_s, - } - Models::IpAddress.create(ip_addresses_params) + Models::Instance.create(instance_params) end get '/test_deployment/instances' diff --git a/src/bosh-director/spec/unit/bosh/director/cloudcheck_helper_spec.rb b/src/bosh-director/spec/unit/bosh/director/cloudcheck_helper_spec.rb index 94d852c80f1..a606e901afe 100644 --- a/src/bosh-director/spec/unit/bosh/director/cloudcheck_helper_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/cloudcheck_helper_spec.rb @@ -32,7 +32,7 @@ def initialize(instance_uuid, data) instance end - let!(:ip_address) { FactoryBot.create(:models_ip_address, instance: instance, address_str: ip_to_i('192.1.3.4').to_s) } + let!(:ip_address) { FactoryBot.create(:models_ip_address, instance: instance, address_str: to_ipaddr('192.1.3.4').to_s) } let!(:vm) { FactoryBot.create(:models_vm, instance: instance, active: true) } let(:spec) do @@ -261,7 +261,7 @@ def expect_vm_gets_created expect(use_existing).to eq(true) expect(instance_plan.network_plans.count).to eq(1) expect(instance_plan.network_plans.first.existing?).to eq(true) - expect(instance_plan.network_plans.first.reservation.ip).to eq(ip_to_i('192.1.3.4')) + expect(instance_plan.network_plans.first.reservation.ip).to eq(to_ipaddr('192.1.3.4')) end expect(rendered_templates_persister).to receive(:persist) diff --git a/src/bosh-director/spec/unit/bosh/director/db/migrations/verify_migration_digest_spec.rb b/src/bosh-director/spec/unit/bosh/director/db/migrations/verify_migration_digest_spec.rb index 192d0b16657..3c2ae7a9ef6 100644 --- a/src/bosh-director/spec/unit/bosh/director/db/migrations/verify_migration_digest_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/db/migrations/verify_migration_digest_spec.rb @@ -20,6 +20,10 @@ module Bosh::Director filename: '20240319204601_remove_dns_records_from_instances.rb', sha1: 'bb637d410772d09caabdb68a1126fbc9f9b4deec', }, + { + filename: '20250618102610_migrate_ip_address_representation_from_integer_to_cidr_notation.rb', + sha1: '83f219a46b0943b5e27355cb3792e639c7507707', + }, ] end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/dynamic_network_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/dynamic_network_spec.rb index 4dae3f9b50a..f7776201f6a 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/dynamic_network_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/dynamic_network_spec.rb @@ -104,6 +104,19 @@ ) }.to raise_error(Bosh::Director::NetworkInvalidProperty, "Network 'foo' must not specify 'azs'.") end + + it "raises error when 'prefix' is present on the network spec" do + expect { + Bosh::Director::DeploymentPlan::DynamicNetwork.parse( + { + 'name' => 'foo', + 'prefix' => '28' + }, + [Bosh::Director::DeploymentPlan::AvailabilityZone.new('foo-zone', {})], + logger + ) + }.to raise_error(Bosh::Director::NetworkInvalidProperty, "Network 'foo' must not specify 'prefix'.") + end end context 'with a manifest specifying subnets' do @@ -242,6 +255,30 @@ }.to raise_error(Bosh::Director::ValidationInvalidType) end + it 'raises error when prefix is defined' do + expect { + Bosh::Director::DeploymentPlan::DynamicNetwork.parse( + { + 'name' => 'foo', + 'subnets' => [ + { + 'dns' => %w[1.2.3.4 5.6.7.8], + 'cloud_properties' => { + 'foz' => 'baz' + }, + 'az' => 'foz-zone', + 'prefix' => 28 + }, + ] + }, + [ + Bosh::Director::DeploymentPlan::AvailabilityZone.new('foz-zone', {}), + ], + logger + ) + }.to raise_error(Bosh::Director::NetworkInvalidProperty, "Prefix property is not supported for dynamic networks.") + end + it 'raises error when dns is present at the top level' do expect { Bosh::Director::DeploymentPlan::DynamicNetwork.parse( @@ -341,7 +378,7 @@ end it 'should fail when for static reservation' do - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, @network, 1) + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, @network, "1.1.1.1") expect { @network.network_settings(reservation) }.to raise_error Bosh::Director::NetworkReservationWrongType @@ -537,7 +574,7 @@ describe :validate_reference_from_job do it 'returns true if job has a valid network spec' do - dynamic_network = Bosh::Director::DeploymentPlan::DynamicNetwork.new('dynamic', [], logger) + dynamic_network = Bosh::Director::DeploymentPlan::DynamicNetwork.new('dynamic', [], '32', logger) job_network_spec = {'name' => 'dynamic'} expect { @@ -547,7 +584,7 @@ context 'when network is dynamic but job network spec uses static ips' do it 'raises StaticIPNotSupportedOnDynamicNetwork' do - dynamic_network = Bosh::Director::DeploymentPlan::DynamicNetwork.new('dynamic', [], logger) + dynamic_network = Bosh::Director::DeploymentPlan::DynamicNetwork.new('dynamic', [], '32', logger) job_network_spec = { 'name' => 'dynamic', 'static_ips' => ['192.168.1.10'] diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_networks_parser_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_networks_parser_spec.rb index cf0331b2d70..dcbc48e907f 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_networks_parser_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_networks_parser_spec.rb @@ -11,10 +11,10 @@ module Bosh::Director::DeploymentPlan instance_group_network['static_ips'] = ['192.168.1.1', '192.168.1.2'] instance_group end - let(:manifest_networks) { [ManualNetwork.new('a', [], per_spec_logger)] } + let(:manifest_networks) { [ManualNetwork.new('a', [], '32', per_spec_logger)] } context 'when instance group references a network not mentioned in the networks spec' do - let(:manifest_networks) { [ManualNetwork.new('my-network', [], per_spec_logger)] } + let(:manifest_networks) { [ManualNetwork.new('my-network', [], '32', per_spec_logger)] } it 'raises JobUnknownNetwork' do expect do @@ -38,7 +38,7 @@ module Bosh::Director::DeploymentPlan end context 'when instance group network spec references dynamic network with static IPs' do - let(:dynamic_network) { Bosh::Director::DeploymentPlan::DynamicNetwork.new('a', [], per_spec_logger) } + let(:dynamic_network) { Bosh::Director::DeploymentPlan::DynamicNetwork.new('a', [], '32', per_spec_logger) } let(:instance_group_spec) do instance_group = SharedSupport::DeploymentManifestHelper.simple_manifest_with_instance_groups['instance_groups'].first instance_group['networks'] = [{ @@ -66,11 +66,29 @@ module Bosh::Director::DeploymentPlan it 'raises an error' do expect do instance_group_networks_parser.parse(instance_group_spec, 'instance-group-name', manifest_networks) - end.to raise_error Bosh::Director::JobInvalidStaticIPs, "Instance group 'instance-group-name' specifies static IP '192.168.1.2' more than once" + end.to raise_error Bosh::Director::JobInvalidStaticIPs, "Instance group 'instance-group-name' specifies static IP '192.168.1.2/32' more than once" end end context 'when called with a valid instance group spec' do + let(:subnet_spec) do + { + 'range' => '192.168.1.0/24', + 'gateway' => '192.168.1.1' + } + end + let(:manifest_networks) { [ManualNetwork.new('a', [ManualNetworkSubnet.parse('a', subnet_spec, "")], '32', per_spec_logger)] } + + let(:instance_group_spec) do + instance_group = SharedSupport::DeploymentManifestHelper.simple_manifest_with_instance_groups['instance_groups'].first + instance_group['networks'] = [{ + 'name' => 'a', + 'static_ips' => ['192.168.1.1', '192.168.1.2'], + }] + instance_group + end + + it 'adds static ips to instance group networks in order as they are in manifest' do networks = instance_group_networks_parser.parse(instance_group_spec, 'instance-group-name', manifest_networks) @@ -80,17 +98,84 @@ module Bosh::Director::DeploymentPlan name: 'a', static_ips: ['192.168.1.1', '192.168.1.2'], default_for: %w[dns gateway], - deployment_network: manifest_networks.first, - ), + deployment_network: manifest_networks.first ), + ) + expect(networks.first.static_ips).to eq(['192.168.1.1', '192.168.1.2']) + end + end + + context 'when called with a valid instance group spec containing two networks' do + let(:subnet_spec) do + { + 'range' => '192.168.1.0/24', + 'gateway' => '192.168.1.1' + } + end + let(:subnet_spec_ipv6) do + { + 'range' => '2001:db8::/112', + 'gateway' => '2001:db8::1' + } + end + let(:manifest_network) { ManualNetwork.new('a', [ManualNetworkSubnet.parse('a', subnet_spec, "")], '32', per_spec_logger) } + let(:manifest_network_ipv6) { ManualNetwork.new('a_ipv6', [ManualNetworkSubnet.parse('a_ipv6', subnet_spec_ipv6, "")], '128', per_spec_logger) } + let(:two_manifest_networks) { [manifest_network, manifest_network_ipv6] } + + let(:instance_group_spec) do + instance_group = SharedSupport::DeploymentManifestHelper.simple_manifest_with_instance_groups['instance_groups'].first + instance_group['networks'] = [{ + 'name' => 'a', + 'static_ips' => ['192.168.1.1', '192.168.1.2'], + }, + { + 'name' => 'a_ipv6', + 'static_ips' => ['2001:db8::1', '2001:db8::2'], + } + ] + instance_group + end + let(:instance_group_spec_with_defaults) do + instance_group = SharedSupport::DeploymentManifestHelper.simple_manifest_with_instance_groups['instance_groups'].first + instance_group['networks'] = [{ + 'name' => 'a', + 'static_ips' => ['192.168.1.1', '192.168.1.2'], + 'default' => ['dns', 'gateway'], + }, + { + 'name' => 'a_ipv6', + 'static_ips' => ['2001:db8::1', '2001:db8::2'], + } + ] + instance_group + end + + + it 'issues an error if no default for dns and gateway is defined' do + expect do + instance_group_networks_parser.parse(instance_group_spec, 'instance-group-name', two_manifest_networks) + end.to raise_error Bosh::Director::JobNetworkMissingDefault, "Instance group 'instance-group-name' must specify which network is default for dns, gateway, since it has more than one network configured" + end + + it 'adds static ips to instance group networks in order as they are in manifest' do + networks = instance_group_networks_parser.parse(instance_group_spec_with_defaults, 'instance-group-name', two_manifest_networks) + + expect(networks.count).to eq(2) + expect(networks.first).to be_an_instance_group_network( + FactoryBot.build(:deployment_plan_job_network, + name: 'a', + static_ips: ['192.168.1.1', '192.168.1.2'], + default_for: %w[dns gateway], + deployment_network: manifest_network ), ) - expect(networks.first.static_ips).to eq([ip_to_i('192.168.1.1'), ip_to_i('192.168.1.2')]) + expect(networks.first.static_ips.map { |ip| ip.base_addr }).to eq(['192.168.1.1', '192.168.1.2']) + expect(networks[1].static_ips.map { |ip| ip.base_addr }).to eq(['2001:db8::1', '2001:db8::2']) end end RSpec::Matchers.define :be_an_instance_group_network do |expected| match do |actual| actual.name == expected.name && - actual.static_ips == expected.static_ips.map { |ip_to_i| IPAddr.new(ip_to_i) } && + actual.static_ips.map { |ip| ip.base_addr } == expected.static_ips && actual.deployment_network == expected.deployment_network end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_spec_parser_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_spec_parser_spec.rb index fa8d9d62ba1..0c13aa1dc5c 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_spec_parser_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_group_spec_parser_spec.rb @@ -32,7 +32,7 @@ module DeploymentPlan ) end let(:deployment_model) { FactoryBot.create(:models_deployment) } - let(:network) { ManualNetwork.new('fake-network-name', [], per_spec_logger) } + let(:network) { ManualNetwork.new('fake-network-name', [], '32', per_spec_logger) } let(:task) { FactoryBot.create(:models_task, id: 42) } let(:task_writer) { Bosh::Director::TaskDBWriter.new(:event_output, task.id) } let(:event_log) { Bosh::Director::EventLog::Log.new(task_writer) } @@ -1002,7 +1002,7 @@ module DeploymentPlan instance_group_spec['instances'] = 3 instance_group_spec['networks'].first['default'] = %w[gateway dns] instance_group_spec['networks'] << instance_group_spec['networks'].first.merge('name' => 'duped-network') # dupe it - duped_network = ManualNetwork.new('duped-network', [], per_spec_logger) + duped_network = ManualNetwork.new('duped-network', [], '32', per_spec_logger) allow(deployment_plan).to receive(:networks).and_return([duped_network, network]) expect do @@ -1029,7 +1029,7 @@ module DeploymentPlan end context 'when there are two networks, each being a separate default' do - let(:network2) { ManualNetwork.new('fake-network-name-2', [], per_spec_logger) } + let(:network2) { ManualNetwork.new('fake-network-name-2', [], '32', per_spec_logger) } it 'picks the only network as default' do instance_group_spec['networks'].first['default'] = ['dns'] diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_network_reservations_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_network_reservations_spec.rb index 978dbb72031..5ea81cb0ac0 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_network_reservations_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_network_reservations_spec.rb @@ -2,6 +2,7 @@ module Bosh::Director describe DeploymentPlan::InstanceNetworkReservations do + include Bosh::Director::IpUtil let(:deployment_model) { FactoryBot.create(:models_deployment, name: 'foo-deployment') } let(:cloud_config) { FactoryBot.create(:models_config_cloud, :with_manifest) } let(:runtime_config) { FactoryBot.create(:models_config_runtime) } @@ -54,8 +55,8 @@ module Bosh::Director describe 'create_from_db' do context 'when there are IP addresses in db' do - let(:ip1) { IPAddr.new('192.168.0.1').to_i } - let(:ip2) { IPAddr.new('192.168.0.2').to_i } + let(:ip1) { to_ipaddr('192.168.0.1/32') } + let(:ip2) { to_ipaddr('192.168.0.2/32') } let(:ip_model1) do FactoryBot.create(:models_ip_address, address_str: ip1.to_s, instance: instance_model, network_name: 'fake-network') @@ -165,7 +166,7 @@ module Bosh::Director context 'when the network name saved in the database is of type Vip Static (ips in instance groups)' do let(:network_with_subnets) { [] } - let(:static_vip_network) { DeploymentPlan::VipNetwork.new('dummy', nil, [], nil) } + let(:static_vip_network) { DeploymentPlan::VipNetwork.new('dummy', nil, [], nil, nil) } before do instance_model.add_ip_address(ip_model1) @@ -195,6 +196,38 @@ module Bosh::Director end end + context 'when there are a nic_groups in db' do + let(:ip1) { to_ipaddr('192.168.0.1/32') } + let(:ip2) { to_ipaddr('192.168.0.2/32') } + + let(:ip_model1) do + FactoryBot.create(:models_ip_address, address_str: ip1.to_s, instance: instance_model, network_name: 'fake-network', nic_group: 1) + end + + let(:ip_model2) do + FactoryBot.create(:models_ip_address, address_str: ip2.to_s, instance: instance_model, network_name: 'fake-network', nic_group: 2) + end + + + context 'when there is a last VM with IP addresses' do + before do + vm1 = FactoryBot.create(:models_vm, instance: instance_model) + vm2 = FactoryBot.create(:models_vm, instance: instance_model) + + vm2.add_ip_address(ip_model1) + vm2.add_ip_address(ip_model2) + + instance_model.add_vm vm1 + instance_model.add_vm vm2 + end + + it 'creates reservations from the last VM associated with an instance' do + reservations = DeploymentPlan::InstanceNetworkReservations.create_from_db(instance_model, deployment, per_spec_logger) + expect(reservations.map(&:nic_group)).to eq([1, 2]) + end + end + end + context 'when instance has dynamic networks in spec' do let(:instance_model) { FactoryBot.create(:models_instance, deployment: deployment_model, spec: instance_spec) } let(:instance_spec) do @@ -202,14 +235,15 @@ module Bosh::Director 'networks' => { 'dynamic-network' => { 'type' => 'dynamic', - 'ip' => '10.10.0.10' + 'ip' => '10.10.0.10', + 'nic_group' => '1', } } } end let(:dynamic_network) do - DeploymentPlan::DynamicNetwork.new('dynamic-network', [], per_spec_logger) + DeploymentPlan::DynamicNetwork.new('dynamic-network', [], '32', per_spec_logger) end before do allow(deployment).to receive(:network).with('dynamic-network').and_return(dynamic_network) @@ -219,6 +253,7 @@ module Bosh::Director reservations = DeploymentPlan::InstanceNetworkReservations.create_from_db(instance_model, deployment, per_spec_logger) expect(reservations.first).to_not be_nil expect(reservations.first.ip).to eq(IPAddr.new('10.10.0.10').to_i) + expect(reservations.first.nic_group).to eq(1) end end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_factory_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_factory_spec.rb index f5c4f9a67b9..3bdf2da1489 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_factory_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_factory_spec.rb @@ -45,7 +45,7 @@ module DeploymentPlan let(:range) { IPAddr.new('192.168.1.1/24') } let(:manual_network_subnet) { ManualNetworkSubnet.new('name-7', range, nil, nil, nil, nil, nil, [], []) } - let(:network) { Bosh::Director::DeploymentPlan::ManualNetwork.new('name-7', [manual_network_subnet], per_spec_logger) } + let(:network) { Bosh::Director::DeploymentPlan::ManualNetwork.new('name-7', [manual_network_subnet], '32', per_spec_logger) } let(:ip_repo) { Bosh::Director::DeploymentPlan::IpRepo.new(per_spec_logger) } let(:deployment_plan) do instance_double( diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_spec.rb index 656ac4ac98e..c83b595b84e 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_plan_spec.rb @@ -123,7 +123,7 @@ module Bosh::Director::DeploymentPlan reservation.resolve_ip('192.168.1.3') reservation end - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } + let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az'], '32') } let(:network_plans) do [ NetworkPlanner::Plan.new(reservation: reservation, existing: true), @@ -205,8 +205,7 @@ module Bosh::Director::DeploymentPlan describe 'networks_changed?' do context 'when the instance plan has desired network plans' do - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], per_spec_logger) } + let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) do [ @@ -295,7 +294,7 @@ module Bosh::Director::DeploymentPlan allow(per_spec_logger).to receive(:debug) expect(per_spec_logger).to receive(:debug).with( 'networks_changed? obsolete reservations: ' \ - "[{type=dynamic, ip=10.0.0.5, network=existing-network, instance=#{instance_model}}]", + "[{type=dynamic, ip=10.0.0.5/32, network=existing-network, instance=#{instance_model}, nic_group=}]", ) instance_plan.networks_changed? end @@ -317,7 +316,29 @@ module Bosh::Director::DeploymentPlan allow(per_spec_logger).to receive(:debug) expect(per_spec_logger).to receive(:debug).with( 'networks_changed? desired reservations: ' \ - "[{type=dynamic, ip=10.0.0.5, network=existing-network, instance=#{instance_model}}]", + "[{type=dynamic, ip=10.0.0.5/32, network=existing-network, instance=#{instance_model}, nic_group=}]", + ) + instance_plan.networks_changed? + end + end + + context 'when there are desired plans with nic group specified' do + let(:network_plans) do + [ + NetworkPlanner::Plan.new(reservation: desired_reservation), + ] + end + let(:desired_reservation) do + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, existing_network, 1) + reservation.resolve_ip('10.0.0.5') + reservation + end + + it 'logs' do + allow(per_spec_logger).to receive(:debug) + expect(per_spec_logger).to receive(:debug).with( + 'networks_changed? desired reservations: ' \ + "[{type=dynamic, ip=10.0.0.5/32, network=existing-network, instance=#{instance_model}, nic_group=1}]", ) instance_plan.networks_changed? end @@ -335,8 +356,7 @@ module Bosh::Director::DeploymentPlan describe 'network_settings_changed?' do context 'when the instance plan has desired network plans' do - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], per_spec_logger) } + let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) do [ @@ -373,6 +393,7 @@ module Bosh::Director::DeploymentPlan 'a' => { 'type' => 'manual', 'ip' => '192.168.1.3', + 'prefix' => '32', 'netmask' => '255.255.255.0', 'cloud_properties' => {}, 'default' => %w[dns gateway], @@ -400,7 +421,7 @@ module Bosh::Director::DeploymentPlan }, } end - let(:subnet) { DynamicNetworkSubnet.new('8.8.8.8', subnet_cloud_properties, ['foo-az']) } + let(:subnet) { DynamicNetworkSubnet.new('8.8.8.8', subnet_cloud_properties, ['foo-az'], '32') } let(:network_plans) { [NetworkPlanner::Plan.new(reservation: existing_reservation, existing: true)] } let(:subnet_cloud_properties) do {} @@ -517,8 +538,8 @@ module Bosh::Director::DeploymentPlan end context 'when the vm type name has changed' do - let(:subnet) { DynamicNetworkSubnet.new(['10.0.0.1'], {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('a', [subnet], per_spec_logger) } + let(:subnet) { DynamicNetworkSubnet.new(['10.0.0.1'], {}, ['foo-az'], '32') } + let(:existing_network) { DynamicNetwork.new('a', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) { [NetworkPlanner::Plan.new(reservation: existing_reservation, existing: true)] } @@ -560,8 +581,7 @@ module Bosh::Director::DeploymentPlan context 'when the network has changed' do # everything else should be the same let(:availability_zone) { AvailabilityZone.new('foo-az', 'old' => 'value') } - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], per_spec_logger) } + let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) do @@ -891,8 +911,7 @@ module Bosh::Director::DeploymentPlan end context 'when the vm type name has changed' do - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('a', [subnet], per_spec_logger) } + let(:existing_network) { DynamicNetwork.new('a', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) { [NetworkPlanner::Plan.new(reservation: existing_reservation, existing: true)] } @@ -934,8 +953,7 @@ module Bosh::Director::DeploymentPlan context 'when the network has changed' do # everything else should be the same let(:availability_zone) { AvailabilityZone.new('foo-az', 'old' => 'value') } - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], per_spec_logger) } + let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) do @@ -1039,8 +1057,7 @@ module Bosh::Director::DeploymentPlan end context 'when the vm type name has changed' do - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('a', [subnet], per_spec_logger) } + let(:existing_network) { DynamicNetwork.new('a', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) { [NetworkPlanner::Plan.new(reservation: existing_reservation, existing: true)] } @@ -1123,8 +1140,7 @@ module Bosh::Director::DeploymentPlan context 'when the network has changed' do # everything else should be the same let(:availability_zone) { AvailabilityZone.new('foo-az', 'old' => 'value') } - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], per_spec_logger) } + let(:existing_network) { DynamicNetwork.new('existing-network', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) do @@ -1277,8 +1293,8 @@ module Bosh::Director::DeploymentPlan end describe '#persist_current_spec' do - let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az']) } - let(:existing_network) { DynamicNetwork.new('a', [subnet], per_spec_logger) } + let(:subnet) { DynamicNetworkSubnet.new('10.0.0.1', {}, ['foo-az'], '32') } + let(:existing_network) { DynamicNetwork.new('a', [subnet], '32', per_spec_logger) } let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(existing_instance, existing_network) } let(:network_plans) do @@ -1545,6 +1561,7 @@ module Bosh::Director::DeploymentPlan 'a' => { 'type' => 'manual', 'ip' => '192.168.1.3', + 'prefix' => '32', 'netmask' => '255.255.255.0', 'cloud_properties' => {}, 'dns' => ['192.168.1.1', '192.168.1.2'], @@ -2061,8 +2078,8 @@ module Bosh::Director::DeploymentPlan let(:network_plans) { [plan1, plan2, plan3, plan4] } - let(:ip1) { IPAddr.new('192.168.1.25').to_i } - let(:ip2) { IPAddr.new('192.168.1.26').to_i } + let(:ip1) { IPAddr.new('192.168.1.25') } + let(:ip2) { IPAddr.new('192.168.1.26') } let(:ip_address1) { FactoryBot.create(:models_ip_address, address_str: ip1.to_s) } let(:ip_address2) { FactoryBot.create(:models_ip_address, address_str: ip2.to_s) } diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_planner_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_planner_spec.rb index 89d856b19da..96f85746543 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_planner_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_planner_spec.rb @@ -590,7 +590,7 @@ def make_instance_with_existing_model(existing_instance_model) expect(existing_instance_plan.network_plans.size).to eq(1) vip_network_plan = existing_instance_plan.network_plans.first expect(vip_network_plan.reservation.network).to eq(vip_network) - expect(vip_network_plan.reservation.ip).to eq(ip_to_i('68.68.68.68')) + expect(vip_network_plan.reservation.ip).to eq(to_ipaddr('68.68.68.68')) end end end @@ -747,7 +747,7 @@ def make_instance_with_existing_model(existing_instance_model) before do FactoryBot.create(:models_ip_address, - address_str: ip_to_i('192.168.1.5').to_s, + address_str: to_ipaddr('192.168.1.5').to_s, network_name: 'fake-network', instance: existing_instance_model, ) @@ -764,7 +764,7 @@ def make_instance_with_existing_model(existing_instance_model) fake_job end - let(:manual_network) { Bosh::Director::DeploymentPlan::ManualNetwork.new('fake-network', [subnet], logger) } + let(:manual_network) { Bosh::Director::DeploymentPlan::ManualNetwork.new('fake-network', [subnet], nil, logger) } let(:subnet) do Bosh::Director::DeploymentPlan::ManualNetworkSubnet.new( 'fake-network', diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_repository_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_repository_spec.rb index 5d83da4aed9..08f3324801e 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_repository_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_repository_spec.rb @@ -7,7 +7,7 @@ subject(:instance_repository) { Bosh::Director::DeploymentPlan::InstanceRepository.new(logger, variables_interpolator) } let(:variables_interpolator) { instance_double(Bosh::Director::ConfigServer::VariablesInterpolator) } - let(:network) { Bosh::Director::DeploymentPlan::DynamicNetwork.new('name-7', [], logger) } + let(:network) { Bosh::Director::DeploymentPlan::DynamicNetwork.new('name-7', [], '32', logger) } let(:deployment_plan) do ip_repo = Bosh::Director::DeploymentPlan::IpRepo.new(logger) @@ -68,13 +68,15 @@ describe 'binding existing reservations' do context 'when instance has reservations in db' do + let(:ip_address) { to_ipaddr('1.1.1.1') } + before do - existing_instance.add_ip_address(FactoryBot.create(:models_ip_address, address_str: '123')) + existing_instance.add_ip_address(FactoryBot.create(:models_ip_address, address_str: ip_address.to_s)) end it 'is using reservation from database' do instance = instance_repository.fetch_existing(existing_instance, {}, desired_instance) - expect(instance.existing_network_reservations.map(&:ip)).to eq([123]) + expect(instance.existing_network_reservations.map(&:ip)).to eq([ip_address.to_s]) end end end @@ -114,7 +116,7 @@ expect(instance.state).to eq('started') expect(instance.current_job_state).to eq('stopped') expect(instance.existing_network_reservations.count).to eq(1) - expect(instance.existing_network_reservations.first.ip).to eq(ip_to_i('192.168.50.6')) + expect(instance.existing_network_reservations.first.ip).to eq(to_ipaddr('192.168.50.6')) end end @@ -200,13 +202,15 @@ end context 'binding existing reservations' do context 'when instance has reservations in db' do + let(:ip_address) { to_ipaddr('1.1.1.1') } + before do - existing_instance.add_ip_address(FactoryBot.create(:models_ip_address, address_str: '123')) + existing_instance.add_ip_address(FactoryBot.create(:models_ip_address, address_str: ip_address.to_s)) end it 'is using reservation from database' do instance = instance_repository.fetch_obsolete_existing(existing_instance, {}, deployment_plan) - expect(instance.existing_network_reservations.map(&:ip)).to eq([123]) + expect(instance.existing_network_reservations.map(&:ip)).to eq([ip_address.to_s]) end end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_spec_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_spec_spec.rb index 1516804cc62..b5896dd48ad 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_spec_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/instance_spec_spec.rb @@ -338,7 +338,7 @@ module Bosh::Director::DeploymentPlan } end let(:subnet) { ManualNetworkSubnet.parse(network_spec['name'], subnet_spec, [availability_zone]) } - let(:network) { ManualNetwork.new(network_spec['name'], [subnet], per_spec_logger) } + let(:network) { ManualNetwork.new(network_spec['name'], [subnet], nil, per_spec_logger) } it 'returns a valid instance template_spec' do network_name = network_spec['name'] diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/database_ip_repo_ipv6_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/database_ip_repo_ipv6_spec.rb index 83bf5b1fd8f..dd9078891c0 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/database_ip_repo_ipv6_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/database_ip_repo_ipv6_spec.rb @@ -1,395 +1,399 @@ require 'spec_helper' -module Bosh::Director::DeploymentPlan - describe IpRepo do - let(:ip_repo) { IpRepo.new(per_spec_logger) } - let(:instance_model) { FactoryBot.create(:models_instance) } - let(:network_spec) do - { - 'name' => 'my-manual-network', - 'subnets' => [ - { - 'range' => 'fdab:d85c:118d:8a46::/125', - 'gateway' => 'fdab:d85c:118d:8a46::1', - 'dns' => ['fdab:d85c:118d:8a46::1', 'fdab:d85c:118d:8a46::2'], - 'static' => [], - 'reserved' => [], - 'cloud_properties' => {}, - 'az' => 'az-1', - }, - ], - } - end - let(:availability_zones) { [Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-1', {})] } - let(:network) do - ManualNetwork.parse( - network_spec, - availability_zones, - per_spec_logger - ) - end - let(:subnet) do - ManualNetworkSubnet.parse( - network.name, - network_spec['subnets'].first, - availability_zones, - ) - end +module Bosh::Director + module DeploymentPlan + describe IpRepo do + include IpUtil + + let(:ip_repo) { IpRepo.new(per_spec_logger) } + let(:instance_model) { FactoryBot.create(:models_instance) } + let(:network_spec) do + { + 'name' => 'my-manual-network', + 'subnets' => [ + { + 'range' => 'fdab:d85c:118d:8a46::/125', + 'gateway' => 'fdab:d85c:118d:8a46::1', + 'dns' => ['fdab:d85c:118d:8a46::1', 'fdab:d85c:118d:8a46::2'], + 'static' => [], + 'reserved' => [], + 'cloud_properties' => {}, + 'az' => 'az-1', + }, + ], + } + end + let(:availability_zones) { [Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-1', {})] } + let(:network) do + ManualNetwork.parse( + network_spec, + availability_zones, + per_spec_logger + ) + end + let(:subnet) do + ManualNetworkSubnet.parse( + network.name, + network_spec['subnets'].first, + availability_zones, + ) + end - let(:other_network_spec) { network_spec.merge('name' => 'my-other-manual-network') } - let(:other_network) do - ManualNetwork.parse( - other_network_spec, - availability_zones, - per_spec_logger - ) - end - let(:other_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, other_network) } - let(:other_subnet) do - ManualNetworkSubnet.parse( - other_network.name, - other_network_spec['subnets'].first, - availability_zones, - ) - end + let(:other_network_spec) { network_spec.merge('name' => 'my-other-manual-network') } + let(:other_network) do + ManualNetwork.parse( + other_network_spec, + availability_zones, + per_spec_logger + ) + end + let(:other_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, other_network) } + let(:other_subnet) do + ManualNetworkSubnet.parse( + other_network.name, + other_network_spec['subnets'].first, + availability_zones, + ) + end - before { fake_job } + before { fake_job } - def cidr_ip(ip) - IPAddr.new(ip).to_i - end + def cidr_ip(ip) + to_ipaddr(ip).to_s + end - context :add do - def dynamic_reservation_with_ip(ip) - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network_without_static_pool) - reservation.resolve_ip(ip) - ip_repo.add(reservation) + context :add do + def dynamic_reservation_with_ip(ip) + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network_without_static_pool) + reservation.resolve_ip(ip) + ip_repo.add(reservation) - reservation - end + reservation + end - let(:network_without_static_pool) do - network_spec['subnets'].first['static'] = [] - ManualNetwork.parse(network_spec, availability_zones, per_spec_logger) - end + let(:network_without_static_pool) do + network_spec['subnets'].first['static'] = [] + ManualNetwork.parse(network_spec, availability_zones, per_spec_logger) + end - context 'when reservation changes type' do - context 'from Static to Dynamic' do - it 'updates type of reservation' do - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') - ip_repo.add(static_reservation) + context 'when reservation changes type' do + context 'from Static to Dynamic' do + it 'updates type of reservation' do + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] + static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') + ip_repo.add(static_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(true) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(true) - dynamic_reservation = dynamic_reservation_with_ip('fdab:d85c:118d:8a46::5') - ip_repo.add(dynamic_reservation) + dynamic_reservation = dynamic_reservation_with_ip('fdab:d85c:118d:8a46::5') + ip_repo.add(dynamic_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - new_address = Bosh::Director::Models::IpAddress.first - expect(new_address.static).to eq(false) - expect(new_address.address_str).to eq(original_address.address_str) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + new_address = Bosh::Director::Models::IpAddress.first + expect(new_address.static).to eq(false) + expect(new_address.address_str).to eq(original_address.address_str) + end end - end - context 'from Dynamic to Static' do - it 'update type of reservation' do - dynamic_reservation = dynamic_reservation_with_ip('fdab:d85c:118d:8a46::5') - ip_repo.add(dynamic_reservation) + context 'from Dynamic to Static' do + it 'update type of reservation' do + dynamic_reservation = dynamic_reservation_with_ip('fdab:d85c:118d:8a46::5') + ip_repo.add(dynamic_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(false) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(false) - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') - ip_repo.add(static_reservation) + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] + static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') + ip_repo.add(static_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - new_address = Bosh::Director::Models::IpAddress.first - expect(new_address.static).to eq(true) - expect(new_address.address_str).to eq(original_address.address_str) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + new_address = Bosh::Director::Models::IpAddress.first + expect(new_address.static).to eq(true) + expect(new_address.address_str).to eq(original_address.address_str) + end end - end - context 'from Existing to Static' do - it 'updates type of reservation' do - dynamic_reservation = dynamic_reservation_with_ip('fdab:d85c:118d:8a46::5') - ip_repo.add(dynamic_reservation) + context 'from Existing to Static' do + it 'updates type of reservation' do + dynamic_reservation = dynamic_reservation_with_ip('fdab:d85c:118d:8a46::5') + ip_repo.add(dynamic_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(false) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(false) - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - existing_reservation = Bosh::Director::ExistingNetworkReservation.new(instance_model, network, 'fdab:d85c:118d:8a46::5', 'manual') - ip_repo.add(existing_reservation) + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] + existing_reservation = Bosh::Director::ExistingNetworkReservation.new(instance_model, network, 'fdab:d85c:118d:8a46::5', 'manual') + ip_repo.add(existing_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - new_address = Bosh::Director::Models::IpAddress.first - expect(new_address.static).to eq(true) - expect(new_address.address_str).to eq(original_address.address_str) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + new_address = Bosh::Director::Models::IpAddress.first + expect(new_address.static).to eq(true) + expect(new_address.address_str).to eq(original_address.address_str) + end end end - end - context 'when reservation changes network' do - it 'updates network name' do - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') - ip_repo.add(static_reservation) + context 'when reservation changes network' do + it 'updates network name' do + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] + static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') + ip_repo.add(static_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(true) - expect(original_address.network_name).to eq(network.name) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(true) + expect(original_address.network_name).to eq(network.name) - static_reservation_on_another_network = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, other_network, 'fdab:d85c:118d:8a46::5') - ip_repo.add(static_reservation_on_another_network) + static_reservation_on_another_network = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, other_network, 'fdab:d85c:118d:8a46::5') + ip_repo.add(static_reservation_on_another_network) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(true) - expect(original_address.network_name).to eq(other_network.name) + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(true) + expect(original_address.network_name).to eq(other_network.name) + end end - end - context 'when IP is released by another deployment' do - it 'retries to reserve it' do - allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do - allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save).and_call_original + context 'when IP is released by another deployment' do + it 'retries to reserve it' do + allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do + allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save).and_call_original - raise Sequel::ValidationFailed.new('address and network_name unique') - end - - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') - ip_repo.add(reservation) + raise Sequel::ValidationFailed.new('address and network_name unique') + end - saved_address = Bosh::Director::Models::IpAddress.order(:address_str).last - expect(saved_address.address_str).to eq(cidr_ip('fdab:d85c:118d:8a46::5').to_s) - expect(saved_address.network_name).to eq('my-manual-network') - expect(saved_address.task_id).to eq('fake-task-id') - expect(saved_address.created_at).to_not be_nil + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') + ip_repo.add(reservation) + + saved_address = Bosh::Director::Models::IpAddress.order(:address_str).last + expect(saved_address.address_str).to eq(cidr_ip('fdab:d85c:118d:8a46::5').to_s) + expect(saved_address.network_name).to eq('my-manual-network') + expect(saved_address.task_id).to eq('fake-task-id') + expect(saved_address.created_at).to_not be_nil + end end - end - - context 'when reserving an IP with any previous reservation' do - it 'should fail if it reserved by a different instance' do - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - other_instance_model = FactoryBot.create(:models_instance, availability_zone: 'az-2') - original_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') - new_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(other_instance_model, network, 'fdab:d85c:118d:8a46::5') + context 'when reserving an IP with any previous reservation' do + it 'should fail if it reserved by a different instance' do + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - ip_repo.add(original_static_network_reservation) + other_instance_model = FactoryBot.create(:models_instance, availability_zone: 'az-2') + original_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') + new_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(other_instance_model, network, 'fdab:d85c:118d:8a46::5') - expect { - ip_repo.add(new_static_network_reservation) - }.to raise_error Bosh::Director::NetworkReservationAlreadyInUse - end + ip_repo.add(original_static_network_reservation) - it 'should succeed if it is reserved by the same instance' do - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] + expect { + ip_repo.add(new_static_network_reservation) + }.to raise_error Bosh::Director::NetworkReservationAlreadyInUse + end - static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') + it 'should succeed if it is reserved by the same instance' do + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - ip_repo.add(static_network_reservation) + static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') - expect { ip_repo.add(static_network_reservation) - }.not_to raise_error + + expect { + ip_repo.add(static_network_reservation) + }.not_to raise_error + end end end - end - describe :allocate_dynamic_ip do - let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } + describe :allocate_dynamic_ip do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } - context 'when there are no IPs reserved' do - it 'returns the first in the range' do - ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) + context 'when there are no IPs reserved' do + it 'returns the first in the range' do + ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) - expected_ip_address = cidr_ip('fdab:d85c:118d:8a46::2') - expect(ip_address).to eq(expected_ip_address) + expected_ip_address = cidr_ip('fdab:d85c:118d:8a46::2') + expect(ip_address).to eq(expected_ip_address) + end end - end - it 'reserves IP as dynamic' do - ip_repo.allocate_dynamic_ip(reservation, subnet) + it 'reserves IP as dynamic' do + ip_repo.allocate_dynamic_ip(reservation, subnet) - saved_address = Bosh::Director::Models::IpAddress.first - expect(saved_address.static).to eq(false) - end + saved_address = Bosh::Director::Models::IpAddress.first + expect(saved_address.static).to eq(false) + end - context 'when reserving more than one ip' do - it 'should reserve the next available address' do - first = ip_repo.allocate_dynamic_ip(reservation, subnet) - second = ip_repo.allocate_dynamic_ip(reservation, subnet) - expect(first).to eq(cidr_ip('fdab:d85c:118d:8a46::2')) - expect(second).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) + context 'when reserving more than one ip' do + it 'should reserve the next available address' do + first = ip_repo.allocate_dynamic_ip(reservation, subnet) + second = ip_repo.allocate_dynamic_ip(reservation, subnet) + expect(first).to eq(cidr_ip('fdab:d85c:118d:8a46::2')) + expect(second).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) + end end - end - context 'when there are restricted ips' do - it 'does not reserve them' do - network_spec['subnets'].first['reserved'] = ['fdab:d85c:118d:8a46::2', 'fdab:d85c:118d:8a46::4'] + context 'when there are restricted ips' do + it 'does not reserve them' do + network_spec['subnets'].first['reserved'] = ['fdab:d85c:118d:8a46::2', 'fdab:d85c:118d:8a46::4'] - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::5')) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::5')) + end end - end - context 'when there are static and restricted ips' do - it 'does not reserve them' do - network_spec['subnets'].first['reserved'] = ['fdab:d85c:118d:8a46::2'] - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::4'] + context 'when there are static and restricted ips' do + it 'does not reserve them' do + network_spec['subnets'].first['reserved'] = ['fdab:d85c:118d:8a46::2'] + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::4'] - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::5')) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::5')) + end end - end - context 'when there are available IPs between reserved IPs' do - it 'returns first non-reserved IP' do - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::2', 'fdab:d85c:118d:8a46::4'] + context 'when there are available IPs between reserved IPs' do + it 'returns first non-reserved IP' do + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::2', 'fdab:d85c:118d:8a46::4'] - reservation_1 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::2') - reservation_2 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::4') + reservation_1 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::2') + reservation_2 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::4') - ip_repo.add(reservation_1) - ip_repo.add(reservation_2) + ip_repo.add(reservation_1) + ip_repo.add(reservation_2) - reservation_3 = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) - ip_address = ip_repo.allocate_dynamic_ip(reservation_3, subnet) + reservation_3 = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) + ip_address = ip_repo.allocate_dynamic_ip(reservation_3, subnet) - expect(ip_address).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) + expect(ip_address).to eq(cidr_ip('fdab:d85c:118d:8a46::3')) + end end - end - context 'when all IPs in the range are taken' do - it 'returns nil' do - network_spec['subnets'].first['range'] = 'fdab:d85c:118d:8a46::0/126' + context 'when all IPs in the range are taken' do + it 'returns nil' do + network_spec['subnets'].first['range'] = 'fdab:d85c:118d:8a46::0/126' - ip_repo.allocate_dynamic_ip(reservation, subnet) + ip_repo.allocate_dynamic_ip(reservation, subnet) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil + end end - end - - context 'when there are IPs reserved by other networks with overlapping subnet' do - it 'returns the next non-reserved IP' do - ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - expected_ip_address = cidr_ip('fdab:d85c:118d:8a46::2') - expect(ip_address).to eq(expected_ip_address) + context 'when there are IPs reserved by other networks with overlapping subnet' do + it 'returns the next non-reserved IP' do + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) + expected_ip_address = cidr_ip('fdab:d85c:118d:8a46::2') + expect(ip_address).to eq(expected_ip_address) - expected_ip_address = cidr_ip('fdab:d85c:118d:8a46::3') - expect(ip_address).to eq(expected_ip_address) - end - end + ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) - context 'when reserving IP fails' do - def fail_saving_ips(ips, fail_error) - original_saves = {} - ips.each do |ip| - ip_address = Bosh::Director::Models::IpAddress.new( - address_str: ip.to_s, - network_name: 'my-manual-network', - instance: instance_model, - task_id: Bosh::Director::Config.current_job.task_id - ) - original_save = ip_address.method(:save) - original_saves[ip.to_s] = original_save - end - - allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do |model| - if ips.map(&:to_s).include?(model.address_str) - original_save = original_saves[model.address_str] - original_save.call - raise fail_error - end - model + expected_ip_address = cidr_ip('fdab:d85c:118d:8a46::3') + expect(ip_address).to eq(expected_ip_address) end end - shared_examples :retries_on_race_condition do - context 'when allocating some IPs fails' do - before do - network_spec['subnets'].first['range'] = 'fdab:d85c:118d:8a46::0/125' - - fail_saving_ips([ - cidr_ip('fdab:d85c:118d:8a46::2'), - cidr_ip('fdab:d85c:118d:8a46::3'), - cidr_ip('fdab:d85c:118d:8a46::4'), - ], - fail_error + context 'when reserving IP fails' do + def fail_saving_ips(ips, fail_error) + original_saves = {} + ips.each do |ip| + ip_address = Bosh::Director::Models::IpAddress.new( + address_str: ip.to_s, + network_name: 'my-manual-network', + instance: instance_model, + task_id: Bosh::Director::Config.current_job.task_id ) + original_save = ip_address.method(:save) + original_saves[ip.to_s] = original_save end - it 'retries until it succeeds' do - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::5')) + allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do |model| + if ips.map(&:to_s).include?(model.address_str) + original_save = original_saves[model.address_str] + original_save.call + raise fail_error + end + model end end - context 'when allocating any IP fails' do - before do - network_spec['subnets'].first['range'] = 'fdab:d85c:118d:8a46::0/125' - network_spec['subnets'].first['reserved'] = ['fdab:d85c:118d:8a46::5', 'fdab:d85c:118d:8a46::6'] - - fail_saving_ips([ - cidr_ip('fdab:d85c:118d:8a46::2'), - cidr_ip('fdab:d85c:118d:8a46::3'), - cidr_ip('fdab:d85c:118d:8a46::4') - ], - fail_error - ) + shared_examples :retries_on_race_condition do + context 'when allocating some IPs fails' do + before do + network_spec['subnets'].first['range'] = 'fdab:d85c:118d:8a46::0/125' + + fail_saving_ips([ + cidr_ip('fdab:d85c:118d:8a46::2'), + cidr_ip('fdab:d85c:118d:8a46::3'), + cidr_ip('fdab:d85c:118d:8a46::4'), + ], + fail_error + ) + end + + it 'retries until it succeeds' do + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('fdab:d85c:118d:8a46::5')) + end end - it 'retries until there are no more IPs available' do - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil + context 'when allocating any IP fails' do + before do + network_spec['subnets'].first['range'] = 'fdab:d85c:118d:8a46::0/125' + network_spec['subnets'].first['reserved'] = ['fdab:d85c:118d:8a46::5', 'fdab:d85c:118d:8a46::6'] + + fail_saving_ips([ + cidr_ip('fdab:d85c:118d:8a46::2'), + cidr_ip('fdab:d85c:118d:8a46::3'), + cidr_ip('fdab:d85c:118d:8a46::4') + ], + fail_error + ) + end + + it 'retries until there are no more IPs available' do + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil + end end end - end - context 'when sequel validation errors' do - let(:fail_error) { Sequel::ValidationFailed.new('address and network are not unique') } + context 'when sequel validation errors' do + let(:fail_error) { Sequel::ValidationFailed.new('address and network are not unique') } - it_behaves_like :retries_on_race_condition - end + it_behaves_like :retries_on_race_condition + end - context 'when postgres unique errors' do - let(:fail_error) { Sequel::DatabaseError.new('duplicate key value violates unique constraint') } + context 'when postgres unique errors' do + let(:fail_error) { Sequel::DatabaseError.new('duplicate key value violates unique constraint') } - it_behaves_like :retries_on_race_condition - end + it_behaves_like :retries_on_race_condition + end - context 'when mysql unique errors' do - let(:fail_error) { Sequel::DatabaseError.new('Duplicate entry') } + context 'when mysql unique errors' do + let(:fail_error) { Sequel::DatabaseError.new('Duplicate entry') } - it_behaves_like :retries_on_race_condition + it_behaves_like :retries_on_race_condition + end end end - end - describe :delete do - before do - network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] + describe :delete do + before do + network_spec['subnets'].first['static'] = ['fdab:d85c:118d:8a46::5'] - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') - ip_repo.add(reservation) - end + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, 'fdab:d85c:118d:8a46::5') + ip_repo.add(reservation) + end - it 'deletes IP address' do - expect { - ip_repo.delete('fdab:d85c:118d:8a46::5') - }.to change { - Bosh::Director::Models::IpAddress.all.size - }.by(-1) + it 'deletes IP address' do + expect { + ip_repo.delete('fdab:d85c:118d:8a46::5') + }.to change { + Bosh::Director::Models::IpAddress.all.size + }.by(-1) + end end end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_provider_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_provider_spec.rb index 23b00411e7f..63aa454b979 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_provider_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_provider_spec.rb @@ -1,516 +1,519 @@ require 'spec_helper' -module Bosh::Director::DeploymentPlan - describe IpProvider do - let(:instance_model) { FactoryBot.create(:models_instance) } - let(:deployment_plan) { instance_double(Planner, name: 'fake-deployment') } - let(:networks) do - { 'my-manual-network' => manual_network } - end - let(:manual_network_spec) do - { - 'name' => 'my-manual-network', - 'subnets' => [ - { - 'range' => '192.168.1.0/30', - 'gateway' => '192.168.1.1', - 'dns' => ['192.168.1.1', '192.168.1.2'], - 'static' => [], - 'reserved' => [], - 'cloud_properties' => {}, - 'az' => 'az-1', - }, - { - 'range' => '192.168.2.0/30', - 'gateway' => '192.168.2.1', - 'dns' => ['192.168.2.1', '192.168.2.2'], - 'static' => [], - 'reserved' => [], - 'cloud_properties' => {}, - 'az' => 'az-2', - }, - { - 'range' => '192.168.3.0/30', - 'gateway' => '192.168.3.1', - 'dns' => ['192.168.3.1', '192.168.3.2'], - 'static' => [], - 'reserved' => [], - 'cloud_properties' => {}, - 'azs' => ['az-2'], - }, - ], - } - end - let(:manual_network) do - ManualNetwork.parse( - manual_network_spec, - [ - Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-1', {}), - Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-2', {}) - ], - per_spec_logger - ) - end - let(:another_manual_network) do - ManualNetwork.parse( +module Bosh::Director + module DeploymentPlan + describe IpProvider do + include IpUtil + let(:instance_model) { FactoryBot.create(:models_instance) } + let(:deployment_plan) { instance_double(Planner, name: 'fake-deployment') } + let(:networks) do + { 'my-manual-network' => manual_network } + end + let(:manual_network_spec) do { - 'name' => 'my-another-network', + 'name' => 'my-manual-network', 'subnets' => [ { - 'range' => '192.168.1.0/24', + 'range' => '192.168.1.0/30', 'gateway' => '192.168.1.1', - } - ] - }, - [], - per_spec_logger - ) - end - let(:vip_network_spec) do - { - 'name' => 'my-vip-network', - 'type' => 'vip', - } - end - let(:vip_network) { VipNetwork.parse(vip_network_spec, [], per_spec_logger) } - let(:ip_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) } - - before do - Bosh::Director::Config.current_job = Bosh::Director::Jobs::BaseJob.new - Bosh::Director::Config.current_job.task_id = 'fake-task-id' - end - - describe 'with a database-backed repo' do - let(:ip_repo) do - instance_double( - IpRepo, - add: nil, - allocate_vip_ip: ip, - allocate_dynamic_ip: ip, + 'dns' => ['192.168.1.1', '192.168.1.2'], + 'static' => [], + 'reserved' => [], + 'cloud_properties' => {}, + 'az' => 'az-1', + }, + { + 'range' => '192.168.2.0/30', + 'gateway' => '192.168.2.1', + 'dns' => ['192.168.2.1', '192.168.2.2'], + 'static' => [], + 'reserved' => [], + 'cloud_properties' => {}, + 'az' => 'az-2', + }, + { + 'range' => '192.168.3.0/30', + 'gateway' => '192.168.3.1', + 'dns' => ['192.168.3.1', '192.168.3.2'], + 'static' => [], + 'reserved' => [], + 'cloud_properties' => {}, + 'azs' => ['az-2'], + }, + ], + } + end + let(:manual_network) do + ManualNetwork.parse( + manual_network_spec, + [ + Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-1', {}), + Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-2', {}) + ], + per_spec_logger ) end - let(:ip) { Bosh::Director::IpAddrOrCidr.new('1.1.1.1') } - let(:ip_provider) { IpProvider.new(ip_repo, networks, per_spec_logger) } - - describe :release do - context 'when reservation does not have an IP' do - it 'should raise an error' do - expect do - ip_provider.release(ip_reservation) - end.to raise_error(Bosh::Director::NetworkReservationIpMissing, "Can't release reservation without an IP") - end + let(:another_manual_network) do + ManualNetwork.parse( + { + 'name' => 'my-another-network', + 'subnets' => [ + { + 'range' => '192.168.1.0/24', + 'gateway' => '192.168.1.1', + } + ] + }, + [], + per_spec_logger + ) + end + let(:vip_network_spec) do + { + 'name' => 'my-vip-network', + 'type' => 'vip', + } + end + let(:vip_network) { VipNetwork.parse(vip_network_spec, [], per_spec_logger) } + let(:ip_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) } + + before do + Bosh::Director::Config.current_job = Bosh::Director::Jobs::BaseJob.new + Bosh::Director::Config.current_job.task_id = 'fake-task-id' + end - context 'when reservation is on dynamic network with no IP address' do - it 'does not fail to release it' do - dynamic_network = DynamicNetwork.new('my-manual-network', [], per_spec_logger) - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, dynamic_network) + describe 'with a database-backed repo' do + let(:ip_repo) do + instance_double( + IpRepo, + add: nil, + allocate_vip_ip: ip, + allocate_dynamic_ip: ip, + ) + end + let(:ip) { to_ipaddr('1.1.1.1') } + let(:ip_provider) { IpProvider.new(ip_repo, networks, per_spec_logger) } + describe :release do + context 'when reservation does not have an IP' do + it 'should raise an error' do expect do - ip_provider.release(reservation) - end.to_not raise_error + ip_provider.release(ip_reservation) + end.to raise_error(Bosh::Director::NetworkReservationIpMissing, "Can't release reservation without an IP") end - end - end - context 'when reservation has an IP' do - it 'should release IP' do - allow(ip_repo).to receive(:delete) + context 'when reservation is on dynamic network with no IP address' do + it 'does not fail to release it' do + dynamic_network = DynamicNetwork.new('my-manual-network', [], nil, per_spec_logger) + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, dynamic_network) - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.1.2') - expect do - ip_provider.release(reservation) - end.not_to raise_error - expect(ip_repo).to have_received(:delete) + expect do + ip_provider.release(reservation) + end.to_not raise_error + end + end end - end - end - describe :reserve_existing_ips do - context 'when dynamic network' do - let(:dynamic_network) { Bosh::Director::DeploymentPlan::DynamicNetwork.new('fake-dynamic-network', [], per_spec_logger) } - let(:existing_network_reservation) do - Bosh::Director::ExistingNetworkReservation.new( - instance_model, - dynamic_network, - '192.168.1.2', - 'dynamic', - ) - end + context 'when reservation has an IP' do + it 'should release IP' do + allow(ip_repo).to receive(:delete) - it 'sets the reservation type to the network type' do - ip_provider.reserve_existing_ips(existing_network_reservation) - expect(existing_network_reservation.dynamic?).to be_truthy + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.1.2') + expect do + ip_provider.release(reservation) + end.not_to raise_error + expect(ip_repo).to have_received(:delete) + end end end - context 'when vip network' do - let(:existing_network_reservation) do - Bosh::Director::ExistingNetworkReservation.new(instance_model, static_vip_network, '69.69.69.69', 'vip') - end - let(:static_vip_network) { Bosh::Director::DeploymentPlan::VipNetwork.parse({ 'name' => 'fake-network' }, [], per_spec_logger) } + describe :reserve_existing_ips do + context 'when dynamic network' do + let(:dynamic_network) { Bosh::Director::DeploymentPlan::DynamicNetwork.new('fake-dynamic-network', [], nil, per_spec_logger) } + let(:existing_network_reservation) do + Bosh::Director::ExistingNetworkReservation.new( + instance_model, + dynamic_network, + '192.168.1.2', + 'dynamic', + ) + end - it 'saves the ip' do - ip_provider.reserve_existing_ips(existing_network_reservation) - expect(existing_network_reservation.static?).to be_truthy - expect(ip_repo).to have_received(:add).with(existing_network_reservation) + it 'sets the reservation type to the network type' do + ip_provider.reserve_existing_ips(existing_network_reservation) + expect(existing_network_reservation.dynamic?).to be_truthy + end end - end - context 'when manual network' do - let(:existing_network_reservation) do - Bosh::Director::ExistingNetworkReservation.new( - instance_model, - manual_network, - '192.168.1.2', - 'manual', - ) - end + context 'when vip network' do + let(:existing_network_reservation) do + Bosh::Director::ExistingNetworkReservation.new(instance_model, static_vip_network, '69.69.69.69', 'vip') + end + let(:static_vip_network) { Bosh::Director::DeploymentPlan::VipNetwork.parse({ 'name' => 'fake-network' }, [], per_spec_logger) } - context 'when IP is a static IP' do - it 'should reserve IP as static' do - manual_network_spec['subnets'].first['static'] = ['192.168.1.2'] + it 'saves the ip' do ip_provider.reserve_existing_ips(existing_network_reservation) - expect(existing_network_reservation.static?).to be_truthy + expect(ip_repo).to have_received(:add).with(existing_network_reservation) end end - context 'when IP is a dynamic IP' do - it 'should reserve IP as dynamic' do - ip_provider.reserve_existing_ips(existing_network_reservation) + context 'when manual network' do + let(:existing_network_reservation) do + Bosh::Director::ExistingNetworkReservation.new( + instance_model, + manual_network, + '192.168.1.2/32', + 'manual', + ) + end - expect(existing_network_reservation.dynamic?).to be_truthy + context 'when IP is a static IP' do + it 'should reserve IP as static' do + manual_network_spec['subnets'].first['static'] = ['192.168.1.2'] + ip_provider.reserve_existing_ips(existing_network_reservation) + + expect(existing_network_reservation.static?).to be_truthy + end end - end - context 'when there are 2 networks with the same subnet but different reserved ranges' do - let(:manual_network_spec) do - { - 'name' => 'my-manual-network', - 'subnets' => [ - { - 'range' => '192.168.1.0/24', - 'gateway' => '192.168.1.1', - 'dns' => ['192.168.1.1', '192.168.1.2'], - 'reserved' => ['192.168.1.2-192.168.1.30'], - }, - ], - } + context 'when IP is a dynamic IP' do + it 'should reserve IP as dynamic' do + ip_provider.reserve_existing_ips(existing_network_reservation) + + expect(existing_network_reservation.dynamic?).to be_truthy + end end - let(:another_manual_network) do - ManualNetwork.parse( + context 'when there are 2 networks with the same subnet but different reserved ranges' do + let(:manual_network_spec) do { - 'name' => 'my-another-network', + 'name' => 'my-manual-network', 'subnets' => [ { 'range' => '192.168.1.0/24', 'gateway' => '192.168.1.1', 'dns' => ['192.168.1.1', '192.168.1.2'], - 'reserved' => ['192.168.1.2-192.168.1.40'], + 'reserved' => ['192.168.1.2-192.168.1.30'], }, ], - }, - [], - per_spec_logger, - ) - end + } + end - let(:networks) do - { - 'my-manual-network' => manual_network, - 'my-another-network' => another_manual_network, - } - end + let(:another_manual_network) do + ManualNetwork.parse( + { + 'name' => 'my-another-network', + 'subnets' => [ + { + 'range' => '192.168.1.0/24', + 'gateway' => '192.168.1.1', + 'dns' => ['192.168.1.1', '192.168.1.2'], + 'reserved' => ['192.168.1.2-192.168.1.40'], + }, + ], + }, + [], + per_spec_logger, + ) + end - let(:existing_network_reservation) do - Bosh::Director::ExistingNetworkReservation.new( - instance_model, - another_manual_network, - '192.168.1.41', - 'manual', - ) - end + let(:networks) do + { + 'my-manual-network' => manual_network, + 'my-another-network' => another_manual_network, + } + end - it 'should keep existing IP on existing network (it should not switch to a different network)' do - ip_provider.reserve_existing_ips(existing_network_reservation) + let(:existing_network_reservation) do + Bosh::Director::ExistingNetworkReservation.new( + instance_model, + another_manual_network, + '192.168.1.41', + 'manual', + ) + end + + it 'should keep existing IP on existing network (it should not switch to a different network)' do + ip_provider.reserve_existing_ips(existing_network_reservation) - expect(existing_network_reservation.network.name).to eq('my-another-network') + expect(existing_network_reservation.network.name).to eq('my-another-network') + end end end end - end - describe :reserve do - context 'when ManualNetwork' do - context 'when IP is provided' do - context 'when reservation does not belong to any subnet' do - context 'when dynamic network reservation' do - let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) } - before { reservation.resolve_ip('192.168.2.6') } + describe :reserve do + context 'when ManualNetwork' do + context 'when IP is provided' do + context 'when reservation does not belong to any subnet' do + context 'when dynamic network reservation' do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) } + before { reservation.resolve_ip('192.168.2.6') } - it 'raises NetworkReservationIpOutsideSubnet' do - expect { - ip_provider.reserve(reservation) - }.to raise_error Bosh::Director::NetworkReservationIpOutsideSubnet + it 'raises NetworkReservationIpOutsideSubnet' do + expect { + ip_provider.reserve(reservation) + }.to raise_error Bosh::Director::NetworkReservationIpOutsideSubnet + end end - end - context 'when static network reservation' do - let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.2.6') } + context 'when static network reservation' do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.2.6') } - it 'raises NetworkReservationIpOutsideSubnet' do - expect { - ip_provider.reserve(reservation) - }.to raise_error Bosh::Director::NetworkReservationIpOutsideSubnet + it 'raises NetworkReservationIpOutsideSubnet' do + expect { + ip_provider.reserve(reservation) + }.to raise_error Bosh::Director::NetworkReservationIpOutsideSubnet + end end end - end - context 'when reservation belongs to subnet' do - context 'when it is a dynamic reservation' do - it 'reserves reservation' do - manual_network_spec['subnets'].first['range'] = '192.168.1.0/24' + context 'when reservation belongs to subnet' do + context 'when it is a dynamic reservation' do + it 'reserves reservation' do + manual_network_spec['subnets'].first['range'] = '192.168.1.0/24' - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) - reservation.resolve_ip('192.168.1.6') - reservation.instance_model.update(availability_zone: 'az-1') + reservation.resolve_ip('192.168.1.6') + reservation.instance_model.update(availability_zone: 'az-1') - ip_provider.reserve(reservation) - expect(reservation.ip).to eq(IPAddr.new('192.168.1.6').to_i) + ip_provider.reserve(reservation) + expect(reservation.ip).to eq(IPAddr.new('192.168.1.6').to_i) + end + + context 'when that IP is now in the reserved range' do + before do + manual_network_spec['subnets'].first['range'] = '192.168.1.0/24' + manual_network_spec['subnets'].first['reserved'] = ['192.168.1.11'] + end + + it 'raises an error' do + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) + reservation.resolve_ip(to_ipaddr('192.168.1.11')) + expect do + ip_provider.reserve(reservation) + end.to raise_error Bosh::Director::NetworkReservationIpReserved, + "Failed to reserve IP '192.168.1.11/32' for network 'my-manual-network': IP belongs to "\ + 'reserved range' + end + end + + context 'when user accidentally includes a static IP in the range' do + it 'raises an error' do + manual_network_spec['subnets'].first['static'] = ['192.168.1.2'] + + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) + reservation.resolve_ip('192.168.1.2') + expect { + ip_provider.reserve(reservation) + }.to raise_error Bosh::Director::NetworkReservationWrongType, + "IP '192.168.1.2/32' on network 'my-manual-network' does not belong to dynamic pool" + end + end end - context 'when that IP is now in the reserved range' do + context 'when it is a static reservation' do before do manual_network_spec['subnets'].first['range'] = '192.168.1.0/24' - manual_network_spec['subnets'].first['reserved'] = ['192.168.1.11'] + manual_network_spec['subnets'].first['static'] = ['192.168.1.5'] end + let(:static_network_reservation) { Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.1.5') } - it 'raises an error' do - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) - reservation.resolve_ip(IPAddr.new('192.168.1.11').to_i) - expect do - ip_provider.reserve(reservation) - end.to raise_error Bosh::Director::NetworkReservationIpReserved, - "Failed to reserve IP '192.168.1.11' for network 'my-manual-network': IP belongs to "\ - 'reserved range' + it 'should reserve static IPs' do + expect { + ip_provider.reserve(static_network_reservation) + }.to_not raise_error + end + + context 'when IP is in reserved range' do + before do + manual_network_spec['subnets'].first['range'] = '192.168.1.0/24' + manual_network_spec['subnets'].first['reserved'] = ['192.168.1.11'] + end + + it 'when IP is in reserved range, raises NetworkReservationIpReserved' do + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.1.11') + expect { + ip_provider.reserve(reservation) + }.to raise_error Bosh::Director::NetworkReservationIpReserved, + "Failed to reserve IP '192.168.1.11/32' for network 'my-manual-network': IP belongs to reserved range" + end + end + + context 'when user accidentally assigns an IP to a job that is NOT a static IP' do + it 'raises an error' do + manual_network_spec['subnets'].first['static'] = ['192.168.1.2'] + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) + reservation.resolve_ip('192.168.1.2') + expect { + ip_provider.reserve(reservation) + }.to raise_error Bosh::Director::NetworkReservationWrongType, + "IP '192.168.1.2/32' on network 'my-manual-network' does not belong to dynamic pool" + end end end + end - context 'when user accidentally includes a static IP in the range' do - it 'raises an error' do - manual_network_spec['subnets'].first['static'] = ['192.168.1.2'] + context 'when there are several networks that have overlapping subnet ranges that include reservation IP' do + let(:networks) do + { + 'my-manual-network' => manual_network, + 'my-another-network' => another_manual_network, + } + end + let(:reservation) do + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) + reservation.resolve_ip('192.168.1.6') + reservation + end + let(:manual_network_spec) do + { + 'name' => 'my-manual-network', + 'subnets' => [ + { + 'range' => '192.168.1.0/24', + 'gateway' => '192.168.1.1', + 'reserved' => manual_network_reserved, + } + ] + } + end + let(:manual_network_reserved) { [] } - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) - reservation.resolve_ip('192.168.1.2') + context 'when reservation network has a subnet that includes the reservation IP' do + it 'saves the ip' do + ip_provider.reserve(reservation) + expect(ip_repo).to have_received(:add).with(reservation) + end + end + + context 'when reservation network does not have subnet that includes reservation IP' do + let(:manual_network_reserved) { ['192.168.1.6'] } + it 'fails to reserve the reservation' do expect { ip_provider.reserve(reservation) - }.to raise_error Bosh::Director::NetworkReservationWrongType, - "IP '192.168.1.2' on network 'my-manual-network' does not belong to dynamic pool" + }.to raise_error Bosh::Director::NetworkReservationIpReserved, "Failed to reserve IP '192.168.1.6/32' for network 'my-manual-network': IP belongs to reserved range" end end end + end - context 'when it is a static reservation' do - before do - manual_network_spec['subnets'].first['range'] = '192.168.1.0/24' - manual_network_spec['subnets'].first['static'] = ['192.168.1.5'] - end - let(:static_network_reservation) { Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.1.5') } + context 'when IP is not provided' do + context 'for dynamic reservation' do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) } + + context 'when the instance does not specify an az' do + before do + allow(ip_repo).to receive(:allocate_dynamic_ip).and_return(nil, nil, ip) + end + + it 'tries to allocate an IP in all of the network subnets' do + ip_provider.reserve(reservation) - it 'should reserve static IPs' do - expect { - ip_provider.reserve(static_network_reservation) - }.to_not raise_error + expect(ip_repo).to have_received(:allocate_dynamic_ip).exactly(3).times + end end - context 'when IP is in reserved range' do + context 'when the instance specifies an AZ' do before do - manual_network_spec['subnets'].first['range'] = '192.168.1.0/24' - manual_network_spec['subnets'].first['reserved'] = ['192.168.1.11'] + allow(ip_repo).to receive(:allocate_dynamic_ip).and_return(nil, ip) end - it 'when IP is in reserved range, raises NetworkReservationIpReserved' do - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, manual_network, '192.168.1.11') - expect { - ip_provider.reserve(reservation) - }.to raise_error Bosh::Director::NetworkReservationIpReserved, - "Failed to reserve IP '192.168.1.11' for network 'my-manual-network': IP belongs to reserved range" + it 'tries to allocate dynamic IPs across multiple subnets that match the az' do + instance_model.update(availability_zone: 'az-2') + ip_provider.reserve(reservation) + + expect(ip_repo).to have_received(:allocate_dynamic_ip).twice end end - context 'when user accidentally assigns an IP to a job that is NOT a static IP' do - it 'raises an error' do - manual_network_spec['subnets'].first['static'] = ['192.168.1.2'] - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) - reservation.resolve_ip('192.168.1.2') + context 'when no subnet has enough capacity to allocate a dynamic IP' do + let(:ip) { nil } + it 'raises NetworkReservationNotEnoughCapacity' do expect { ip_provider.reserve(reservation) - }.to raise_error Bosh::Director::NetworkReservationWrongType, - "IP '192.168.1.2' on network 'my-manual-network' does not belong to dynamic pool" + }.to raise_error Bosh::Director::NetworkReservationNotEnoughCapacity end end end end + end - context 'when there are several networks that have overlapping subnet ranges that include reservation IP' do - let(:networks) do + context 'when VipNetwork' do + context 'when globally allocating vips' do + let(:vip_network_spec) do { - 'my-manual-network' => manual_network, - 'my-another-network' => another_manual_network, - } - end - let(:reservation) do - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) - reservation.resolve_ip('192.168.1.6') - reservation - end - let(:manual_network_spec) do - { - 'name' => 'my-manual-network', + 'name' => 'my-vip-network', + 'type' => 'vip', 'subnets' => [ { - 'range' => '192.168.1.0/24', - 'gateway' => '192.168.1.1', - 'reserved' => manual_network_reserved, - } - ] + 'static' => ['1.1.1.1', '2.2.2.2'], + }, + { + 'static' => ['3.3.3.3', '4.4.4.4'], + }, + ], } end - let(:manual_network_reserved) { [] } - - context 'when reservation network has a subnet that includes the reservation IP' do - it 'saves the ip' do - ip_provider.reserve(reservation) - expect(ip_repo).to have_received(:add).with(reservation) - end - end - - context 'when reservation network does not have subnet that includes reservation IP' do - let(:manual_network_reserved) { ['192.168.1.6'] } - it 'fails to reserve the reservation' do - expect { - ip_provider.reserve(reservation) - }.to raise_error Bosh::Director::NetworkReservationIpReserved, "Failed to reserve IP '192.168.1.6' for network 'my-manual-network': IP belongs to reserved range" - end - end - end - end - context 'when IP is not provided' do - context 'for dynamic reservation' do - let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, manual_network) } - - context 'when the instance does not specify an az' do - before do - allow(ip_repo).to receive(:allocate_dynamic_ip).and_return(nil, nil, ip) + context 'when the reservation already exists' do + let(:reservation) do + Bosh::Director::ExistingNetworkReservation.new( + instance_model, + vip_network, + '1.1.1.1', + 'vip', + ) end - it 'tries to allocate an IP in all of the network subnets' do + it 'adds the ip address to the ip repository' do ip_provider.reserve(reservation) - - expect(ip_repo).to have_received(:allocate_dynamic_ip).exactly(3).times + expect(reservation.ip).to eq('1.1.1.1') end end - context 'when the instance specifies an AZ' do - before do - allow(ip_repo).to receive(:allocate_dynamic_ip).and_return(nil, ip) - end + context 'when a new reservation is needed' do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, vip_network) } - it 'tries to allocate dynamic IPs across multiple subnets that match the az' do - instance_model.update(availability_zone: 'az-2') + it 'allocates an ip address for the reservation' do ip_provider.reserve(reservation) - - expect(ip_repo).to have_received(:allocate_dynamic_ip).twice + expect(reservation.ip).to eq('1.1.1.1') end - end - - context 'when no subnet has enough capacity to allocate a dynamic IP' do - let(:ip) { nil } - it 'raises NetworkReservationNotEnoughCapacity' do - expect { - ip_provider.reserve(reservation) - }.to raise_error Bosh::Director::NetworkReservationNotEnoughCapacity - end - end - end - end - end - - context 'when VipNetwork' do - context 'when globally allocating vips' do - let(:vip_network_spec) do - { - 'name' => 'my-vip-network', - 'type' => 'vip', - 'subnets' => [ - { - 'static' => ['1.1.1.1', '2.2.2.2'], - }, - { - 'static' => ['3.3.3.3', '4.4.4.4'], - }, - ], - } - end - context 'when the reservation already exists' do - let(:reservation) do - Bosh::Director::ExistingNetworkReservation.new( - instance_model, - vip_network, - '1.1.1.1', - 'vip', - ) - end - - it 'adds the ip address to the ip repository' do - ip_provider.reserve(reservation) - expect(reservation.ip).to eq(IPAddr.new('1.1.1.1').to_i) - end - end - - context 'when a new reservation is needed' do - let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, vip_network) } - - it 'allocates an ip address for the reservation' do - ip_provider.reserve(reservation) - expect(reservation.ip).to eq(IPAddr.new('1.1.1.1').to_i) - end - - context 'and there are no available vips' do - let(:vip_network_spec) do - { - 'name' => 'my-vip-network', - 'type' => 'vip', - 'subnets' => [ - { - 'static' => [], - }, - ], - } - end - let(:ip) { nil } + context 'and there are no available vips' do + let(:vip_network_spec) do + { + 'name' => 'my-vip-network', + 'type' => 'vip', + 'subnets' => [ + { + 'static' => [], + }, + ], + } + end + let(:ip) { nil } - it 'raises an error' do - expect do - ip_provider.reserve(reservation) - end.to raise_error(Bosh::Director::NetworkReservationNotEnoughCapacity, - /Failed to reserve IP for '.+' for vip network 'my-vip-network': no more available/) + it 'raises an error' do + expect do + ip_provider.reserve(reservation) + end.to raise_error(Bosh::Director::NetworkReservationNotEnoughCapacity, + /Failed to reserve IP for '.+' for vip network 'my-vip-network': no more available/) + end end end end - end - context 'when IP is provided and can be reserved' do - it 'reserves the IP as a StaticNetworkReservation' do - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, vip_network, '192.168.1.2') + context 'when IP is provided and can be reserved' do + it 'reserves the IP as a StaticNetworkReservation' do + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, vip_network, '192.168.1.2') - expect do - ip_provider.reserve(reservation) - end.not_to raise_error + expect do + ip_provider.reserve(reservation) + end.not_to raise_error + end end end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_repo_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_repo_spec.rb index 946047a9ce9..275bf297d7e 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_repo_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/ip_provider/ip_repo_spec.rb @@ -1,81 +1,162 @@ require 'spec_helper' -module Bosh::Director::DeploymentPlan - describe IpRepo do - let(:ip_repo) { IpRepo.new(per_spec_logger) } - let(:instance_model) { FactoryBot.create(:models_instance) } - let(:network_spec) do - { - 'name' => 'my-manual-network', - 'subnets' => [ - { - 'range' => '192.168.1.0/29', - 'gateway' => '192.168.1.1', - 'dns' => ['192.168.1.1', '192.168.1.2'], - 'static' => [], - 'reserved' => [], - 'cloud_properties' => {}, - 'az' => 'az-1', - } - ] - } - end - let(:availability_zones) { [Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-1', {})] } - let(:network) do - ManualNetwork.parse( - network_spec, - availability_zones, - per_spec_logger - ) - end - let(:subnet) do - ManualNetworkSubnet.parse( - network.name, - network_spec['subnets'].first, - availability_zones, - ) - end - - let(:other_network_spec) { network_spec.merge('name' => 'my-other-manual-network') } - let(:other_network) do - ManualNetwork.parse( - other_network_spec, - availability_zones, - per_spec_logger - ) - end - let(:other_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, other_network) } - let(:other_subnet) do - ManualNetworkSubnet.parse( - other_network.name, - other_network_spec['subnets'].first, - availability_zones, - ) - end +module Bosh::Director + module DeploymentPlan + describe IpRepo do + include IpUtil - before { fake_job } + let(:ip_repo) { IpRepo.new(per_spec_logger) } + let(:instance_model) { FactoryBot.create(:models_instance) } + let(:network_spec) do + { + 'name' => 'my-manual-network', + 'subnets' => [ + { + 'range' => '192.168.1.0/29', + 'gateway' => '192.168.1.1', + 'dns' => ['192.168.1.1', '192.168.1.2'], + 'static' => [], + 'reserved' => [], + 'cloud_properties' => {}, + 'az' => 'az-1', + } + ] + } + end + let(:availability_zones) { [Bosh::Director::DeploymentPlan::AvailabilityZone.new('az-1', {})] } + let(:network) do + ManualNetwork.parse( + network_spec, + availability_zones, + per_spec_logger + ) + end + let(:subnet) do + ManualNetworkSubnet.parse( + network.name, + network_spec['subnets'].first, + availability_zones, + ) + end - def cidr_ip(ip) - IPAddr.new(ip).to_i - end + let(:subnet_with_prefix) do + ManualNetworkSubnet.parse( + network.name, + network_spec['subnets'].first.merge('prefix' => '31'), + availability_zones, + ) + end - context :add do - def dynamic_reservation_with_ip(ip) - reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network_without_static_pool) - reservation.resolve_ip(ip) - ip_repo.add(reservation) + let(:subnet_with_too_big_prefix) do + ManualNetworkSubnet.parse( + network.name, + network_spec['subnets'].first.merge('prefix' => '30'), + availability_zones, + ) + end - reservation + let(:other_network_spec) { network_spec.merge('name' => 'my-other-manual-network') } + let(:other_network) do + ManualNetwork.parse( + other_network_spec, + availability_zones, + per_spec_logger + ) + end + let(:other_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, other_network) } + let(:other_subnet) do + ManualNetworkSubnet.parse( + other_network.name, + other_network_spec['subnets'].first, + availability_zones, + ) end - let(:network_without_static_pool) do - network_spec['subnets'].first['static'] = [] - ManualNetwork.parse(network_spec, availability_zones, per_spec_logger) + before { fake_job } + + def cidr_ip(ip) + to_ipaddr(ip) end - context 'when reservation changes type' do - context 'from Static to Dynamic' do - it 'updates type of reservation' do + context :add do + def dynamic_reservation_with_ip(ip) + reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network_without_static_pool) + reservation.resolve_ip(ip) + ip_repo.add(reservation) + + reservation + end + + let(:network_without_static_pool) do + network_spec['subnets'].first['static'] = [] + ManualNetwork.parse(network_spec, availability_zones, per_spec_logger) + end + + context 'when reservation changes type' do + context 'from Static to Dynamic' do + it 'updates type of reservation' do + network_spec['subnets'].first['static'] = ['192.168.1.5'] + static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + ip_repo.add(static_reservation) + + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(true) + + dynamic_reservation = dynamic_reservation_with_ip('192.168.1.5') + ip_repo.add(dynamic_reservation) + + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + new_address = Bosh::Director::Models::IpAddress.first + expect(new_address.static).to eq(false) + expect(new_address.address_str).to eq(original_address.address_str) + end + end + + context 'from Dynamic to Static' do + it 'update type of reservation' do + dynamic_reservation = dynamic_reservation_with_ip('192.168.1.5') + + ip_repo.add(dynamic_reservation) + + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(false) + + network_spec['subnets'].first['static'] = ['192.168.1.5'] + static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + ip_repo.add(static_reservation) + + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + new_address = Bosh::Director::Models::IpAddress.first + expect(new_address.static).to eq(true) + expect(new_address.address_str).to eq(original_address.address_str) + end + end + + context 'from Existing to Static' do + it 'updates type of reservation' do + dynamic_reservation = dynamic_reservation_with_ip('192.168.1.5') + ip_repo.add(dynamic_reservation) + + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(false) + + network_spec['subnets'].first['static'] = ['192.168.1.5'] + existing_reservation = Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.5', 'manual') + ip_repo.add(existing_reservation) + + expect(Bosh::Director::Models::IpAddress.count).to eq(1) + new_address = Bosh::Director::Models::IpAddress.first + expect(new_address.static).to eq(true) + expect(new_address.address_str).to eq(original_address.address_str) + end + end + end + + context 'when reservation changes network' do + it 'updates network name' do network_spec['subnets'].first['static'] = ['192.168.1.5'] static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') ip_repo.add(static_reservation) @@ -83,434 +164,434 @@ def dynamic_reservation_with_ip(ip) expect(Bosh::Director::Models::IpAddress.count).to eq(1) original_address = Bosh::Director::Models::IpAddress.first expect(original_address.static).to eq(true) + expect(original_address.network_name).to eq(network.name) - dynamic_reservation = dynamic_reservation_with_ip('192.168.1.5') - ip_repo.add(dynamic_reservation) + static_reservation_on_another_network = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, other_network, '192.168.1.5') + ip_repo.add(static_reservation_on_another_network) expect(Bosh::Director::Models::IpAddress.count).to eq(1) - new_address = Bosh::Director::Models::IpAddress.first - expect(new_address.static).to eq(false) - expect(new_address.address_str).to eq(original_address.address_str) + original_address = Bosh::Director::Models::IpAddress.first + expect(original_address.static).to eq(true) + expect(original_address.network_name).to eq(other_network.name) end end - context 'from Dynamic to Static' do - it 'update type of reservation' do - dynamic_reservation = dynamic_reservation_with_ip('192.168.1.5') - ip_repo.add(dynamic_reservation) + context 'when IP is released by another deployment' do + it 'retries to reserve it' do + allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do + allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save).and_call_original - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(false) + raise Sequel::ValidationFailed.new('address and network_name unique') + end network_spec['subnets'].first['static'] = ['192.168.1.5'] - static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') - ip_repo.add(static_reservation) - - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - new_address = Bosh::Director::Models::IpAddress.first - expect(new_address.static).to eq(true) - expect(new_address.address_str).to eq(original_address.address_str) + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + ip_repo.add(reservation) + + saved_address = Bosh::Director::Models::IpAddress.order(:address_str).last + expect(saved_address.address_str).to eq(cidr_ip('192.168.1.5').to_s) + expect(saved_address.network_name).to eq('my-manual-network') + expect(saved_address.task_id).to eq('fake-task-id') + expect(saved_address.created_at).to_not be_nil end end - context 'from Existing to Static' do - it 'updates type of reservation' do - dynamic_reservation = dynamic_reservation_with_ip('192.168.1.5') - ip_repo.add(dynamic_reservation) + context 'when reserving an IP with any previous reservation' do + it 'should fail if it reserved by a different instance' do + network_spec['subnets'].first['static'] = ['192.168.1.5'] - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(false) + other_instance_model = FactoryBot.create(:models_instance, availability_zone: 'az-2') + original_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + new_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(other_instance_model, network, '192.168.1.5') - network_spec['subnets'].first['static'] = ['192.168.1.5'] - existing_reservation = Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.5', 'manual') - ip_repo.add(existing_reservation) + ip_repo.add(original_static_network_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - new_address = Bosh::Director::Models::IpAddress.first - expect(new_address.static).to eq(true) - expect(new_address.address_str).to eq(original_address.address_str) + expect { + ip_repo.add(new_static_network_reservation) + }.to raise_error Bosh::Director::NetworkReservationAlreadyInUse end - end - end - - context 'when reservation changes network' do - it 'updates network name' do - network_spec['subnets'].first['static'] = ['192.168.1.5'] - static_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') - ip_repo.add(static_reservation) - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(true) - expect(original_address.network_name).to eq(network.name) + it 'should fail if the reserved instance does not exist' do + network_spec['subnets'].first['static'] = ['192.168.1.5'] - static_reservation_on_another_network = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, other_network, '192.168.1.5') - ip_repo.add(static_reservation_on_another_network) + other_instance_model = FactoryBot.create(:models_instance, availability_zone: 'az-2') + original_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + new_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(other_instance_model, network, '192.168.1.5') - expect(Bosh::Director::Models::IpAddress.count).to eq(1) - original_address = Bosh::Director::Models::IpAddress.first - expect(original_address.static).to eq(true) - expect(original_address.network_name).to eq(other_network.name) - end - end + ip_repo.add(original_static_network_reservation) - context 'when IP is released by another deployment' do - it 'retries to reserve it' do - allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do - allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save).and_call_original + vm = Bosh::Director::Models::OrphanedVm.create(cid: 'some-cid', orphaned_at: Time.now) + Bosh::Director::Models::IpAddress.first.update(instance_id: nil, orphaned_vm: vm) - raise Sequel::ValidationFailed.new('address and network_name unique') + expect do + ip_repo.add(new_static_network_reservation) + end.to raise_error Bosh::Director::NetworkReservationAlreadyInUse end - network_spec['subnets'].first['static'] = ['192.168.1.5'] - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') - ip_repo.add(reservation) + it 'should succeed if it is reserved by the same instance' do + network_spec['subnets'].first['static'] = ['192.168.1.5'] + + static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + + ip_repo.add(static_network_reservation) - saved_address = Bosh::Director::Models::IpAddress.order(:address_str).last - expect(saved_address.address_str).to eq(cidr_ip('192.168.1.5').to_s) - expect(saved_address.network_name).to eq('my-manual-network') - expect(saved_address.task_id).to eq('fake-task-id') - expect(saved_address.created_at).to_not be_nil + expect { + ip_repo.add(static_network_reservation) + }.not_to raise_error + end end end - context 'when reserving an IP with any previous reservation' do - it 'should fail if it reserved by a different instance' do - network_spec['subnets'].first['static'] = ['192.168.1.5'] + describe :allocate_dynamic_ip do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } - other_instance_model = FactoryBot.create(:models_instance, availability_zone: 'az-2') - original_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') - new_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(other_instance_model, network, '192.168.1.5') + context 'when there are no IPs reserved' do + it 'returns the first in the range' do + ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) - ip_repo.add(original_static_network_reservation) + expected_ip_address = cidr_ip('192.168.1.2') + expect(ip_address).to eq(expected_ip_address) + end + end - expect { - ip_repo.add(new_static_network_reservation) - }.to raise_error Bosh::Director::NetworkReservationAlreadyInUse + it 'reserves IP as dynamic' do + ip_repo.allocate_dynamic_ip(reservation, subnet) + + saved_address = Bosh::Director::Models::IpAddress.first + expect(saved_address.static).to eq(false) end - it 'should fail if the reserved instance does not exist' do - network_spec['subnets'].first['static'] = ['192.168.1.5'] + context 'when reserving more than one ip' do + it 'should reserve the next available address' do + first = ip_repo.allocate_dynamic_ip(reservation, subnet) + second = ip_repo.allocate_dynamic_ip(reservation, subnet) + expect(first).to eq(cidr_ip('192.168.1.2')) + expect(second).to eq(cidr_ip('192.168.1.3')) + end + end - other_instance_model = FactoryBot.create(:models_instance, availability_zone: 'az-2') - original_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') - new_static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(other_instance_model, network, '192.168.1.5') + context 'when there are restricted ips' do + it 'does not reserve them' do + network_spec['subnets'].first['reserved'] = ['192.168.1.2', '192.168.1.4'] - ip_repo.add(original_static_network_reservation) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.3')) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.5')) + end + end - vm = Bosh::Director::Models::OrphanedVm.create(cid: 'some-cid', orphaned_at: Time.now) - Bosh::Director::Models::IpAddress.first.update(instance_id: nil, orphaned_vm: vm) + context 'when there are static and restricted ips' do + it 'does not reserve them' do + network_spec['subnets'].first['reserved'] = ['192.168.1.2'] + network_spec['subnets'].first['static'] = ['192.168.1.4'] - expect do - ip_repo.add(new_static_network_reservation) - end.to raise_error Bosh::Director::NetworkReservationAlreadyInUse + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.3')) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.5')) + end end - it 'should succeed if it is reserved by the same instance' do - network_spec['subnets'].first['static'] = ['192.168.1.5'] + context 'when there are available IPs between reserved IPs' do + it 'returns first non-reserved IP' do + network_spec['subnets'].first['static'] = ['192.168.1.2', '192.168.1.4'] - static_network_reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + reservation_1 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.2') + reservation_2 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.4') - ip_repo.add(static_network_reservation) + ip_repo.add(reservation_1) + ip_repo.add(reservation_2) - expect { - ip_repo.add(static_network_reservation) - }.not_to raise_error + reservation_3 = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) + ip_address = ip_repo.allocate_dynamic_ip(reservation_3, subnet) + + expect(ip_address).to eq(cidr_ip('192.168.1.3')) + end end - end - end - describe :allocate_dynamic_ip do - let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } + context 'when all IPs in the range are taken' do + it 'returns nil' do + network_spec['subnets'].first['range'] = '192.168.1.0/30' - context 'when there are no IPs reserved' do - it 'returns the first in the range' do - ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) + ip_repo.allocate_dynamic_ip(reservation, subnet) - expected_ip_address = cidr_ip('192.168.1.2') - expect(ip_address).to eq(expected_ip_address) + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil + end end - end - it 'reserves IP as dynamic' do - ip_repo.allocate_dynamic_ip(reservation, subnet) + context 'when there are IPs reserved by other networks with overlapping subnet' do + it 'returns the next non-reserved IP' do + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - saved_address = Bosh::Director::Models::IpAddress.first - expect(saved_address.static).to eq(false) - end + expected_ip_address = cidr_ip('192.168.1.2') + expect(ip_address).to eq(expected_ip_address) - context 'when reserving more than one ip' do - it 'should reserve the next available address' do - first = ip_repo.allocate_dynamic_ip(reservation, subnet) - second = ip_repo.allocate_dynamic_ip(reservation, subnet) - expect(first).to eq(cidr_ip('192.168.1.2')) - expect(second).to eq(cidr_ip('192.168.1.3')) + ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) + + expected_ip_address = cidr_ip('192.168.1.3') + expect(ip_address).to eq(expected_ip_address) + end end - end - context 'when there are restricted ips' do - it 'does not reserve them' do - network_spec['subnets'].first['reserved'] = ['192.168.1.2', '192.168.1.4'] + context 'when a prefix is assigned to the subnet' do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } + it 'reserves the prefix address' do + ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet_with_prefix) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.3')) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.5')) - end - end + expect(ip_address).to eq(cidr_ip('192.168.1.2/31')) + end - context 'when there are static and restricted ips' do - it 'does not reserve them' do - network_spec['subnets'].first['reserved'] = ['192.168.1.2'] - network_spec['subnets'].first['static'] = ['192.168.1.4'] + it 'reserves the next available prefix address' do + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.3')) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.5')) - end - end + expected_ip_address = cidr_ip('192.168.1.2') + expect(ip_address).to eq(expected_ip_address) - context 'when there are available IPs between reserved IPs' do - it 'returns first non-reserved IP' do - network_spec['subnets'].first['static'] = ['192.168.1.2', '192.168.1.4'] + ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet_with_prefix) - reservation_1 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.2') - reservation_2 = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.4') + expected_ip_address = cidr_ip('192.168.1.4/31') + expect(ip_address).to eq(expected_ip_address) - ip_repo.add(reservation_1) - ip_repo.add(reservation_2) + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - reservation_3 = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) - ip_address = ip_repo.allocate_dynamic_ip(reservation_3, subnet) + expected_ip_address = cidr_ip('192.168.1.3') + expect(ip_address).to eq(expected_ip_address) - expect(ip_address).to eq(cidr_ip('192.168.1.3')) - end - end + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - context 'when all IPs in the range are taken' do - it 'returns nil' do - network_spec['subnets'].first['range'] = '192.168.1.0/30' + expected_ip_address = cidr_ip('192.168.1.6') + expect(ip_address).to eq(expected_ip_address) + end - ip_repo.allocate_dynamic_ip(reservation, subnet) + it 'should stop retrying and return nil if no sufficient range is available' do + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil - end - end + expected_ip_address = cidr_ip('192.168.1.2') + expect(ip_address).to eq(expected_ip_address) - context 'when there are IPs reserved by other networks with overlapping subnet' do - it 'returns the next non-reserved IP' do - ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) + ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet_with_prefix) - expected_ip_address = cidr_ip('192.168.1.2') - expect(ip_address).to eq(expected_ip_address) + expected_ip_address = cidr_ip('192.168.1.4/31') + expect(ip_address).to eq(expected_ip_address) - ip_address = ip_repo.allocate_dynamic_ip(reservation, subnet) + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, other_subnet) - expected_ip_address = cidr_ip('192.168.1.3') - expect(ip_address).to eq(expected_ip_address) - end - end + expected_ip_address = cidr_ip('192.168.1.3') + expect(ip_address).to eq(expected_ip_address) - context 'when reserving IP fails' do - def fail_saving_ips(ips, fail_error) - original_saves = {} - ips.each do |ip| - ip_address = Bosh::Director::Models::IpAddress.new( - address_str: ip.to_s, - network_name: 'my-manual-network', - instance: instance_model, - task_id: Bosh::Director::Config.current_job.task_id - ) - original_save = ip_address.method(:save) - original_saves[ip.to_s] = original_save + expect do + ip_address = ip_repo.allocate_dynamic_ip(other_reservation, subnet_with_prefix) + expect(ip_address).to be_nil + end.not_to(change { Bosh::Director::Models::IpAddress.count }) end - allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do |model| - if ips.map(&:to_s).include?(model.address_str) - original_save = original_saves[model.address_str] - original_save.call - raise fail_error - end - model + it 'should stop retrying and return nil if no sufficient range is available' do + expect do + ip = ip_repo.allocate_dynamic_ip(reservation, subnet_with_too_big_prefix) + expect(ip).to be_nil + end.not_to(change { Bosh::Director::Models::IpAddress.count }) end end - shared_examples :retries_on_race_condition do - context 'when allocating some IPs fails' do - before do - network_spec['subnets'].first['range'] = '192.168.1.0/29' - - fail_saving_ips([ - cidr_ip('192.168.1.2'), - cidr_ip('192.168.1.3'), - cidr_ip('192.168.1.4'), - ], - fail_error + context 'when reserving IP fails' do + def fail_saving_ips(ips, fail_error) + original_saves = {} + ips.each do |ip| + ip_address = Bosh::Director::Models::IpAddress.new( + address_str: ip.to_s, + network_name: 'my-manual-network', + instance: instance_model, + task_id: Bosh::Director::Config.current_job.task_id ) + original_save = ip_address.method(:save) + original_saves[ip.to_s] = original_save end - it 'retries until it succeeds' do - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.5')) + allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do |model| + if ips.map(&:to_s).include?(model.address_str) + original_save = original_saves[model.address_str] + original_save.call + raise fail_error + end + model end end - context 'when allocating any IP fails' do - before do - network_spec['subnets'].first['range'] = '192.168.1.0/29' - network_spec['subnets'].first['reserved'] = ['192.168.1.5', '192.168.1.6'] - - fail_saving_ips([ - cidr_ip('192.168.1.2'), - cidr_ip('192.168.1.3'), - cidr_ip('192.168.1.4') - ], - fail_error - ) + shared_examples :retries_on_race_condition do + context 'when allocating some IPs fails' do + before do + network_spec['subnets'].first['range'] = '192.168.1.0/29' + + fail_saving_ips([ + cidr_ip('192.168.1.2'), + cidr_ip('192.168.1.3'), + cidr_ip('192.168.1.4'), + ], + fail_error + ) + end + + it 'retries until it succeeds' do + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to eq(cidr_ip('192.168.1.5')) + end end - it 'retries until there are no more IPs available' do - expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil + context 'when allocating any IP fails' do + before do + network_spec['subnets'].first['range'] = '192.168.1.0/29' + network_spec['subnets'].first['reserved'] = ['192.168.1.5', '192.168.1.6'] + + fail_saving_ips([ + cidr_ip('192.168.1.2'), + cidr_ip('192.168.1.3'), + cidr_ip('192.168.1.4') + ], + fail_error + ) + end + + it 'retries until there are no more IPs available' do + expect(ip_repo.allocate_dynamic_ip(reservation, subnet)).to be_nil + end end end - end - context 'when sequel validation errors' do - let(:fail_error) { Sequel::ValidationFailed.new('address and network are not unique') } + context 'when sequel validation errors' do + let(:fail_error) { Sequel::ValidationFailed.new('address and network are not unique') } - it_behaves_like :retries_on_race_condition - end + it_behaves_like :retries_on_race_condition + end - context 'when postgres unique errors' do - let(:fail_error) { Sequel::DatabaseError.new('duplicate key value violates unique constraint') } + context 'when postgres unique errors' do + let(:fail_error) { Sequel::DatabaseError.new('duplicate key value violates unique constraint') } - it_behaves_like :retries_on_race_condition - end + it_behaves_like :retries_on_race_condition + end - context 'when mysql unique errors' do - let(:fail_error) { Sequel::DatabaseError.new('Duplicate entry') } + context 'when mysql unique errors' do + let(:fail_error) { Sequel::DatabaseError.new('Duplicate entry') } - it_behaves_like :retries_on_race_condition + it_behaves_like :retries_on_race_condition + end end end - end - - describe :allocate_vip_ip do - let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } - - let(:network) do - VipNetwork.parse( - network_spec, - availability_zones, - per_spec_logger, - ) - end - - let(:network_spec) do - { - 'name' => 'my-vip-network', - 'subnets' => [ - { - 'static' => ['69.69.69.69'], - 'azs' => ['z1'], - }, - ], - } - end - - let(:availability_zones) { [Bosh::Director::DeploymentPlan::AvailabilityZone.new('z1', {})] } - it 'reserves the next available vip and saves it in the database' do - expect do - ip = ip_repo.allocate_vip_ip(reservation, network.subnets.first) - expect(ip).to eq(cidr_ip('69.69.69.69')) - end.to change { Bosh::Director::Models::IpAddress.count }.by(1) + describe :allocate_vip_ip do + let(:reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } - ip_address = instance_model.ip_addresses.first - expect(ip_address.address_str.to_i).to eq(cidr_ip('69.69.69.69')) - end + let(:network) do + VipNetwork.parse( + network_spec, + availability_zones, + per_spec_logger, + ) + end - context 'when there are no vips defined in the network' do let(:network_spec) do { 'name' => 'my-vip-network', 'subnets' => [ { - 'static' => [], + 'static' => ['69.69.69.69'], 'azs' => ['z1'], }, ], } end - it 'should stop retrying and return nil' do + let(:availability_zones) { [Bosh::Director::DeploymentPlan::AvailabilityZone.new('z1', {})] } + + it 'reserves the next available vip and saves it in the database' do expect do ip = ip_repo.allocate_vip_ip(reservation, network.subnets.first) - expect(ip).to be_nil - end.not_to(change { Bosh::Director::Models::IpAddress.count }) + expect(ip).to eq(cidr_ip('69.69.69.69').base_addr) + end.to change { Bosh::Director::Models::IpAddress.count }.by(1) - expect(instance_model.ip_addresses.size).to eq(0) + ip_address = instance_model.ip_addresses.first + expect(ip_address.address_str).to eq(cidr_ip('69.69.69.69').to_s) end - end - context 'when there are no available vips' do - let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } + context 'when there are no vips defined in the network' do + let(:network_spec) do + { + 'name' => 'my-vip-network', + 'subnets' => [ + { + 'static' => [], + 'azs' => ['z1'], + }, + ], + } + end - before do - ip_repo.allocate_vip_ip(existing_reservation, network.subnets.first) - end + it 'should stop retrying and return nil' do + expect do + ip = ip_repo.allocate_vip_ip(reservation, network.subnets.first) + expect(ip).to be_nil + end.not_to(change { Bosh::Director::Models::IpAddress.count }) - it 'should stop retrying and return nil' do - expect do - ip = ip_repo.allocate_vip_ip(reservation, network.subnets.first) - expect(ip).to be_nil - end.not_to(change { Bosh::Director::Models::IpAddress.count }) + expect(instance_model.ip_addresses.size).to eq(0) + end end - end - context 'when saving the IP address to the database fails' do - before do - response_values = [:raise] - allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do - v = response_values.shift - v == :raise ? raise(fail_error) : v + context 'when there are no available vips' do + let(:existing_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } + + before do + ip_repo.allocate_vip_ip(existing_reservation, network.subnets.first) + end + + it 'should stop retrying and return nil' do + expect do + ip = ip_repo.allocate_vip_ip(reservation, network.subnets.first) + expect(ip).to be_nil + end.not_to(change { Bosh::Director::Models::IpAddress.count }) end end - context 'for a retryable error' do - let(:fail_error) { Sequel::ValidationFailed.new('address and network are not unique') } + context 'when saving the IP address to the database fails' do + before do + response_values = [:raise] + allow_any_instance_of(Bosh::Director::Models::IpAddress).to receive(:save) do + v = response_values.shift + v == :raise ? raise(fail_error) : v + end + end - it 'retries to allocate the vip' do - ip = ip_repo.allocate_vip_ip(reservation, network.subnets.first) - expect(ip).to eq(cidr_ip('69.69.69.69')) + context 'for a retryable error' do + let(:fail_error) { Sequel::ValidationFailed.new('address and network are not unique') } + + it 'retries to allocate the vip' do + ip = ip_repo.allocate_vip_ip(reservation, network.subnets.first) + expect(ip).to eq(cidr_ip('69.69.69.69').base_addr) + end end - end - context 'for any other error' do - let(:fail_error) { Sequel::DatabaseError.new } + context 'for any other error' do + let(:fail_error) { Sequel::DatabaseError.new } - it 'raises the error' do - expect do - ip_repo.allocate_vip_ip(reservation, network.subnets.first) - end.to raise_error(Sequel::DatabaseError) + it 'raises the error' do + expect do + ip_repo.allocate_vip_ip(reservation, network.subnets.first) + end.to raise_error(Sequel::DatabaseError) + end end end end - end - describe :delete do - before do - network_spec['subnets'].first['static'] = ['192.168.1.5'] + describe :delete do + before do + network_spec['subnets'].first['static'] = ['192.168.1.5'] - reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') - ip_repo.add(reservation) - end + reservation = Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.5') + ip_repo.add(reservation) + end - it 'deletes IP address' do - expect { - ip_repo.delete('192.168.1.5') - }.to change { - Bosh::Director::Models::IpAddress.all.size - }.by(-1) + it 'deletes IP address' do + expect { + ip_repo.delete('192.168.1.5/32') + }.to change { + Bosh::Director::Models::IpAddress.all.size + }.by(-1) + end end end - end +end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_spec.rb index d09ba135424..45350ad09cb 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_spec.rb @@ -8,6 +8,13 @@ cloud_config['networks'].first['subnets'].first['static'] = static_ips cloud_config end + let(:cloud_config_hash_ipv6) do + cloud_config = SharedSupport::DeploymentManifestHelper.simple_cloud_config_ipv6 + cloud_config['networks'].first['subnets'].first['range'] = network_range_ipv6 + cloud_config['networks'].first['subnets'].first['reserved'] << '2001:db8::3' + cloud_config['networks'].first['subnets'].first['static'] = static_ips + cloud_config + end let(:manifest_hash) do manifest = SharedSupport::DeploymentManifestHelper.minimal_manifest manifest['stemcells'].first['version'] = 1 @@ -15,8 +22,10 @@ end let(:manifest) { Bosh::Director::Manifest.new(manifest_hash, YAML.dump(manifest_hash), cloud_config_hash, nil) } let(:network_range) { '192.168.1.0/24' } + let(:network_range_ipv6) { '2001:db8::/32' } let(:static_ips) { [] } let(:network_spec) { cloud_config_hash['networks'].first } + let(:network_spec_ipv6) { cloud_config_hash_ipv6['networks'].first } let(:planner_factory) do Bosh::Director::DeploymentPlan::PlannerFactory.create(Bosh::Director::Config.logger) end @@ -40,6 +49,17 @@ ) end + let(:manual_network_ipv6) do + Bosh::Director::DeploymentPlan::ManualNetwork.parse( + network_spec_ipv6, + [ + Bosh::Director::DeploymentPlan::AvailabilityZone.new('zone_1', {}), + Bosh::Director::DeploymentPlan::AvailabilityZone.new('zone_2', {}), + ], + per_spec_logger, + ) + end + let(:mock_client) do instance_double(Bosh::Director::ConfigServer::ConfigServerClient) end @@ -122,6 +142,7 @@ before do # manual_network needs to be evaluated before instance_model for unclear reasons manual_network + manual_network_ipv6 end it 'should provide the network settings from the subnet' do reservation = Bosh::Director::DesiredNetworkReservation.new_static( @@ -133,6 +154,7 @@ expect(manual_network.network_settings(reservation, [])).to eq( 'type' => 'manual', 'ip' => '192.168.1.2', + 'prefix' => '32', 'netmask' => '255.255.255.0', 'cloud_properties' => {}, 'gateway' => '192.168.1.1', @@ -141,6 +163,25 @@ ) end + it 'should provide the network settings from the subnet for ipv6' do + reservation = Bosh::Director::DesiredNetworkReservation.new_static( + instance_model, + manual_network_ipv6, + '2001:db8::2', + ) + + expect(manual_network_ipv6.network_settings(reservation, [])).to eq( + 'type' => 'manual', + 'ip' => '2001:db8::2', + 'prefix' => '128', + 'netmask' => 'ffff:ffff:0000:0000:0000:0000:0000:0000', + 'cloud_properties' => {}, + 'gateway' => '2001:db8::1', + 'dns' => ['fd00:ec2::253'], + 'default' => [], + ) + end + it 'should set the defaults' do reservation = Bosh::Director::DesiredNetworkReservation.new_static( instance_model, @@ -151,6 +192,7 @@ expect(manual_network.network_settings(reservation)).to eq( 'type' => 'manual', 'ip' => '192.168.1.2', + 'prefix' => '32', 'netmask' => '255.255.255.0', 'cloud_properties' => {}, 'gateway' => '192.168.1.1', @@ -159,6 +201,32 @@ ) end + context 'when a prefix is maintained for a subnet' do + let(:network_spec) do + cloud_config_hash['networks'].first['subnets'].first['prefix'] = '30' + cloud_config_hash['networks'].first + end + + it 'should set the correct prefix' do + reservation = Bosh::Director::DesiredNetworkReservation.new_static( + instance_model, + manual_network, + '192.168.1.2', + ) + + expect(manual_network.network_settings(reservation)).to eq( + 'type' => 'manual', + 'ip' => '192.168.1.0', + 'prefix' => '30', + 'netmask' => '255.255.255.0', + 'cloud_properties' => {}, + 'gateway' => '192.168.1.1', + 'dns' => ['192.168.1.1', '192.168.1.2'], + 'default' => %w[dns gateway], + ) + end + end + it 'should fail when there is no IP' do reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic( instance_model, diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_subnet_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_subnet_spec.rb index 185363dccb1..d4f6cc60667 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_subnet_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/manual_network_subnet_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Bosh::Director::DeploymentPlan::ManualNetworkSubnet do +include Bosh::Director::IpUtil before { @network = instance_double('Bosh::Director::DeploymentPlan::Network', name: 'net_a') } def make_subnet(properties, availability_zones) @@ -24,12 +25,37 @@ def make_managed_subnet(properties, availability_zones) [], ) - expect(subnet.range.to_cidr_s).to eq('192.168.0.0/24') + expect(subnet.range.to_s).to eq('192.168.0.0/24') expect(subnet.netmask).to eq('255.255.255.0') expect(subnet.gateway).to eq('192.168.0.254') expect(subnet.dns).to eq(nil) end + it 'should create a subnet spec with restricted_ips in cidr format' do + allow(Bosh::Director::Config).to receive(:director_ips).and_return([to_ipaddr('10.0.3.11').to_s]) + + subnet = make_subnet( + { + 'dns' => ['10.0.0.2'], + 'gateway' => '10.0.3.1', + 'range' => '10.0.3.0/24', + 'reserved' => [ + '10.0.3.0 - 10.0.3.35', + '10.0.3.242 - 10.0.3.255'] + }, + [], + ) + + expected_restricted_ips = Set.new([to_ipaddr('10.0.3.0/27'), to_ipaddr('10.0.3.32/30'), to_ipaddr('10.0.3.242/31'), to_ipaddr('10.0.3.244/30'), to_ipaddr('10.0.3.248/29') ]) + + expect(subnet.range.to_s).to eq('10.0.3.0/24') + expect(subnet.netmask).to eq('255.255.255.0') + expect(subnet.gateway).to eq('10.0.3.1') + expect(subnet.dns).to eq(['10.0.0.2']) + expect(subnet.restricted_ips).to eq(expected_restricted_ips) + + end + it 'should create valid subnet spec for managed networks' do subnet = make_managed_subnet( { @@ -41,7 +67,7 @@ def make_managed_subnet(properties, availability_zones) [], ) - expect(subnet.range.to_cidr_s).to eq('192.168.0.0/24') + expect(subnet.range.to_s).to eq('192.168.0.0/24') expect(subnet.netmask).to eq('255.255.255.0') expect(subnet.gateway).to eq('192.168.0.254') expect(subnet.dns).to eq(nil) @@ -94,8 +120,8 @@ def make_managed_subnet(properties, availability_zones) expect(subnet.netmask).to eq('ffff:ffff:ffff:ffff:0000:0000:0000:0000') expect(subnet.gateway).to eq('fdab:d85c:118d:8a46:0000:0000:0000:0001') expect(subnet.dns).to eq([ - "2001:4860:4860:0000:0000:0000:0000:8888", - "2001:4860:4860:0000:0000:0000:0000:8844", + "2001:4860:4860::8888", + "2001:4860:4860::8844", ]) end @@ -175,7 +201,7 @@ def make_managed_subnet(properties, availability_zones) [] ) - expect(subnet.gateway.to_s).to eq('192.168.0.254') + expect(subnet.gateway.base_addr).to eq('192.168.0.254') end it 'should make sure gateway is a single ip' do @@ -260,7 +286,7 @@ def make_managed_subnet(properties, availability_zones) [] ) }.to raise_error(Bosh::Director::NetworkReservedIpOutOfRange, - "Reserved IP '192.167.0.5' is out of " + + "Reserved IP '192.167.0.5/32' is out of " + "network 'net_a' range") end @@ -290,7 +316,7 @@ def make_managed_subnet(properties, availability_zones) [] ) }.to raise_error(Bosh::Director::NetworkStaticIpOutOfRange, - "Static IP '192.167.0.5' is out of " + + "Static IP '192.167.0.5/32' is out of " + "network 'net_a' range") end @@ -307,7 +333,7 @@ def make_managed_subnet(properties, availability_zones) [] ) }.to raise_error(Bosh::Director::NetworkStaticIpOutOfRange, - "Static IP '192.168.0.5' is in network 'net_a' reserved range") + "Static IP '192.168.0.5/32' is in network 'net_a' reserved range") end it 'should include the directors ip addresses in the reserved range' do @@ -328,6 +354,76 @@ def make_managed_subnet(properties, availability_zones) expect(subnet.restricted_ips).to include(ip1.to_i) expect(subnet.restricted_ips).to include(ip2.to_i) end + + it 'should create a subnet spec with prefix' do + subnet = make_subnet( + { + 'range' => '192.168.0.0/24', + 'gateway' => '192.168.0.254', + 'cloud_properties' => {'foo' => 'bar'}, + 'prefix' => '25' + }, + [], + ) + + expect(subnet.range.to_s).to eq('192.168.0.0/24') + expect(subnet.netmask).to eq('255.255.255.0') + expect(subnet.gateway).to eq('192.168.0.254') + expect(subnet.prefix).to eq('25') + expect(subnet.dns).to eq(nil) + end + + + it 'should fail if the prefix size is larger than the range' do + expect { + make_subnet( + { + 'range' => '192.168.0.0/24', + 'gateway' => '192.168.0.254', + 'cloud_properties' => {'foo' => 'bar'}, + 'prefix' => '23' + }, + [], + )}.to raise_error(Bosh::Director::NetworkPrefixSizeTooBig, + "Prefix size '23' is larger than range prefix '24'") + end + + it 'should ignore static ips which are not a base address of the prefix' do + ip1 = to_ipaddr('192.168.0.64') + ip2 = to_ipaddr('192.168.0.128') + + subnet = make_subnet( + { + 'range' => '192.168.0.0/24', + 'gateway' => '192.168.0.254', + 'static' => ['192.168.0.64','192.168.0.128','192.168.0.191'], + 'cloud_properties' => {'foo' => 'bar'}, + 'prefix' => '26' + }, + [], + ) + + expect(subnet.static_ips).to eq(Set.new([ip1, ip2])) + end + + it 'should find the correct base address of the prefix from static ip ranges' do + ip1 = to_ipaddr('192.168.0.32') + ip2 = to_ipaddr('192.168.0.64') + ip3 = to_ipaddr('192.168.0.96') + + subnet = make_subnet( + { + 'range' => '192.168.0.0/24', + 'gateway' => '192.168.0.254', + 'static' => ['192.168.0.30-192.168.0.40','192.168.0.96/26'], + 'cloud_properties' => {'foo' => 'bar'}, + 'prefix' => '27' + }, + [], + ) + + expect(subnet.static_ips).to eq(Set.new([ip1, ip2, ip3])) + end end describe :overlaps? do @@ -397,26 +493,34 @@ def make_managed_subnet(properties, availability_zones) let(:reserved) { ['192.168.0.50-192.168.0.60'] } it 'returns false' do - expect(subnet.is_reservable?(IPAddr.new('192.168.0.55'))).to be_falsey + expect(subnet.is_reservable?(to_ipaddr('192.168.0.55'))).to be_falsey end end context 'when subnet reserved does not include IP' do it 'returns true' do - expect(subnet.is_reservable?(IPAddr.new('192.168.0.55'))).to be_truthy + expect(subnet.is_reservable?(to_ipaddr('192.168.0.55'))).to be_truthy + end + end + + context 'when subnet reserved does not include any address of a cidr' do + let(:reserved) { ['192.168.0.50-192.168.0.60'] } + + it 'returns true' do + expect(subnet.is_reservable?(to_ipaddr('192.168.0.62/31'))).to be_truthy end end end context 'when subnet range does not include IP' do it 'returns false' do - expect(subnet.is_reservable?(IPAddr.new('192.168.10.55'))).to be_falsey + expect(subnet.is_reservable?(to_ipaddr('192.168.10.55'))).to be_falsey end end context 'when subnet range is not the same IP version' do it 'returns false' do - expect(subnet.is_reservable?(IPAddr.new('f1ee:0000:0000:0000:0000:0000:0000:0001'))).to be_falsey + expect(subnet.is_reservable?(to_ipaddr('f1ee:0000:0000:0000:0000:0000:0000:0001'))).to be_falsey end end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_parser/name_servers_parser_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_parser/name_servers_parser_spec.rb index e2562315aaa..40642d8c2e3 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_parser/name_servers_parser_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_parser/name_servers_parser_spec.rb @@ -16,7 +16,7 @@ module DeploymentPlan::NetworkParser it "should raise an error if a DNS server isn't specified with as an IP" do expect { name_servers_parser.parse('network', {'dns' => %w[1.2.3.4 foo.bar]}) - }.to raise_error(/invalid address/) + }.to raise_error(/Invalid IP or CIDR format/) end it 'returns an array containing the nameserver address' do diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/planner_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/planner_spec.rb index b9c02d890d2..50100ed8d71 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/planner_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/planner_spec.rb @@ -19,11 +19,11 @@ module Bosh::Director::DeploymentPlan 'network_A', IPAddr.new('192.168.1.0/24'), nil, nil, nil, nil, ['zone_1'], [], - ['192.168.1.10', '192.168.1.11', '192.168.1.12', '192.168.1.13', '192.168.1.14'] + ['192.168.1.10', '192.168.1.11', '192.168.1.12', '192.168.1.13', '192.168.1.14'], nil, nil, 32 ), ] end - let(:deployment_network) { ManualNetwork.new('network_A', deployment_subnets, nil) } + let(:deployment_network) { ManualNetwork.new('network_A', deployment_subnets, nil, nil) } let(:instance_group_network) { FactoryBot.build(:deployment_plan_job_network, name: 'network_A', deployment_network: deployment_network) } describe 'network_plan_with_dynamic_reservation' do @@ -40,7 +40,7 @@ module Bosh::Director::DeploymentPlan network_plan = planner.network_plan_with_static_reservation(instance_plan, instance_group_network, '192.168.2.10') expect(network_plan.reservation.static?).to be_truthy expect(network_plan.reservation.instance_model).to eq(instance_model) - expect(network_plan.reservation.ip).to eq(ip_to_i('192.168.2.10')) + expect(network_plan.reservation.ip).to eq(to_ipaddr('192.168.2.10')) expect(network_plan.reservation.network).to eq(deployment_network) end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/reservation_reconciler_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/reservation_reconciler_spec.rb index 1b090973a70..baff4b6ee95 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/reservation_reconciler_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/reservation_reconciler_spec.rb @@ -9,20 +9,20 @@ module Bosh::Director::DeploymentPlan let(:instance_model) { FactoryBot.create(:models_instance, availability_zone: initial_az) } let(:instance) { instance_double(Instance, model: instance_model) } let(:variables_interpolator) { instance_double(Bosh::Director::ConfigServer::VariablesInterpolator) } - let(:network) { ManualNetwork.new('my-network', subnets, per_spec_logger) } + let(:network) { ManualNetwork.new('my-network', subnets, '32', per_spec_logger) } let(:subnets) do [ ManualNetworkSubnet.new( 'my-network', IPAddr.new('192.168.1.0/24'), nil, nil, nil, nil, ['zone_1'], [], - ['192.168.1.10'] + ['192.168.1.10'], nil, nil, '32' ), ManualNetworkSubnet.new( 'my-network', IPAddr.new('192.168.2.0/24'), nil, nil, nil, nil, ['zone_2'], [], - ['192.168.2.10'] + ['192.168.2.10'], nil, nil, '32' ), ] end @@ -40,13 +40,13 @@ module Bosh::Director::DeploymentPlan let(:desired_az) { AvailabilityZone.new('zone_1', {}) } let(:existing_reservations) do [ - Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'manual'), - Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.3', 'manual'), + Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'manual'), + Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.3/32', 'manual'), ] end let(:dynamic_network_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } - let(:static_network_reservation) { Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.2') } + let(:static_network_reservation) { Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.2/32') } let(:should_create_swap_delete?) { false } @@ -55,9 +55,9 @@ module Bosh::Director::DeploymentPlan end context 'when the instance group is on a dynamic network' do - let(:network) { DynamicNetwork.new('my-network', subnets, per_spec_logger) } + let(:network) { DynamicNetwork.new('my-network', subnets, '32', per_spec_logger) } let(:desired_reservations) { [dynamic_network_reservation] } - let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'dynamic')] } + let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'dynamic')] } it 'uses the existing reservation' do existing_reservations.map { |reservation| reservation.resolve_type(:dynamic) } @@ -82,7 +82,7 @@ module Bosh::Director::DeploymentPlan let(:existing_reservations) do [ - Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'manual'), + Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'manual'), ] end @@ -178,8 +178,8 @@ module Bosh::Director::DeploymentPlan let(:existing_reservations) do [ - Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'manual'), - Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.3', 'manual'), + Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'manual'), + Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.3/32', 'manual'), ] end @@ -202,7 +202,7 @@ module Bosh::Director::DeploymentPlan end describe 'changes to specifications about the instances network' do - let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'manual')] } + let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'manual')] } let(:desired_reservations) { [Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network2)] } before do @@ -210,7 +210,7 @@ module Bosh::Director::DeploymentPlan end context 'when the existing ip is part of a vip static network but the desired network is a global vip network' do - let(:network) { VipNetwork.new('static-vip-network', nil, [], per_spec_logger) } + let(:network) { VipNetwork.new('static-vip-network', nil, [], '32', per_spec_logger) } let(:network_spec) do { 'name' => 'global-vip-network', @@ -221,7 +221,7 @@ module Bosh::Director::DeploymentPlan end let(:network2) { VipNetwork.parse(network_spec, [], per_spec_logger) } - let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'vip')] } + let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'vip')] } let(:initial_az) { 'zone_1' } it 'should keep the existing reservation' do network_plans = network_planner.reconcile(existing_reservations) @@ -235,13 +235,13 @@ module Bosh::Director::DeploymentPlan expect(existing_plans[0].reservation.network.name).to eq('global-vip-network') expect(existing_plans[0].reservation.instance_model).to eq(instance_model) - expect(to_ipaddr(existing_plans[0].reservation.ip)).to eq('192.168.1.2') + expect(existing_plans[0].reservation.ip).to eq('192.168.1.2') end end context 'when the network name changes' do let(:initial_az) { 'zone_1' } - let(:network2) { ManualNetwork.new('my-network-2', subnets, per_spec_logger) } + let(:network2) { ManualNetwork.new('my-network-2', subnets, '32', per_spec_logger) } it 'should keep existing reservations' do network_plans = network_planner.reconcile(existing_reservations) @@ -255,7 +255,7 @@ module Bosh::Director::DeploymentPlan expect(existing_plans[0].reservation.network.name).to eq('my-network-2') expect(existing_plans[0].reservation.instance_model).to eq(instance_model) - expect(to_ipaddr(existing_plans[0].reservation.ip)).to eq('192.168.1.2') + expect(existing_plans[0].reservation.ip).to eq('192.168.1.2') end context 'and the new network does not match az' do @@ -282,7 +282,7 @@ module Bosh::Director::DeploymentPlan 'my-network', IPAddr.new('192.168.1.0/24'), nil, nil, nil, nil, [], [], - ['192.168.1.10'] + ['192.168.1.10'], '32' ), ] end @@ -306,14 +306,14 @@ module Bosh::Director::DeploymentPlan context 'when the instance model has no az' do let(:initial_az) { '' } let(:desired_az) { nil } - let(:network2) { ManualNetwork.new('my-network-2', subnets, per_spec_logger) } + let(:network2) { ManualNetwork.new('my-network-2', subnets, '32', per_spec_logger) } let(:subnets) do [ ManualNetworkSubnet.new( 'my-network', IPAddr.new('192.168.1.0/24'), nil, nil, nil, nil, [], [], - ['192.168.1.10'] + ['192.168.1.10'], nil, nil, '32' ), ] end @@ -334,7 +334,7 @@ module Bosh::Director::DeploymentPlan end context 'when the network type changes to dynamic' do - let(:network2) { DynamicNetwork.new('my-network-2', subnets, per_spec_logger) } + let(:network2) { DynamicNetwork.new('my-network-2', subnets, '32', per_spec_logger) } it 'should have a new reservation' do network_plans = network_planner.reconcile(existing_reservations) @@ -352,8 +352,8 @@ module Bosh::Director::DeploymentPlan end context 'when the network type changes to manual' do - let(:network) { DynamicNetwork.new('my-network', subnets, per_spec_logger) } - let(:network2) { ManualNetwork.new('my-network-2', subnets, per_spec_logger) } + let(:network) { DynamicNetwork.new('my-network', subnets, '32', per_spec_logger) } + let(:network2) { ManualNetwork.new('my-network-2', subnets, '32', per_spec_logger) } it 'should have a new reservation' do network_plans = network_planner.reconcile(existing_reservations) @@ -417,18 +417,18 @@ module Bosh::Director::DeploymentPlan desired_plans = network_plans.reject(&:existing?).reject(&:obsolete?) expect(obsolete_plans.count).to eq(1) - expect(to_ipaddr(obsolete_plans.first.reservation.ip)).to eq('192.168.1.2') + expect(obsolete_plans.first.reservation.ip).to eq('192.168.1.2') expect(existing_plans.count).to eq(1) - expect(to_ipaddr(existing_plans.first.reservation.ip)).to eq('192.168.1.3') + expect(existing_plans.first.reservation.ip).to eq('192.168.1.3') expect(desired_plans.count).to eq(1) - expect(to_ipaddr(desired_plans.first.reservation.ip)).to eq('192.168.1.4') + expect(desired_plans.first.reservation.ip).to eq('192.168.1.4') end end end context 'when existing reservation availability zones do not match job availability zones' do let(:desired_az) { AvailabilityZone.new('zone_2', {}) } - let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'manual')] } + let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'manual')] } let(:desired_reservations) { [Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network)] } before { existing_reservations[0].resolve_type(:dynamic) } @@ -440,7 +440,7 @@ module Bosh::Director::DeploymentPlan desired_plans = network_plans.reject(&:existing?).reject(&:obsolete?) expect(obsolete_plans.count).to eq(1) - expect(to_ipaddr(obsolete_plans.first.reservation.ip)).to eq('192.168.1.2') + expect(obsolete_plans.first.reservation.ip).to eq('192.168.1.2') expect(existing_plans.count).to eq(0) expect(desired_plans.count).to eq(1) expect(desired_plans.first.reservation.type).to eq(:dynamic) @@ -462,7 +462,7 @@ module Bosh::Director::DeploymentPlan context 'when existing reservation and job do not belong to any availability zone' do let(:desired_az) { nil } - let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2', 'manual')] } + let(:existing_reservations) { [Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'manual')] } let(:desired_reservations) { [Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network)] } let(:subnets) do [ @@ -470,7 +470,9 @@ module Bosh::Director::DeploymentPlan 'my-network', IPAddr.new('192.168.1.0/24'), nil, nil, nil, nil, nil, [], - ['192.168.1.10'] + ['192.168.1.10'], + nil, nil, + '32' ), ] end @@ -485,17 +487,16 @@ module Bosh::Director::DeploymentPlan expect(obsolete_plans.count).to eq(0) expect(existing_plans.count).to eq(1) - expect(to_ipaddr(existing_plans.first.reservation.ip)).to eq('192.168.1.2') + expect(existing_plans.first.reservation.ip).to eq('192.168.1.2/32') expect(desired_plans.count).to eq(0) end end context 'when there are new reservations' do - let(:dynamic_network_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } let(:desired_reservations) do [ - Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.2'), - Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.4'), + Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.2/32'), + Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.4/32'), dynamic_network_reservation, ] end @@ -518,7 +519,6 @@ module Bosh::Director::DeploymentPlan end context 'when there is no desired reservations' do - let(:dynamic_network_reservation) { Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) } let(:desired_reservations) { [] } before do @@ -526,7 +526,7 @@ module Bosh::Director::DeploymentPlan existing_reservations[1].resolve_type(:dynamic) end - it 'should return desired network plans for the new reservations' do + it 'should return obsolete network plans for the new reservations' do network_plans = network_planner.reconcile(existing_reservations) existing_plans = network_plans.select(&:existing?) obsolete_plans = network_plans.select(&:obsolete?) @@ -537,6 +537,33 @@ module Bosh::Director::DeploymentPlan expect(obsolete_plans.count).to eq(2) end end + + context 'when the nic group differs for an existing and desired reservation' do + let(:desired_reservation_with_nic_group_2) { Bosh::Director::DesiredNetworkReservation.new_static(instance_model, network, '192.168.1.2/32', 2) } + let(:existing_reservation_with_nic_group_1) { Bosh::Director::ExistingNetworkReservation.new(instance_model, network, '192.168.1.2/32', 'manual', 1) } + + let(:desired_reservations) do + [ + desired_reservation_with_nic_group_2, + ] + end + + before do + existing_reservation_with_nic_group_1.resolve_type(:static) + end + + it 'should have the updated nic_group in the existing plans network reservation' do + network_plans = network_planner.reconcile([existing_reservation_with_nic_group_1]) + existing_plans = network_plans.select(&:existing?) + obsolete_plans = network_plans.select(&:obsolete?) + desired_plans = network_plans.reject(&:existing?).reject(&:obsolete?) + + expect(existing_plans.count).to eq(1) + expect(existing_plans.first.reservation.nic_group).to eq(2) + expect(desired_plans.count).to eq(0) + expect(obsolete_plans.count).to eq(0) + end + end end end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/vip_planner_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/vip_planner_spec.rb index 828d8832930..c8a11867bad 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/vip_planner_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_planner/vip_planner_spec.rb @@ -3,7 +3,6 @@ module Bosh::Director describe DeploymentPlan::NetworkPlanner::VipPlanner do include IpUtil - subject(:planner) { DeploymentPlan::NetworkPlanner::VipPlanner.new(network_planner, per_spec_logger) } let(:network_planner) { DeploymentPlan::NetworkPlanner::Planner.new(per_spec_logger) } @@ -48,6 +47,7 @@ def make_instance_plan instance_group_static_ips1, [], vip_deployment_network1, + nil ) end @@ -57,6 +57,7 @@ def make_instance_plan instance_group_static_ips2, [], vip_deployment_network2, + nil ) end @@ -69,17 +70,17 @@ def make_instance_plan end context 'and there are static ips defined only on the job network' do - let(:instance_group_static_ips1) { [ip_to_i('68.68.68.68'), ip_to_i('69.69.69.69')] } - let(:instance_group_static_ips2) { [ip_to_i('77.77.77.77'), ip_to_i('79.79.79.79')] } + let(:instance_group_static_ips1) { [to_ipaddr('68.68.68.68'), to_ipaddr('69.69.69.69')] } + let(:instance_group_static_ips2) { [to_ipaddr('77.77.77.77'), to_ipaddr('79.79.79.79')] } let(:vip_network_spec1) { { 'name' => 'vip-network-1' } } let(:vip_network_spec2) { { 'name' => 'vip-network-2' } } it 'creates network plans with static IP from each vip network' do planner.add_vip_network_plans(instance_plans, vip_networks) - expect(instance_plan.network_plans[0].reservation.ip).to eq(ip_to_i('68.68.68.68')) + expect(instance_plan.network_plans[0].reservation.ip).to eq(to_ipaddr('68.68.68.68')) expect(instance_plan.network_plans[0].reservation.network).to eq(vip_deployment_network1) - expect(instance_plan.network_plans[1].reservation.ip).to eq(ip_to_i('77.77.77.77')) + expect(instance_plan.network_plans[1].reservation.ip).to eq(to_ipaddr('77.77.77.77')) expect(instance_plan.network_plans[1].reservation.network).to eq(vip_deployment_network2) end @@ -87,13 +88,13 @@ def make_instance_plan context 'when existing instance IPs can be reused' do before do FactoryBot.create(:models_ip_address, - address_str: ip_to_i('69.69.69.69').to_s, + address_str: to_ipaddr('69.69.69.69').to_s, network_name: 'fake-network-1', instance: instance_plan.existing_instance, ) FactoryBot.create(:models_ip_address, - address_str: ip_to_i('79.79.79.79').to_s, + address_str: to_ipaddr('79.79.79.79').to_s, network_name: 'fake-network-2', instance: instance_plan.existing_instance, ) @@ -101,10 +102,10 @@ def make_instance_plan it 'assigns vip static IP that instance is currently using' do planner.add_vip_network_plans(instance_plans, vip_networks) - expect(instance_plan.network_plans[0].reservation.ip).to eq(ip_to_i('69.69.69.69')) + expect(instance_plan.network_plans[0].reservation.ip).to eq(to_ipaddr('69.69.69.69')) expect(instance_plan.network_plans[0].reservation.network).to eq(vip_deployment_network1) - expect(instance_plan.network_plans[1].reservation.ip).to eq(ip_to_i('79.79.79.79')) + expect(instance_plan.network_plans[1].reservation.ip).to eq(to_ipaddr('79.79.79.79')) expect(instance_plan.network_plans[1].reservation.network).to eq(vip_deployment_network2) end end @@ -112,13 +113,13 @@ def make_instance_plan context 'when existing instance static IP is no longer in the list of IPs' do before do FactoryBot.create(:models_ip_address, - address_str: ip_to_i('65.65.65.65').to_s, + address_str: to_ipaddr('65.65.65.65').to_s, network_name: 'fake-network-1', instance: instance_plan.existing_instance, ) FactoryBot.create(:models_ip_address, - address_str: ip_to_i('79.79.79.79').to_s, + address_str: to_ipaddr('79.79.79.79').to_s, network_name: 'fake-network-2', instance: instance_plan.existing_instance, ) @@ -127,10 +128,10 @@ def make_instance_plan it 'picks new IP for instance' do planner.add_vip_network_plans(instance_plans, vip_networks) instance_plan = instance_plans.first - expect(instance_plan.network_plans[0].reservation.ip).to eq(ip_to_i('68.68.68.68')) + expect(instance_plan.network_plans[0].reservation.ip).to eq(to_ipaddr('68.68.68.68')) expect(instance_plan.network_plans[0].reservation.network).to eq(vip_deployment_network1) - expect(instance_plan.network_plans[1].reservation.ip).to eq(ip_to_i('79.79.79.79')) + expect(instance_plan.network_plans[1].reservation.ip).to eq(to_ipaddr('79.79.79.79')) expect(instance_plan.network_plans[1].reservation.network).to eq(vip_deployment_network2) end end @@ -142,13 +143,13 @@ def make_instance_plan before do FactoryBot.create(:models_ip_address, - address_str: ip_to_i('68.68.68.68').to_s, + address_str: to_ipaddr('68.68.68.68').to_s, network_name: 'fake-network-1', instance: instance_plans[1].existing_instance, ) FactoryBot.create(:models_ip_address, - address_str: ip_to_i('77.77.77.77').to_s, + address_str: to_ipaddr('77.77.77.77').to_s, network_name: 'fake-network-2', instance: instance_plans[1].existing_instance, ) @@ -157,15 +158,15 @@ def make_instance_plan it 'properly assigns vip IPs based on current instance IPs' do planner.add_vip_network_plans(instance_plans, vip_networks) first_instance_plan = instance_plans[0] - expect(first_instance_plan.network_plans[0].reservation.ip).to eq(ip_to_i('69.69.69.69')) + expect(first_instance_plan.network_plans[0].reservation.ip).to eq(to_ipaddr('69.69.69.69')) expect(first_instance_plan.network_plans[0].reservation.network).to eq(vip_deployment_network1) - expect(first_instance_plan.network_plans[1].reservation.ip).to eq(ip_to_i('79.79.79.79')) + expect(first_instance_plan.network_plans[1].reservation.ip).to eq(to_ipaddr('79.79.79.79')) expect(first_instance_plan.network_plans[1].reservation.network).to eq(vip_deployment_network2) second_instance_plan = instance_plans[1] - expect(second_instance_plan.network_plans[0].reservation.ip).to eq(ip_to_i('68.68.68.68')) + expect(second_instance_plan.network_plans[0].reservation.ip).to eq(to_ipaddr('68.68.68.68')) expect(second_instance_plan.network_plans[0].reservation.network).to eq(vip_deployment_network1) - expect(second_instance_plan.network_plans[1].reservation.ip).to eq(ip_to_i('77.77.77.77')) + expect(second_instance_plan.network_plans[1].reservation.ip).to eq(to_ipaddr('77.77.77.77')) expect(second_instance_plan.network_plans[1].reservation.network).to eq(vip_deployment_network2) end end @@ -201,7 +202,7 @@ def make_instance_plan end context 'and there are static ips defined on the job network and in the network in the cloud config' do - let(:instance_group_static_ips1) { [ip_to_i('68.68.68.68'), ip_to_i('69.69.69.69')] } + let(:instance_group_static_ips1) { [to_ipaddr('68.68.68.68'), to_ipaddr('69.69.69.69')] } let(:instance_group_static_ips2) { [] } let(:vip_network_spec1) { { 'name' => 'vip-network-1', 'subnets' => [{ 'static' => ['1.1.1.1'] }] } } let(:vip_network_spec2) { { 'name' => 'vip-network-2' } } diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_settings_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_settings_spec.rb index 8b5ca81aa51..b1048f0090e 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_settings_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/network_settings_spec.rb @@ -49,6 +49,10 @@ module Bosh::Director::DeploymentPlan let(:plan) { instance_double(Planner, name: 'fake-deployment') } let(:use_short_dns_addresses) { false } let(:use_link_dns_addresses) { false } + let(:dynamic_network) do + subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], {'foo' => 'bar'}, 'az-1', '32')] + DynamicNetwork.new('net_a', subnets, '32', per_spec_logger) + end before do allow_any_instance_of(Bosh::Director::DnsEncoder).to receive(:num_for_uuid) @@ -61,11 +65,6 @@ module Bosh::Director::DeploymentPlan describe '#to_hash' do context 'dynamic network' do - let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], {'foo' => 'bar'}, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) - end - let(:reservations) { [Bosh::Director::DesiredNetworkReservation.new_dynamic(nil, dynamic_network)] } it 'returns the network settings plus current IP, Netmask & Gateway from agent state' do @@ -130,10 +129,6 @@ module Bosh::Director::DeploymentPlan end context 'when it is a dynamic network' do - let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], {'foo' => 'bar'}, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) - end let(:reservations) {[Bosh::Director::DesiredNetworkReservation.new_dynamic(nil, dynamic_network)]} context 'when local dns is disabled' do @@ -251,10 +246,6 @@ module Bosh::Director::DeploymentPlan end context 'when it is a dynamic network' do - let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], {'foo' => 'bar'}, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) - end let(:reservations) {[Bosh::Director::DesiredNetworkReservation.new_dynamic(nil, dynamic_network)]} context 'when local dns is disabled' do @@ -291,11 +282,6 @@ module Bosh::Director::DeploymentPlan describe '#network_addresses' do context 'dynamic network' do - let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], {'foo' => 'bar'}, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) - end - let(:reservations) {[Bosh::Director::DesiredNetworkReservation.new_dynamic(nil, dynamic_network)]} context 'when DNS entries are requested' do it 'includes the network name and domain record' do @@ -379,8 +365,8 @@ module Bosh::Director::DeploymentPlan context 'when it is a dynamic network' do let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) + subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1', '32')] + DynamicNetwork.new('net_a', subnets, '32', per_spec_logger) end let(:reservations) { [Bosh::Director::DesiredNetworkReservation.new_dynamic(nil, dynamic_network)] } @@ -493,8 +479,8 @@ module Bosh::Director::DeploymentPlan context 'when it is a dynamic network' do let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) + subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1', '32')] + DynamicNetwork.new('net_a', subnets, '32', per_spec_logger) end let(:reservations) { [Bosh::Director::DesiredNetworkReservation.new_dynamic(nil, dynamic_network)] } @@ -531,13 +517,13 @@ module Bosh::Director::DeploymentPlan context 'when there are multiple network reservations' do let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) + subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1', '32')] + DynamicNetwork.new('net_a', subnets, '32', per_spec_logger) end let(:dynamic_network2) do - subnets = [DynamicNetworkSubnet.new(['9.8.7.6'], { 'bob' => 'joe' }, 'az-1')] - DynamicNetwork.new('net_b', subnets, per_spec_logger) + subnets = [DynamicNetworkSubnet.new(['9.8.7.6'], { 'bob' => 'joe' }, 'az-1', '32')] + DynamicNetwork.new('net_b', subnets, '32', per_spec_logger) end let(:reservations) do @@ -561,8 +547,8 @@ module Bosh::Director::DeploymentPlan context 'dynamic network' do let(:dynamic_network) do - subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1')] - DynamicNetwork.new('net_a', subnets, per_spec_logger) + subnets = [DynamicNetworkSubnet.new(['1.2.3.4'], { 'foo' => 'bar' }, 'az-1', '32')] + DynamicNetwork.new('net_a', subnets, '32', per_spec_logger) end let(:reservations) { [Bosh::Director::DesiredNetworkReservation.new_dynamic(nil, dynamic_network)] } diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/networks_to_static_ips_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/networks_to_static_ips_spec.rb index 52550914189..7b18a636661 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/networks_to_static_ips_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/networks_to_static_ips_spec.rb @@ -1,150 +1,160 @@ require 'spec_helper' -module Bosh::Director::DeploymentPlan - describe PlacementPlanner::NetworksToStaticIps do - subject(:networks_to_static_ips) { described_class.new(networks_to_static_ips_hash, 'fake-job') } - - let(:networks_to_static_ips_hash) do - { - 'network-1' => [ - PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.1', ['z2', 'z1']), - PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.2', ['z2']), - ], - 'network-2' => [ - PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.3', ['z2']), - PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.4', ['z1']), - ], - } - end - - describe '#validate_azs_are_declared_in_job_and_subnets' do - context 'when there are AZs that are declared in job networks but not in desired azs'do - let(:desired_azs) { nil } - - it 'raises an error' do - expect { - networks_to_static_ips.validate_azs_are_declared_in_job_and_subnets(desired_azs) - }.to raise_error Bosh::Director::JobInvalidAvailabilityZone, "Instance group 'fake-job' subnets declare availability zones and the instance group does not" - end +module Bosh::Director + module DeploymentPlan + describe PlacementPlanner::NetworksToStaticIps do + include IpUtil + subject(:networks_to_static_ips) { described_class.new(networks_to_static_ips_hash, 'fake-job') } + + let(:networks_to_static_ips_hash) do + { + 'network-1' => [ + PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.1', ['z2', 'z1']), + PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.2', ['z2']), + ], + 'network-2' => [ + PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.3', ['z2']), + PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.4', ['z1']), + ], + } end - context 'when there are AZs that are declared in job networks and in desired azs'do - let(:desired_azs) do - [ - AvailabilityZone.new('z1', {}), - AvailabilityZone.new('z2', {}), - ] - end + describe '#validate_azs_are_declared_in_job_and_subnets' do + context 'when there are AZs that are declared in job networks but not in desired azs'do + let(:desired_azs) { nil } - it 'does not raise an error' do - expect { - networks_to_static_ips.validate_azs_are_declared_in_job_and_subnets(desired_azs) - }.to_not raise_error + it 'raises an error' do + expect { + networks_to_static_ips.validate_azs_are_declared_in_job_and_subnets(desired_azs) + }.to raise_error Bosh::Director::JobInvalidAvailabilityZone, "Instance group 'fake-job' subnets declare availability zones and the instance group does not" + end end - end - end - describe 'validate_ips_are_in_desired_azs' do - context 'when there are no AZs that job can put its static ips in'do - let(:desired_azs) do - [ - AvailabilityZone.new('z3', {}), - ] - end + context 'when there are AZs that are declared in job networks and in desired azs'do + let(:desired_azs) do + [ + AvailabilityZone.new('z1', {}), + AvailabilityZone.new('z2', {}), + ] + end - it 'raises an error' do - expect { - networks_to_static_ips.validate_ips_are_in_desired_azs(desired_azs) - }.to raise_error Bosh::Director::JobStaticIpsFromInvalidAvailabilityZone, - "Instance group 'fake-job' declares static ip '192.168.0.1' which does not belong to any of the instance group's availability zones." + it 'does not raise an error' do + expect { + networks_to_static_ips.validate_azs_are_declared_in_job_and_subnets(desired_azs) + }.to_not raise_error + end end end - context 'when job declares azs which is subset of azs on ip subnet' do - let(:desired_azs) do - [ - AvailabilityZone.new('z1', {}), - ] - end + describe 'validate_ips_are_in_desired_azs' do + context 'when there are no AZs that job can put its static ips in'do + let(:desired_azs) do + [ + AvailabilityZone.new('z3', {}), + ] + end - let(:networks_to_static_ips_hash) do - { - 'network-1' => [ - PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.1', ['z2', 'z1']), - ] - } + it 'raises an error' do + expect { + networks_to_static_ips.validate_ips_are_in_desired_azs(desired_azs) + }.to raise_error Bosh::Director::JobStaticIpsFromInvalidAvailabilityZone, + "Instance group 'fake-job' declares static ip '192.168.0.1' which does not belong to any of the instance group's availability zones." + end end - it 'does not raise an error' do - expect { - networks_to_static_ips.validate_ips_are_in_desired_azs(desired_azs) - }.to_not raise_error - end - end + context 'when job declares azs which is subset of azs on ip subnet' do + let(:desired_azs) do + [ + AvailabilityZone.new('z1', {}), + ] + end - context 'when there are AZs that are declared in job networks and in desired azs'do - let(:desired_azs) do - [ - AvailabilityZone.new('z1', {}), - AvailabilityZone.new('z2', {}), - ] - end + let(:networks_to_static_ips_hash) do + { + 'network-1' => [ + PlacementPlanner::NetworksToStaticIps::StaticIpToAzs.new('192.168.0.1', ['z2', 'z1']), + ] + } + end - it 'does not raise an error' do - expect { - networks_to_static_ips.validate_ips_are_in_desired_azs(desired_azs) - }.to_not raise_error + it 'does not raise an error' do + expect { + networks_to_static_ips.validate_ips_are_in_desired_azs(desired_azs) + }.to_not raise_error + end end - end - describe '#next_ip_for_network' do - let(:deployment_subnets) do - [ - ManualNetworkSubnet.new( - 'network_A', - IPAddr.new('192.168.1.0/24'), - nil, nil, nil, nil, subnet_azs, [], - ['192.168.1.10', '192.168.1.11', '192.168.1.12', '192.168.1.13', '192.168.1.14']) - ] - end - let(:deployment_network) { ManualNetwork.new('network_A', deployment_subnets, nil) } - let(:job_networks) do - [FactoryBot.build(:deployment_plan_job_network, name: 'network_A', static_ips: job_static_ips, deployment_network: deployment_network)] - end - let(:job_static_ips) { ['192.168.1.10', '192.168.1.11'] } - let(:desired_azs) { [AvailabilityZone.new('zone_1', {})] } - let(:subnet_azs) { ['zone_1'] } - it 'finds first unclaimed IP in network' do - networks_to_static_ips = PlacementPlanner::NetworksToStaticIps.create(job_networks, desired_azs, 'fake-job') - static_ip_to_azs = networks_to_static_ips.next_ip_for_network(job_networks[0]) - expect(static_ip_to_azs.ip).to eq('192.168.1.10') - networks_to_static_ips.claim_in_az(static_ip_to_azs.ip, 'zone_1') - - static_ip_to_azs = networks_to_static_ips.next_ip_for_network(job_networks[0]) - expect(static_ip_to_azs.ip).to eq('192.168.1.11') - end + context 'when there are AZs that are declared in job networks and in desired azs'do + let(:desired_azs) do + [ + AvailabilityZone.new('z1', {}), + AvailabilityZone.new('z2', {}), + ] + end - context 'when the job specifies a static ip that belongs to no subnet' do - let(:job_static_ips) { ['192.168.1.10', '192.168.1.244'] } - it 'raises' do + it 'does not raise an error' do expect { - PlacementPlanner::NetworksToStaticIps.create(job_networks, desired_azs, 'fake-job') - }.to raise_error(Bosh::Director::InstanceGroupNetworkInstanceIpMismatch, - "Instance group 'fake-job' with network 'network_A' declares static ip '192.168.1.244', which belongs to no subnet") + networks_to_static_ips.validate_ips_are_in_desired_azs(desired_azs) + }.to_not raise_error end end - context 'when desired azs are subset of subnet azs' do - let(:subnet_azs) { ['zone_2', 'zone_1'] } - - it 'returns static ip in desired az' do + describe '#next_ip_for_network' do + let(:deployment_subnets) do + [ + ManualNetworkSubnet.new( + 'network_A', + IPAddr.new('192.168.1.0/24'), + nil, nil, nil, nil, subnet_azs, [], + [ + to_ipaddr('192.168.1.10'), + to_ipaddr('192.168.1.11'), + to_ipaddr('192.168.1.12'), + to_ipaddr('192.168.1.13'), + to_ipaddr('192.168.1.14') + ] + ) + ] + end + let(:deployment_network) { ManualNetwork.new('network_A', deployment_subnets, '32', nil) } + let(:job_networks) do + [FactoryBot.build(:deployment_plan_job_network, name: 'network_A', static_ips: job_static_ips, deployment_network: deployment_network)] + end + let(:job_static_ips) { ['192.168.1.10', '192.168.1.11'] } + let(:desired_azs) { [AvailabilityZone.new('zone_1', {})] } + let(:subnet_azs) { ['zone_1'] } + it 'finds first unclaimed IP in network' do networks_to_static_ips = PlacementPlanner::NetworksToStaticIps.create(job_networks, desired_azs, 'fake-job') static_ip_to_azs = networks_to_static_ips.next_ip_for_network(job_networks[0]) expect(static_ip_to_azs.ip).to eq('192.168.1.10') - expect(static_ip_to_azs.az_names).to eq(['zone_1']) + networks_to_static_ips.claim_in_az(static_ip_to_azs.ip, 'zone_1') + + static_ip_to_azs = networks_to_static_ips.next_ip_for_network(job_networks[0]) + expect(static_ip_to_azs.ip).to eq('192.168.1.11') + end + + context 'when the job specifies a static ip that belongs to no subnet' do + let(:job_static_ips) { ['192.168.1.10', '192.168.1.244'] } + it 'raises' do + expect { + PlacementPlanner::NetworksToStaticIps.create(job_networks, desired_azs, 'fake-job') + }.to raise_error(Bosh::Director::InstanceGroupNetworkInstanceIpMismatch, + "Instance group 'fake-job' with network 'network_A' declares static ip '192.168.1.244', which belongs to no subnet") + end + end + + context 'when desired azs are subset of subnet azs' do + let(:subnet_azs) { ['zone_2', 'zone_1'] } + + it 'returns static ip in desired az' do + networks_to_static_ips = PlacementPlanner::NetworksToStaticIps.create(job_networks, desired_azs, 'fake-job') + static_ip_to_azs = networks_to_static_ips.next_ip_for_network(job_networks[0]) + expect(static_ip_to_azs.ip).to eq('192.168.1.10') + expect(static_ip_to_azs.az_names).to eq(['zone_1']) + end end end end end end -end +end \ No newline at end of file diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/plan_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/plan_spec.rb index 3c7f5d6c11c..43a6e10f868 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/plan_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/plan_spec.rb @@ -1,134 +1,149 @@ require 'spec_helper' -module Bosh::Director::DeploymentPlan - describe PlacementPlanner::Plan do - subject(:plan) { PlacementPlanner::Plan.new(instance_plan_factory, network_planner, per_spec_logger) } - let(:network_planner) { NetworkPlanner::Planner.new(per_spec_logger) } - let(:variables_interpolator) { instance_double(Bosh::Director::ConfigServer::VariablesInterpolator) } - - let(:instance_plan_factory) do - InstancePlanFactory.new( - instance_repo, - {}, - deployment, - index_assigner, - variables_interpolator, - [], - ) - end +module Bosh::Director + module DeploymentPlan + describe PlacementPlanner::Plan do + include IpUtil + subject(:plan) { PlacementPlanner::Plan.new(instance_plan_factory, network_planner, per_spec_logger) } + let(:network_planner) { NetworkPlanner::Planner.new(per_spec_logger) } + let(:variables_interpolator) { instance_double(Bosh::Director::ConfigServer::VariablesInterpolator) } + + let(:instance_plan_factory) do + InstancePlanFactory.new( + instance_repo, + {}, + deployment, + index_assigner, + variables_interpolator, + [], + ) + end - let(:index_assigner) { PlacementPlanner::IndexAssigner.new(deployment_model) } - let(:instance_repo) { Bosh::Director::DeploymentPlan::InstanceRepository.new(per_spec_logger, variables_interpolator) } - let(:instance_plans) do - plan.create_instance_plans(desired, existing, job_networks, availability_zones, 'jobname') - end - let(:availability_zones) { [zone_1, zone_2] } - let(:zone_1) { AvailabilityZone.new('zone_1', {}) } - let(:zone_2) { AvailabilityZone.new('zone_2', {}) } - let(:zone_3) { AvailabilityZone.new('zone_3', {}) } - - let(:instance_group) { FactoryBot.build(:deployment_plan_instance_group) } - - let(:desired) do - [ - DesiredInstance.new(instance_group, deployment), - DesiredInstance.new(instance_group, deployment), - DesiredInstance.new(instance_group, deployment), - ] - end - let(:existing) do - [ - existing_instance_with_az(2, zone_1.name), - existing_instance_with_az(0, zone_3.name), - existing_instance_with_az(1, zone_2.name), - ] - end - let(:deployment) { instance_double(Planner, model: deployment_model, skip_drain: SkipDrain.new(true)) } - let(:deployment_model) { FactoryBot.create(:models_deployment) } - - let(:deployment_network) { ManualNetwork.new('network_A', deployment_subnets, nil) } - let(:deployment_subnets) do - [ - ManualNetworkSubnet.new( - 'network_A', - IPAddr.new('192.168.1.0/24'), - nil, nil, nil, nil, ['zone_1'], [], - %w[ - 192.168.1.10 - 192.168.1.11 - 192.168.1.12 - 192.168.1.13 - 192.168.1.14 - ] - ), - ManualNetworkSubnet.new( - 'network_A', - IPAddr.new('10.10.1.0/24'), - nil, nil, nil, nil, ['zone_2'], [], - %w[ - 10.10.1.10 - 10.10.1.11 - 10.10.1.12 - 10.10.1.13 - 10.10.1.14 - ] - ), - ManualNetworkSubnet.new( - 'network_A', - IPAddr.new('10.0.1.0/24'), - nil, nil, nil, nil, ['zone_3'], [], - %w[ - 10.0.1.10 - 10.0.1.11 - 10.0.1.12 - 10.0.1.13 - 10.0.1.14 - ] - ), - ] - end - let(:job_networks) do - [FactoryBot.build(:deployment_plan_job_network, name: 'network_A', static_ips: job_static_ips, deployment_network: deployment_network)] - end + let(:index_assigner) { PlacementPlanner::IndexAssigner.new(deployment_model) } + let(:instance_repo) { Bosh::Director::DeploymentPlan::InstanceRepository.new(per_spec_logger, variables_interpolator) } + let(:instance_plans) do + plan.create_instance_plans(desired, existing, job_networks, availability_zones, 'jobname') + end + let(:availability_zones) { [zone_1, zone_2] } + let(:zone_1) { AvailabilityZone.new('zone_1', {}) } + let(:zone_2) { AvailabilityZone.new('zone_2', {}) } + let(:zone_3) { AvailabilityZone.new('zone_3', {}) } + + let(:instance_group) { FactoryBot.build(:deployment_plan_instance_group) } + + let(:desired) do + [ + DesiredInstance.new(instance_group, deployment), + DesiredInstance.new(instance_group, deployment), + DesiredInstance.new(instance_group, deployment), + ] + end + let(:existing) do + [ + existing_instance_with_az(2, zone_1.name), + existing_instance_with_az(0, zone_3.name), + existing_instance_with_az(1, zone_2.name), + ] + end + let(:deployment) { instance_double(Planner, model: deployment_model, skip_drain: SkipDrain.new(true)) } + let(:deployment_model) { FactoryBot.create(:models_deployment) } + + let(:deployment_network) { ManualNetwork.new('network_A', deployment_subnets, nil, nil) } + let(:deployment_subnets) do + [ + ManualNetworkSubnet.new( + 'network_A', + IPAddr.new('192.168.1.0/24'), + nil, nil, nil, nil, ['zone_1'], [], + [ + to_ipaddr('192.168.1.10'), + to_ipaddr('192.168.1.11'), + to_ipaddr('192.168.1.12'), + to_ipaddr('192.168.1.13'), + to_ipaddr('192.168.1.14'), + ], + nil, nil, + '32' + ), + ManualNetworkSubnet.new( + 'network_A', + IPAddr.new('10.10.1.0/24'), + nil, nil, nil, nil, ['zone_2'], [], + [ + to_ipaddr('10.10.1.10'), + to_ipaddr('10.10.1.11'), + to_ipaddr('10.10.1.12'), + to_ipaddr('10.10.1.13'), + to_ipaddr('10.10.1.14'), + ], + nil, nil, + '32' + ), + ManualNetworkSubnet.new( + 'network_A', + IPAddr.new('10.0.1.0/24'), + nil, nil, nil, nil, ['zone_3'], [], + [ + to_ipaddr('10.0.1.10'), + to_ipaddr('10.0.1.11'), + to_ipaddr('10.0.1.12'), + to_ipaddr('10.0.1.13'), + to_ipaddr('10.0.1.14'), + ], + nil, nil, + '32' + ), + ] + end + let(:job_networks) do + [FactoryBot.build(:deployment_plan_job_network, name: 'network_A', static_ips: job_static_ips, deployment_network: deployment_network)] + end - before do - FactoryBot.create(:models_variable_set, deployment: deployment_model) - end + before do + FactoryBot.create(:models_variable_set, deployment: deployment_model) + end - context 'when job networks include static IPs' do - let(:job_static_ips) { ['192.168.1.10', '192.168.1.11', '10.10.1.10'] } + context 'when job networks include static IPs' do + let(:job_static_ips) do + [ + to_ipaddr('192.168.1.10'), + to_ipaddr('192.168.1.11'), + to_ipaddr('10.10.1.10') + ] + end - it 'places the instances in azs there static IPs are in order of their indexes' do - expect(instance_plans.select(&:new?).map(&:desired_instance).map(&:az)).to eq([zone_1]) + it 'places the instances in azs there static IPs are in order of their indexes' do + expect(instance_plans.select(&:new?).map(&:desired_instance).map(&:az)).to eq([zone_1]) - expect(instance_plans.select(&:existing?).map(&:desired_instance).map(&:az)).to match_array([zone_1, zone_2]) - expect(instance_plans.select(&:existing?).map(&:existing_instance)).to match_array([existing[0], existing[2]]) - expect(instance_plans.select(&:existing?).map(&:desired_instance)).to match_array([desired[0], desired[1]]) + expect(instance_plans.select(&:existing?).map(&:desired_instance).map(&:az)).to match_array([zone_1, zone_2]) + expect(instance_plans.select(&:existing?).map(&:existing_instance)).to match_array([existing[0], existing[2]]) + expect(instance_plans.select(&:existing?).map(&:desired_instance)).to match_array([desired[0], desired[1]]) - expect(instance_plans.select(&:obsolete?).map(&:existing_instance)).to match_array([existing[1]]) + expect(instance_plans.select(&:obsolete?).map(&:existing_instance)).to match_array([existing[1]]) + end end - end - context 'when job networks do not include static IPs' do - let(:job_static_ips) { nil } + context 'when job networks do not include static IPs' do + let(:job_static_ips) { nil } - it 'evenly distributes the instances' do - expect(instance_plans.select(&:new?).map(&:desired_instance).map(&:az)).to eq([zone_1]) + it 'evenly distributes the instances' do + expect(instance_plans.select(&:new?).map(&:desired_instance).map(&:az)).to eq([zone_1]) - expect(instance_plans.select(&:existing?).map(&:existing_instance)).to match_array([existing[0], existing[2]]) - expect(instance_plans.select(&:existing?).map(&:desired_instance)).to match_array([desired[0], desired[1]]) - expect(instance_plans.select(&:existing?).map(&:desired_instance).map(&:az)).to eq([zone_1, zone_2]) + expect(instance_plans.select(&:existing?).map(&:existing_instance)).to match_array([existing[0], existing[2]]) + expect(instance_plans.select(&:existing?).map(&:desired_instance)).to match_array([desired[0], desired[1]]) + expect(instance_plans.select(&:existing?).map(&:desired_instance).map(&:az)).to eq([zone_1, zone_2]) - expect(instance_plans.select(&:obsolete?).map(&:existing_instance)).to match_array([existing[1]]) + expect(instance_plans.select(&:obsolete?).map(&:existing_instance)).to match_array([existing[1]]) + end end - end - def existing_instance_with_az(index, az) - FactoryBot.create(:models_instance, index: index, availability_zone: az) - end + def existing_instance_with_az(index, az) + FactoryBot.create(:models_instance, index: index, availability_zone: az) + end - def desired_instance(zone = nil) - DesiredInstance.new(instance_group, nil, zone, nil) + def desired_instance(zone = nil) + DesiredInstance.new(instance_group, nil, zone, nil) + end end end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker_spec.rb index 693138623b8..a84f37a88cd 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/placement_planner/static_ips_availability_zone_picker_spec.rb @@ -59,7 +59,7 @@ module Bosh::Director::DeploymentPlan def make_subnet_spec(range, static_ips, zone_names) spec = { 'range' => range, - 'gateway' => IPAddr.new(range).to_range.to_a[1].to_string, + 'gateway' => IPAddr.new(range).to_range.to_a[1].to_s, 'dns' => ['8.8.8.8'], 'static' => static_ips, 'reserved' => [], @@ -166,7 +166,7 @@ def make_subnet_spec(range, static_ips, zone_names) expect(obsolete_instance_plans).to eq([]) expect(new_instance_plans.map(&:desired_instance).map(&:az).map(&:name)).to eq(%w[zone1 zone1 zone1]) expect(new_instance_plans.map(&:network_plans).flatten.map(&:reservation).map(&:ip)).to eq( - [ip_to_i('192.168.1.10'), ip_to_i('192.168.1.11'), ip_to_i('192.168.1.12')], + ['192.168.1.10', '192.168.1.11', '192.168.1.12'], ) end end @@ -179,7 +179,7 @@ def make_subnet_spec(range, static_ips, zone_names) expect { instance_plans }.to raise_error( Bosh::Director::InstanceGroupNetworkInstanceIpMismatch, "Instance group 'fake-instance-group' with network 'a' " \ - "declares static ip '192.168.3.5', which belongs to no subnet", + "declares static ip '192.168.3.5/32', which belongs to no subnet", ) end end @@ -240,12 +240,12 @@ def make_subnet_spec(range, static_ips, zone_names) expect(new_instance_plans[0].desired_instance.az.name).to eq('zone1') expect(new_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq( - [ip_to_i('192.168.1.10'), ip_to_i('10.10.1.10')], + [to_ipaddr('192.168.1.10'), to_ipaddr('10.10.1.10')], ) expect(new_instance_plans[1].desired_instance.az.name).to eq('zone1') expect(new_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq( - [ip_to_i('192.168.1.11'), ip_to_i('10.10.1.11')], + [to_ipaddr('192.168.1.11'), to_ipaddr('10.10.1.11')], ) end end @@ -266,12 +266,12 @@ def make_subnet_spec(range, static_ips, zone_names) expect(new_instance_plans[0].desired_instance.az.name).to eq('zone1') expect(new_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq( - [ip_to_i('192.168.1.10'), ip_to_i('10.10.1.10')], + [to_ipaddr('192.168.1.10'), to_ipaddr('10.10.1.10')], ) expect(new_instance_plans[1].desired_instance.az.name).to eq('zone2') expect(new_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq( - [ip_to_i('192.168.2.10'), ip_to_i('10.10.2.10')], + [to_ipaddr('192.168.2.10'), to_ipaddr('10.10.2.10')], ) end end @@ -324,13 +324,13 @@ def make_subnet_spec(range, static_ips, zone_names) network_plans = {} new_instance_plans.map(&:network_plans).flatten.each do |network_plan| network_plans[network_plan.reservation.network.name] ||= [] - network_plans[network_plan.reservation.network.name] << format_ip(network_plan.reservation.ip) + network_plans[network_plan.reservation.network.name] << to_ipaddr(network_plan.reservation.ip) end - expect(network_plans['a']).to match_array(['192.168.1.10', '192.168.1.11', '192.168.1.12', '192.168.2.10']) - expect(network_plans['b']).to match_array(['10.10.1.10', '10.10.1.11', '10.10.2.10', '10.10.2.11']) - expect(network_plans['c']).to match_array(['172.16.1.10', '172.16.2.12', '172.16.2.10', '172.16.2.11']) - expect(network_plans['d']).to match_array(['64.8.1.10', '64.8.2.10', '64.8.3.10', '64.8.3.11']) + expect(network_plans['a']).to match_array([to_ipaddr('192.168.1.10'), to_ipaddr('192.168.1.11'), to_ipaddr('192.168.1.12'), to_ipaddr('192.168.2.10')]) + expect(network_plans['b']).to match_array([to_ipaddr('10.10.1.10'), to_ipaddr('10.10.1.11'), to_ipaddr('10.10.2.10'), to_ipaddr('10.10.2.11')]) + expect(network_plans['c']).to match_array([to_ipaddr('172.16.1.10'), to_ipaddr('172.16.2.12'), to_ipaddr('172.16.2.10'), to_ipaddr('172.16.2.11')]) + expect(network_plans['d']).to match_array([to_ipaddr('64.8.1.10'), to_ipaddr('64.8.2.10'), to_ipaddr('64.8.3.10'), to_ipaddr('64.8.3.11')]) expect(new_instance_plans.map(&:desired_instance).map(&:az).map(&:name)).to match_array(%w[z1 z2 z3 z4]) end @@ -361,8 +361,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:static_ips) { ['192.168.1.10', '192.168.2.10'] } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32']), ] end @@ -371,9 +371,30 @@ def make_subnet_spec(range, static_ips, zone_names) expect(obsolete_instance_plans).to eq([]) expect(existing_instance_plans.size).to eq(2) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.1.10')]) + expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.1.10')]) expect(existing_instance_plans[1].desired_instance.az.name).to eq('zone2') - expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.2.10')]) + expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.2.10')]) + end + end + + context 'when all existing instances match static IPs and AZs but the ips are still stored as integer format' do + let(:desired_instance_count) { 2 } + let(:static_ips) { ['192.168.1.10', '192.168.2.10'] } + let(:existing_instances) do + [ + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32']), + ] + end + + it 'reuses existing instances' do + expect(new_instance_plans).to eq([]) + expect(obsolete_instance_plans).to eq([]) + expect(existing_instance_plans.size).to eq(2) + expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') + expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.1.10')]) + expect(existing_instance_plans[1].desired_instance.az.name).to eq('zone2') + expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.2.10')]) end end @@ -382,8 +403,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:static_ips) { ['192.168.1.10', '192.168.2.10'] } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32']), ] end let(:instance_group_availability_zones) { ['zone1'] } @@ -398,7 +419,7 @@ def make_subnet_spec(range, static_ips, zone_names) end.to raise_error( Bosh::Director::NetworkReservationError, "Existing instance 'fake-instance-group/#{existing_instances[1].index}' " \ - "is using IP '192.168.2.10' in availability zone 'zone2'", + "is using IP '192.168.2.10/32' in availability zone 'zone2'", ) end end @@ -408,8 +429,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:static_ips) { ['192.168.1.14', '192.168.2.14'] } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32']), ] end let(:instance_group_availability_zones) { %w[zone1 zone2] } @@ -420,9 +441,9 @@ def make_subnet_spec(range, static_ips, zone_names) expect(obsolete_instance_plans).to eq([]) expect(existing_instance_plans.size).to eq(2) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.1.14')]) + expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.1.14')]) expect(existing_instance_plans[1].desired_instance.az.name).to eq('zone2') - expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.2.14')]) + expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.2.14')]) end context 'when the instance that was assigned that ip is in ignore state' do @@ -450,14 +471,14 @@ def make_subnet_spec(range, static_ips, zone_names) it 'recreates instance in new AZ with new IP' do expect(new_instance_plans.size).to eq(1) expect(new_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(new_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.1.14')]) + expect(new_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.1.14')]) expect(obsolete_instance_plans.size).to eq(1) expect(obsolete_instance_plans.first.existing_instance).to eq(existing_instances[1]) expect(existing_instance_plans.size).to eq(1) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.1.10')]) + expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.1.10')]) end it 'raises error if removed IP belonged to an ignored instance' do @@ -487,14 +508,14 @@ def make_subnet_spec(range, static_ips, zone_names) end let(:new_subnet_azs) { %w[zone2 zone1] } let(:static_ips) { ['192.168.1.10'] } - let(:existing_instances) { [existing_instance_with_az_and_ips('zone1', ['192.168.1.10'])] } + let(:existing_instances) { [existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32'])] } it 'reuses AZ that existing instance with static IP belongs to' do expect(new_instance_plans).to eq([]) expect(obsolete_instance_plans).to eq([]) expect(existing_instance_plans.size).to eq(1) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.1.10')]) + expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.1.10')]) end context 'when AZ to which instance belongs is removed' do @@ -508,7 +529,7 @@ def make_subnet_spec(range, static_ips, zone_names) end.to raise_error( Bosh::Director::NetworkReservationError, "Existing instance 'fake-instance-group/#{existing_instances[0].index}' " \ - "is using IP '192.168.1.10' in availability zone 'zone1'", + "is using IP '192.168.1.10/32' in availability zone 'zone1'", ) end end @@ -518,8 +539,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:static_ips) { ['192.168.1.10', '192.168.1.11', '192.168.1.12', '192.168.1.13'] } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10']), - existing_instance_with_az_and_ips('zone1', ['192.168.1.12']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.12/32']), ] end it 'should distribute the instances across the azs taking into account the existing instances' do @@ -541,8 +562,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:static_ips) { ['192.168.1.10', '192.168.2.11'] } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32']), ] end @@ -577,10 +598,10 @@ def make_subnet_spec(range, static_ips, zone_names) context 'when all existing instances match specified static ips' do let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '10.10.1.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10', '10.10.2.10']), - existing_instance_with_az_and_ips('zone1', ['192.168.1.11', '10.10.1.11']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.11', '10.10.2.11']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '10.10.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32', '10.10.2.10/32']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.11/32', '10.10.1.11/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.11/32', '10.10.2.11/32']), ] end @@ -591,22 +612,22 @@ def make_subnet_spec(range, static_ips, zone_names) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.1.10'), ip_to_i('10.10.1.10')], + [to_ipaddr('192.168.1.10'), to_ipaddr('10.10.1.10')], ) expect(existing_instance_plans[1].desired_instance.az.name).to eq('zone2') expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.2.10'), ip_to_i('10.10.2.10')], + [to_ipaddr('192.168.2.10'), to_ipaddr('10.10.2.10')], ) expect(existing_instance_plans[2].desired_instance.az.name).to eq('zone1') expect(existing_instance_plans[2].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.1.11'), ip_to_i('10.10.1.11')], + [to_ipaddr('192.168.1.11'), to_ipaddr('10.10.1.11')], ) expect(existing_instance_plans[3].desired_instance.az.name).to eq('zone2') expect(existing_instance_plans[3].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.2.11'), ip_to_i('10.10.2.11')], + [to_ipaddr('192.168.2.11'), to_ipaddr('10.10.2.11')], ) end end @@ -614,10 +635,10 @@ def make_subnet_spec(range, static_ips, zone_names) context 'when some existing instances have IPs that are different from the instance group static IPs' do let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '10.10.1.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.14', '10.10.2.14']), - existing_instance_with_az_and_ips('zone1', ['192.168.1.14', '10.10.1.14']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.11', '10.10.2.11']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '10.10.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.14/32', '10.10.2.14/32']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.14/32', '10.10.1.14/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.11/32', '10.10.2.11/32']), ] end @@ -631,22 +652,22 @@ def make_subnet_spec(range, static_ips, zone_names) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.1.10'), ip_to_i('10.10.1.10')], + [to_ipaddr('192.168.1.10'), to_ipaddr('10.10.1.10')], ) expect(existing_instance_plans[1].desired_instance.az.name).to eq('zone2') expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.2.11'), ip_to_i('10.10.2.11')], + [to_ipaddr('192.168.2.11'), to_ipaddr('10.10.2.11')], ) expect(existing_instance_plans[2].desired_instance.az.name).to eq('zone2') expect(existing_instance_plans[2].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.2.10'), ip_to_i('10.10.2.10')], + [to_ipaddr('192.168.2.10'), to_ipaddr('10.10.2.10')], ) expect(existing_instance_plans[3].desired_instance.az.name).to eq('zone1') expect(existing_instance_plans[3].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.1.11'), ip_to_i('10.10.1.12')], + [to_ipaddr('192.168.1.11'), to_ipaddr('10.10.1.12')], ) end end @@ -655,7 +676,7 @@ def make_subnet_spec(range, static_ips, zone_names) let(:desired_instance_count) { 1 } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '10.10.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '10.10.2.10/32']), ] end let(:a_static_ips) { ['192.168.1.10'] } @@ -668,7 +689,7 @@ def make_subnet_spec(range, static_ips, zone_names) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.1.10'), ip_to_i('10.10.1.10')], + [to_ipaddr('192.168.1.10'), to_ipaddr('10.10.1.10')], ) end @@ -676,8 +697,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:desired_instance_count) { 3 } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '10.10.1.10']), - existing_instance_with_az_and_ips('zone1', ['192.168.1.11', '10.10.1.11']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '10.10.1.10/32']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.11/32', '10.10.1.11/32']), ] end let(:a_static_ips) { ['192.168.1.10 - 192.168.1.11', '192.168.2.10'] } @@ -687,7 +708,7 @@ def make_subnet_spec(range, static_ips, zone_names) expect(new_instance_plans.size).to eq(1) expect(new_instance_plans[0].desired_instance.az.name).to eq('zone2') expect(new_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.2.10'), ip_to_i('10.10.2.10')], + [to_ipaddr('192.168.2.10'), to_ipaddr('10.10.2.10')], ) expect(obsolete_instance_plans).to eq([]) expect(existing_instance_plans.size).to eq(2) @@ -698,9 +719,9 @@ def make_subnet_spec(range, static_ips, zone_names) let(:desired_instance_count) { 2 } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '10.10.1.10']), - existing_instance_with_az_and_ips('zone1', ['192.168.1.11', '10.10.1.11']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10', '10.10.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '10.10.1.10/32']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.11/32', '10.10.1.11/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32', '10.10.2.10/32']), ] end let(:a_static_ips) { ['192.168.1.10', '192.168.2.10'] } @@ -725,8 +746,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '10.10.2.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10', '10.10.2.11']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '10.10.2.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32', '10.10.2.11/32']), ] end let(:a_static_ips) { ['192.168.1.10', '192.168.2.10'] } @@ -765,8 +786,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:desired_instance_count) { 2 } let(:existing_instances) do [ - existing_instance_with_az_and_ips(nil, ['192.168.1.10', '10.10.1.10']), - existing_instance_with_az_and_ips(nil, ['192.168.2.10', '10.10.2.11']), + existing_instance_with_az_and_ips(nil, ['192.168.1.10/32', '10.10.1.10/32']), + existing_instance_with_az_and_ips(nil, ['192.168.2.10/32', '10.10.2.11/32']), ] end let(:a_static_ips) { ['192.168.1.10', '192.168.2.10'] } @@ -780,8 +801,8 @@ def make_subnet_spec(range, static_ips, zone_names) context 'when existing instances have AZs' do let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '10.10.1.10']), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10', '10.10.2.11']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '10.10.1.10/32']), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32', '10.10.2.11/32']), ] end @@ -791,7 +812,7 @@ def make_subnet_spec(range, static_ips, zone_names) end.to raise_error( Bosh::Director::NetworkReservationError, "Existing instance 'fake-instance-group/#{existing_instances[0].index}' " \ - "is using IP '192.168.1.10' in availability zone 'zone1'", + "is using IP '192.168.1.10/32' in availability zone 'zone1'", ) end end @@ -816,8 +837,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:desired_instance_count) { 2 } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.5.10', '10.10.5.10']), - existing_instance_with_az_and_ips('zone1', ['192.168.6.10', '10.10.6.11']), + existing_instance_with_az_and_ips('zone1', ['192.168.5.10/32', '10.10.5.10/32']), + existing_instance_with_az_and_ips('zone1', ['192.168.6.10/32', '10.10.6.11/32']), ] end let(:a_static_ips) { ['192.168.1.10', '192.168.2.10'] } @@ -828,12 +849,12 @@ def make_subnet_spec(range, static_ips, zone_names) expect(existing_instance_plans.size).to eq(2) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.1.10'), ip_to_i('10.10.1.10')], + [to_ipaddr('192.168.1.10'), to_ipaddr('10.10.1.10')], ) expect(existing_instance_plans[1].desired_instance.az.name).to eq('zone1') expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to match_array( - [ip_to_i('192.168.2.10'), ip_to_i('10.10.2.10')], + [to_ipaddr('192.168.2.10'), to_ipaddr('10.10.2.10')], ) end end @@ -848,7 +869,7 @@ def make_subnet_spec(range, static_ips, zone_names) end let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '192.168.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '192.168.2.10/32']), ] end let(:a_static_ips) { ['192.168.1.10 - 192.168.1.11'] } @@ -857,14 +878,14 @@ def make_subnet_spec(range, static_ips, zone_names) expect(new_instance_plans.size).to eq(1) expect(new_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(new_instance_plans[0].network_plans.map(&:reservation).find(&:static?).ip).to eq(ip_to_i('192.168.1.11')) + expect(new_instance_plans[0].network_plans.map(&:reservation).find(&:static?).ip).to eq(to_ipaddr('192.168.1.11')) expect(new_instance_plans[0].network_plans.map(&:reservation).select(&:dynamic?).size).to eq(1) expect(obsolete_instance_plans).to eq([]) expect(existing_instance_plans.size).to eq(1) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(existing_instance_plans[0].network_plans.map(&:reservation).find(&:static?).ip).to eq(ip_to_i('192.168.1.10')) + expect(existing_instance_plans[0].network_plans.map(&:reservation).find(&:static?).ip).to eq(to_ipaddr('192.168.1.10')) expect(existing_instance_plans[0].network_plans.map(&:reservation).select(&:dynamic?).size).to eq(1) end end @@ -882,7 +903,7 @@ def make_subnet_spec(range, static_ips, zone_names) let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10', '192.168.2.10']), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32', '192.168.2.10/32']), ] end @@ -909,8 +930,8 @@ def make_subnet_spec(range, static_ips, zone_names) let(:static_ips) { ['192.168.1.10', '192.168.2.10'] } let(:existing_instances) do [ - existing_instance_with_az_and_ips('zone1', ['192.168.1.10'], 'old-network-name'), - existing_instance_with_az_and_ips('zone2', ['192.168.2.10'], 'old-network-name'), + existing_instance_with_az_and_ips('zone1', ['192.168.1.10/32'], 'old-network-name'), + existing_instance_with_az_and_ips('zone2', ['192.168.2.10/32'], 'old-network-name'), ] end @@ -919,9 +940,9 @@ def make_subnet_spec(range, static_ips, zone_names) expect(obsolete_instance_plans).to eq([]) expect(existing_instance_plans.size).to eq(2) expect(existing_instance_plans[0].desired_instance.az.name).to eq('zone1') - expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.1.10')]) + expect(existing_instance_plans[0].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.1.10')]) expect(existing_instance_plans[1].desired_instance.az.name).to eq('zone2') - expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq([ip_to_i('192.168.2.10')]) + expect(existing_instance_plans[1].network_plans.map(&:reservation).map(&:ip)).to eq([to_ipaddr('192.168.2.10')]) end it 'should fail if ignored instances belonged to that network' do @@ -952,7 +973,7 @@ def existing_instance_with_az_and_ips(az, ips, network_name = 'a') ips.each do |ip| instance.add_ip_address( FactoryBot.create(:models_ip_address, - address_str: IPAddr.new(ip).to_i.to_s, + address_str: ip, network_name: network_name, ), ) diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/commit_instance_network_settings_step_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/commit_instance_network_settings_step_spec.rb index a80027ed712..6f4026d51b4 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/commit_instance_network_settings_step_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/commit_instance_network_settings_step_spec.rb @@ -4,6 +4,7 @@ module Bosh::Director module DeploymentPlan module Steps describe CommitInstanceNetworkSettingsStep do + include IpUtil subject(:step) { described_class.new } let(:report) { Stages::Report.new.tap { |report| report.vm = vm } } let(:network_plans) do @@ -14,13 +15,13 @@ module Steps ] end - let(:existing_ip_address_string) { 'existing_ip_address_string' } - let(:obsolete_ip_address_string) { 'obsolete_ip_address_string' } - let(:desired_ip_address_string) { 'desired_ip_address_string' } + let(:existing_ip_address_string) { '1.1.1.1/32' } + let(:obsolete_ip_address_string) { '2.2.2.2/32' } + let(:desired_ip_address_string) { '3.3.3.3/32' } - let(:existing_reservation) { instance_double(NetworkReservation, ip: existing_ip_address_string) } - let(:obsolete_reservation) { instance_double(NetworkReservation, ip: obsolete_ip_address_string) } - let(:desired_reservation) { instance_double(NetworkReservation, ip: desired_ip_address_string) } + let(:existing_reservation) { instance_double(NetworkReservation, ip: to_ipaddr(existing_ip_address_string)) } + let(:obsolete_reservation) { instance_double(NetworkReservation, ip: to_ipaddr(obsolete_ip_address_string)) } + let(:desired_reservation) { instance_double(NetworkReservation, ip: to_ipaddr(desired_ip_address_string)) } let!(:existing_ip_address) { FactoryBot.create(:models_ip_address, address_str: existing_ip_address_string) } let!(:obsolete_ip_address) { FactoryBot.create(:models_ip_address, address_str: obsolete_ip_address_string) } diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb index a42255fe256..921d10dc7c0 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/steps/create_vm_step_spec.rb @@ -146,8 +146,8 @@ module Steps end let(:reservation) do - subnet = Bosh::Director::DeploymentPlan::DynamicNetworkSubnet.new('dns', network_cloud_properties, ['az-1']) - network = Bosh::Director::DeploymentPlan::DynamicNetwork.new('name', [subnet], per_spec_logger) + subnet = Bosh::Director::DeploymentPlan::DynamicNetworkSubnet.new('dns', network_cloud_properties, ['az-1'], '32') + network = Bosh::Director::DeploymentPlan::DynamicNetwork.new('name', [subnet], '32', per_spec_logger) reservation = Bosh::Director::DesiredNetworkReservation.new_dynamic(instance_model, network) reservation end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_spec.rb index 2e05982e416..4b1ddbc926f 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_spec.rb @@ -103,7 +103,7 @@ it 'returns the availability zones associated with the given ip' do network = Bosh::Director::DeploymentPlan::VipNetwork.parse(network_spec, azs, per_spec_logger) - az = network.find_az_names_for_ip(IPAddr.new('69.69.69.69').to_i) + az = network.find_az_names_for_ip(to_ipaddr('69.69.69.69')) expect(az).to include('z1') end end diff --git a/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_subnet_spec.rb b/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_subnet_spec.rb index 11bc9a5520a..443233427f8 100644 --- a/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_subnet_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/deployment_plan/vip_network_subnet_spec.rb @@ -27,7 +27,7 @@ expect(vip_subnet.static_ips.size).to eq(5) vip_static_ips = vip_subnet.static_ips.map do |static_ip| - format_ip(static_ip) + to_ipaddr(static_ip) end expect(vip_static_ips).to include( '69.69.69.69', diff --git a/src/bosh-director/spec/unit/bosh/director/ip_addr_or_cidr_spec.rb b/src/bosh-director/spec/unit/bosh/director/ip_addr_or_cidr_spec.rb index 9f097e31b99..d09abc9fe1d 100644 --- a/src/bosh-director/spec/unit/bosh/director/ip_addr_or_cidr_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/ip_addr_or_cidr_spec.rb @@ -60,12 +60,12 @@ module Bosh::Director end end - describe '#to_cidr_s' do + describe '#to_s' do context 'IPv4' do let(:input) { '192.168.0.0/24' } it 'returns a string representing the IP' do - expect(ip_addr_or_cidr.to_cidr_s).to eq(input) + expect(ip_addr_or_cidr.to_s).to eq(input) end end @@ -73,7 +73,7 @@ module Bosh::Director let(:input) { 'fd00::/8' } it 'returns a string representing the IP' do - expect(ip_addr_or_cidr.to_cidr_s).to eq(input) + expect(ip_addr_or_cidr.to_s).to eq(input) end end end @@ -96,38 +96,38 @@ module Bosh::Director end end - describe '#to_s' do + describe '#base_addr' do context 'IPv4' do let(:input) { '10.20.0.32' } it 'returns a string representing the IP' do - expect(ip_addr_or_cidr.to_s).to eq(input) + expect(ip_addr_or_cidr.base_addr).to eq(input) end end context 'IPv6' do - let(:input) { '2001:0db8:85a3:7334:8a2e:0000:0000:0000' } + let(:input) { '2001:db8:85a3:7334:8a2e::' } it 'returns a string representing the IP' do - expect(ip_addr_or_cidr.to_s).to eq(input) + expect(ip_addr_or_cidr.base_addr).to eq(input) end end end - describe '#to_string' do + describe '#to_s' do context 'IPv4' do - let(:input) { '10.20.0.32' } + let(:input) { '10.20.0.32/32' } it 'returns a string representing the IP' do - expect(ip_addr_or_cidr.to_string).to eq(input) + expect(ip_addr_or_cidr.to_s).to eq(input) end end context 'IPv6' do - let(:input) { '2001:0db8:85a3:7334:8a2e:0000:0000:0000' } + let(:input) { '2001:db8:85a3:7334:8a2e::/128' } it 'returns a string representing the IP' do - expect(ip_addr_or_cidr.to_string).to eq(input) + expect(ip_addr_or_cidr.to_s).to eq(input) end end end diff --git a/src/bosh-director/spec/unit/bosh/director/ip_util_spec.rb b/src/bosh-director/spec/unit/bosh/director/ip_util_spec.rb index 6a917afca27..9f3e267f6f6 100644 --- a/src/bosh-director/spec/unit/bosh/director/ip_util_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/ip_util_spec.rb @@ -10,41 +10,151 @@ end describe 'each_ip' do - it 'should handle single ip' do - counter = 0 - ip_util_includer.each_ip('1.2.3.4') do |ip| - expect(ip).to eql(IPAddr.new('1.2.3.4').to_i) - counter += 1 + context 'when expanding is turned on' do + it 'should handle single ip' do + counter = 0 + ip_util_includer.each_ip('1.2.3.4') do |ip| + expect(ip).to eq(to_ipaddr('1.2.3.4').to_i) + counter += 1 + end + expect(counter).to eq(1) end - expect(counter).to eq(1) - end - it 'should handle a range' do - counter = 0 - ip_util_includer.each_ip('1.0.0.0/24') do |ip| - expect(ip).to eql(IPAddr.new('1.0.0.0').to_i + counter) - counter += 1 + it 'should handle a range' do + counter = 0 + ip_util_includer.each_ip('1.0.0.0/24') do |ip| + expect(ip).to eq(to_ipaddr('1.0.0.0').to_i + counter) + counter += 1 + end + expect(counter).to eq(256) + end + + it 'should handle a differently formatted range' do + counter = 0 + ip_util_includer.each_ip('1.0.0.0 - 1.0.1.0') do |ip| + expect(ip).to eq(to_ipaddr('1.0.0.0').to_i + counter) + counter += 1 + end + expect(counter).to eq(257) end - expect(counter).to eq(256) end - it 'should handle a differently formatted range' do - counter = 0 - ip_util_includer.each_ip('1.0.0.0 - 1.0.1.0') do |ip| - expect(ip).to eql(IPAddr.new('1.0.0.0').to_i + counter) - counter += 1 + context 'when expanding is turned off' do + it 'should handle a range' do + counter = 0 + ip_util_includer.each_ip('1.0.0.0/24', false) do |ip| + expect(ip).to eq(to_ipaddr('1.0.0.0').to_i + counter) + expect(ip.prefix).to eq(24) + counter += 1 + end + expect(counter).to eq(1) + end + + it 'formats the ips to cidr blocks' do + counter = 0 + ip_util_includer.each_ip('1.0.0.0 - 1.0.1.0', false) do |ip| + if counter == 0 + expect(ip).to eq(to_ipaddr("1.0.0.0")) + expect(ip.prefix).to eq(24) + elsif counter == 1 + expect(ip).to eq(to_ipaddr("1.0.1.0")) + expect(ip.prefix).to eq(32) + else + raise "Unexpected counter value: #{counter}" + end + + counter += 1 + end + expect(counter).to eq(2) + end + + it 'formats the ips to cidr blocks' do + counter = 0 + ip_util_includer.each_ip('1.0.0.5 - 1.0.0.98', false) do |ip| + if counter == 0 + expect(ip).to eq(to_ipaddr('1.0.0.5').to_i) + expect(ip.prefix).to eq(32) + elsif counter == 1 + expect(ip).to eq(to_ipaddr('1.0.0.6').to_i) + expect(ip.prefix).to eq(31) + elsif counter == 2 + expect(ip).to eq(to_ipaddr('1.0.0.8').to_i) + expect(ip.prefix).to eq(29) + elsif counter == 3 + expect(ip).to eq(to_ipaddr('1.0.0.16').to_i) + expect(ip.prefix).to eq(28) + elsif counter == 4 + expect(ip).to eq(to_ipaddr('1.0.0.32').to_i) + expect(ip.prefix).to eq(27) + elsif counter == 5 + expect(ip).to eq(to_ipaddr('1.0.0.64').to_i) + expect(ip.prefix).to eq(27) + elsif counter == 6 + expect(ip).to eq(to_ipaddr('1.0.0.96').to_i) + expect(ip.prefix).to eq(31) + elsif counter == 7 + expect(ip).to eq(to_ipaddr('1.0.0.98').to_i) + expect(ip.prefix).to eq(32) + else + raise "Unexpected counter value: #{counter}" + end + counter += 1 + + end + expect(counter).to eq(8) + end + + it 'formats the ips to cidr blocks for ipv6' do + counter = 0 + ip_util_includer.each_ip('2001:db8::5 - 2001:db8::e', false) do |ip| + if counter == 0 + expect(ip).to eq(to_ipaddr('2001:db8::5').to_i + counter) + expect(ip.prefix).to eq(128) + elsif counter == 1 + expect(ip).to eq(to_ipaddr('2001:db8::6').to_i) + expect(ip.prefix).to eq(127) + elsif counter == 2 + expect(ip).to eq(to_ipaddr('2001:db8::8').to_i) + expect(ip.prefix).to eq(126) + elsif counter == 3 + expect(ip).to eq(to_ipaddr('2001:db8::c').to_i) + expect(ip.prefix).to eq(127) + elsif counter == 4 + expect(ip).to eq(to_ipaddr('2001:db8::e').to_i) + expect(ip.prefix).to eq(128) + else + raise "Unexpected counter value: #{counter}" + end + counter += 1 + + end + expect(counter).to eq(5) + end + + it 'formats the ips to cidr blocks for ipv6' do + counter = 0 + ip_util_includer.each_ip('2001:db8:: - 2001:db8::ff', false) do |ip| + if counter == 0 + expect(ip).to eq(to_ipaddr('2001:db8::').to_i) + expect(ip.prefix).to eq(120) + else + raise "Unexpected counter value: #{counter}" + end + counter += 1 + + end + expect(counter).to eq(1) end - expect(counter).to eq(257) end it 'should not accept invalid input' do - expect { ip_util_includer.each_ip('1.2.4') }.to raise_error(Bosh::Director::NetworkInvalidIpRangeFormat, /invalid address/) + expect { ip_util_includer.each_ip('1.2.4') }.to raise_error(Bosh::Director::NetworkInvalidIpRangeFormat, /Invalid IP or CIDR format/) end it 'should ignore nil values' do counter = 0 ip_util_includer.each_ip(nil) do |ip| - expect(ip).to eql(IPAddr.new('1.2.3.4').to_i) + expect(ip).to eq(to_ipaddr('1.2.3.4').to_i) counter += 1 end expect(counter).to eq(0) @@ -65,12 +175,6 @@ end end - describe 'format_ip' do - it 'converts integer to CIDR IP' do - expect(ip_util_includer.format_ip(168427582)).to eq('10.10.0.62') - end - end - describe 'ip_address?' do it 'verifies ip address' do expect(ip_util_includer.ip_address?('127.0.0.1')).to eq(true) diff --git a/src/bosh-director/spec/unit/bosh/director/jobs/vm_state_spec.rb b/src/bosh-director/spec/unit/bosh/director/jobs/vm_state_spec.rb index 36f54d641b6..8958e67452e 100644 --- a/src/bosh-director/spec/unit/bosh/director/jobs/vm_state_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/jobs/vm_state_spec.rb @@ -2,6 +2,7 @@ module Bosh::Director describe Jobs::VmState do + include IpUtil def stub_agent_get_state_to_return_state_with_vitals expect(agent).to receive(:get_state).with('full').and_return( 'vm_cid' => 'fake-vm-cid', @@ -55,7 +56,7 @@ def stub_agent_get_state_to_return_state_with_vitals FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('1.1.1.1').to_i.to_s, + address_str: to_ipaddr('1.1.1.1').to_s, task_id: '12345', ) expect(agent).to receive(:get_state).with('full').and_return( @@ -82,13 +83,13 @@ def stub_agent_get_state_to_return_state_with_vitals FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('1.1.1.1').to_i.to_s, + address_str: to_ipaddr('1.1.1.1').to_s, task_id: '12345', ) FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('2.2.2.2').to_i.to_s, + address_str: to_ipaddr('2.2.2.2').to_s, task_id: '12345', ) end @@ -104,7 +105,7 @@ def stub_agent_get_state_to_return_state_with_vitals context "when 'ip_addresses' is empty for instance" do before do - vm.network_spec = { 'a' => { 'ip' => '3.3.3.3' }, 'b' => { 'ip' => '4.4.4.4' } } + vm.network_spec = { 'a' => { 'ip' => '3.3.3.3/32' }, 'b' => { 'ip' => '4.4.4.4/32' } } vm.save instance.spec = { 'networks' => { 'a' => { 'ip' => '1.1.1.1' }, 'b' => { 'ip' => '2.2.2.2' } } } instance.save @@ -125,19 +126,19 @@ def stub_agent_get_state_to_return_state_with_vitals FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('1.1.1.1').to_i.to_s, + address_str: to_ipaddr('1.1.1.1').to_s, task_id: '12345', ) FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('2.2.2.2').to_i.to_s, + address_str: to_ipaddr('2.2.2.2').to_s, task_id: '12345', ) vm.network_spec = { - 'a' => { 'ip' => '3.3.3.3' }, - 'b' => { 'ip' => '4.4.4.4' }, + 'a' => { 'ip' => '3.3.3.3/32' }, + 'b' => { 'ip' => '4.4.4.4/32' }, } vm.save @@ -168,7 +169,7 @@ def stub_agent_get_state_to_return_state_with_vitals FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('1.1.1.1').to_i.to_s, + address_str: to_ipaddr('1.1.1.1').to_s, task_id: '12345', ) stub_agent_get_state_to_return_state_with_vitals @@ -359,7 +360,7 @@ def stub_agent_get_state_to_return_state_with_vitals FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('1.1.1.1').to_i.to_s, + address_str: to_ipaddr('1.1.1.1').to_s, task_id: '12345', ) instance.update(spec: { 'vm_type' => { 'name' => 'fake-vm-type', 'cloud_properties' => {} } }) @@ -440,13 +441,13 @@ def stub_agent_get_state_to_return_state_with_vitals FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: vm.id, - address_str: IPAddr.new('1.1.1.1').to_i.to_s, + address_str: to_ipaddr('1.1.1.1').to_s, task_id: '12345', ) FactoryBot.create(:models_ip_address, instance_id: instance.id, vm_id: inactive_vm.id, - address_str: IPAddr.new('1.1.1.2').to_i.to_s, + address_str: to_ipaddr('1.1.1.2').to_s, task_id: '12345', ) allow(AgentClient).to receive(:with_agent_id).with('other_agent_id', anything, timeout: 5).and_return(lazy_agent) diff --git a/src/bosh-director/spec/unit/bosh/director/models/ip_address_spec.rb b/src/bosh-director/spec/unit/bosh/director/models/ip_address_spec.rb index 6531d13e5e5..caa100ea422 100644 --- a/src/bosh-director/spec/unit/bosh/director/models/ip_address_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/models/ip_address_spec.rb @@ -1,84 +1,87 @@ require 'spec_helper' -module Bosh::Director::Models - describe IpAddress do - subject(:ip_address) do - IpAddress.new(instance: instance, - network_name: 'foonetwork', - address_str: IPAddr.new('10.10.0.1').to_i.to_s, - task_id: 'fake-task-id', - static: true, - vm: vm) - end - let(:instance) { FactoryBot.create(:models_instance, job: 'foojob', index: 1, deployment: deployment) } - let(:vm) { FactoryBot.create(:models_vm, instance: instance) } - let(:deployment) { FactoryBot.create(:models_deployment, name: 'foodeployment') } - - context '#info' do - it 'should display debugging information (job, index, network name and ip address)' do - expect(ip_address.info).to eq('foodeployment.foojob/1 - foonetwork - 10.10.0.1 (static)') +module Bosh::Director + module Models + describe IpAddress do + include IpUtil + subject(:ip_address) do + IpAddress.new(instance: instance, + network_name: 'foonetwork', + address_str: to_ipaddr('10.10.0.1/32').to_s, + task_id: 'fake-task-id', + static: true, + vm: vm) end - end - - context 'validations' do - it 'be valid with just an orphaned_vm_id' do - ip_address.instance_id = nil - ip_address.vm_id = nil - ip_address.orphaned_vm_id = 111 - - expect { ip_address.save }.not_to raise_error + let(:instance) { FactoryBot.create(:models_instance, job: 'foojob', index: 1, deployment: deployment) } + let(:vm) { FactoryBot.create(:models_vm, instance: instance) } + let(:deployment) { FactoryBot.create(:models_deployment, name: 'foodeployment') } + + context '#info' do + it 'should display debugging information (job, index, network name and ip address)' do + expect(ip_address.info).to eq('foodeployment.foojob/1 - foonetwork - 10.10.0.1/32 (static)') + end end - it 'be valid with just an instance_id' do - ip_address.vm_id = nil + context 'validations' do + it 'be valid with just an orphaned_vm_id' do + ip_address.instance_id = nil + ip_address.vm_id = nil + ip_address.orphaned_vm_id = 111 - expect { ip_address.save }.not_to raise_error - end + expect { ip_address.save }.not_to raise_error + end - it 'should require ip address' do - ip_address.address_str = "" - expect { ip_address.save }.to raise_error /address_str presence/ + it 'be valid with just an instance_id' do + ip_address.vm_id = nil - ip_address.address_str = IPAddr.new('10.10.0.1').to_i.to_s - expect { ip_address.save }.not_to raise_error - end + expect { ip_address.save }.not_to raise_error + end - it 'must have either an instance_id ord orphaned_vm_id' do - ip_address.instance_id = nil - ip_address.vm_id = nil - ip_address.orphaned_vm_id = nil + it 'should require ip address' do + ip_address.address_str = "" + expect { ip_address.save }.to raise_error /address_str presence/ - expect { ip_address.save }.to raise_error('No instance or orphaned VM associated with IP') - end - - it 'cannot have both instance_id and orphaned_vm_id' do - ip_address.instance_id = instance.id - ip_address.vm_id = nil - ip_address.orphaned_vm_id = 111 + ip_address.address_str = IPAddr.new('10.10.0.1').to_i.to_s + expect { ip_address.save }.not_to raise_error + end - expect { ip_address.save }.to raise_error('IP address cannot have both instance id and orphaned VM id') - end - end + it 'must have either an instance_id ord orphaned_vm_id' do + ip_address.instance_id = nil + ip_address.vm_id = nil + ip_address.orphaned_vm_id = nil - describe '#address' do - it 'returns address in int form from address str' do - expect(ip_address.address).to eq(168427521) - end + expect { ip_address.save }.to raise_error('No instance or orphaned VM associated with IP') + end - it 'raises an error when the address is an empty string' do - ip_address.address_str = "" - expect { ip_address.address }.to raise_error(/Unexpected address/) - end + it 'cannot have both instance_id and orphaned_vm_id' do + ip_address.instance_id = instance.id + ip_address.vm_id = nil + ip_address.orphaned_vm_id = 111 - it 'raises an error when the address is a string that does not contain an integer' do - ip_address.address_str = "168427521a" - expect { ip_address.address }.to raise_error(/Unexpected address '168427521a'/) + expect { ip_address.save }.to raise_error('IP address cannot have both instance id and orphaned VM id') + end end - it 'raises an error when the address is padded' do - ip_address.address_str = " 168427521 " - expect { ip_address.address }.to raise_error(/Unexpected address ' 168427521 '/) + describe '#address' do + it 'returns address in int form from address str' do + expect(ip_address.address).to eq(168427521) + end + + it 'raises an error when the address is an empty string' do + ip_address.address_str = "" + expect { ip_address.address }.to raise_error(/Unexpected address/) + end + + it 'raises an error when the address is a string that does not contain an integer' do + ip_address.address_str = "168427521a" + expect { ip_address.address }.to raise_error(/Unexpected address '168427521a'/) + end + + it 'raises an error when the address is padded' do + ip_address.address_str = " 168427521 " + expect { ip_address.address }.to raise_error(/Unexpected address ' 168427521 '/) + end end end end -end +end \ No newline at end of file diff --git a/src/bosh-director/spec/unit/bosh/director/models/vm_spec.rb b/src/bosh-director/spec/unit/bosh/director/models/vm_spec.rb index b7c5a11f6ba..e65c1f1775d 100644 --- a/src/bosh-director/spec/unit/bosh/director/models/vm_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/models/vm_spec.rb @@ -1,53 +1,56 @@ require 'spec_helper' -module Bosh::Director::Models - describe Vm do - subject(:vm) { FactoryBot.create(:models_vm, instance: instance) } +module Bosh::Director + module Models + describe Vm do + include IpUtil + subject(:vm) { FactoryBot.create(:models_vm, instance: instance) } - let!(:instance) { FactoryBot.create(:models_instance) } + let!(:instance) { FactoryBot.create(:models_instance) } - it 'has a many-to-one relationship to instances' do - FactoryBot.create(:models_vm, instance_id: instance.id, id: 1) - FactoryBot.create(:models_vm, instance_id: instance.id, id: 2) + it 'has a many-to-one relationship to instances' do + FactoryBot.create(:models_vm, instance_id: instance.id, id: 1) + FactoryBot.create(:models_vm, instance_id: instance.id, id: 2) - expect(Vm.find(id: 1).instance).to eq(instance) - expect(Vm.find(id: 2).instance).to eq(instance) - end + expect(Vm.find(id: 1).instance).to eq(instance) + expect(Vm.find(id: 2).instance).to eq(instance) + end - describe '#network_spec' do - it 'unmarshals network_spec_json' do - vm.network_spec_json = JSON.dump('some' => 'spec') + describe '#network_spec' do + it 'unmarshals network_spec_json' do + vm.network_spec_json = JSON.dump('some' => 'spec') - expect(vm.network_spec).to eq('some' => 'spec') - end + expect(vm.network_spec).to eq('some' => 'spec') + end - context 'when network_spec_json is nil' do - it 'returns empty hash' do - vm.network_spec_json = nil + context 'when network_spec_json is nil' do + it 'returns empty hash' do + vm.network_spec_json = nil - expect(vm.network_spec).to eq({}) + expect(vm.network_spec).to eq({}) + end end end - end - describe '#network_spec=' do - it 'sets network_spec_json with json-ified value' do - vm.network_spec = { 'some' => 'spec' } + describe '#network_spec=' do + it 'sets network_spec_json with json-ified value' do + vm.network_spec = { 'some' => 'spec' } - expect(vm.network_spec_json).to eq(JSON.dump('some' => 'spec')) + expect(vm.network_spec_json).to eq(JSON.dump('some' => 'spec')) + end end - end - describe '#ips' do - let!(:ip_address) { FactoryBot.create(:models_ip_address, vm: vm, address_str: IPAddr.new('1.1.1.1').to_i.to_s) } - let!(:ip_address2) { FactoryBot.create(:models_ip_address, vm: vm, address_str: IPAddr.new('1.1.1.2').to_i.to_s) } + describe '#ips' do + let!(:ip_address) { FactoryBot.create(:models_ip_address, vm: vm, address_str: to_ipaddr('1.1.1.1/32').to_s) } + let!(:ip_address2) { FactoryBot.create(:models_ip_address, vm: vm, address_str: to_ipaddr('1.1.1.2/32').to_s) } - before do - vm.network_spec = { 'some' => { 'ip' => '1.1.1.3' } } - end + before do + vm.network_spec = { 'some' => { 'ip' => '1.1.1.3/32' } } + end - it 'returns all ips for the vm' do - expect(vm.ips).to match_array(['1.1.1.1', '1.1.1.2', '1.1.1.3']) + it 'returns all ips for the vm' do + expect(vm.ips).to match_array(['1.1.1.1', '1.1.1.2', '1.1.1.3']) + end end end end diff --git a/src/bosh-director/spec/unit/bosh/director/orphaned_vm_deleter_spec.rb b/src/bosh-director/spec/unit/bosh/director/orphaned_vm_deleter_spec.rb index e3d74802c8a..cd546866464 100644 --- a/src/bosh-director/spec/unit/bosh/director/orphaned_vm_deleter_spec.rb +++ b/src/bosh-director/spec/unit/bosh/director/orphaned_vm_deleter_spec.rb @@ -3,6 +3,7 @@ module Bosh module Director describe OrphanedVMDeleter do + include IpUtil subject { OrphanedVMDeleter.new(per_spec_logger) } describe '#delete_all' do @@ -20,7 +21,7 @@ module Director Bosh::Director::Models::IpAddress.create( orphaned_vm: orphaned_vm1, network_name: 'my-manual-network', - address_str: IPAddr.new('127.0.0.2').to_i, + address_str: to_ipaddr('127.0.0.2/32').to_s, task_id: 1, ) end @@ -38,7 +39,7 @@ module Director Bosh::Director::Models::IpAddress.create( orphaned_vm: orphaned_vm2, network_name: 'my-manual-network', - address_str: IPAddr.new('127.0.0.1').to_i, + address_str: to_ipaddr('127.0.0.1/32').to_s, task_id: 1, ) end diff --git a/src/spec/integration/global_networking/failing_deploy_spec.rb b/src/spec/integration/global_networking/failing_deploy_spec.rb index 93e6b92b6c2..99e7b525fed 100644 --- a/src/spec/integration/global_networking/failing_deploy_spec.rb +++ b/src/spec/integration/global_networking/failing_deploy_spec.rb @@ -4,7 +4,7 @@ def make_subnet_spec(range, static_ips, zone_names = nil) spec = { 'range' => range, - 'gateway' => IPAddr.new(range).to_range.to_a[1].to_string, + 'gateway' => IPAddr.new(range).to_range.to_a[1].to_s, 'dns' => ['8.8.8.8'], 'static' => static_ips, 'reserved' => [], diff --git a/src/spec/shared/shared_support/deployment_manifest_helper.rb b/src/spec/shared/shared_support/deployment_manifest_helper.rb index 068dfa26441..9db3edd9434 100644 --- a/src/spec/shared/shared_support/deployment_manifest_helper.rb +++ b/src/spec/shared/shared_support/deployment_manifest_helper.rb @@ -17,6 +17,13 @@ def self.simple_cloud_config ) end + def self.simple_cloud_config_ipv6 + minimal_cloud_config.merge( + 'networks' => [network_ipv6], + 'vm_types' => [vm_type], + ) + end + def self.minimal_cloud_config { 'networks' => [{ @@ -42,6 +49,13 @@ def self.network(options = {}) }.merge!(options) end + def self.network_ipv6(options = {}) + { + 'name' => 'a', + 'subnets' => [subnet_ipv6], + }.merge!(options) + end + # TODO: used by bosh-director def self.subnet(options = {}) { @@ -54,6 +68,18 @@ def self.subnet(options = {}) }.merge!(options) end + # TODO: used by bosh-director + def self.subnet_ipv6(options = {}) + { + 'range' => '2001:db8::/112', + 'gateway' => '2001:db8::1', + 'dns' => ['fd00:ec2::253'], + 'static' => ['2001:db8::10'], + 'reserved' => [], + 'cloud_properties' => {}, + }.merge!(options) + end + # TODO: used by bosh-director def self.vm_type { @@ -991,7 +1017,7 @@ def self.make_subnet(opts) range_string = opts.fetch(:range, '192.168.1.0/24') range_ip_addr = IPAddr.new(range_string) - ip_range = range_ip_addr.to_range.to_a.map(&:to_string) + ip_range = range_ip_addr.to_range.to_a.map(&:to_s) ip_range_shift = opts.fetch(:shift_ip_range_by, 0) available_ips = opts.fetch(:available_ips) raise "not enough IPs, don't be so greedy" if available_ips > ip_range.size