puppet_x_eos_eapi 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +24 -0
  4. data/LICENSE.txt +202 -0
  5. data/README.md +87 -0
  6. data/Rakefile +1 -0
  7. data/lib/puppet_x/eos/autoload.rb +57 -0
  8. data/lib/puppet_x/eos/eapi.rb +259 -0
  9. data/lib/puppet_x/eos/module_base.rb +37 -0
  10. data/lib/puppet_x/eos/modules/daemon.rb +109 -0
  11. data/lib/puppet_x/eos/modules/extension.rb +167 -0
  12. data/lib/puppet_x/eos/modules/interface.rb +180 -0
  13. data/lib/puppet_x/eos/modules/ipinterface.rb +133 -0
  14. data/lib/puppet_x/eos/modules/mlag.rb +268 -0
  15. data/lib/puppet_x/eos/modules/ntp.rb +129 -0
  16. data/lib/puppet_x/eos/modules/ospf.rb +129 -0
  17. data/lib/puppet_x/eos/modules/portchannel.rb +277 -0
  18. data/lib/puppet_x/eos/modules/radius.rb +367 -0
  19. data/lib/puppet_x/eos/modules/snmp.rb +177 -0
  20. data/lib/puppet_x/eos/modules/switchport.rb +255 -0
  21. data/lib/puppet_x/eos/modules/system.rb +138 -0
  22. data/lib/puppet_x/eos/modules/tacacs.rb +302 -0
  23. data/lib/puppet_x/eos/modules/vlan.rb +179 -0
  24. data/lib/puppet_x/eos/modules/vxlan.rb +132 -0
  25. data/lib/puppet_x/eos/provider.rb +71 -0
  26. data/lib/puppet_x/eos/version.rb +41 -0
  27. data/lib/puppet_x/net_dev/eos_api.rb +1011 -0
  28. data/lib/puppet_x/net_dev/eos_api/common_methods.rb +27 -0
  29. data/lib/puppet_x/net_dev/eos_api/snmp_methods.rb +647 -0
  30. data/lib/puppet_x/net_dev/eos_api/version.rb +8 -0
  31. data/lib/puppet_x_eos_eapi.rb +4 -0
  32. data/puppet_x_eos_eapi.gemspec +31 -0
  33. data/spec/fixtures/fixture_all_portchannel_modes.json +8 -0
  34. data/spec/fixtures/fixture_all_portchannels_detailed.json +15 -0
  35. data/spec/fixtures/fixture_create_vlan_error.json +17 -0
  36. data/spec/fixtures/fixture_create_vlan_success.json +12 -0
  37. data/spec/fixtures/fixture_eapi_conf.yaml +4 -0
  38. data/spec/fixtures/fixture_enable_configure_vlan_3111_name_foo.json +14 -0
  39. data/spec/fixtures/fixture_enable_configure_vlan_foo_name_bar.json +19 -0
  40. data/spec/fixtures/fixture_get_snmp_communities_non_existent_acl.yaml +2 -0
  41. data/spec/fixtures/fixture_get_snmp_location_westeros.json +5 -0
  42. data/spec/fixtures/fixture_portchannel_min_links_1.json +8 -0
  43. data/spec/fixtures/fixture_portchannel_min_links_2.json +8 -0
  44. data/spec/fixtures/fixture_running_config.yaml +1 -0
  45. data/spec/fixtures/fixture_running_configuration_radius_configured.yaml +30 -0
  46. data/spec/fixtures/fixture_running_configuration_radius_default.yaml +29 -0
  47. data/spec/fixtures/fixture_running_configuration_radius_server_groups.yaml +38 -0
  48. data/spec/fixtures/fixture_running_configuration_radius_servers.yaml +34 -0
  49. data/spec/fixtures/fixture_running_configuration_tacacs_configured.yaml +38 -0
  50. data/spec/fixtures/fixture_running_configuration_tacacs_default.yaml +38 -0
  51. data/spec/fixtures/fixture_running_configuration_tacacs_groups.yaml +1 -0
  52. data/spec/fixtures/fixture_running_configuration_tacacs_groups_3.yaml +43 -0
  53. data/spec/fixtures/fixture_running_configuration_tacacs_servers.yaml +41 -0
  54. data/spec/fixtures/fixture_s4_show_etherchannel_detailed.json +9 -0
  55. data/spec/fixtures/fixture_show_flowcontrol_et1.json +5 -0
  56. data/spec/fixtures/fixture_show_interfaces.json +297 -0
  57. data/spec/fixtures/fixture_show_interfaces_switchport_format_text.json +9 -0
  58. data/spec/fixtures/fixture_show_port_channel_summary_2_lags.json +9 -0
  59. data/spec/fixtures/fixture_show_port_channel_summary_static.json +9 -0
  60. data/spec/fixtures/fixture_show_snmp_community.yaml +2 -0
  61. data/spec/fixtures/fixture_show_snmp_contact_empty.json +5 -0
  62. data/spec/fixtures/fixture_show_snmp_contact_name.json +5 -0
  63. data/spec/fixtures/fixture_show_snmp_disabled.json +5 -0
  64. data/spec/fixtures/fixture_show_snmp_enabled.json +5 -0
  65. data/spec/fixtures/fixture_show_snmp_host.yaml +2 -0
  66. data/spec/fixtures/fixture_show_snmp_host_duplicates.yaml +2 -0
  67. data/spec/fixtures/fixture_show_snmp_host_more_duplicates.yaml +2 -0
  68. data/spec/fixtures/fixture_show_snmp_location_empty.json +5 -0
  69. data/spec/fixtures/fixture_show_snmp_trap.yaml +2 -0
  70. data/spec/fixtures/fixture_show_snmp_user.yaml +2 -0
  71. data/spec/fixtures/fixture_show_snmp_user_raw_text.yaml +1 -0
  72. data/spec/fixtures/fixture_show_vlan.json +37 -0
  73. data/spec/fixtures/fixture_show_vlan_3110.json +18 -0
  74. data/spec/fixtures/fixture_show_vlan_4000.json +18 -0
  75. data/spec/fixtures/fixture_snmp_host_opts.yaml +11 -0
  76. data/spec/spec_helper.rb +21 -0
  77. data/spec/support/fixtures.rb +104 -0
  78. data/spec/unit/puppet_x/eos/eapi_spec.rb +182 -0
  79. data/spec/unit/puppet_x/eos/module_base_spec.rb +26 -0
  80. data/spec/unit/puppet_x/eos/modules/daemon_spec.rb +110 -0
  81. data/spec/unit/puppet_x/eos/modules/extension_spec.rb +197 -0
  82. data/spec/unit/puppet_x/eos/modules/fixtures/daemon_getall.json +3 -0
  83. data/spec/unit/puppet_x/eos/modules/fixtures/extension_getall.json +28 -0
  84. data/spec/unit/puppet_x/eos/modules/fixtures/hostname.json +6 -0
  85. data/spec/unit/puppet_x/eos/modules/fixtures/interface_getall.json +509 -0
  86. data/spec/unit/puppet_x/eos/modules/fixtures/ipinterface_getall.json +56 -0
  87. data/spec/unit/puppet_x/eos/modules/fixtures/mlag_get.json +21 -0
  88. data/spec/unit/puppet_x/eos/modules/fixtures/mlag_get_interfaces.json +18 -0
  89. data/spec/unit/puppet_x/eos/modules/fixtures/ntp_get.json +5 -0
  90. data/spec/unit/puppet_x/eos/modules/fixtures/ospf_instance_getall.json +58 -0
  91. data/spec/unit/puppet_x/eos/modules/fixtures/portchannel_get.json +54 -0
  92. data/spec/unit/puppet_x/eos/modules/fixtures/portchannel_getlacpmode.json +5 -0
  93. data/spec/unit/puppet_x/eos/modules/fixtures/portchannel_getmembers.json +5 -0
  94. data/spec/unit/puppet_x/eos/modules/fixtures/portchannel_po1.json +7 -0
  95. data/spec/unit/puppet_x/eos/modules/fixtures/snmp_get.json +14 -0
  96. data/spec/unit/puppet_x/eos/modules/fixtures/switchport_get.json +5 -0
  97. data/spec/unit/puppet_x/eos/modules/fixtures/switchport_get_et1.json +7 -0
  98. data/spec/unit/puppet_x/eos/modules/fixtures/switchport_getall_interfaces.json +230 -0
  99. data/spec/unit/puppet_x/eos/modules/fixtures/system_domain_list.json +5 -0
  100. data/spec/unit/puppet_x/eos/modules/fixtures/system_domain_name.json +5 -0
  101. data/spec/unit/puppet_x/eos/modules/fixtures/system_hostname.json +6 -0
  102. data/spec/unit/puppet_x/eos/modules/fixtures/system_name_servers.json +5 -0
  103. data/spec/unit/puppet_x/eos/modules/fixtures/vlan_getall.json +123 -0
  104. data/spec/unit/puppet_x/eos/modules/fixtures/vxlan_get.json +24 -0
  105. data/spec/unit/puppet_x/eos/modules/interface_spec.rb +281 -0
  106. data/spec/unit/puppet_x/eos/modules/ipinterface_spec.rb +143 -0
  107. data/spec/unit/puppet_x/eos/modules/mlag_spec.rb +349 -0
  108. data/spec/unit/puppet_x/eos/modules/ntp_spec.rb +136 -0
  109. data/spec/unit/puppet_x/eos/modules/ospf_spec.rb +143 -0
  110. data/spec/unit/puppet_x/eos/modules/portchannel_spec.rb +357 -0
  111. data/spec/unit/puppet_x/eos/modules/radius_spec.rb +509 -0
  112. data/spec/unit/puppet_x/eos/modules/snmp_spec.rb +202 -0
  113. data/spec/unit/puppet_x/eos/modules/switchport_get_et1.json +7 -0
  114. data/spec/unit/puppet_x/eos/modules/switchport_spec.rb +307 -0
  115. data/spec/unit/puppet_x/eos/modules/system_spec.rb +170 -0
  116. data/spec/unit/puppet_x/eos/modules/tacacs_spec.rb +448 -0
  117. data/spec/unit/puppet_x/eos/modules/vlan_spec.rb +244 -0
  118. data/spec/unit/puppet_x/eos/modules/vxlan_spec.rb +189 -0
  119. data/spec/unit/puppet_x/eos/provider_spec.rb +35 -0
  120. data/spec/unit/puppet_x/net_dev/eos_api/common_methods_spec.rb +34 -0
  121. data/spec/unit/puppet_x/net_dev/eos_api/snmp_methods_spec.rb +842 -0
  122. data/spec/unit/puppet_x/net_dev/eos_api_spec.rb +1000 -0
  123. metadata +369 -0
