forked from theforeman/foreman
/
subnet.rb
362 lines (294 loc) · 12.1 KB
/
subnet.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
require 'ipaddr'
class Subnet < ApplicationRecord
audited
IP_FIELDS = [:network, :mask, :gateway, :dns_primary, :dns_secondary, :from, :to]
REQUIRED_IP_FIELDS = [:network, :mask]
SUBNET_TYPES = {:'Subnet::Ipv4' => N_('IPv4'), :'Subnet::Ipv6' => N_('IPv6')}
BOOT_MODES = {:static => N_('Static'), :dhcp => N_('DHCP')}
include Authorizable
prepend Foreman::STI
extend FriendlyId
friendly_id :name
include Taxonomix
include Parameterizable::ByIdName
include Exportable
include BelongsToProxies
attr_exportable :name, :network, :mask, :gateway, :dns_primary, :dns_secondary, :from, :to, :boot_mode,
:ipam, :vlanid, :mtu, :network_type, :description
# This sets the rails model name of all child classes to the
# model name of the parent class, i.e. Subnet.
# This is necessary for all STI classes to share the same
# route_key, param_key, ...
def self.inherited(child)
child.instance_eval do
# rubocop:disable Rails/Delegate
def model_name
superclass.model_name
end
# rubocop:enable Rails/Delegate
end
super
end
graphql_type '::Types::Subnet'
validates_lengths_from_database :except => [:gateway]
before_destroy EnsureNotUsedBy.new(:hosts, :hostgroups, :interfaces, :domains)
belongs_to_proxy :dhcp,
:feature => 'DHCP',
:label => N_('DHCP Proxy'),
:description => N_('DHCP Proxy to use within this subnet'),
:api_description => N_('DHCP Proxy ID to use within this subnet'),
:if => ->(subnet) { subnet.supports_ipam_mode?(:dhcp) }
belongs_to_proxy :tftp,
:feature => N_('TFTP'),
:label => N_('TFTP Proxy'),
:api_description => N_('TFTP Proxy ID to use within this subnet'),
:description => N_('TFTP Proxy to use within this subnet')
belongs_to_proxy :httpboot,
:feature => N_('HTTPBoot'),
:label => N_('HTTPBoot Proxy'),
:api_description => N_('HTTPBoot Proxy ID to use within this subnet'),
:description => N_('HTTPBoot Proxy to use within this subnet')
belongs_to_proxy :external_ipam,
:feature => N_('external_ipam'),
:label => N_('IP Address Management with various external IPAM providers'),
:api_description => N_('External IPAM Proxy ID to use within this subnet'),
:description => N_('External IPAM Proxy to use within this subnet')
belongs_to_proxy :dns,
:feature => N_('DNS'),
:label => N_('Reverse DNS Proxy'),
:api_description => N_('DNS Proxy ID to use within this subnet'),
:description => N_('DNS Proxy to use within this subnet for managing PTR records, note that A and AAAA records are managed via Domain DNS proxy')
belongs_to_proxy :template,
:feature => N_('Templates'),
:label => N_('Template Proxy'),
:api_description => N_('Template HTTP(S) Proxy ID to use within this subnet'),
:description => N_('Template HTTP(S) Proxy to use within this subnet to allow access templating endpoint from isolated networks')
has_many :hostgroups
has_many :subnet_domains, :dependent => :destroy, :inverse_of => :subnet
has_many :domains, :through => :subnet_domains
has_many :subnet_parameters, :dependent => :destroy, :foreign_key => :reference_id, :inverse_of => :subnet
has_many :parameters, :dependent => :destroy, :foreign_key => :reference_id, :class_name => "SubnetParameter"
accepts_nested_attributes_for :subnet_parameters, :allow_destroy => true
validates :network, :mask, :name, :cidr, :presence => true
validates_associated :subnet_domains
validates :boot_mode, :inclusion => BOOT_MODES.values
validates :ipam, :inclusion => {:in => Proc.new { |subnet| subnet.supported_ipam_modes.map {|m| IPAM::MODES[m]} }, :message => N_('not supported by this protocol')}
validates :type, :inclusion => {:in => Proc.new { Subnet::SUBNET_TYPES.keys.map(&:to_s) }, :message => N_("must be one of [ %s ]" % Subnet::SUBNET_TYPES.keys.map(&:to_s).join(', ')) }
validates :name, :length => {:maximum => 255}, :uniqueness => true
validates :vlanid, numericality: { :only_integer => true, :greater_than_or_equal_to => 0, :less_than => 4096}, :allow_blank => true
validates :mtu, numericality: { :only_integer => true, :greater_than_or_equal_to => 68}, :presence => true
before_validation :normalize_addresses
after_validation :validate_against_external_ipam
validate :ensure_ip_addrs_valid
validate :validate_ranges
validate :check_if_type_changed, :on => :update
default_scope lambda {
with_taxonomy_scope do
order(:vlanid)
end
}
scoped_search :on => [:name, :network, :mask, :gateway, :dns_primary, :dns_secondary,
:vlanid, :mtu, :ipam, :boot_mode, :type], :complete_value => true
scoped_search :relation => :domains, :on => :name, :rename => :domain, :complete_value => true
scoped_search :relation => :subnet_parameters, :on => :value, :on_key => :name, :complete_value => true, :only_explicit => true, :rename => :params
delegate :supports_ipam_mode?, :supported_ipam_modes, :show_mask?, to: 'self.class'
class Jail < ::Safemode::Jail
allow :name, :network, :mask, :cidr, :title, :to_label, :gateway, :dns_primary, :dns_secondary, :dns_servers,
:vlanid, :mtu, :boot_mode, :dhcp?, :nil?, :has_vlanid?, :dhcp_boot_mode?, :description, :present?
end
# Subnets are displayed in the form of their network network/network mask
def network_address
"#{network}/#{cidr}"
end
def to_label
"#{name} (#{network_address})"
end
def to_s
name
end
def network_type
SUBNET_TYPES[type.to_sym]
end
def network_type=(value)
self[:type] = SUBNET_TYPES.key(value)
end
# Indicates whether the IP is within this subnet
# [+ip+] String: IPv4 or IPv6 address
# Returns Boolean: True if if ip is in this subnet
def contains?(ip)
ipaddr.include? IPAddr.new(ip, family)
end
def ipaddr
IPAddr.new("#{network}/#{mask}", family)
end
def cidr
return if mask.nil?
IPAddr.new(mask).to_i.to_s(2).count("1")
rescue invalid_address_error
nil
end
def cidr=(cidr)
return if cidr.nil?
self[:mask] = IPAddr.new(in_mask, family).mask(cidr).to_s
rescue invalid_address_error
nil
end
def dhcp?
supports_ipam_mode?(:dhcp) && dhcp && dhcp.url.present?
end
def dhcp_proxy(attrs = {})
@dhcp_proxy ||= ProxyAPI::DHCP.new({:url => dhcp.url}.merge(attrs)) if dhcp?
end
def tftp?
!!(tftp && tftp.url && tftp.url.present?)
end
def tftp_proxy(attrs = {})
@tftp_proxy ||= ProxyAPI::TFTP.new({:url => tftp.url}.merge(attrs)) if tftp?
end
def httpboot?
!!(httpboot && httpboot.url && httpboot.url.present?)
end
def httpboot_proxy(attrs = {})
@httpboot_proxy ||= ProxyAPI::TFTP.new({:url => httpboot.url}.merge(attrs)) if httpboot?
end
# do we support DNS PTR records for this subnet
def dns?
!!(dns && dns.url && dns.url.present?)
end
def dns_proxy(attrs = {})
@dns_proxy ||= ProxyAPI::DNS.new({:url => dns.url}.merge(attrs)) if dns?
end
def template?
!!(template && template.url)
end
def template_proxy(attrs = {})
@template_proxy ||= ProxyAPI::Template.new({:url => template.url}.merge(attrs)) if template?
end
def external_ipam?
supports_ipam_mode?(:external_ipam) && external_ipam && external_ipam.url.present?
end
def external_ipam_proxy(attrs = {})
@external_ipam_proxy ||= ProxyAPI::ExternalIpam.new({:url => external_ipam.url}.merge(attrs)) if external_ipam?
end
def ipam?
self.ipam != IPAM::MODES[:none]
end
def ipam_needs_range?
ipam? && self.ipam != IPAM::MODES[:eui64] && self.ipam != IPAM::MODES[:external_ipam]
end
def dhcp_boot_mode?
self.boot_mode == Subnet::BOOT_MODES[:dhcp]
end
def unused_ip(mac = nil, excluded_ips = [])
unless supported_ipam_modes.map {|m| IPAM::MODES[m]}.include?(self.ipam)
raise ::Foreman::Exception.new(N_("Unsupported IPAM mode for %s"), self.class.name)
end
opts = {:subnet => self, :mac => mac, :excluded_ips => excluded_ips}
IPAM.new(self.ipam, opts)
end
def validate_against_external_ipam
return unless self.errors.full_messages.empty?
if self.external_ipam?
external_ipam_proxy = SmartProxy.with_features('external_ipam').first
if external_ipam_proxy.nil?
self.errors.add :ipam, _('There must be at least one Smart Proxy present with External IPAM plugin installed and configured')
elsif !subnet_exists_in_external_ipam
self.errors.add :network, _('Subnet not found in the configured External IPAM instance')
end
end
end
def subnet_exists_in_external_ipam
subnet = self.external_ipam_proxy.get_subnet(self) if self.external_ipam_proxy
!(!subnet.kind_of?(Array) && subnet['message'] && subnet['message'] == 'No subnets found')
end
def known_ips
self.interfaces.reload
ips = self.interfaces.map(&ip_sym) + self.hosts.includes(:interfaces).map(&ip_sym)
ips += [self.gateway, self.dns_primary, self.dns_secondary].select(&:present?)
ips.compact.uniq
end
def proxies
[dhcp, tftp, dns, httpboot].compact
end
def has_vlanid?
self.vlanid.present?
end
# overwrite method in taxonomix, since subnet is not direct association of host anymore
def used_taxonomy_ids(type)
return [] if new_record?
Host::Base.joins(:primary_interface).where(:nics => {:subnet_id => id}).distinct.pluck(type).compact
end
def as_json(options = {})
super({:methods => [:to_label, :type]}.merge(options))
end
def dns_servers
[dns_primary, dns_secondary].select(&:present?)
end
private
def validate_ranges
if from.present? || to.present?
errors.add(:from, _("must be specified if to is defined")) if from.blank?
errors.add(:to, _("must be specified if from is defined")) if to.blank?
end
return if errors.key?(:from) || errors.key?(:to)
errors.add(:from, _("does not belong to subnet")) if from.present? && !self.contains?(f = IPAddr.new(from))
errors.add(:to, _("does not belong to subnet")) if to.present? && !self.contains?(t = IPAddr.new(to))
errors.add(:from, _("can't be bigger than to range")) if from.present? && t.present? && f > t
end
def check_if_type_changed
if self.type_changed?
errors.add(:type, _("can't be updated after subnet is saved"))
end
end
def normalize_addresses
IP_FIELDS.each do |f|
val = send(f)
send("#{f}=", normalize_ip(val)) if val.present?
end
self
end
def ensure_ip_addrs_valid
IP_FIELDS.each do |f|
errors.add(f, _("is invalid")) if (send(f).present? || REQUIRED_IP_FIELDS.include?(f)) && !validate_ip(send(f)) && !errors.key?(f)
end
end
class << self
def boot_modes_with_translations
BOOT_MODES.map { |_, mode_name| [_(mode_name), mode_name] }
end
def supports_ipam_mode?(mode)
supported_ipam_modes.include?(mode)
end
def supported_ipam_modes_with_translations
supported_ipam_modes.map {|mode| [_(IPAM::MODES[mode]), IPAM::MODES[mode]]}
end
# Given an IP returns the subnet that contains that IP preferring highest CIDR prefix
# [+ip+] : IPv4 or IPv6 address
# Returns : Subnet object or nil if not found
def subnet_for(ip)
return unless ip.present?
ip = IPAddr.new(ip)
Subnet.unscoped.all.select {|s| s.family == ip.family && s.contains?(ip)}.max_by(&:cidr)
end
# This casts Subnet to Subnet::Ipv4 if no type is set
def new(*attributes, &block)
type = attributes.first.with_indifferent_access.delete(:type) if attributes.first.is_a?(Hash)
return Subnet::Ipv4.new(*attributes, &block) if self == Subnet && type.nil?
super
end
# allows to create a specific subnet class based on the network_type.
# network_type is more user friendly than the class names
def new_network_type(args)
network_type = args.delete(:network_type) || 'IPv4'
SUBNET_TYPES.each do |network_type_class, network_type_name|
return network_type_class.to_s.constantize.new(args) if network_type_name.downcase == network_type.downcase
end
raise ::Foreman::Exception.new N_("unknown network_type")
end
end
def invalid_address_error
# IPAddr::InvalidAddressError is undefined for ruby 1.9
return IPAddr::InvalidAddressError if IPAddr.const_defined?('InvalidAddressError')
ArgumentError
end
end