vpp_config: Rework for Python2/3 compatibility.
[vpp.git] / extras / vpp_config / vpp_config.py
1 #!/usr/bin/env python
2
3 # Copyright (c) 2016 Cisco and/or its affiliates.
4 # Copyright (c) 2018 Vinci Consulting Corp.  All rights reserved.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at:
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 """VPP Configuration Main Entry"""
18 from __future__ import absolute_import, division, print_function
19
20 import re
21 import os
22 import sys
23 import logging
24 import argparse
25
26 from vpplib.AutoConfig import AutoConfig
27 from vpplib.VPPUtil import VPPUtil
28
29 #  Python2/3 compatible
30 try:
31     input = raw_input  # noqa
32 except NameError:
33     pass
34
35 VPP_DRYRUNDIR = '/vpp/vpp-config/dryrun'
36 VPP_AUTO_CONFIGURATION_FILE = '/vpp/vpp-config/configs/auto-config.yaml'
37 VPP_HUGE_PAGE_FILE = '/vpp/vpp-config/dryrun/sysctl.d/80-vpp.conf'
38 VPP_STARTUP_FILE = '/vpp/vpp-config/dryrun/vpp/startup.conf'
39 VPP_GRUB_FILE = '/vpp/vpp-config/dryrun/default/grub'
40 VPP_REAL_HUGE_PAGE_FILE = '/etc/sysctl.d/80-vpp.conf'
41 VPP_REAL_STARTUP_FILE = '/etc/vpp/startup.conf'
42 VPP_REAL_GRUB_FILE = '/etc/default/grub'
43
44 rootdir = ''
45
46
47 def autoconfig_yn(question, default):
48     """
49     Ask the user a yes or no question.
50
51     :param question: The text of the question
52     :param default: Value to be returned if '\n' is entered
53     :type question: string
54     :type default: string
55     :returns: The Answer
56     :rtype: string
57     """
58     input_valid = False
59     default = default.lower()
60     answer = ''
61     while not input_valid:
62         answer = input(question)
63         if len(answer) == 0:
64             answer = default
65         if re.findall(r'[YyNn]', answer):
66             input_valid = True
67             answer = answer[0].lower()
68         else:
69             print ("Please answer Y, N or Return.")
70
71     return answer
72
73
74 def autoconfig_cp(node, src, dst):
75     """
76     Copies a file, saving the original if needed.
77
78     :param node: Node dictionary with cpuinfo.
79     :param src: Source File
80     :param dst: Destination file
81     :type node: dict
82     :type src: string
83     :type dst: string
84     :raises RuntimeError: If command fails
85     """
86
87     # If the destination file exist, create a copy if one does not already
88     # exist
89     ofile = dst + '.orig'
90     (ret, stdout, stderr) = VPPUtil.exec_command('ls {}'.format(dst))
91     if ret == 0:
92         cmd = 'cp {} {}'.format(dst, ofile)
93         (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
94         if ret != 0:
95             raise RuntimeError('{} failed on node {} {} {}'.
96                                format(cmd,
97                                       node['host'],
98                                       stdout,
99                                       stderr))
100
101     # Copy the source file
102     cmd = 'cp {} {}'.format(src, os.path.dirname(dst))
103     (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
104     if ret != 0:
105         raise RuntimeError('{} failed on node {} {}'.
106                            format(cmd, node['host'], stderr))
107
108
109 def autoconfig_diff(node, src, dst):
110     """
111     Returns the diffs of 2 files.
112
113     :param node: Node dictionary with cpuinfo.
114     :param src: Source File
115     :param dst: Destination file
116     :type node: dict
117     :type src: string
118     :type dst: string
119     :returns: The Answer
120     :rtype: string
121     :raises RuntimeError: If command fails
122     """
123
124     # Diff the files and return the output
125     cmd = "diff {} {}".format(src, dst)
126     (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
127     if stderr != '':
128         raise RuntimeError('{} failed on node {} {} {}'.
129                            format(cmd,
130                                   node['host'],
131                                   ret,
132                                   stderr))
133
134     return stdout
135
136
137 def autoconfig_show_system():
138     """
139     Shows the system information.
140
141     """
142
143     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
144
145     acfg.discover()
146
147     acfg.sys_info()
148
149
150 def autoconfig_hugepage_apply(node, ask_questions=True):
151     """
152     Apply the huge page configuration.
153     :param node: The node structure
154     :type node: dict
155     :param ask_questions: When True ask the user questions
156     :type ask_questions: bool
157     :returns: -1 if the caller should return, 0 if not
158     :rtype: int
159
160     """
161
162     diffs = autoconfig_diff(node, VPP_REAL_HUGE_PAGE_FILE, rootdir + VPP_HUGE_PAGE_FILE)
163     if diffs != '':
164         print ("These are the changes we will apply to")
165         print ("the huge page file ({}).\n".format(VPP_REAL_HUGE_PAGE_FILE))
166         print (diffs)
167         if ask_questions:
168             answer = autoconfig_yn("\nAre you sure you want to apply these changes [Y/n]? ", 'y')
169             if answer == 'n':
170                 return -1
171
172         # Copy and sysctl
173         autoconfig_cp(node, rootdir + VPP_HUGE_PAGE_FILE, VPP_REAL_HUGE_PAGE_FILE)
174         cmd = "sysctl -p {}".format(VPP_REAL_HUGE_PAGE_FILE)
175         (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
176         if ret != 0:
177             raise RuntimeError('{} failed on node {} {} {}'.
178                                format(cmd, node['host'], stdout, stderr))
179     else:
180         print ('\nThere are no changes to the huge page configuration.')
181
182     return 0
183
184
185 def autoconfig_vpp_apply(node, ask_questions=True):
186     """
187     Apply the vpp configuration.
188
189     :param node: The node structure
190     :type node: dict
191     :param ask_questions: When True ask the user questions
192     :type ask_questions: bool
193     :returns: -1 if the caller should return, 0 if not
194     :rtype: int
195
196     """
197
198     diffs = autoconfig_diff(node, VPP_REAL_STARTUP_FILE, rootdir + VPP_STARTUP_FILE)
199     if diffs != '':
200         print ("These are the changes we will apply to")
201         print ("the VPP startup file ({}).\n".format(VPP_REAL_STARTUP_FILE))
202         print (diffs)
203         if ask_questions:
204             answer = autoconfig_yn("\nAre you sure you want to apply these changes [Y/n]? ", 'y')
205             if answer == 'n':
206                 return -1
207
208         # Copy the VPP startup
209         autoconfig_cp(node, rootdir + VPP_STARTUP_FILE, VPP_REAL_STARTUP_FILE)
210     else:
211         print ('\nThere are no changes to VPP startup.')
212
213     return 0
214
215
216 def autoconfig_grub_apply(node, ask_questions=True):
217     """
218     Apply the grub configuration.
219
220     :param node: The node structure
221     :type node: dict
222     :param ask_questions: When True ask the user questions
223     :type ask_questions: bool
224     :returns: -1 if the caller should return, 0 if not
225     :rtype: int
226
227     """
228
229     print ("\nThe configured grub cmdline looks like this:")
230     configured_cmdline = node['grub']['default_cmdline']
231     current_cmdline = node['grub']['current_cmdline']
232     print (configured_cmdline)
233     print ("\nThe current boot cmdline looks like this:")
234     print (current_cmdline)
235     if ask_questions:
236         question = "\nDo you want to keep the current boot cmdline [Y/n]? "
237         answer = autoconfig_yn(question, 'y')
238         if answer == 'y':
239             return
240
241     node['grub']['keep_cmdline'] = False
242
243     # Diff the file
244     diffs = autoconfig_diff(node, VPP_REAL_GRUB_FILE, rootdir + VPP_GRUB_FILE)
245     if diffs != '':
246         print ("These are the changes we will apply to")
247         print ("the GRUB file ({}).\n".format(VPP_REAL_GRUB_FILE))
248         print (diffs)
249         if ask_questions:
250             answer = autoconfig_yn("\nAre you sure you want to apply these changes [y/N]? ", 'n')
251             if answer == 'n':
252                 return -1
253
254         # Copy and update grub
255         autoconfig_cp(node, rootdir + VPP_GRUB_FILE, VPP_REAL_GRUB_FILE)
256         distro = VPPUtil.get_linux_distro()
257         if distro[0] == 'Ubuntu':
258             cmd = "update-grub"
259         else:
260             cmd = "grub2-mkconfig -o /boot/grub2/grub.cfg"
261
262         (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
263         if ret != 0:
264             raise RuntimeError('{} failed on node {} {} {}'.
265                                format(cmd, node['host'], stdout, stderr))
266
267         print ("There have been changes to the GRUB config a", end=' ')
268         print ("reboot will be required.")
269         return -1
270     else:
271         print ('\nThere are no changes to the GRUB config.')
272
273     return 0
274
275
276 def autoconfig_apply(ask_questions=True):
277     """
278     Apply the configuration.
279
280     Show the diff of the dryrun file and the actual configuration file
281     Copy the files from the dryrun directory to the actual file.
282     Peform the system function
283
284     :param ask_questions: When true ask the user questions
285     :type ask_questions: bool
286
287     """
288
289     vutil = VPPUtil()
290     pkgs = vutil.get_installed_vpp_pkgs()
291     if len(pkgs) == 0:
292         print ("\nVPP is not installed, Install VPP with option 4.")
293         return
294
295     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
296
297     if ask_questions:
298         print ("\nWe are now going to configure your system(s).\n")
299         answer = autoconfig_yn("Are you sure you want to do this [Y/n]? ", 'y')
300         if answer == 'n':
301             return
302
303     nodes = acfg.get_nodes()
304     for i in nodes.items():
305         node = i[1]
306
307         # Check the system resources
308         if not acfg.min_system_resources(node):
309             return
310
311         # Stop VPP
312         VPPUtil.stop(node)
313
314         # Huge Pages
315         ret = autoconfig_hugepage_apply(node, ask_questions)
316         if ret != 0:
317             return
318
319         # VPP
320         ret = autoconfig_vpp_apply(node, ask_questions)
321         if ret != 0:
322             return
323
324         # Grub
325         ret = autoconfig_grub_apply(node, ask_questions)
326         if ret != 0:
327             # We can still start VPP, even if we haven't configured grub
328             VPPUtil.start(node)
329             return
330
331         # Everything is configured start vpp
332         VPPUtil.start(node)
333
334
335 def autoconfig_dryrun(ask_questions=True):
336     """
337     Execute the dryrun function.
338
339     :param ask_questions: When true ask the user for paraameters
340     :type ask_questions: bool
341
342     """
343
344     vutil = VPPUtil()
345     pkgs = vutil.get_installed_vpp_pkgs()
346     if len(pkgs) == 0:
347         print ("\nVPP is not installed, please install VPP.")
348         return
349
350     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE, clean=True)
351
352     # Stop VPP on each node
353     nodes = acfg.get_nodes()
354     for i in nodes.items():
355         node = i[1]
356         VPPUtil.stop(node)
357
358     # Discover
359     acfg.discover()
360
361     # Check the system resources
362     nodes = acfg.get_nodes()
363     for i in nodes.items():
364         node = i[1]
365         if not acfg.min_system_resources(node):
366             return
367
368     # Modify the devices
369     if ask_questions:
370         acfg.modify_devices()
371     else:
372         acfg.update_interfaces_config()
373
374     # Modify CPU
375     acfg.modify_cpu(ask_questions)
376
377     # Calculate the cpu parameters
378     acfg.calculate_cpu_parameters()
379
380     # Acquire TCP stack parameters
381     if ask_questions:
382         acfg.acquire_tcp_params()
383
384     # Apply the startup
385     acfg.apply_vpp_startup()
386
387     # Apply the grub configuration
388     acfg.apply_grub_cmdline()
389
390     # Huge Pages
391     if ask_questions:
392         acfg.modify_huge_pages()
393     acfg.apply_huge_pages()
394
395
396 def autoconfig_install():
397     """
398     Install or Uninstall VPP.
399
400     """
401
402     # Since these commands will take a while, we
403     # want to see the progress
404     logger = logging.getLogger()
405
406     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
407     vutil = VPPUtil()
408
409     nodes = acfg.get_nodes()
410     for i in nodes.items():
411         node = i[1]
412
413         pkgs = vutil.get_installed_vpp_pkgs()
414
415         if len(pkgs) > 0:
416             print ("\nThese packages are installed on node {}"
417                    .format(node['host']))
418             print ("{:25} {}".format("Name", "Version"))
419             for pkg in pkgs:
420                 try:
421                     print ("{:25} {}".format(
422                         pkg['name'], pkg['version']))
423                 except KeyError:
424                     print ("{}".format(pkg['name']))
425
426             question = "\nDo you want to uninstall these "
427             question += "packages [y/N]? "
428             answer = autoconfig_yn(question, 'n')
429             if answer == 'y':
430                 logger.setLevel(logging.INFO)
431                 vutil.uninstall_vpp(node)
432         else:
433             print ("\nThere are no VPP packages on node {}."
434                    .format(node['host']))
435             question = "Do you want to install VPP [Y/n]? "
436             answer = autoconfig_yn(question, 'y')
437             if answer == 'y':
438                 question = "Do you want to install the release version [Y/n]? "
439                 answer = autoconfig_yn(question, 'y')
440                 if answer == 'y':
441                     branch = 'release'
442                 else:
443                     branch = 'master'
444                 logger.setLevel(logging.INFO)
445                 vutil.install_vpp(node, branch)
446
447     # Set the logging level back
448     logger.setLevel(logging.ERROR)
449
450
451 def autoconfig_patch_qemu():
452     """
453     Patch the correct qemu version that is needed for openstack
454
455     """
456
457     # Since these commands will take a while, we
458     # want to see the progress
459     logger = logging.getLogger()
460
461     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
462
463     nodes = acfg.get_nodes()
464     for i in nodes.items():
465         node = i[1]
466
467         logger.setLevel(logging.INFO)
468         acfg.patch_qemu(node)
469
470
471 def autoconfig_ipv4_setup():
472     """
473     Setup IPv4 interfaces
474
475     """
476
477     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
478     acfg.ipv4_interface_setup()
479
480
481 def autoconfig_create_iperf_vm():
482     """
483     Setup IPv4 interfaces
484
485     """
486
487     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
488     acfg.destroy_iperf_vm('iperf-server')
489     acfg.create_and_bridge_iperf_virtual_interface()
490     acfg.create_iperf_vm('iperf-server')
491
492
493 def autoconfig_not_implemented():
494     """
495     This feature is not implemented
496
497     """
498
499     print ("\nThis Feature is not implemented yet....")
500
501
502 def autoconfig_basic_test_menu():
503     """
504     The auto configuration basic test menu
505
506     """
507
508     basic_menu_text = '\nWhat would you like to do?\n\n\
509 1) List/Create Simple IPv4 Setup\n\
510 2) Create an iperf VM and Connect to VPP an interface\n\
511 9 or q) Back to main menu.'
512
513     print ("{}".format(basic_menu_text))
514
515     input_valid = False
516     answer = ''
517     while not input_valid:
518         answer = input("\nCommand: ")
519         if len(answer) > 1:
520             print ("Please enter only 1 character.")
521             continue
522         if re.findall(r'[Qq1-29]', answer):
523             input_valid = True
524             answer = answer[0].lower()
525         else:
526             print ("Please enter a character between 1 and 2 or 9.")
527
528         if answer == '9':
529             answer = 'q'
530
531     return answer
532
533
534 def autoconfig_basic_test():
535     """
536     The auto configuration basic test menu
537
538     """
539     vutil = VPPUtil()
540     pkgs = vutil.get_installed_vpp_pkgs()
541     if len(pkgs) == 0:
542         print ("\nVPP is not installed, install VPP with option 4.")
543         return
544
545     answer = ''
546     while answer != 'q':
547         answer = autoconfig_basic_test_menu()
548         if answer == '1':
549             autoconfig_ipv4_setup()
550         elif answer == '2':
551             autoconfig_create_iperf_vm()
552         elif answer == '9' or answer == 'q':
553             return
554         else:
555             autoconfig_not_implemented()
556
557
558 def autoconfig_main_menu():
559     """
560     The auto configuration main menu
561
562     """
563
564     main_menu_text = '\nWhat would you like to do?\n\n\
565 1) Show basic system information\n\
566 2) Dry Run (Saves the configuration files in {}/vpp/vpp-config/dryrun.\n\
567 3) Full configuration (WARNING: This will change the system configuration)\n\
568 4) List/Install/Uninstall VPP.\n\
569 q) Quit'.format(rootdir, rootdir)
570
571     # 5) Dry Run from {}/vpp/vpp-config/auto-config.yaml (will not ask questions).\n\
572     # 6) Install QEMU patch (Needed when running openstack).\n\
573
574     print ("{}".format(main_menu_text))
575
576     input_valid = False
577     answer = ''
578     while not input_valid:
579         answer = input("\nCommand: ")
580         if len(answer) > 1:
581             print ("Please enter only 1 character.")
582             continue
583         if re.findall(r'[Qq1-4]', answer):
584             input_valid = True
585             answer = answer[0].lower()
586         else:
587             print ("Please enter a character between 1 and 4 or q.")
588
589     return answer
590
591
592 def autoconfig_main():
593     """
594     The auto configuration main entry point
595
596     """
597
598     # Setup
599     autoconfig_setup()
600
601     answer = ''
602     while answer != 'q':
603         answer = autoconfig_main_menu()
604         if answer == '1':
605             autoconfig_show_system()
606         elif answer == '2':
607             autoconfig_dryrun()
608         elif answer == '3':
609             autoconfig_apply()
610         elif answer == '4':
611             autoconfig_install()
612         elif answer == 'q':
613             return
614         else:
615             autoconfig_not_implemented()
616
617
618 def autoconfig_setup(ask_questions=True):
619     """
620     The auto configuration setup function.
621
622     We will copy the configuration files to the dryrun directory.
623
624     """
625
626     global rootdir
627
628     distro = VPPUtil.get_linux_distro()
629     if distro[0] == 'Ubuntu':
630         rootdir = '/usr/local'
631     else:
632         rootdir = '/usr'
633
634     # If there is a system configuration file use that, if not use the initial auto-config file
635     filename = rootdir + VPP_AUTO_CONFIGURATION_FILE
636     if os.path.isfile(filename) is True:
637         acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
638     else:
639         raise RuntimeError('The Auto configuration file does not exist {}'.
640                            format(filename))
641
642     if ask_questions:
643         print ("\nWelcome to the VPP system configuration utility")
644
645         print ("\nThese are the files we will modify:")
646         print ("    /etc/vpp/startup.conf")
647         print ("    /etc/sysctl.d/80-vpp.conf")
648         print ("    /etc/default/grub")
649
650         print (
651             "\nBefore we change them, we'll create working copies in "
652             "{}".format(rootdir + VPP_DRYRUNDIR))
653         print (
654             "Please inspect them carefully before applying the actual "
655             "configuration (option 3)!")
656
657     nodes = acfg.get_nodes()
658     for i in nodes.items():
659         node = i[1]
660
661         if (os.path.isfile(rootdir + VPP_STARTUP_FILE) is not True) and \
662                 (os.path.isfile(VPP_REAL_STARTUP_FILE) is True):
663             autoconfig_cp(node, VPP_REAL_STARTUP_FILE, '{}'.format(rootdir + VPP_STARTUP_FILE))
664         if (os.path.isfile(rootdir + VPP_HUGE_PAGE_FILE) is not True) and \
665                 (os.path.isfile(VPP_REAL_HUGE_PAGE_FILE) is True):
666             autoconfig_cp(node, VPP_REAL_HUGE_PAGE_FILE, '{}'.format(rootdir + VPP_HUGE_PAGE_FILE))
667         if (os.path.isfile(rootdir + VPP_GRUB_FILE) is not True) and \
668                 (os.path.isfile(VPP_REAL_GRUB_FILE) is True):
669             autoconfig_cp(node, VPP_REAL_GRUB_FILE, '{}'.format(rootdir + VPP_GRUB_FILE))
670
671         # Be sure the uio_pci_generic driver is installed
672         cmd = 'modprobe uio_pci_generic'
673         (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
674         if ret != 0:
675             logging.warning('{} failed on node {} {}'. format(cmd, node['host'], stderr))
676
677
678 # noinspection PyUnresolvedReferences
679 def execute_with_args(args):
680     """
681     Execute the configuration utility with agruments.
682
683     :param args: The Command line arguments
684     :type args: tuple
685     """
686
687     # Setup
688     autoconfig_setup(ask_questions=False)
689
690     # Execute the command
691     if args.show:
692         autoconfig_show_system()
693     elif args.dry_run:
694         autoconfig_dryrun(ask_questions=False)
695     elif args.apply:
696         autoconfig_apply(ask_questions=False)
697     else:
698         autoconfig_not_implemented()
699
700
701 def config_main():
702     """
703     The vpp configuration utility main entry point.
704
705     """
706
707     # Check for root
708     if not os.geteuid() == 0:
709         sys.exit('\nPlease run the VPP Configuration Utility as root.')
710
711     if len(sys.argv) > 1 and ((sys.argv[1] == '-d') or (
712             sys.argv[1] == '--debug')):
713         logging.basicConfig(level=logging.DEBUG)
714     else:
715         logging.basicConfig(level=logging.ERROR)
716
717     # If no arguments were entered, ask the user questions to
718     # get the main parameters
719     if len(sys.argv) == 1:
720         autoconfig_main()
721         return
722     elif len(sys.argv) == 2 and ((sys.argv[1] == '-d') or (
723             sys.argv[1] == '--debug')):
724         autoconfig_main()
725         return
726
727     # There were arguments specified, so execute the utility using
728     # command line arguments
729     description = 'The VPP configuration utility allows the user to '
730     'configure VPP in a simple and safe manner. The utility takes input '
731     'from the user or the specified .yaml file. The user should then '
732     'examine these files to be sure they are correct and then actually '
733     'apply the configuration. When run without arguments the utility run '
734     'in an interactive mode'
735
736     main_parser = argparse.ArgumentParser(
737         prog='arg-test',
738         description=description,
739         epilog='See "%(prog)s help COMMAND" for help on a specific command.')
740     main_parser.add_argument('--apply', '-a', action='store_true',
741                              help='Apply the cofiguration.')
742     main_parser.add_argument('--dry-run', '-dr', action='store_true',
743                              help='Create the dryrun configuration files.')
744     main_parser.add_argument('--show', '-s', action='store_true',
745                              help='Shows basic system information')
746     main_parser.add_argument('--debug', '-d', action='count',
747                              help='Print debug output (multiple levels)')
748
749     args = main_parser.parse_args()
750
751     return execute_with_args(args)
752
753
754 if __name__ == '__main__':
755     config_main()