Class: Cisco::Client

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

Overview

Base class for clients of various RPC formats

Direct Known Subclasses

GRPC, NETCONF

Defined Under Namespace

Classes: GRPC, NETCONF

Constant Summary collapse

@@clients =

rubocop:disable Style/ClassVars

[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

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

Returns a new instance of Client.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/util/client/client.rb', line 38

def initialize(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.platform = platform
end

Instance Attribute Details

#addressObject (readonly)

Returns the value of attribute address.



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

def address
  @address
end

#hostObject (readonly)

Returns the value of attribute host.



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

def host
  @host
end

#passwordObject (readonly)

Returns the value of attribute password.



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

def password
  @password
end

#platformObject

Returns the value of attribute platform.



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

def platform
  @platform
end

#portObject (readonly)

Returns the value of attribute port.



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

def port
  @port
end

#usernameObject (readonly)

Returns the value of attribute username.



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

def username
  @username
end

Class Method Details

.clientsObject



27
28
29
# File 'lib/util/client/client.rb', line 27

def self.clients
  @@clients
end

.create(client_class) ⇒ Object

Try to create an instance of the specified subclass



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

def self.create(client_class)
  env = environment(client_class)
  env_name = environment_name(client_class)

  fail Cisco::ClientError, "No client environment configured for '#{env_name}'" unless env

  host = env[:host]
  port = env[:port]
  debug "Trying to connect to #{host}:#{port} as #{client_class}"
  errors = []
  begin
    client = client_class.new(**env)
    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
  handle_errors(errors)
end

.environment(client_class) ⇒ Object



75
76
77
# File 'lib/util/client/client.rb', line 75

def self.environment(client_class)
  Cisco::Util::Environment.environment(environment_name(client_class))
end

.environment_name(client_class) ⇒ Object



71
72
73
# File 'lib/util/client/client.rb', line 71

def self.environment_name(client_class)
  client_class.name.split('::').last.downcase
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) (defaults to: nil)

    The body of text to search

  • context (*Regex) (defaults to: nil)

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

  • value (Regex) (defaults to: nil)

    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/util/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

.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/util/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



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/util/client/client.rb', line 102

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/util/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.



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

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.



129
130
131
132
133
134
135
# File 'lib/util/client/utils.rb', line 129

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
# File 'lib/util/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
    if input[0] == '/' && input[-1] == '/'
      # '/foo/' => %r{foo}
      return Regexp.new(input[1..-2])
    elsif input[0] == '/' && input[-2..-1] == '/i'
      # '/foo/i' => %r{foo}i
      return Regexp.new(input[1..-3], Regexp::IGNORECASE)
    else
      # 'foo' => %r{^foo$}i
      return Regexp.new("^#{input}$", Regexp::IGNORECASE)
    end
  end
end

.validate_args(**kwargs) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/util/client/client.rb', line 53

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

#get(command: nil, **_kwargs) ⇒ String, ...

Get the given state from the device.

Parameters:

  • command (String) (defaults to: nil)

    the get command to execute

  • 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.



153
154
155
156
157
158
159
160
# File 'lib/util/client/client.rb', line 153

def get(command: nil,
        **_kwargs)
  # subclasses will generally want to call Client.munge_to_array()
  # on value before calling super()
  Cisco::Logger.debug("  executing command:\n    #{command}") \
    unless command.nil? || command.empty?
  # to be implemented by subclasses
end

#inspectObject



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

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

#munge_to_array(val) ⇒ Object



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

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

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

Configure the given state on the device.

Parameters:

  • values (String, Array<String>) (defaults to: nil)

    Actual configuration to set

  • kwargs

    data-format-specific args



138
139
140
141
142
143
144
145
# File 'lib/util/client/client.rb', line 138

def set(values: nil,
        **_kwargs)
  # subclasses will generally want to call Client.munge_to_array()
  # on values before calling super()
  Cisco::Logger.debug("values: #{values})") \
    unless values.nil? || values.empty?
  # to be implemented by subclasses
end

#to_sObject



126
127
128
# File 'lib/util/client/client.rb', line 126

def to_s
  @address.to_s
end