Class: Cisco::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/cisco_node_utils/client.rb,
lib/cisco_node_utils/client/utils.rb,
lib/cisco_node_utils/client/client.rb

Overview

Base class for clients of various RPC formats

Direct Known Subclasses

GRPC, NXAPI

Defined Under Namespace

Classes: GRPC, NXAPI

Constant Summary collapse

@@clients =

rubocop:disable Style/ClassVars

[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data_formats: [], platform: nil, **kwargs) ⇒ Client

Returns a new instance of Client


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/cisco_node_utils/client/client.rb', line 42

def initialize(data_formats: [],
               platform:     nil,
               **kwargs)
  if self.class == Cisco::Client
    fail NotImplementedError, 'Cisco::Client is an abstract class. ' \
      "Instantiate one of #{@@clients} or use Cisco::Client.create() instead"
  end
  self.class.validate_args(**kwargs)
  @host = kwargs[:host]
  @port = kwargs[:port]
  @address = @port.nil? ? @host : "#{@host}:#{@port}"
  @username = kwargs[:username]
  @password = kwargs[:password]
  self.data_formats = data_formats
  self.platform = platform
  @cache_enable = true
  @cache_auto = true
  cache_flush
end

Instance Attribute Details

#cache_auto=(value) ⇒ Object (writeonly)

Sets the attribute cache_auto

Parameters:

  • value

    the value to set the attribute cache_auto to.


151
152
153
# File 'lib/cisco_node_utils/client/client.rb', line 151

def cache_auto=(value)
  @cache_auto = value
end

#data_formatsObject

Returns the value of attribute data_formats


40
41
42
# File 'lib/cisco_node_utils/client/client.rb', line 40

def data_formats
  @data_formats
end

#platformObject

Returns the value of attribute platform


40
41
42
# File 'lib/cisco_node_utils/client/client.rb', line 40

def platform
  @platform
end

Class Method Details

.clientsObject


31
32
33
# File 'lib/cisco_node_utils/client/client.rb', line 31

def self.clients
  @@clients
end

.create(environment_name = nil) ⇒ Object

Try to create an instance of an appropriate subclass


85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/cisco_node_utils/client/client.rb', line 85

def self.create(environment_name=nil)
  fail 'No client implementations available!' if clients.empty?
  debug "Trying to establish client connection. clients = #{clients}"
  environment = Cisco::Environment.environment(environment_name)
  host = environment[:host]
  errors = []
  clients.each do |client_class|
    begin
      debug "Trying to connect to #{host} as #{client_class}"
      client = client_class.new(**environment)
      debug "#{client_class} connected successfully"
      return client
    rescue Cisco::ClientError, TypeError, ArgumentError => e
      debug "Unable to connect to #{host} as #{client_class}: #{e.message}"
      debug e.backtrace.join("\n  ")
      errors << e
    end
  end
  handle_errors(errors)
end

.filter_cli(cli_output: nil, context: nil, value: nil) ⇒ [String]?

Helper function that subclasses may use with get(data_format: :cli) Method for working with hierarchical show command output such as “show running-config”. Searches the given multi-line string for all matches to the given value query. If context is provided, the matches will be filtered to only those that are located “under” the given context sequence (as determined by indentation).

Examples:

Find all OSPF router names in the running-config

ospf_names = filter_cli(cli_output: running_cfg,
                        value:      /^router ospf (\d+)/)

Find all address-family types under the given BGP router