@@ -0,0 +1,1000 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe PuppetX::NetDev::EosApi do
6
+ let(:api) { described_class.new }
7
+
8
+ ##
9
+ # api_response returns a JSON fixture that presents a representative REST API
10
+ # response. The method is memoized to reduce filesystem operations.
11
+ #
12
+ # @param [Symbol] key The fixture to load, e.g. :foo will load
13
+ # 'fixture_foo.json'
14
+ def api_response(key)
15
+ memo = Fixtures[key]
16
+ return memo if memo
17
+ file = File.join(File.dirname(__FILE__), "fixture_#{key}.json")
18
+ Fixtures[key] = JSON.load(File.read(file))
19
+ end
20
+
21
+ context 'initializing the API instance' do
22
+ [:address, :port, :username, :password].each do |option|
23
+ it "initializes with #{option}" do
24
+ api = described_class.new(option => 'foo')
25
+ expect(api.send(option)).to eq('foo')
26
+ end
27
+ end
28
+
29
+ it 'address defaults to unix:///var/run/command-api.sock' do
30
+ expect(subject.address).to eq('unix:///var/run/command-api.sock')
31
+ end
32
+ end
33
+
34
+ describe '#vlan(id)' do
35
+ context 'when the vlan exists' do
36
+ subject { api.vlan(3110) }
37
+
38
+ before do
39
+ allow(api).to receive(:eapi_call)
40
+ .with('show vlan 3110', {})
41
+ .and_return(fixture(:show_vlan_3110))
42
+ end
43
+
44
+ it 'has only one key' do
45
+ expect(subject.size).to eq 1
46
+ end
47
+ it { is_expected.to be_a_kind_of Hash }
48
+ it { is_expected.to have_key '3110' }
49
+ it { is_expected.not_to have_key 'results' }
50
+ end
51
+
52
+ context 'when the vlan does not exist' do
53
+ subject { api.vlan(4000) }
54
+
55
+ before do
56
+ allow(api).to receive(:eapi_call)
57
+ .with('show vlan 4000', {})
58
+ .and_return(fixture(:show_vlan_4000))
59
+ end
60
+
61
+ it 'raises Puppet:Error with message "could not list vlans"' do
62
+ expect { subject }.to raise_error PuppetX::NetDev::ApiError,
63
+ /could not list vlans/
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '#vlan_create(id)' do
69
+ subject { api.vlan_create(3100) }
70
+
71
+ context 'when eAPI reports no errors' do
72
+ it 'accepts 3100 without error' do
73
+ allow(api).to receive(:eapi_call)
74
+ .with(['enable', 'configure', 'vlan 3100'], {})
75
+ .and_return(fixture(:create_vlan_success))
76
+ subject
77
+ end
78
+ end
79
+
80
+ context 'when eAPI reports errors' do
81
+ before do
82
+ allow(api).to receive(:eapi_call)
83
+ .with(['enable', 'configure', 'vlan 3100'], {})
84
+ .and_return(fixture(:create_vlan_error))
85
+ end
86
+
87
+ it 'raises PuppetX::NetDev::ApiError on eAPI errors' do
88
+ expect { subject }.to raise_error PuppetX::NetDev::ApiError,
89
+ /could not create vlan 3100/
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#http' do
95
+ it 'Returns a NetX::HTTPUnix instance' do
96
+ expect(subject.send(:http)).to be_a_kind_of NetX::HTTPUnix
97
+ end
98
+
99
+ it 'Attempts to open address unix:///dev/null as a socket' do
100
+ socket_http = described_class.new(address: 'unix:///dev/null').send(:http)
101
+ expect { socket_http.get('/') }.to raise_error Errno::ENOTSOCK
102
+ end
103
+ end
104
+
105
+ describe '#format_command' do
106
+ context 'with a single command' do
107
+ subject do
108
+ json = described_class.new.send(:format_command, 'list vlan')
109
+ JSON.parse(json)
110
+ end
111
+
112
+ it 'accepts "list vlan" as a single command and returns JSON' do
113
+ expect(subject['params']['cmds']).to eq(['list vlan'])
114
+ end
115
+
116
+ it 'generates an ID when an explicit id is not given' do
117
+ expect(subject['id']).not_to be_empty
118
+ end
119
+
120
+ it 'uses the explicitly provided ID' do
121
+ json = described_class.new.send(:format_command,
122
+ 'list vlan',
123
+ id: 'ID:X')
124
+ expect(JSON.parse(json)['id']).to eq 'ID:X'
125
+ end
126
+ end
127
+ end
128
+
129
+ describe '#eapi_request' do
130
+ subject { api.send(:eapi_request, '{}') }
131
+
132
+ let :mock_http_post do
133
+ post = double(Net::HTTP::Post)
134
+ allow(post).to receive(:basic_auth)
135
+ post
136
+ end
137
+
138
+ before :each do
139
+ allow(Net::HTTP::Post).to receive(:new)
140
+ .with('/command-api/')
141
+ .and_return(mock_http_post)
142
+ end
143
+
144
+ it 'sets the body of the post to the string provided' do
145
+ allow(mock_http_post).to receive(:body=).with('{}')
146
+ subject
147
+ end
148
+ end
149
+
150
+ describe '#eapi_call' do
151
+ subject { api.send(:eapi_call, 'show vlan') }
152
+
153
+ before :each do
154
+ mock_response = double(Net::HTTPOK)
155
+ allow(mock_response).to receive(:body)
156
+ .and_return(fixture(:show_vlan, format: :json))
157
+
158
+ mock_http = double(NetX::HTTPUnix)
159
+ allow(mock_http).to receive(:request)
160
+ .and_return(mock_response)
161
+
162
+ allow(api).to receive(:http).and_return(mock_http)
163
+ end
164
+
165
+ it 'decodes the JSON response from the switch' do
166
+ expect(subject).to eq(fixture(:show_vlan))
167
+ end
168
+ end
169
+
170
+ describe '#eapi_action' do
171
+ before :each do
172
+ mock_response = double(Net::HTTPOK)
173
+ allow(mock_response).to receive(:body)
174
+ .and_return(api_response_body)
175
+
176
+ mock_http = double(NetX::HTTPUnix)
177
+ allow(mock_http).to receive(:request)
178
+ .and_return(mock_response)
179
+
180
+ allow(api).to receive(:http).and_return(mock_http)
181
+ end
182
+
183
+ context 'format: "json" (default)' do
184
+ let :api_response_body do
185
+ fixture(:show_vlan, format: :json)
186
+ end
187
+
188
+ it 'accepts a human readable description as argument 2' do
189
+ expect(api.send(:eapi_action, 'show vlan', 'show all vlans'))
190
+ .to eq(fixture(:show_vlan)['result'])
191
+ end
192
+ end
193
+
194
+ context 'format: "text"' do
195
+ let :api_response_body do
196
+ fixture(:show_interfaces_switchport_format_text, format: :json)
197
+ end
198
+
199
+ it 'accepts a keyword argument of :format => "text"' do
200
+ args = ['show interfaces switchport', 'desc', { format: 'text' }]
201
+ expect(api.send(:eapi_action, *args))
202
+ .to eq(JSON.parse(api_response_body)['result'])
203
+ end
204
+ end
205
+ end
206
+
207
+ describe '#all_vlans' do
208
+ subject { api.all_vlans }
209
+
210
+ before do
211
+ allow(api).to receive(:eapi_call)
212
+ .with('show vlan', {})
213
+ .and_return(fixture(:show_vlan))
214
+ end
215
+
216
+ it { is_expected.to be_a_kind_of Hash }
217
+ it { is_expected.to have_key '1' }
218
+ it { is_expected.to have_key '3110' }
219
+ it { is_expected.not_to have_key 'results' }
220
+
221
+ describe '#all_vlans["1"]' do
222
+ subject { api.all_vlans['1'] }
223
+
224
+ %w(status name interfaces dynamic).each do |k|
225
+ it { is_expected.to have_key k }
226
+ end
227
+ end
228
+ end
229
+
230
+ describe '#all_interfaces' do
231
+ subject { api.all_interfaces }
232
+
233
+ before do
234
+ allow(api).to receive(:eapi_call)
235
+ .with('show interfaces', {})
236
+ .and_return(fixture(:show_interfaces))
237
+ end
238
+
239
+ it { is_expected.not_to have_key 'results' }
240
+ it { is_expected.to have_key 'Management1' }
241
+
242
+ describe '#all_interfaces["Management1"]' do
243
+ subject { api.all_interfaces['Management1'] }
244
+
245
+ it { is_expected.to be_a_kind_of Hash }
246
+ it 'has an mtu of 1500' do
247
+ expect(subject['mtu']).to eq 1500
248
+ end
249
+ it 'is hardware ethernet' do
250
+ expect(subject['hardware']).to eq 'ethernet'
251
+ end
252
+ it 'has duplex of duplexFull' do
253
+ expect(subject['duplex']).to eq 'duplexFull'
254
+ end
255
+ it 'has bandwidth of 1000000000' do
256
+ expect(subject['bandwidth']).to eq 1_000_000_000
257
+ end
258
+ it 'has an interfaceAddress key with Array value' do
259
+ expect(subject['interfaceAddress']).to be_an Array
260
+ end
261
+ it 'has a physicalAddress of 00:42:00:6e:00:96' do
262
+ expect(subject['physicalAddress']).to eq '00:42:00:6e:00:96'
263
+ end
264
+ end
265
+ end
266
+
267
+ describe '#all_portchannels_detailed' do
268
+ subject { api.all_portchannels_detailed }
269
+
270
+ before :each do
271
+ allow(api).to receive(:eapi_call)
272
+ .with('show etherchannel detailed', format: 'text')
273
+ .and_return(fixture(:s4_show_etherchannel_detailed))
274
+ end
275
+
276
+ it { is_expected.not_to have_key 'results' }
277
+ it { is_expected.to have_key 'Port-Channel3' }
278
+ it { is_expected.to have_key 'Port-Channel4' }
279
+
280
+ describe 'nested attribute hash' do
281
+ subject { api.all_portchannels_detailed['Port-Channel3'] }
282
+ it { is_expected.to have_key 'name' }
283
+ it { is_expected.to have_key 'ports' }
284
+
285
+ describe 'ports array' do
286
+ subject { api.all_portchannels_detailed['Port-Channel3']['ports'] }
287
+ it { is_expected.to include('Ethernet1') }
288
+ it { is_expected.to include('Ethernet2') }
289
+ it 'sorts the list of member ports' do
290
+ expect(subject).to eq(subject.sort)
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ describe '#all_portchannel_modes' do
297
+ subject { api.all_portchannel_modes }
298
+
299
+ context 'with two LAGS, one LACP passive, one active' do
300
+ before :each do
301
+ allow(api).to receive(:eapi_call)
302
+ .with('show port-channel summary', format: 'text')
303
+ .and_return(fixture(:show_port_channel_summary_2_lags))
304
+ end
305
+
306
+ let :expected_results do
307
+ {
308
+ 'Port-Channel3' => { 'mode' => :passive },
309
+ 'Port-Channel4' => { 'mode' => :active }
310
+ }
311
+ end
312
+
313
+ it { is_expected.to have_key 'Port-Channel3' }
314
+ it { is_expected.to have_key 'Port-Channel4' }
315
+ it { is_expected.to eq expected_results }
316
+ end
317
+
318
+ context 'with one LAG in LACP static mode' do
319
+ before :each do
320
+ allow(api).to receive(:eapi_call)
321
+ .with('show port-channel summary', format: 'text')
322
+ .and_return(fixture(:show_port_channel_summary_static))
323
+ end
324
+
325
+ let :expected_results do
326
+ {
327
+ 'Port-Channel4' => { 'mode' => :active },
328
+ 'Port-Channel9' => { 'mode' => :disabled }
329
+ }
330
+ end
331
+
332
+ it { is_expected.to have_key 'Port-Channel4' }
333
+ it { is_expected.to have_key 'Port-Channel9' }
334
+ it { is_expected.to eq expected_results }
335
+ end
336
+ end
337
+
338
+ describe '#all_portchannels' do
339
+ subject { api.all_portchannels }
340
+
341
+ before :each do
342
+ allow(api).to receive(:all_portchannels_detailed)
343
+ .and_return(fixture(:all_portchannels_detailed))
344
+ allow(api).to receive(:all_portchannel_modes)
345
+ .and_return(fixture(:all_portchannel_modes))
346
+ allow(api).to receive(:portchannel_min_links).and_return(2)
347
+ end
348
+
349
+ it { is_expected.to be_a Hash }
350
+ it { is_expected.to have_key 'Port-Channel4' }
351
+ it { is_expected.to have_key 'Port-Channel9' }
352
+ it 'has a mode attribute for each port channel' do
353
+ modes = subject.values.map { |v| v['mode'] }
354
+ expect(modes).to eq %w(active active)
355
+ end
356
+ end
357
+
358
+ describe '#channel_group_destroy' do
359
+ let :port_channels_detailed do
360
+ data = {
361
+ 'name' => 'Port-Channel9',
362
+ 'ports' => %w(Ethernet1 Ethernet2)
363
+ }
364
+ { 'Port-Channel9' => data }
365
+ end
366
+
367
+ before :each do
368
+ allow(api).to receive(:all_portchannels_detailed)
369
+ .and_return(port_channels_detailed)
370
+ end
371
+
372
+ it 'removes interfaces from the channel group' do
373
+ expect(api).to receive(:interface_unset_channel_group).with('Ethernet1')
374
+ expect(api).to receive(:interface_unset_channel_group).with('Ethernet2')
375
+ api.channel_group_destroy('Port-Channel9')
376
+ end
377
+
378
+ it 'raises ArgumentError when the channel group is unknown' do
379
+ expect { api.channel_group_destroy('Port-Channel1') }
380
+ .to raise_error ArgumentError, /Port-Channel1 is not in \[.*?\]/
381
+ end
382
+ end
383
+
384
+ describe '#interface_unset_channel_group' do
385
+ it 'sets no channel-group using the API' do
386
+ cmd = %w(enable configure) << 'interface Ethernet1'
387
+ cmd << 'no channel-group'
388
+
389
+ expect(api).to receive(:eapi_action)
390
+ .with(cmd, 'remove Ethernet1 from channel group')
391
+
392
+ api.interface_unset_channel_group('Ethernet1')
393
+ end
394
+ end
395
+
396
+ describe '#port_channel_destroy' do
397
+ it 'removes the channel interface' do
398
+ cmd = %w(enable configure) << 'no interface Port-Channel1'
399
+
400
+ expect(api).to receive(:eapi_action)
401
+ .with(cmd, 'remove Port-Channel1')
402
+
403
+ api.port_channel_destroy('Port-Channel1')
404
+ end
405
+ end
406
+
407
+ describe '#channel_group_create' do
408
+ context 'with Port-Channel9 and Ethernet1 LACP mode active' do
409
+ it 'sets the interface channel group to active' do
410
+ expect(api).to receive(:interface_set_channel_group)
411
+ .with('Ethernet1', mode: :active, group: 9)
412
+
413
+ api.channel_group_create('Port-Channel9',
414
+ interfaces: ['Ethernet1'],
415
+ mode: :active)
416
+ end
417
+ end
418
+ end
419
+
420
+ describe '#interface_set_channel_group' do
421
+ subject do
422
+ api.interface_set_channel_group('Ethernet1', group: 9, mode: mode)
423
+ end
424
+ let(:cmd_prefix) { ['enable', 'configure', 'interface Ethernet1'] }
425
+ let(:msg) { 'join Ethernet1 to channel group 9' }
426
+
427
+ context 'when mode is active' do
428
+ let(:mode) { :active }
429
+ let(:config_cmd) { 'channel-group 9 mode active' }
430
+
431
+ it 'configures Ethernet1 in group 9 as active' do
432
+ expect(api).to receive(:eapi_action)
433
+ .with([*cmd_prefix, config_cmd], msg)
434
+
435
+ subject
436
+ end
437
+ end
438
+
439
+ context 'when mode is passive' do
440
+ let(:mode) { :passive }
441
+ let(:config_cmd) { 'channel-group 9 mode passive' }
442
+
443
+ it 'configures Ethernet1 in group 9 as passive' do
444
+ expect(api).to receive(:eapi_action)
445
+ .with([*cmd_prefix, config_cmd], msg)
446
+
447
+ subject
448
+ end
449
+ end
450
+
451
+ context 'when mode is disabled' do
452
+ let(:mode) { :disabled }
453
+ let(:config_cmd) { 'channel-group 9 mode on' }
454
+
455
+ it 'configures Ethernet1 in group 9 as static' do
456
+ expect(api).to receive(:eapi_action)
457
+ .with([*cmd_prefix, config_cmd], msg)
458
+
459
+ subject
460
+ end
461
+ end
462
+
463
+ context 'when mode is invalid' do
464
+ it 'raises ArgumentError' do
465
+ expect { api.interface_set_channel_group('Ethernet1', mode: 'bad') }
466
+ .to raise_error ArgumentError, 'Unknown LACP mode bad'
467
+ end
468
+ end
469
+ end
470
+
471
+ describe '#uri' do
472
+ context 'with username and password' do
473
+ let :opts do
474
+ { address: 'localhost', username: 'foo', password: 'bar' }
475
+ end
476
+ subject { described_class.new(opts).uri.to_s }
477
+ it { is_expected.to eq 'http://foo:bar@localhost' }
478
+ end
479
+
480
+ context 'without username and password' do
481
+ subject { described_class.new(address: 'foo.lan', port: 90).uri.to_s }
482
+ it { is_expected.to eq 'http://admin:puppet@foo.lan:90' }
483
+ end
484
+ end
485
+
486
+ describe '#set_vlan_name' do
487
+ context 'with valid arguments of 3111, "foo"' do
488
+ before do
489
+ allow(api).to receive(:eapi_call)
490
+ .with(['enable', 'configure', 'vlan 3111', 'name foo'], {})
491
+ .and_return(fixture(:enable_configure_vlan_3111_name_foo))
492
+ end
493
+
494
+ it 'names the vlan "foo"' do
495
+ api.set_vlan_name(3111, 'foo')
496
+ end
497
+ end
498
+
499
+ context 'with invalid arguments of "foo", "bar"' do
500
+ before do
501
+ allow(api).to receive(:eapi_call)
502
+ .with(['enable', 'configure', 'vlan foo', 'name bar'], {})
503
+ .and_return(fixture(:enable_configure_vlan_foo_name_bar))
504
+ end
505
+
506
+ it 'raises PuppetX::NetDev::ApiError' do
507
+ expect { api.set_vlan_name('foo', 'bar') }
508
+ .to raise_error PuppetX::NetDev::ApiError
509
+ end
510
+ end
511
+ end
512
+
513
+ describe '#vlan_destroy' do
514
+ context 'with valid arguments of 3111' do
515
+ let :api_response do
516
+ {
517
+ 'jsonrpc' => '2.0',
518
+ 'result' => [{}, {}, {}],
519
+ 'id' => '7af750fd-9324-4f91-b4fb-cedf0c6d6a91'
520
+ }
521
+ end
522
+
523
+ before do
524
+ allow(api).to receive(:eapi_call)
525
+ .with(['enable', 'configure', 'no vlan 3111'], {})
526
+ .and_return(api_response)
527
+ end
528
+
529
+ it 'destroys the vlan without raising errors' do
530
+ api.vlan_destroy(3111)
531
+ end
532
+ end
533
+
534
+ context 'with invalid arguments of "foo"' do
535
+ let :api_response do
536
+ msg = "CLI command 3 of 3 'no vlan foo' failed: invalid command"
537
+ {
538
+ 'jsonrpc' => '2.0',
539
+ 'id' => '1cc2e684-2928-4bfe-a86c-7a1397ea05fd',
540
+ 'error' => {
541
+ 'data' => [
542
+ {},
543
+ {},
544
+ {
545
+ 'errors' => ["Invalid input (at token 2: 'foo')"]
546
+ }
547
+ ],
548
+ 'message' => msg,
549
+ 'code' => 1002
550
+ }
551
+ }
552
+ end
553
+
554
+ before do
555
+ allow(api).to receive(:eapi_call)
556
+ .with(['enable', 'configure', 'no vlan foo'], {})
557
+ .and_return(api_response)
558
+ end
559
+
560
+ it 'raises PuppetX::NetDev::ApiError' do
561
+ expect { api.vlan_destroy('foo') }.to raise_error PuppetX::NetDev::ApiError
562
+ end
563
+ end
564
+ end
565
+
566
+ describe '#set_vlan_state' do
567
+ context 'with valid arguments' do
568
+ let :api_response do
569
+ {
570
+ 'jsonrpc' => '2.0',
571
+ 'result' => [{}, {}, {}],
572
+ 'id' => '7af750fd-9324-4f91-b4fb-cedf0c6d6a91'
573
+ }
574
+ end
575
+
576
+ it 'returns the API response as a Hash when state is "active"' do
577
+ allow(api).to receive(:eapi_call)
578
+ .with(['enable', 'configure', 'vlan 3111', 'state active'], {})
579
+ .and_return(api_response)
580
+ api.set_vlan_state(3111, 'active')
581
+ end
582
+
583
+ it 'returns the API response as a Hash when state is "suspend"' do
584
+ allow(api).to receive(:eapi_call)
585
+ .with(['enable', 'configure', 'vlan 3111', 'state suspend'], {})
586
+ .and_return(api_response)
587
+ api.set_vlan_state(3111, 'suspend')
588
+ end
589
+ end
590
+
591
+ context 'with invalid arguments' do
592
+ let :api_response do
593
+ msg = "CLI command 4 of 4 'state foo' failed: invalid command"
594
+ {
595
+ 'jsonrpc' => '2.0',
596
+ 'id' => '1ed4e6ba-89f0-45fa-aeba-f7816b4e7da3',
597
+ 'error' => {
598
+ 'data' => [
599
+ {},
600
+ {},
601
+ {
602
+ 'errors' => ["Invalid input (at token 1: 'foo')"]
603
+ }
604
+ ],
605
+ 'message' => msg,
606
+ 'code' => 1002
607
+ }
608
+ }
609
+ end
610
+
611
+ before do
612
+ allow(api).to receive(:eapi_call)
613
+ .with(['enable', 'configure', 'vlan 3111', 'state foo'], {})
614
+ .and_return(api_response)
615
+ end
616
+
617
+ it 'raises PuppetX::NetDev::ApiError' do
618
+ expect { api.set_vlan_state(3111, 'foo') }
619
+ .to raise_error PuppetX::NetDev::ApiError
620
+ end
621
+ end
622
+ end
623
+
624
+ describe '#set_interface_state' do
625
+ context 'with valid arguments' do
626
+ let :api_response do
627
+ {
628
+ 'jsonrpc' => '2.0',
629
+ 'result' => [{}, {}, {}],
630
+ 'id' => '7af750fd-9324-4f91-b4fb-cedf0c6d6a91'
631
+ }
632
+ end
633
+
634
+ it 'accepts "shutdown"' do
635
+ allow(api).to receive(:eapi_call)
636
+ .with(['enable', 'configure', 'interface Ethernet1', 'shutdown'], {})
637
+ .and_return(api_response)
638
+ api.set_interface_state('Ethernet1', 'shutdown')
639
+ end
640
+
641
+ it 'accepts "no shutdown"' do
642
+ args = ['enable', 'configure', 'interface Ethernet1', 'no shutdown']
643
+ allow(api).to receive(:eapi_call)
644
+ .with(args, {})
645
+ .and_return(api_response)
646
+ api.set_interface_state('Ethernet1', 'no shutdown')
647
+ end
648
+ end
649
+ context 'with invalid arguments' do
650
+ let :api_response do
651
+ {
652
+ 'jsonrpc' => '2.0',
653
+ 'id' => 'f449e942-940f-499e-989f-74ab4b8b9950',
654
+ 'error' => {
655
+ 'data' => [
656
+ {},
657
+ {},
658
+ {},
659
+ { 'errors' => ["Invalid input (at token 0: 'garbage')"] }
660
+ ],
661
+ 'message' => "CLI command 4 of 4 'garbage' failed: invalid command",
662
+ 'code' => 1002
663
+ }
664
+ }
665
+ end
666
+
667
+ it 'raises PuppetX::NetDev::ApiError on an API error' do
668
+ allow(api).to receive(:eapi_call)
669
+ .with(['enable', 'configure', 'interface Ethernet1', 'garbage'], {})
670
+ .and_return(api_response)
671
+ expect do
672
+ api.set_interface_state('Ethernet1', 'garbage')
673
+ end.to raise_error PuppetX::NetDev::ApiError
674
+ end
675
+ end
676
+ end
677
+
678
+ describe '#set_interface_description' do
679
+ context 'with valid arguments' do
680
+ let :api_response do
681
+ {
682
+ 'jsonrpc' => '2.0',
683
+ 'result' => [{}, {}, {}],
684
+ 'id' => '7af750fd-9324-4f91-b4fb-cedf0c6d6a91'
685
+ }
686
+ end
687
+
688
+ it 'accepts a non-empty string' do
689
+ preamble = ['enable', 'configure', 'interface Ethernet1']
690
+ allow(api).to receive(:eapi_call)
691
+ .with([*preamble, 'description foobar'], {})
692
+ .and_return(api_response)
693
+ api.set_interface_description('Ethernet1', 'foobar')
694
+ end
695
+ end
696
+
697
+ context 'with invalid arguments' do
698
+ let :api_response do
699
+ msg = "CLI command 4 of 4 'description ' failed: invalid command"
700
+ {
701
+ 'jsonrpc' => '2.0',
702
+ 'id' => 'b2c792ea-f256-42f3-a60d-5a094b29fb0b',
703
+ 'error' => {
704
+ 'data' => [
705
+ {},
706
+ {},
707
+ {},
708
+ { 'errors' => ['incomplete command (at token 1: None)'] }
709
+ ],
710
+ 'message' => msg,
711
+ 'code' => 1002
712
+ }
713
+ }
714
+ end
715
+
716
+ it 'raises PuppetX::NetDev::ApiError on an API error' do
717
+ args = ['enable', 'configure', 'interface Ethernet1', 'description ']
718
+ allow(api).to receive(:eapi_call)
719
+ .with(args, {})
720
+ .and_return(api_response)
721
+ expect do
722
+ api.set_interface_description('Ethernet1', '')
723
+ end.to raise_error PuppetX::NetDev::ApiError, /incomplete command/
724
+ end
725
+ end
726
+ end
727
+
728
+ describe '#set_interface_speed' do
729
+ context 'with valid arguments' do
730
+ let :api_response do
731
+ {
732
+ 'jsonrpc' => '2.0',
733
+ 'result' => [{}, {}, {}],
734
+ 'id' => '7af750fd-9324-4f91-b4fb-cedf0c6d6a91'
735
+ }
736
+ end
737
+
738
+ it 'accepts 1000full' do
739
+ preamble = ['enable', 'configure', 'interface Ethernet1']
740
+ allow(api).to receive(:eapi_call)
741
+ .with([*preamble, 'speed forced 1000full'], {})
742
+ .and_return(api_response)
743
+ api.set_interface_speed('Ethernet1', '1000full')
744
+ end
745
+ end
746
+
747
+ context 'when the API returns an error' do
748
+ let :api_response do
749
+ errors = 'Speed and duplex settings are not compatible '\
750
+ 'with transceiver for interface Ethernet1.'
751
+ {
752
+ 'jsonrpc' => '2.0',
753
+ 'id' => 'd110e10b-a3e9-4378-8361-fd04f1f552fd',
754
+ 'error' => {
755
+ 'data' => [
756
+ {},
757
+ {},
758
+ {},
759
+ { 'errors' => [*errors] }
760
+ ],
761
+ 'message' => "CLI command 4 of 4 'speed forced 1000full'"\
762
+ ' failed: could not run command',
763
+ 'code' => 1000
764
+ }
765
+ }
766
+ end
767
+
768
+ it 'raises PuppetX::NetDev::ApiError with error message from the api' do
769
+ preamble = ['enable', 'configure', 'interface Ethernet1']
770
+ allow(api).to receive(:eapi_call)
771
+ .with([*preamble, 'speed forced 1000full'], {})
772
+ .and_return(api_response)
773
+ expect do
774
+ api.set_interface_speed('Ethernet1', '1000full')
775
+ end.to raise_error PuppetX::NetDev::ApiError,
776
+ /not compatible with transceiver/
777
+ end
778
+ end
779
+ end
780
+
781
+ describe '#set_interface_mtu' do
782
+ context 'with valid arguments' do
783
+ let :api_response do
784
+ {
785
+ 'jsonrpc' => '2.0',
786
+ 'result' => [{}, {}, {}],
787
+ 'id' => '7af750fd-9324-4f91-b4fb-cedf0c6d6a91'
788
+ }
789
+ end
790
+
791
+ it 'accepts 9000' do
792
+ preamble = ['enable', 'configure', 'interface Ethernet1']
793
+ allow(api).to receive(:eapi_call)
794
+ .with([*preamble, 'mtu 9000'], {})
795
+ .and_return(api_response)
796
+ api.set_interface_mtu('Ethernet1', 9000)
797
+ end
798
+ end
799
+ end
800
+
801
+ describe '#portchannel_min_links' do
802
+ subject { api.portchannel_min_links(name) }
803
+
804
+ before :each do
805
+ api_commands = ['enable', "show running-config interfaces #{name}"]
806
+ msg = 'obtain port channel min links value'
807
+
808
+ allow(api).to receive(:eapi_action)
809
+ .with(api_commands, msg, format: 'text')
810
+ .and_return(fixture(fixture_name))
811
+ end
812
+
813
+ context 'min-links 2 in runnning-config' do
814
+ let(:name) { 'Port-Channel4' }
815
+ let(:fixture_name) { :portchannel_min_links_1 }
816
+
817
+ it 'returns 2 for the number of minimum links' do
818
+ is_expected.to eq(2)
819
+ end
820
+ end
821
+
822
+ context 'min-links not in runnning-config' do
823
+ let(:name) { 'Port-Channel9' }
824
+ let(:fixture_name) { :portchannel_min_links_2 }
825
+
826
+ it 'assumes and returns 0 for the number of minimum links' do
827
+ is_expected.to eq(0)
828
+ end
829
+ end
830
+ end
831
+
832
+ describe '#set_portchannel_min_links' do
833
+ subject { api.set_portchannel_min_links(name, min_links) }
834
+ let(:name) { 'Port-Channel4' }
835
+ let(:expected) do
836
+ cmd = %w(enable configure) << "interface #{name}"
837
+ cmd << "port-channel min-links #{min_links}"
838
+ end
839
+
840
+ [0, 1, 2, 3].each do |val|
841
+ context "when min-links is #{val}" do
842
+ let(:min_links) { val }
843
+
844
+ it "calls eapi_action to set min links to #{val}" do
845
+ expect(api).to receive(:eapi_action)
846
+ .with(expected, 'set port-channel min links')
847
+ subject
848
+ end
849
+ end
850
+ end
851
+ end
852
+
853
+ describe '#parse_min_links' do
854
+ subject { api.parse_min_links(text) }
855
+ context 'when text does not contain a min-links line' do
856
+ let(:text) { "interface Port-Channel4\n description Office Backbone\n" }
857
+ it { is_expected.to eq(0) }
858
+ end
859
+
860
+ context 'when text contain min-links 2' do
861
+ let(:text) { "interface Port-Channel4\n port-channel min-links 2\n" }
862
+ it { is_expected.to eq(2) }
863
+ end
864
+ end
865
+
866
+ describe '#get_flowcontrol' do
867
+ subject { api.get_flowcontrol(name) }
868
+ let(:name) { 'Ethernet1' }
869
+
870
+ before :each do
871
+ allow(api).to receive(:eapi_action)
872
+ .with('show flowcontrol interface Ethernet1',
873
+ 'get flowcontrol config',
874
+ format: 'text')
875
+ .and_return(fixture(:show_flowcontrol_et1))
876
+ end
877
+ it { is_expected.to eq(send: 'off', receive: 'off') }
878
+ end
879
+
880
+ describe '#parse_flowcontrol_single' do
881
+ subject { api.parse_flowcontrol_single(text) }
882
+
883
+ context 'when the input text is valid' do
884
+ let(:text) do
885
+ <<-TEXT
886
+ Port Send FlowControl Receive FlowControl RxPause TxPause
887
+ admin oper admin oper
888
+ ---------- -------- -------- -------- -------- ------------- -------------
889
+ Et1 off unknown off unknown 0 0
890
+ TEXT
891
+ end
892
+
893
+ it { is_expected.to eq(send: 'off', receive: 'off') }
894
+ end
895
+
896
+ context 'when the input text is invalid' do
897
+ let(:text) { 'garbage' }
898
+ it 'raises ArgumentError' do
899
+ expect { subject }.to raise_error ArgumentError, /could not parse/
900
+ end
901
+ end
902
+ end
903
+
904
+ describe '#set_flowcontrol_send' do
905
+ subject { api.set_flowcontrol_send(name, value) }
906
+ let(:name) { 'Ethernet1' }
907
+ let(:value) { :on }
908
+ let(:expected) do
909
+ cmd = %w(enable configure) << "interface #{name}"
910
+ cmd << "flowcontrol send #{value}"
911
+ end
912
+
913
+ it 'calls eapi_action to configure the device' do
914
+ expect(api).to receive(:eapi_action)
915
+ .with(expected, 'configure flowcontrol send')
916
+ subject
917
+ end
918
+ end
919
+
920
+ describe '#set_flowcontrol_recv' do
921
+ subject { api.set_flowcontrol_recv(name, value) }
922
+ let(:name) { 'Ethernet1' }
923
+ let(:value) { :on }
924
+ let(:expected) do
925
+ cmd = %w(enable configure) << "interface #{name}"
926
+ cmd << "flowcontrol receive #{value}"
927
+ end
928
+
929
+ it 'calls eapi_action to configure the device' do
930
+ expect(api).to receive(:eapi_action)
931
+ .with(expected, 'configure flowcontrol receive')
932
+ subject
933
+ end
934
+ end
935
+ end
936
+
937
+ describe 'PuppetX::NetDev::EosProviderMethods' do
938
+ let :fake_provider do
939
+ klass = Class.new do
940
+ include PuppetX::NetDev::EosProviderMethods
941
+ extend PuppetX::NetDev::EosProviderMethods
942
+ end
943
+ klass.new
944
+ end
945
+
946
+ describe '#api' do
947
+ subject { fake_provider.api }
948
+ it { is_expected.to be_a PuppetX::NetDev::EosApi }
949
+ end
950
+
951
+ describe '#bandwidth_to_speed' do
952
+ it 'converts 10_000_000 to 10m' do
953
+ expect(fake_provider.bandwidth_to_speed(10_000_000)).to eq '10m'
954
+ end
955
+ it 'converts 10_000_000_000 to 10g' do
956
+ expect(fake_provider.bandwidth_to_speed(10_000_000_000)).to eq '10g'
957
+ end
958
+ it 'converts 1_000_000_000 to 1g' do
959
+ expect(fake_provider.bandwidth_to_speed(1_000_000_000)).to eq '1g'
960
+ end
961
+ it 'converts 56_000_000_000 to 56g' do
962
+ expect(fake_provider.bandwidth_to_speed(56_000_000_000)).to eq '56g'
963
+ end
964
+ end
965
+
966
+ describe '#duplex_to_value' do
967
+ it 'converts "duplexFull" to :full' do
968
+ expect(fake_provider.duplex_to_value('duplexFull')).to eq :full
969
+ end
970
+ it 'converts "duplexHalf" to :half' do
971
+ expect(fake_provider.duplex_to_value('duplexHalf')).to eq :half
972
+ end
973
+ it 'raises ArgumentError with unknown input' do
974
+ expect do
975
+ fake_provider.duplex_to_value('garbage')
976
+ end.to raise_error ArgumentError
977
+ end
978
+ end
979
+
980
+ describe '#interface_status_to_enable' do
981
+ it 'converts "connected" to :true' do
982
+ expect(fake_provider.interface_status_to_enable('connected')).to eq :true
983
+ end
984
+ it 'converts "disabled" to :false' do
985
+ expect(fake_provider.interface_status_to_enable('disabled')).to eq :false
986
+ end
987
+ end
988
+
989
+ describe 'interface_attributes' do
990
+ let :attr_hash do
991
+ {
992
+ 'interfaceStatus' => 'connected',
993
+ 'bandwidth' => 10_000_000_000,
994
+ 'duplex' => 'duplexFull'
995
+ }
996
+ end
997
+ subject { fake_provider.interface_attributes(attr_hash) }
998
+ it { is_expected.to be_a_kind_of Hash }
999
+ end
1000
+ end