Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fixes #16951 - ipv6 compute orchestration #3943

Merged
merged 1 commit into from Nov 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions app/helpers/hosts_helper.rb
Expand Up @@ -257,6 +257,7 @@ def overview_fields(host)
fields += [[_("Domain"), (link_to(host.domain, hosts_path(:search => "domain = #{host.domain}")))]] if host.domain.present?
fields += [[_("Realm"), (link_to(host.realm, hosts_path(:search => "realm = #{host.realm}")))]] if host.realm.present?
fields += [[_("IP Address"), host.ip]] if host.ip.present?
fields += [[_("IPv6 Address"), host.ip6]] if host.ip6.present?
fields += [[_("MAC Address"), host.mac]] if host.mac.present?
fields += [[_("Puppet Environment"), (link_to(host.environment, hosts_path(:search => "environment = #{host.environment}")))]] if host.environment.present?
fields += [[_("Host Architecture"), (link_to(host.arch, hosts_path(:search => "architecture = #{host.arch}")))]] if host.arch.present?
Expand Down
67 changes: 46 additions & 21 deletions app/models/concerns/orchestration/compute.rb
Expand Up @@ -33,6 +33,10 @@ def ip_available?
ip.present? || compute_provides?(:ip)
end

def ip6_available?
ip6.present? || compute_provides?(:ip6)
end