bgp_afs = filter_cli(cli_output: show_run_bgp,
                     context:    [/^router bgp #{ASN}/],
                     value:      /^address-family (.*)/)

Parameters:

  • cli_output (String)

    The body of text to search

  • context (*Regex)

    zero or more regular expressions defining the parent configs to filter by.

  • value (Regex)

    The regular expression to match

Returns:

  • ([String], nil)

    array of matching (sub)strings, else nil.


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/cisco_node_utils/client/utils.rb', line 57

def self.filter_cli(cli_output: nil,
                    context:    nil,
                    value:      nil)
  return cli_output if cli_output.nil?
  context ||= []
  context.each { |filter| cli_output = find_subconfig(cli_output, filter) }
  return nil if cli_output.nil? || cli_output.empty?
  return cli_output if value.nil?
  value = to_regexp(value)
  match = cli_output.scan(value)
  return nil if match.empty?
  # find matches and return as array of String if it only does one match.
  # Otherwise return array of array.
  match.flatten! if match[0].is_a?(Array) && match[0].length == 1
  match
end

.filter_data(data: nil, keys: nil) ⇒ Object

Helper method for get(data_format: :nxapi_structured).

Parameters:

  • data (Array, Hash)

    structured output from node

  • keys (Array)

    lookup sequence


141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/cisco_node_utils/client/utils.rb', line 141

def self.filter_data(data: nil,
                     keys: nil)
  return nil if data.nil? || data.empty?
  keys ||= []
  keys.each do |filter|
    # if filter is a Hash and data is an array, check each
    # array index (which should return another hash) to see if
    # it contains the matching key/value pairs specified in token,
    # and return the first match (or nil)
    if filter.kind_of?(Hash)
      fail "Expected Array, got #{data.class}" unless data.is_a? Array
      data = data.select { |x| filter.all? { |k, v| x[k] == v } }
      fail "Multiple matches found for #{filter}" if data.length > 1
      fail "No match found for #{filter}" if data.length == 0
      data = data[0]
    else # data is array or hash
      if data.is_a? Array
        final = []
        data.each do |row|
          final << row[filter]
        end
        return final
      end
      fail "No key \"#{filter}\" in #{data}" unless data.key?(filter)
      data = data[filter]
    end
  end
  data
end

.find_subconfig(body, regexp_query) ⇒ String?

Returns the subsection associated with the given line of config to retrieve the subsection appropriately, or nil if no such subsection exists.

Parameters:

  • the (String)

    body of text to search

  • the (Regex)

    regex key of the config for which

Returns:

  • (String, nil)

    the subsection of body, de-indented


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/cisco_node_utils/client/utils.rb', line 81

def self.find_subconfig(body, regexp_query)
  return nil if body.nil? || regexp_query.nil?
  regexp_query = to_regexp(regexp_query)

  rows = body.split("\n")
  match_row_index = rows.index { |row| regexp_query =~ row }
  return nil if match_row_index.nil?

  cur = match_row_index + 1
  subconfig = []

  until (/\A\s+.*/ =~ rows[cur]).nil? || cur == rows.length
    subconfig << rows[cur]
    cur += 1
  end
  return nil if subconfig.empty?
  # Strip an appropriate minimal amount of leading whitespace from
  # all lines in the subconfig
  min_leading = subconfig.map { |line| line[/\A */].size }.min
  subconfig = subconfig.map { |line| line[min_leading..-1] }
  subconfig.join("\n")
end

.handle_errors(errors) ⇒ Object


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/cisco_node_utils/client/client.rb', line 106

def self.handle_errors(errors)
  # ClientError means we tried to connect but failed,
  # so it's 'more significant' than input validation errors.
  client_errors = errors.select { |e| e.kind_of? Cisco::ClientError }
  if !client_errors.empty?
    # Reraise the specific error if just one
    fail client_errors[0] if client_errors.length == 1
    # Otherwise clump them together into a new error
    e_cls = client_errors[0].class
    unless client_errors.all? { |e| e.class == e_cls }
      e_cls = Cisco::ClientError
    end
    fail e_cls, ("Unable to establish any client connection:\n" +
                 errors.each(&:message).join("\n"))
  elsif errors.any? { |e| e.kind_of? ArgumentError }
    fail ArgumentError, ("Invalid arguments:\n" +
                         errors.each(&:message).join("\n"))
  elsif errors.any? { |e| e.kind_of? TypeError }
    fail TypeError, ("Invalid arguments:\n" +
                     errors.each(&:message).join("\n"))
  end
  fail Cisco::ClientError, 'No client connected, but no errors were reported?'
end

.munge_to_array(val) ⇒ Object

Make a best effort to convert a given input value to an Array. Strings are split by newlines, and nil becomes an empty Array.


26
27
28
29
30
# File 'lib/cisco_node_utils/client/utils.rb', line 26

def self.munge_to_array(val)
  val = [] if val.nil?
  val = val.split("\n") if val.is_a?(String)
  val
end

.register_client(client) ⇒ Object

Each subclass should call this method to register itself.


36
37
38
# File 'lib/cisco_node_utils/client/client.rb', line 36

def self.register_client(client)
  @@clients << client
end

.silence_warnings(&block) ⇒ Object

Helper method for calls into third-party code - suppresses Ruby warnings for the given block since we have no control over that code.


173
174
175
176
177
178
179
# File 'lib/cisco_node_utils/client/utils.rb', line 173

def self.silence_warnings(&block)
  warn_level = $VERBOSE
  $VERBOSE = nil
  result = block.call
  $VERBOSE = warn_level
  result
end

.to_regexp(input) ⇒ Object

Helper method for CLI getters

Convert a string or array of strings to a Regexp or array thereof


107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/cisco_node_utils/client/utils.rb', line 107

def self.to_regexp(input)
  if input.is_a?(Regexp)
    return input
  elsif input.is_a?(Array)
    return input.map { |item| to_regexp(item) }
  else
    # The string might be explicitly formatted as a regexp
    # Dynamically handle modifiers
    input.match(%r{(?<regex>^\/.*\/)(?<options>[imx]*)?}) do |m|
      options = []
      m['options'].each_char do |c|
        case c
        when 'i'
          options << Regexp::IGNORECASE
        when 'm'
          options << Regexp::MULTILINE
        when 'x'
          options << Regexp::EXTENDED
        end
      end
      return Regexp.new(m['regex'][1..-2], options.reduce(:|))
    end
    # otherwise this value is a regular string
    # convert to case insensitive regex
    # 'foo' => %r{^foo$}i
    return Regexp.new("^#{input}$", Regexp::IGNORECASE)

  end
end

.validate_args(**kwargs) ⇒ Object


62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/cisco_node_utils/client/client.rb', line 62

def self.validate_args(**kwargs)
  host = kwargs[:host]
  unless host.nil?
    fail TypeError, 'invalid address' unless host.is_a?(String)
    fail ArgumentError, 'empty address' if host.empty?
  end
  username = kwargs[:username]
  unless username.nil?
    fail TypeError, 'invalid username' unless username.is_a?(String)
    fail ArgumentError, 'empty username' if username.empty?
  end
  password = kwargs[:password]
  unless password.nil?
    fail TypeError, 'invalid password' unless password.is_a?(String)
    fail ArgumentError, 'empty password' if password.empty?
  end
end

Instance Method Details

#cache_auto?Boolean

Returns:

  • (Boolean)

147
148
149
# File 'lib/cisco_node_utils/client/client.rb', line 147

def cache_auto?
  @cache_auto
end

#cache_enable=(enable) ⇒ Object


142
143
144
145
# File 'lib/cisco_node_utils/client/client.rb', line 142

def cache_enable=(enable)
  @cache_enable = enable
  cache_flush unless enable
end

#cache_enable?Boolean

Returns:

  • (Boolean)

138
139
140
# File 'lib/cisco_node_utils/client/client.rb', line 138

def cache_enable?
  @cache_enable
end

#cache_flushObject

Clear the cache of CLI output results.

If cache_auto is true (default) then this will be performed automatically whenever a set() is called, but providers may also call this to explicitly force the cache to be cleared.


158
159
160
# File 'lib/cisco_node_utils/client/client.rb', line 158

def cache_flush
  # to be implemented by subclasses
end

#get(data_format: :cli, command: nil, context: nil, value: nil, **_kwargs) ⇒ String, ...

Get the given state from the device.

Unlike set() this will not clear the CLI cache; multiple calls with the same parameters may return cached data rather than querying the device repeatedly.

Parameters:

  • data_format

    one of Cisco::DATA_FORMATS. Default is :cli

  • command (String)

    the get command to execute

  • context (String, Array<String>)

    Context to refine/filter the results

  • value (String, Regexp)

    Specific key or regexp to look up

  • kwargs

    data-format-specific args

Returns:

  • (String, Hash, nil)

    The state found, or nil if not found.

Raises:


202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/cisco_node_utils/client/client.rb', line 202

def get(data_format: :cli,
        command:     nil,
        context:     nil,
        value:       nil,
        **_kwargs)
  # subclasses will generally want to call Client.munge_to_array()
  # on context and/or value before calling super()
  fail Cisco::RequestNotSupported unless self.supports?(data_format)
  Cisco::Logger.debug("Get state using data format '#{data_format}'")
  Cisco::Logger.debug("  executing command:\n    #{command}") \
    unless command.nil? || command.empty?
  Cisco::Logger.debug("  with context:\n    #{context.join("\n    ")}") \
    unless context.nil? || context.empty?
  Cisco::Logger.debug("  to get value:     #{value}") \
    unless value.nil?
  # to be implemented by subclasses
end

#inspectObject


134
135
136
# File 'lib/cisco_node_utils/client/client.rb', line 134

def inspect
  "<#{self.class} of #{@address}>"
end

#munge_to_array(val) ⇒ Object


32
33
34
# File 'lib/cisco_node_utils/client/utils.rb', line 32

def munge_to_array(val)
  self.class.munge_to_array(val)
end

#set(data_format: :cli, context: nil, values: nil, **_kwargs) ⇒ Object

Configure the given state on the device.

Parameters:

  • data_format

    one of Cisco::DATA_FORMATS. Default is :cli

  • context (String, Array<String>)

    Context for the configuration

  • values (String, Array<String>)

    Actual configuration to set

  • kwargs

    data-format-specific args

Raises:


171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/cisco_node_utils/client/client.rb', line 171

def set(data_format: :cli,
        context:     nil,
        values:      nil,
        **_kwargs)
  # subclasses will generally want to call Client.munge_to_array()
  # on context and/or values before calling super()
  fail Cisco::RequestNotSupported unless self.supports?(data_format)
  cache_flush if cache_auto?
  Cisco::Logger.debug("Set state using data format '#{data_format}'")
  Cisco::Logger.debug("  with context:\n    #{context.join("\n    ")}") \
    unless context.nil? || context.empty?
  Cisco::Logger.debug("  to value(s):\n    #{values.join("\n    ")}") \
    unless values.nil? || values.empty?
  # to be implemented by subclasses
end

#supports?(data_format) ⇒ Boolean

Returns:

  • (Boolean)

80
81
82
# File 'lib/cisco_node_utils/client/client.rb', line 80

def supports?(data_format)
  data_formats.include?(data_format)
end

#to_sObject


130
131
132
# File 'lib/cisco_node_utils/client/client.rb', line 130

def to_s
  @address.to_s
end