class PuppetX::NetDev::EosApi

EosApi provides utility methods to interact with the eAPI using JSON-RPC. The API may be accessed over a normal TCP connection using the address and port settings, or a Unix domain socket using a ‘unix://…` address.

@example Get all VLAN identifiers as strings

>> api = EosApi.new(address: 'unix:///var/lib/eapi.sock')
>> vlans = api.all_vlans
>> vlans.keys
=> ['1', '3110']

Eapi class

Constants

VERSION

Attributes

address[R]

IP address or hostname of the REST api

password[R]

API password

port[R]

TCP port of the REST api

username[R]

API username

Public Class Methods

new(opts = {}) click to toggle source

initialize an API instance. The API will communicate with the HTTP server over TCP or a Unix Domain Socket. If a unix domain socket is being used then the address parameter should be set to the socket path. The port, username, and password are not necessary.

@option opts [String] :address The address to connect to the HTTP

server.  This can be a hostname, address or the full path to a unix
domain socket in the form of `unix:///path/to/socket`.

@option opts [Fixnum] :port The TCP port for an IP connection to the

HTTP API server.

@option opts [String] :username (‘admin’) The username to log into the

API server when using TCP/IP HTTP API connections.  This option is
not necessary when using a unix:// socket connection.

@option opts [String] :password The password to use to log into the API

server.

@return [PuppetX::NetDev::EosApi]

rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/PerceivedComplexity

# File lib/puppet_x/net_dev/eos_api.rb, line 65
def initialize(opts = {})
  @address = opts[:address] || ENV['EOS_HOSTNAME'] || 'unix:///var/run/command-api.sock'
  @port = opts[:port] || ENV['EOS_PORT'] || 80
  @username = opts[:username] || ENV['EOS_USERNAME'] || 'admin'
  @password = opts[:password] || ENV['EOS_PASSWORD'] || 'puppet'
end

Public Instance Methods

all_interfaces() click to toggle source

all_interfaces returns a hash of all interfaces

@api public

@return [Hash<String,Hash>] where the key is the interface name, e.g.

'Management1'
# File lib/puppet_x/net_dev/eos_api.rb, line 508
def all_interfaces
  result = eapi_action('show interfaces', 'list all interfaces')
  result.first['interfaces']
end
all_portchannel_modes() click to toggle source

all_portchannel_modes returns a hash of each of the port channel LACP modes. This method could be merged with the data from the all_portchannels method.

@api private

@return [Hash<String,Hash>] where the key is the port channel name,

e.g. 'Port-Channel10'
# File lib/puppet_x/net_dev/eos_api.rb, line 372
def all_portchannel_modes
  # JSON format is not supported in EOS 4.13.7M so use text format
  result = eapi_action('show port-channel summary', 'get lag modes',
                       format: 'text')
  text = result.first['output']
  parse_portchannel_modes(text)
end
all_portchannels() click to toggle source

all_portchannels returns a hash of all port channels based on multiple sources of data from the API.

@api public

@return [Hash<String,Hash>] where the key is the port channel name,

e.g. 'Port-Channel10'
# File lib/puppet_x/net_dev/eos_api.rb, line 281
def all_portchannels
  detailed = all_portchannels_detailed
  modes = all_portchannel_modes
  # Merge the two
  detailed.each_with_object(Hash.new) do |(name, attr), hsh|
    hsh[name] = modes[name] ? attr.merge(modes[name]) : attr
    hsh[name]['minimum_links'] = portchannel_min_links(name)
  end
end
all_portchannels_detailed() click to toggle source