def mac_available?
mac.present? || compute_provides?(:mac)
end
Expand All @@ -53,8 +57,8 @@ def queue_compute_create
:action => [self, :setUserData]) if find_image.try(:user_data)
queue.create(:name => _("Set up compute instance %s") % self, :priority => 2,
:action => [self, :setCompute])
queue.create(:name => _("Acquire IP address for %s") % self, :priority => 3,
:action => [self, :setComputeIP]) if compute_provides?(:ip)
queue.create(:name => _("Acquire IP addresses for %s") % self, :priority => 3,
:action => [self, :setComputeIP]) if compute_provides?(:ip) || compute_provides?(:ip6)
queue.create(:name => _("Query instance details for %s") % self, :priority => 4,
:action => [self, :setComputeDetails])
queue.create(:name => _("Power up compute instance %s") % self, :priority => 1000,
Expand Down Expand Up @@ -124,16 +128,21 @@ def setComputeDetails
attrs.each do |foreman_attr, fog_attr|
if foreman_attr == :mac
return false unless match_macs_to_nics(fog_attr)
elsif foreman_attr == :ip
value = vm.send(fog_attr) || find_address
elsif [:ip, :ip6].include?(foreman_attr)
value = vm.send(fog_attr) || find_address(foreman_attr)
self.send("#{foreman_attr}=", value)
return false unless validate_foreman_attr(value, ::Nic::Base, foreman_attr)
return false if self.send(foreman_attr).present? && !validate_foreman_attr(value, ::Nic::Base, foreman_attr)
else
value = vm.send(fog_attr)
self.send("#{foreman_attr}=", value)
return false unless validate_foreman_attr(value, Host, foreman_attr)
return false unless validate_required_foreman_attr(value, Host, foreman_attr)
end
end

if ip.blank? && ip6.blank? && (compute_provides?(:ip) || compute_provides?(:ip6))
return failure(_("Failed to acquire IP addresses from compute resource for %s") % name)
end

true
else
failure _("failed to save %s") % name
Expand All @@ -144,12 +153,14 @@ def delComputeDetails; end

def setComputeIP
attrs = compute_resource.provided_attributes
if attrs.keys.include?(:ip)
if attrs.keys.include?(:ip) || attrs.keys.include?(:ip6)
logger.info "Waiting for #{name} to become ready"
vm.wait_for { self.ready? }
logger.info "waiting for instance to acquire ip address"
vm.wait_for do
self.send(attrs[:ip]).present? || self.ip_addresses.present?
(attrs.keys.include?(:ip) && self.send(attrs[:ip]).present?) ||
(attrs.keys.include?(:ip6) && self.send(attrs[:ip6]).present?) ||
self.ip_addresses.present?
end
end
rescue => e
Expand Down Expand Up @@ -225,19 +236,25 @@ def validate_compute_provisioning
end
end

def find_address
def find_address(type)
vm_addresses = filter_ip_addresses(vm.ip_addresses, type)

# We can exit early if the host already has any kind of ip and the vm does not
# provide one for this kind to speed up things
return if (ip.present? || ip6.present?) && vm_addresses.empty?

# We need to return fast for user-data, so that we save the host before
# cloud-init finishes, even if the IP is not reachable by Foreman. We do have
# to return a real IP though, or Foreman will fail to save the host.
return vm.ip_addresses.first if (vm.ip_addresses.present? && self.compute_attributes[:user_data].present?)
return vm_addresses.first if (vm_addresses.present? && self.compute_attributes[:user_data].present?)

# Loop over the addresses waiting for one to come up
ip = nil
begin
Timeout.timeout(120) do
until ip
ip = vm.ip_addresses.find { |addr| ssh_open?(addr) }
sleep 2
ip = filter_ip_addresses(vm.ip_addresses, type).detect { |addr| ssh_open?(addr) }
sleep 2 unless ip
end
end
rescue Timeout::Error
Expand All @@ -246,12 +263,17 @@ def find_address
# images do require an IP, but it's more accurate to return something here
# if we have it, and let the SSH orchestration fail (and notify) for an
# unreachable IP
ip = vm.ip_addresses.first if ip.blank?
logger.info "acquisition of ip address timed out, using #{ip}"
ip = filter_ip_addresses(vm.ip_addresses, type).first if ip.blank?
logger.info "acquisition of #{type} address timed out, using #{ip}"
end
ip
end

def filter_ip_addresses(addresses, type)
check_method = type == :ip6 ? :ipv6? : :ipv4?
addresses.map {|ip| IPAddr.new(ip) rescue nil}.compact.select(&check_method).map(&:to_s)
end

def ssh_open?(ip)
begin
Timeout.timeout(1) do
Expand All @@ -269,20 +291,23 @@ def ssh_open?(ip)
false
end

def validate_foreman_attr(value,object,attr)
def validate_foreman_attr(value, object, attr)
# we can't ensure uniqueness of #foreman_attr using normal rails
# validations as that gets in a later step in the process
# therefore we must validate its not used already in our db.
if value.blank?
delCompute
return failure("#{attr} value is blank!")
elsif (other_object = object.send("find_by_#{attr}", value))
delCompute
if (other_object = object.send("find_by_#{attr}", value))
return failure("#{attr} #{value} is already used by #{other_object}")
end
true
end

def validate_required_foreman_attr(value, object, attr)
if value.blank?
return failure("#{attr} value is blank!")
end
validate_foreman_attr(value, object, attr)
end

def match_macs_to_nics(fog_attr)
# mac/ip are properties of the NIC, and there may be more than one,
# so we need to loop. First store the nics returned from Fog in a local
Expand All @@ -309,7 +334,7 @@ def match_macs_to_nics(fog_attr)

# validate_foreman_attr handles the failure msg, so we just bubble
# the false state up the stack
return false unless validate_foreman_attr(mac,Nic::Base.physical,:mac)
return false unless validate_required_foreman_attr(mac,Nic::Base.physical,:mac)
end
true
end
Expand Down
13 changes: 13 additions & 0 deletions app/models/nic/base.rb
Expand Up @@ -166,10 +166,23 @@ def require_ip6_validation?
require_ip_validation?(:ip6, :ip, subnet6)
end

# Overwrite setter for ip to force normalization
# even when address is set during a callback
def ip=(addr)
super(Net::Validations.normalize_ip(addr))
end

# Overwrite setter for ip6 to force normalization
# even when address is set during a callback
def ip6=(addr)
super(Net::Validations.normalize_ip6(addr))
end

protected

def normalize_mac
self.mac = Net::Validations.normalize_mac(mac)
true
rescue Net::Validations::Error => e
self.errors.add(:mac, e.message)
end
Expand Down
21 changes: 21 additions & 0 deletions test/models/nics/base_test.rb
Expand Up @@ -150,4 +150,25 @@ class NicBaseTest < ActiveSupport::TestCase
end
end
end

describe 'normalization' do
let(:host) { FactoryGirl.build(:host) }
let(:nic) { FactoryGirl.build(:nic_base, :host => host) }

test 'it normalizes ipv4 address' do
nic.ip = '001.001.001.001'
assert_equal '1.1.1.1', nic.ip
end

test 'it normalizes ipv6 address' do
nic.ip6 = '2001:0db8:0000:0000:0000::0001'
assert_equal '2001:db8::1', nic.ip6
end

test 'it normalizes mac' do
nic.mac = 'aa-bb-cc-dd-ee-ff'
assert_valid nic # normalization is done before validation
assert_equal 'aa:bb:cc:dd:ee:ff', nic.mac
end
end
end
38 changes: 35 additions & 3 deletions test/models/orchestration/compute_test.rb
Expand Up @@ -18,9 +18,41 @@ class ComputeOrchestrationTest < ActiveSupport::TestCase
an_ip = '1.2.3.4'
@cr.stubs(:provided_attributes).returns({:ip => :ip})
@host.vm.expects(:ip).returns(an_ip)
@host.expects(:ip=).returns(an_ip)
@host.expects(:validate_foreman_attr).returns(true)
assert(@host.send :setComputeDetails)
assert @host.send(:setComputeDetails), "Failed to setComputeDetails, errors: #{@host.errors.full_messages}"
assert_equal an_ip, @host.ip
end

test "are set for CR providing IP via find_addresses" do
an_ip = '1.2.3.4'
@cr.stubs(:provided_attributes).returns({:ip => :ip})
@host.vm.stubs(:ip_addresses).returns([an_ip])
@host.vm.expects(:ip).returns(nil)
@host.expects(:ssh_open?).at_least_once.with(an_ip).returns(true)
@host.stubs(:compute_attributes).returns({})
assert @host.send(:setComputeDetails), "Failed to setComputeDetails, errors: #{@host.errors.full_messages}"
assert_equal an_ip, @host.ip
end

test "are set for CR providing IPv6" do
an_ip6 = '2001:db8::1'
@cr.stubs(:provided_attributes).returns({:ip6 => :ip6})
@host.vm.expects(:ip6).returns(an_ip6)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test that returns a blank value for :ip6 while claiming to provide ip4 + ip6 would be a good idea.

assert @host.send(:setComputeDetails), "Failed to setComputeDetails, errors: #{@host.errors.full_messages}"
assert_equal an_ip6, @host.ip6
end

test "are set for CR returning a blank value for ip6 while claiming to provide ip4 + ip6" do
# Create a host with ip6 = nil to test that validate_foreman_attr does not detect a duplicate ip
FactoryGirl.create(:host)

an_ip = '1.2.3.4'
@cr.stubs(:provided_attributes).returns({:ip => :ip, :ip6 => :ip6})
@host.vm.stubs(:ip_addresses).returns([an_ip])
@host.vm.expects(:ip).returns(an_ip)
@host.vm.expects(:ip6).returns(nil)
assert @host.send(:setComputeDetails), "Failed to setComputeDetails, errors: #{@host.errors.full_messages}"
assert_equal an_ip, @host.ip
assert_nil @host.ip6
end

test "are set for CR providing an unknown attribute" do
Expand Down
8 changes: 8 additions & 0 deletions test/unit/net/validations_test.rb
Expand Up @@ -186,4 +186,12 @@ class ValidationsTest < ActiveSupport::TestCase
test "should ignore invalid data when normalizing IPv4 address" do
assert_equal "xyz.1.2.3", Net::Validations.normalize_ip("xyz.1.2.3")
end

test "should normalize IPv6 address" do
assert_equal '2001:db8::1', Net::Validations.normalize_ip6('2001:0db8:0000:0000::0001')
end

test "should ignore invalid data when normalizing IPv6 address" do
assert_equal 'invalid', Net::Validations.normalize_ip6('invalid')
end
end