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