Class: Host::Discovered

Inherits:
Base
  • Object
show all
Includes:
BelongsToProxies, Hostext::OperatingSystem, ScopedSearchExtensions
Defined in:
app/models/host/discovered.rb

Constant Summary collapse

NAMING_PATTERNS =
{
  'Fact' => _('Fact + prefix'),
  'Random-name' => _('Random name'),
  'MAC-name' => _('MAC-based name')
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.import_host(facts) ⇒ Object

Discovery import workflow: discovered#import_host -> ForemanDiscovery::HostFactImporter#import_facts -> ::HostFactImporter#import_facts -> ::HostFactImporter#parse_facts -> discovered#populate_fields_from_facts -> base#populate_fields_from_facts -> base#set_interfaces -> discovered#populate_discovery_fields_from_facts

Raises:

  • (::Foreman::Exception)


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/models/host/discovered.rb', line 54

def self.import_host facts
  raise(::Foreman::Exception.new(N_("Invalid facts, must be a Hash"))) unless facts.is_a?(Hash) || facts.is_a?(ActionController::Parameters)

  # filter facts
  facts.reject!{|k,v| k =~ /kernel|operatingsystem|osfamily|ruby|path|time|swap|free|filesystem/i }

  raise ::Foreman::Exception.new(N_("Expected discovery_fact '%s' is missing, unable to detect primary interface and set hostname") % FacterUtils::bootif_name) unless FacterUtils::bootif_present(facts)

  # construct hostname
  bootif_mac = FacterUtils::bootif_mac(facts).try(:downcase)
  hostname = ''
  if Setting[:discovery_naming] == 'MAC-name'
    hostname_mac = return_first_valid_mac(Setting['discovery_hostname'], facts) || bootif_mac
    hostname = NameGenerator.new.generate_next_mac_name(hostname_mac)
  elsif Setting[:discovery_naming] == 'Random-name'
    hostname = NameGenerator.new.generate_next_random_name
  else
    prefix_from_settings = Setting[:discovery_prefix]
    hostname_prefix = prefix_from_settings if prefix_from_settings.present? && prefix_from_settings.match(/^[a-zA-Z].*/)
    name_fact = return_first_valid_fact(Setting['discovery_hostname'], facts)
    raise(::Foreman::Exception.new(N_("Invalid facts: hash does not contain a valid value for any of the facts in the discovery_hostname setting: %s"), Setting['discovery_hostname'].join(', '))) unless name_fact && name_fact.present?
    hostname = normalize_string_for_hostname("#{hostname_prefix}#{name_fact}")
  end
  Rails.logger.warn "Hostname does not start with an alphabetical character" unless hostname.downcase.match(/^[a-z]/)

  # check for existing managed hosts and fail or warn
  existing_managed = Nic::Managed.joins(:host).where(:mac => bootif_mac, :provision => true, :hosts => {:type => "Host::Managed"}).limit(1)
  if existing_managed.count > 0
    if Setting[:discovery_error_on_existing]
      raise ::Foreman::Exception.new("One or more existing managed hosts found: %s", "#{existing_managed.first.name}/#{bootif_mac}")
    else
      Rails.logger.warn("One or more existing managed hosts found: #{existing_managed.first.name}/#{bootif_mac}")
    end
  end

  # find existing discovered host (pick the oldest if multiple) or create new discovery host record
  existing_discovery_hosts = Nic::Managed.joins(:host).where(:mac => bootif_mac, :provision => true, :hosts => {:type => "Host::Discovered"}).order('created_at DESC')
  if existing_discovery_hosts.empty?
    host = Host.new(:name => hostname, :type => "Host::Discovered")
    send_notifications = true
  else
    Rails.logger.warn "Multiple (#{existing_discovery_hosts.count}) discovery hosts found with MAC address #{name_fact} - picking most recent NIC entry" if existing_discovery_hosts.count > 1
    host = existing_discovery_hosts.first.host
    send_notifications = false
  end

  # and save (interfaces are created via puppet parser extension)
  host.save(:validate => false) if host.new_record?
  importer = ForemanDiscovery::HostFactImporter.new(host)
  raise ::Foreman::Exception.new(N_("Facts could not be imported")) unless importer.import_facts(facts)

  # finally, send out notifications for new hosts
  host.create_notification if send_notifications

  host
end

.model_nameObject



210
211
212
# File 'app/models/host/discovered.rb', line 210

def self.model_name
  ActiveModel::Name.new(Host)
end

.normalize_string_for_hostname(hostname) ⇒ Object

Raises:

  • (::Foreman::Exception)


224
225
226
227
228
# File 'app/models/host/discovered.rb', line 224

def self.normalize_string_for_hostname(hostname)
  hostname = hostname.to_s.downcase.gsub(/(^[^a-z0-9]*|[^a-z0-9-]|[^a-z0-9]*$)/,'')
  raise(::Foreman::Exception.new(N_("Invalid hostname: Could not normalize the hostname"))) unless hostname && hostname.present?
  hostname
end

.return_first_valid_fact(facts_array, facts) ⇒ Object



238
239
240
241
242
243
244
# File 'app/models/host/discovered.rb', line 238

def self.return_first_valid_fact(facts_array, facts)
  return facts[facts_array] if !facts_array.is_a?(Array)
  facts_array.each do |value|
    return facts[value] if !facts[value].nil?
  end
  return nil
end

.return_first_valid_mac(facts_array, facts) ⇒ Object



230
231
232
233
234
235
236
# File 'app/models/host/discovered.rb', line 230

def self.return_first_valid_mac(facts_array, facts)
  return facts[facts_array] if !facts_array.is_a?(Array)
  facts_array.each do |value|
    return facts[value] if !facts[value].nil? && facts[value].match(/([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})/)
  end
  return nil
end

Instance Method Details

#attributes_to_import_from_factsObject



117
118
119
# File 'app/models/host/discovered.rb', line 117

def attributes_to_import_from_facts
  super
end

#compute_resourceObject



214
215
216
# File 'app/models/host/discovered.rb', line 214

def compute_resource
  false
end

#create_notificationObject



246
247
248
# File 'app/models/host/discovered.rb', line 246

def create_notification
  ForemanDiscovery::UINotifications::NewHost.deliver!(self)
end

#delete_notificationObject



250
251
252
# File 'app/models/host/discovered.rb', line 250

def delete_notification
  ForemanDiscovery::UINotifications::DestroyHost.deliver!(self)
end

#ip4or6Object



142
143
144
145
146
147
148
# File 'app/models/host/discovered.rb', line 142

def ip4or6
  if Setting[:discovery_prefer_ipv6]
    IPAddr.new(self.ip6 || self.ip)
  else
    IPAddr.new(self.ip || self.ip6)
  end
end

#kexec(json, old_ip = nil, new_ip = nil, old_ip6 = nil, new_ip6 = nil) ⇒ Object

Raises:

  • (::Foreman::Exception)


187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'app/models/host/discovered.rb', line 187

def kexec(json, old_ip = nil, new_ip = nil, old_ip6 = nil, new_ip6 = nil)
  # perform the action against the original lease as well as the new reservation
  if Setting[:discovery_prefer_ipv6]
    ips = [old_ip6, new_ip6, self.ip6, old_ip, new_ip, self.ip].compact.uniq
  else
    ips = [old_ip, new_ip, self.ip, old_ip6, new_ip6, self.ip6].compact.uniq
  end
  logger.debug "Performing kexec calls against #{ips.to_sentence}, #{facts.count} facts left"
  ips.each do |next_ip|
    begin
      node_url = proxy_url(IPAddr.new(next_ip))
      logger.debug "Performing kexec call against #{node_url}"
      resource = ::ForemanDiscovery::NodeAPI::Power.service(:url => node_url)
      return true if resource.kexec(json)
    rescue => e
      msg = N_("Unable to perform kexec on %{name} (%{url}): %{msg}")
      ::Foreman::Logging.exception(msg % { :name => name, :url => node_url, :msg => e.to_s }, e)
    end
  end
  msg = N_("Unable to perform %{action} on %{ips}")
  raise ::Foreman::Exception.new(msg, action: "kexec", ips: ips.to_sentence)
end

#lookup_value_matchObject



218
219
220
221
222
# File 'app/models/host/discovered.rb', line 218

def lookup_value_match
  # We don't really expect lookup values to be used to match discovered hosts,
  # so simply put a string that won't match anything here
  "discovery-not-matched"
end

#notification_recipients_idsObject



254
255
256
257
258
259
260
261
# File 'app/models/host/discovered.rb', line 254

def notification_recipients_ids
  org_recipients = find_organization_users
  org_recipients ||= []

  admins = User.unscoped.only_admin.except_hidden.
    reorder('').distinct.pluck(:id)
  (org_recipients + admins).uniq
end

#populate_discovery_fields_from_facts(facts) ⇒ Object

set additional discovery attributes



132
133
134
135
136
# File 'app/models/host/discovered.rb', line 132

def populate_discovery_fields_from_facts(facts)
  ForemanDiscovery::ImportHookService.new(host: self, facts: facts).after_populate
ensure
  self.save!
end

#populate_fields_from_facts(parser, type, source_proxy) ⇒ Object



121
122
123
124
125
126
127
128
129
# File 'app/models/host/discovered.rb', line 121

def populate_fields_from_facts(parser, type, source_proxy)
  facts = parser.facts

  # detect interfaces and primary interface using extensions
  super(parser, type, source_proxy)

  populate_discovery_fields_from_facts(facts)
  parser
end

#proxied?Boolean

Returns:

  • (Boolean)


138
139
140
# File 'app/models/host/discovered.rb', line 138

def proxied?
  subnet.present? && subnet.discovery.present?
end

#proxy_url(node_ip) ⇒ Object



150
151
152
153
# File 'app/models/host/discovered.rb', line 150

def proxy_url(node_ip)
  wrapped_ip = node_ip.ipv6? ? "[#{node_ip}]" : node_ip
  proxied? ? subnet.discovery.url + "/discovery/#{node_ip}" : "https://#{wrapped_ip}:8443"
end

#reboot(old_ip = nil, new_ip = nil, old_ip6 = nil, new_ip6 = nil) ⇒ Object

Raises:

  • (::Foreman::Exception)


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'app/models/host/discovered.rb', line 164

def reboot(old_ip = nil, new_ip = nil, old_ip6 = nil, new_ip6 = nil)
  # perform the action against the original lease as well as the new reservation
  if Setting[:discovery_prefer_ipv6]
    ips = [old_ip6, new_ip6, self.ip6, old_ip, new_ip, self.ip].compact.uniq
  else
    ips = [old_ip, new_ip, self.ip, old_ip6, new_ip6, self.ip6].compact.uniq
  end
  logger.debug "Performing reboot calls against #{ips.to_sentence}, facts left #{facts.count}"
  ips.each do |next_ip|
    begin
      node_url = proxy_url(IPAddr.new(next_ip))
      logger.debug "Performing reboot call against #{node_url}"
      resource = ::ForemanDiscovery::NodeAPI::Power.service(:url => node_url)
      return true if resource.reboot
    rescue => e
      msg = N_("Unable to perform reboot on %{name} (%{url}): %{msg}")
      ::Foreman::Logging.exception(msg % { :name => name, :url => node_url, :msg => e.to_s }, e)
    end
  end
  msg = N_("Unable to perform %{action} on %{ips}")
  raise ::Foreman::Exception.new(msg, action: "reboot", ips: ips.to_sentence)
end

#refresh_factsObject



155
156
157
158
159
160
161
162
# File 'app/models/host/discovered.rb', line 155

def refresh_facts
  facts = ::ForemanDiscovery::NodeAPI::Inventory.new(:url => proxy_url(ip4or6)).facter
  self.class.import_host facts
  ::ForemanDiscovery::HostFactImporter.new(self).import_facts facts
rescue => e
  ::Foreman::Logging.exception("Unable to get facts from proxy", e)
  raise ::Foreman::WrappedException.new(e, N_("Could not get facts from proxy %{url}: %{error}"), :url => proxy_url(ip4or6), :error => e)
end

#setup_cloneObject



111
112
113
114
115
# File 'app/models/host/discovered.rb', line 111

def setup_clone
  # Nic::Managed needs this method but Discovered hosts shouldn't
  # be doing orchestration anyway...
  clone
end