all_portchannels_detailed returns a hash of all port channels based on the ‘show etherchannel detailed` command.

@api private

@return [Hash<String,Hash>] where the key is the port channel name,

e.g. 'Port-Channel10'
# File lib/puppet_x/net_dev/eos_api.rb, line 355
def all_portchannels_detailed
  # JSON format is not supported in EOS 4.13.7M so use text format
  result = eapi_action('show etherchannel detailed', 'list port channels',
                       format: 'text')
  text = result.first['output']
  parse_portchannels(text)
end
all_vlans() click to toggle source

all_vlans returns a hash of all vlans

@example List all vlans

api.all_vlans
=> {
"1"=>{
  "status"=>"active",
  "name"=>"default",
  "interfaces"=>{
    "Ethernet2"=>{"privatePromoted"=>false},
    "Ethernet3"=>{"privatePromoted"=>false},
    "Ethernet1"=>{"privatePromoted"=>false},
    "Ethernet4"=>{"privatePromoted"=>false}},
  "dynamic"=>false},
"3110"=>{
  "status"=>"active",
  "name"=>"VLAN3110",
  "interfaces"=>{},
  "dynamic"=>false}}

@api public

@return [Hash<String,Hash>]

# File lib/puppet_x/net_dev/eos_api.rb, line 268
def all_vlans
  result = eapi_action('show vlan', 'list all vlans')
  result.first['vlans']
end
channel_group_create(name, opts) click to toggle source

channel_group_create creates a channel group and associated port channel interface if the interface does not already exist.

@param [String] name The name of the port channel interface, e.g.

'Port-Channel3'.

@option opts [Symbol] :mode (:active, :passive, :disabled) The LACP

operating mode of the interface.  Note, the only way to change the
LACP mode is to delete the channel group and re-create the channel
group.

@option opts [Symbol] :interfaces ([‘Ethernet1’, ‘Ethernet2’]) The

member interfaces of the channel group.

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 205
def channel_group_create(name, opts)
  channel_group = name.scan(/\d+/).first.to_i
  interfaces = [*opts[:interfaces]]
  if interfaces.empty?
    fail ArgumentError, 'Cannot create a channel group with no interfaces'
  end
  interfaces.each do |interface|
    set_opts = { mode: opts[:mode], group: channel_group }
    interface_set_channel_group(interface, set_opts)
  end
end
channel_group_destroy(name) click to toggle source

channel_group_destroy destroys a port channel group.

@param [String] name The port channel name, e.g ‘Port-Channel3’

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 120
def channel_group_destroy(name)
  # Need to remove all interfaces from the channel group.
  port_channels = all_portchannels_detailed
  channel_group = port_channels[name]
  unless channel_group
    msg = "#{name} is not in #{port_channels.keys.inspect}"
    fail ArgumentError, msg
  end
  interfaces = channel_group['ports']
  interfaces.each { |iface| interface_unset_channel_group(iface) }
end
format_error(data) click to toggle source

format_error takes the value of the ‘error’ key from the EOS API response and formats the error strings into a string suitable for error messages.

@param [Array<Hash>] data Array of data from the API response, this

will be lcoated in the sub-key api_response['error']['data']

@api private

@return [String] the human readable error message

# File lib/puppet_x/net_dev/eos_api.rb, line 582
def format_error(data)
  if data
    data.each_with_object([]) do |i, ary|
      ary.push(*i['errors']) if i['errors']
    end.join(', ')
  else
    'unknown error'
  end
end
get_flowcontrol(name) click to toggle source

get_flowcontrol obtains the configured flow_control send and receive values from the target device.

@param [String] name The interface name, e.g. ‘Ethernet1’

@api public

@return [Hash<Symbol,String>] e.g. { send: ‘on’, receive: ‘off’ }

# File lib/puppet_x/net_dev/eos_api.rb, line 437
def get_flowcontrol(name)
  cmd = "show flowcontrol interface #{name}"
  result = eapi_action(cmd, 'get flowcontrol config', format: 'text')
  text = result.first['output']
  parse_flowcontrol_single(text)
end
http() click to toggle source

http returns a memoized HTTP client instance conforming to the Net::HTTP interface.

@api public

@return [NetX::HttpUnix]

# File lib/puppet_x/net_dev/eos_api.rb, line 599
def http
  @http ||= NetX::HTTPUnix.new(address, port)
end
interface_set_channel_group(interface, opts) click to toggle source

interface_set_channel_group configures an interface to be a member of a specified channel group ID.

@param [String] interface The interface name to add to the channel

group, e.g. 'Ethernet1'.

@option opts [Fixnum] :group The group ID the interface will become a

member of, e.g. 3.

@option opts [Symbol] :mode (:active, :passive, :disabled) The LACP

operating mode of the interface.  Note, the only way to change the
LACP mode is to delete the channel group and re-create the channel
group.

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 162
def interface_set_channel_group(interface, opts)
  channel_group = opts[:group]
  mode = case opts[:mode]
         when :active, :passive then opts[:mode]
         when :disabled then :on
         else fail ArgumentError, "Unknown LACP mode #{opts[:mode]}"
         end

  cmd = %w(enable configure) << "interface #{interface}"
  cmd << "channel-group #{channel_group} mode #{mode}"
  msg = "join #{interface} to channel group #{channel_group}"
  eapi_action(cmd, msg)
end
interface_unset_channel_group(interface) click to toggle source

interface_unset_channel_group removes a specific interface from all channel groups.

@param [String] interface The interface name to remove from its

associated channel group, e.g. 'Ethernet1'

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 140
def interface_unset_channel_group(interface)
  cmds = %w(enable configure) << "interface #{interface}"
  cmds << 'no channel-group'
  eapi_action(cmds, "remove #{interface} from channel group")
end
parse_flowcontrol_single(text) click to toggle source

parse_flowcontrol_single parses the text output of the ‘show flowcontrol <interface>` command where there is a single entry for the named interface.

