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