Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--------------------------------------------------------------------------------
New
--------------------------------------------------------------------------------
* IOSXE
* Modified ShowRunningConfigAAAUsernameSchema(MetaParser):
* added: optional 'autocommand'
* added: optional 'nopassword'

* IOSXE
* Modified ShowRunningConfigAAAUsername(ShowRunningConfigAAAUsernameSchema)
* Added support for 'autocommand'
* Added support for 'nopassword'
* Added support for multiline usernames
* Added logging (warning) for unsupported options

--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------
* IOSXE
* Modified ShowRunningConfigAAAUsername(ShowRunningConfigAAAUsernameSchema)
* Changed how the cli() function parses arguments and parameters.

223 changes: 112 additions & 111 deletions src/genie/libs/parser/iosxe/show_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# Python
import re
import logging

# Metaparser
from genie.metaparser import MetaParser
Expand All @@ -22,6 +23,8 @@
# import parser utils
from genie.libs.parser.utils.common import Common

log = logging.getLogger(__name__)

# =================================================
# Schema for:
# * 'show run policy-map {name}'
Expand Down Expand Up @@ -2750,7 +2753,9 @@ class ShowRunningConfigAAAUsernameSchema(MetaParser):
Optional('common_criteria_policy'): str,
Optional('view'): str,
Optional('type'): str,
Optional('autocommand'): str,
Optional('onetime'): bool,
Optional('nopassword'): bool,
Optional('secret'): {
Optional('type'): int,
Optional('secret'): str,
Expand Down Expand Up @@ -2896,139 +2901,135 @@ def cli(self, output=None):
else:
out = output

# username testuser password 0 lab
p1 = re.compile(r'^username +(?P<username>\S+) +password +(?P<type>\d) +(?P<password>.*)$')
# NOTE: All of the following regular expressions should be anchored to
# the begining of the line ('^'). As each is used the line will be
# shortened. Think of this as popping arguments (and their parameters)
# off of a stack (the front of the line).
#
# There are some arguments that cannot have any subsequent arguments.
# These are:
# 1) password
# 2) secret
# 3) autocommand
# These arguments shall also match the end of the line ('$').
#
# All arguments that are not matched to the end of the line shall match
# an optional trailing space (' ?').

# username testuser
username_cmd = re.compile(r'^username (?P<username>\S+) ?')

# username testuser common-criteria-policy Test-CC password 0 password
p2 = re.compile(
r'^username +(?P<username>\S+) +common-criteria-policy +(?P<common_criteria_policy>.*) '
r'+password +(?P<type>\d) +(?P<password>.*)$')
# common-criteria-policy Test-CC
common_criteria_policy = re.compile(r'^common-criteria-policy (?P<common_criteria_policy>\S+) ?')

# username testuser secret 9 $9$A2OfV.30kNlIhE$ZEJQIT6aUj.TfCzqGQr.h4AmjQd/bWikQaGRlaLv0nQ
p3 = re.compile(r'^username +(?P<username>\S+) +secret +(?P<type>\d) +(?P<secret>.*)$')
# secret 9 $9$A2OfV.30kNlIhE$ZEJQIT6aUj.TfCzqGQr.h4AmjQd/bWikQaGRlaLv0nQ
secret = re.compile(r'^secret (?P<type>\d) (?P<secret>.*)$')

# username testuser one-time secret 9 $9$AuJ8xgW8aBBuF.$HyAzLk.3ILFsKrEvd4YjaAHbtonVMLikXw2pnrlkYJY
p4 = re.compile(
r'^username +(?P<username>\S+) +one-time +(?P<Onetime>)\s*secret +(?P<type>\d+) +(?P<secret>.*)$')
# privilege 15
privilege = re.compile(r'^privilege (?P<privilege>\d+) ?')

# username testuser privilege 15 password 0 lab
p5 = re.compile(
r'^username +(?P<username>\S+) +privilege +(?P<privilege>\d+) +password +(?P<type>\d) +(?P<password>.*)$')
# one-time
onetime = re.compile(r'^one-time ?')

# username testuser common-criteria-policy Test-CC secret 9 $9$7K9qbCZMJa2Vuk$6bS3.Bv7AkBXhTHpTH9V9fhMnJCQe1a9O7xBWHtOKo.
p6 = re.compile(
r'^username +(?P<username>\S+) +common-criteria-policy +(?P<common_criteria_policy>.*) '
r'+secret +(?P<type>\d) +(?P<secret>.*)$')
# nopassword
nopassword = re.compile(r'^nopassword ?')

# username testuser one-time password 0 password
p7 = re.compile(
r'^username +(?P<username>\S+) +one-time +(?P<Onetime>)\s*password +(?P<type>\d) +(?P<password>.*)$')
# password 0 lab
password = re.compile(r'^password (?P<type>\d) (?P<password>.*)$')

# username developer privilege 15 secret 9 $9$oNguEA9um9vRx.$MsDk0DOy1rzBjKAcySWdNjoKcA7GetG9YNnKOs8S67A
p8 = re.compile(r'^username +(?P<username>\S+) +privilege +(?P<privilege>\d+) +secret +(?P<secret_type>\d+) +(?P<secret>\S+)$')
# autocommand show ip bgp summary
autocommand = re.compile(r'^autocommand (?P<autocommand>.*)$')

# Initial return dictionary
ret_dict = {}

for line in out.splitlines():
line = line.strip()

# username testuser password 0 lab
m = p1.match(line)
if m:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})
pass_dict = users_dict.setdefault('password', {})
pass_dict['type'] = int(group['type'])
pass_dict['password'] = group['password']
# username testuser
m = username_cmd.match(line)
if not m:
# CLAIM: This is not a line with a 'username' command.
continue