Port        Send FlowControl  Receive FlowControl  RxPause       TxPause
            admin    oper     admin    oper
----------  -------- -------- -------- --------    ------------- -------------
Et1         off      unknown  off      unknown     0             0

@param [String] text The text to parse

@api private

@return [Hash<Symbol,String>] e.g. { send: ‘on’, receive: ‘off’ }

# File lib/puppet_x/net_dev/eos_api.rb, line 459
def parse_flowcontrol_single(text)
  re = /----\n(.*?)\s+(.*?)\s+.*?\s+(.*?)\s+.*\n/m
  mdata = re.match(text)
  if mdata
    { send: mdata[2], receive: mdata[3] }
  else
    fail ArgumentError, 'could not parse flowcontrol'
  end
end
parse_portchannel_active_ports(group_lines) click to toggle source

parse_portchannel_active_ports takes a portchannel section from ‘show port-channel detailed` and parses all of the active ports from the section.

@param [Array<String>] lines Array of string lines for the section,

@api private

@return [Array<String>] Array of string port names, e.g. [‘Ethernet1’,

'Ethernet2']
# File lib/puppet_x/net_dev/eos_api.rb, line 672
def parse_portchannel_active_ports(group_lines)
  lines = group_lines.dup
  # Check if there are no active ports
  mdata = /(No)? Active Ports/.match(lines.shift)
  return [] if mdata[1] # return if there are none
  lines.shift until /^\s*Port /.match(lines.first) || lines.empty?
  lines.shift(2) # heading line and ---- line
  # Read interfaces until the first blank line
  lines.each_with_object([]) do |l, a|
    l.chomp!
    break a if l.empty?
    a << l.split.first
  end
end
parse_portchannel_configured_ports(group_lines) click to toggle source

parse_portchannel_configured_ports takes a portchannel section from ‘show port-channel detailed` and parses all of the active ports from the section.

@param [Array<String>] lines Array of string lines for the section,

@api private

@return [Array<String>] Array of string port names, e.g. [‘Ethernet1’,

'Ethernet2']
# File lib/puppet_x/net_dev/eos_api.rb, line 698
def parse_portchannel_configured_ports(group_lines)
  lines = group_lines.dup
  # Check if there are no active ports
  lines.shift until /inactive ports/.match(lines.first) || lines.empty?
  return [] if lines.empty?
  lines.shift(3)
  lines.each_with_object([]) do |l, a|
    l.chomp!
    break a if l.empty?
    a << l.split.first
  end
end
parse_portchannel_modes(text) click to toggle source

