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,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