misc: Fix python scripts shebang line
[vpp.git] / extras / vpp_config / vpp_config.py
1 #!/usr/bin/env python3
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     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE, clean=True)
345
346     # Stop VPP on each node
347     nodes = acfg.get_nodes()
348     for i in nodes.items():
349         node = i[1]
350         VPPUtil.stop(node)
351
352     # Discover
353     acfg.discover()
354
355     # Check the system resources
356     nodes = acfg.get_nodes()
357     for i in nodes.items():
358         node = i[1]
359         if not acfg.min_system_resources(node):
360             return
361
362     # Modify the devices
363     if ask_questions:
364         acfg.modify_devices()
365     else:
366         acfg.update_interfaces_config()
367
368     # If there are no interfaces, just return
369     for i in nodes.items():
370         node = i[1]
371         if not acfg.has_interfaces(node):
372             print("\nThere are no VPP interfaces configured, please configure at least 1.")
373             return
374
375     # Modify CPU
376     acfg.modify_cpu(ask_questions)
377
378     # Calculate the cpu parameters
379     acfg.calculate_cpu_parameters()
380
381     # Acquire TCP stack parameters
382     if ask_questions:
383         acfg.acquire_tcp_params()
384
385     # Apply the startup
386     acfg.apply_vpp_startup()
387
388     # Apply the grub configuration
389     acfg.apply_grub_cmdline()
390
391     # Huge Pages
392     if ask_questions:
393         acfg.modify_huge_pages()
394     acfg.apply_huge_pages()
395
396
397 def autoconfig_install():
398     """
399     Install or Uninstall VPP.
400
401     """
402
403     # Since these commands will take a while, we
404     # want to see the progress
405     logger = logging.getLogger()
406
407     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
408     vutil = VPPUtil()
409
410     nodes = acfg.get_nodes()
411     for i in nodes.items():
412         node = i[1]
413
414         pkgs = vutil.get_installed_vpp_pkgs()
415
416         if len(pkgs) > 0:
417             print ("\nThese packages are installed on node {}"
418                    .format(node['host']))
419             print ("{:25} {}".format("Name", "Version"))
420             for pkg in pkgs:
421                 try:
422                     print ("{:25} {}".format(
423                         pkg['name'], pkg['version']))
424                 except KeyError:
425                     print ("{}".format(pkg['name']))
426
427             question = "\nDo you want to uninstall these "
428             question += "packages [y/N]? "
429             answer = autoconfig_yn(question, 'n')
430             if answer == 'y':
431                 logger.setLevel(logging.INFO)
432                 vutil.uninstall_vpp(node)
433         else:
434             print ("\nThere are no VPP packages on node {}."
435                    .format(node['host']))
436             question = "Do you want to install VPP [Y/n]? "
437             answer = autoconfig_yn(question, 'y')
438             if answer == 'y':
439                 question = "Do you want to install the release version [Y/n]? "
440                 answer = autoconfig_yn(question, 'y')
441                 if answer == 'y':
442                     branch = 'release'
443                 else:
444                     branch = 'master'
445                 logger.setLevel(logging.INFO)
446                 vutil.install_vpp(node, branch)
447
448     # Set the logging level back
449     logger.setLevel(logging.ERROR)
450
451
452 def autoconfig_patch_qemu():
453     """
454     Patch the correct qemu version that is needed for openstack
455
456     """
457
458     # Since these commands will take a while, we
459     # want to see the progress
460     logger = logging.getLogger()
461
462     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
463
464     nodes = acfg.get_nodes()
465     for i in nodes.items():
466         node = i[1]
467
468         logger.setLevel(logging.INFO)
469         acfg.patch_qemu(node)
470
471
472 def autoconfig_ipv4_setup():
473     """
474     Setup IPv4 interfaces
475
476     """
477
478     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
479     acfg.ipv4_interface_setup()
480
481
482 def autoconfig_create_iperf_vm():
483     """
484     Setup IPv4 interfaces
485
486     """
487
488     acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
489     acfg.destroy_iperf_vm('iperf-server')
490     acfg.create_and_bridge_iperf_virtual_interface()
491     acfg.create_iperf_vm('iperf-server')
492
493
494 def autoconfig_not_implemented():
495     """
496     This feature is not implemented
497
498     """
499
500     print ("\nThis Feature is not implemented yet....")
501
502
503 def autoconfig_basic_test_menu():
504     """
505     The auto configuration basic test menu
506
507     """
508
509     basic_menu_text = '\nWhat would you like to do?\n\n\
510 1) List/Create Simple IPv4 Setup\n\
511 2) Create an iperf VM and Connect to VPP an interface\n\
512 9 or q) Back to main menu.'
513
514     print ("{}".format(basic_menu_text))
515
516     input_valid = False
517     answer = ''
518     while not input_valid:
519         answer = input("\nCommand: ")
520         if len(answer) > 1:
521             print ("Please enter only 1 character.")
522             continue
523         if re.findall(r'[Qq1-29]', answer):
524             input_valid = True
525             answer = answer[0].lower()
526         else:
527             print ("Please enter a character between 1 and 2 or 9.")
528
529         if answer == '9':
530             answer = 'q'
531
532     return answer
533
534
535 def autoconfig_basic_test():
536     """
537     The auto configuration basic test menu
538
539     """
540     vutil = VPPUtil()
541     pkgs = vutil.get_installed_vpp_pkgs()
542     if len(pkgs) == 0:
543         print ("\nVPP is not installed, install VPP with option 4.")
544         return
545
546     answer = ''
547     while answer != 'q':
548         answer = autoconfig_basic_test_menu()
549         if answer == '1':
550             autoconfig_ipv4_setup()
551         elif answer == '2':
552             autoconfig_create_iperf_vm()
553         elif answer == '9' or answer == 'q':
554             return
555         else:
556             autoconfig_not_implemented()
557
558
559 def autoconfig_main_menu():
560     """
561     The auto configuration main menu
562
563     """
564
565     main_menu_text = '\nWhat would you like to do?\n\n\
566 1) Show basic system information\n\
567 2) Dry Run (Saves the configuration files in {}/vpp/vpp-config/dryrun.\n\
568 3) Full configuration (WARNING: This will change the system configuration)\n\
569 4) List/Install/Uninstall VPP.\n\
570 q) Quit'.format(rootdir, rootdir)
571
572     # 5) Dry Run from {}/vpp/vpp-config/auto-config.yaml (will not ask questions).\n\
573     # 6) Install QEMU patch (Needed when running openstack).\n\
574
575     print ("{}".format(main_menu_text))
576
577     input_valid = False
578     answer = ''
579     while not input_valid:
580         answer = input("\nCommand: ")
581         if len(answer) > 1:
582             print ("Please enter only 1 character.")
583             continue
584         if re.findall(r'[Qq1-4]', answer):
585             input_valid = True
586             answer = answer[0].lower()
587         else:
588             print ("Please enter a character between 1 and 4 or q.")
589
590     return answer
591
592
593 def autoconfig_main():
594     """
595     The auto configuration main entry point
596
597     """
598
599     # Setup
600     autoconfig_setup()
601
602     answer = ''
603     while answer != 'q':
604         answer = autoconfig_main_menu()
605         if answer == '1':
606             autoconfig_show_system()
607         elif answer == '2':
608             autoconfig_dryrun()
609         elif answer == '3':
610             autoconfig_apply()
611         elif answer == '4':
612             autoconfig_install()
613         elif answer == 'q':
614             return
615         else:
616             autoconfig_not_implemented()
617
618
619 def autoconfig_setup(ask_questions=True):
620     """
621     The auto configuration setup function.
622
623     We will copy the configuration files to the dryrun directory.
624
625     """
626
627     global rootdir
628
629     distro = VPPUtil.get_linux_distro()
630     if distro[0] == 'Ubuntu':
631         rootdir = '/usr/local'
632     else:
633         rootdir = '/usr'
634
635     # If there is a system configuration file use that, if not use the initial auto-config file
636     filename = rootdir + VPP_AUTO_CONFIGURATION_FILE
637     if os.path.isfile(filename) is True:
638         acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE)
639     else:
640         raise RuntimeError('The Auto configuration file does not exist {}'.
641                            format(filename))
642
643     if ask_questions:
644         print ("\nWelcome to the VPP system configuration utility")
645
646         print ("\nThese are the files we will modify:")
647         print ("    /etc/vpp/startup.conf")
648         print ("    /etc/sysctl.d/80-vpp.conf")
649         print ("    /etc/default/grub")
650
651         print (
652             "\nBefore we change them, we'll create working copies in "
653             "{}".format(rootdir + VPP_DRYRUNDIR))
654         print (
655             "Please inspect them carefully before applying the actual "
656             "configuration (option 3)!")
657
658     nodes = acfg.get_nodes()
659     for i in nodes.items():
660         node = i[1]
661
662         if (os.path.isfile(rootdir + VPP_STARTUP_FILE) is not True) and \
663                 (os.path.isfile(VPP_REAL_STARTUP_FILE) is True):
664             autoconfig_cp(node, VPP_REAL_STARTUP_FILE, '{}'.format(rootdir + VPP_STARTUP_FILE))
665         if (os.path.isfile(rootdir + VPP_HUGE_PAGE_FILE) is not True) and \
666                 (os.path.isfile(VPP_REAL_HUGE_PAGE_FILE) is True):
667             autoconfig_cp(node, VPP_REAL_HUGE_PAGE_FILE, '{}'.format(rootdir + VPP_HUGE_PAGE_FILE))
668         if (os.path.isfile(rootdir + VPP_GRUB_FILE) is not True) and \
669                 (os.path.isfile(VPP_REAL_GRUB_FILE) is True):
670             autoconfig_cp(node, VPP_REAL_GRUB_FILE, '{}'.format(rootdir + VPP_GRUB_FILE))
671
672         # Be sure the uio_pci_generic driver is installed
673         cmd = 'modprobe uio_pci_generic'
674         (ret, stdout, stderr) = VPPUtil.exec_command(cmd)
675         if ret != 0:
676             logging.warning('{} failed on node {} {}'. format(cmd, node['host'], stderr))
677
678
679 # noinspection PyUnresolvedReferences
680 def execute_with_args(args):
681     """
682     Execute the configuration utility with agruments.
683
684     :param args: The Command line arguments
685     :type args: tuple
686     """
687
688     # Setup
689     autoconfig_setup(ask_questions=False)
690
691     # Execute the command
692     if args.show:
693         autoconfig_show_system()
694     elif args.dry_run:
695         autoconfig_dryrun(ask_questions=False)
696     elif args.apply:
697         autoconfig_apply(ask_questions=False)
698     else:
699         autoconfig_not_implemented()
700
701
702 def config_main():
703     """
704     The vpp configuration utility main entry point.
705
706     """
707
708     # Check for root
709     if not os.geteuid() == 0:
710         sys.exit('\nPlease run the VPP Configuration Utility as root.')
711
712     if len(sys.argv) > 1 and ((sys.argv[1] == '-d') or (
713             sys.argv[1] == '--debug')):
714         logging.basicConfig(level=logging.DEBUG)
715     else:
716         logging.basicConfig(level=logging.ERROR)
717
718     # If no arguments were entered, ask the user questions to
719     # get the main parameters
720     if len(sys.argv) == 1:
721         autoconfig_main()
722         return
723     elif len(sys.argv) == 2 and ((sys.argv[1] == '-d') or (
724             sys.argv[1] == '--debug')):
725         autoconfig_main()
726         return
727
728     # There were arguments specified, so execute the utility using
729     # command line arguments
730     description = 'The VPP configuration utility allows the user to '
731     'configure VPP in a simple and safe manner. The utility takes input '
732     'from the user or the specified .yaml file. The user should then '
733     'examine these files to be sure they are correct and then actually '
734     'apply the configuration. When run without arguments the utility run '
735     'in an interactive mode'
736
737     main_parser = argparse.ArgumentParser(
738         prog='arg-test',
739         description=description,
740         epilog='See "%(prog)s help COMMAND" for help on a specific command.')
741     main_parser.add_argument('--apply', '-a', action='store_true',
742                              help='Apply the cofiguration.')
743     main_parser.add_argument('--dry-run', '-dr', action='store_true',
744                              help='Create the dryrun configuration files.')
745     main_parser.add_argument('--show', '-s', action='store_true',
746                              help='Shows basic system information')
747     main_parser.add_argument('--debug', '-d', action='count',
748                              help='Print debug output (multiple levels)')
749
750     args = main_parser.parse_args()
751
752     return execute_with_args(args)
753
754
755 if __name__ == '__main__':
756     config_main()