Parse the portchannel modes from the text of the ‘show port-channel summary` command. The following is an example of two channel groups, one static, one active.

rubocop:disable Metrics/LineLength, Metrics/MethodLength, Style/TrailingWhitespace

                 Flags
------------------------ ---------------------------- -------------------------
  a - LACP Active          p - LACP Passive           * - static fallback
  F - Fallback enabled     f - Fallback configured    ^ - individual fallback
  U - In Use               D - Down
  + - In-Sync              - - Out-of-Sync            i - incompatible with agg
  P - bundled in Po        s - suspended              G - Aggregable
  I - Individual           S - ShortTimeout           w - wait for agg

Number of channels in use: 1
Number of aggregators:1

   Port-Channel       Protocol    Ports
------------------ -------------- ----------------
   Po3(U)             Static       Et1(D) Et2(P)
   Po4(D)             LACP(a)      Et3(G-) Et4(G-)

@api private

@return [Hash<String,Hash>] where the key is the port channel name,

e.g. 'Port-Channel10'
# File lib/puppet_x/net_dev/eos_api.rb, line 408
def parse_portchannel_modes(text)
  lines = text.lines.each_with_object(Array.new) do |v, ary|
    ary << v.chomp if /^\s*Po\d/.match(v)
  end
  lines.each_with_object(Hash.new) do |line, hsh|
    mdata = /^\s+Po(\d+).*?\s+([a-zA-Z()0-9_-]+)/.match(line)
    idx = mdata[1]
    protocol = mdata[2]
    mode = case protocol
           when 'Static' then :disabled
           when /LACP/
             flags = /\((.*?)\)/.match(protocol)[1]
             if flags.include? 'p' then :passive
             elsif flags.include? 'a' then :active
             end
           end
    hsh["Port-Channel#{idx}"] = { 'mode' => mode }
  end
end
port_channel_destroy(name) click to toggle source

port_channel_destroy destroys a port channel interface and removes all interfaces from the channel group.

@param [String] name The name of the port channel interface, e.g

'Port-Channel3'

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 184
def port_channel_destroy(name)
  cmds = %w(enable configure) << "no interface #{name}"
  eapi_action(cmds, "remove #{name}")
end
set_flowcontrol_recv(name, value) click to toggle source

set_flowcontrol_recv Configures a specific interface’s flow control

receive value.

@param [String] name The name of the interface to configure, e.g.

'Ethernet1'

@param [Symbol] value the value to configure, e.g. ‘:on`, `:off`

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 495
def set_flowcontrol_recv(name, value)
  cmd = %w(enable configure) << "interface #{name}"
  cmd << "flowcontrol receive #{value}"
  eapi_action(cmd, 'configure flowcontrol receive')
end
set_flowcontrol_send(name, value) click to toggle source

set_flowcontrol_send Configures a specific interface’s flow control

send value.

@param [String] name The name of the interface to configure, e.g.

'Ethernet1'

@param [Symbol] value the value to configure, e.g. ‘:on`, `:off`

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 479
def set_flowcontrol_send(name, value)
  cmd = %w(enable configure) << "interface #{name}"
  cmd << "flowcontrol send #{value}"
  eapi_action(cmd, 'configure flowcontrol send')
end
set_interface_description(name, description) click to toggle source

set_interface_description configures the description string for an interface.

@param [String] name The interface name, e.g. ‘Ethernet1’

@param [String] description The description to assign the interface.

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 536
def set_interface_description(name, description)
  cmd = %w(enable configure) << "interface #{name}"
  cmd << "description #{description}"
  eapi_action(cmd, "set interface #{name} description to #{description}")
end
set_interface_mtu(name, mtu) click to toggle source

set_interface_mtu configures the interface MTU

@param [String] name The interface name, e.g. ‘Ethernet1’

@param [Fixnum] mtu The interface mtu, e.g. 9000

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 565
def set_interface_mtu(name, mtu)
  cmd = %w(enable configure) << "interface #{name}"
  cmd << "mtu #{mtu}"
  eapi_action(cmd, "set interface #{name} mtu to #{mtu}")
end
set_interface_speed(name, speed) click to toggle source

set_interface_speed enable a network interface

@param [String] name The interface name, e.g. ‘Ethernet1’

@param [String] speed The interface state, e.g. ‘1000full’ or

'40gfull'

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 551
def set_interface_speed(name, speed)
  cmd = %w(enable configure) << "interface #{name}"
  cmd << "speed forced #{speed}"
  eapi_action(cmd, "set interface #{name} speed to #{speed}")
end
set_interface_state(name, state) click to toggle source

set_interface_state enables or disables a network interface

@param [String] name The interface name, e.g. ‘Ethernet1’

@param [String] state The interface state, e.g. ‘no shutdown’ or

'shutdown'

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 522
def set_interface_state(name, state)
  cmd = %w(enable configure) << "interface #{name}" << state
  eapi_action(cmd, "set interface #{name} state to #{state}")
end
set_vlan_name(id, name) click to toggle source

set_vlan_name assigns a name to a vlan

@param [Integer] id The vlan ID

@param [String] name The vlan name

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 225
def set_vlan_name(id, name)
  cmds = ['enable', 'configure', "vlan #{id}"] << "name #{name}"
  eapi_action(cmds, "set vlan #{id} name to #{name}")
end
set_vlan_state(id, state) click to toggle source

set_vlan_state set a vlan to the state specified

@param [Integer] id The vlan ID

@param [String] state The state of the vlan, e.g. ‘active’ or

'suspend'

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 239
def set_vlan_state(id, state)
  cmds = ['enable', 'configure', "vlan #{id}"] << "state #{state}"
  eapi_action(cmds, "set vlan #{id} state to #{state}")
end
uri() click to toggle source

@return [URI] the URI of the server

# File lib/puppet_x/net_dev/eos_api.rb, line 825
def uri
  return @uri if @uri
  if username && password
    @uri = URI("http://#{username}:#{password}@#{address}:#{port}")
  else
    @uri = URI("http://#{address}:#{port}")
  end
end
vlan(id) click to toggle source

vlan returns data about a specific VLAN identified by the VLAN ID number. This API call maps roughly to the ‘show vlan <id>` command. This method returns nil if no VLAN was found matching the ID provided.

