Class: Aws::ENI::Interface

Inherits:
Object
  • Object
show all
Extended by:
Enumerable
Defined in:
lib/aws-eni/interface.rb

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, auto_config = true) ⇒ Interface

Returns a new instance of Interface.



147
148
149
150
151
152
153
154
155
156
# File 'lib/aws-eni/interface.rb', line 147

def initialize(name, auto_config = true)
  unless name =~ /^eth([0-9]+)$/
    raise Errors::InvalidInterface, "Invalid interface: #{name}"
  end
  @name = name
  @device_number = $1.to_i
  @route_table = @device_number + 10000
  @lock = Mutex.new
  configure if auto_config
end

Class Attribute Details

.verboseObject

Returns the value of attribute verbose.



15
16
17
# File 'lib/aws-eni/interface.rb', line 15

def verbose
  @verbose
end

Instance Attribute Details

#device_numberObject (readonly)

Returns the value of attribute device_number.



145
146
147
# File 'lib/aws-eni/interface.rb', line 145

def device_number
  @device_number
end

#nameObject (readonly)

Returns the value of attribute name.



145
146
147
# File 'lib/aws-eni/interface.rb', line 145

def name
  @name
end

#route_tableObject (readonly)

Returns the value of attribute route_table.



145
146
147
# File 'lib/aws-eni/interface.rb', line 145

def route_table
  @route_table
end

Class Method Details

.[](index) ⇒ Object

Array-like accessor to automatically instantiate our class



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/aws-eni/interface.rb', line 18

def [](index)
  case index
  when Integer
    @lock.synchronize do
      @instance_cache ||= []
      @instance_cache[index] ||= new("eth#{index}", false)
    end
  when nil
    self[next_available_index]
  when /^(?:eth)?([0-9]+)$/
    self[$1.to_i]
  when /^eni-/
    find { |dev| dev.interface_id == index }
  when /^[0-9a-f:]+$/i
    find { |dev| dev.hwaddr.casecmp(index) == 0 }
  when /^[0-9\.]+$/
    find { |dev| dev.has_ip?(index) }
  end.tap do |dev|
    raise Errors::UnknownInterface, "No interface found matching #{index}" unless dev
  end
end

.cleanObject

Purge and deconfigure non-existent interfaces from the cache



41
42
43
44
# File 'lib/aws-eni/interface.rb', line 41

def clean
  # exists? will automatically call deconfigure if necessary
  @instance_cache.map!{ |dev| dev if dev.exists? }
end

.cmd(command, options = {}) ⇒ Object

Execute an ‘ip’ command



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/aws-eni/interface.rb', line 101

def cmd(command, options = {})
  options[:sudo] = options[:sudo] != false
  errors = options[:errors]
  options[:errors] = true
  begin
    exec("/sbin/ip #{command}", options)
  rescue Errors::InterfaceOperationError => e
    case e.message
    when /operation not permitted/i, /password is required/i
      raise Errors::InterfacePermissionError, "Operation not permitted"
    else
      raise if errors
    end
  end
end

.configure(selector = nil, options = {}) ⇒ Object

Configure all available interfaces identified by an optional selector



64
65
66
67
68
# File 'lib/aws-eni/interface.rb', line 64

def configure(selector = nil, options = {})
  filter(selector).reduce(0) do |count, dev|
    count + dev.configure(options[:dry_run])
  end
end

.deconfigure(selector = nil) ⇒ Object

Remove configuration on available interfaces identified by an optional selector



72
73
74
75
# File 'lib/aws-eni/interface.rb', line 72

def deconfigure(selector = nil)
  filter(selector).each(&:deconfigure)
  true
end

.each(&block) ⇒ Object

Iterate over available ethernet interfaces (required for Enumerable)



54
55
56
# File 'lib/aws-eni/interface.rb', line 54

def each(&block)
  Dir.entries("/sys/class/net/").grep(/^eth[0-9]+$/){ |name| self[name] }.each(&block)
end

.enabledObject

Return array of enabled interfaces



59
60
61
# File 'lib/aws-eni/interface.rb', line 59

