3 # Copyright (C) 2014 Cumulus Networks, Inc.
5 # This file is part of Frr.
7 # Frr is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by the
9 # Free Software Foundation; either version 2, or (at your option) any
12 # Frr is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Frr; see the file COPYING. If not, write to the Free
19 # Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
24 - reads a frr configuration text file
25 - reads frr's current running configuration via "vtysh -c 'show running'"
26 - compares the two configs and determines what commands to execute to
27 synchronize frr's running configuration with the configuation in the
40 from collections
import OrderedDict
41 from ipaddr
import IPv6Address
42 from pprint
import pformat
45 log
= logging
.getLogger(__name__
)
48 class VtyshMarkException(Exception):
52 class Context(object):
55 A Context object represents a section of frr configuration such as:
58 description swp3 -> r8's swp1
63 or a single line context object such as this:
69 def __init__(self
, keys
, lines
):
73 # Keep a dictionary of the lines, this is to make it easy to tell if a
74 # line exists in this Context
75 self
.dlines
= OrderedDict()
78 self
.dlines
[ligne
] = True
80 def add_lines(self
, lines
):
82 Add lines to specified context
85 self
.lines
.extend(lines
)
88 self
.dlines
[ligne
] = True
94 A frr configuration is stored in a Config object. A Config object
95 contains a dictionary of Context objects where the Context keys
96 ('router ospf' for example) are our dictionary key.
101 self
.contexts
= OrderedDict()
103 def load_from_file(self
, filename
):
105 Read configuration from specified file and slurp it into internal memory
106 The internal representation has been marked appropriately by passing it
107 through vtysh with the -m parameter
109 log
.info('Loading Config object from file %s', filename
)
112 file_output
= subprocess
.check_output(['/usr/bin/vtysh', '-m', '-f', filename
])
113 except subprocess
.CalledProcessError
as e
:
114 raise VtyshMarkException(str(e
))
116 for line
in file_output
.split('\n'):
119 qv6_line
= get_normalized_ipv6_line(line
)
120 self
.lines
.append(qv6_line
)
122 self
.lines
.append(line
)
126 def load_from_show_running(self
):
128 Read running configuration and slurp it into internal memory
129 The internal representation has been marked appropriately by passing it
130 through vtysh with the -m parameter
132 log
.info('Loading Config object from vtysh show running')
135 config_text
= subprocess
.check_output(
136 "/usr/bin/vtysh -c 'show run' | /usr/bin/tail -n +4 | /usr/bin/vtysh -m -f -",
138 except subprocess
.CalledProcessError
as e
:
139 raise VtyshMarkException(str(e
))
141 for line
in config_text
.split('\n'):
144 if (line
== 'Building configuration...' or
145 line
== 'Current configuration:' or
149 self
.lines
.append(line
)
155 Return the lines read in from the configuration
158 return '\n'.join(self
.lines
)
160 def get_contexts(self
):
162 Return the parsed context as strings for display, log etc.
165 for (_
, ctx
) in sorted(self
.contexts
.iteritems()):
166 print str(ctx
) + '\n'
168 def save_contexts(self
, key
, lines
):
170 Save the provided key and lines as a context
177 if tuple(key
) not in self
.contexts
:
178 ctx
= Context(tuple(key
), lines
)
179 self
.contexts
[tuple(key
)] = ctx
181 ctx
= self
.contexts
[tuple(key
)]
185 if tuple(key
) not in self
.contexts
:
186 ctx
= Context(tuple(key
), [])
187 self
.contexts
[tuple(key
)] = ctx
189 def load_contexts(self
):
191 Parse the configuration and create contexts for each appropriate block
194 current_context_lines
= []
198 The end of a context is flagged via the 'end' keyword:
207 bgp router-id 10.0.0.1
208 bgp log-neighbor-changes
209 no bgp default ipv4-unicast
210 neighbor EBGP peer-group
211 neighbor EBGP advertisement-interval 1
212 neighbor EBGP timers connect 10
213 neighbor 2001:40:1:4::6 remote-as 40
214 neighbor 2001:40:1:8::a remote-as 40
218 neighbor IBGPv6 activate
219 neighbor 2001:10::2 peer-group IBGPv6
220 neighbor 2001:10::3 peer-group IBGPv6
225 ospf router-id 10.0.0.1
226 log-adjacency-changes detail
227 timers throttle spf 0 50 5000
232 # The code assumes that its working on the output from the "vtysh -m"
233 # command. That provides the appropriate markers to signify end of
234 # a context. This routine uses that to build the contexts for the
237 # There are single line contexts such as "log file /media/node/zebra.log"
238 # and multi-line contexts such as "router ospf" and subcontexts
239 # within a context such as "address-family" within "router bgp"
240 # In each of these cases, the first line of the context becomes the
241 # key of the context. So "router bgp 10" is the key for the non-address
242 # family part of bgp, "router bgp 10, address-family ipv6 unicast" is
243 # the key for the subcontext and so on.
248 # the keywords that we know are single line contexts. bgp in this case
249 # is not the main router bgp block, but enabling multi-instance
250 oneline_ctx_keywords
= ("access-list ",
267 for line
in self
.lines
:
272 if line
.startswith('!') or line
.startswith('#'):
276 if new_ctx
is True and any(line
.startswith(keyword
) for keyword
in oneline_ctx_keywords
):
277 self
.save_contexts(ctx_keys
, current_context_lines
)
279 # Start a new context
282 current_context_lines
= []
284 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
285 self
.save_contexts(ctx_keys
, current_context_lines
)
289 self
.save_contexts(ctx_keys
, current_context_lines
)
290 log
.debug('LINE %-50s: exiting old context, %-50s', line
, ctx_keys
)
292 # Start a new context
296 current_context_lines
= []
298 elif line
== "exit-address-family" or line
== "exit":
299 # if this exit is for address-family ipv4 unicast, ignore the pop
301 self
.save_contexts(ctx_keys
, current_context_lines
)
303 # Start a new context
304 ctx_keys
= copy
.deepcopy(main_ctx_key
)
305 current_context_lines
= []
306 log
.debug('LINE %-50s: popping from subcontext to ctx%-50s', line
, ctx_keys
)
308 elif new_ctx
is True:
312 ctx_keys
= copy
.deepcopy(main_ctx_key
)
315 current_context_lines
= []
317 log
.debug('LINE %-50s: entering new context, %-50s', line
, ctx_keys
)
319 elif "address-family " in line
:
322 # Save old context first
323 self
.save_contexts(ctx_keys
, current_context_lines
)
324 current_context_lines
= []
325 main_ctx_key
= copy
.deepcopy(ctx_keys
)
326 log
.debug('LINE %-50s: entering sub-context, append to ctx_keys', line
)
328 if line
== "address-family ipv6":
329 ctx_keys
.append("address-family ipv6 unicast")
330 elif line
== "address-family ipv4":
331 ctx_keys
.append("address-family ipv4 unicast")
333 ctx_keys
.append(line
)
336 # Continuing in an existing context, add non-commented lines to it
337 current_context_lines
.append(line
)
338 log
.debug('LINE %-50s: append to current_context_lines, %-50s', line
, ctx_keys
)
340 # Save the context of the last one
341 self
.save_contexts(ctx_keys
, current_context_lines
)
344 def line_to_vtysh_conft(ctx_keys
, line
, delete
):
346 Return the vtysh command for the specified context line
355 for ctx_key
in ctx_keys
:
364 if line
.startswith('no '):
365 cmd
.append('%s' % line
[3:])
367 cmd
.append('no %s' % line
)
373 # If line is None then we are typically deleting an entire
374 # context ('no router ospf' for example)
379 # Only put the 'no' on the last sub-context
380 for ctx_key
in ctx_keys
:
383 if ctx_key
== ctx_keys
[-1]:
384 cmd
.append('no %s' % ctx_key
)
386 cmd
.append('%s' % ctx_key
)
388 for ctx_key
in ctx_keys
:
395 def line_for_vtysh_file(ctx_keys
, line
, delete
):
397 Return the command as it would appear in Frr.conf
402 for (i
, ctx_key
) in enumerate(ctx_keys
):
403 cmd
.append(' ' * i
+ ctx_key
)
406 indent
= len(ctx_keys
) * ' '
409 if line
.startswith('no '):
410 cmd
.append('%s%s' % (indent
, line
[3:]))
412 cmd
.append('%sno %s' % (indent
, line
))
415 cmd
.append(indent
+ line
)
417 # If line is None then we are typically deleting an entire
418 # context ('no router ospf' for example)
422 # Only put the 'no' on the last sub-context
423 for ctx_key
in ctx_keys
:
425 if ctx_key
== ctx_keys
[-1]:
426 cmd
.append('no %s' % ctx_key
)
428 cmd
.append('%s' % ctx_key
)
430 for ctx_key
in ctx_keys
:
433 return '\n' + '\n'.join(cmd
)
436 def get_normalized_ipv6_line(line
):
438 Return a normalized IPv6 line as produced by frr,
439 with all letters in lower case and trailing and leading
443 words
= line
.split(' ')
447 norm_word
= str(IPv6Address(word
)).lower()
452 norm_line
= norm_line
+ " " + norm_word
454 return norm_line
.strip()
457 def line_exist(lines
, target_ctx_keys
, target_line
):
458 for (ctx_keys
, line
) in lines
:
459 if ctx_keys
== target_ctx_keys
and line
== target_line
:
464 def ignore_delete_re_add_lines(lines_to_add
, lines_to_del
):
466 # Quite possibly the most confusing (while accurate) variable names in history
467 lines_to_add_to_del
= []
468 lines_to_del_to_del
= []
470 for (ctx_keys
, line
) in lines_to_del
:
473 if ctx_keys
[0].startswith('router bgp') and line
and line
.startswith('neighbor '):
475 BGP changed how it displays swpX peers that are part of peer-group. Older
476 versions of frr would display these on separate lines:
477 neighbor swp1 interface
478 neighbor swp1 peer-group FOO
480 but today we display via a single line
481 neighbor swp1 interface peer-group FOO
483 This change confuses frr-reload.py so check to see if we are deleting
484 neighbor swp1 interface peer-group FOO
487 neighbor swp1 interface
488 neighbor swp1 peer-group FOO
490 If so then chop the del line and the corresponding add lines
493 re_swpx_int_peergroup
= re
.search('neighbor (\S+) interface peer-group (\S+)', line
)
494 re_swpx_int_v6only_peergroup
= re
.search('neighbor (\S+) interface v6only peer-group (\S+)', line
)
496 if re_swpx_int_peergroup
or re_swpx_int_v6only_peergroup
:
497 swpx_interface
= None
498 swpx_peergroup
= None
500 if re_swpx_int_peergroup
:
501 swpx
= re_swpx_int_peergroup
.group(1)
502 peergroup
= re_swpx_int_peergroup
.group(2)
503 swpx_interface
= "neighbor %s interface" % swpx
504 elif re_swpx_int_v6only_peergroup
:
505 swpx
= re_swpx_int_v6only_peergroup
.group(1)
506 peergroup
= re_swpx_int_v6only_peergroup
.group(2)
507 swpx_interface
= "neighbor %s interface v6only" % swpx
509 swpx_peergroup
= "neighbor %s peer-group %s" % (swpx
, peergroup
)
510 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
511 found_add_swpx_peergroup
= line_exist(lines_to_add
, ctx_keys
, swpx_peergroup
)
512 tmp_ctx_keys
= tuple(list(ctx_keys
))
514 if not found_add_swpx_peergroup
:
515 tmp_ctx_keys
= list(ctx_keys
)
516 tmp_ctx_keys
.append('address-family ipv4 unicast')
517 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
518 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
520 if not found_add_swpx_peergroup
:
521 tmp_ctx_keys
= list(ctx_keys
)
522 tmp_ctx_keys
.append('address-family ipv6 unicast')
523 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
524 found_add_swpx_peergroup
= line_exist(lines_to_add
, tmp_ctx_keys
, swpx_peergroup
)
526 if found_add_swpx_interface
and found_add_swpx_peergroup
:
528 lines_to_del_to_del
.append((ctx_keys
, line
))
529 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
530 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_peergroup
))
533 In 3.0.1 we changed how we display neighbor interface command. Older
534 versions of frr would display the following:
535 neighbor swp1 interface
536 neighbor swp1 remote-as external
537 neighbor swp1 capability extended-nexthop
539 but today we display via a single line
540 neighbor swp1 interface remote-as external
542 and capability extended-nexthop is no longer needed because we
543 automatically enable it when the neighbor is of type interface.
545 This change confuses frr-reload.py so check to see if we are deleting
546 neighbor swp1 interface remote-as (external|internal|ASNUM)
549 neighbor swp1 interface
550 neighbor swp1 remote-as (external|internal|ASNUM)
551 neighbor swp1 capability extended-nexthop
553 If so then chop the del line and the corresponding add lines
555 re_swpx_int_remoteas
= re
.search('neighbor (\S+) interface remote-as (\S+)', line
)
556 re_swpx_int_v6only_remoteas
= re
.search('neighbor (\S+) interface v6only remote-as (\S+)', line
)
558 if re_swpx_int_remoteas
or re_swpx_int_v6only_remoteas
:
559 swpx_interface
= None
562 if re_swpx_int_remoteas
:
563 swpx
= re_swpx_int_remoteas
.group(1)
564 remoteas
= re_swpx_int_remoteas
.group(2)
565 swpx_interface
= "neighbor %s interface" % swpx
566 elif re_swpx_int_v6only_remoteas
:
567 swpx
= re_swpx_int_v6only_remoteas
.group(1)
568 remoteas
= re_swpx_int_v6only_remoteas
.group(2)
569 swpx_interface
= "neighbor %s interface v6only" % swpx
571 swpx_remoteas
= "neighbor %s remote-as %s" % (swpx
, remoteas
)
572 found_add_swpx_interface
= line_exist(lines_to_add
, ctx_keys
, swpx_interface
)
573 found_add_swpx_remoteas
= line_exist(lines_to_add
, ctx_keys
, swpx_remoteas
)
574 tmp_ctx_keys
= tuple(list(ctx_keys
))
576 if found_add_swpx_interface
and found_add_swpx_remoteas
:
578 lines_to_del_to_del
.append((ctx_keys
, line
))
579 lines_to_add_to_del
.append((ctx_keys
, swpx_interface
))
580 lines_to_add_to_del
.append((tmp_ctx_keys
, swpx_remoteas
))
583 found_add_line
= line_exist(lines_to_add
, ctx_keys
, line
)
586 lines_to_del_to_del
.append((ctx_keys
, line
))
587 lines_to_add_to_del
.append((ctx_keys
, line
))
590 We have commands that used to be displayed in the global part
591 of 'router bgp' that are now displayed under 'address-family ipv4 unicast'
595 neighbor ISL advertisement-interval 0
601 address-family ipv4 unicast
602 neighbor ISL advertisement-interval 0
604 Look to see if we are deleting it in one format just to add it back in the other
606 if ctx_keys
[0].startswith('router bgp') and len(ctx_keys
) > 1 and ctx_keys
[1] == 'address-family ipv4 unicast':
607 tmp_ctx_keys
= list(ctx_keys
)[:-1]
608 tmp_ctx_keys
= tuple(tmp_ctx_keys
)
610 found_add_line
= line_exist(lines_to_add
, tmp_ctx_keys
, line
)
613 lines_to_del_to_del
.append((ctx_keys
, line
))
614 lines_to_add_to_del
.append((tmp_ctx_keys
, line
))
616 for (ctx_keys
, line
) in lines_to_del_to_del
:
617 lines_to_del
.remove((ctx_keys
, line
))
619 for (ctx_keys
, line
) in lines_to_add_to_del
:
620 lines_to_add
.remove((ctx_keys
, line
))
622 return (lines_to_add
, lines_to_del
)
625 def compare_context_objects(newconf
, running
):
627 Create a context diff for the two specified contexts
630 # Compare the two Config objects to find the lines that we need to add/del
635 # Find contexts that are in newconf but not in running
636 # Find contexts that are in running but not in newconf
637 for (running_ctx_keys
, running_ctx
) in running
.contexts
.iteritems():
639 if running_ctx_keys
not in newconf
.contexts
:
641 # We check that the len is 1 here so that we only look at ('router bgp 10')
642 # and not ('router bgp 10', 'address-family ipv4 unicast'). The
643 # latter could cause a false delete_bgpd positive if ipv4 unicast is in
644 # running but not in newconf.
645 if "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) == 1:
647 lines_to_del
.append((running_ctx_keys
, None))
649 # If this is an address-family under 'router bgp' and we are already deleting the
650 # entire 'router bgp' context then ignore this sub-context
651 elif "router bgp" in running_ctx_keys
[0] and len(running_ctx_keys
) > 1 and delete_bgpd
:
655 elif running_ctx_keys
and not any("address-family" in key
for key
in running_ctx_keys
):
656 lines_to_del
.append((running_ctx_keys
, None))
660 for line
in running_ctx
.lines
:
661 lines_to_del
.append((running_ctx_keys
, line
))
663 # Find the lines within each context to add
664 # Find the lines within each context to del
665 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
667 if newconf_ctx_keys
in running
.contexts
:
668 running_ctx
= running
.contexts
[newconf_ctx_keys
]
670 for line
in newconf_ctx
.lines
:
671 if line
not in running_ctx
.dlines
:
672 lines_to_add
.append((newconf_ctx_keys
, line
))
674 for line
in running_ctx
.lines
:
675 if line
not in newconf_ctx
.dlines
:
676 lines_to_del
.append((newconf_ctx_keys
, line
))
678 for (newconf_ctx_keys
, newconf_ctx
) in newconf
.contexts
.iteritems():
680 if newconf_ctx_keys
not in running
.contexts
:
681 lines_to_add
.append((newconf_ctx_keys
, None))
683 for line
in newconf_ctx
.lines
:
684 lines_to_add
.append((newconf_ctx_keys
, line
))
686 (lines_to_add
, lines_to_del
) = ignore_delete_re_add_lines(lines_to_add
, lines_to_del
)
688 return (lines_to_add
, lines_to_del
)
690 if __name__
== '__main__':
691 # Command line options
692 parser
= argparse
.ArgumentParser(description
='Dynamically apply diff in frr configs')
693 parser
.add_argument('--input', help='Read running config from file instead of "show running"')
694 group
= parser
.add_mutually_exclusive_group(required
=True)
695 group
.add_argument('--reload', action
='store_true', help='Apply the deltas', default
=False)
696 group
.add_argument('--test', action
='store_true', help='Show the deltas', default
=False)
697 parser
.add_argument('--debug', action
='store_true', help='Enable debugs', default
=False)
698 parser
.add_argument('--stdout', action
='store_true', help='Log to STDOUT', default
=False)
699 parser
.add_argument('filename', help='Location of new frr config file')
700 args
= parser
.parse_args()
703 # For --test log to stdout
704 # For --reload log to /var/log/frr/frr-reload.log
705 if args
.test
or args
.stdout
:
706 logging
.basicConfig(level
=logging
.INFO
,
707 format
='%(asctime)s %(levelname)5s: %(message)s')
709 # Color the errors and warnings in red
710 logging
.addLevelName(logging
.ERROR
, "\033[91m %s\033[0m" % logging
.getLevelName(logging
.ERROR
))
711 logging
.addLevelName(logging
.WARNING
, "\033[91m%s\033[0m" % logging
.getLevelName(logging
.WARNING
))
714 if not os
.path
.isdir('/var/log/frr/'):
715 os
.makedirs('/var/log/frr/')
717 logging
.basicConfig(filename
='/var/log/frr/frr-reload.log',
719 format
='%(asctime)s %(levelname)5s: %(message)s')
721 # argparse should prevent this from happening but just to be safe...
723 raise Exception('Must specify --reload or --test')
724 log
= logging
.getLogger(__name__
)
726 # Verify the new config file is valid
727 if not os
.path
.isfile(args
.filename
):
728 print "Filename %s does not exist" % args
.filename
731 if not os
.path
.getsize(args
.filename
):
732 print "Filename %s is an empty file" % args
.filename
735 # Verify that 'service integrated-vtysh-config' is configured
736 vtysh_filename
= '/etc/frr/vtysh.conf'
737 service_integrated_vtysh_config
= True
739 if os
.path
.isfile(vtysh_filename
):
740 with
open(vtysh_filename
, 'r') as fh
:
741 for line
in fh
.readlines():
744 if line
== 'no service integrated-vtysh-config':
745 service_integrated_vtysh_config
= False
748 if not service_integrated_vtysh_config
:
749 print "'service integrated-vtysh-config' is not configured, this is required for 'service frr reload'"
753 log
.setLevel(logging
.DEBUG
)
755 log
.info('Called via "%s"', str(args
))
757 # Create a Config object from the config generated by newconf
759 newconf
.load_from_file(args
.filename
)
763 # Create a Config object from the running config
767 running
.load_from_file(args
.input)
769 running
.load_from_show_running()
771 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
772 lines_to_configure
= []
775 print "\nLines To Delete"
776 print "==============="
778 for (ctx_keys
, line
) in lines_to_del
:
783 cmd
= line_for_vtysh_file(ctx_keys
, line
, True)
784 lines_to_configure
.append(cmd
)
788 print "\nLines To Add"
791 for (ctx_keys
, line
) in lines_to_add
:
796 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
797 lines_to_configure
.append(cmd
)
802 log
.debug('New Frr Config\n%s', newconf
.get_lines())
804 # This looks a little odd but we have to do this twice...here is why
805 # If the user had this running bgp config:
808 # neighbor 1.1.1.1 remote-as 50
809 # neighbor 1.1.1.1 route-map FOO out
811 # and this config in the newconf config file
814 # neighbor 1.1.1.1 remote-as 999
815 # neighbor 1.1.1.1 route-map FOO out
818 # Then the script will do
819 # - no neighbor 1.1.1.1 remote-as 50
820 # - neighbor 1.1.1.1 remote-as 999
822 # The problem is the "no neighbor 1.1.1.1 remote-as 50" will also remove
823 # the "neighbor 1.1.1.1 route-map FOO out" line...so we compare the
824 # configs again to put this line back.
828 running
.load_from_show_running()
829 log
.debug('Running Frr Config (Pass #%d)\n%s', x
, running
.get_lines())
831 (lines_to_add
, lines_to_del
) = compare_context_objects(newconf
, running
)
834 for (ctx_keys
, line
) in lines_to_del
:
839 # 'no' commands are tricky, we can't just put them in a file and
840 # vtysh -f that file. See the next comment for an explanation
842 cmd
= line_to_vtysh_conft(ctx_keys
, line
, True)
845 # Some commands in frr are picky about taking a "no" of the entire line.
846 # OSPF is bad about this, you can't "no" the entire line, you have to "no"
847 # only the beginning. If we hit one of these command an exception will be
848 # thrown. Catch it and remove the last '-c', 'FOO' from cmd and try again.
851 # frr(config-if)# ip ospf authentication message-digest 1.1.1.1
852 # frr(config-if)# no ip ospf authentication message-digest 1.1.1.1
854 # frr(config-if)# no ip ospf authentication message-digest
856 # frr(config-if)# no ip ospf authentication
861 _
= subprocess
.check_output(cmd
)
863 except subprocess
.CalledProcessError
:
865 # - Pull the last entry from cmd (this would be
866 # 'no ip ospf authentication message-digest 1.1.1.1' in
868 # - Split that last entry by whitespace and drop the last word
869 log
.warning('Failed to execute %s', ' '.join(cmd
))
870 last_arg
= cmd
[-1].split(' ')
872 if len(last_arg
) <= 2:
873 log
.error('"%s" we failed to remove this command', original_cmd
)
876 new_last_arg
= last_arg
[0:-1]
877 cmd
[-1] = ' '.join(new_last_arg
)
879 log
.info('Executed "%s"', ' '.join(cmd
))
883 lines_to_configure
= []
885 for (ctx_keys
, line
) in lines_to_add
:
890 cmd
= line_for_vtysh_file(ctx_keys
, line
, False)
891 lines_to_configure
.append(cmd
)
893 if lines_to_configure
:
894 random_string
= ''.join(random
.SystemRandom().choice(
895 string
.ascii_uppercase
+
896 string
.digits
) for _
in range(6))
898 filename
= "/var/run/frr/reload-%s.txt" % random_string
899 log
.info("%s content\n%s" % (filename
, pformat(lines_to_configure
)))
901 with
open(filename
, 'w') as fh
:
902 for line
in lines_to_configure
:
903 fh
.write(line
+ '\n')
904 subprocess
.call(['/usr/bin/vtysh', '-f', filename
])
907 # Make these changes persistent
908 subprocess
.call(['/usr/bin/vtysh', '-c', 'write'])