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,132 @@
1
+ #
2
+ # Copyright (c) 2014, Arista Networks, Inc.
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # Redistributions of source code must retain the above copyright notice,
10
+ # this list of conditions and the following disclaimer.
11
+ #
12
+ # Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # Neither the name of Arista Networks nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
24
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
30
+ # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ ##
34
+ # Eos is the toplevel namespace for working with Arista EOS nodes
35
+ module PuppetX
36
+ ##
37
+ # Eapi is module namesapce for working with the EOS command API
38
+ module Eos
39
+ ##
40
+ # The Vxlan provides an instance for managing vxlan virtual tunnel
41
+ # interfaces in EOS
42
+ #
43
+ class Vxlan
44
+ def initialize(api)
45
+ @api = api
46
+ end
47
+
48
+ ##
49
+ # Returns the vlan data for the provided id with the
50
+ # show vlan <id> command. If the id doesn't exist then
51
+ # nil is returned
52
+ #
53
+ #
54
+ # @return [nil, Hash<String, String|Hash|Array>] Hash describing the
55
+ # vlan configuration specified by id. If the id is not
56
+ # found then nil is returned
57
+ def get
58
+ @api.enable('show interfaces vxlan 1')
59
+ end
60
+
61
+ ##
62
+ # Creates a new logical vxlan virtual interface in the running-config
63
+ #
64
+ # @return [Boolean] returns true if the command completed successfully
65
+ def create
66
+ @api.config('interface vxlan 1') == [{}]
67
+ end
68
+
69
+ ##
70
+ # Deletes an existing vxlan logical interface from the running-config
71
+ #
72
+ # @return [Boolean] always returns true
73
+ def delete
74
+ @api.config('no interface vxlan 1') == [{}]
75
+ end
76
+
77
+ ##
78
+ # Defaults an existing vxlan logical interface from the running-config)
79
+ #
80
+ # @return [Boolean] returns true if the command completed successfully
81
+ def default
82
+ @api.config('default interface vxlan 1') == [{}]
83
+ end
84
+
85
+ ##
86
+ # Configures the source-interface parameter for the Vxlan interface
87
+ #
88
+ # @param [Hash] opts The configuration parameters for the VLAN
89
+ # @option opts [string] :value The value to set the name to
90
+ # @option opts [Boolean] :default The value should be set to default
91
+ #
92
+ # @return [Boolean] returns true if the command completed successfully
93
+ def set_source_interface(opts = {})
94
+ value = opts[:value]
95
+ default = opts[:default] || false
96
+
97
+ cmds = ['interface vxlan 1']
98
+ case default
99
+ when true
100
+ cmds << 'default vxlan source-interface'
101
+ when false
102
+ cmds << (value.nil? ? 'no vxlan source-interface' : \
103
+ "vxlan source-interface #{value}")
104
+ end
105
+ @api.config(cmds) == [{}, {}]
106
+ end
107
+
108
+ ##
109
+ # Configures the multicast-group parameter for the Vxlan interface
110
+ #
111
+ # @param [Hash] opts The configuration parameters for the VLAN
112
+ # @option opts [string] :value The value to set the name to
113
+ # @option opts [Boolean] :default The value should be set to default
114
+ #
115
+ # @return [Boolean] returns true if the command completed successfully
116
+ def set_multicast_group(opts = {})
117
+ value = opts[:value]
118
+ default = opts[:default] || false
119
+
120
+ cmds = ['interface vxlan 1']
121
+ case default
122
+ when true
123
+ cmds << 'default vxlan multicast-group'
124
+ when false
125
+ cmds << (value.nil? ? 'no vxlan multicast-group' : \
126
+ "vxlan multicast-group #{value}")
127
+ end
128
+ @api.config(cmds) == [{}, {}]
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,71 @@
1
+ #
2
+ # Copyright (c) 2014, Arista Networks, Inc.
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # Redistributions of source code must retain the above copyright notice,
10
+ # this list of conditions and the following disclaimer.
11
+ #
12
+ # Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # Neither the name of Arista Networks nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
24
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
30
+ # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+ require 'puppet_x/eos/eapi'
33
+ require 'pathname'
34
+
35
+ ##
36
+ # PuppetX namespace
37
+ module PuppetX
38
+ ##
39
+ # Eos namesapece
40
+ module Eos
41
+ ##
42
+ # EapiProviderMixin module
43
+ module EapiProviderMixin
44
+ def prefetch(resources)
45
+ provider_hash = instances.each_with_object({}) do |provider, hsh|
46
+ hsh[provider.name] = provider
47
+ end
48
+
49
+ resources.each_pair do |name, resource|
50
+ resource.provider = provider_hash[name] if provider_hash[name]
51
+ end
52
+ end
53
+
54
+ ##
55
+ # conf loads a YAML file from '/mnt/flash/eapi.conf' if it exists. If it
56
+ # does not exist an empty hash is returned.
57
+ def conf
58
+ config_file = Pathname.new('/mnt/flash/eapi.conf')
59
+ if config_file.exist?
60
+ YAML.load_file(config_file.to_s)
61
+ else
62
+ Hash.new
63
+ end
64
+ end
65
+
66
+ def eapi
67
+ @eapi ||= PuppetX::Eos::Eapi.new(conf)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ #
2
+ # Copyright (c) 2014, Arista Networks, Inc.
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # Redistributions of source code must retain the above copyright notice,
10
+ # this list of conditions and the following disclaimer.
11
+ #
12
+ # Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # Neither the name of Arista Networks nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS
24
+ # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
27
+ # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
29
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
30
+ # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
+ #
32
+
33
+ # #
34
+ # PuppetX namespace
35
+ module PuppetX
36
+ ##
37
+ # Arista EOS namespace
38
+ module Eos
39
+ VERSION = '0.2.0'
40
+ end
41
+ end
@@ -0,0 +1,1011 @@
1
+ # encoding: utf-8
2
+
3
+ require 'net_http_unix'
4
+ require 'puppet_x/net_dev/eos_api/common_methods'
5
+ require 'puppet_x/net_dev/eos_api/snmp_methods'
6
+ require 'securerandom'
7
+
8
+ ##
9
+ # PuppetX is where utility extensions live.
10
+ module PuppetX
11
+ ##
12
+ # NetDev is the module namespace for puppet supported
13
+ module NetDev
14
+ ## ApiError is raised on REST API errors
15
+ class ApiError < Exception; end
16
+
17
+ ##
18
+ # EosApi provides utility methods to interact with the eAPI using JSON-RPC.
19
+ # The API may be accessed over a normal TCP connection using the address
20
+ # and port settings, or a Unix domain socket using a `unix://...` address.
21
+ #
22
+ # @example Get all VLAN identifiers as strings
23
+ # >> api = EosApi.new(address: 'unix:///var/lib/eapi.sock')
24
+ # >> vlans = api.all_vlans
25
+ # >> vlans.keys
26
+ # => ['1', '3110']
27
+ class EosApi # rubocop:disable Metrics/ClassLength
28
+ # IP address or hostname of the REST api
29
+ attr_reader :address
30
+ # TCP port of the REST api
31
+ attr_reader :port
32
+ # API username
33
+ attr_reader :username
34
+ # API password
35
+ attr_reader :password
36
+
37
+ # Include type specific methods, broken out for clear organization.
38
+ include CommonMethods
39
+ include SnmpMethods
40
+
41
+ ##
42
+ # initialize an API instance. The API will communicate with the HTTP
43
+ # server over TCP or a Unix Domain Socket. If a unix domain socket is
44
+ # being used then the address parameter should be set to the socket path.
45
+ # The port, username, and password are not necessary.
46
+ #
47
+ # @option opts [String] :address The address to connect to the HTTP
48
+ # server. This can be a hostname, address or the full path to a unix
49
+ # domain socket in the form of `unix:///path/to/socket`.
50
+ #
51
+ # @option opts [Fixnum] :port The TCP port for an IP connection to the
52
+ # HTTP API server.
53
+ #
54
+ # @option opts [String] :username ('admin') The username to log into the
55
+ # API server when using TCP/IP HTTP API connections. This option is
56
+ # not necessary when using a unix:// socket connection.
57
+ #
58
+ # @option opts [String] :password The password to use to log into the API
59
+ # server.
60
+ #
61
+ # @return [PuppetX::NetDev::EosApi]
62
+ #
63
+ # rubocop:disable Metrics/CyclomaticComplexity
64
+ # rubocop:disable Metrics/PerceivedComplexity
65
+ def initialize(opts = {})
66
+ @address = opts[:address] || ENV['EOS_HOSTNAME'] || 'unix:///var/run/command-api.sock'
67
+ @port = opts[:port] || ENV['EOS_PORT'] || 80
68
+ @username = opts[:username] || ENV['EOS_USERNAME'] || 'admin'
69
+ @password = opts[:password] || ENV['EOS_PASSWORD'] || 'puppet'
70
+ end
71
+
72
+ ##
73
+ # vlan returns data about a specific VLAN identified by the VLAN ID
74
+ # number. This API call maps roughly to the `show vlan <id>` command.
75
+ # This method returns nil if no VLAN was found matching the ID provided.
76
+ #
77
+ # @param [Fixnum] id The VLAN id to obtain information about.
78
+ #
79
+ # @api public
80
+ #
81
+ # @return [nil,Hash<String,Hash>] Hash describing the VLAN attributes, or
82
+ # nil if no vlan was found matching the id provided. The format of
83
+ # this hash matches the format of {all_vlans}
84
+ def vlan(id)
85
+ result = eapi_action("show vlan #{id}", 'list vlans')
86
+ result.first['vlans'] if result
87
+ end
88
+
89
+ ##
90
+ # vlan_create creates a VLAN that does not yet exist on the target
91
+ # device.
92
+ #
93
+ # @param [Fixnum] id The VLAN id to create
94
+ #
95
+ # @api public
96
+ #
97
+ # @return [Boolean] true if the vlan was created
98
+ def vlan_create(id)
99
+ cmds = ['enable', 'configure', "vlan #{id}"]
100
+ eapi_action(cmds, "create vlan #{id}")
101
+ end
102
+
103
+ ##
104
+ # vlan_destroy destroys a vlan
105
+ #
106
+ # @param [Integer] id The VLAN ID to destroy
107
+ #
108
+ # @api public
109
+ def vlan_destroy(id)
110
+ cmds = ['enable', 'configure', "no vlan #{id}"]
111
+ eapi_action(cmds, "destroy vlan #{id}")
112
+ end
113
+
114
+ ##
115
+ # channel_group_destroy destroys a port channel group.
116
+ #
117
+ # @param [String] name The port channel name, e.g 'Port-Channel3'
118
+ #
119
+ # @api public
120
+ def channel_group_destroy(name)
121
+ # Need to remove all interfaces from the channel group.
122
+ port_channels = all_portchannels_detailed
123
+ channel_group = port_channels[name]
124
+ unless channel_group
125
+ msg = "#{name} is not in #{port_channels.keys.inspect}"
126
+ fail ArgumentError, msg
127
+ end
128
+ interfaces = channel_group['ports']
129
+ interfaces.each { |iface| interface_unset_channel_group(iface) }
130
+ end
131
+
132
+ ##
133
+ # interface_unset_channel_group removes a specific interface from all
134
+ # channel groups.
135
+ #
136
+ # @param [String] interface The interface name to remove from its
137
+ # associated channel group, e.g. 'Ethernet1'
138
+ #
139
+ # @api public
140
+ def interface_unset_channel_group(interface)
141
+ cmds = %w(enable configure) << "interface #{interface}"
142
+ cmds << 'no channel-group'
143
+ eapi_action(cmds, "remove #{interface} from channel group")
144
+ end
145
+
146
+ ##
147
+ # interface_set_channel_group configures an interface to be a member of a
148
+ # specified channel group ID.
149
+ #
150
+ # @param [String] interface The interface name to add to the channel
151
+ # group, e.g. 'Ethernet1'.
152
+ #
153
+ # @option opts [Fixnum] :group The group ID the interface will become a
154
+ # member of, e.g. 3.
155
+ #
156
+ # @option opts [Symbol] :mode (:active, :passive, :disabled) The LACP
157
+ # operating mode of the interface. Note, the only way to change the
158
+ # LACP mode is to delete the channel group and re-create the channel
159
+ # group.
160
+ #
161
+ # @api public
162
+ def interface_set_channel_group(interface, opts)
163
+ channel_group = opts[:group]
164
+ mode = case opts[:mode]
165
+ when :active, :passive then opts[:mode]
166
+ when :disabled then :on
167
+ else fail ArgumentError, "Unknown LACP mode #{opts[:mode]}"
168
+ end
169
+
170
+ cmd = %w(enable configure) << "interface #{interface}"
171
+ cmd << "channel-group #{channel_group} mode #{mode}"
172
+ msg = "join #{interface} to channel group #{channel_group}"
173
+ eapi_action(cmd, msg)
174
+ end
175
+
176
+ ##
177
+ # port_channel_destroy destroys a port channel interface and removes all
178
+ # interfaces from the channel group.
179
+ #
180
+ # @param [String] name The name of the port channel interface, e.g
181
+ # 'Port-Channel3'
182
+ #
183
+ # @api public
184
+ def port_channel_destroy(name)
185
+ cmds = %w(enable configure) << "no interface #{name}"
186
+ eapi_action(cmds, "remove #{name}")
187
+ end
188
+
189
+ ##
190
+ # channel_group_create creates a channel group and associated port
191
+ # channel interface if the interface does not already exist.
192
+ #
193
+ # @param [String] name The name of the port channel interface, e.g.
194
+ # 'Port-Channel3'.
195
+ #
196
+ # @option opts [Symbol] :mode (:active, :passive, :disabled) The LACP
197
+ # operating mode of the interface. Note, the only way to change the
198
+ # LACP mode is to delete the channel group and re-create the channel
199
+ # group.
200
+ #
201
+ # @option opts [Symbol] :interfaces (['Ethernet1', 'Ethernet2']) The
202
+ # member interfaces of the channel group.
203
+ #
204
+ # @api public
205
+ def channel_group_create(name, opts)
206
+ channel_group = name.scan(/\d+/).first.to_i
207
+ interfaces = [*opts[:interfaces]]
208
+ if interfaces.empty?
209
+ fail ArgumentError, 'Cannot create a channel group with no interfaces'
210
+ end
211
+ interfaces.each do |interface|
212
+ set_opts = { mode: opts[:mode], group: channel_group }
213
+ interface_set_channel_group(interface, set_opts)
214
+ end
215
+ end
216
+
217
+ ##
218
+ # set_vlan_name assigns a name to a vlan
219
+ #
220
+ # @param [Integer] id The vlan ID
221
+ #
222
+ # @param [String] name The vlan name
223
+ #
224
+ # @api public
225
+ def set_vlan_name(id, name)
226
+ cmds = ['enable', 'configure', "vlan #{id}"] << "name #{name}"
227
+ eapi_action(cmds, "set vlan #{id} name to #{name}")
228
+ end
229
+
230
+ ##
231
+ # set_vlan_state set a vlan to the state specified
232
+ #
233
+ # @param [Integer] id The vlan ID
234
+ #
235
+ # @param [String] state The state of the vlan, e.g. 'active' or
236
+ # 'suspend'
237
+ #
238
+ # @api public
239
+ def set_vlan_state(id, state)
240
+ cmds = ['enable', 'configure', "vlan #{id}"] << "state #{state}"
241
+ eapi_action(cmds, "set vlan #{id} state to #{state}")
242
+ end
243
+
244
+ ##
245
+ # all_vlans returns a hash of all vlans
246
+ #
247
+ # @example List all vlans
248
+ # api.all_vlans
249
+ # => {
250
+ # "1"=>{
251
+ # "status"=>"active",
252
+ # "name"=>"default",
253
+ # "interfaces"=>{
254
+ # "Ethernet2"=>{"privatePromoted"=>false},
255
+ # "Ethernet3"=>{"privatePromoted"=>false},
256
+ # "Ethernet1"=>{"privatePromoted"=>false},
257
+ # "Ethernet4"=>{"privatePromoted"=>false}},
258
+ # "dynamic"=>false},
259
+ # "3110"=>{
260
+ # "status"=>"active",
261
+ # "name"=>"VLAN3110",
262
+ # "interfaces"=>{},
263
+ # "dynamic"=>false}}
264
+ #
265
+ # @api public
266
+ #
267
+ # @return [Hash<String,Hash>]
268
+ def all_vlans
269
+ result = eapi_action('show vlan', 'list all vlans')
270
+ result.first['vlans']
271
+ end
272
+
273
+ ##
274
+ # all_portchannels returns a hash of all port channels based on multiple
275
+ # sources of data from the API.
276
+ #
277
+ # @api public
278
+ #
279
+ # @return [Hash<String,Hash>] where the key is the port channel name,
280
+ # e.g. 'Port-Channel10'
281
+ def all_portchannels
282
+ detailed = all_portchannels_detailed
283
+ modes = all_portchannel_modes
284
+ # Merge the two
285
+ detailed.each_with_object(Hash.new) do |(name, attr), hsh|
286
+ hsh[name] = modes[name] ? attr.merge(modes[name]) : attr
287
+ hsh[name]['minimum_links'] = portchannel_min_links(name)
288
+ end
289
+ end
290
+
291
+ ##
292
+ # portchannel_min_links takes the name of a Port Channel interface and
293
+ # obtains the currently configured min-links value by parsing the text of
294
+ # the running configuration.
295
+ #
296
+ # @api private
297
+ #
298
+ # @return [Fixnum] the minimum number of links for the channel group to
299
+ # become active.
300
+ def portchannel_min_links(name)
301
+ api_commands = ['enable', "show running-config interfaces #{name}"]
302
+ result = eapi_action(api_commands,
303
+ 'obtain port channel min links value',
304
+ format: 'text')
305
+ text = result[1]['output'] # skip over the enable command output.
306
+ parse_min_links(text)
307
+ end
308
+
309
+ ##
310
+ # set_portchannel_min_links Configures the minimum links value for a
311
+ # channel group.
312
+ #
313
+ # @param [String] name The port channel name, e.g 'Port-Channel4'.
314
+ #
315
+ # @param [Fixnum] min_links The minimum number of active links for the
316
+ # channel group to be active.
317
+ #
318
+ # @api public
319
+ def set_portchannel_min_links(name, min_links)
320
+ cmd = %w(enable configure)
321
+ cmd << "interface #{name}"
322
+ cmd << "port-channel min-links #{min_links}"
323
+ eapi_action(cmd, 'set port-channel min links')
324
+ end
325
+
326
+ ##
327
+ # parse_min_links takes the text from the `show running-config interfaces
328
+ # Port-ChannelX` API command and parses out the currently configured
329
+ # number of minimum links. If there is no min-links value we (safely)
330
+ # assume it is configured to 0. Example output is:
331
+ #
332
+ # interface Port-Channel4
333
+ # description Office Backbone
334
+ # port-channel min-links 2
335
+ #
336
+ # @param [String] text The raw text output from the API.
337
+ #
338
+ # @api private
339
+ #
340
+ # @return [Fixnum] the number of minimum links
341
+ def parse_min_links(text)
342
+ re = /min-links\s+(\d+)/m
343
+ mdata = re.match(text)
344
+ mdata ? mdata[1].to_i : 0
345
+ end
346
+
347
+ ##
348
+ # all_portchannels_detailed returns a hash of all port channels based on
349
+ # the `show etherchannel detailed` command.
350
+ #
351
+ # @api private
352
+ #
353
+ # @return [Hash<String,Hash>] where the key is the port channel name,
354
+ # e.g. 'Port-Channel10'
355
+ def all_portchannels_detailed
356
+ # JSON format is not supported in EOS 4.13.7M so use text format
357
+ result = eapi_action('show etherchannel detailed', 'list port channels',
358
+ format: 'text')
359
+ text = result.first['output']
360
+ parse_portchannels(text)
361
+ end
362
+
363
+ ##
364
+ # all_portchannel_modes returns a hash of each of the port channel LACP
365
+ # modes. This method could be merged with the data from the
366
+ # all_portchannels method.
367
+ #
368
+ # @api private
369
+ #
370
+ # @return [Hash<String,Hash>] where the key is the port channel name,
371
+ # e.g. 'Port-Channel10'
372
+ def all_portchannel_modes
373
+ # JSON format is not supported in EOS 4.13.7M so use text format
374
+ result = eapi_action('show port-channel summary', 'get lag modes',
375
+ format: 'text')
376
+ text = result.first['output']
377
+ parse_portchannel_modes(text)
378
+ end
379
+
380
+ ##
381
+ # Parse the portchannel modes from the text of the `show port-channel
382
+ # summary` command. The following is an example of two channel groups,
383
+ # one static, one active.
384
+ #
385
+ # rubocop:disable Metrics/LineLength, Metrics/MethodLength, Style/TrailingWhitespace
386
+ #
387
+ # Flags
388
+ # ------------------------ ---------------------------- -------------------------
389
+ # a - LACP Active p - LACP Passive * - static fallback
390
+ # F - Fallback enabled f - Fallback configured ^ - individual fallback
391
+ # U - In Use D - Down
392
+ # + - In-Sync - - Out-of-Sync i - incompatible with agg
393
+ # P - bundled in Po s - suspended G - Aggregable
394
+ # I - Individual S - ShortTimeout w - wait for agg
395
+ #
396
+ # Number of channels in use: 1
397
+ # Number of aggregators:1
398
+ #
399
+ # Port-Channel Protocol Ports
400
+ # ------------------ -------------- ----------------
401
+ # Po3(U) Static Et1(D) Et2(P)
402
+ # Po4(D) LACP(a) Et3(G-) Et4(G-)
403
+ #
404
+ # @api private
405
+ #
406
+ # @return [Hash<String,Hash>] where the key is the port channel name,
407
+ # e.g. 'Port-Channel10'
408
+ def parse_portchannel_modes(text)
409
+ lines = text.lines.each_with_object(Array.new) do |v, ary|
410
+ ary << v.chomp if /^\s*Po\d/.match(v)
411
+ end
412
+ lines.each_with_object(Hash.new) do |line, hsh|
413
+ mdata = /^\s+Po(\d+).*?\s+([a-zA-Z()0-9_-]+)/.match(line)
414
+ idx = mdata[1]
415
+ protocol = mdata[2]
416
+ mode = case protocol
417
+ when 'Static' then :disabled
418
+ when /LACP/
419
+ flags = /\((.*?)\)/.match(protocol)[1]
420
+ if flags.include? 'p' then :passive
421
+ elsif flags.include? 'a' then :active
422
+ end
423
+ end
424
+ hsh["Port-Channel#{idx}"] = { 'mode' => mode }
425
+ end
426
+ end
427
+
428
+ ##
429
+ # get_flowcontrol obtains the configured flow_control send and receive
430
+ # values from the target device.
431
+ #
432
+ # @param [String] name The interface name, e.g. 'Ethernet1'
433
+ #
434
+ # @api public
435
+ #
436
+ # @return [Hash<Symbol,String>] e.g. { send: 'on', receive: 'off' }
437
+ def get_flowcontrol(name)
438
+ cmd = "show flowcontrol interface #{name}"
439
+ result = eapi_action(cmd, 'get flowcontrol config', format: 'text')
440
+ text = result.first['output']
441
+ parse_flowcontrol_single(text)
442
+ end
443
+
444
+ ##
445
+ # parse_flowcontrol_single parses the text output of the `show
446
+ # flowcontrol <interface>` command where there is a single entry for the
447
+ # named interface.
448
+ #
449
+ # Port Send FlowControl Receive FlowControl RxPause TxPause
450
+ # admin oper admin oper
451
+ # ---------- -------- -------- -------- -------- ------------- -------------
452
+ # Et1 off unknown off unknown 0 0
453
+ #
454
+ # @param [String] text The text to parse
455
+ #
456
+ # @api private
457
+ #
458
+ # @return [Hash<Symbol,String>] e.g. { send: 'on', receive: 'off' }
459
+ def parse_flowcontrol_single(text)
460
+ re = /----\n(.*?)\s+(.*?)\s+.*?\s+(.*?)\s+.*\n/m
461
+ mdata = re.match(text)
462
+ if mdata
463
+ { send: mdata[2], receive: mdata[3] }
464
+ else
465
+ fail ArgumentError, 'could not parse flowcontrol'
466
+ end
467
+ end
468
+
469
+ ##
470
+ # set_flowcontrol_send Configures a specific interface's flow control
471
+ # send value.
472
+ #
473
+ # @param [String] name The name of the interface to configure, e.g.
474
+ # 'Ethernet1'
475
+ #
476
+ # @param [Symbol] value the value to configure, e.g. `:on`, `:off`
477
+ #
478
+ # @api public
479
+ def set_flowcontrol_send(name, value)
480
+ cmd = %w(enable configure) << "interface #{name}"
481
+ cmd << "flowcontrol send #{value}"
482
+ eapi_action(cmd, 'configure flowcontrol send')
483
+ end
484
+
485
+ ##
486
+ # set_flowcontrol_recv Configures a specific interface's flow control
487
+ # receive value.
488
+ #
489
+ # @param [String] name The name of the interface to configure, e.g.
490
+ # 'Ethernet1'
491
+ #
492
+ # @param [Symbol] value the value to configure, e.g. `:on`, `:off`
493
+ #
494
+ # @api public
495
+ def set_flowcontrol_recv(name, value)
496
+ cmd = %w(enable configure) << "interface #{name}"
497
+ cmd << "flowcontrol receive #{value}"
498
+ eapi_action(cmd, 'configure flowcontrol receive')
499
+ end
500
+
501
+ ##
502
+ # all_interfaces returns a hash of all interfaces
503
+ #
504
+ # @api public
505
+ #
506
+ # @return [Hash<String,Hash>] where the key is the interface name, e.g.
507
+ # 'Management1'
508
+ def all_interfaces
509
+ result = eapi_action('show interfaces', 'list all interfaces')
510
+ result.first['interfaces']
511
+ end
512
+
513
+ ##
514
+ # set_interface_state enables or disables a network interface
515
+ #
516
+ # @param [String] name The interface name, e.g. 'Ethernet1'
517
+ #
518
+ # @param [String] state The interface state, e.g. 'no shutdown' or
519
+ # 'shutdown'
520
+ #
521
+ # @api public
522
+ def set_interface_state(name, state)
523
+ cmd = %w(enable configure) << "interface #{name}" << state
524
+ eapi_action(cmd, "set interface #{name} state to #{state}")
525
+ end
526
+
527
+ ##
528
+ # set_interface_description configures the description string for an
529
+ # interface.
530
+ #
531
+ # @param [String] name The interface name, e.g. 'Ethernet1'
532
+ #
533
+ # @param [String] description The description to assign the interface.
534
+ #
535
+ # @api public
536
+ def set_interface_description(name, description)
537
+ cmd = %w(enable configure) << "interface #{name}"
538
+ cmd << "description #{description}"
539
+ eapi_action(cmd, "set interface #{name} description to #{description}")
540
+ end
541
+
542
+ ##
543
+ # set_interface_speed enable a network interface
544
+ #
545
+ # @param [String] name The interface name, e.g. 'Ethernet1'
546
+ #
547
+ # @param [String] speed The interface state, e.g. '1000full' or
548
+ # '40gfull'
549
+ #
550
+ # @api public
551
+ def set_interface_speed(name, speed)
552
+ cmd = %w(enable configure) << "interface #{name}"
553
+ cmd << "speed forced #{speed}"
554
+ eapi_action(cmd, "set interface #{name} speed to #{speed}")
555
+ end
556
+
557
+ ##
558
+ # set_interface_mtu configures the interface MTU
559
+ #
560
+ # @param [String] name The interface name, e.g. 'Ethernet1'
561
+ #
562
+ # @param [Fixnum] mtu The interface mtu, e.g. 9000
563
+ #
564
+ # @api public
565
+ def set_interface_mtu(name, mtu)
566
+ cmd = %w(enable configure) << "interface #{name}"
567
+ cmd << "mtu #{mtu}"
568
+ eapi_action(cmd, "set interface #{name} mtu to #{mtu}")
569
+ end
570
+
571
+ ##
572
+ # format_error takes the value of the 'error' key from the EOS API
573
+ # response and formats the error strings into a string suitable for error
574
+ # messages.
575
+ #
576
+ # @param [Array<Hash>] data Array of data from the API response, this
577
+ # will be lcoated in the sub-key api_response['error']['data']
578
+ #
579
+ # @api private
580
+ #
581
+ # @return [String] the human readable error message
582
+ def format_error(data)
583
+ if data
584
+ data.each_with_object([]) do |i, ary|
585
+ ary.push(*i['errors']) if i['errors']
586
+ end.join(', ')
587
+ else
588
+ 'unknown error'
589
+ end
590
+ end
591
+
592
+ ##
593
+ # http returns a memoized HTTP client instance conforming to the
594
+ # Net::HTTP interface.
595
+ #
596
+ # @api public
597
+ #
598
+ # @return [NetX::HttpUnix]
599
+ def http
600
+ @http ||= NetX::HTTPUnix.new(address, port)
601
+ end
602
+
603
+ ##
604
+ # format_command takes an EOS command as a string and returns the
605
+ # appropriate data structure for use with the EOS REST API.
606
+ #
607
+ # @param [String, Array<String>] command The command to execute on the
608
+ # switch, e.g. 'show vlan' or ['show vlan 1', 'show vlan 2'].
609
+ #
610
+ # @option opts [String] :id The identifier for this request. If omitted,
611
+ # a unique identifier will be generated.
612
+ #
613
+ # @option opts [String] :format ('json') The desired format of the
614
+ # response, e.g. 'text' or 'json'. Defaults to 'json' if not provided.
615
+ #
616
+ # @api private
617
+ #
618
+ # @return [String] The JSON string suitable for use with HTTP POST API
619
+ # calls to the EOS API.
620
+ def format_command(command, options = {})
621
+ cmds = [*command]
622
+ req_id = options[:id].nil? ? SecureRandom.uuid : options[:id]
623
+ format = options[:format].nil? ? 'json' : options[:format]
624
+ params = { 'version' => 1, 'cmds' => cmds, 'format' => format }
625
+ request = {
626
+ 'jsonrpc' => '2.0', 'method' => 'runCmds',
627
+ 'params' => params, 'id' => req_id
628
+ }
629
+ JSON.dump(request)
630
+ end
631
+ private :format_command
632
+
633
+ ##
634
+ # parse_portchannels accepts the text output of the `show etherchannel
635
+ # detailed` command and parses the text into structured data with the
636
+ # portchannel names as keys and portchannel attributes as key/values in a
637
+ # hash.
638
+ #
639
+ # @param [String] text The text output to parse.
640
+ #
641
+ # @api private
642
+ #
643
+ # @return [Hash<String,Hash>] where the key is the port channel name,
644
+ # e.g. 'Port-Channel10'
645
+ def parse_portchannels(text) # rubocop:disable Metrics/MethodLength
646
+ groups = text.split('Port Channel ')
647
+ groups.each_with_object({}) do |str, group|
648
+ lines = [*str.lines]
649
+ name = parse_portchannel_name(lines.shift)
650
+ next unless name
651
+ active_ports = parse_portchannel_active_ports(lines)
652
+ configured_ports = parse_portchannel_configured_ports(lines)
653
+ group[name] = {
654
+ 'name' => name,
655
+ 'ports' => [*active_ports, *configured_ports].sort
656
+ }
657
+ end
658
+ end
659
+ private :parse_portchannels
660
+
661
+ ##
662
+ # parse_portchannel_active_ports takes a portchannel section from `show
663
+ # port-channel detailed` and parses all of the active ports from the
664
+ # section.
665
+ #
666
+ # @param [Array<String>] lines Array of string lines for the section,
667
+ #
668
+ # @api private
669
+ #
670
+ # @return [Array<String>] Array of string port names, e.g. ['Ethernet1',
671
+ # 'Ethernet2']
672
+ def parse_portchannel_active_ports(group_lines)
673
+ lines = group_lines.dup
674
+ # Check if there are no active ports
675
+ mdata = /(No)? Active Ports/.match(lines.shift)
676
+ return [] if mdata[1] # return if there are none
677
+ lines.shift until /^\s*Port /.match(lines.first) || lines.empty?
678
+ lines.shift(2) # heading line and ---- line
679
+ # Read interfaces until the first blank line
680
+ lines.each_with_object([]) do |l, a|
681
+ l.chomp!
682
+ break a if l.empty?
683
+ a << l.split.first
684
+ end
685
+ end
686
+
687
+ ##
688
+ # parse_portchannel_configured_ports takes a portchannel section from
689
+ # `show port-channel detailed` and parses all of the active ports from
690
+ # the section.
691
+ #
692
+ # @param [Array<String>] lines Array of string lines for the section,
693
+ #
694
+ # @api private
695
+ #
696
+ # @return [Array<String>] Array of string port names, e.g. ['Ethernet1',
697
+ # 'Ethernet2']
698
+ def parse_portchannel_configured_ports(group_lines)
699
+ lines = group_lines.dup
700
+ # Check if there are no active ports
701
+ lines.shift until /inactive ports/.match(lines.first) || lines.empty?
702
+ return [] if lines.empty?
703
+ lines.shift(3)
704
+ lines.each_with_object([]) do |l, a|
705
+ l.chomp!
706
+ break a if l.empty?
707
+ a << l.split.first
708
+ end
709
+ end
710
+
711
+ ##
712
+ # parse_portchannel_name parses out the portchannel name from the first
713
+ # line of a group section.
714
+ #
715
+ # @param [String] line The first line of a portchannel group detailed
716
+ # show statement, e.g. 'Port Channel Port-Channel1 (Fallback State:
717
+ # Unconfigured):'
718
+ #
719
+ # @api private
720
+ #
721
+ # @return [String,nil] the name of the portchannel group or nil if the
722
+ # name could not be parsed.
723
+ def parse_portchannel_name(line)
724
+ mdata = /^(?:Port Channel )?(Port-Channel\d+)/.match(line)
725
+ mdata[1] if mdata
726
+ end
727
+ private :parse_portchannel_name
728
+
729
+ ##
730
+ # eapi_request returns a Net::HTTP::Post instance suitable for use with
731
+ # the http client to make an API call to EOS. The request will
732
+ # automatically be initialized with an username and password if the
733
+ # attributes have been initialized.
734
+ #
735
+ # @param [String] request_body The data to post to the API represented as
736
+ # a string, usually JSON encoded.
737
+ #
738
+ # @api private
739
+ #
740
+ # @return [Net::HTTP::Post] A request instance suitable for use with
741
+ # Net::HTTP#request
742
+ def eapi_request(request_body)
743
+ # JSON-RPC 2.0 to /command-api/ location
744
+ req = Net::HTTP::Post.new('/command-api/')
745
+ req.basic_auth(username, password) if username && password
746
+ req.body = request_body
747
+ req
748
+ end
749
+ private :eapi_request
750
+
751
+ ##
752
+ # eapi_call takes a string as an arista command and executes the command
753
+ # on the switch using the eAPI. This method decodes the API response and
754
+ # returns the value. For example:
755
+ #
756
+ # [1] pry(#<PuppetX::NetDev::EosApi>)> eapi_call('show version')
757
+ # => {"jsonrpc"=>"2.0",
758
+ # "result"=>
759
+ # [{"modelName"=>"vEOS",
760
+ # "internalVersion"=>"4.13.7M-1877079.4137M.1",
761
+ # "systemMacAddress"=>"00:42:00:08:17:78",
762
+ # "serialNumber"=>"",
763
+ # "memTotal"=>2033744,
764
+ # "bootupTimestamp"=>1403732020.05,
765
+ # "memFree"=>143688,
766
+ # "version"=>"4.13.7M",
767
+ # "architecture"=>"i386",
768
+ # "internalBuildId"=>"54a9c4ce-bbb0-4f6b-9448-9507de824905",
769
+ # "hardwareRevision"=>""}],
770
+ # "id"=>"a4e14732-e0f2-430d-823e-1c801273ec60"}
771
+ #
772
+ # @param [String,Array<String>] command The command or commands to
773
+ # execute, e.g. 'show vlan'
774
+ #
775
+ # @option opts [String] :id The identifier for this request. If omitted,
776
+ # a unique identifier will be generated.
777
+ #
778
+ # @option opts [String] :format ('json') The desired format of the
779
+ # response, e.g. 'text' or 'json'. Defaults to 'json' if not provided.
780
+ #
781
+ # @api private
782
+ #
783
+ # @return [Hash] the response from the API
784
+ def eapi_call(command, options = {})
785
+ cmds = [*command]
786
+ request_body = format_command(cmds, options)
787
+ req = eapi_request(request_body)
788
+ resp = http.request(req)
789
+ decoded_response = JSON.parse(resp.body)
790
+ decoded_response
791
+ end
792
+ private :eapi_call
793
+
794
+ ##
795
+ # eapi_action makes an API call and handles any error messages in the
796
+ # return value.
797
+ #
798
+ # @param [String,Array<String>] command The command or commands to
799
+ # execute, e.g. 'show vlan'
800
+ #
801
+ # @param [String] action The action being performed, e.g. 'set interface
802
+ # description'. Used to format error messages on API errors.
803
+ #
804
+ # @option opts [String] :id The identifier for this request. If omitted,
805
+ # a unique identifier will be generated.
806
+ #
807
+ # @option opts [String] :format ('json') The desired format of the
808
+ # response, e.g. 'text' or 'json'. Defaults to 'json' if not provided.
809
+ #
810
+ # @api private
811
+ #
812
+ # @return [Array<Hash>] the value of the 'result' key from the API
813
+ # response.
814
+ def eapi_action(command, action = 'make api call', options = {})
815
+ api_response = eapi_call(command, options)
816
+
817
+ return api_response['result'] unless api_response['error']
818
+ err_msg = format_error(api_response['error']['data'])
819
+ fail ApiError, "could not #{action}: #{err_msg}"
820
+ end
821
+ private :eapi_action
822
+
823
+ ##
824
+ # @return [URI] the URI of the server
825
+ def uri
826
+ return @uri if @uri
827
+ if username && password
828
+ @uri = URI("http://#{username}:#{password}@#{address}:#{port}")
829
+ else
830
+ @uri = URI("http://#{address}:#{port}")
831
+ end
832
+ end
833
+ end
834
+
835
+ ##
836
+ # EosProviderMethods is meant to be mixed into the provider to make api
837
+ # methods available.
838
+ module EosProviderMethods
839
+ ##
840
+ # api returns a memoized instance of the EosApi. This method is intended
841
+ # to be used from providers that have mixed in the EosProviderMethods
842
+ # module.
843
+ #
844
+ # @return [PuppetX::NetDev::EosApi] api instance
845
+ def api
846
+ @api ||= EosApi.new
847
+ end
848
+
849
+ ##
850
+ # bandwidth_to_speed converts a raw bandwidth integer to a Link speed
851
+ # [10m|100m|1g|10g|40g|56g|100g]
852
+ #
853
+ # @param [Fixnum] bandwidth The bandwdith value in bytes per second
854
+ #
855
+ # @api public
856
+ #
857
+ # @return [String] Link speed [10m|100m|1g|10g|40g|56g|100g]
858
+ def bandwidth_to_speed(bandwidth)
859
+ if bandwidth >= 1_000_000_000
860
+ "#{(bandwidth / 1_000_000_000).to_i}g"
861
+ else
862
+ "#{(bandwidth / 1_000_000).to_i}m"
863
+ end
864
+ end
865
+
866
+ ##
867
+ # duplex_to_value Convert a duplex string from the API response to the
868
+ # provider value
869
+ #
870
+ # @param [String] duplex The value from the API response
871
+ #
872
+ # @api public
873
+ #
874
+ # @return [Symbol] the value for the provider
875
+ def duplex_to_value(duplex)
876
+ case duplex
877
+ when 'duplexFull' then :full
878
+ when 'duplexHalf' then :half
879
+ else fail ArgumentError, "Unknown duplex value #{duplex.inspect}"
880
+ end
881
+ end
882
+
883
+ ##
884
+ # interface_status_to_enable maps the interfaceStatus attribute of the
885
+ # API response to the enable state of :true or :false
886
+ #
887
+ # The interfaceStatus reflects realtime status so its a bit funny how it
888
+ # works. If interfaceStatus == 'disabled' then the interface is
889
+ # administratively disabled (ie configured to be disabled) otherwise its
890
+ # enabled (ie no shutdown). So in your conversion here you can just
891
+ # reflect if interfaceStatus == 'disabled' or not as the state.
892
+ #
893
+ # @param [String] status the value of interfaceStatus returned by the API
894
+ #
895
+ # @return [Symbol] :true or :false
896
+ def interface_status_to_enable(status)
897
+ status == 'disabled' ? :false : :true
898
+ end
899
+
900
+ ##
901
+ # interface_attributes takes an attribute hash from the EOS API and maps
902
+ # the values to provider attributes for the network_interface type.
903
+ #
904
+ # @param [Hash] attr_hash Interface attribute hash
905
+ #
906
+ # @api public
907
+ #
908
+ # @return [Hash] provider attributes suitable for merge into a provider
909
+ # hash that will be passed to the provider initializer.
910
+ def interface_attributes(attr_hash)
911
+ hsh = {}
912
+ status = attr_hash['interfaceStatus']
913
+ hsh[:enable] = interface_status_to_enable(status)
914
+ hsh[:mtu] = attr_hash['mtu']
915
+ hsh[:speed] = bandwidth_to_speed(attr_hash['bandwidth'])
916
+ hsh[:duplex] = duplex_to_value(attr_hash['duplex'])
917
+ hsh[:description] = attr_hash['description']
918
+ hsh
919
+ end
920
+
921
+ ##
922
+ # port_channel_attributes takes an attribute hash from the EOS API and
923
+ # maps the values to provider attributes for the port_channel type.
924
+ #
925
+ # @param [Hash] attr_hash Interface attribute hash
926
+ #
927
+ # @api public
928
+ #
929
+ # @return [Hash] provider attributes suitable for merge into a provider
930
+ # hash that will be passed to the provider initializer.
931
+ def port_channel_attributes(attr_hash)
932
+ hsh = {}
933
+ hsh[:speed] = bandwidth_to_speed(attr_hash['bandwidth'])
934
+ hsh[:description] = attr_hash['description']
935
+ hsh
936
+ end
937
+
938
+ ##
939
+ # flush_speed_and_duplex consolidates the duplex and speed settings into one
940
+ # API call to manage the interface speed.
941
+ #
942
+ # @param [String] name The name of the interface, e.g. 'Ethernet1'
943
+ def flush_speed_and_duplex(name)
944
+ speed = convert_speed(@property_flush[:speed])
945
+ duplex = @property_flush[:duplex]
946
+ return nil unless speed || duplex
947
+
948
+ speed_out = speed ? speed : convert_speed(@property_hash[:speed])
949
+ duplex_out = duplex ? duplex.downcase : @property_hash[:duplex].to_s
950
+
951
+ api.set_interface_speed(name, "#{speed_out}#{duplex_out}")
952
+ end
953
+
954
+ ##
955
+ # convert_speed takes a speed value from the catalog as a string and converts
956
+ # it to a speed prefix suitable for the Arista API. The following table is
957
+ # used to perform the conversion.
958
+ #
959
+ # 10000full Disable autoneg and force 10 Gbps/full duplex operation
960
+ # 1000full Disable autoneg and force 1 Gbps/full duplex operation
961
+ # 1000half Disable autoneg and force 1 Gbps/half duplex operation
962
+ # 100full Disable autoneg and force 100 Mbps/full duplex operation
963
+ # 100gfull Disable autoneg and force 100 Gbps/full duplex operation
964
+ # 100half Disable autoneg and force 100 Mbps/half duplex operation
965
+ # 10full Disable autoneg and force 10 Mbps/full duplex operation
966
+ # 10half Disable autoneg and force 10 Mbps/half duplex operation
967
+ # 40gfull Disable autoneg and force 40 Gbps/full duplex operation
968
+ #
969
+ # @param [String] speed The speed specified in the catalog, e.g. 1g
970
+ #
971
+ # @api private
972
+ #
973
+ # @return [String] The speed for the API, e.g. 1000
974
+ def convert_speed(value)
975
+ speed = value.to_s
976
+ if /g$/i.match(speed) && (speed.to_i > 40)
977
+ speed
978
+ elsif /g$/i.match(speed)
979
+ (speed.to_i * 1000).to_s
980
+ elsif /m$/i.match(speed)
981
+ speed.to_i.to_s
982
+ end
983
+ end
984
+ end
985
+
986
+ ##
987
+ # EosProviderClassMethods implements common methods, e.g. `self.prefetch`
988
+ # for EOS providers.
989
+ module EosProviderClassMethods
990
+ ##
991
+ # prefetch associates resources declared in the Puppet catalog with
992
+ # resources discovered on the system using the instances class method.
993
+ # Each resource that has a matching provider in the instances list will
994
+ # have the provider bound to the resource.
995
+ #
996
+ # @param [Hash] resources The set of resources declared in the catalog.
997
+ #
998
+ # @return [Hash<String,Puppet::Type>] catalog resources with updated
999
+ # provider instances.
1000
+ def prefetch(resources)
1001
+ provider_hash = instances.each_with_object({}) do |provider, hsh|
1002
+ hsh[provider.name] = provider
1003
+ end
1004
+
1005
+ resources.each_pair do |name, resource|
1006
+ resource.provider = provider_hash[name] if provider_hash[name]
1007
+ end
1008
+ end
1009
+ end
1010
+ end
1011
+ end