def enabled
  select(&:enabled?)
end

.exec(command, options = {}) ⇒ Object

Execute a command, returns output as string or nil on error



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/aws-eni/interface.rb', line 125

def exec(command, options = {})
  output = nil
  errors = options[:errors]
  verbose = self.verbose || options[:verbose]
  command = "sudo -n #{command}" if options[:sudo]

  puts command if verbose
  Open3.popen3(command) do |i,o,e,t|
    if t.value.success?
      output = o.read
    else
      error = e.read
      warn "Warning: #{error}" if verbose
      raise Errors::InterfaceOperationError, error if errors
    end
  end
  output
end

.filter(filter = nil) ⇒ Object

Return an array of available interfaces identified by name, id, hwaddr, or subnet id.



79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/aws-eni/interface.rb', line 79

def filter(filter = nil)
  case filter
  when nil
    to_a
  when /^eni-/, /^eth[0-9]+$/, /^[0-9a-f:]+$/i, /^[0-9\.]+$/
    [*self[filter]]
  when /^subnet-/
    select { |dev| dev.subnet_id == filter }
  end.tap do |devs|
    raise Errors::UnknownInterface, "No interface found matching #{filter}" if devs.nil? || devs.empty?
  end
end

.mutable?Boolean

Test whether we have permission to run RTNETLINK commands

Returns:

  • (Boolean)


93
94
95
96
97
98
# File 'lib/aws-eni/interface.rb', line 93

def mutable?
  cmd('link set dev eth0') # innocuous command
  true
rescue Errors::InterfacePermissionError
  false
end

.next_available_indexObject

Return the next unused device index



47
48
49
50
51
# File 'lib/aws-eni/interface.rb', line 47

def next_available_index
  for index in 0..32 do
    break index unless self[index].exists?
  end
end

.test(ip, options = {}) ⇒ Object

Test connectivity from a given ip address



118
119
120
121
122
# File 'lib/aws-eni/interface.rb', line 118

def test(ip, options = {})
  timeout = Integer(options[:timeout] || 30)
  target = options[:target] || '8.8.8.8'
  !!exec("ping -w #{timeout} -c 1 -I #{ip} #{target}")
end

Instance Method Details

#add_alias(ip) ⇒ Object

Add a secondary ip to this interface



329
330
331
332
333
334
# File 'lib/aws-eni/interface.rb', line 329

def add_alias(ip)
  cmd("addr add #{ip}/#{prefix} brd + dev #{name}")
  unless name == 'eth0' || cmd("rule list").include?("from #{ip} lookup #{route_table}")
    cmd("rule add from #{ip} lookup #{route_table}")
  end
end

#assert(attr) ⇒ Object

Throw exception unless this interface matches the provided attributes else returns self



357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/aws-eni/interface.rb', line 357

def assert(attr)
  error = nil
  attr.find do |attr,val|
    next if val.nil?
    error = case attr
      when :exists
        if val
          "The specified interface does not exist." unless exists?
        else
          "Interface #{name} exists." if exists?
        end
      when :enabled
        if val
          "Interface #{name} is not enabled." unless enabled?
        else
          "Interface #{name} is not disabled." if enabled?
        end
      when :name, :device_name
        "The specified interface does not match" unless name == val
      when :index, :device_index, :device_number
        "Interface #{name} is device number #{val}" unless device_number == val.to_i
      when :hwaddr
        "Interface #{name} does not match hwaddr #{val}" unless hwaddr == val
      when :interface_id
        "Interface #{name} does not have interface id #{val}" unless interface_id == val
      when :subnet_id
        "Interface #{name} does not have subnet id #{val}" unless subnet_id == val
      when :ip, :has_ip
        "Interface #{name} does not have IP #{val}" unless has_ip? val
      when :public_ip
        "Interface #{name} does not have public IP #{val}" unless public_ips.has_value? val
      when :local_ip, :private_ip
        "Interface #{name} does not have private IP #{val}" unless local_ips.include? val
      else
        "Unknown attribute: #{attr}"
      end
  end
  raise Errors::UnknownInterface, error if error
  self