# username testuser common-criteria-policy Test-CC password 0 password
m = p2.match(line)
if m:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})
users_dict['common_criteria_policy'] = group['common_criteria_policy']
pass_dict = users_dict.setdefault('password', {})
pass_dict['type'] = int(group['type'])
pass_dict['password'] = group['password']
continue
# CLAIM: this is a username line
# GOAL: extract the specified username and switch to that
# sub-dictionary:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})

# username testuser secret 9 $9$A2OfV.30kNlIhE$ZEJQIT6aUj.TfCzqGQr.h4AmjQd/bWikQaGRlaLv0nQ
m = p3.match(line)
if m:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})
secret_dict = users_dict.setdefault('secret', {})
secret_dict['type'] = int(group['type'])
secret_dict['secret'] = group['secret']
continue
# GOAL: remove the matched portion from the begining of the line
# so that we can match the subsequent argument (if any):
line = line[m.end():]

# username testuser one-time secret 9 $9$AuJ8xgW8aBBuF.$HyAzLk.3ILFsKrEvd4YjaAHbtonVMLikXw2pnrlkYJY
m = p4.match(line)
if m:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})
users_dict['onetime'] = True
secret_dict = users_dict.setdefault('secret', {})
secret_dict['type'] = int(group['type'])
secret_dict['secret'] = group['secret']
continue
while line:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While we typically discourage the use of re.search, it might actually be preferable to use it over a while loop

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced that re.search will be able to successfully identify all of the weird user-configurable portions of the line. Consider this abomonation:

Router1(config)#do sh running-config aaa username
!
username testuser12 common-criteria-policy nopassword password 0 nopassword

# GOAL: parse through the line an argument at a time,
# shortening the line as we go.

# username testuser privilege 15 password 0 lab
m = p5.match(line)
if m:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})
users_dict['privilege'] = int(group['privilege'])
pass_dict = users_dict.setdefault('password', {})
pass_dict['type'] = int(group['type'])
pass_dict['password'] = group['password']
continue
# GOAL: match the 'common-criteria-policy' option and return its parameter
# Sample: "common-criteria-policy MyPolicy"
if m := common_criteria_policy.match(line):
group = m.groupdict()
users_dict['common_criteria_policy'] = group['common_criteria_policy']
line = line[m.end():]
continue

# username testuser common-criteria-policy Test-CC secret 9 $9$7K9qbCZMJa2Vuk$6bS3.Bv7AkBXhTHpTH9V9fhMnJCQe1a9O7xBWHtOKo.
m = p6.match(line)
if m:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})
users_dict['common_criteria_policy'] = group['common_criteria_policy']
secret_dict = users_dict.setdefault('secret', {})
secret_dict['type'] = int(group['type'])
secret_dict['secret'] = group['secret']
continue
# GOAL: match the 'privilege' option and return its parameter
# Sample: "privilege 15"
if m := privilege.match(line):
group = m.groupdict()
users_dict['privilege'] = int(group['privilege'])
line = line[m.end():]
continue

# username testuser one-time password 0 password
m = p7.match(line)
if m:
group = m.groupdict()
username = group['username']
users_dict = ret_dict.setdefault('username', {}).setdefault(username, {})
users_dict['onetime'] = True
pass_dict = users_dict.setdefault('password', {})
pass_dict['type'] = int(group['type'])
pass_dict['password'] = group['password']
continue
# GOAL: match the 'secret' option and return its parameters ('type' and 'secret')
# Sample: "secret 9 $9$oNguEA9um9vRx.$MsDk0DOy1rzBjKAcySWdNjoKcA7GetG9YNnKOs8S67A"
if m := secret.match(line):
group = m.groupdict()
pass_dict = users_dict.setdefault('secret', {})
pass_dict['type'] = int(group['type'])
pass_dict['secret'] = group['secret']
line = line[m.end():]
continue

# username developer privilege 15 secret 9 $9$oNguEA9um9vRx.$MsDk0DOy1rzBjKAcySWdNjoKcA7GetG9YNnKOs8S67A
m = p8.match(line)
if m:
group = m.groupdict()
user_dict = ret_dict.setdefault('username', {}).setdefault(group['username'], {})
user_dict.update({
'privilege': int(group['privilege']),
})
# GOAL: match the 'onetime' flag
# Sample: "onetime"
if m := onetime.match(line):
group = m.groupdict()
users_dict['onetime'] = True
line = line[m.end():]
continue

