puppet_x_eos_eapi 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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