Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI support for PAC #3265

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
25 changes: 25 additions & 0 deletions clear/main.py
Original file line number Diff line number Diff line change
@@ -208,6 +208,31 @@ def dropcounters():
command = ["dropstat", "-c", "clear"]
run_command(command)

#
# 'authentication' command ###
#

@cli.group()
def authentication():
"""Clear authentication sessions"""
pass


@authentication.command('sessions')
@click.option('-i', '--interface')
@click.option('-m', '--mac')
def authentication_clear(interface, mac):
if interface is None and mac is None:
command = ['pacclear', '-a']
run_command(command)
elif mac is not None:
command = ['pacclear', '-m {}'.format(mac)]
run_command(command)
elif interface is not None:
command = ['pacclear', '-i {}'.format(interface)]
run_command(command)


@cli.command()
def tunnelcounters():
"""Clear Tunnel counters"""
285 changes: 285 additions & 0 deletions config/main.py
Original file line number Diff line number Diff line change
@@ -64,6 +64,7 @@
from . import switchport
from . import dns
from . import bgp_cli
from . import pac


# mock masic APIs for unit test
@@ -1412,6 +1413,7 @@ def config(ctx):
config.add_command(nat.nat)
config.add_command(vlan.vlan)
config.add_command(vxlan.vxlan)
config.add_command(pac.dot1x)

#add mclag commands
config.add_command(mclag.mclag)
@@ -6854,6 +6856,289 @@ def del_interface_storm(ctx,port_name,storm_type, namespace):
if storm_control_delete_entry(port_name, storm_type) is False:
ctx.fail("Unable to delete {} storm-control from interface {}".format(storm_type, port_name))


#
# 'authentication' group ('config interface authentication ...')
#
@interface.group('authentication')
@click.pass_context
def authentication(ctx):
"""PAC authentication-related configuration tasks"""
pass

#
# 'authentication port-control' command ('config interface authentication port-control <interface> <control_mode>')
#
@authentication.command('port-control')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('control_mode', metavar='<control_mode>', required=True, type=click.Choice(['auto', 'force-authorized', 'force-unauthorized']))
def add_portcontrol(ctx, control_mode, interface_name):
"""Add port-control configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

try:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'port_control_mode': control_mode })

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))


#
# 'authentication host-mode' command ('config interface authentication host-mode <interface> <host_mode>')
#
@authentication.command('host-mode')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('host_mode', metavar='<host_mode>', required=True, type=click.Choice(['single-host', 'multi-host', 'multi-auth']))
def add_hostmode(ctx, host_mode, interface_name):
"""Add host-mode configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

try:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'host_control_mode': host_mode })

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))

#
# 'authentication max-users' command ('config interface authentication max-users <interface> <max_users>')
#
@authentication.command('max-users')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('max_users', metavar='<host_mode>', required=True, type=int)
def add_maxusers(ctx, max_users, interface_name):
"""Add max-users configutation"""
if max_users not in range(1, 16):
ctx.fail("Given max_users {} is not allowed. Please enter a valid range <1-16>!!".format(max_users))

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

try:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'max_users_per_port': max_users })

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))

#
# 'authentication periodic' command ('config interface authentication periodic <interface> <status>')
#
@authentication.command('periodic')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('status', metavar='<status>', required=True, type=click.Choice(['enable', 'disable']))
def add_periodic(ctx, status, interface_name):
"""Add periodic authentication configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

try:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'reauth_enable': True if status == 'enable' else False })

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))

#
# 'authentication reauth-period' command ('config interface authentication reauth-period <interface> <server|reauth_period>')
#
@authentication.command('reauth-period')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('status', metavar='<reauth_period>', required=True)
def add_reauthperiod(ctx, status, interface_name):
"""Add reauth-period configutation"""


config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()
is_server = False
val = 0
if status == 'server':
is_server = True
else:
val = int(status)
if val not in range(1,65535):

Choose a reason for hiding this comment

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

Any reason why we have chose this upper and lower bound for range?

In most NOS i see the range as "60 to 7200"

1 --> would mean there would too frequent authentication request, this would cause load on radius server depending on the deployment/scale.
65535 --> conversely this range seems to high.

Also is there there a way to disable "reauth timer" ?

ctx.fail("Input not in range <1-65535>")
try:
if is_server:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'reauth_period_from_server': True})
else:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'reauth_period': val})
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'reauth_period_from_server': False})


except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))


#
# 'authentication order' command ('config interface authentication order <interface> <order_list>')
#
@authentication.command('order')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('order_list', metavar='<order_list>', required=True, type=click.Choice(['dot1x,mab', 'mab,dot1x', 'dot1x', 'mab']))
def add_order(ctx, order_list, interface_name):
"""Add order configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

order_list.split()
try:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'method_list@': order_list })

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))