@param [Fixnum] id The VLAN id to obtain information about.

@api public

@return [nil,Hash<String,Hash>] Hash describing the VLAN attributes, or

nil if no vlan was found matching the id provided.  The format of
this hash matches the format of {all_vlans}
# File lib/puppet_x/net_dev/eos_api.rb, line 84
def vlan(id)
  result = eapi_action("show vlan #{id}", 'list vlans')
  result.first['vlans'] if result
end
vlan_create(id) click to toggle source

vlan_create creates a VLAN that does not yet exist on the target device.

@param [Fixnum] id The VLAN id to create

@api public

@return [Boolean] true if the vlan was created

# File lib/puppet_x/net_dev/eos_api.rb, line 98
def vlan_create(id)
  cmds = ['enable', 'configure', "vlan #{id}"]
  eapi_action(cmds, "create vlan #{id}")
end
vlan_destroy(id) click to toggle source

vlan_destroy destroys a vlan

@param [Integer] id The VLAN ID to destroy

@api public

# File lib/puppet_x/net_dev/eos_api.rb, line 109
def vlan_destroy(id)
  cmds = ['enable', 'configure', "no vlan #{id}"]
  eapi_action(cmds, "destroy vlan #{id}")
end

Private Instance Methods

eapi_action(command, action = 'make api call', options = {}) click to toggle source

eapi_action makes an API call and handles any error messages in the return value.

@param [String,Array<String>] command The command or commands to

execute, e.g. 'show vlan'

@param [String] action The action being performed, e.g. ‘set interface

description'.  Used to format error messages on API errors.

@option opts [String] :id The identifier for this request. If omitted,

a unique identifier will be generated.

@option opts [String] :format (‘json’) The desired format of the

response, e.g. 'text' or 'json'.  Defaults to 'json' if not provided.

@api private

@return [Array<Hash>] the value of the ‘result’ key from the API

response.
# File lib/puppet_x/net_dev/eos_api.rb, line 814
def eapi_action(command, action = 'make api call', options = {})
  api_response = eapi_call(command, options)

  return api_response['result'] unless api_response['error']
  err_msg = format_error(api_response['error']['data'])
  fail ApiError, "could not #{action}: #{err_msg}"
end
eapi_call(command, options = {}) click to toggle source

eapi_call takes a string as an arista command and executes the command on the switch using the eAPI. This method decodes the API response and returns the value. For example:

[1] pry(#<PuppetX::NetDev::EosApi>)> eapi_call('show version')
=> {"jsonrpc"=>"2.0",
 "result"=>
  [{"modelName"=>"vEOS",
    "internalVersion"=>"4.13.7M-1877079.4137M.1",
    "systemMacAddress"=>"00:42:00:08:17:78",
    "serialNumber"=>"",
    "memTotal"=>2033744,
    "bootupTimestamp"=>1403732020.05,
    "memFree"=>143688,
    "version"=>"4.13.7M",
    "architecture"=>"i386",
    "internalBuildId"=>"54a9c4ce-bbb0-4f6b-9448-9507de824905",
    "hardwareRevision"=>""}],
  "id"=>"a4e14732-e0f2-430d-823e-1c801273ec60"}

@param [String,Array<String>] command The command or commands to

execute, e.g. 'show vlan'

@option opts [String] :id The identifier for this request. If omitted,

a unique identifier will be generated.

@option opts [String] :format (‘json’) The desired format of the

response, e.g. 'text' or 'json'.  Defaults to 'json' if not provided.

@api private

@return [Hash] the response from the API

# File lib/puppet_x/net_dev/eos_api.rb, line 784
def eapi_call(command, options = {})
  cmds = [*command]
  request_body = format_command(cmds, options)
  req = eapi_request(request_body)
  resp = http.request(req)
  decoded_response = JSON.parse(resp.body)
  decoded_response
end
eapi_request(request_body) click to toggle source

eapi_request returns a Net::HTTP::Post instance suitable for use with the http client to make an API call to EOS. The request will automatically be initialized with an username and password if the attributes have been initialized.

@param [String] request_body The data to post to the API represented as

a string, usually JSON encoded.

@api private

@return [Net::HTTP::Post] A request instance suitable for use with

Net::HTTP#request
# File lib/puppet_x/net_dev/eos_api.rb, line 742
def eapi_request(request_body)
  # JSON-RPC 2.0 to /command-api/ location
  req = Net::HTTP::Post.new('/command-api/')
  req.basic_auth(username, password) if username && password
  req.body = request_body
  req
end
format_command(command, options = {}) click to toggle source

format_command takes an EOS command as a string and returns the appropriate data structure for use with the EOS REST API.

@param [String, Array<String>] command The command to execute on the

switch, e.g. 'show vlan' or ['show vlan 1', 'show vlan 2'].

@option opts [String] :id The identifier for this request. If omitted,

a unique identifier will be generated.

@option opts [String] :format (‘json’) The desired format of the

response, e.g. 'text' or 'json'.  Defaults to 'json' if not provided.

@api private

@return [String] The JSON string suitable for use with HTTP POST API

calls to the EOS API.
# File lib/puppet_x/net_dev/eos_api.rb, line 620
def format_command(command, options = {})
  cmds = [*command]
  req_id = options[:id].nil? ? SecureRandom.uuid : options[:id]
  format = options[:format].nil? ? 'json' : options[:format]
  params = { 'version' => 1, 'cmds' => cmds, 'format' => format }
  request = {
    'jsonrpc' => '2.0', 'method' => 'runCmds',
    'params' => params, 'id' => req_id
  }
  JSON.dump(request)
end
parse_portchannel_name(line) click to toggle source

parse_portchannel_name parses out the portchannel name from the first line of a group section.

@param [String] line The first line of a portchannel group detailed

show statement, e.g. 'Port Channel Port-Channel1 (Fallback State:
Unconfigured):'

@api private

@return [String,nil] the name of the portchannel group or nil if the

name could not be parsed.
# File lib/puppet_x/net_dev/eos_api.rb, line 723
def parse_portchannel_name(line)
  mdata = /^(?:Port Channel )?(Port-Channel\d+)/.match(line)
  mdata[1] if mdata
end
parse_portchannels(text) click to toggle source

parse_portchannels accepts the text output of the ‘show etherchannel detailed` command and parses the text into structured data with the portchannel names as keys and portchannel attributes as key/values in a hash.

@param [String] text The text output to parse.

@api private

@return [Hash<String,Hash>] where the key is the port channel name,

e.g. 'Port-Channel10'
# File lib/puppet_x/net_dev/eos_api.rb, line 645
def parse_portchannels(text) # rubocop:disable Metrics/MethodLength
  groups = text.split('Port Channel ')
  groups.each_with_object({}) do |str, group|
    lines = [*str.lines]
    name = parse_portchannel_name(lines.shift)
    next unless name
    active_ports = parse_portchannel_active_ports(lines)
    configured_ports = parse_portchannel_configured_ports(lines)
    group[name] = {
      'name'  => name,
      'ports' => [*active_ports, *configured_ports].sort
    }
  end
end