From cbf75bb34c3ac5d836a212e13bcc173495a46e66 Mon Sep 17 00:00:00 2001 From: Marek Isalski Date: Sun, 16 Aug 2020 08:41:39 +0100 Subject: [PATCH] promote, demote, and migrate seem to work --- README.md | 2 + automonty | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 265 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c29bcfc..558cdce 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ loopbacks: - 46.227.207.255/32 default_ipv4_loopback: 46.227.207.255/32 + +routing_filter_chain: automonty ``` We have specified: diff --git a/automonty b/automonty index a534b7a..696cbc3 100755 --- a/automonty +++ b/automonty @@ -10,14 +10,17 @@ import dns.resolver, dns.reversename import ipaddress # comment format: -# hostname.example.com [aM flag1 flag2 key1=value1 key2=option3:value3,option4:value4] selector.example.com +# hostname.example.com [aM flag1 flag2 key1=value1 key2=option3:value3,option4:value4] selector.example.com # # flags: # static - do not auto-enable or auto-disable this address (loopbacks, linknets) # home - this is the normal home-location of the IP -# evacuated - this has been moved from this host to elsewhere -# evacuee - this is an evacuated address +# standby - this is an active interface, but not intended as primary route # teardown - being destroyed +# +# keys: +# evacuated=router - this address has been moved from this host to router +# evacuee=router - this is an evacuated address originally from router aM1 = re.compile( r"\[aM ?([^]]*)\]" ) def parse_comment( comment ): @@ -59,7 +62,7 @@ def make_comment( selector, rval, hostname ): rvalbits.append( k ) elif isinstance( v, dict ): rvalbits.append( "%s=%s" % ( k, ",".join( "%s:%s" % vi for vi in v.items() ) ) ) - else: + elif v: rvalbits.append( "%s=%s" % ( k, v ) ) if selector == hostname or selector is None: return "%s [%s]" % ( hostname, " ".join( rvalbits ) ) @@ -154,6 +157,11 @@ def monty_fixup( config, args, routers ): else: print( name, ":", item[ 'interface' ], new_comment ) ip_address.update( **{ 'comment': new_comment, '.id': item[ '.id' ] } ) + addr = ipaddress.ip_interface( item[ 'address' ] ) + if item[ 'address' ] in config[ 'loopbacks' ]: + addr = ipaddress.ip_interface( item[ 'network' ] ) + _adjust_filter( config, api, addr.network, hst, sel, home = rval.get( 'home', False ), standby = False, static = rval.get( 'static', False ), rval = rval ) + for item in ipv6_address: if item[ 'interface' ].startswith( "loop" ): continue @@ -177,6 +185,66 @@ def monty_fixup( config, args, routers ): else: print( name, ":", item[ 'interface' ], new_comment ) ipv6_address.update( **{ 'comment': new_comment, '.id': item[ '.id' ] } ) + addr = ipaddress.ip_interface( item[ 'address' ] ) + _adjust_filter( config, api, addr.network, hst, sel, home = rval.get( 'home', False ), standby = False, static = rval.get( 'static', False ), rval = rval ) + +def _adjust_filter( config, api, network, hostname, selector = None, home = None, standby = None, static = None, delete = False, rval = None, disabled = False ): + + def _make_filter_attrs( comment, disabled, home, static, standby ): + attrs = { 'action': 'accept', + 'comment': comment, + 'disabled': 'yes' if disabled else 'no', + } + if home or static: + attrs[ '!set-bgp-prepend' ] = '' + attrs[ 'set-bgp-local-pref' ] = 980 + elif standby: + attrs[ 'set-bgp-prepend' ] = 1 + attrs[ 'set-bgp-local-pref' ] = 970 + else: + attrs[ 'set-bgp-prepend' ] = 2 + attrs[ 'set-bgp-local-pref' ] = 960 + return attrs + + if isinstance( network, ( ipaddress.IPv4Interface, ipaddress.IPv6Interface ) ): + network = network.network + + filters = api.path( 'routing', 'filter' ) + found = False + if rval is None: + rval = {} + else: + _rval = rval + rval = {} + rval.update( _rval ) + if home is not None: + rval[ 'home' ] = home + if standby is not None: + rval[ 'standby' ] = standby + if static is not None: + rval[ 'static' ] = static + comment = make_comment( selector, rval, hostname ) + for f in filters: + if f[ 'chain' ] == config[ 'routing_filter_chain' ]: + match = str( network ) + if isinstance( network, ipaddress.IPv4Network ) and network.prefixlen == 32: + match = match.split( "/", 1 )[ 0 ] + elif isinstance( network, ipaddress.IPv6Network ) and network.prefixlen == 128: + match = match.split( "/", 1 )[ 0 ] + if f[ 'prefix' ] == match: + if delete: + filters.delete( f[ '.id' ] ) + else: + update = _make_filter_attrs( comment, disabled, home, static, standby ) + update[ '.id' ] = f[ '.id' ] + filters.update( **update ) + found = True + if not found and not delete: + create = _make_filter_attrs( comment, disabled, home, static, standby ) + create[ 'chain' ] = config[ 'routing_filter_chain' ] + create[ 'prefix' ] = str( network ) + filters.add( **create ) + return rval def monty_provision( config, args, routers ): reverse = {} @@ -200,6 +268,9 @@ def monty_provision( config, args, routers ): elif name in args.home: rval[ 'home' ] = True disabled = 'no' + elif name in args.standby: + rval[ 'standby' ] = True + disabled = 'no' else: disabled = 'yes' comment = make_comment( args.selector, rval, args.hostname ) @@ -218,6 +289,11 @@ def monty_provision( config, args, routers ): print( '/ipv6 address add interface="%s" address="%s" disabled=%s comment="%s" eui-64=yes advertise=yes' % ( vlan[ 'name' ], addr.network, disabled, comment ) ) else: + prefix = _extract_bgp_announcement( addr[ 'address' ], addr.get( 'network', None ) ) + _adjust_filter( config, api, prefix, args.hostname, + home = ( name in args.home ), + standby = ( name not in args.home ) and ( name in args.standby ), + selector = args.selector, rval = rval ) if isinstance( addr, ipaddress.IPv4Interface ): v4addr = { 'interface': vlan[ 'name' ], 'disabled': disabled, @@ -311,6 +387,158 @@ def monty_teardown( config, args, routers ): if args.dry_run: print() +def _extract_bgp_announcement( address, network = None ): + addr = ipaddress.ip_interface( address ) + if isinstance( addr, ipaddress.IPv4Interface ): + if addr.network.prefixlen == 32: + return ipaddress.ip_interface( network ) + return addr.network + elif isinstance( addr, ipaddress.IPv6Interface ): + return addr.network + +def monty_promote( config, args, routers ): + for new_home in args.new_home: + if new_home not in routers: + raise ValueError( 'cannot find new home "%s" in routers' % new_home ) + for standby in args.standby: + if standby not in routers: + raise ValueError( 'cannot find new standby "%s" in routers' % standby ) + + done_work = False + + for ( name, api ) in routers.items(): + vlans = api.path( 'interface', 'vlan' ) + ip_address = api.path( 'ip', 'address' ) + ipv6_address = api.path( 'ipv6', 'address' ) + + spare_vlans = {} + for vlan in vlans: + if vlan[ 'vlan-id' ] == args.vlan: + spare_vlans[ vlan[ 'name' ] ] = True + + for af in ( ip_address, ipv6_address ): + for addr in af: + ( sel, rval, hst ) = parse_comment( addr.get( 'comment', '' ) ) + if sel == args.hostname or hst == args.hostname: + if addr.get( 'disabled', False ): + if ( name in args.standby ) or ( name in args.new_home ): + address = _extract_bgp_announcement( addr[ 'address' ], addr.get( 'network', None ) ) + rval = _adjust_filter( config, api, address, hst, selector = sel, + home = ( name in args.new_home ), + standby = ( name not in args.new_home ) and ( name in args.standby ), + static = args.static, + rval = rval, + ) + new_comment = make_comment( sel, rval, hst ) + af.update( **{ '.id': addr[ '.id' ], + 'disabled': False, + 'comment': new_comment, + } ) + done_work = True + else: + if ( name in args.new_home ): + address = _extract_bgp_announcement( addr[ 'address' ], addr.get( 'network', None ) ) + rval = _adjust_filter( config, api, address, hst, selector = sel, + home = ( name in args.new_home ), + standby = False, + static = args.static, + rval = rval, + ) + new_comment = make_comment( sel, rval, hst ) + af.update( **{ '.id': addr[ '.id' ], + 'disabled': False, + 'comment': new_comment, + } ) + done_work = True + + spare_vlans.pop( addr[ 'interface' ], None ) + + for vlan in spare_vlans: + spare.append( ( name, ip_address, ipv6_address, vlan ) ) + + if args.create: + for ( name, ip_address, ipv6_address, vlan ) in spare: + rval = {} + disabled = 'yes' + if name in args.new_home: + rval[ 'home' ] = True + disabled = 'no' + elif name in args.standby: + disabled = 'no' + new_comment = make_comment( args.selector, rval, args.hostname ) + for addr in args.create: + rval = _adjust_filter( config, api, addr.network, args.hostname, selector = args.selector, + home = ( name in args.new_home ), + standby = ( name not in args.new_home ) and ( name in args.standby ), + static = args.static, + rval = rval, + ) + if isinstance( addr, ipnetwork.IPv4Interface ): + print( "creating address on interface" ) + v4addr = { 'interface': vlan, + 'disabled': disabled, + 'comment': new_comment, + } + if addr.network.prefixlen == 32: + v4addr[ 'address' ] = config[ 'default_ipv4_loopback' ] + v4addr[ 'network' ] = str( addr.network.network_address ) + else: + v4addr[ 'address' ] = str( addr ) + ip_address.create( **v4addr ) + done_work = True + elif isinstance( addr, ipnetwork.IPv6Interface ): + print( "creating address on interface" ) + v6addr = { 'interface': vlan, + 'disabled': disabled, + 'comment': comment, + } + if args.static: + v6addr[ 'address' ] = str( addr ) + else: + v6addr[ 'address' ] = str( addr.network ) + v6addr[ 'eui-64' ] = 'yes' + v6addr[ 'advertise' ] = 'yes' + ipv6_address.create( **v6addr ) + done_work = True + return done_work + +def monty_demote( config, args, routers ): + for disable in args.disable: + if disable not in routers: + raise ValueError( 'cannot find disable "%s" in routers' % disable ) + for standby in args.standby: + if standby not in routers: + raise ValueError( 'cannot find standby "%s" in routers' % standby ) + + done_work = False + + for ( name, api ) in routers.items(): + vlans = api.path( 'interface', 'vlan' ) + ip_address = api.path( 'ip', 'address' ) + ipv6_address = api.path( 'ipv6', 'address' ) + + for af in ( ip_address, ipv6_address ): + for addr in af: + ( sel, rval, hst ) = parse_comment( addr.get( 'comment', '' ) ) + if sel == args.hostname or hst == args.hostname: + if ( name in args.standby ) or ( name in args.disable ): + address = _extract_bgp_announcement( addr[ 'address' ], addr.get( 'network', None ) ) + rval = _adjust_filter( config, api, address, hst, selector = sel, + home = False, + standby = ( name in args.standby ), + rval = rval, + ) + new_comment = make_comment( sel, rval, hst ) + af.update( **{ '.id': addr[ '.id' ], + 'disabled': ( name in args.disable ), + 'comment': new_comment, + } ) + done_work = True + +def monty_migrate( config, args, routers ): + if monty_promote( config, args, routers ): + monty_demote( config, args, routers ) + def load_configuration(): try: config = open( os.path.expanduser( '~/.automonty.yaml' ), 'r' ) @@ -339,6 +567,7 @@ def main(): parser_provision = subparsers.add_parser( 'provision' ) parser_provision.add_argument( '--home', action = 'append', default = [] ) + parser_provision.add_argument( '--standby', action = 'append', default = [] ) parser_provision.add_argument( '-s', '--static', action = 'store_true', default = False ) parser_provision.add_argument( '--selector', type = str, default = None ) parser_provision.add_argument( 'hostname' ) @@ -346,6 +575,36 @@ def main(): parser_provision.add_argument( 'address', type = ipaddress.ip_interface, nargs = "+" ) parser_provision.set_defaults( func = monty_provision ) + parser_promote = subparsers.add_parser( 'promote' ) + parser_promote.add_argument( '-e', '--enable', action = 'store_true', default = False ) + parser_promote.add_argument( '-c', '--create', type = ipaddress.ip_interface, default = [], action = 'append' ) + parser_promote.add_argument( '-s', '--static', action = 'store_true', default = False ) + parser_promote.add_argument( '--standby', action = 'append', default = [] ) + parser_promote.add_argument( '--selector', type = str, default = None ) + parser_promote.add_argument( 'hostname' ) + parser_promote.add_argument( 'vlan', type = int ) + parser_promote.add_argument( 'new_home', nargs = "*" ) + parser_promote.set_defaults( func = monty_promote ) + + parser_demote = subparsers.add_parser( 'demote' ) + parser_demote.add_argument( '--standby', action = 'append', default = [] ) + parser_demote.add_argument( 'hostname' ) + parser_demote.add_argument( 'vlan', type = int ) + parser_demote.add_argument( 'disable', nargs = "*" ) + parser_demote.set_defaults( func = monty_demote ) + + parser_migrate = subparsers.add_parser( 'migrate' ) + parser_migrate.add_argument( '-e', '--enable', action = 'store_true', default = False ) + parser_migrate.add_argument( '-c', '--create', type = ipaddress.ip_interface, default = [], action = 'append' ) + parser_migrate.add_argument( '-s', '--static', action = 'store_true', default = False ) + parser_migrate.add_argument( '--selector', type = str, default = None ) + parser_migrate.add_argument( '--standby', action = 'append', default = [] ) + parser_migrate.add_argument( '--disable', action = 'append', default = [] ) + parser_migrate.add_argument( 'hostname' ) + parser_migrate.add_argument( 'vlan', type = int ) + parser_migrate.add_argument( 'new_home', nargs = "*" ) + parser_migrate.set_defaults( func = monty_migrate ) + parser_teardown = subparsers.add_parser( 'teardown' ) parser_teardown.add_argument( '--delete', action = 'store_true', default = False ) parser_teardown.add_argument( 'hostname' )