end

#configure(dry_run = false) ⇒ Object

Initialize a new interface config



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
# File 'lib/aws-eni/interface.rb', line 265

def configure(dry_run = false)
  changes = 0
  prefix = self.prefix # prevent exists? check on each use

  local_primary, *local_aliases = local_ips
  meta_primary, *meta_aliases = meta_ips

  # ensure primary ip address is correct
  if name != 'eth0' && local_primary != meta_primary
    unless dry_run
      deconfigure
      cmd("addr add #{meta_primary}/#{prefix} brd + dev #{name}")
    end
    changes += 1
  end

  # add missing secondary ips
  (meta_aliases - local_aliases).each do |ip|
    cmd("addr add #{ip}/#{prefix} brd + dev #{name}") unless dry_run
    changes += 1
  end

  # remove extra secondary ips
  (local_aliases - meta_aliases).each do |ip|
    cmd("addr del #{ip}/#{prefix} dev #{name}") unless dry_run
    changes += 1
  end

  # add and remove source-ip based rules
  unless name == 'eth0'
    rules_to_add = meta_ips || []
    cmd("rule list").lines.grep(/^([0-9]+):.*\s([0-9\.]+)\s+lookup #{route_table}/) do
      unless rules_to_add.delete($2)
        cmd("rule delete pref #{$1}") unless dry_run
        changes += 1
      end
    end
    rules_to_add.each do |ip|
      cmd("rule add from #{ip} lookup #{route_table}") unless dry_run
      changes += 1
    end
  end

  @clean = nil
  changes
end

#deconfigureObject

Remove configuration for an interface



313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/aws-eni/interface.rb', line 313

def deconfigure
  # assume eth0 primary ip is managed by dhcp
  if name == 'eth0'
    cmd("addr flush dev eth0 secondary")
  else
    cmd("rule list").lines.grep(/^([0-9]+):.*lookup #{route_table}/) do
      cmd("rule delete pref #{$1}")
    end
    cmd("addr flush dev #{name}")
    cmd("route flush table #{route_table}")
    cmd("route flush cache")
  end
  @clean = true
end

#disableObject

Disable our interface



255
256
257
# File 'lib/aws-eni/interface.rb', line 255

def disable
  cmd("link set dev #{name} down")
end

#enableObject

Enable our interface and create necessary routes



248
249
250
251
252
# File 'lib/aws-eni/interface.rb', line 248

def enable
  cmd("link set dev #{name} up")
  cmd("route add default via #{gateway} dev #{name} table #{route_table}")
  cmd("route flush cache")
end

#enabled?Boolean

Check whether our interface is enabled

Returns:

  • (Boolean)


260
261
262
# File 'lib/aws-eni/interface.rb', line 260

def enabled?
  exists? && cmd("link show up", sudo: false).include?(name)
end

#exists?Boolean

Verify device exists on our system

Returns:

  • (Boolean)


169
170
171
172
173
# File 'lib/aws-eni/interface.rb', line 169

def exists?
  File.directory?("/sys/class/net/#{name}").tap do |exists|
    deconfigure unless exists || @clean
  end
end

#gatewayObject



208
209
210
# File 'lib/aws-eni/interface.rb', line 208

def gateway
  IPAddr.new(subnet_cidr).succ.to_s
end

#has_ip?(ip_addr) ⇒ Boolean

Return true if the ip address is associated with this interface

Returns:

  • (Boolean)


345
346
347
348
349
350
351
352
353
# File 'lib/aws-eni/interface.rb', line 345

def has_ip?(ip_addr)
  if IPAddr.new(subnet_cidr) === IPAddr.new(ip_addr)
    # ip within subnet
    local_ips.include? ip_addr
  else
    # ip outside subnet
    public_ips.has_value? ip_addr
  end
end

#hwaddrObject

Get our interface’s MAC address



159
160
161
162
163
164
165
166
# File 'lib/aws-eni/interface.rb', line 159

def hwaddr
  begin
    exists? && IO.read("/sys/class/net/#{name}/address").strip
  rescue Errno::ENOENT
  end.tap do |address|
    raise Errors::UnknownInterface, "Interface #{name} not found on this machine" unless address
  end
end

#infoObject

Validate and return basic interface metadata



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/aws-eni/interface.rb', line 176

def info
  @lock.synchronize do
    hwaddr = self.hwaddr
    unless @meta_cache && @meta_cache[:hwaddr] == hwaddr
      @meta_cache = Meta.connection do
        raise Errors::MetaBadResponse unless Meta.interface(hwaddr, '', not_found: nil)
        {
          hwaddr:       hwaddr,
          interface_id: Meta.interface(hwaddr, 'interface-id'),
          subnet_id:    Meta.interface(hwaddr, 'subnet-id'),
          subnet_cidr:  Meta.interface(hwaddr, 'subnet-ipv4-cidr-block')
        }.freeze
      end
    end
    @meta_cache
  end
rescue Errors::MetaConnectionFailed
  raise Errors::InvalidInterface, "Interface #{name} could not be found in the EC2 instance meta-data"
end

#interface_idObject



196
197
198
# File 'lib/aws-eni/interface.rb', line 196

def interface_id
  info[:interface_id]
end

#local_ipsObject

Return an array of configured ip addresses (primary + secondary)



217
218
219
220
221
# File 'lib/aws-eni/interface.rb', line 217

def local_ips
  list = cmd("addr show dev #{name} primary", sudo: false) +
         cmd("addr show dev #{name} secondary", sudo: false)
  list.lines.grep(/inet ([0-9\.]+)\/.* #{name}/i){ $1 }
end

#meta_ipsObject

Return an array of ip addresses found in our instance metadata



224
225
226
227
228
229
# File 'lib/aws-eni/interface.rb', line 224

def meta_ips
  # hack to use cached hwaddr when available since this is often polled
  # continuously for changes
  hwaddr = (@meta_cache && @meta_cache[:hwaddr]) || hwaddr
  Meta.interface(hwaddr, 'local-ipv4s', cache: false).lines.map(&:strip)
end

#prefixObject



212
213
214
# File 'lib/aws-eni/interface.rb', line 212

def prefix
  subnet_cidr.split('/').last.to_i
end

#public_ipsObject

Return a hash of local/public ip associations found in instance metadata



232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/aws-eni/interface.rb', line 232

def public_ips
  hwaddr = self.hwaddr
  Hash[
    Meta.connection do
      Meta.interface(hwaddr, 'ipv4-associations/', not_found: '', cache: false).lines.map do |public_ip|
        public_ip.strip!
        unless private_ip = Meta.interface(hwaddr, "ipv4-associations/#{public_ip}", not_found: nil, cache: false)
          raise Errors::MetaBadResponse
        end
        [ private_ip, public_ip ]
      end
    end
  ]
end

#remove_alias(ip) ⇒ Object

Remove a secondary ip from this interface



337
338
339
340
341
342
# File 'lib/aws-eni/interface.rb', line 337

def remove_alias(ip)
  cmd("addr del #{ip}/#{prefix} dev #{name}")
  unless name == 'eth0' || !cmd("rule list").match(/([0-9]+):\s+from #{ip} lookup #{route_table}/)
    cmd("rule delete pref #{$1}")
  end
end

#subnet_cidrObject



204
205
206
# File 'lib/aws-eni/interface.rb', line 204

def subnet_cidr
  info[:subnet_cidr]
end

#subnet_idObject



200
201
202
# File 'lib/aws-eni/interface.rb', line 200

def subnet_id
  info[:subnet_id]
end

#to_hObject

Return an array representation of our interface config, including public ip associations and enabled status



400
401
402
403
404
405
406
407
408
409
# File 'lib/aws-eni/interface.rb', line 400

def to_h
  info.merge(
    name:          name,
    device_number: device_number,
    route_table:   route_table,
    local_ips:     local_ips,
    public_ips:    public_ips,
    enabled:       enabled?
  )
end