aws-eni 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,278 @@
1
+ require 'time'
2
+ require 'aws-sdk'
3
+ require 'aws-eni/version'
4
+ require 'aws-eni/errors'
5
+ require 'aws-eni/meta'
6
+ require 'aws-eni/ifconfig'
7
+
8
+ module Aws
9
+ module ENI
10
+ extend self
11
+
12
+ def environment
13
+ @environment ||= {}.tap do |e|
14
+ hwaddr = IFconfig['eth0'].hwaddr
15
+ Meta.open_connection do |conn|
16
+ e[:instance_id] = Meta.http_get(conn, 'instance-id')
17
+ e[:availability_zone] = Meta.http_get(conn, 'placement/availability-zone')
18
+ e[:region] = e[:availability_zone].sub(/(.*)[a-z]/,'\1')
19
+ e[:vpc_id] = Meta.http_get(conn, "network/interfaces/macs/#{hwaddr}/vpc-id")
20
+ e[:vpc_cidr] = Meta.http_get(conn, "network/interfaces/macs/#{hwaddr}/vpc-ipv4-cidr-block")
21
+ end
22
+ unless e[:vpc_id]
23
+ raise EnvironmentError, "Unable to detect VPC settings, library incompatible with EC2-Classic"
24
+ end
25
+ end.freeze
26
+ rescue Meta::ConnectionFailed
27
+ raise EnvironmentError, "Unable to load EC2 meta-data"
28
+ end
29
+
30
+ def owner_tag(new_owner = nil)
31
+ @owner_tag = new_owner.to_s if new_owner
32
+ @owner_tag || 'aws-eni script'
33
+ end
34
+
35
+ def client
36
+ @client ||= Aws::EC2::Client.new(region: environment[:region])
37
+ end
38
+
39
+ # return our internal model of this instance's network configuration on AWS
40
+ def list(filter = nil)
41
+ IFconfig.filter(filter).map(&:to_h) if environment
42
+ end
43
+
44
+ # sync local machine's network interface config with the EC2 meta-data
45
+ # pass dry_run option to check whether configuration is out of sync without
46
+ # modifying it
47
+ def configure(filter = nil, options = {})
48
+ IFconfig.configure(filter, options) if environment
49
+ end
50
+
51
+ # clear local machine's network interface config
52
+ def deconfigure(filter = nil)
53
+ IFconfig.deconfigure(filter) if environment
54
+ end
55
+
56
+ # create network interface
57
+ def create_interface(options = {})
58
+ timestamp = Time.now.xmlschema
59
+ params = {}
60
+ params[:subnet_id] = options[:subnet_id] || IFconfig.first.subnet_id
61
+ params[:private_ip_address] = options[:primary_ip] if options[:primary_ip]
62
+ params[:groups] = [*options[:security_groups]] if options[:security_groups]
63
+ params[:description] = "generated by #{owner_tag} from #{environment[:instance_id]} on #{timestamp}"
64
+
65
+ response = client.create_network_interface(params)
66
+ client.create_tags(resources: [response[:network_interface][:network_interface_id]], tags: [
67
+ { key: 'created by', value: owner_tag },
68
+ { key: 'created on', value: timestamp },
69
+ { key: 'created from', value: environment[:instance_id] }
70
+ ])
71
+ {
72
+ id: response[:network_interface][:network_interface_id],
73
+ subnet_id: response[:network_interface][:subnet_id],
74
+ api_response: response[:network_interface]
75
+ }
76
+ end
77
+
78
+ # attach network interface
79
+ def attach_interface(id, options = {})
80
+ interface = IFconfig[options[:device_number] || options[:name]]
81
+ raise InvalidParameterError, "Interface #{interface.name} is already in use" if interface.exists?
82
+
83
+ params = {}
84
+ params[:network_interface_id] = id
85
+ params[:instance_id] = environment[:instance_id]
86
+ params[:device_index] = interface.device_number
87
+
88
+ response = client.attach_network_interface(params)
89
+ attached = wait_for(10) { interface.exists? }
90
+ raise TimeoutError, "Timed out waiting for the interface to attach" unless attached
91
+ interface.configure if options[:configure]
92
+ interface.enable if options[:enable]
93
+ {
94
+ id: interface.interface_id,
95
+ name: interface.name,
96
+ configured: options[:configure],
97
+ api_response: response
98
+ }
99
+ end
100
+
101
+ # detach network interface
102
+ def detach_interface(id, options = {})
103
+ interface = IFconfig.filter(id).first
104
+ raise InvalidParameterError, "Interface #{interface.name} does not exist" unless interface && interface.exists?
105
+ if options[:name] && interface.name != options[:name]
106
+ raise InvalidParameterError, "Interface #{interface.interface_id} not found on #{options[:name]}"
107
+ end
108
+ if options[:device_number] && interface.device_number != options[:device_number].to_i
109
+ raise InvalidParameterError, "Interface #{interface.interface_id} not found at index #{options[:device_number]}"
110
+ end
111
+
112
+ description = client.describe_network_interfaces(filters: [{
113
+ name: 'attachment.instance-id',
114
+ values: [environment[:instance_id]]
115
+ },{
116
+ name: 'network-interface-id',
117
+ values: [interface.interface_id]
118
+ }])
119
+ description = description[:network_interfaces].first
120
+ raise UnknownInterfaceError, "Interface attachment could not be located" unless description
121
+
122
+ interface.disable
123
+ interface.deconfigure
124
+ client.detach_network_interface(
125
+ attachment_id: description[:attachment][:attachment_id],
126
+ force: true
127
+ )
128
+ deleted = false
129
+ created_by_us = description.tag_set.any? { |tag| tag.key == 'created by' && tag.value == owner_tag }
130
+ unless options[:delete] == false || options[:delete].nil? && !created_by_us
131
+ detached = wait_for(10, 0.3) do
132
+ !interface.exists? && interface_status(description[:network_interface_id]) == 'available'
133
+ end
134
+ raise TimeoutError, "Timed out waiting for the interface to detach" unless detached
135
+ client.delete_network_interface(network_interface_id: description[:network_interface_id])
136
+ deleted = true
137
+ end
138
+ {
139
+ id: description[:network_interface_id],
140
+ name: "eth#{description[:attachment][:device_index]}",
141
+ device_number: description[:attachment][:device_index],
142
+ created_by_us: created_by_us,
143
+ deleted: deleted,
144
+ api_response: description
145
+ }
146
+ end
147
+
148
+ # delete unattached network interfaces
149
+ def clean_interfaces(filter = nil, options = {})
150
+ safe_mode = true unless options[:safe_mode] == false
151
+
152
+ filters = [
153
+ { name: 'vpc-id', values: [environment[:vpc_id]] },
154
+ { name: 'status', values: ['available'] }
155
+ ]
156
+ if filter
157
+ case filter
158
+ when /^eni-/
159
+ filters << { name: 'network-interface-id', values: [filter] }
160
+ when /^subnet-/
161
+ filters << { name: 'subnet-id', values: [filter] }
162
+ when /^#{environment[:region]}[a-z]$/
163
+ filters << { name: 'availability-zone', values: [filter] }
164
+ else
165
+ raise InvalidParameterError, "Unknown resource filter: #{filter}"
166
+ end
167
+ end
168
+ if safe_mode
169
+ filters << { name: 'tag:created by', values: [owner_tag] }
170
+ end
171
+
172
+ descriptions = client.describe_network_interfaces(filters: filters)
173
+ interfaces = descriptions[:network_interfaces].select do |interface|
174
+ skip = safe_mode && interface.tag_set.any? do |tag|
175
+ begin
176
+ tag.key == 'created on' && Time.now - Time.parse(tag.value) < 60
177
+ rescue ArgumentError
178
+ false
179
+ end
180
+ end
181
+ unless skip
182
+ client.delete_network_interface(network_interface_id: interface[:network_interface_id])
183
+ true
184
+ end
185
+ end
186
+ {
187
+ count: interfaces.count,
188
+ deleted: interfaces.map { |eni| eni[:network_interface_id] },
189
+ api_response: interfaces
190
+ }
191
+ end
192
+
193
+ # add new private ip using the AWS api and add it to our local ip config
194
+ def assign_secondary_ip(interface, options = {})
195
+ raise NoMethodError, "assign_secondary_ip not yet implemented"
196
+ {
197
+ private_ip: '0.0.0.0',
198
+ device_name: 'eth0',
199
+ interface_id: 'eni-1a2b3c4d'
200
+ }
201
+ end
202
+
203
+ # remove a private ip using the AWS api and remove it from local config
204
+ def unassign_secondary_ip(private_ip, options = {})
205
+ raise NoMethodError, "unassign_secondary_ip not yet implemented"
206
+ {
207
+ private_ip: '0.0.0.0',
208
+ device_name: 'eth0',
209
+ interface_id: 'eni-1a2b3c4d',
210
+ public_ip: '0.0.0.0',
211
+ allocation_id: 'eipalloc-1a2b3c4d',
212
+ association_id: 'eipassoc-1a2b3c4d',
213
+ released: true
214
+ }
215
+ end
216
+
217
+ # associate a private ip with an elastic ip through the AWS api
218
+ def associate_elastic_ip(private_ip, options = {})
219
+ raise NoMethodError, "associate_elastic_ip not yet implemented"
220
+ {
221
+ private_ip: '0.0.0.0',
222
+ device_name: 'eth0',
223
+ interface_id: 'eni-1a2b3c4d',
224
+ public_ip: '0.0.0.0',
225
+ allocation_id: 'eipalloc-1a2b3c4d',
226
+ association_id: 'eipassoc-1a2b3c4d'
227
+ }
228
+ end
229
+
230
+ # dissociate a public ip from a private ip through the AWS api and
231
+ # optionally release the public ip
232
+ def dissociate_elastic_ip(ip, options = {})
233
+ raise NoMethodError, "dissociate_elastic_ip not yet implemented"
234
+ {
235
+ private_ip: '0.0.0.0',
236
+ device_name: 'eth0',
237
+ interface_id: 'eni-1a2b3c4d',
238
+ public_ip: '0.0.0.0',
239
+ allocation_id: 'eipalloc-1a2b3c4d',
240
+ association_id: 'eipassoc-1a2b3c4d',
241
+ released: true
242
+ }
243
+ end
244
+
245
+ # allocate a new elastic ip address
246
+ def allocate_elastic_ip
247
+ raise NoMethodError, "allocate_elastic_ip not yet implemented"
248
+ {
249
+ public_ip: '0.0.0.0',
250
+ allocation_id: 'eipalloc-1a2b3c4d'
251
+ }
252
+ end
253
+
254
+ # release the specified elastic ip address
255
+ def release_elastic_ip(ip, options = {})
256
+ raise NoMethodError, "release_elastic_ip not yet implemented"
257
+ {
258
+ public_ip: '0.0.0.0',
259
+ allocation_id: 'eipalloc-1a2b3c4d'
260
+ }
261
+ end
262
+
263
+ private
264
+
265
+ def interface_status(id)
266
+ resp = client.describe_network_interfaces(network_interface_ids: [id])
267
+ resp[:network_interfaces].first[:status] unless resp[:network_interfaces].empty?
268
+ end
269
+
270
+ def wait_for(timer = 5, interval = 0.1, &block)
271
+ until timer < 0 or block.call
272
+ timer -= interval
273
+ sleep interval
274
+ end
275
+ timer > 0
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,13 @@
1
+
2
+ module Aws
3
+ module ENI
4
+ class Error < RuntimeError; end
5
+ class TimeoutError < Error; end
6
+ class MissingParameterError < Error; end
7
+ class InvalidParameterError < Error; end
8
+ class UnknownInterfaceError < Error; end
9
+ class EnvironmentError < Error; end
10
+ class CommandError < Error; end
11
+ class PermissionError < CommandError; end
12
+ end
13
+ end
@@ -0,0 +1,309 @@
1
+ require 'ipaddr'
2
+ require 'open3'
3
+ require 'aws-eni/errors'
4
+
5
+ module Aws
6
+ module ENI
7
+ class IFconfig
8
+
9
+ class << self
10
+ include Enumerable
11
+
12
+ attr_accessor :verbose
13
+
14
+ # Array-like accessor to automatically instantiate our class
15
+ def [](index)
16
+ index = $1.to_i if index.to_s =~ /^(?:eth)?([0-9]+)$/
17
+ index ||= next_available_index
18
+ @instance_cache ||= []
19
+ @instance_cache[index] ||= new("eth#{index}", false)
20
+ end
21
+
22
+ # Purge and deconfigure non-existent interfaces from the cache
23
+ def clean
24
+ # exists? will automatically call deconfigure if necessary
25
+ @instance_cache.map!{ |dev| dev if dev.exists? }
26
+ end
27
+
28
+ # Return array of available ethernet interfaces
29
+ def existing
30
+ Dir.entries("/sys/class/net/").grep(/^eth[0-9]+$/){ |name| self[name] }
31
+ end
32
+
33
+ # Return the next unused device index
34
+ def next_available_index
35
+ for index in 0..32 do
36
+ break index unless self[index].exists?
37
+ end
38
+ end
39
+
40
+ # Iterate over available ethernet interfaces (required for Enumerable)
41
+ def each(&block)
42
+ existing.each(&block)
43
+ end
44
+
45
+ # Return array of enabled interfaces
46
+ def enabled
47
+ select(&:enabled?)
48
+ end
49
+
50
+ # Configure all available interfaces identified by an optional selector
51
+ def configure(selector = nil, options = {})
52
+ filter(selector).reduce(0) do |count, dev|
53
+ count + dev.configure(options[:dry_run])
54
+ end
55
+ end
56
+
57
+ # Remove configuration on available interfaces identified by an optional
58
+ # selector
59
+ def deconfigure(selector = nil)
60
+ filter(selector).each(&:deconfigure)
61
+ true
62
+ end
63
+
64
+ # Return an array of available interfaces identified by name, id,
65
+ # hwaddr, or subnet id.
66
+ def filter(match = nil)
67
+ return existing unless match
68
+ select{ |dev| dev.is?(match) }.tap do |result|
69
+ raise UnknownInterfaceError, "No interface found matching \"#{match}\"" if result.empty?
70
+ end
71
+ end
72
+
73
+ # Execute a command
74
+ def exec(command, options = {})
75
+ output = nil
76
+ verbose = self.verbose || options[:verbose]
77
+ raise_errors = options[:raise_errors]
78
+ puts "ip #{command}" if verbose
79
+
80
+ Open3.popen3("/sbin/ip #{command}") do |i,o,e,t|
81
+ unless t.value.success?
82
+ error_msg = e.read
83
+ if error_msg =~ /operation not permitted/i
84
+ raise PermissionError, "Operation not permitted"
85
+ end
86
+ warn "Warning: #{error_msg}" if verbose
87
+ raise CommandError, error_msg if raise_errors
88
+ end
89
+ output = o.read
90
+ end
91
+ output
92
+ end
93
+ end
94
+
95
+ attr_reader :name, :device_number, :route_table
96
+
97
+ def initialize(name, auto_config = true)
98
+ unless name =~ /^eth([0-9]+)$/
99
+ raise UnknownInterfaceError, "Invalid interface: #{name}"
100
+ end
101
+ @name = name
102
+ @device_number = $1.to_i
103
+ @route_table = @device_number + 10000
104
+ configure if auto_config
105
+ end
106
+
107
+ # Get our interface's MAC address
108
+ def hwaddr
109
+ begin
110
+ exists? && IO.read("/sys/class/net/#{name}/address").strip
111
+ rescue Errno::ENOENT
112
+ end.tap do |address|
113
+ raise UnknownInterfaceError, "Unknown interface: #{name}" unless address
114
+ end
115
+ end
116
+
117
+ # Verify device exists on our system
118
+ def exists?
119
+ File.directory?("/sys/class/net/#{name}").tap do |exists|
120
+ deconfigure unless exists || @clean
121
+ end
122
+ end
123
+
124
+ # Validate and return basic interface metadata
125
+ def info
126
+ hwaddr = self.hwaddr
127
+ unless @meta_cache && hwaddr == @meta_cache[:hwaddr]
128
+ dev_path = "network/interfaces/macs/#{hwaddr}"
129
+ Meta.open_connection do |conn|
130
+ raise Meta::BadResponse unless Meta.http_get(conn, "#{dev_path}/")
131
+ @meta_cache = {
132
+ hwaddr: hwaddr,
133
+ interface_id: Meta.http_get(conn, "#{dev_path}/interface-id"),
134
+ subnet_id: Meta.http_get(conn, "#{dev_path}/subnet-id"),
135
+ subnet_cidr: Meta.http_get(conn, "#{dev_path}/subnet-ipv4-cidr-block")
136
+ }.freeze
137
+ end
138
+ end
139
+ @meta_cache
140
+ end
141
+
142
+ def interface_id
143
+ info[:interface_id]
144
+ end
145
+
146
+ def subnet_id
147
+ info[:subnet_id]
148
+ end
149
+
150
+ def subnet_cidr
151
+ info[:subnet_cidr]
152
+ end
153
+
154
+ # Return an array of configured ip addresses (primary + secondary)
155
+ def local_ips
156
+ list = exec("addr show dev #{name} primary") +
157
+ exec("addr show dev #{name} secondary")
158
+ list.lines.grep(/inet ([0-9\.]+)\/.* #{name}/i){ $1 }
159
+ end
160
+
161
+ def public_ips
162
+ ip_assoc = {}
163
+ dev_path = "network/interfaces/macs/#{hwaddr}"
164
+ Meta.open_connection do |conn|
165
+ # return an array of configured ip addresses (primary + secondary)
166
+ Meta.http_get(conn, "#{dev_path}/ipv4-associations/").to_s.each_line do |public_ip|
167
+ public_ip.strip!
168
+ local_ip = Meta.http_get(conn, "#{dev_path}/ipv4-associations/#{public_ip}")
169
+ ip_assoc[local_ip] = public_ip
170
+ end
171
+ end
172
+ ip_assoc
173
+ end
174
+
175
+ # Enable our interface
176
+ def enable
177
+ exec("link set dev #{name} up")
178
+ end
179
+
180
+ # Disable our interface
181
+ def disable
182
+ exec("link set dev #{name} down")
183
+ end
184
+
185
+ # Check whether our interface is enabled
186
+ def enabled?
187
+ exists? && exec("link show up").include?(name)
188
+ end
189
+
190
+ # Initialize a new interface config
191
+ def configure(dry_run = false)
192
+ changes = 0
193
+ info = self.info
194
+ prefix = info[:subnet_cidr].split('/').last.to_i
195
+ gateway = IPAddr.new(info[:subnet_cidr]).succ.to_s
196
+
197
+ meta_ips = Meta.get("network/interfaces/macs/#{info[:hwaddr]}/local-ipv4s").lines.map(&:strip)
198
+ local_primary, *local_aliases = local_ips
199
+ meta_primary, *meta_aliases = meta_ips
200
+
201
+ # ensure primary ip address is correct
202
+ if name != 'eth0' && local_primary != meta_primary
203
+ unless dry_run
204
+ deconfigure
205
+ exec("addr add #{meta_primary}/#{prefix} brd + dev #{name}")
206
+ exec("route add default via #{gateway} dev #{name} table #{route_table}")
207
+ exec("route flush cache")
208
+ end
209
+ changes += 1
210
+ end
211
+
212
+ # add missing secondary ips
213
+ (meta_aliases - local_aliases).each do |ip|
214
+ exec("addr add #{ip}/#{prefix} brd + dev #{name}") unless dry_run
215
+ changes += 1
216
+ end
217
+
218
+ # remove extra secondary ips
219
+ (local_aliases - meta_aliases).each do |ip|
220
+ exec("addr del #{ip}/#{prefix} dev #{name}") unless dry_run
221
+ changes += 1
222
+ end
223
+
224
+ unless name == 'eth0'
225
+ rules_to_add = meta_ips || []
226
+ exec("rule list").lines.grep(/^([0-9]+):.*\s([0-9\.]+)\s+lookup #{route_table}/) do
227
+ unless rules_to_add.delete($2)
228
+ exec("rule delete pref #{$1}") unless dry_run
229
+ changes += 1
230
+ end
231
+ end
232
+ rules_to_add.each do |ip|
233
+ exec("rule add from #{ip} lookup #{route_table}") unless dry_run
234
+ changes += 1
235
+ end
236
+ end
237
+
238
+ @clean = nil
239
+ changes
240
+ end
241
+
242
+ # Remove configuration for an interface
243
+ def deconfigure
244
+ # assume eth0 primary ip is managed by dhcp
245
+ if name == 'eth0'
246
+ exec("addr flush dev eth0 secondary")
247
+ else
248
+ exec("rule list").lines.grep(/^([0-9]+):.*lookup #{route_table}/) do
249
+ exec("rule delete pref #{$1}")
250
+ end
251
+ exec("addr flush dev #{name}")
252
+ exec("route flush table #{route_table}")
253
+ exec("route flush cache")
254
+ end
255
+ @clean = true
256
+ end
257
+
258
+ # Add a secondary ip to this interface
259
+ def add_alias(ip)
260
+ prefix = info[:subnet_cidr].split('/').last.to_i
261
+ exec("addr add #{ip}/#{prefix} brd + dev #{name}")
262
+
263
+ unless name == 'eth0' || exec("rule list") =~ /from #{ip} lookup #{route_table}/
264
+ exec("rule add from #{ip} lookup #{route_table}")
265
+ end
266
+ end
267
+
268
+ # Remove a secondary ip from this interface
269
+ def remove_alias
270
+ prefix = info[:subnet_cidr].split('/').last.to_i
271
+ exec("addr del #{ip}/#{prefix} dev #{name}")
272
+
273
+ if name != 'eth0' && exec("rule list") =~ /([0-9]+):\s+from #{ip} lookup #{route_table}/
274
+ exec("rule delete pref #{$1}")
275
+ end
276
+ end
277
+
278
+ # Identify this interface by one of its attributes
279
+ def is?(match)
280
+ if match == name
281
+ true
282
+ else
283
+ info = self.info
284
+ match == info[:interface_id] || match == info[:hwaddr] || match == info[:subnet_id]
285
+ end
286
+ end
287
+
288
+ # Return an array representation of our interface config, including public
289
+ # ip associations and enabled status
290
+ def to_h
291
+ info.merge({
292
+ name: name,
293
+ device_number: device_number,
294
+ route_table: route_table,
295
+ local_ips: local_ips,
296
+ public_ips: public_ips,
297
+ enabled: enabled?
298
+ })
299
+ end
300
+
301
+ private
302
+
303
+ # Alias for static method
304
+ def exec(command, options = {})
305
+ self.class.exec(command, options)
306
+ end
307
+ end
308
+ end
309
+ end