secret_dict = user_dict.setdefault('secret', {})
secret_dict.update({
'type': int(group['secret_type']),
'secret': group['secret']
})
# GOAL: match the 'nopassword' flag
# Sample: "nopassword"
if m := nopassword.match(line):
group = m.groupdict()
users_dict['nopassword'] = True
line = line[m.end():]
continue

# GOAL: match the 'autocommand' option and return all subsequent text
# Sample: "autocommand show ip bgp summary"
if m := autocommand.match(line):
group = m.groupdict()
users_dict['autocommand'] = group['autocommand']
line = line[m.end():]
continue

# GOAL: match the 'password' option and return its parameters ('type' and 'password')
# Sample: "password 0 lab"
if m := password.match(line):
group = m.groupdict()
pass_dict = users_dict.setdefault('password', {})
pass_dict['type'] = int(group['type'])
pass_dict['password'] = group['password']
line = line[m.end():]
continue

# CLAIM: There is an unhandled argument.
log.warning(f"Unhandled argument in parser 'show running-config aaa username': {line}")
break

return ret_dict

Expand Down Expand Up @@ -5009,4 +5010,4 @@ def cli(self, output=None):
radius_server_dict.update({'dtls_trustpoint_server': m.groupdict()['dtls_trustpoint_server']})
continue

return ret_dict
return ret_dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
expected_output = {
"username": {
"testuser07": {
"nopassword": True,
"privilege": 3
},
"testuser08": {
"common_criteria_policy": "Test-CC",
"privilege": 15,
"secret": {
"secret": "$9$oNguEA9um9vRx.$MsDk0DOy1rzBjKAcySWdNjoKcA7GetG9YNnKOs8S67A",
"type": 9
}
},
"testuser09": {
"autocommand": "show ip bgp summary",
"privilege": 15,
"secret": {
"secret": "$9$UuxZCcqGu2IgBU$teHrzSPJK5FgLH0YAnUezoA1JwaqGBcJI4Xb6c3S7tU",
"type": 9
}
},
"testuser10": {
"common_criteria_policy": "Test-CC",
"password": {
"password": "lab",
"type": 0
},
"privilege": 15
},
"testuser11": {
"privilege": 15
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
9400-HA#show running-config aaa username
!
! You may also need to setup a common criteria policy for testing:
! aaa new-model
! aaa common-criteria policy Test-CC
! min-length 1
username testuser07 privilege 3 nopassword
username testuser08 privilege 15 common-criteria-policy Test-CC secret 9 $9$oNguEA9um9vRx.$MsDk0DOy1rzBjKAcySWdNjoKcA7GetG9YNnKOs8S67A
! Some usernames can span multiple lines:
username testuser09 privilege 15 secret 9 $9$UuxZCcqGu2IgBU$teHrzSPJK5FgLH0YAnUezoA1JwaqGBcJI4Xb6c3S7tU
username testuser09 autocommand show ip bgp summary
username testuser10 privilege 15 common-criteria-policy Test-CC password 0 lab
Comment on lines +7 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually interested in this format. Is this entered exactly as the user entered it, or is it rearranged to fit an underlying order. Does the command only allow options to be added in a particular order?

If the order is set in stone, then it's actually possible to write a regular expression that should be able to match anything. It gets a bit difficult if say, both of these are valid

username testuser10 privilege 15 common-criteria-policy Test-CC password 0 lab
username testuser10 common-criteria-policy Test-CC privilege 15 password 0 lab

But if only one is valid, then a regular express like this becomes possible

"^username (?P<username>\S+) (privilege (?P<privilege>\d+) )?(common-criteria-policy (?P<common_criteria_policy>\S+) )?(?P<flags>((one-time|nopassword) ?)* ?)?((?P<password_type>secret|password|autocommand) (((?P<type>\d+) (?P<password>\S+))|(?P<command>.*)))?$"

Admittedly, that does become a tad tedious to maintain. The re.search method or what you currently have might be better to work with

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took all of the provided text from the running-config of a virtual device.

Ordering for these commands does seem to be static. Whether or not it's the same order on all versions of IOS-XE is beyond me. I see your regex and I like where it's going. Consider something like this borked (but accepted) config:

    Router1(config)#do sh run | i username
    username testuser11 privilege 3 common-criteria-policy privilege nopassword
    username testuser11 autocommand 15 borked-command

In this case the 15 is a valid command - it's the session to resume... but borked-command would cause it to fail as session resumption doesn't take any arguments. I think the <type> <password> portion of your regex will greedily capture it. You'd need an even more complex regex (lookbehind?) to handle people doing silly things like this.

Alternatively your regex should work with some nesting. Either way the regex would become significantly more complex.

Also note the name privilege for the applied common criteria policy. Apparently you can put keywords as the name of the policy. I shudder to think that somewhere out there is a config with a policy named secret or password.

! username with privilege and no password can happen if SSH pubkey auth is used:
username testuser11 privilege 15