#
# 'authentication priority' command ('config interface authentication priority <interface> <priority_list>')
#
@authentication.command('priority')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('priority_list', metavar='<priority_list>', required=True, type=click.Choice(['dot1x,mab', 'mab,dot1x', 'dot1x', 'mab']))
def add_priority(ctx, priority_list, interface_name):
"""Add priority configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

priority_list.split()
try:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'priority_list@': priority_list })

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))

#
# 'mab' group ('config interface mab ...')
#
@interface.group('mab')
@click.pass_context
def mab(ctx):
"""Mac auth bypass authentication-related configuration tasks"""
pass



#
# 'mab' command ('config interface mab <interface> <enable|disable> [eap-md5|pap|chap]')
#
@interface.command('mab')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('status', metavar='<status>', required=True, type=click.Choice(['enable', 'disable']))
@click.option('-a', '--auth-type', required=False, type=click.Choice(['eap-md5', 'pap', 'chap']))
def add_mab(ctx, status, auth_type, interface_name):
"""Add mab configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

print(status)
print(auth_type)
print(interface_name)
try:
config_db.mod_entry('MAB_PORT_CONFIG_TABLE', (interface_name), {'mab_enable': True if status == 'enable' else False})

Choose a reason for hiding this comment

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

When dot1x/mab is enabled on an interface, do we need to check for static MAC if it exists on that port and error out? Alternatively once PAC is enabled on a port, we should restrict any static MAC add on that port.

if auth_type == 'pap' or auth_type == 'chap':
config_db.mod_entry('MAB_PORT_CONFIG_TABLE', (interface_name), {'mab_auth_type': auth_type})
else:
config_db.mod_entry('MAB_PORT_CONFIG_TABLE', (interface_name), {'mab_auth_type': 'eap-md5'})

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))


def pac_is_port_any_vlan_member(config_db, port):

Choose a reason for hiding this comment

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

While a port is dot1X enabled, it should not be allowed to become a router-port. In other words, it should not be removed from all the vlans right. Is that taken care as part of seperate PR?

"""Check if port is a member of any vlan"""

vlan_ports_data = config_db.get_table('VLAN_MEMBER')
for key in vlan_ports_data:
if key[1] == port:
return True
Copy link
Contributor

Choose a reason for hiding this comment

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

Now We also have notation of switchport (access, trunk), what will be behavior of this. Will it work for both ports? Please revisit this as well.


return False


def pac_interface_name_valid(ctx, config_db, interface_name):
"""Check if the given interface is valid for pac config"""

valid_interface = False
if interface_name.startswith("Ethernet"):

valid_interface = True
if valid_interface and clicommon.is_port_router_interface(config_db, interface_name):
ctx.fail("{} is a router interface!".format(interface_name))
valid_interface = False

if valid_interface and clicommon.is_port_mirror_dst_port(config_db, interface_name):
ctx.fail("{} is configured as mirror destination port".format(interface_name))
valid_interface = False

portchannel_member_table = config_db.get_table('PORTCHANNEL_MEMBER')

if valid_interface and clicommon.interface_is_in_portchannel(portchannel_member_table, interface_name):
ctx.fail("{} is part of portchannel!".format(interface_name))
valid_interface = False

if valid_interface and (pac_is_port_any_vlan_member(config_db, interface_name) is False):
ctx.fail("{} is not a member of any vlan".format(interface_name))
valid_interface = False

if valid_interface:
return True
else:
return False



#
# 'dot1x' group ('config interface dot1x ...')
#
@interface.group('dot1x')
@click.pass_context
def dot1x(ctx):
"""PAC authentication-related configuration tasks"""
pass



#
# 'interface dot1x pae' command ('config interface dot1x pae <interface> <authenticator|none>')
#
@dot1x.command('pae')
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('role', metavar='<role>', required=True, type=click.Choice(['authenticator', 'none']))
def add_pae(ctx, role, interface_name):
"""Add pae configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

if role == 'authenticator' and pac_interface_name_valid(ctx, config_db, interface_name) is False:
ctx.fail("Given interface {} is not allowed. Please enter a valid interface for pac!!".format(interface_name))

try:
config_db.mod_entry('PAC_PORT_CONFIG_TABLE', (interface_name), {'port_pae_role': role})

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))






def is_loopback_name_valid(loopback_name):
"""Loopback name validation
"""
37 changes: 37 additions & 0 deletions config/pac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import click
import utilities_common.cli as clicommon
from jsonpatch import JsonPatchConflict
from jsonpointer import JsonPointerException
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector
from .validated_config_db_connector import ValidatedConfigDBConnector


############### PAC Configuration ##################
#
# 'dot1x' group ('config dot1x ...')
#
@click.group('dot1x')
def dot1x():
"""802.1x authentication-related configuration tasks"""
pass


#
# 'dot1x system-auth-control' command ('config dot1x system-auth-control <status> ')
#
@dot1x.command('system-auth-control')
@click.pass_context
@click.argument('status', metavar='<status>', required=True, type=click.Choice(['enable', 'disable']))
def add_system_auth_control(ctx, status):
"""Add dot1x system-auth-control related configutation"""

config_db = ValidatedConfigDBConnector(ConfigDBConnector())
config_db.connect()

try:
config_db.mod_entry('HOSTAPD_GLOBAL_CONFIG_TABLE', ('GLOBAL'), {'dot1x_system_auth_control': "true" if status == 'enable' else "false"})

except ValueError as e:
ctx.fail("Invalid ConfigDB. Error: {}".format(e))


Loading