From 68b0ee3a38e3a86f0389d8cc695915df190c3dfb Mon Sep 17 00:00:00 2001 From: John DeNisco Date: Wed, 27 Sep 2017 16:35:23 -0400 Subject: Redhat and small system support Initial 17.10 commit Final 17.07 cleanup, 17.10 next Added CentOS grub support, this should complete the CentOS support Added Centos install/unistall Added TCP parameters. Change-Id: I064e3a4118969ac36e62924a6a3f8a98f132ba60 Signed-off-by: John DeNisco Signed-off-by: Dave Barach --- extras/vpp_config/LICENSE.txt | 202 ++++ extras/vpp_config/README.rst | 515 ++++++++++ extras/vpp_config/__init__.py | 0 extras/vpp_config/data/80-vpp.conf.template | 15 + extras/vpp_config/data/auto-config.yaml | 23 + extras/vpp_config/data/grub.template | 34 + extras/vpp_config/data/startup.conf.template | 123 +++ extras/vpp_config/scripts/clean.sh | 73 ++ extras/vpp_config/scripts/cp-data.sh | 74 ++ extras/vpp_config/scripts/dpdk-devbind.py | 651 ++++++++++++ extras/vpp_config/scripts/vpp-config | 30 + extras/vpp_config/setup.cfg | 5 + extras/vpp_config/setup.py | 22 + extras/vpp_config/vpp_config.py | 573 +++++++++++ extras/vpp_config/vpplib/AutoConfig.py | 1427 ++++++++++++++++++++++++++ extras/vpp_config/vpplib/CpuUtils.py | 287 ++++++ extras/vpp_config/vpplib/QemuUtils.py | 680 ++++++++++++ extras/vpp_config/vpplib/VPPUtil.py | 662 ++++++++++++ extras/vpp_config/vpplib/VppGrubUtil.py | 236 +++++ extras/vpp_config/vpplib/VppHugePageUtil.py | 122 +++ extras/vpp_config/vpplib/VppPCIUtil.py | 330 ++++++ extras/vpp_config/vpplib/__init__.py | 16 + extras/vpp_config/vpplib/constants.py | 48 + 23 files changed, 6148 insertions(+) create mode 100644 extras/vpp_config/LICENSE.txt create mode 100644 extras/vpp_config/README.rst create mode 100644 extras/vpp_config/__init__.py create mode 100644 extras/vpp_config/data/80-vpp.conf.template create mode 100644 extras/vpp_config/data/auto-config.yaml create mode 100644 extras/vpp_config/data/grub.template create mode 100644 extras/vpp_config/data/startup.conf.template create mode 100755 extras/vpp_config/scripts/clean.sh create mode 100755 extras/vpp_config/scripts/cp-data.sh create mode 100755 extras/vpp_config/scripts/dpdk-devbind.py create mode 100755 extras/vpp_config/scripts/vpp-config create mode 100644 extras/vpp_config/setup.cfg create mode 100644 extras/vpp_config/setup.py create mode 100755 extras/vpp_config/vpp_config.py create mode 100644 extras/vpp_config/vpplib/AutoConfig.py create mode 100644 extras/vpp_config/vpplib/CpuUtils.py create mode 100644 extras/vpp_config/vpplib/QemuUtils.py create mode 100644 extras/vpp_config/vpplib/VPPUtil.py create mode 100644 extras/vpp_config/vpplib/VppGrubUtil.py create mode 100644 extras/vpp_config/vpplib/VppHugePageUtil.py create mode 100644 extras/vpp_config/vpplib/VppPCIUtil.py create mode 100644 extras/vpp_config/vpplib/__init__.py create mode 100644 extras/vpp_config/vpplib/constants.py diff --git a/extras/vpp_config/LICENSE.txt b/extras/vpp_config/LICENSE.txt new file mode 100644 index 00000000000..8f71f43fee3 --- /dev/null +++ b/extras/vpp_config/LICENSE.txt @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/extras/vpp_config/README.rst b/extras/vpp_config/README.rst new file mode 100644 index 00000000000..748f66f7c0e --- /dev/null +++ b/extras/vpp_config/README.rst @@ -0,0 +1,515 @@ +Summary: + +The purpose of the VPP configuration utility is to allow the user to configure +VPP in a simple and safe manner. The utility takes input from the user and +then modifies the key configuration files. The user can then examine these files +to be sure they are correct and then actually apply the configuration. The user +can also install a released and stable version of VPP. This is currently +released with release 17.07. + +Use: + +The installation and executing of the VPP configuration utility is simple. First +install the python pip module. Using pip install, then pip install vpp-config. +Then simply type Òvpp-configÓ and answer the questions. If you are not sure what +to answer choose the default. For yes or no questions the capital letter +designates the default. For example, for a question that shows [Y/n] Y is the +default. For numbers the default is within the brackets for example for a +question that shows [1024]. 1024 is the default. + +The flow of the utility is to inspect the system, if VPP is not install it, +create dry run configurations, inspect the files created during the dry run, +apply the configuration and then inspect the system again and then repeat. + +Caveats: + +- Only supports Ubuntu, centos7 is coming shortly. +- When Inspecting the system, you may see a Huge page error, inspect the system a +few more times, if the error persists it is real. + +For Developers: + +Modifying the code is reasonable simple. The process would be edit and debug the +code from the root directory. In order to do this, we need a script that will copy +or data files to the proper place. This is where they end up with pip install. For +Ubuntu, this is /usr/local/vpp/vpp-config. I have provided a script that will copy +the relevant files correctly. I have also provided a script that will clean the +environment so you can start from scratch. These are the steps to run the utility +in this environment. The scripts are meant to be run from the root directory. + + ./scripts/clean.sh + ./scripts/cp-data.sh + ./vpp_config.py + +When the utility is installed with pip the wrapper scripts/vpp-config is written to +/usr/local/bin. However, the starting point when debugging this script locally is +./vpp_config.py. Run the utility by executing ./vpp_config.py from the root directory. + +The start point in the code is in vpp_config.py. However, most of the work is done in +the files in ./vpplib + +Uploading to PyPi: + +To upload this utility to PpPi simple do the following. Currently, I have my own account +when we want everyone to contribute we will need to change that. + + sudo ÐH bash + cd vpp_config + python setup.py sdist bdist_wheel + twine upload dist/* + +Example Run: + +# pip install vpp-config +# vpp-config + +Welcome to the VPP system configuration utility + +These are the files we will modify: + /etc/vpp/startup.conf + /etc/sysctl.d/80-vpp.conf + /etc/default/grub + +Before we change them, we'll create working copies in /usr/local/vpp/vpp-config/dryrun +Please inspect them carefully before applying the actual configuration (option 3)! + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: 1 + +============================== +NODE: DUT1 + +CPU: + Model name: Intel(R) Xeon(R) CPU E5-2667 v3 @ 3.20GHz + CPU(s): 32 + Thread(s) per core: 2 + Core(s) per socket: 8 + Socket(s): 2 + NUMA node0 CPU(s): 0-7,16-23 + NUMA node1 CPU(s): 8-15,24-31 + CPU max MHz: 3600.0000 + CPU min MHz: 1200.0000 + SMT: Enabled + +VPP Threads: (Name: Cpu Number) + +Grub Command Line: + Current: BOOT_IMAGE=/boot/vmlinuz-4.4.0-96-generic root=UUID=d760b82f-f37b-47e2-9815-db8d479a3557 ro + Configured: GRUB_CMDLINE_LINUX_DEFAULT="" + +Huge Pages: + Total System Memory : 65863484 kB + Total Free Memory : 41325924 kB + Actual Huge Page Total : 8192 + Configured Huge Page Total : 1024 + Huge Pages Free : 8192 + Huge Page Size : 2048 kB + +Devices: + +Status: + Not Installed + +============================== + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: 4 + +There are no VPP packages on node localhost. +Do you want to install VPP [Y/n]? +INFO:root: Local Command: ls /etc/apt/sources.list.d/99fd.io.list.orig +INFO:root: /etc/apt/sources.list.d/99fd.io.list.orig +ÉÉ.. + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: 1 + +============================== +NODE: DUT1 + +CPU: + Model name: Intel(R) Xeon(R) CPU E5-2667 v3 @ 3.20GHz + CPU(s): 32 + Thread(s) per core: 2 + Core(s) per socket: 8 + Socket(s): 2 + NUMA node0 CPU(s): 0-7,16-23 + NUMA node1 CPU(s): 8-15,24-31 + CPU max MHz: 3600.0000 + CPU min MHz: 1200.0000 + SMT: Enabled + +VPP Threads: (Name: Cpu Number) + vpp_main : 0 + vpp_stats : 0 + +Grub Command Line: + Current: BOOT_IMAGE=/boot/vmlinuz-4.4.0-96-generic root=UUID=d760b82f-f37b-47e2-9815-db8d479a3557 ro + Configured: GRUB_CMDLINE_LINUX_DEFAULT="" + +Huge Pages: + Total System Memory : 65863484 kB + Total Free Memory : 55877364 kB + Actual Huge Page Total : 1024 + Configured Huge Page Total : 1024 + Huge Pages Free : 1024 + Huge Page Size : 2048 kB + +Devices: +Name Socket RXQs RXDescs TXQs TXDescs + +Status: + active (running) + +============================== + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: 2 + +These device(s) are currently NOT being used by VPP or the OS. + +PCI ID Description +---------------------------------------------------------------- +0000:02:00.0 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:02:00.1 82599ES 10-Gigabit SFI/SFP+ Network Connection + +Would you like to give any of these devices back to the OS [y/N]? y +Would you like to use device 0000:02:00.0 for the OS [y/N]? y +Would you like to use device 0000:02:00.1 for the OS [y/N]? y + +These devices have kernel interfaces, but appear to be safe to use with VPP. + +PCI ID Kernel Interface(s) Description +------------------------------------------------------------------------------------------ +0000:90:00.0 enp144s0 VIC Ethernet NIC +0000:8f:00.0 enp143s0 VIC Ethernet NIC +0000:84:00.0 enp132s0f0,enp132s0f0d1 Ethernet Controller XL710 for 40GbE QSFP+ +0000:84:00.1 enp132s0f1,enp132s0f1d1 Ethernet Controller XL710 for 40GbE QSFP+ +0000:08:00.1 enp8s0f1 I350 Gigabit Network Connection +0000:02:00.0 enp2s0f0 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:02:00.1 enp2s0f1 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:86:00.0 enp134s0f0 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:86:00.1 enp134s0f1 82599ES 10-Gigabit SFI/SFP+ Network Connection + +Would you like to use any of these device(s) for VPP [y/N]? y +Would you like to use device 0000:90:00.0 for VPP [y/N]? +Would you like to use device 0000:8f:00.0 for VPP [y/N]? +Would you like to use device 0000:84:00.0 for VPP [y/N]? +Would you like to use device 0000:84:00.1 for VPP [y/N]? +Would you like to use device 0000:08:00.1 for VPP [y/N]? +Would you like to use device 0000:02:00.0 for VPP [y/N]? y +Would you like to use device 0000:02:00.1 for VPP [y/N]? y +Would you like to use device 0000:86:00.0 for VPP [y/N]? y +Would you like to use device 0000:86:00.1 for VPP [y/N]? y + +These device(s) will be used by VPP. + +PCI ID Description +---------------------------------------------------------------- +0000:86:00.0 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:86:00.1 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:02:00.0 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:02:00.1 82599ES 10-Gigabit SFI/SFP+ Network Connection + +Would you like to remove any of these device(s) [y/N]? + +These device(s) will be used by VPP, please rerun this option if this is incorrect. + +PCI ID Description +---------------------------------------------------------------- +0000:86:00.0 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:86:00.1 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:02:00.0 82599ES 10-Gigabit SFI/SFP+ Network Connection +0000:02:00.1 82599ES 10-Gigabit SFI/SFP+ Network Connection + +Your system has 32 core(s) and 2 Numa Nodes. +To begin, we suggest not reserving any cores for VPP or other processes. +Then to improve performance try reserving cores as needed. + +How many core(s) do you want to reserve for processes other than VPP? [0-16][0]? 4 +How many core(s) shall we reserve for VPP workers[0-4][0]? 2 +Should we reserve 1 core for the VPP Main thread? [Y/n]? + +There currently 1024 2048 kB huge pages free. +Do you want to reconfigure the number of huge pages [y/N]? y + +There currently a total of 1024 huge pages. +How many huge pages do you want [1024 - 22511][1024]? 8192 + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: 3 + +We are now going to configure your system(s). + +Are you sure you want to do this [Y/n]? +These are the changes we will apply to +the huge page file (/etc/sysctl.d/80-vpp.conf). + +1,2d0 +< # Number of 2MB hugepages desired +< vm.nr_hugepages=1024 +4,7c2,3 +< # Must be greater than or equal to (2 * vm.nr_hugepages). +< vm.max_map_count=3096 +< +< # All groups allowed to access hugepages +--- +> vm.nr_hugepages=8192 +> vm.max_map_count=17408 +8a5 +> kernel.shmmax=17179869184 +10,15d6 +< # Shared Memory Max must be greator or equal to the total size of hugepages. +< # For 2MB pages, TotalHugepageSize = vm.nr_hugepages * 2 * 1024 * 1024 +< # If the existing kernel.shmmax setting (cat /sys/proc/kernel/shmmax) +< # is greater than the calculated TotalHugepageSize then set this parameter +< # to current shmmax value. +< kernel.shmmax=2147483648 + + +Are you sure you want to apply these changes [Y/n]? +These are the changes we will apply to +the VPP startup file (/etc/vpp/startup.conf). + +3c3 +< nodaemon +--- +> interactive +5a6 +> cli-listen /run/vpp/cli.sock +17c18,25 +< ## In the VPP there is one main thread and optionally the user can create worker(s) +--- +> +> main-core 8 +> corelist-workers 9-10,5-6 +> +> scheduler-policy fifo +> scheduler-priority 50 +> +> ## In the VPP there is one main thread and optionally the user can create worker(s) +52,53c60,76 +< # dpdk { +< ## Change default settings for all intefaces +--- +> dpdk { +> +> dev 0000:86:00.0 { +> num-rx-queues 2 +> } +> dev 0000:86:00.1 { +> num-rx-queues 2 +> } +> dev 0000:02:00.0 { +> num-rx-queues 2 +> } +> dev 0000:02:00.1 { +> num-rx-queues 2 +> } +> num-mbufs 71680 +> +> ## Change default settings for all intefaces +82a106,115 +> ## Specify bonded interface and its slaves via PCI addresses +> ## +> ## Bonded interface in XOR load balance mode (mode 2) with L3 and L4 headers +> # vdev eth_bond0,mode=2,slave=0000:02:00.0,slave=0000:03:00.0,xmit_policy=l34 +> # vdev eth_bond1,mode=2,slave=0000:02:00.1,slave=0000:03:00.1,xmit_policy=l34 +> ## +> ## Bonded interface in Active-Back up mode (mode 1) +> # vdev eth_bond0,mode=1,slave=0000:02:00.0,slave=0000:03:00.0 +> # vdev eth_bond1,mode=1,slave=0000:02:00.1,slave=0000:03:00.1 +> +99c132 +< # } +--- +> } +108a142 +> + + +Are you sure you want to apply these changes [Y/n]? + +The configured grub cmdline looks like this: +GRUB_CMDLINE_LINUX_DEFAULT="intel_pstate=disable isolcpus=1-4,8,9-10,5-6 nohz_full=1-4,8,9-10,5-6 rcu_nocbs=1-4,8,9-10,5-6" + +The current boot cmdline looks like this: +BOOT_IMAGE=/boot/vmlinuz-4.4.0-96-generic root=UUID=d760b82f-f37b-47e2-9815-db8d479a3557 ro + +Do you want to keep the current boot cmdline [Y/n]? + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: 1 + +============================== +NODE: DUT1 + +CPU: + Model name: Intel(R) Xeon(R) CPU E5-2667 v3 @ 3.20GHz + CPU(s): 32 + Thread(s) per core: 2 + Core(s) per socket: 8 + Socket(s): 2 + NUMA node0 CPU(s): 0-7,16-23 + NUMA node1 CPU(s): 8-15,24-31 + CPU max MHz: 3600.0000 + CPU min MHz: 1200.0000 + SMT: Enabled + +VPP Threads: (Name: Cpu Number) + +Grub Command Line: + Current: BOOT_IMAGE=/boot/vmlinuz-4.4.0-96-generic root=UUID=d760b82f-f37b-47e2-9815-db8d479a3557 ro + Configured: GRUB_CMDLINE_LINUX_DEFAULT="intel_pstate=disable isolcpus=1-4,8,9-10,5-6 nohz_full=1-4,8,9-10,5-6 rcu_nocbs=1-4,8,9-10,5-6" + +Huge Pages: + Total System Memory : 65863484 kB + Total Free Memory : 41163916 kB + Actual Huge Page Total : 8192 + Configured Huge Page Total : 8192 + Huge Pages Free : 3108 + Huge Page Size : 2048 kB + +Devices: +Total Number of Buffers: 71680 + +Status: + active (running) + Sep 27 12:49:59 tf-ucs-3 vpp[13671]: EAL: No free hugepages reported in hugepages-1048576kB + +============================== + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: 1 + +============================== +NODE: DUT1 + +CPU: + Model name: Intel(R) Xeon(R) CPU E5-2667 v3 @ 3.20GHz + CPU(s): 32 + Thread(s) per core: 2 + Core(s) per socket: 8 + Socket(s): 2 + NUMA node0 CPU(s): 0-7,16-23 + NUMA node1 CPU(s): 8-15,24-31 + CPU max MHz: 3600.0000 + CPU min MHz: 1200.0000 + SMT: Enabled + +VPP Threads: (Name: Cpu Number) + vpp_stats : 0 + vpp_wk_2 : 9 + vpp_wk_3 : 10 + vpp_wk_0 : 5 + vpp_wk_1 : 6 + vpp_main : 8 + +Grub Command Line: + Current: BOOT_IMAGE=/boot/vmlinuz-4.4.0-96-generic root=UUID=d760b82f-f37b-47e2-9815-db8d479a3557 ro + Configured: GRUB_CMDLINE_LINUX_DEFAULT="intel_pstate=disable isolcpus=1-4,8,9-10,5-6 nohz_full=1-4,8,9-10,5-6 rcu_nocbs=1-4,8,9-10,5-6" + +Huge Pages: + Total System Memory : 65863484 kB + Total Free Memory : 41170684 kB + Actual Huge Page Total : 8192 + Configured Huge Page Total : 8192 + Huge Pages Free : 7936 + Huge Page Size : 2048 kB + +Devices: +Total Number of Buffers: 71680 +Name Socket RXQs RXDescs TXQs TXDescs +TenGigabitEthernet2/0/0 0 2 1024 5 1024 +TenGigabitEthernet2/0/1 0 2 1024 5 1024 +TenGigabitEthernet86/0/0 1 2 1024 5 1024 +TenGigabitEthernet86/0/1 1 2 1024 5 1024 + +Status: + active (running) + +============================== + +What would you like to do? + +1) Show basic system information +2) Dry Run (Will save the configuration files in /usr/local/vpp/vpp-config/dryrun for inspection) + and user input in /usr/local/vpp/vpp-config/configs/auto-config.yaml +3) Full configuration (WARNING: This will change the system configuration) +4) Install/Uninstall VPP. +5) Dry Run from /usr/local/vpp/vpp-config/auto-config.yaml (will not ask questions). +6) Install QEMU patch (Needed when running openstack). +9 or q) Quit + +Command: q +# diff --git a/extras/vpp_config/__init__.py b/extras/vpp_config/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extras/vpp_config/data/80-vpp.conf.template b/extras/vpp_config/data/80-vpp.conf.template new file mode 100644 index 00000000000..8fdf184cf83 --- /dev/null +++ b/extras/vpp_config/data/80-vpp.conf.template @@ -0,0 +1,15 @@ +# Number of 2MB hugepages desired +vm.nr_hugepages=1024 + +# Must be greater than or equal to (2 * vm.nr_hugepages). +vm.max_map_count=3096 + +# All groups allowed to access hugepages +vm.hugetlb_shm_group=0 + +# Shared Memory Max must be greator or equal to the total size of hugepages. +# For 2MB pages, TotalHugepageSize = vm.nr_hugepages * 2 * 1024 * 1024 +# If the existing kernel.shmmax setting (cat /sys/proc/kernel/shmmax) +# is greater than the calculated TotalHugepageSize then set this parameter +# to current shmmax value. +kernel.shmmax=2147483648 diff --git a/extras/vpp_config/data/auto-config.yaml b/extras/vpp_config/data/auto-config.yaml new file mode 100644 index 00000000000..4a74de21882 --- /dev/null +++ b/extras/vpp_config/data/auto-config.yaml @@ -0,0 +1,23 @@ +metadata: + system_config_file: /vpp/vpp-config/configs/system-config.yaml + version: 0.1 +nodes: + DUT1: + cpu: + grub_config_file: /vpp/vpp-config/dryrun/default/grub + reserve_vpp_main_core: true + total_other_cpus: 0 + total_vpp_cpus: 2 + host: localhost + hugepages: + hugepage_config_file: /vpp/vpp-config/dryrun/sysctl.d/80-vpp.conf + total: '1024' + interfaces: + tcp: + active_open_sessions: 0 + passive_open_sessions: 0 + type: DUT + vpp: + startup_config_file: /vpp/vpp-config/dryrun/vpp/startup.conf + unix: + interactive: false diff --git a/extras/vpp_config/data/grub.template b/extras/vpp_config/data/grub.template new file mode 100644 index 00000000000..88138000e20 --- /dev/null +++ b/extras/vpp_config/data/grub.template @@ -0,0 +1,34 @@ +# If you change this file, run 'update-grub' afterwards to update +# /boot/grub/grub.cfg. +# For full documentation of the options in this file, see: +# info -f grub -n 'Simple configuration' + +GRUB_DEFAULT=0 +#GRUB_HIDDEN_TIMEOUT=0 +GRUB_HIDDEN_TIMEOUT_QUIET=true +GRUB_TIMEOUT=2 +GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian` +GRUB_CMDLINE_LINUX_DEFAULT="" +GRUB_CMDLINE_LINUX="" + +# Uncomment to enable BadRAM filtering, modify to suit your needs +# This works with Linux (no patch required) and with any kernel that obtains +# the memory map information from GRUB (GNU Mach, kernel of FreeBSD ...) +#GRUB_BADRAM="0x01234567,0xfefefefe,0x89abcdef,0xefefefef" + +# Uncomment to disable graphical terminal (grub-pc only) +#GRUB_TERMINAL=console + +# The resolution used on graphical terminal +# note that you can use only modes which your graphic card supports via VBE +# you can see them in real GRUB with the command `vbeinfo' +#GRUB_GFXMODE=640x480 + +# Uncomment if you don't want GRUB to pass "root=UUID=xxx" parameter to Linux +#GRUB_DISABLE_LINUX_UUID=true + +# Uncomment to disable generation of recovery mode menu entries +#GRUB_DISABLE_RECOVERY="true" + +# Uncomment to get a beep at grub start +#GRUB_INIT_TUNE="480 440 1" diff --git a/extras/vpp_config/data/startup.conf.template b/extras/vpp_config/data/startup.conf.template new file mode 100644 index 00000000000..91b92c62aea --- /dev/null +++ b/extras/vpp_config/data/startup.conf.template @@ -0,0 +1,123 @@ + +unix {{ +{unix} + log /tmp/vpp.log + full-coredump + cli-listen /run/vpp/cli.sock +}} + +api-trace {{ + on +}} + +cpu {{ +{cpu} + scheduler-policy fifo + scheduler-priority 50 + + ## In the VPP there is one main thread and optionally the user can create worker(s) + ## The main thread and worker thread(s) can be pinned to CPU core(s) manually or automatically + + ## Manual pinning of thread(s) to CPU core(s) + + ## Set logical CPU core where main thread runs + # main-core 1 + + ## Set logical CPU core(s) where worker threads are running + # corelist-workers 2-3,18-19 + + ## Automatic pinning of thread(s) to CPU core(s) + + ## Sets number of CPU core(s) to be skipped (1 ... N-1) + ## Skipped CPU core(s) are not used for pinning main thread and working thread(s). + ## The main thread is automatically pinned to the first available CPU core and worker(s) + ## are pinned to next free CPU core(s) after core assigned to main thread + # skip-cores 4 + + ## Specify a number of workers to be created + ## Workers are pinned to N consecutive CPU cores while skipping "skip-cores" CPU core(s) + ## and main thread's CPU core + # workers 2 + + ## Set scheduling policy and priority of main and worker threads + + ## Scheduling policy options are: other (SCHED_OTHER), batch (SCHED_BATCH) + ## idle (SCHED_IDLE), fifo (SCHED_FIFO), rr (SCHED_RR) + # scheduler-policy fifo + + ## Scheduling priority is used only for "real-time policies (fifo and rr), + ## and has to be in the range of priorities supported for a particular policy + # scheduler-priority 50 +}} + +dpdk {{ +{devices} + + ## Change default settings for all intefaces + # dev default {{ + ## Number of receive queues, enables RSS + ## Default is 1 + # num-rx-queues 3 + + ## Number of transmit queues, Default is equal + ## to number of worker threads or 1 if no workers treads + # num-tx-queues 3 + + ## Number of descriptors in transmit and receive rings + ## increasing or reducing number can impact performance + ## Default is 1024 for both rx and tx + # num-rx-desc 512 + # num-tx-desc 512 + + ## VLAN strip offload mode for interface + ## Default is off + # vlan-strip-offload on + # }} + + ## Whitelist specific interface by specifying PCI address + # dev 0000:02:00.0 + + ## Whitelist specific interface by specifying PCI address and in + ## addition specify custom parameters for this interface + # dev 0000:02:00.1 {{ + # num-rx-queues 2 + # }} + + ## Specify bonded interface and its slaves via PCI addresses + ## + ## Bonded interface in XOR load balance mode (mode 2) with L3 and L4 headers + # vdev eth_bond0,mode=2,slave=0000:02:00.0,slave=0000:03:00.0,xmit_policy=l34 + # vdev eth_bond1,mode=2,slave=0000:02:00.1,slave=0000:03:00.1,xmit_policy=l34 + ## + ## Bonded interface in Active-Back up mode (mode 1) + # vdev eth_bond0,mode=1,slave=0000:02:00.0,slave=0000:03:00.0 + # vdev eth_bond1,mode=1,slave=0000:02:00.1,slave=0000:03:00.1 + + ## Change UIO driver used by VPP, Options are: igb_uio, vfio-pci + ## and uio_pci_generic (default) + # uio-driver vfio-pci + + ## Disable mutli-segment buffers, improves performance but + ## disables Jumbo MTU support + # no-multi-seg + + ## Increase number of buffers allocated, needed only in scenarios with + ## large number of interfaces and worker threads. Value is per CPU socket. + ## Default is 16384 + # num-mbufs 128000 + + ## Change hugepages allocation per-socket, needed only if there is need for + ## larger number of mbufs. Default is 256M on each detected CPU socket + # socket-mem 2048,2048 +}} + +# Adjusting the plugin path depending on where the VPP plugins are: +#plugins +#{{ +# path /home/bms/vpp/build-root/install-vpp-native/vpp/lib64/vpp_plugins +#}} + +# Alternate syntax to choose plugin path +#plugin_path /home/bms/vpp/build-root/install-vpp-native/vpp/lib64/vpp_plugins + +{tcp} diff --git a/extras/vpp_config/scripts/clean.sh b/extras/vpp_config/scripts/clean.sh new file mode 100755 index 00000000000..908e6297fe3 --- /dev/null +++ b/extras/vpp_config/scripts/clean.sh @@ -0,0 +1,73 @@ +#!/bin/bash -x +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +check_os() +{ + + # perform some very rudimentary platform detection + lsb_dist='' + if command_exists lsb_release; then + lsb_dist="$(lsb_release -si)" + fi + if [ -z "$lsb_dist" ] && [ -r /etc/lsb-release ]; then + lsb_dist="$(. /etc/lsb-release && echo "$DISTRIB_ID")" + fi + if [ -z "$lsb_dist" ] && [ -r /etc/fedora-release ]; then + lsb_dist='fedora' + fi + if [ -z "$lsb_dist" ] && [ -r /etc/centos-release ]; then + lsb_dist='centos' + fi + if [ -z "$lsb_dist" ] && [ -r /etc/os-release ]; then + lsb_dist="$(. /etc/os-release && echo "$ID")" + fi + + lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" + case "$lsb_dist" in + fedora|centos|ubuntu|debian) + ;; + *) + echo "Operating system [$lsb_dist] is unsupported" + exit 0 + ;; + esac + LSB=$lsb_dist +} + +check_os +case "$LSB" in + centos) + ROOTDIR='/usr' + ;; + ubuntu) + ROOTDIR='/usr/local' + ;; + *) + echo "$LSB is not supported" + exit 1 + ;; +esac + +sudo -H pip uninstall vpp-config +sudo rm *~ +sudo rm *.pyc +sudo rm vpplib/*~ +sudo rm vpplib/*.pyc +sudo rm scripts/*~ +sudo rm data/*~ +sudo rm -r build +sudo rm -r dist +sudo rm -r vpp_config.egg-info +sudo rm -r $ROOTDIR/vpp/vpp-config +sudo rm $ROOTDIR/bin/vpp-config diff --git a/extras/vpp_config/scripts/cp-data.sh b/extras/vpp_config/scripts/cp-data.sh new file mode 100755 index 00000000000..a0053dce074 --- /dev/null +++ b/extras/vpp_config/scripts/cp-data.sh @@ -0,0 +1,74 @@ +#!/bin/bash -x +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +check_os() +{ + + # perform some very rudimentary platform detection + lsb_dist='' + if command_exists lsb_release; then + lsb_dist="$(lsb_release -si)" + fi + if [ -z "$lsb_dist" ] && [ -r /etc/lsb-release ]; then + lsb_dist="$(. /etc/lsb-release && echo "$DISTRIB_ID")" + fi + if [ -z "$lsb_dist" ] && [ -r /etc/fedora-release ]; then + lsb_dist='fedora' + fi + if [ -z "$lsb_dist" ] && [ -r /etc/centos-release ]; then + lsb_dist='centos' + fi + if [ -z "$lsb_dist" ] && [ -r /etc/os-release ]; then + lsb_dist="$(. /etc/os-release && echo "$ID")" + fi + + lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" + case "$lsb_dist" in + fedora|centos|ubuntu|debian) + ;; + *) + echo "Operating system [$lsb_dist] is unsupported" + exit 0 + ;; + esac + LSB=$lsb_dist +} + +check_os +case "$LSB" in + centos) + ROOTDIR='/usr' + ;; + ubuntu) + ROOTDIR='/usr/local' + ;; + *) + echo "$LSB is not supported" + exit 1 + ;; +esac + +sudo mkdir $ROOTDIR/vpp +sudo mkdir $ROOTDIR/vpp/vpp-config +sudo mkdir $ROOTDIR/vpp/vpp-config/dryrun +sudo mkdir $ROOTDIR/vpp/vpp-config/scripts +sudo mkdir $ROOTDIR/vpp/vpp-config/configs +sudo mkdir $ROOTDIR/vpp/vpp-config/dryrun/default +sudo mkdir $ROOTDIR/vpp/vpp-config/dryrun/sysctl.d +sudo mkdir $ROOTDIR/vpp/vpp-config/dryrun/vpp +sudo cp data/auto-config.yaml $ROOTDIR/vpp/vpp-config/configs/. +sudo cp data/grub.template $ROOTDIR/vpp/vpp-config/dryrun/default/. +sudo cp data/startup.conf.template $ROOTDIR/vpp/vpp-config/dryrun/vpp/. +sudo cp data/80-vpp.conf.template $ROOTDIR/vpp/vpp-config/dryrun/sysctl.d/. +sudo cp scripts/dpdk-devbind.py $ROOTDIR/vpp/vpp-config/scripts/. diff --git a/extras/vpp_config/scripts/dpdk-devbind.py b/extras/vpp_config/scripts/dpdk-devbind.py new file mode 100755 index 00000000000..f1d374d6b08 --- /dev/null +++ b/extras/vpp_config/scripts/dpdk-devbind.py @@ -0,0 +1,651 @@ +#! /usr/bin/python +# +# BSD LICENSE +# +# Copyright(c) 2010-2014 Intel Corporation. All rights reserved. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Intel Corporation nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +import sys +import os +import getopt +import subprocess +from os.path import exists, abspath, dirname, basename + +# The PCI base class for NETWORK devices +NETWORK_BASE_CLASS = "02" +CRYPTO_BASE_CLASS = "0b" + +# global dict ethernet devices present. Dictionary indexed by PCI address. +# Each device within this is itself a dictionary of device properties +devices = {} +# list of supported DPDK drivers +dpdk_drivers = ["igb_uio", "vfio-pci", "uio_pci_generic"] + +# command-line arg flags +b_flag = None +status_flag = False +force_flag = False +args = [] + + +def usage(): + '''Print usage information for the program''' + argv0 = basename(sys.argv[0]) + print(""" +Usage: +------ + + %(argv0)s [options] DEVICE1 DEVICE2 .... + +where DEVICE1, DEVICE2 etc, are specified via PCI "domain:bus:slot.func" syntax +or "bus:slot.func" syntax. For devices bound to Linux kernel drivers, they may +also be referred to by Linux interface name e.g. eth0, eth1, em0, em1, etc. + +Options: + --help, --usage: + Display usage information and quit + + -s, --status: + Print the current status of all known network and crypto devices. + For each device, it displays the PCI domain, bus, slot and function, + along with a text description of the device. Depending upon whether the + device is being used by a kernel driver, the igb_uio driver, or no + driver, other relevant information will be displayed: + * the Linux interface name e.g. if=eth0 + * the driver being used e.g. drv=igb_uio + * any suitable drivers not currently using that device + e.g. unused=igb_uio + NOTE: if this flag is passed along with a bind/unbind option, the + status display will always occur after the other operations have taken + place. + + -b driver, --bind=driver: + Select the driver to use or \"none\" to unbind the device + + -u, --unbind: + Unbind a device (Equivalent to \"-b none\") + + --force: + By default, network devices which are used by Linux - as indicated by having + routes in the routing table - cannot be modified. Using the --force + flag overrides this behavior, allowing active links to be forcibly + unbound. + WARNING: This can lead to loss of network connection and should be used + with caution. + +Examples: +--------- + +To display current device status: + %(argv0)s --status + +To bind eth1 from the current driver and move to use igb_uio + %(argv0)s --bind=igb_uio eth1 + +To unbind 0000:01:00.0 from using any driver + %(argv0)s -u 0000:01:00.0 + +To bind 0000:02:00.0 and 0000:02:00.1 to the ixgbe kernel driver + %(argv0)s -b ixgbe 02:00.0 02:00.1 + + """ % locals()) # replace items from local variables + + +# This is roughly compatible with check_output function in subprocess module +# which is only available in python 2.7. +def check_output(args, stderr=None): + '''Run a command and capture its output''' + return subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=stderr).communicate()[0] + + +def find_module(mod): + '''find the .ko file for kernel module named mod. + Searches the $RTE_SDK/$RTE_TARGET directory, the kernel + modules directory and finally under the parent directory of + the script ''' + # check $RTE_SDK/$RTE_TARGET directory + if 'RTE_SDK' in os.environ and 'RTE_TARGET' in os.environ: + path = "%s/%s/kmod/%s.ko" % (os.environ['RTE_SDK'], + os.environ['RTE_TARGET'], mod) + if exists(path): + return path + + # check using depmod + try: + depmod_out = check_output(["modinfo", "-n", mod], + stderr=subprocess.STDOUT).lower() + if "error" not in depmod_out: + path = depmod_out.strip() + if exists(path): + return path + except: # if modinfo can't find module, it fails, so continue + pass + + # check for a copy based off current path + tools_dir = dirname(abspath(sys.argv[0])) + if (tools_dir.endswith("tools")): + base_dir = dirname(tools_dir) + find_out = check_output(["find", base_dir, "-name", mod + ".ko"]) + if len(find_out) > 0: # something matched + path = find_out.splitlines()[0] + if exists(path): + return path + + +def check_modules(): + '''Checks that igb_uio is loaded''' + global dpdk_drivers + + # list of supported modules + mods = [{"Name": driver, "Found": False} for driver in dpdk_drivers] + + # first check if module is loaded + try: + # Get list of sysfs modules (both built-in and dynamically loaded) + sysfs_path = '/sys/module/' + + # Get the list of directories in sysfs_path + sysfs_mods = [os.path.join(sysfs_path, o) for o + in os.listdir(sysfs_path) + if os.path.isdir(os.path.join(sysfs_path, o))] + + # Extract the last element of '/sys/module/abc' in the array + sysfs_mods = [a.split('/')[-1] for a in sysfs_mods] + + # special case for vfio_pci (module is named vfio-pci, + # but its .ko is named vfio_pci) + sysfs_mods = map(lambda a: + a if a != 'vfio_pci' else 'vfio-pci', sysfs_mods) + + for mod in mods: + if mod["Name"] in sysfs_mods: + mod["Found"] = True + except: + pass + + # check if we have at least one loaded module + if True not in [mod["Found"] for mod in mods] and b_flag is not None: + if b_flag in dpdk_drivers: + print("Error - no supported modules(DPDK driver) are loaded") + sys.exit(1) + else: + print("Warning - no supported modules(DPDK driver) are loaded") + + # change DPDK driver list to only contain drivers that are loaded + dpdk_drivers = [mod["Name"] for mod in mods if mod["Found"]] + + +def has_driver(dev_id): + '''return true if a device is assigned to a driver. False otherwise''' + return "Driver_str" in devices[dev_id] + + +def get_pci_device_details(dev_id): + '''This function gets additional details for a PCI device''' + device = {} + + extra_info = check_output(["lspci", "-vmmks", dev_id]).splitlines() + + # parse lspci details + for line in extra_info: + if len(line) == 0: + continue + name, value = line.decode().split("\t", 1) + name = name.strip(":") + "_str" + device[name] = value + # check for a unix interface name + device["Interface"] = "" + for base, dirs, _ in os.walk("/sys/bus/pci/devices/%s/" % dev_id): + if "net" in dirs: + device["Interface"] = \ + ",".join(os.listdir(os.path.join(base, "net"))) + break + # check if a port is used for ssh connection + device["Ssh_if"] = False + device["Active"] = "" + + return device + + +def get_nic_details(): + '''This function populates the "devices" dictionary. The keys used are + the pci addresses (domain:bus:slot.func). The values are themselves + dictionaries - one for each NIC.''' + global devices + global dpdk_drivers + + # clear any old data + devices = {} + # first loop through and read details for all devices + # request machine readable format, with numeric IDs + dev = {} + dev_lines = check_output(["lspci", "-Dvmmn"]).splitlines() + for dev_line in dev_lines: + if (len(dev_line) == 0): + if dev["Class"][0:2] == NETWORK_BASE_CLASS: + # convert device and vendor ids to numbers, then add to global + dev["Vendor"] = int(dev["Vendor"], 16) + dev["Device"] = int(dev["Device"], 16) + # use dict to make copy of dev + devices[dev["Slot"]] = dict(dev) + else: + name, value = dev_line.decode().split("\t", 1) + dev[name.rstrip(":")] = value + + # check what is the interface if any for an ssh connection if + # any to this host, so we can mark it later. + ssh_if = [] + route = check_output(["ip", "-o", "route"]) + # filter out all lines for 169.254 routes + route = "\n".join(filter(lambda ln: not ln.startswith("169.254"), + route.decode().splitlines())) + rt_info = route.split() + for i in range(len(rt_info) - 1): + if rt_info[i] == "dev": + ssh_if.append(rt_info[i+1]) + + # based on the basic info, get extended text details + for d in devices.keys(): + # get additional info and add it to existing data + devices[d] = devices[d].copy() + devices[d].update(get_pci_device_details(d).items()) + + for _if in ssh_if: + if _if in devices[d]["Interface"].split(","): + devices[d]["Ssh_if"] = True + devices[d]["Active"] = "*Active*" + break + + # add igb_uio to list of supporting modules if needed + if "Module_str" in devices[d]: + for driver in dpdk_drivers: + if driver not in devices[d]["Module_str"]: + devices[d]["Module_str"] = \ + devices[d]["Module_str"] + ",%s" % driver + else: + devices[d]["Module_str"] = ",".join(dpdk_drivers) + + # make sure the driver and module strings do not have any duplicates + if has_driver(d): + modules = devices[d]["Module_str"].split(",") + if devices[d]["Driver_str"] in modules: + modules.remove(devices[d]["Driver_str"]) + devices[d]["Module_str"] = ",".join(modules) + + +def get_crypto_details(): + '''This function populates the "devices" dictionary. The keys used are + the pci addresses (domain:bus:slot.func). The values are themselves + dictionaries - one for each NIC.''' + global devices + global dpdk_drivers + + # clear any old data + # devices = {} + # first loop through and read details for all devices + # request machine readable format, with numeric IDs + dev = {} + dev_lines = check_output(["lspci", "-Dvmmn"]).splitlines() + for dev_line in dev_lines: + if (len(dev_line) == 0): + if (dev["Class"][0:2] == CRYPTO_BASE_CLASS): + # convert device and vendor ids to numbers, then add to global + dev["Vendor"] = int(dev["Vendor"], 16) + dev["Device"] = int(dev["Device"], 16) + # use dict to make copy of dev + devices[dev["Slot"]] = dict(dev) + else: + name, value = dev_line.decode().split("\t", 1) + dev[name.rstrip(":")] = value + + # based on the basic info, get extended text details + for d in devices.keys(): + # get additional info and add it to existing data + devices[d] = devices[d].copy() + devices[d].update(get_pci_device_details(d).items()) + + # add igb_uio to list of supporting modules if needed + if "Module_str" in devices[d]: + for driver in dpdk_drivers: + if driver not in devices[d]["Module_str"]: + devices[d]["Module_str"] = \ + devices[d]["Module_str"] + ",%s" % driver + else: + devices[d]["Module_str"] = ",".join(dpdk_drivers) + + # make sure the driver and module strings do not have any duplicates + if has_driver(d): + modules = devices[d]["Module_str"].split(",") + if devices[d]["Driver_str"] in modules: + modules.remove(devices[d]["Driver_str"]) + devices[d]["Module_str"] = ",".join(modules) + + +def dev_id_from_dev_name(dev_name): + '''Take a device "name" - a string passed in by user to identify a NIC + device, and determine the device id - i.e. the domain:bus:slot.func - for + it, which can then be used to index into the devices array''' + + # check if it's already a suitable index + if dev_name in devices: + return dev_name + # check if it's an index just missing the domain part + elif "0000:" + dev_name in devices: + return "0000:" + dev_name + else: + # check if it's an interface name, e.g. eth1 + for d in devices.keys(): + if dev_name in devices[d]["Interface"].split(","): + return devices[d]["Slot"] + # if nothing else matches - error + print("Unknown device: %s. " + "Please specify device in \"bus:slot.func\" format" % dev_name) + sys.exit(1) + + +def unbind_one(dev_id, force): + '''Unbind the device identified by "dev_id" from its current driver''' + dev = devices[dev_id] + if not has_driver(dev_id): + print("%s %s %s is not currently managed by any driver\n" % + (dev["Slot"], dev["Device_str"], dev["Interface"])) + return + + # prevent us disconnecting ourselves + if dev["Ssh_if"] and not force: + print("Routing table indicates that interface %s is active. " + "Skipping unbind" % (dev_id)) + return + + # write to /sys to unbind + filename = "/sys/bus/pci/drivers/%s/unbind" % dev["Driver_str"] + try: + f = open(filename, "a") + except: + print("Error: unbind failed for %s - Cannot open %s" + % (dev_id, filename)) + sys.exit(1) + f.write(dev_id) + f.close() + + +def bind_one(dev_id, driver, force): + '''Bind the device given by "dev_id" to the driver "driver". If the device + is already bound to a different driver, it will be unbound first''' + dev = devices[dev_id] + saved_driver = None # used to rollback any unbind in case of failure + + # prevent disconnection of our ssh session + if dev["Ssh_if"] and not force: + print("Routing table indicates that interface %s is active. " + "Not modifying" % (dev_id)) + return + + # unbind any existing drivers we don't want + if has_driver(dev_id): + if dev["Driver_str"] == driver: + print("%s already bound to driver %s, skipping\n" + % (dev_id, driver)) + return + else: + saved_driver = dev["Driver_str"] + unbind_one(dev_id, force) + dev["Driver_str"] = "" # clear driver string + + # if we are binding to one of DPDK drivers, add PCI id's to that driver + if driver in dpdk_drivers: + filename = "/sys/bus/pci/drivers/%s/new_id" % driver + try: + f = open(filename, "w") + except: + print("Error: bind failed for %s - Cannot open %s" + % (dev_id, filename)) + return + try: + f.write("%04x %04x" % (dev["Vendor"], dev["Device"])) + f.close() + except: + print("Error: bind failed for %s - Cannot write new PCI ID to " + "driver %s" % (dev_id, driver)) + return + + # do the bind by writing to /sys + filename = "/sys/bus/pci/drivers/%s/bind" % driver + try: + f = open(filename, "a") + except: + print("Error: bind failed for %s - Cannot open %s" + % (dev_id, filename)) + if saved_driver is not None: # restore any previous driver + bind_one(dev_id, saved_driver, force) + return + try: + f.write(dev_id) + f.close() + except: + # for some reason, closing dev_id after adding a new PCI ID to new_id + # results in IOError. however, if the device was successfully bound, + # we don't care for any errors and can safely ignore IOError + tmp = get_pci_device_details(dev_id) + if "Driver_str" in tmp and tmp["Driver_str"] == driver: + return + print("Error: bind failed for %s - Cannot bind to driver %s" + % (dev_id, driver)) + if saved_driver is not None: # restore any previous driver + bind_one(dev_id, saved_driver, force) + return + + +def unbind_all(dev_list, force=False): + """Unbind method, takes a list of device locations""" + dev_list = map(dev_id_from_dev_name, dev_list) + for d in dev_list: + unbind_one(d, force) + + +def bind_all(dev_list, driver, force=False): + """Bind method, takes a list of device locations""" + global devices + + dev_list = map(dev_id_from_dev_name, dev_list) + + for d in dev_list: + bind_one(d, driver, force) + + # when binding devices to a generic driver (i.e. one that doesn't have a + # PCI ID table), some devices that are not bound to any other driver could + # be bound even if no one has asked them to. hence, we check the list of + # drivers again, and see if some of the previously-unbound devices were + # erroneously bound. + for d in devices.keys(): + # skip devices that were already bound or that we know should be bound + if "Driver_str" in devices[d] or d in dev_list: + continue + + # update information about this device + devices[d] = dict(devices[d].items() + + get_pci_device_details(d).items()) + + # check if updated information indicates that the device was bound + if "Driver_str" in devices[d]: + unbind_one(d, force) + + +def display_devices(title, dev_list, extra_params=None): + '''Displays to the user the details of a list of devices given in + "dev_list". The "extra_params" parameter, if given, should contain a string + with %()s fields in it for replacement by the named fields in each + device's dictionary.''' + strings = [] # this holds the strings to print. We sort before printing + print("\n%s" % title) + print("="*len(title)) + if len(dev_list) == 0: + strings.append("") + else: + for dev in dev_list: + if extra_params is not None: + strings.append("%s '%s' %s" % (dev["Slot"], + dev["Device_str"], extra_params % dev)) + else: + strings.append("%s '%s'" % (dev["Slot"], dev["Device_str"])) + # sort before printing, so that the entries appear in PCI order + strings.sort() + print("\n".join(strings)) # print one per line + + +def show_status(): + '''Function called when the script is passed the "--status" option. + Displays to the user what devices are bound to the igb_uio driver, the + kernel driver or to no driver''' + global dpdk_drivers + kernel_drv = [] + dpdk_drv = [] + no_drv = [] + + # split our list of network devices into the three categories above + for d in devices.keys(): + if (NETWORK_BASE_CLASS in devices[d]["Class"]): + if not has_driver(d): + no_drv.append(devices[d]) + continue + if devices[d]["Driver_str"] in dpdk_drivers: + dpdk_drv.append(devices[d]) + else: + kernel_drv.append(devices[d]) + + # print each category separately, so we can clearly see what's used by DPDK + display_devices("Network devices using DPDK-compatible driver", dpdk_drv, + "drv=%(Driver_str)s unused=%(Module_str)s") + display_devices("Network devices using kernel driver", kernel_drv, + "if=%(Interface)s drv=%(Driver_str)s " + "unused=%(Module_str)s %(Active)s") + display_devices("Other network devices", no_drv, "unused=%(Module_str)s") + + # split our list of crypto devices into the three categories above + kernel_drv = [] + dpdk_drv = [] + no_drv = [] + + for d in devices.keys(): + if (CRYPTO_BASE_CLASS in devices[d]["Class"]): + if not has_driver(d): + no_drv.append(devices[d]) + continue + if devices[d]["Driver_str"] in dpdk_drivers: + dpdk_drv.append(devices[d]) + else: + kernel_drv.append(devices[d]) + + display_devices("Crypto devices using DPDK-compatible driver", dpdk_drv, + "drv=%(Driver_str)s unused=%(Module_str)s") + display_devices("Crypto devices using kernel driver", kernel_drv, + "drv=%(Driver_str)s " + "unused=%(Module_str)s") + display_devices("Other crypto devices", no_drv, "unused=%(Module_str)s") + + +def parse_args(): + '''Parses the command-line arguments given by the user and takes the + appropriate action for each''' + global b_flag + global status_flag + global force_flag + global args + if len(sys.argv) <= 1: + usage() + sys.exit(0) + + try: + opts, args = getopt.getopt(sys.argv[1:], "b:us", + ["help", "usage", "status", "force", + "bind=", "unbind"]) + except getopt.GetoptError as error: + print(str(error)) + print("Run '%s --usage' for further information" % sys.argv[0]) + sys.exit(1) + + for opt, arg in opts: + if opt == "--help" or opt == "--usage": + usage() + sys.exit(0) + if opt == "--status" or opt == "-s": + status_flag = True + if opt == "--force": + force_flag = True + if opt == "-b" or opt == "-u" or opt == "--bind" or opt == "--unbind": + if b_flag is not None: + print("Error - Only one bind or unbind may be specified\n") + sys.exit(1) + if opt == "-u" or opt == "--unbind": + b_flag = "none" + else: + b_flag = arg + + +def do_arg_actions(): + '''do the actual action requested by the user''' + global b_flag + global status_flag + global force_flag + global args + + if b_flag is None and not status_flag: + print("Error: No action specified for devices." + "Please give a -b or -u option") + print("Run '%s --usage' for further information" % sys.argv[0]) + sys.exit(1) + + if b_flag is not None and len(args) == 0: + print("Error: No devices specified.") + print("Run '%s --usage' for further information" % sys.argv[0]) + sys.exit(1) + + if b_flag == "none" or b_flag == "None": + unbind_all(args, force_flag) + elif b_flag is not None: + bind_all(args, b_flag, force_flag) + if status_flag: + if b_flag is not None: + get_nic_details() # refresh if we have changed anything + get_crypto_details() # refresh if we have changed anything + show_status() + + +def main(): + '''program main function''' + parse_args() + check_modules() + get_nic_details() + get_crypto_details() + do_arg_actions() + +if __name__ == "__main__": + main() diff --git a/extras/vpp_config/scripts/vpp-config b/extras/vpp_config/scripts/vpp-config new file mode 100755 index 00000000000..8f5314bca72 --- /dev/null +++ b/extras/vpp_config/scripts/vpp-config @@ -0,0 +1,30 @@ +#!/usr/bin/python + +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""VPP Configuration Utility Wrapper""" + +import os +import sys +import vpp_config as vppcfg + +# Check for root +if not os.geteuid() == 0: + sys.exit('\nPlease run the VPP Configuration Utility as root.') + +# Setup +vppcfg.autoconfig_setup() + +# Main menu +vppcfg.autoconfig_main() diff --git a/extras/vpp_config/setup.cfg b/extras/vpp_config/setup.cfg new file mode 100644 index 00000000000..79bc67848ff --- /dev/null +++ b/extras/vpp_config/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=1 diff --git a/extras/vpp_config/setup.py b/extras/vpp_config/setup.py new file mode 100644 index 00000000000..3444e685f58 --- /dev/null +++ b/extras/vpp_config/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup + +setup(name="vpp_config", + version="17.10.1", + author="John DeNisco", + author_email="jdenisco@cisco.com", + description="VPP Configuration Utility", + license = 'Apache-2.0', + keywords="vppconfig", + url = 'https://wiki.fd.io/view/VPP', + py_modules=['vpp_config'], + install_requires=['pyyaml'], + packages=['vpplib'], + scripts=['scripts/vpp-config'], + data_files=[('vpp/vpp-config/scripts', ['scripts/dpdk-devbind.py']), + ('vpp/vpp-config/configs', ['data/auto-config.yaml']), + ('vpp/vpp-config/dryrun/sysctl.d', ['data/80-vpp.conf.template']), + ('vpp/vpp-config/dryrun/default', ['data/grub.template']), + ('vpp/vpp-config/dryrun/vpp', ['data/startup.conf.template']), + ], + long_description="The VPP configuration utility can be used to easily configure VPP.", + ) diff --git a/extras/vpp_config/vpp_config.py b/extras/vpp_config/vpp_config.py new file mode 100755 index 00000000000..2e644185236 --- /dev/null +++ b/extras/vpp_config/vpp_config.py @@ -0,0 +1,573 @@ +#!/usr/bin/python + +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""VPP Configuration Main Entry""" + +import re +import os +import sys +import logging + +from vpplib.AutoConfig import AutoConfig +from vpplib.VPPUtil import VPPUtil + +VPP_DRYRUNDIR = '/vpp/vpp-config/dryrun' +VPP_AUTO_CONFIGURATION_FILE = '/vpp/vpp-config/configs/auto-config.yaml' +VPP_HUGE_PAGE_FILE = '/vpp/vpp-config/dryrun/sysctl.d/80-vpp.conf' +VPP_STARTUP_FILE = '/vpp/vpp-config/dryrun/vpp/startup.conf' +VPP_GRUB_FILE = '/vpp/vpp-config/dryrun/default/grub' +VPP_REAL_HUGE_PAGE_FILE = '/etc/sysctl.d/80-vpp.conf' +VPP_REAL_STARTUP_FILE = '/etc/vpp/startup.conf' +VPP_REAL_GRUB_FILE = '/etc/default/grub' + +rootdir = '' + + +def autoconfig_yn(question, default): + """ + Ask the user a yes or no question. + + :param question: The text of the question + :param default: Value to be returned if '\n' is entered + :type question: string + :type default: string + :returns: The Answer + :rtype: string + """ + input_valid = False + default = default.lower() + answer = '' + while not input_valid: + answer = raw_input(question) + if len(answer) == 0: + answer = default + if re.findall(r'[YyNn]', answer): + input_valid = True + answer = answer[0].lower() + else: + print "Please answer Y, N or Return." + + return answer + + +def autoconfig_cp(node, src, dst): + """ + Copies a file, saving the original if needed. + + :param node: Node dictionary with cpuinfo. + :param src: Source File + :param dst: Destination file + :type node: dict + :type src: string + :type dst: string + :raises RuntimeError: If command fails + """ + + # If the destination file exist, create a copy if one does not already + # exist + ofile = dst + '.orig' + (ret, stdout, stderr) = VPPUtil.exec_command('ls {}'.format(dst)) + if ret == 0: + cmd = 'cp {} {}'.format(dst, ofile) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, + node['host'], + stdout, + stderr)) + + # Copy the source file + cmd = 'cp {} {}'.format(src, os.path.dirname(dst)) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'. + format(cmd, node['host'], stderr)) + + +def autoconfig_diff(node, src, dst): + """ + Returns the diffs of 2 files. + + :param node: Node dictionary with cpuinfo. + :param src: Source File + :param dst: Destination file + :type node: dict + :type src: string + :type dst: string + :returns: The Answer + :rtype: string + :raises RuntimeError: If command fails + """ + + # Diff the files and return the output + cmd = "diff {} {}".format(src, dst) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if stderr != '': + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, + node['host'], + ret, + stderr)) + + return stdout + + +def autoconfig_show_system(): + """ + Shows the system information. + + """ + + acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE) + + acfg.discover() + + acfg.sys_info() + + +def autoconfig_hugepage_apply(node): + """ + Apply the huge page configuration. + :param node: The node structure + :type node: dict + :returns: -1 if the caller should return, 0 if not + :rtype: int + + """ + + diffs = autoconfig_diff(node, VPP_REAL_HUGE_PAGE_FILE, rootdir + VPP_HUGE_PAGE_FILE) + if diffs != '': + print "These are the changes we will apply to" + print "the huge page file ({}).\n".format(VPP_REAL_HUGE_PAGE_FILE) + print diffs + answer = autoconfig_yn( + "\nAre you sure you want to apply these changes [Y/n]? ", + 'y') + if answer == 'n': + return -1 + + # Copy and sysctl + autoconfig_cp(node, rootdir + VPP_HUGE_PAGE_FILE, VPP_REAL_HUGE_PAGE_FILE) + cmd = "sysctl -p {}".format(VPP_REAL_HUGE_PAGE_FILE) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, node['host'], stdout, stderr)) + else: + print '\nThere are no changes to the huge page configuration.' + + return 0 + + +def autoconfig_vpp_apply(node): + """ + Apply the vpp configuration. + + :param node: The node structure + :type node: dict + :returns: -1 if the caller should return, 0 if not + :rtype: int + + """ + + cmd = "service vpp stop" + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, node['host'], stdout, stderr)) + + diffs = autoconfig_diff(node, VPP_REAL_STARTUP_FILE, rootdir + VPP_STARTUP_FILE) + if diffs != '': + print "These are the changes we will apply to" + print "the VPP startup file ({}).\n".format(VPP_REAL_STARTUP_FILE) + print diffs + answer = autoconfig_yn( + "\nAre you sure you want to apply these changes [Y/n]? ", + 'y') + if answer == 'n': + return -1 + + # Copy the VPP startup + autoconfig_cp(node, rootdir + VPP_STARTUP_FILE, VPP_REAL_STARTUP_FILE) + else: + print '\nThere are no changes to VPP startup.' + + return 0 + + +def autoconfig_grub_apply(node): + """ + Apply the grub configuration. + + :param node: The node structure + :type node: dict + :returns: -1 if the caller should return, 0 if not + :rtype: int + + """ + print "\nThe configured grub cmdline looks like this:" + configured_cmdline = node['grub']['default_cmdline'] + current_cmdline = node['grub']['current_cmdline'] + print configured_cmdline + print "\nThe current boot cmdline looks like this:" + print current_cmdline + question = "\nDo you want to keep the current boot cmdline [Y/n]? " + answer = autoconfig_yn(question, 'y') + if answer == 'n': + node['grub']['keep_cmdline'] = False + + # Diff the file + diffs = autoconfig_diff(node, VPP_REAL_GRUB_FILE, rootdir + VPP_GRUB_FILE) + if diffs != '': + print "These are the changes we will apply to" + print "the GRUB file ({}).\n".format(VPP_REAL_GRUB_FILE) + print diffs + answer = autoconfig_yn( + "\nAre you sure you want to apply these changes [y/N]? ", + 'n') + if answer == 'n': + return -1 + + # Copy and update grub + autoconfig_cp(node, rootdir + VPP_GRUB_FILE, VPP_REAL_GRUB_FILE) + distro = VPPUtil.get_linux_distro() + if distro[0] == 'Ubuntu': + cmd = "update-grub" + else: + cmd = "grub2-mkconfig -o /boot/grub2/grub.cfg" + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, + node['host'], + stdout, + stderr)) + print "There have been changes to the GRUB config a", + print "reboot will be required." + return -1 + else: + print '\nThere are no changes to the GRUB config.' + + return 0 + + +def autoconfig_apply(): + """ + Apply the configuration. + + Show the diff of the dryrun file and the actual configuration file + Copy the files from the dryrun directory to the actual file. + Peform the system function + + """ + + vutil = VPPUtil() + pkgs = vutil.get_installed_vpp_pkgs() + if len(pkgs) == 0: + print "\nVPP is not installed, Install VPP with option 4." + return + + acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE) + + print "\nWe are now going to configure your system(s).\n" + answer = autoconfig_yn("Are you sure you want to do this [Y/n]? ", 'y') + if answer == 'n': + return + + nodes = acfg.get_nodes() + for i in nodes.items(): + node = i[1] + + # Check the system resources + if not acfg.min_system_resources(node): + return + + # Huge Pages + ret = autoconfig_hugepage_apply(node) + if ret != 0: + return + + # VPP + ret = autoconfig_vpp_apply(node) + if ret != 0: + return + + # Grub + ret = autoconfig_grub_apply(node) + if ret != 0: + return + + # Everything is configured start vpp + cmd = "service vpp start" + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, node['host'], stdout, stderr)) + + +def autoconfig_dryrun(): + """ + Execute the dryrun function. + + """ + + vutil = VPPUtil() + pkgs = vutil.get_installed_vpp_pkgs() + if len(pkgs) == 0: + print "\nVPP is not installed, install VPP with option 4." + return + + acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE) + + # Stop VPP on each node + nodes = acfg.get_nodes() + for i in nodes.items(): + node = i[1] + VPPUtil.stop(node) + + # Discover + acfg.discover() + + # Check the system resources + nodes = acfg.get_nodes() + for i in nodes.items(): + node = i[1] + if not acfg.min_system_resources(node): + return + + # Modify the devices + acfg.modify_devices() + + # Modify CPU + acfg.modify_cpu() + + # Calculate the cpu parameters + acfg.calculate_cpu_parameters() + + # Acquire TCP stack parameters + acfg.acquire_tcp_params() + + # Apply the startup + acfg.apply_vpp_startup() + + # Apply the grub configuration + acfg.apply_grub_cmdline() + + # Huge Pages + acfg.modify_huge_pages() + acfg.apply_huge_pages() + + +def autoconfig_install(): + """ + Install or Uninstall VPP. + + """ + + # Since these commands will take a while, we + # want to see the progress + logger = logging.getLogger() + + acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE) + vutil = VPPUtil() + + nodes = acfg.get_nodes() + for i in nodes.items(): + node = i[1] + + pkgs = vutil.get_installed_vpp_pkgs() + + if len(pkgs) > 0: + print "\nThese packages are installed on node {}" \ + .format(node['host']) + print "{:25} {}".format("Name", "Version") + for pkg in pkgs: + if 'version' in pkg: + print "{:25} {}".format( + pkg['name'], pkg['version']) + else: + print "{}".format(pkg['name']) + + question = "\nDo you want to uninstall these " + question += "packages [y/N]? " + answer = autoconfig_yn(question, 'n') + if answer == 'y': + logger.setLevel(logging.INFO) + vutil.uninstall_vpp(node) + else: + print "\nThere are no VPP packages on node {}." \ + .format(node['host']) + question = "Do you want to install VPP [Y/n]? " + answer = autoconfig_yn(question, 'y') + if answer == 'y': + logger.setLevel(logging.INFO) + vutil.install_vpp(node) + + # Set the logging level back + logger.setLevel(logging.ERROR) + + +def autoconfig_patch_qemu(): + """ + Patch the correct qemu version that is needed for openstack + + """ + + # Since these commands will take a while, we + # want to see the progress + logger = logging.getLogger() + + acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE) + + nodes = acfg.get_nodes() + for i in nodes.items(): + node = i[1] + + logger.setLevel(logging.INFO) + acfg.patch_qemu(node) + + +def autoconfig_not_implemented(): + """ + This feature is not implemented + + """ + + print "\nThis Feature is not implemented yet...." + + +def autoconfig_main_menu(): + """ + The auto configuration main menu + + """ + + main_menu_text = '\nWhat would you like to do?\n\n\ +1) Show basic system information\n\ +2) Dry Run (Will save the configuration files in {}/vpp/vpp-config/dryrun for inspection)\n\ + and user input in {}/vpp/vpp-config/configs/auto-config.yaml\n\ +3) Full configuration (WARNING: This will change the system configuration)\n\ +4) List/Install/Uninstall VPP.\n\ +9 or q) Quit'.format(rootdir, rootdir) + + # 5) Dry Run from {}/vpp/vpp-config/auto-config.yaml (will not ask questions).\n\ + # 6) Install QEMU patch (Needed when running openstack).\n\ + + print "{}".format(main_menu_text) + + input_valid = False + answer = '' + while not input_valid: + answer = raw_input("\nCommand: ") + if len(answer) > 1: + print "Please enter only 1 character." + continue + if re.findall(r'[Qq1-79]', answer): + input_valid = True + answer = answer[0].lower() + else: + print "Please enter a character between 1 and 7 or 9." + + if answer == '9': + answer = 'q' + return answer + + +def autoconfig_main(): + """ + The auto configuration main entry point + + """ + + answer = '' + while answer != 'q': + answer = autoconfig_main_menu() + if answer == '1': + autoconfig_show_system() + elif answer == '2': + autoconfig_dryrun() + elif answer == '3': + autoconfig_apply() + elif answer == '4': + autoconfig_install() + elif answer == '9' or answer == 'q': + return + else: + autoconfig_not_implemented() + + +def autoconfig_setup(): + """ + The auto configuration setup function. + + We will copy the configuration files to the dryrun directory. + + """ + + global rootdir + + logging.basicConfig(level=logging.ERROR) + + distro = VPPUtil.get_linux_distro() + if distro[0] == 'Ubuntu': + rootdir = '/usr/local' + else: + rootdir = '/usr' + + # If there is a system configuration file use that, if not use the initial auto-config file + filename = rootdir + VPP_AUTO_CONFIGURATION_FILE + if os.path.isfile(filename) is True: + acfg = AutoConfig(rootdir, VPP_AUTO_CONFIGURATION_FILE) + else: + raise RuntimeError('The Auto configuration file does not exist {}'. + format(filename)) + + print "\nWelcome to the VPP system configuration utility" + + print "\nThese are the files we will modify:" + print " /etc/vpp/startup.conf" + print " /etc/sysctl.d/80-vpp.conf" + print " /etc/default/grub" + + print "\nBefore we change them, we'll create working copies in {}".format(rootdir + VPP_DRYRUNDIR) + print "Please inspect them carefully before applying the actual configuration (option 3)!" + + nodes = acfg.get_nodes() + for i in nodes.items(): + node = i[1] + + if (os.path.isfile(rootdir + VPP_STARTUP_FILE) is not True) and \ + (os.path.isfile(VPP_REAL_STARTUP_FILE) is True): + autoconfig_cp(node, VPP_REAL_STARTUP_FILE, '{}'.format(rootdir + VPP_STARTUP_FILE)) + if (os.path.isfile(rootdir + VPP_HUGE_PAGE_FILE) is not True) and \ + (os.path.isfile(VPP_REAL_HUGE_PAGE_FILE) is True): + autoconfig_cp(node, VPP_REAL_HUGE_PAGE_FILE, '{}'.format(rootdir + VPP_HUGE_PAGE_FILE)) + if (os.path.isfile(rootdir + VPP_GRUB_FILE) is not True) and \ + (os.path.isfile(VPP_REAL_GRUB_FILE) is True): + autoconfig_cp(node, VPP_REAL_GRUB_FILE, '{}'.format(rootdir + VPP_GRUB_FILE)) + + +if __name__ == '__main__': + + # Check for root + if not os.geteuid() == 0: + sys.exit('\nPlease run the VPP Configuration Utility as root.') + + # Setup + autoconfig_setup() + + # Main menu + autoconfig_main() diff --git a/extras/vpp_config/vpplib/AutoConfig.py b/extras/vpp_config/vpplib/AutoConfig.py new file mode 100644 index 00000000000..49c7d54257c --- /dev/null +++ b/extras/vpp_config/vpplib/AutoConfig.py @@ -0,0 +1,1427 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Library that supports Auto Configuration.""" + +import logging +import os +import re +import yaml + +from vpplib.VPPUtil import VPPUtil +from vpplib.VppPCIUtil import VppPCIUtil +from vpplib.VppHugePageUtil import VppHugePageUtil +from vpplib.CpuUtils import CpuUtils +from vpplib.VppGrubUtil import VppGrubUtil +from vpplib.QemuUtils import QemuUtils + +__all__ = ["AutoConfig"] + +# Constants +MIN_SYSTEM_CPUS = 2 +MIN_TOTAL_HUGE_PAGES = 1024 +MAX_PERCENT_FOR_HUGE_PAGES = 70 + + +class AutoConfig(object): + """Auto Configuration Tools""" + + def __init__(self, rootdir, filename): + """ + The Auto Configure class. + + :param rootdir: The root directory for all the auto configuration files + :param filename: The autoconfiguration file + :type rootdir: str + :type filename: str + """ + self._autoconfig_filename = rootdir + filename + self._rootdir = rootdir + self._metadata = {} + self._nodes = {} + self._vpp_devices_node = {} + self._hugepage_config = "" + self._loadconfig() + + def get_nodes(self): + """ + Returns the nodes dictionary. + + :returns: The nodes + :rtype: dictionary + """ + + return self._nodes + + @staticmethod + def _autoconfig_backup_file(filename): + """ + Create a backup file. + + :param filename: The file to backup + :type filename: str + """ + + # Does a copy of the file exist, if not create one + ofile = filename + '.orig' + (ret, stdout, stderr) = VPPUtil.exec_command('ls {}'.format(ofile)) + if ret != 0: + logging.debug(stderr) + if stdout.strip('\n') != ofile: + cmd = 'sudo cp {} {}'.format(filename, ofile) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + logging.debug(stderr) + + @staticmethod + def _ask_user_range(question, first, last, default): + """ + Asks the user for a number within a range. + default is returned if return is entered. + + :param question: Text of a question. + :param first: First number in the range + :param last: Last number in the range + :param default: The value returned when return is entered + :type question: string + :type first: int + :type last: int + :type default: int + :returns: The answer to the question + :rtype: int + """ + + while True: + answer = raw_input(question) + if answer == '': + answer = default + break + if re.findall(r'[0-9+]', answer): + if int(answer) in range(first, last + 1): + break + else: + print "Please a value between {} and {} or Return.". \ + format(first, last) + else: + print "Please a number between {} and {} or Return.". \ + format(first, last) + + return int(answer) + + @staticmethod + def _ask_user_yn(question, default): + """ + Asks the user for a yes or no question. + + :param question: Text of a question. + :param default: The value returned when return is entered + :type question: string + :type default: string + :returns: The answer to the question + :rtype: string + """ + + input_valid = False + default = default.lower() + answer = '' + while not input_valid: + answer = raw_input(question) + if answer == '': + answer = default + if re.findall(r'[YyNn]', answer): + input_valid = True + answer = answer[0].lower() + else: + print "Please answer Y, N or Return." + + return answer + + def _loadconfig(self): + """ + Load the testbed configuration, given the auto configuration file. + + """ + + # Get the Topology, from the topology layout file + topo = {} + with open(self._autoconfig_filename, 'r') as stream: + try: + topo = yaml.load(stream) + if 'metadata' in topo: + self._metadata = topo['metadata'] + except yaml.YAMLError as exc: + raise RuntimeError("Couldn't read the Auto config file {}.".format(self._autoconfig_filename, exc)) + + systemfile = self._rootdir + self._metadata['system_config_file'] + if os.path.isfile(systemfile): + with open(systemfile, 'r') as sysstream: + try: + systopo = yaml.load(sysstream) + if 'nodes' in systopo: + self._nodes = systopo['nodes'] + except yaml.YAMLError as sysexc: + raise RuntimeError("Couldn't read the System config file {}.".format(systemfile, sysexc)) + else: + # Get the nodes from Auto Config + if 'nodes' in topo: + self._nodes = topo['nodes'] + + # Set the root directory in all the nodes + for i in self._nodes.items(): + node = i[1] + node['rootdir'] = self._rootdir + + def updateconfig(self): + """ + Update the testbed configuration, given the auto configuration file. + We will write the system configuration file with the current node + information + + """ + + # Initialize the yaml data + ydata = {'metadata': self._metadata, 'nodes': self._nodes} + + # Write the system config file + filename = self._rootdir + self._metadata['system_config_file'] + with open(filename, 'w') as yamlfile: + yaml.dump(ydata, yamlfile, default_flow_style=False) + + def _update_auto_config(self): + """ + Write the auto configuration file with the new configuration data, + input from the user. + + """ + + # Initialize the yaml data + nodes = {} + with open(self._autoconfig_filename, 'r') as stream: + try: + ydata = yaml.load(stream) + if 'nodes' in ydata: + nodes = ydata['nodes'] + except yaml.YAMLError as exc: + print exc + return + + for i in nodes.items(): + key = i[0] + node = i[1] + + # Interfaces + node['interfaces'] = {} + for item in self._nodes[key]['interfaces'].items(): + port = item[0] + interface = item[1] + + node['interfaces'][port] = {} + node['interfaces'][port]['pci_address'] = \ + interface['pci_address'] + if 'mac_address' in interface: + node['interfaces'][port]['mac_address'] = \ + interface['mac_address'] + + if 'total_other_cpus' in self._nodes[key]['cpu']: + node['cpu']['total_other_cpus'] = \ + self._nodes[key]['cpu']['total_other_cpus'] + if 'total_vpp_cpus' in self._nodes[key]['cpu']: + node['cpu']['total_vpp_cpus'] = \ + self._nodes[key]['cpu']['total_vpp_cpus'] + if 'reserve_vpp_main_core' in self._nodes[key]['cpu']: + node['cpu']['reserve_vpp_main_core'] = \ + self._nodes[key]['cpu']['reserve_vpp_main_core'] + + # TCP + if 'active_open_sessions' in self._nodes[key]['tcp']: + node['tcp']['active_open_sessions'] = \ + self._nodes[key]['tcp']['active_open_sessions'] + if 'passive_open_sessions' in self._nodes[key]['tcp']: + node['tcp']['passive_open_sessions'] = \ + self._nodes[key]['tcp']['passive_open_sessions'] + + # Huge pages + node['hugepages']['total'] = self._nodes[key]['hugepages']['total'] + + # Write the auto config config file + with open(self._autoconfig_filename, 'w') as yamlfile: + yaml.dump(ydata, yamlfile, default_flow_style=False) + + def apply_huge_pages(self): + """ + Apply the huge page config + + """ + + for i in self._nodes.items(): + node = i[1] + + hpg = VppHugePageUtil(node) + hpg.hugepages_dryrun_apply() + + @staticmethod + def _apply_vpp_unix(node): + """ + Apply the VPP Unix config + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + + unix = ' nodaemon\n' + if 'unix' not in node['vpp']: + return '' + + unixv = node['vpp']['unix'] + if 'interactive' in unixv: + interactive = unixv['interactive'] + if interactive is True: + unix = ' interactive\n' + + return unix.rstrip('\n') + + @staticmethod + def _apply_vpp_cpu(node): + """ + Apply the VPP cpu config + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + + # Get main core + cpu = '\n' + vpp_main_core = node['cpu']['vpp_main_core'] + if vpp_main_core is not 0: + cpu += ' main-core {}\n'.format(vpp_main_core) + + # Get workers + vpp_workers = node['cpu']['vpp_workers'] + vpp_worker_len = len(vpp_workers) + if vpp_worker_len > 0: + vpp_worker_str = '' + for i, worker in enumerate(vpp_workers): + if i > 0: + vpp_worker_str += ',' + if worker[0] == worker[1]: + vpp_worker_str += "{}".format(worker[0]) + else: + vpp_worker_str += "{}-{}".format(worker[0], worker[1]) + + cpu += ' corelist-workers {}\n'.format(vpp_worker_str) + + return cpu + + @staticmethod + def _apply_vpp_devices(node): + """ + Apply VPP PCI Device configuration to vpp startup. + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + + devices = '' + ports_per_numa = node['cpu']['ports_per_numa'] + total_mbufs = node['cpu']['total_mbufs'] + + for item in ports_per_numa.items(): + value = item[1] + interfaces = value['interfaces'] + + # if 0 was specified for the number of vpp workers, use 1 queue + num_rx_queues = None + num_tx_queues = None + if 'rx_queues' in value: + num_rx_queues = value['rx_queues'] + if 'tx_queues' in value: + num_tx_queues = value['tx_queues'] + + num_rx_desc = None + num_tx_desc = None + + # Create the devices string + for interface in interfaces: + pci_address = interface['pci_address'] + pci_address = pci_address.lstrip("'").rstrip("'") + devices += '\n' + devices += ' dev {} {{ \n'.format(pci_address) + if num_rx_queues: + devices += ' num-rx-queues {}\n'.format(num_rx_queues) + else: + devices += ' num-rx-queues {}\n'.format(1) + if num_tx_queues: + devices += ' num-tx-queues {}\n'.format(num_tx_queues) + if num_rx_desc: + devices += ' num-rx-desc {}\n'.format(num_rx_desc) + if num_tx_desc: + devices += ' num-tx-desc {}\n'.format(num_tx_desc) + devices += ' }' + + if total_mbufs is not 0: + devices += '\n num-mbufs {}'.format(total_mbufs) + + return devices + + @staticmethod + def _calc_vpp_workers(node, vpp_workers, numa_node, + other_cpus_end, total_vpp_workers, + reserve_vpp_main_core): + """ + Calculate the VPP worker information + + :param node: Node dictionary + :param vpp_workers: List of VPP workers + :param numa_node: Numa node + :param other_cpus_end: The end of the cpus allocated for cores + other than vpp + :param total_vpp_workers: The number of vpp workers needed + :param reserve_vpp_main_core: Is there a core needed for + the vpp main core + :type node: dict + :type numa_node: int + :type other_cpus_end: int + :type total_vpp_workers: int + :type reserve_vpp_main_core: bool + :returns: Is a core still needed for the vpp main core + :rtype: bool + """ + + # Can we fit the workers in one of these slices + cpus = node['cpu']['cpus_per_node'][numa_node] + for cpu in cpus: + start = cpu[0] + end = cpu[1] + if start <= other_cpus_end: + start = other_cpus_end + 1 + + if reserve_vpp_main_core: + start += 1 + + workers_end = start + total_vpp_workers - 1 + if workers_end <= end: + if reserve_vpp_main_core: + node['cpu']['vpp_main_core'] = start - 1 + reserve_vpp_main_core = False + if total_vpp_workers: + vpp_workers.append((start, workers_end)) + break + + # We still need to reserve the main core + if reserve_vpp_main_core: + node['cpu']['vpp_main_core'] = other_cpus_end + 1 + + return reserve_vpp_main_core + + @staticmethod + def _calc_desc_and_queues(total_numa_nodes, + total_ports_per_numa, + total_vpp_cpus, + ports_per_numa_value): + """ + Calculate the number of descriptors and queues + + :param total_numa_nodes: The total number of numa nodes + :param total_ports_per_numa: The total number of ports for this + numa node + :param total_vpp_cpus: The total number of cpus to allocate for vpp + :param ports_per_numa_value: The value from the ports_per_numa + dictionary + :type total_numa_nodes: int + :type total_ports_per_numa: int + :type total_vpp_cpus: int + :type ports_per_numa_value: dict + :returns The total number of message buffers + :returns: The total number of vpp workers + :rtype: int + :rtype: int + """ + + # Get the total vpp workers + total_vpp_workers = total_vpp_cpus + ports_per_numa_value['total_vpp_workers'] = total_vpp_workers + + # Get the number of rx queues + rx_queues = max(1, total_vpp_workers) + tx_queues = total_vpp_workers * total_numa_nodes + 1 + + # Get the descriptor entries + desc_entries = 1024 + ports_per_numa_value['rx_queues'] = rx_queues + total_mbufs = (((rx_queues * desc_entries) + + (tx_queues * desc_entries)) * + total_ports_per_numa) + total_mbufs = total_mbufs + + return total_mbufs, total_vpp_workers + + @staticmethod + def _create_ports_per_numa(node, interfaces): + """ + Create a dictionary or ports per numa node + :param node: Node dictionary + :param interfaces: All the interfaces to be used by vpp + :type node: dict + :type interfaces: dict + :returns: The ports per numa dictionary + :rtype: dict + """ + + # Make a list of ports by numa node + ports_per_numa = {} + for item in interfaces.items(): + i = item[1] + if i['numa_node'] not in ports_per_numa: + ports_per_numa[i['numa_node']] = {'interfaces': []} + ports_per_numa[i['numa_node']]['interfaces'].append(i) + else: + ports_per_numa[i['numa_node']]['interfaces'].append(i) + node['cpu']['ports_per_numa'] = ports_per_numa + + return ports_per_numa + + def calculate_cpu_parameters(self): + """ + Calculate the cpu configuration. + + """ + + # Calculate the cpu parameters, needed for the + # vpp_startup and grub configuration + for i in self._nodes.items(): + node = i[1] + + # get total number of nic ports + interfaces = node['interfaces'] + + # Make a list of ports by numa node + ports_per_numa = self._create_ports_per_numa(node, interfaces) + + # Get the number of cpus to skip, we never use the first cpu + other_cpus_start = 1 + other_cpus_end = other_cpus_start + \ + node['cpu']['total_other_cpus'] - 1 + other_workers = None + if other_cpus_end is not 0: + other_workers = (other_cpus_start, other_cpus_end) + node['cpu']['other_workers'] = other_workers + + # Allocate the VPP main core and workers + vpp_workers = [] + reserve_vpp_main_core = node['cpu']['reserve_vpp_main_core'] + total_vpp_cpus = node['cpu']['total_vpp_cpus'] + + # If total_vpp_cpus is 0 or is less than the numa nodes with ports + # then we shouldn't get workers + total_with_main = total_vpp_cpus + if reserve_vpp_main_core: + total_with_main += 1 + total_mbufs = 0 + if total_with_main is not 0: + for item in ports_per_numa.items(): + numa_node = item[0] + value = item[1] + + # Get the number of descriptors and queues + mbufs, total_vpp_workers = self._calc_desc_and_queues( + len(ports_per_numa), + len(value['interfaces']), total_vpp_cpus, value) + total_mbufs += mbufs + + # Get the VPP workers + reserve_vpp_main_core = self._calc_vpp_workers( + node, vpp_workers, numa_node, other_cpus_end, + total_vpp_workers, reserve_vpp_main_core) + + total_mbufs *= 2.5 + total_mbufs = int(total_mbufs) + else: + total_mbufs = 0 + + # Save the info + node['cpu']['vpp_workers'] = vpp_workers + node['cpu']['total_mbufs'] = total_mbufs + + # Write the config + self.updateconfig() + + @staticmethod + def _apply_vpp_tcp(node): + """ + Apply the VPP Unix config + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + + active_open_sessions = node['tcp']['active_open_sessions'] + aos = int(active_open_sessions) + + passive_open_sessions = node['tcp']['passive_open_sessions'] + pos = int(passive_open_sessions) + + # Generate the api-segment gid vpp sheit in any case + if (aos + pos) == 0: + tcp = "api-segment {\n" + tcp = tcp + " gid vpp\n" + tcp = tcp + "}\n" + return tcp.rstrip('\n') + + tcp = "# TCP stack-related configuration parameters\n" + tcp = tcp + "# expecting {:d} client sessions, {:d} server sessions\n\n".format(aos, pos) + tcp = tcp + "heapsize 4g\n\n" + tcp = tcp + "api-segment {\n" + tcp = tcp + " global-size 2000M\n" + tcp = tcp + " api-size 1G\n" + tcp = tcp + "}\n\n" + + tcp = tcp + "session {\n" + tcp = tcp + " event-queue-length " + "{:d}".format(aos + pos) + "\n" + tcp = tcp + " preallocated-sessions " + "{:d}".format(aos + pos) + "\n" + tcp = tcp + " v4-session-table-buckets " + "{:d}".format((aos + pos) / 4) + "\n" + tcp = tcp + " v4-session-table-memory 3g\n" + if aos > 0: + tcp = tcp + " v4-halfopen-table-buckets " + \ + "{:d}".format((aos + pos) / 4) + "\n" + tcp = tcp + " v4-halfopen-table-memory 3g\n" + tcp = tcp + "}\n\n" + + tcp = tcp + "tcp {\n" + tcp = tcp + " preallocated-connections " + "{:d}".format(aos + pos) + "\n" + if aos > 0: + tcp = tcp + " preallocated-half-open-connections " + "{:d}".format(aos) + "\n" + tcp = tcp + " local-endpoints-table-buckets " + "{:d}".format((aos + pos) / 4) + "\n" + tcp = tcp + " local-endpoints-table-memory 3g\n" + tcp = tcp + "}\n\n" + + return tcp.rstrip('\n') + + def apply_vpp_startup(self): + """ + Apply the vpp startup configration + + """ + + # Apply the VPP startup configruation + for i in self._nodes.items(): + node = i[1] + + # Get the startup file + rootdir = node['rootdir'] + sfile = rootdir + node['vpp']['startup_config_file'] + + # Get the devices + devices = self._apply_vpp_devices(node) + + # Get the CPU config + cpu = self._apply_vpp_cpu(node) + + # Get the unix config + unix = self._apply_vpp_unix(node) + + # Get the TCP configuration, if any + tcp = self._apply_vpp_tcp(node) + + # Make a backup if needed + self._autoconfig_backup_file(sfile) + + # Get the template + tfile = sfile + '.template' + (ret, stdout, stderr) = \ + VPPUtil.exec_command('cat {}'.format(tfile)) + if ret != 0: + raise RuntimeError('Executing cat command failed to node {}'. + format(node['host'])) + startup = stdout.format(unix=unix, + cpu=cpu, + devices=devices, + tcp=tcp) + + (ret, stdout, stderr) = \ + VPPUtil.exec_command('rm {}'.format(sfile)) + if ret != 0: + logging.debug(stderr) + + cmd = "sudo cat > {0} << EOF\n{1}\n".format(sfile, startup) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('Writing config failed node {}'. + format(node['host'])) + + def apply_grub_cmdline(self): + """ + Apply the grub cmdline + + """ + + for i in self._nodes.items(): + node = i[1] + + # Get the isolated CPUs + other_workers = node['cpu']['other_workers'] + vpp_workers = node['cpu']['vpp_workers'] + vpp_main_core = node['cpu']['vpp_main_core'] + all_workers = [] + if other_workers is not None: + all_workers = [other_workers] + if vpp_main_core is not 0: + all_workers += [(vpp_main_core, vpp_main_core)] + all_workers += vpp_workers + isolated_cpus = '' + for idx, worker in enumerate(all_workers): + if worker is None: + continue + if idx > 0: + isolated_cpus += ',' + if worker[0] == worker[1]: + isolated_cpus += "{}".format(worker[0]) + else: + isolated_cpus += "{}-{}".format(worker[0], worker[1]) + + vppgrb = VppGrubUtil(node) + current_cmdline = vppgrb.get_current_cmdline() + if 'grub' not in node: + node['grub'] = {} + node['grub']['current_cmdline'] = current_cmdline + node['grub']['default_cmdline'] = \ + vppgrb.apply_cmdline(node, isolated_cpus) + + self.updateconfig() + + def get_hugepages(self): + """ + Get the hugepage configuration + + """ + + for i in self._nodes.items(): + node = i[1] + + hpg = VppHugePageUtil(node) + max_map_count, shmmax = hpg.get_huge_page_config() + node['hugepages']['max_map_count'] = max_map_count + node['hugepages']['shmax'] = shmmax + total, free, size, memtotal, memfree = hpg.get_actual_huge_pages() + node['hugepages']['actual_total'] = total + node['hugepages']['free'] = free + node['hugepages']['size'] = size + node['hugepages']['memtotal'] = memtotal + node['hugepages']['memfree'] = memfree + + self.updateconfig() + + def get_grub(self): + """ + Get the grub configuration + + """ + + for i in self._nodes.items(): + node = i[1] + + vppgrb = VppGrubUtil(node) + current_cmdline = vppgrb.get_current_cmdline() + default_cmdline = vppgrb.get_default_cmdline() + + # Get the total number of isolated CPUs + current_iso_cpus = 0 + iso_cpur = re.findall(r'isolcpus=[\w+\-,]+', current_cmdline) + iso_cpurl = len(iso_cpur) + if iso_cpurl > 0: + iso_cpu_str = iso_cpur[0] + iso_cpu_str = iso_cpu_str.split('=')[1] + iso_cpul = iso_cpu_str.split(',') + for iso_cpu in iso_cpul: + isocpuspl = iso_cpu.split('-') + if len(isocpuspl) is 1: + current_iso_cpus += 1 + else: + first = int(isocpuspl[0]) + second = int(isocpuspl[1]) + if first == second: + current_iso_cpus += 1 + else: + current_iso_cpus += second - first + + if 'grub' not in node: + node['grub'] = {} + node['grub']['current_cmdline'] = current_cmdline + node['grub']['default_cmdline'] = default_cmdline + node['grub']['current_iso_cpus'] = current_iso_cpus + + self.updateconfig() + + @staticmethod + def _get_device(node): + """ + Get the device configuration for a single node + + :param node: Node dictionary with cpuinfo. + :type node: dict + + """ + + vpp = VppPCIUtil(node) + vpp.get_all_devices() + + # Save the device information + node['devices'] = {} + node['devices']['dpdk_devices'] = vpp.get_dpdk_devices() + node['devices']['kernel_devices'] = vpp.get_kernel_devices() + node['devices']['other_devices'] = vpp.get_other_devices() + node['devices']['linkup_devices'] = vpp.get_link_up_devices() + + def get_devices_per_node(self): + """ + Get the device configuration for all the nodes + + """ + + for i in self._nodes.items(): + node = i[1] + # Update the interface data + + self._get_device(node) + + self.updateconfig() + + @staticmethod + def get_cpu_layout(node): + """ + Get the cpu layout + + using lscpu -p get the cpu layout. + Returns a list with each item representing a single cpu. + + :param node: Node dictionary. + :type node: dict + :returns: The cpu layout + :rtype: list + """ + + cmd = 'lscpu -p' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'. + format(cmd, node['host'], stderr)) + + pcpus = [] + lines = stdout.split('\n') + for line in lines: + if line == '' or line[0] == '#': + continue + linesplit = line.split(',') + layout = {'cpu': linesplit[0], 'core': linesplit[1], + 'socket': linesplit[2], 'node': linesplit[3]} + + # cpu, core, socket, node + pcpus.append(layout) + + return pcpus + + def get_cpu(self): + """ + Get the cpu configuration + + """ + + # Get the CPU layout + CpuUtils.get_cpu_layout_from_all_nodes(self._nodes) + + for i in self._nodes.items(): + node = i[1] + + # Get the cpu layout + layout = self.get_cpu_layout(node) + node['cpu']['layout'] = layout + + cpuinfo = node['cpuinfo'] + smt_enabled = CpuUtils.is_smt_enabled(cpuinfo) + node['cpu']['smt_enabled'] = smt_enabled + + # We don't want to write the cpuinfo + node['cpuinfo'] = "" + + # Write the config + self.updateconfig() + + def discover(self): + """ + Get the current system configuration. + + """ + + # Get the Huge Page configuration + self.get_hugepages() + + # Get the device configuration + self.get_devices_per_node() + + # Get the CPU configuration + self.get_cpu() + + # Get the current grub cmdline + self.get_grub() + + def _modify_cpu_questions(self, node, total_cpus, numa_nodes): + """ + Ask the user questions related to the cpu configuration. + + :param node: Node dictionary + :param total_cpus: The total number of cpus in the system + :param numa_nodes: The list of numa nodes in the system + :type node: dict + :type total_cpus: int + :type numa_nodes: list + """ + + print "\nYour system has {} core(s) and {} Numa Nodes.". \ + format(total_cpus, len(numa_nodes)) + print "To begin, we suggest not reserving any cores for VPP", + print "or other processes." + print "Then to improve performance try reserving cores as needed. " + + max_other_cores = total_cpus / 2 + question = '\nHow many core(s) do you want to reserve for processes \ +other than VPP? [0-{}][0]? '.format(str(max_other_cores)) + total_other_cpus = self._ask_user_range(question, 0, max_other_cores, + 0) + node['cpu']['total_other_cpus'] = total_other_cpus + + max_vpp_cpus = 4 + total_vpp_cpus = 0 + if max_vpp_cpus > 0: + question = "How many core(s) shall we reserve for VPP workers[0-{}][0]? ". \ + format(max_vpp_cpus) + total_vpp_cpus = self._ask_user_range(question, 0, max_vpp_cpus, 0) + node['cpu']['total_vpp_cpus'] = total_vpp_cpus + + max_main_cpus = max_vpp_cpus - total_vpp_cpus + reserve_vpp_main_core = False + if max_main_cpus > 0: + question = "Should we reserve 1 core for the VPP Main thread? " + question += "[y/N]? " + answer = self._ask_user_yn(question, 'n') + if answer == 'y': + reserve_vpp_main_core = True + node['cpu']['reserve_vpp_main_core'] = reserve_vpp_main_core + node['cpu']['vpp_main_core'] = 0 + + def modify_cpu(self): + """ + Modify the cpu configuration, asking for the user for the values. + + """ + + # Get the CPU layout + CpuUtils.get_cpu_layout_from_all_nodes(self._nodes) + + for i in self._nodes.items(): + node = i[1] + total_cpus = 0 + total_cpus_per_slice = 0 + cpus_per_node = {} + numa_nodes = [] + cores = [] + cpu_layout = self.get_cpu_layout(node) + + # Assume the number of cpus per slice is always the same as the + # first slice + first_node = '0' + for cpu in cpu_layout: + if cpu['node'] != first_node: + break + total_cpus_per_slice += 1 + + # Get the total number of cpus, cores, and numa nodes from the + # cpu layout + for cpul in cpu_layout: + numa_node = cpul['node'] + core = cpul['core'] + cpu = cpul['cpu'] + total_cpus += 1 + + if numa_node not in cpus_per_node: + cpus_per_node[numa_node] = [] + cpuperslice = int(cpu) % total_cpus_per_slice + if cpuperslice == 0: + cpus_per_node[numa_node].append((int(cpu), int(cpu) + + total_cpus_per_slice - 1)) + if numa_node not in numa_nodes: + numa_nodes.append(numa_node) + if core not in cores: + cores.append(core) + node['cpu']['cpus_per_node'] = cpus_per_node + + # Ask the user some questions + self._modify_cpu_questions(node, total_cpus, numa_nodes) + + # Populate the interfaces with the numa node + ikeys = node['interfaces'].keys() + VPPUtil.get_interfaces_numa_node(node, *tuple(ikeys)) + + # We don't want to write the cpuinfo + node['cpuinfo'] = "" + + # Write the configs + self._update_auto_config() + self.updateconfig() + + def _modify_other_devices(self, node, + other_devices, kernel_devices, dpdk_devices): + """ + Modify the devices configuration, asking for the user for the values. + + """ + + odevices_len = len(other_devices) + if odevices_len > 0: + print "\nThese device(s) are currently NOT being used", + print "by VPP or the OS.\n" + VppPCIUtil.show_vpp_devices(other_devices, show_interfaces=False) + question = "\nWould you like to give any of these devices" + question += " back to the OS [Y/n]? " + answer = self._ask_user_yn(question, 'Y') + if answer == 'y': + vppd = {} + for dit in other_devices.items(): + dvid = dit[0] + device = dit[1] + question = "Would you like to use device {} for". \ + format(dvid) + question += " the OS [y/N]? " + answer = self._ask_user_yn(question, 'n') + if answer == 'y': + driver = device['unused'][0] + VppPCIUtil.bind_vpp_device(node, driver, dvid) + vppd[dvid] = device + for dit in vppd.items(): + dvid = dit[0] + device = dit[1] + kernel_devices[dvid] = device + del other_devices[dvid] + + odevices_len = len(other_devices) + if odevices_len > 0: + print "\nThese device(s) are still NOT being used ", + print "by VPP or the OS.\n" + VppPCIUtil.show_vpp_devices(other_devices, show_interfaces=False) + question = "\nWould you like use any of these for VPP [y/N]? " + answer = self._ask_user_yn(question, 'N') + if answer == 'y': + vppd = {} + for dit in other_devices.items(): + dvid = dit[0] + device = dit[1] + question = "Would you like to use device {} ".format(dvid) + question += "for VPP [y/N]? " + answer = self._ask_user_yn(question, 'n') + if answer == 'y': + vppd[dvid] = device + for dit in vppd.items(): + dvid = dit[0] + device = dit[1] + dpdk_devices[dvid] = device + del other_devices[dvid] + + def modify_devices(self): + """ + Modify the devices configuration, asking for the user for the values. + + """ + + for i in self._nodes.items(): + node = i[1] + devices = node['devices'] + other_devices = devices['other_devices'] + kernel_devices = devices['kernel_devices'] + dpdk_devices = devices['dpdk_devices'] + + if other_devices: + self._modify_other_devices(node, other_devices, + kernel_devices, dpdk_devices) + + # Get the devices again for this node + self._get_device(node) + devices = node['devices'] + kernel_devices = devices['kernel_devices'] + dpdk_devices = devices['dpdk_devices'] + + klen = len(kernel_devices) + if klen > 0: + print "\nThese devices have kernel interfaces, but", + print "appear to be safe to use with VPP.\n" + VppPCIUtil.show_vpp_devices(kernel_devices) + question = "\nWould you like to use any of these " + question += "device(s) for VPP [y/N]? " + answer = self._ask_user_yn(question, 'n') + if answer == 'y': + vppd = {} + for dit in kernel_devices.items(): + dvid = dit[0] + device = dit[1] + question = "Would you like to use device {} ". \ + format(dvid) + question += "for VPP [y/N]? " + answer = self._ask_user_yn(question, 'n') + if answer == 'y': + vppd[dvid] = device + for dit in vppd.items(): + dvid = dit[0] + device = dit[1] + dpdk_devices[dvid] = device + del kernel_devices[dvid] + + dlen = len(dpdk_devices) + if dlen > 0: + print "\nThese device(s) will be used by VPP.\n" + VppPCIUtil.show_vpp_devices(dpdk_devices, show_interfaces=False) + question = "\nWould you like to remove any of " + question += "these device(s) [y/N]? " + answer = self._ask_user_yn(question, 'n') + if answer == 'y': + vppd = {} + for dit in dpdk_devices.items(): + dvid = dit[0] + device = dit[1] + question = "Would you like to remove {} [y/N]? ". \ + format(dvid) + answer = self._ask_user_yn(question, 'n') + if answer == 'y': + vppd[dvid] = device + for dit in vppd.items(): + dvid = dit[0] + device = dit[1] + driver = device['unused'][0] + VppPCIUtil.bind_vpp_device(node, driver, dvid) + kernel_devices[dvid] = device + del dpdk_devices[dvid] + + interfaces = {} + for dit in dpdk_devices.items(): + dvid = dit[0] + device = dit[1] + VppPCIUtil.vpp_create_interface(interfaces, dvid, device) + node['interfaces'] = interfaces + + print "\nThese device(s) will be used by VPP, please", + print "rerun this option if this is incorrect.\n" + VppPCIUtil.show_vpp_devices(dpdk_devices, show_interfaces=False) + + self._update_auto_config() + self.updateconfig() + + def modify_huge_pages(self): + """ + Modify the huge page configuration, asking for the user for the values. + + """ + + for i in self._nodes.items(): + node = i[1] + + total = node['hugepages']['actual_total'] + free = node['hugepages']['free'] + size = node['hugepages']['size'] + memfree = node['hugepages']['memfree'].split(' ')[0] + hugesize = int(size.split(' ')[0]) + # The max number of huge pages should be no more than + # 70% of total free memory + maxpages = (int(memfree) * MAX_PERCENT_FOR_HUGE_PAGES / 100) / hugesize + print "\nThere currently {} {} huge pages free.". \ + format(free, size) + question = "Do you want to reconfigure the number of " + question += "huge pages [y/N]? " + answer = self._ask_user_yn(question, 'n') + if answer == 'n': + node['hugepages']['total'] = total + continue + + print "\nThere currently a total of {} huge pages.". \ + format(total) + question = \ + "How many huge pages do you want [{} - {}][{}]? ".\ + format(MIN_TOTAL_HUGE_PAGES, maxpages, MIN_TOTAL_HUGE_PAGES) + answer = self._ask_user_range(question, 1024, maxpages, 1024) + node['hugepages']['total'] = str(answer) + + # Update auto-config.yaml + self._update_auto_config() + + # Rediscover just the hugepages + self.get_hugepages() + + def get_tcp_params(self): + """ + Get the tcp configuration + + """ + # maybe nothing to do here? + self.updateconfig() + + def acquire_tcp_params(self): + """ + Ask the user for TCP stack configuration parameters + + """ + + for i in self._nodes.items(): + node = i[1] + + question = "\nHow many active-open / tcp client sessions are expected " + question = question + "[0-10000000][0]? " + answer = self._ask_user_range(question, 0, 10000000, 0) + # Less than 10K is equivalent to 0 + if int(answer) < 10000: + answer = 0 + node['tcp']['active_open_sessions'] = answer + + question = "How many passive-open / tcp server sessions are expected " + question = question + "[0-10000000][0]? " + answer = self._ask_user_range(question, 0, 10000000, 0) + # Less than 10K is equivalent to 0 + if int(answer) < 10000: + answer = 0 + node['tcp']['passive_open_sessions'] = answer + + # Update auto-config.yaml + self._update_auto_config() + + # Rediscover tcp parameters + self.get_tcp_params() + + @staticmethod + def patch_qemu(node): + """ + Patch qemu with the correct patches. + + :param node: Node dictionary + :type node: dict + """ + + print '\nWe are patching the node "{}":\n'.format(node['host']) + QemuUtils.build_qemu(node, force_install=True, apply_patch=True) + + @staticmethod + def cpu_info(node): + """ + print the CPU information + + """ + + cpu = CpuUtils.get_cpu_info_per_node(node) + + item = 'Model name' + if item in cpu: + print "{:>20}: {}".format(item, cpu[item]) + item = 'CPU(s)' + if item in cpu: + print "{:>20}: {}".format(item, cpu[item]) + item = 'Thread(s) per core' + if item in cpu: + print "{:>20}: {}".format(item, cpu[item]) + item = 'Core(s) per socket' + if item in cpu: + print "{:>20}: {}".format(item, cpu[item]) + item = 'Socket(s)' + if item in cpu: + print "{:>20}: {}".format(item, cpu[item]) + item = 'NUMA node(s)' + numa_nodes = 0 + if item in cpu: + numa_nodes = int(cpu[item]) + for i in xrange(0, numa_nodes): + item = "NUMA node{} CPU(s)".format(i) + print "{:>20}: {}".format(item, cpu[item]) + item = 'CPU max MHz' + if item in cpu: + print "{:>20}: {}".format(item, cpu[item]) + item = 'CPU min MHz' + if item in cpu: + print "{:>20}: {}".format(item, cpu[item]) + + if node['cpu']['smt_enabled']: + smt = 'Enabled' + else: + smt = 'Disabled' + print "{:>20}: {}".format('SMT', smt) + + # VPP Threads + print "\nVPP Threads: (Name: Cpu Number)" + vpp_processes = cpu['vpp_processes'] + for i in vpp_processes.items(): + print " {:10}: {:4}".format(i[0], i[1]) + + @staticmethod + def device_info(node): + """ + Show the device information. + + """ + + if 'cpu' in node and 'total_mbufs' in node['cpu']: + total_mbufs = node['cpu']['total_mbufs'] + if total_mbufs is not 0: + print "Total Number of Buffers: {}".format(total_mbufs) + + vpp = VppPCIUtil(node) + vpp.get_all_devices() + linkup_devs = vpp.get_link_up_devices() + if len(linkup_devs): + print ("\nDevices with link up (can not be used with VPP):") + vpp.show_vpp_devices(linkup_devs, show_header=False) + # for dev in linkup_devs: + # print (" " + dev) + kernel_devs = vpp.get_kernel_devices() + if len(kernel_devs): + print ("\nDevices bound to kernel drivers:") + vpp.show_vpp_devices(kernel_devs, show_header=False) + else: + print ("\nNo devices bound to kernel drivers") + + dpdk_devs = vpp.get_dpdk_devices() + if len(dpdk_devs): + print ("\nDevices bound to DPDK drivers:") + vpp.show_vpp_devices(dpdk_devs, show_interfaces=True, + show_header=False) + else: + print ("\nNo devices bound to DPDK drivers") + + vpputl = VPPUtil() + interfaces = vpputl.get_hardware(node) + if interfaces == {}: + return + + print ("\nDevices in use by VPP:") + + if len(interfaces.items()) < 2: + print ("None") + return + + print "{:30} {:6} {:4} {:7} {:4} {:7}". \ + format('Name', 'Socket', 'RXQs', + 'RXDescs', 'TXQs', 'TXDescs') + for intf in sorted(interfaces.items()): + name = intf[0] + value = intf[1] + if name == 'local0': + continue + socket = rx_qs = rx_ds = tx_qs = tx_ds = '' + if 'cpu socket' in value: + socket = int(value['cpu socket']) + if 'rx queues' in value: + rx_qs = int(value['rx queues']) + if 'rx descs' in value: + rx_ds = int(value['rx descs']) + if 'tx queues' in value: + tx_qs = int(value['tx queues']) + if 'tx descs' in value: + tx_ds = int(value['tx descs']) + + print ("{:30} {:>6} {:>4} {:>7} {:>4} {:>7}". + format(name, socket, rx_qs, rx_ds, tx_qs, tx_ds)) + + @staticmethod + def hugepage_info(node): + """ + Show the huge page information. + + """ + + hpg = VppHugePageUtil(node) + hpg.show_huge_pages() + + @staticmethod + def min_system_resources(node): + """ + Check the system for basic minimum resources, return true if + there is enough. + + :returns: boolean + :rtype: dict + """ + + min_sys_res = True + + # CPUs + if 'layout' in node['cpu']: + total_cpus = len(node['cpu']['layout']) + if total_cpus < 2: + print "\nThere is only {} CPU(s) available on this system.".format(total_cpus) + print "This is not enough to run VPP." + min_sys_res = False + + # System Memory + if 'free' in node['hugepages'] and \ + 'memfree' in node['hugepages'] and \ + 'size' in node['hugepages']: + free = node['hugepages']['free'] + memfree = float(node['hugepages']['memfree'].split(' ')[0]) + hugesize = float(node['hugepages']['size'].split(' ')[0]) + + memhugepages = MIN_TOTAL_HUGE_PAGES * hugesize + percentmemhugepages = (memhugepages / memfree) * 100 + if free is '0' and \ + percentmemhugepages > MAX_PERCENT_FOR_HUGE_PAGES: + print "\nThe System has only {} of free memory.".format(int(memfree)) + print "You will not be able to allocate enough Huge Pages for VPP." + min_sys_res = False + + return min_sys_res + + def sys_info(self): + """ + Print the system information + + """ + + for i in self._nodes.items(): + print "\n==============================" + name = i[0] + node = i[1] + + print "NODE: {}\n".format(name) + + # CPU + print "CPU:" + self.cpu_info(node) + + # Grub + print "\nGrub Command Line:" + if 'grub' in node: + print \ + " Current: {}".format( + node['grub']['current_cmdline']) + print \ + " Configured: {}".format( + node['grub']['default_cmdline']) + + # Huge Pages + print "\nHuge Pages:" + self.hugepage_info(node) + + # Devices + print "\nDevices:" + self.device_info(node) + + # Status + print "\nVPP Service Status:" + state, errors = VPPUtil.status(node) + print " {}".format(state) + for e in errors: + print " {}".format(e) + + # Minimum system resources + self.min_system_resources(node) + + print "\n==============================" diff --git a/extras/vpp_config/vpplib/CpuUtils.py b/extras/vpp_config/vpplib/CpuUtils.py new file mode 100644 index 00000000000..f5c23bc141a --- /dev/null +++ b/extras/vpp_config/vpplib/CpuUtils.py @@ -0,0 +1,287 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""CPU utilities library.""" + +import re + +from vpplib.VPPUtil import VPPUtil + +__all__ = ["CpuUtils"] + + +class CpuUtils(object): + """CPU utilities""" + + # Number of threads per core. + NR_OF_THREADS = 2 + + @staticmethod + def __str2int(string): + """Conversion from string to integer, 0 in case of empty string. + + :param string: Input string. + :type string: str + :returns: Integer converted from string, 0 in case of ValueError. + :rtype: int + """ + try: + return int(string) + except ValueError: + return 0 + + @staticmethod + def is_smt_enabled(cpu_info): + """Uses CPU mapping to find out if SMT is enabled or not. If SMT is + enabled, the L1d,L1i,L2,L3 setting is the same for two processors. These + two processors are two threads of one core. + + :param cpu_info: CPU info, the output of "lscpu -p". + :type cpu_info: list + :returns: True if SMT is enabled, False if SMT is disabled. + :rtype: bool + """ + + cpu_mems = [item[-4:] for item in cpu_info] + cpu_mems_len = len(cpu_mems) / CpuUtils.NR_OF_THREADS + count = 0 + for cpu_mem in cpu_mems[:cpu_mems_len]: + if cpu_mem in cpu_mems[cpu_mems_len:]: + count += 1 + return bool(count == cpu_mems_len) + + @staticmethod + def get_cpu_layout_from_all_nodes(nodes): + """Retrieve cpu layout from all nodes, assuming all nodes + are Linux nodes. + + :param nodes: DICT__nodes from Topology.DICT__nodes. + :type nodes: dict + :raises RuntimeError: If the ssh command "lscpu -p" fails. + """ + for node in nodes.values(): + cmd = "lscpu -p" + ret, stdout, stderr = VPPUtil.exec_command(cmd) + # parsing of "lscpu -p" output: + # # CPU,Core,Socket,Node,,L1d,L1i,L2,L3 + # 0,0,0,0,,0,0,0,0 + # 1,1,0,0,,1,1,1,0 + if ret != 0: + raise RuntimeError( + "Failed to execute ssh command, ret: {} err: {}".format( + ret, stderr)) + node['cpuinfo'] = list() + for line in stdout.split("\n"): + if line != '' and line[0] != "#": + node['cpuinfo'].append([CpuUtils.__str2int(x) for x in + line.split(",")]) + + @staticmethod + def cpu_node_count(node): + """Return count of numa nodes. + + :param node: Targeted node. + :type node: dict + :returns: Count of numa nodes. + :rtype: int + :raises RuntimeError: If node cpuinfo is not available. + """ + cpu_info = node.get("cpuinfo") + if cpu_info is not None: + return node["cpuinfo"][-1][3] + 1 + else: + raise RuntimeError("Node cpuinfo not available.") + + @staticmethod + def cpu_list_per_node(node, cpu_node, smt_used=False): + """Return node related list of CPU numbers. + + :param node: Node dictionary with cpuinfo. + :param cpu_node: Numa node number. + :param smt_used: True - we want to use SMT, otherwise false. + :type node: dict + :type cpu_node: int + :type smt_used: bool + :returns: List of cpu numbers related to numa from argument. + :rtype: list of int + :raises RuntimeError: If node cpuinfo is not available or if SMT is not + enabled. + """ + + cpu_node = int(cpu_node) + cpu_info = node.get("cpuinfo") + if cpu_info is None: + raise RuntimeError("Node cpuinfo not available.") + + smt_enabled = CpuUtils.is_smt_enabled(cpu_info) + if not smt_enabled and smt_used: + raise RuntimeError("SMT is not enabled.") + + cpu_list = [] + for cpu in cpu_info: + if cpu[3] == cpu_node: + cpu_list.append(cpu[0]) + + if not smt_enabled or smt_enabled and smt_used: + pass + + if smt_enabled and not smt_used: + cpu_list_len = len(cpu_list) + cpu_list = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS] + + return cpu_list + + @staticmethod + def cpu_slice_of_list_per_node(node, cpu_node, skip_cnt=0, cpu_cnt=0, + smt_used=False): + """Return string of node related list of CPU numbers. + + :param node: Node dictionary with cpuinfo. + :param cpu_node: Numa node number. + :param skip_cnt: Skip first "skip_cnt" CPUs. + :param cpu_cnt: Count of cpus to return, if 0 then return all. + :param smt_used: True - we want to use SMT, otherwise false. + :type node: dict + :type cpu_node: int + :type skip_cnt: int + :type cpu_cnt: int + :type smt_used: bool + :returns: Cpu numbers related to numa from argument. + :rtype: list + :raises RuntimeError: If we require more cpus than available. + """ + + cpu_list = CpuUtils.cpu_list_per_node(node, cpu_node, smt_used) + + cpu_list_len = len(cpu_list) + if cpu_cnt + skip_cnt > cpu_list_len: + raise RuntimeError("cpu_cnt + skip_cnt > length(cpu list).") + + if cpu_cnt == 0: + cpu_cnt = cpu_list_len - skip_cnt + + if smt_used: + cpu_list_0 = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS] + cpu_list_1 = cpu_list[cpu_list_len / CpuUtils.NR_OF_THREADS:] + cpu_list = [cpu for cpu in cpu_list_0[skip_cnt:skip_cnt + cpu_cnt]] + cpu_list_ex = [cpu for cpu in + cpu_list_1[skip_cnt:skip_cnt + cpu_cnt]] + cpu_list.extend(cpu_list_ex) + else: + cpu_list = [cpu for cpu in cpu_list[skip_cnt:skip_cnt + cpu_cnt]] + + return cpu_list + + @staticmethod + def cpu_list_per_node_str(node, cpu_node, skip_cnt=0, cpu_cnt=0, sep=",", + smt_used=False): + """Return string of node related list of CPU numbers. + + :param node: Node dictionary with cpuinfo. + :param cpu_node: Numa node number. + :param skip_cnt: Skip first "skip_cnt" CPUs. + :param cpu_cnt: Count of cpus to return, if 0 then return all. + :param sep: Separator, default: 1,2,3,4,.... + :param smt_used: True - we want to use SMT, otherwise false. + :type node: dict + :type cpu_node: int + :type skip_cnt: int + :type cpu_cnt: int + :type sep: str + :type smt_used: bool + :returns: Cpu numbers related to numa from argument. + :rtype: str + """ + + cpu_list = CpuUtils.cpu_slice_of_list_per_node(node, cpu_node, + skip_cnt=skip_cnt, + cpu_cnt=cpu_cnt, + smt_used=smt_used) + return sep.join(str(cpu) for cpu in cpu_list) + + @staticmethod + def cpu_range_per_node_str(node, cpu_node, skip_cnt=0, cpu_cnt=0, sep="-", + smt_used=False): + """Return string of node related range of CPU numbers, e.g. 0-4. + + :param node: Node dictionary with cpuinfo. + :param cpu_node: Numa node number. + :param skip_cnt: Skip first "skip_cnt" CPUs. + :param cpu_cnt: Count of cpus to return, if 0 then return all. + :param sep: Separator, default: "-". + :param smt_used: True - we want to use SMT, otherwise false. + :type node: dict + :type cpu_node: int + :type skip_cnt: int + :type cpu_cnt: int + :type sep: str + :type smt_used: bool + :returns: String of node related range of CPU numbers. + :rtype: str + """ + + cpu_list = CpuUtils.cpu_slice_of_list_per_node(node, cpu_node, + skip_cnt=skip_cnt, + cpu_cnt=cpu_cnt, + smt_used=smt_used) + if smt_used: + cpu_list_len = len(cpu_list) + cpu_list_0 = cpu_list[:cpu_list_len / CpuUtils.NR_OF_THREADS] + cpu_list_1 = cpu_list[cpu_list_len / CpuUtils.NR_OF_THREADS:] + cpu_range = "{}{}{},{}{}{}".format(cpu_list_0[0], sep, + cpu_list_0[-1], + cpu_list_1[0], sep, + cpu_list_1[-1]) + else: + cpu_range = "{}{}{}".format(cpu_list[0], sep, cpu_list[-1]) + + return cpu_range + + @staticmethod + def get_cpu_info_per_node(node): + """Return node related list of CPU numbers. + + :param node: Node dictionary with cpuinfo. + :type node: dict + :returns: Important CPU information. + :rtype: dict + """ + + cmd = "lscpu" + ret, stdout, stderr = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError("lscpu command failed on node {} {}." + .format(node['host'], stderr)) + + cpuinfo = {} + lines = stdout.split('\n') + for line in lines: + if line != '': + linesplit = re.split(r':\s+', line) + cpuinfo[linesplit[0]] = linesplit[1] + + cmd = "cat /proc/*/task/*/stat | awk '{print $1" "$2" "$39}'" + ret, stdout, stderr = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError("cat command failed on node {} {}." + .format(node['host'], stderr)) + + vpp_processes = {} + vpp_lines = re.findall(r'\w+\(vpp_\w+\)\w+', stdout) + for line in vpp_lines: + linesplit = re.split(r'\w+\(', line)[1].split(')') + vpp_processes[linesplit[0]] = linesplit[1] + + cpuinfo['vpp_processes'] = vpp_processes + + return cpuinfo diff --git a/extras/vpp_config/vpplib/QemuUtils.py b/extras/vpp_config/vpplib/QemuUtils.py new file mode 100644 index 00000000000..37a13e2afb3 --- /dev/null +++ b/extras/vpp_config/vpplib/QemuUtils.py @@ -0,0 +1,680 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""QEMU utilities library.""" + +from time import time, sleep +import json +import logging + +from vpplib.VPPUtil import VPPUtil +from vpplib.constants import Constants + + +class NodeType(object): + """Defines node types used in topology dictionaries.""" + # Device Under Test (this node has VPP running on it) + DUT = 'DUT' + # Traffic Generator (this node has traffic generator on it) + TG = 'TG' + # Virtual Machine (this node running on DUT node) + VM = 'VM' + + +class QemuUtils(object): + """QEMU utilities.""" + + def __init__(self, qemu_id=1): + self._qemu_id = qemu_id + # Path to QEMU binary + self._qemu_bin = '/usr/bin/qemu-system-x86_64' + # QEMU Machine Protocol socket + self._qmp_sock = '/tmp/qmp{0}.sock'.format(self._qemu_id) + # QEMU Guest Agent socket + self._qga_sock = '/tmp/qga{0}.sock'.format(self._qemu_id) + # QEMU PID file + self._pid_file = '/tmp/qemu{0}.pid'.format(self._qemu_id) + self._qemu_opt = {} + # Default 1 CPU. + self._qemu_opt['smp'] = '-smp 1,sockets=1,cores=1,threads=1' + # Daemonize the QEMU process after initialization. Default one + # management interface. + self._qemu_opt['options'] = '-cpu host -daemonize -enable-kvm ' \ + '-machine pc,accel=kvm,usb=off,mem-merge=off ' \ + '-net nic,macaddr=52:54:00:00:{0:02x}:ff -balloon none'\ + .format(self._qemu_id) + self._qemu_opt['ssh_fwd_port'] = 10021 + qemu_id + # Default serial console port + self._qemu_opt['serial_port'] = 4555 + qemu_id + # Default 512MB virtual RAM + self._qemu_opt['mem_size'] = 512 + # Default huge page mount point, required for Vhost-user interfaces. + self._qemu_opt['huge_mnt'] = '/mnt/huge' + # Default do not allocate huge pages. + self._qemu_opt['huge_allocate'] = False + # Default image for CSIT virl setup + self._qemu_opt['disk_image'] = '/var/lib/vm/vhost-nested.img' + # VM node info dict + self._vm_info = { + 'type': NodeType.VM, + 'port': self._qemu_opt['ssh_fwd_port'], + 'username': 'cisco', + 'password': 'cisco', + 'interfaces': {}, + } + # Virtio queue count + self._qemu_opt['queues'] = 1 + self._vhost_id = 0 + self._ssh = None + self._node = None + self._socks = [self._qmp_sock, self._qga_sock] + + def qemu_set_bin(self, path): + """Set binary path for QEMU. + + :param path: Absolute path in filesystem. + :type path: str + """ + self._qemu_bin = path + + def qemu_set_smp(self, cpus, cores, threads, sockets): + """Set SMP option for QEMU. + + :param cpus: Number of CPUs. + :param cores: Number of CPU cores on one socket. + :param threads: Number of threads on one CPU core. + :param sockets: Number of discrete sockets in the system. + :type cpus: int + :type cores: int + :type threads: int + :type sockets: int + """ + self._qemu_opt['smp'] = '-smp {},cores={},threads={},sockets={}'.format( + cpus, cores, threads, sockets) + + def qemu_set_ssh_fwd_port(self, fwd_port): + """Set host port for guest SSH forwarding. + + :param fwd_port: Port number on host for guest SSH forwarding. + :type fwd_port: int + """ + self._qemu_opt['ssh_fwd_port'] = fwd_port + self._vm_info['port'] = fwd_port + + def qemu_set_serial_port(self, port): + """Set serial console port. + + :param port: Serial console port. + :type port: int + """ + self._qemu_opt['serial_port'] = port + + def qemu_set_mem_size(self, mem_size): + """Set virtual RAM size. + + :param mem_size: RAM size in Mega Bytes. + :type mem_size: int + """ + self._qemu_opt['mem_size'] = int(mem_size) + + def qemu_set_huge_mnt(self, huge_mnt): + """Set hugefile mount point. + + :param huge_mnt: System hugefile mount point. + :type huge_mnt: int + """ + self._qemu_opt['huge_mnt'] = huge_mnt + + def qemu_set_huge_allocate(self): + """Set flag to allocate more huge pages if needed.""" + self._qemu_opt['huge_allocate'] = True + + def qemu_set_disk_image(self, disk_image): + """Set disk image. + + :param disk_image: Path of the disk image. + :type disk_image: str + """ + self._qemu_opt['disk_image'] = disk_image + + def qemu_set_affinity(self, *host_cpus): + """Set qemu affinity by getting thread PIDs via QMP and taskset to list + of CPU cores. + + :param host_cpus: List of CPU cores. + :type host_cpus: list + """ + qemu_cpus = self._qemu_qmp_exec('query-cpus')['return'] + + if len(qemu_cpus) != len(host_cpus): + logging.debug('Host CPU count {0}, Qemu Thread count {1}'.format( + len(host_cpus), len(qemu_cpus))) + raise ValueError('Host CPU count must match Qemu Thread count') + + for qemu_cpu, host_cpu in zip(qemu_cpus, host_cpus): + cmd = 'taskset -pc {0} {1}'.format(host_cpu, qemu_cpu['thread_id']) + (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + logging.debug('Set affinity failed {0}'.format(stderr)) + raise RuntimeError('Set affinity failed on {0}'.format( + self._node['host'])) + + def qemu_set_scheduler_policy(self): + """Set scheduler policy to SCHED_RR with priority 1 for all Qemu CPU + processes. + + :raises RuntimeError: Set scheduler policy failed. + """ + qemu_cpus = self._qemu_qmp_exec('query-cpus')['return'] + + for qemu_cpu in qemu_cpus: + cmd = 'chrt -r -p 1 {0}'.format(qemu_cpu['thread_id']) + (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + logging.debug('Set SCHED_RR failed {0}'.format(stderr)) + raise RuntimeError('Set SCHED_RR failed on {0}'.format( + self._node['host'])) + + def qemu_set_node(self, node): + """Set node to run QEMU on. + + :param node: Node to run QEMU on. + :type node: dict + """ + self._node = node + self._vm_info['host'] = node['host'] + + def qemu_add_vhost_user_if(self, socket, server=True, mac=None): + """Add Vhost-user interface. + + :param socket: Path of the unix socket. + :param server: If True the socket shall be a listening socket. + :param mac: Vhost-user interface MAC address (optional, otherwise is + used auto-generated MAC 52:54:00:00:xx:yy). + :type socket: str + :type server: bool + :type mac: str + """ + self._vhost_id += 1 + # Create unix socket character device. + chardev = ' -chardev socket,id=char{0},path={1}'.format(self._vhost_id, + socket) + if server is True: + chardev += ',server' + self._qemu_opt['options'] += chardev + # Create Vhost-user network backend. + netdev = (' -netdev vhost-user,id=vhost{0},chardev=char{0},queues={1}' + .format(self._vhost_id, self._qemu_opt['queues'])) + self._qemu_opt['options'] += netdev + # If MAC is not specified use auto-generated MAC address based on + # template 52:54:00:00::, e.g. vhost1 MAC of QEMU + # with ID 1 is 52:54:00:00:01:01 + if mac is None: + mac = '52:54:00:00:{0:02x}:{1:02x}'.\ + format(self._qemu_id, self._vhost_id) + extend_options = 'mq=on,csum=off,gso=off,guest_tso4=off,'\ + 'guest_tso6=off,guest_ecn=off,mrg_rxbuf=off' + # Create Virtio network device. + device = ' -device virtio-net-pci,netdev=vhost{0},mac={1},{2}'.format( + self._vhost_id, mac, extend_options) + self._qemu_opt['options'] += device + # Add interface MAC and socket to the node dict + if_data = {'mac_address': mac, 'socket': socket} + if_name = 'vhost{}'.format(self._vhost_id) + self._vm_info['interfaces'][if_name] = if_data + # Add socket to the socket list + self._socks.append(socket) + + def _qemu_qmp_exec(self, cmd): + """Execute QMP command. + + QMP is JSON based protocol which allows to control QEMU instance. + + :param cmd: QMP command to execute. + :type cmd: str + :return: Command output in python representation of JSON format. The + { "return": {} } response is QMP's success response. An error + response will contain the "error" keyword instead of "return". + """ + # To enter command mode, the qmp_capabilities command must be issued. + qmp_cmd = 'echo "{ \\"execute\\": \\"qmp_capabilities\\" }' \ + '{ \\"execute\\": \\"' + cmd + \ + '\\" }" | sudo -S socat - UNIX-CONNECT:' + self._qmp_sock + + (ret_code, stdout, stderr) = self._ssh.exec_command(qmp_cmd) + if int(ret_code) != 0: + logging.debug('QMP execute failed {0}'.format(stderr)) + raise RuntimeError('QMP execute "{0}"' + ' failed on {1}'.format(cmd, self._node['host'])) + logging.debug(stdout) + # Skip capabilities negotiation messages. + out_list = stdout.splitlines() + if len(out_list) < 3: + raise RuntimeError('Invalid QMP output on {0}'.format( + self._node['host'])) + return json.loads(out_list[2]) + + def _qemu_qga_flush(self): + """Flush the QGA parser state + """ + qga_cmd = '(printf "\xFF"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \ + self._qga_sock + # TODO: probably need something else + (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd) + if int(ret_code) != 0: + logging.debug('QGA execute failed {0}'.format(stderr)) + raise RuntimeError('QGA execute "{0}" ' + 'failed on {1}'.format(qga_cmd, + self._node['host'])) + logging.debug(stdout) + if not stdout: + return {} + return json.loads(stdout.split('\n', 1)[0]) + + def _qemu_qga_exec(self, cmd): + """Execute QGA command. + + QGA provide access to a system-level agent via standard QMP commands. + + :param cmd: QGA command to execute. + :type cmd: str + """ + qga_cmd = '(echo "{ \\"execute\\": \\"' + \ + cmd + \ + '\\" }"; sleep 1) | sudo -S socat - UNIX-CONNECT:' + \ + self._qga_sock + (ret_code, stdout, stderr) = self._ssh.exec_command(qga_cmd) + if int(ret_code) != 0: + logging.debug('QGA execute failed {0}'.format(stderr)) + raise RuntimeError('QGA execute "{0}"' + ' failed on {1}'.format(cmd, self._node['host'])) + logging.debug(stdout) + if not stdout: + return {} + return json.loads(stdout.split('\n', 1)[0]) + + def _wait_until_vm_boot(self, timeout=60): + """Wait until QEMU VM is booted. + + Ping QEMU guest agent each 5s until VM booted or timeout. + + :param timeout: Waiting timeout in seconds (optional, default 60s). + :type timeout: int + """ + start = time() + while True: + if time() - start > timeout: + raise RuntimeError('timeout, VM {0} not booted on {1}'.format( + self._qemu_opt['disk_image'], self._node['host'])) + out = None + try: + self._qemu_qga_flush() + out = self._qemu_qga_exec('guest-ping') + except ValueError: + logging.debug('QGA guest-ping unexpected output {}'.format(out)) + # Empty output - VM not booted yet + if not out: + sleep(5) + # Non-error return - VM booted + elif out.get('return') is not None: + break + # Skip error and wait + elif out.get('error') is not None: + sleep(5) + else: + # If there is an unexpected output from QGA guest-info, try + # again until timeout. + logging.debug('QGA guest-ping unexpected output {}'.format(out)) + + logging.debug('VM {0} booted on {1}'.format(self._qemu_opt['disk_image'], + self._node['host'])) + + def _update_vm_interfaces(self): + """Update interface names in VM node dict.""" + # Send guest-network-get-interfaces command via QGA, output example: + # {"return": [{"name": "eth0", "hardware-address": "52:54:00:00:04:01"}, + # {"name": "eth1", "hardware-address": "52:54:00:00:04:02"}]} + out = self._qemu_qga_exec('guest-network-get-interfaces') + interfaces = out.get('return') + mac_name = {} + if not interfaces: + raise RuntimeError('Get VM {0} interface list failed on {1}'.format( + self._qemu_opt['disk_image'], self._node['host'])) + # Create MAC-name dict + for interface in interfaces: + if 'hardware-address' not in interface: + continue + mac_name[interface['hardware-address']] = interface['name'] + # Match interface by MAC and save interface name + for interface in self._vm_info['interfaces'].values(): + mac = interface.get('mac_address') + if_name = mac_name.get(mac) + if if_name is None: + logging.debug('Interface name for MAC {} not found'.format(mac)) + else: + interface['name'] = if_name + + def _huge_page_check(self, allocate=False): + """Huge page check.""" + huge_mnt = self._qemu_opt.get('huge_mnt') + mem_size = self._qemu_opt.get('mem_size') + + # Get huge pages information + huge_size = self._get_huge_page_size() + huge_free = self._get_huge_page_free(huge_size) + huge_total = self._get_huge_page_total(huge_size) + + # Check if memory reqested by qemu is available on host + if (mem_size * 1024) > (huge_free * huge_size): + # If we want to allocate hugepage dynamically + if allocate: + mem_needed = abs((huge_free * huge_size) - (mem_size * 1024)) + huge_to_allocate = ((mem_needed / huge_size) * 2) + huge_total + max_map_count = huge_to_allocate*4 + # Increase maximum number of memory map areas a process may have + cmd = 'echo "{0}" | sudo tee /proc/sys/vm/max_map_count'.format( + max_map_count) + (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd) + # Increase hugepage count + cmd = 'echo "{0}" | sudo tee /proc/sys/vm/nr_hugepages'.format( + huge_to_allocate) + (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + logging.debug('Mount huge pages failed {0}'.format(stderr)) + raise RuntimeError('Mount huge pages failed on {0}'.format( + self._node['host'])) + # If we do not want to allocate dynamicaly end with error + else: + raise RuntimeError( + 'Not enough free huge pages: {0}, ' + '{1} MB'.format(huge_free, huge_free * huge_size) + ) + # Check if huge pages mount point exist + has_huge_mnt = False + (_, output, _) = self._ssh.exec_command('cat /proc/mounts') + for line in output.splitlines(): + # Try to find something like: + # none /mnt/huge hugetlbfs rw,relatime,pagesize=2048k 0 0 + mount = line.split() + if mount[2] == 'hugetlbfs' and mount[1] == huge_mnt: + has_huge_mnt = True + break + # If huge page mount point not exist create one + if not has_huge_mnt: + cmd = 'mkdir -p {0}'.format(huge_mnt) + (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + logging.debug('Create mount dir failed: {0}'.format(stderr)) + raise RuntimeError('Create mount dir failed on {0}'.format( + self._node['host'])) + cmd = 'mount -t hugetlbfs -o pagesize=2048k none {0}'.format( + huge_mnt) + (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd) + if int(ret_code) != 0: + logging.debug('Mount huge pages failed {0}'.format(stderr)) + raise RuntimeError('Mount huge pages failed on {0}'.format( + self._node['host'])) + + def _get_huge_page_size(self): + """Get default size of huge pages in system. + + :returns: Default size of free huge pages in system. + :rtype: int + :raises: RuntimeError if reading failed for three times. + """ + # TODO: remove to dedicated library + cmd_huge_size = "grep Hugepagesize /proc/meminfo | awk '{ print $2 }'" + for _ in range(3): + (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_size) + if ret == 0: + try: + huge_size = int(out) + except ValueError: + logging.debug('Reading huge page size information failed') + else: + break + else: + raise RuntimeError('Getting huge page size information failed.') + return huge_size + + def _get_huge_page_free(self, huge_size): + """Get total number of huge pages in system. + + :param huge_size: Size of hugepages. + :type huge_size: int + :returns: Number of free huge pages in system. + :rtype: int + :raises: RuntimeError if reading failed for three times. + """ + # TODO: add numa aware option + # TODO: remove to dedicated library + cmd_huge_free = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\ + 'free_hugepages'.format(huge_size) + for _ in range(3): + (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_free) + if ret == 0: + try: + huge_free = int(out) + except ValueError: + logging.debug('Reading free huge pages information failed') + else: + break + else: + raise RuntimeError('Getting free huge pages information failed.') + return huge_free + + def _get_huge_page_total(self, huge_size): + """Get total number of huge pages in system. + + :param huge_size: Size of hugepages. + :type huge_size: int + :returns: Total number of huge pages in system. + :rtype: int + :raises: RuntimeError if reading failed for three times. + """ + # TODO: add numa aware option + # TODO: remove to dedicated library + cmd_huge_total = 'cat /sys/kernel/mm/hugepages/hugepages-{0}kB/'\ + 'nr_hugepages'.format(huge_size) + for _ in range(3): + (ret, out, _) = self._ssh.exec_command_sudo(cmd_huge_total) + if ret == 0: + try: + huge_total = int(out) + except ValueError: + logging.debug('Reading total huge pages information failed') + else: + break + else: + raise RuntimeError('Getting total huge pages information failed.') + return huge_total + + def qemu_start(self): + """Start QEMU and wait until VM boot. + + :return: VM node info. + :rtype: dict + .. note:: First set at least node to run QEMU on. + .. warning:: Starts only one VM on the node. + """ + # SSH forwarding + ssh_fwd = '-net user,hostfwd=tcp::{0}-:22'.format( + self._qemu_opt.get('ssh_fwd_port')) + # Memory and huge pages + mem = '-object memory-backend-file,id=mem,size={0}M,mem-path={1},' \ + 'share=on -m {0} -numa node,memdev=mem'.format( + self._qemu_opt.get('mem_size'), self._qemu_opt.get('huge_mnt')) + + # By default check only if hugepages are available. + # If 'huge_allocate' is set to true try to allocate as well. + self._huge_page_check(allocate=self._qemu_opt.get('huge_allocate')) + + # Disk option + drive = '-drive file={0},format=raw,cache=none,if=virtio'.format( + self._qemu_opt.get('disk_image')) + # Setup QMP via unix socket + qmp = '-qmp unix:{0},server,nowait'.format(self._qmp_sock) + # Setup serial console + serial = '-chardev socket,host=127.0.0.1,port={0},id=gnc0,server,' \ + 'nowait -device isa-serial,chardev=gnc0'.format( + self._qemu_opt.get('serial_port')) + # Setup QGA via chardev (unix socket) and isa-serial channel + qga = '-chardev socket,path={0},server,nowait,id=qga0 ' \ + '-device isa-serial,chardev=qga0'.format(self._qga_sock) + # Graphic setup + graphic = '-monitor none -display none -vga none' + # PID file + pid = '-pidfile {}'.format(self._pid_file) + + # Run QEMU + cmd = '{0} {1} {2} {3} {4} {5} {6} {7} {8} {9} {10}'.format( + self._qemu_bin, self._qemu_opt.get('smp'), mem, ssh_fwd, + self._qemu_opt.get('options'), + drive, qmp, serial, qga, graphic, pid) + (ret_code, _, stderr) = self._ssh.exec_command_sudo(cmd, timeout=300) + if int(ret_code) != 0: + logging.debug('QEMU start failed {0}'.format(stderr)) + raise RuntimeError('QEMU start failed on {0}'.format( + self._node['host'])) + logging.debug('QEMU running') + # Wait until VM boot + try: + self._wait_until_vm_boot() + except RuntimeError: + self.qemu_kill_all() + self.qemu_clear_socks() + raise + # Update interface names in VM node dict + self._update_vm_interfaces() + # Return VM node dict + return self._vm_info + + def qemu_quit(self): + """Quit the QEMU emulator.""" + out = self._qemu_qmp_exec('quit') + err = out.get('error') + if err is not None: + raise RuntimeError('QEMU quit failed on {0}, error: {1}'.format( + self._node['host'], json.dumps(err))) + + def qemu_system_powerdown(self): + """Power down the system (if supported).""" + out = self._qemu_qmp_exec('system_powerdown') + err = out.get('error') + if err is not None: + raise RuntimeError( + 'QEMU system powerdown failed on {0}, ' + 'error: {1}'.format(self._node['host'], json.dumps(err)) + ) + + def qemu_system_reset(self): + """Reset the system.""" + out = self._qemu_qmp_exec('system_reset') + err = out.get('error') + if err is not None: + raise RuntimeError( + 'QEMU system reset failed on {0}, ' + 'error: {1}'.format(self._node['host'], json.dumps(err))) + + def qemu_kill(self): + """Kill qemu process.""" + # Note: in QEMU start phase there are 3 QEMU processes because we + # daemonize QEMU + self._ssh.exec_command_sudo('chmod +r {}'.format(self._pid_file)) + self._ssh.exec_command_sudo('kill -SIGKILL $(cat {})' + .format(self._pid_file)) + # Delete PID file + cmd = 'rm -f {}'.format(self._pid_file) + self._ssh.exec_command_sudo(cmd) + + def qemu_kill_all(self, node=None): + """Kill all qemu processes on DUT node if specified. + + :param node: Node to kill all QEMU processes on. + :type node: dict + """ + if node: + self.qemu_set_node(node) + self._ssh.exec_command_sudo('pkill -SIGKILL qemu') + + def qemu_clear_socks(self): + """Remove all sockets created by QEMU.""" + # If serial console port still open kill process + cmd = 'fuser -k {}/tcp'.format(self._qemu_opt.get('serial_port')) + self._ssh.exec_command_sudo(cmd) + # Delete all created sockets + for sock in self._socks: + cmd = 'rm -f {}'.format(sock) + self._ssh.exec_command_sudo(cmd) + + def qemu_system_status(self): + """Return current VM status. + + VM should be in following status: + + - debug: QEMU running on a debugger + - finish-migrate: paused to finish the migration process + - inmigrate: waiting for an incoming migration + - internal-error: internal error has occurred + - io-error: the last IOP has failed + - paused: paused + - postmigrate: paused following a successful migrate + - prelaunch: QEMU was started with -S and guest has not started + - restore-vm: paused to restore VM state + - running: actively running + - save-vm: paused to save the VM state + - shutdown: shut down (and -no-shutdown is in use) + - suspended: suspended (ACPI S3) + - watchdog: watchdog action has been triggered + - guest-panicked: panicked as a result of guest OS panic + + :return: VM status. + :rtype: str + """ + out = self._qemu_qmp_exec('query-status') + ret = out.get('return') + if ret is not None: + return ret.get('status') + else: + err = out.get('error') + raise RuntimeError( + 'QEMU query-status failed on {0}, ' + 'error: {1}'.format(self._node['host'], json.dumps(err))) + + @staticmethod + def build_qemu(node, force_install=False, apply_patch=False): + """Build QEMU from sources. + + :param node: Node to build QEMU on. + :param force_install: If True, then remove previous build. + :param apply_patch: If True, then apply patches from qemu_patches dir. + :type node: dict + :type force_install: bool + :type apply_patch: bool + :raises: RuntimeError if building QEMU failed. + """ + + directory = ' --directory={0}'.format(Constants.QEMU_INSTALL_DIR) + version = ' --version={0}'.format(Constants.QEMU_INSTALL_VERSION) + force = ' --force' if force_install else '' + patch = ' --patch' if apply_patch else '' + + (ret_code, stdout, stderr) = VPPUtil. \ + exec_command( + "sudo -E sh -c '{0}/{1}/qemu_build.sh{2}{3}{4}{5}'". + format(Constants.REMOTE_FW_DIR, Constants.RESOURCES_LIB_SH, + version, directory, force, patch), 1000) + + if int(ret_code) != 0: + logging.debug('QEMU build failed {0}'.format(stdout + stderr)) + raise RuntimeError('QEMU build failed on {0}'.format(node['host'])) diff --git a/extras/vpp_config/vpplib/VPPUtil.py b/extras/vpp_config/vpplib/VPPUtil.py new file mode 100644 index 00000000000..350b7759a03 --- /dev/null +++ b/extras/vpp_config/vpplib/VPPUtil.py @@ -0,0 +1,662 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""VPP util library""" +import logging +import re +import subprocess +import platform + +from collections import Counter + +# VPP_VERSION = '1707' +VPP_VERSION = '1710' + + +class VPPUtil(object): + """General class for any VPP related methods/functions.""" + + @staticmethod + def exec_command(cmd, timeout=None): + """Execute a command on the local node. + + :param cmd: Command to run locally. + :param timeout: Timeout value + :type cmd: str + :type timeout: int + :return return_code, stdout, stderr + :rtype: tuple(int, str, str) + """ + + logging.info(" Local Command: {}".format(cmd)) + out = '' + err = '' + prc = subprocess.Popen(cmd, shell=True, bufsize=1, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + with prc.stdout: + for line in iter(prc.stdout.readline, b''): + logging.info(" {}".format(line.strip('\n'))) + out += line + + with prc.stderr: + for line in iter(prc.stderr.readline, b''): + logging.warn(" {}".format(line.strip('\n'))) + err += line + + ret = prc.wait() + + return ret, out, err + + def _autoconfig_backup_file(self, filename): + """ + Create a backup file. + + :param filename: The file to backup + :type filename: str + """ + + # Does a copy of the file exist, if not create one + ofile = filename + '.orig' + (ret, stdout, stderr) = self.exec_command('ls {}'.format(ofile)) + if ret != 0: + logging.debug(stderr) + if stdout.strip('\n') != ofile: + cmd = 'sudo cp {} {}'.format(filename, ofile) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + logging.debug(stderr) + + def _install_vpp_pkg_ubuntu(self, node, pkg): + """ + Install the VPP packages + + :param node: Node dictionary + :param pkg: The vpp packages + :type node: dict + :type pkg: string + """ + + cmd = 'apt-get -y install {}'.format(pkg) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'.format( + cmd, node['host'], stdout, stderr)) + + def _install_vpp_pkg_centos(self, node, pkg): + """ + Install the VPP packages + + :param node: Node dictionary + :param pkg: The vpp packages + :type node: dict + :type pkg: string + """ + + cmd = 'yum -y install {}'.format(pkg) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'.format( + cmd, node['host'], stdout, stderr)) + + def _install_vpp_ubuntu(self, node, fdio_release=VPP_VERSION, + ubuntu_version='xenial'): + """ + Install the VPP packages + + :param node: Node dictionary with cpuinfo. + :param fdio_release: VPP release number + :param ubuntu_version: Ubuntu Version + :type node: dict + :type fdio_release: string + :type ubuntu_version: string + """ + + # Modify the sources list + sfile = '/etc/apt/sources.list.d/99fd.io.list' + + # Backup the sources list + self._autoconfig_backup_file(sfile) + + # Remove the current file + cmd = 'rm {}'.format(sfile) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + logging.debug('{} failed on node {} {}'.format( + cmd, + node['host'], + stderr)) + + reps = 'deb [trusted=yes] https://nexus.fd.io/content/' + reps += 'repositories/fd.io.stable.{}.ubuntu.{}.main/ ./\n' \ + .format(fdio_release, ubuntu_version) + + cmd = 'echo "{0}" | sudo tee {1}'.format(reps, sfile) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'.format( + cmd, + node['host'], + stderr)) + + # Install the package + cmd = 'apt-get -y update' + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} apt-get update failed on node {} {}'.format( + cmd, + node['host'], + stderr)) + + self._install_vpp_pkg_ubuntu(node, 'vpp-lib') + self._install_vpp_pkg_ubuntu(node, 'vpp') + self._install_vpp_pkg_ubuntu(node, 'vpp-plugins') + self._install_vpp_pkg_ubuntu(node, 'vpp-dpdk-dkms') + self._install_vpp_pkg_ubuntu(node, 'vpp-dpdk-dev') + self._install_vpp_pkg_ubuntu(node, 'vpp-api-python') + self._install_vpp_pkg_ubuntu(node, 'vpp-api-java') + self._install_vpp_pkg_ubuntu(node, 'vpp-api-lua') + self._install_vpp_pkg_ubuntu(node, 'vpp-dev') + self._install_vpp_pkg_ubuntu(node, 'vpp-dbg') + + def _install_vpp_centos(self, node, fdio_release=VPP_VERSION, + centos_version='centos7'): + """ + Install the VPP packages + + :param node: Node dictionary with cpuinfo. + :param fdio_release: VPP release number + :param centos_version: Ubuntu Version + :type node: dict + :type fdio_release: string + :type centos_version: string + """ + + # Modify the sources list + sfile = '/etc/yum.repos.d/fdio-release.repo' + + # Backup the sources list + self._autoconfig_backup_file(sfile) + + # Remove the current file + cmd = 'rm {}'.format(sfile) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + logging.debug('{} failed on node {} {}'.format( + cmd, + node['host'], + stderr)) + + reps = '[fdio-stable-{}]\n'.format(fdio_release) + reps += 'name=fd.io stable/{} branch latest merge\n'.format(fdio_release) + reps += 'baseurl=https://nexus.fd.io/content/repositories/fd.io.stable.{}.{}/\n'.\ + format(fdio_release, centos_version) + reps += 'enabled=1\n' + reps += 'gpgcheck=0' + + cmd = 'echo "{0}" | sudo tee {1}'.format(reps, sfile) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'.format( + cmd, + node['host'], + stderr)) + + # Install the packages + + self._install_vpp_pkg_centos(node, 'vpp-lib') + self._install_vpp_pkg_centos(node, 'vpp') + self._install_vpp_pkg_centos(node, 'vpp-plugins') + # jadfix Check with Ole + # self._install_vpp_pkg_centos(node, 'vpp-dpdk-devel') + self._install_vpp_pkg_centos(node, 'vpp-api-python') + self._install_vpp_pkg_centos(node, 'vpp-api-java') + self._install_vpp_pkg_centos(node, 'vpp-api-lua') + self._install_vpp_pkg_centos(node, 'vpp-devel') + + def install_vpp(self, node): + """ + Install the VPP packages + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + distro = self.get_linux_distro() + if distro[0] == 'Ubuntu': + self._install_vpp_ubuntu(node) + elif distro[0] == 'CentOS Linux': + logging.info("Install CentOS") + self._install_vpp_centos(node) + else: + return + + def _uninstall_vpp_pkg_ubuntu(self, node, pkg): + """ + Uninstall the VPP packages + + :param node: Node dictionary + :param pkg: The vpp packages + :type node: dict + :type pkg: string + """ + cmd = 'dpkg --purge {}'.format(pkg) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'.format( + cmd, node['host'], stdout, stderr)) + + def _uninstall_vpp_pkg_centos(self, node, pkg): + """ + Uninstall the VPP packages + + :param node: Node dictionary + :param pkg: The vpp packages + :type node: dict + :type pkg: string + """ + cmd = 'yum -y remove {}'.format(pkg) + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'.format( + cmd, node['host'], stdout, stderr)) + + def _uninstall_vpp_ubuntu(self, node): + """ + Uninstall the VPP packages + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + pkgs = self.get_installed_vpp_pkgs() + + if len(pkgs) > 0: + if 'version' in pkgs[0]: + logging.info("Uninstall Ubuntu Packages") + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-api-python') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-api-java') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-api-lua') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-plugins') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-dpdk-dev') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-dpdk-dkms') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-dev') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-dbg') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp') + self._uninstall_vpp_pkg_ubuntu(node, 'vpp-lib') + else: + logging.info("Uninstall locally installed Ubuntu Packages") + for pkg in pkgs: + self._uninstall_vpp_pkg_ubuntu(node, pkg['name']) + else: + logging.error("There are no Ubuntu packages installed") + + def _uninstall_vpp_centos(self, node): + """ + Uninstall the VPP packages + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + + pkgs = self.get_installed_vpp_pkgs() + + if len(pkgs) > 0: + if 'version' in pkgs[0]: + logging.info("Uninstall CentOS Packages") + self._uninstall_vpp_pkg_centos(node, 'vpp-api-python') + self._uninstall_vpp_pkg_centos(node, 'vpp-api-java') + self._uninstall_vpp_pkg_centos(node, 'vpp-api-lua') + self._uninstall_vpp_pkg_centos(node, 'vpp-plugins') + self._uninstall_vpp_pkg_centos(node, 'vpp-dpdk-devel') + self._uninstall_vpp_pkg_centos(node, 'vpp-devel') + self._uninstall_vpp_pkg_centos(node, 'vpp') + self._uninstall_vpp_pkg_centos(node, 'vpp-lib') + else: + logging.info("Uninstall locally installed CentOS Packages") + for pkg in pkgs: + self._uninstall_vpp_pkg_centos(node, pkg['name']) + else: + logging.error("There are no CentOS packages installed") + + def uninstall_vpp(self, node): + """ + Uninstall the VPP packages + + :param node: Node dictionary with cpuinfo. + :type node: dict + """ + distro = self.get_linux_distro() + if distro[0] == 'Ubuntu': + self._uninstall_vpp_ubuntu(node) + elif distro[0] == 'CentOS Linux': + logging.info("Uninstall CentOS") + self._uninstall_vpp_centos(node) + else: + return + + def show_vpp_settings(self, *additional_cmds): + """ + Print default VPP settings. In case others are needed, can be + accepted as next parameters (each setting one parameter), preferably + in form of a string. + + :param additional_cmds: Additional commands that the vpp should print + settings for. + :type additional_cmds: tuple + """ + def_setting_tb_displayed = { + 'IPv6 FIB': 'ip6 fib', + 'IPv4 FIB': 'ip fib', + 'Interface IP': 'int addr', + 'Interfaces': 'int', + 'ARP': 'ip arp', + 'Errors': 'err' + } + + if additional_cmds: + for cmd in additional_cmds: + def_setting_tb_displayed['Custom Setting: {}'.format(cmd)] \ + = cmd + + for _, value in def_setting_tb_displayed.items(): + self.exec_command('vppctl sh {}'.format(value)) + + @staticmethod + def get_hardware(node): + """ + Get the VPP hardware information and return it in a + dictionary + + :param node: VPP node. + :type node: dict + :returns: Dictionary containing improtant VPP information + :rtype: dictionary + """ + + interfaces = {} + cmd = 'vppctl show hard' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + return interfaces + + lines = stdout.split('\n') + if len(lines[0]) is not 0: + if lines[0].split(' ')[0] == 'FileNotFoundError': + return interfaces + + for line in lines: + if len(line) is 0: + continue + + # If the first character is not whitespace + # create a new interface + if len(re.findall(r'\s', line[0])) is 0: + spl = line.split() + name = spl[0] + interfaces[name] = {} + interfaces[name]['index'] = spl[1] + interfaces[name]['state'] = spl[2] + + # Ethernet address + rfall = re.findall(r'Ethernet address', line) + if rfall: + spl = line.split() + interfaces[name]['mac'] = spl[2] + + # Carrier + rfall = re.findall(r'carrier', line) + if rfall: + spl = line.split('carrier ') + interfaces[name]['carrier'] = spl[1] + + # Socket + rfall = re.findall(r'cpu socket', line) + if rfall: + spl = line.split('cpu socket ') + interfaces[name]['cpu socket'] = spl[1] + + # Queues and Descriptors + rfall = re.findall(r'rx queues', line) + if rfall: + spl = line.split(',') + interfaces[name]['rx queues'] = spl[0].lstrip(' ').split(' ')[2] + interfaces[name]['rx descs'] = spl[1].split(' ')[3] + interfaces[name]['tx queues'] = spl[2].split(' ')[3] + interfaces[name]['tx descs'] = spl[3].split(' ')[3] + + return interfaces + + def _get_installed_vpp_pkgs_ubuntu(self): + """ + Get the VPP hardware information and return it in a + dictionary + + :returns: List of the packages installed + :rtype: list + """ + + pkgs = [] + cmd = 'dpkg -l | grep vpp' + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + return pkgs + + lines = stdout.split('\n') + for line in lines: + items = line.split() + if len(items) < 2: + continue + pkg = {'name': items[1], 'version': items[2]} + pkgs.append(pkg) + + return pkgs + + def _get_installed_vpp_pkgs_centos(self): + """ + Get the VPP hardware information and return it in a + dictionary + + :returns: List of the packages installed + :rtype: list + """ + + pkgs = [] + cmd = 'rpm -qa | grep vpp' + (ret, stdout, stderr) = self.exec_command(cmd) + if ret != 0: + return pkgs + + lines = stdout.split('\n') + for line in lines: + if len(line) == 0: + continue + + items = line.split() + if len(items) < 2: + pkg = {'name': items[0]} + else: + pkg = {'name': items[1], 'version': items[2]} + + pkgs.append(pkg) + + return pkgs + + def get_installed_vpp_pkgs(self): + """ + Get the VPP hardware information and return it in a + dictionary + + :returns: List of the packages installed + :rtype: list + """ + + distro = self.get_linux_distro() + if distro[0] == 'Ubuntu': + pkgs = self._get_installed_vpp_pkgs_ubuntu() + elif distro[0] == 'CentOS Linux': + pkgs = self._get_installed_vpp_pkgs_centos() + else: + return [] + + return pkgs + + @staticmethod + def get_interfaces_numa_node(node, *iface_keys): + """Get numa node on which are located most of the interfaces. + + Return numa node with highest count of interfaces provided as arguments. + Return 0 if the interface does not have numa_node information available. + If all interfaces have unknown location (-1), then return 0. + If most of interfaces have unknown location (-1), but there are + some interfaces with known location, then return the second most + location of the provided interfaces. + + :param node: Node from DICT__nodes. + :param iface_keys: Interface keys for lookup. + :type node: dict + :type iface_keys: strings + """ + numa_list = [] + for if_key in iface_keys: + try: + numa_list.append(node['interfaces'][if_key].get('numa_node')) + except KeyError: + pass + + numa_cnt_mc = Counter(numa_list).most_common() + numa_cnt_mc_len = len(numa_cnt_mc) + if numa_cnt_mc_len > 0 and numa_cnt_mc[0][0] != -1: + return numa_cnt_mc[0][0] + elif numa_cnt_mc_len > 1 and numa_cnt_mc[0][0] == -1: + return numa_cnt_mc[1][0] + + return 0 + + @staticmethod + def start(node): + """ + + Starts vpp for a given node + + :param node: VPP node. + :type node: dict + """ + + cmd = 'service vpp start' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, node['host'], + stdout, stderr)) + + @staticmethod + def stop(node): + """ + + Stops vpp for a given node + + :param node: VPP node. + :type node: dict + """ + + cmd = 'service vpp stop' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, node['host'], + stdout, stderr)) + + @staticmethod + def status(node): + """ + + Gets VPP status + + :param: node + :type node: dict + :returns: status, errors + :rtype: tuple(str, list) + """ + errors = [] + vutil = VPPUtil() + pkgs = vutil.get_installed_vpp_pkgs() + if len(pkgs) == 0: + return "Not Installed", errors + + cmd = 'service vpp status' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + + # Get the active status + state = re.findall(r'Active:[\w (\)]+', stdout)[0].split(' ') + if len(state) > 2: + statestr = "{} {}".format(state[1], state[2]) + else: + statestr = "Invalid" + + # For now we won't look for DPDK errors + # lines = stdout.split('\n') + # for line in lines: + # if 'EAL' in line or \ + # 'FAILURE' in line or \ + # 'failed' in line or \ + # 'Failed' in line: + # errors.append(line.lstrip(' ')) + + return statestr, errors + + @staticmethod + def get_linux_distro(): + """ + Get the linux distribution and check if it is supported + + :returns: linux distro, None if the distro is not supported + :rtype: list + """ + + distro = platform.linux_distribution() + if distro[0] == 'Ubuntu' or \ + distro[0] == 'CentOS Linux' or \ + distro[:26] == 'Linux Distribution Red Hat': + return distro + else: + raise RuntimeError('Linux Distribution {} is not supported'.format(distro[0])) + + @staticmethod + def version(): + """ + + Gets VPP Version information + + :returns: version + :rtype: dict + """ + + version = {} + cmd = 'vppctl show version verbose' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + return version + + lines = stdout.split('\n') + if len(lines[0]) is not 0: + if lines[0].split(' ')[0] == 'FileNotFoundError': + return version + + for line in lines: + if len(line) is 0: + continue + dct = line.split(':') + version[dct[0]] = dct[1].lstrip(' ') + + return version diff --git a/extras/vpp_config/vpplib/VppGrubUtil.py b/extras/vpp_config/vpplib/VppGrubUtil.py new file mode 100644 index 00000000000..4aac427c22a --- /dev/null +++ b/extras/vpp_config/vpplib/VppGrubUtil.py @@ -0,0 +1,236 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""VPP Grub Utility Library.""" + +import re + +from vpplib.VPPUtil import VPPUtil + +__all__ = ['VppGrubUtil'] + + +class VppGrubUtil(object): + """ VPP Grub Utilities.""" + + def _get_current_cmdline(self): + """ + Using /proc/cmdline return the current grub cmdline + + :returns: The current grub cmdline + :rtype: string + """ + + # Get the memory information using /proc/meminfo + cmd = 'sudo cat /proc/cmdline' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} on node {} {} {}'. + format(cmd, self._node['host'], + stdout, stderr)) + + self._current_cmdline = stdout.strip('\n') + + def _get_default_cmdline(self): + """ + Using /etc/default/grub return the default grub cmdline + + :returns: The default grub cmdline + :rtype: string + """ + + # Get the default grub cmdline + rootdir = self._node['rootdir'] + gfile = self._node['cpu']['grub_config_file'] + grubcmdline = self._node['cpu']['grubcmdline'] + cmd = 'cat {}'.format(rootdir + gfile) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} Executing failed on node {} {}'. + format(cmd, self._node['host'], stderr)) + + # Get the Default Linux command line, ignoring commented lines + lines = stdout.split('\n') + for line in lines: + if line == '' or line[0] == '#': + continue + ldefault = re.findall(r'{}=.+'.format(grubcmdline), line) + if ldefault: + self._default_cmdline = ldefault[0] + break + + def get_current_cmdline(self): + """ + Returns the saved grub cmdline + + :returns: The saved grub cmdline + :rtype: string + """ + return self._current_cmdline + + def get_default_cmdline(self): + """ + Returns the default grub cmdline + + :returns: The default grub cmdline + :rtype: string + """ + return self._default_cmdline + + def create_cmdline(self, isolated_cpus): + """ + Create the new grub cmdline + + :param isolated_cpus: The isolated cpu string + :type isolated_cpus: string + :returns: The command line + :rtype: string + """ + grubcmdline = self._node['cpu']['grubcmdline'] + cmdline = self._default_cmdline + value = cmdline.split('{}='.format(grubcmdline))[1] + value = value.rstrip('"').lstrip('"') + + iommu = re.findall(r'iommu=\w+', value) + pstate = re.findall(r'intel_pstate=\w+', value) + # If there is already some iommu commands set, leave them, + # if not use ours + if iommu == [] and pstate == []: + value = '{} intel_pstate=disable'.format(value) + + # Replace isolcpus with ours + isolcpus = re.findall(r'isolcpus=[\w+\-,]+', value) + if not isolcpus: + if isolated_cpus != '': + value = "{} isolcpus={}".format(value, isolated_cpus) + else: + if isolated_cpus != '': + value = re.sub(r'isolcpus=[\w+\-,]+', + 'isolcpus={}'.format(isolated_cpus), + value) + else: + value = re.sub(r'isolcpus=[\w+\-,]+', '', value) + + nohz = re.findall(r'nohz_full=[\w+\-,]+', value) + if not nohz: + if isolated_cpus != '': + value = "{} nohz_full={}".format(value, isolated_cpus) + else: + if isolated_cpus != '': + value = re.sub(r'nohz_full=[\w+\-,]+', + 'nohz_full={}'.format(isolated_cpus), + value) + else: + value = re.sub(r'nohz_full=[\w+\-,]+', '', value) + + rcu = re.findall(r'rcu_nocbs=[\w+\-,]+', value) + if not rcu: + if isolated_cpus != '': + value = "{} rcu_nocbs={}".format(value, isolated_cpus) + else: + if isolated_cpus != '': + value = re.sub(r'rcu_nocbs=[\w+\-,]+', + 'rcu_nocbs={}'.format(isolated_cpus), + value) + else: + value = re.sub(r'rcu_nocbs=[\w+\-,]+', '', value) + + value = value.lstrip(' ').rstrip(' ') + cmdline = '{}="{}"'.format(grubcmdline, value) + return cmdline + + def apply_cmdline(self, node, isolated_cpus): + """ + Apply cmdline to the default grub file + + :param node: Node dictionary with cpuinfo. + :param isolated_cpus: The isolated cpu string + :type node: dict + :type isolated_cpus: string + :return The vpp cmdline + :rtype string + """ + + vpp_cmdline = self.create_cmdline(isolated_cpus) + if vpp_cmdline == '': + return vpp_cmdline + + # Update grub + # Save the original file + rootdir = node['rootdir'] + grubcmdline = node['cpu']['grubcmdline'] + ofilename = rootdir + node['cpu']['grub_config_file'] + '.orig' + filename = rootdir + node['cpu']['grub_config_file'] + + # Write the output file + # Does a copy of the original file exist, if not create one + (ret, stdout, stderr) = VPPUtil.exec_command('ls {}'.format(ofilename)) + if ret != 0: + if stdout.strip('\n') != ofilename: + cmd = 'sudo cp {} {}'.format(filename, ofilename) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'. + format(cmd, self._node['host'], stderr)) + + # Get the contents of the current grub config file + cmd = 'cat {}'.format(filename) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'.format( + cmd, + self._node['host'], + stderr)) + + # Write the new contents + # Get the Default Linux command line, ignoring commented lines + content = "" + lines = stdout.split('\n') + for line in lines: + if line == '': + content += line + '\n' + continue + if line[0] == '#': + content += line + '\n' + continue + + ldefault = re.findall(r'{}=.+'.format(grubcmdline), line) + if ldefault: + content += vpp_cmdline + '\n' + else: + content += line + '\n' + + content = content.replace(r"`", r"\`") + content = content.rstrip('\n') + cmd = "sudo cat > {0} << EOF\n{1}\n".format(filename, content) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'.format( + cmd, + self._node['host'], + stderr)) + + return vpp_cmdline + + def __init__(self, node): + distro = VPPUtil.get_linux_distro() + if distro[0] == 'Ubuntu': + node['cpu']['grubcmdline'] = 'GRUB_CMDLINE_LINUX_DEFAULT' + else: + node['cpu']['grubcmdline'] = 'GRUB_CMDLINE_LINUX' + + self._node = node + self._current_cmdline = "" + self._default_cmdline = "" + self._get_current_cmdline() + self._get_default_cmdline() diff --git a/extras/vpp_config/vpplib/VppHugePageUtil.py b/extras/vpp_config/vpplib/VppHugePageUtil.py new file mode 100644 index 00000000000..43df72a455a --- /dev/null +++ b/extras/vpp_config/vpplib/VppHugePageUtil.py @@ -0,0 +1,122 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""VPP Huge Page Utilities""" + +import re + +from vpplib.VPPUtil import VPPUtil + +# VPP Huge page File +DEFAULT_VPP_HUGE_PAGE_CONFIG_FILENAME = "/etc/vpp/80-vpp.conf" +VPP_HUGEPAGE_CONFIG = """ +vm.nr_hugepages={nr_hugepages} +vm.max_map_count={max_map_count} +vm.hugetlb_shm_group=0 +kernel.shmmax={shmmax} +""" + + +class VppHugePageUtil(object): + """ + Huge Page Utilities + """ + def hugepages_dryrun_apply(self): + """ + Apply the huge page configuration + + """ + + node = self._node + hugepages = node['hugepages'] + + vpp_hugepage_config = VPP_HUGEPAGE_CONFIG.format( + nr_hugepages=hugepages['total'], + max_map_count=hugepages['max_map_count'], + shmmax=hugepages['shmax']) + + rootdir = node['rootdir'] + filename = rootdir + node['hugepages']['hugepage_config_file'] + + cmd = 'echo "{0}" | sudo tee {1}'.\ + format(vpp_hugepage_config, filename) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'. + format(cmd, node['host'], + stdout, stderr)) + + def get_actual_huge_pages(self): + """ + Get the current huge page configuration + + :returns the hugepage total, hugepage free, hugepage size, + total memory, and total memory free + :rtype: tuple + """ + + # Get the memory information using /proc/meminfo + cmd = 'sudo cat /proc/meminfo' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError( + '{} failed on node {} {} {}'.format( + cmd, self._node['host'], + stdout, stderr)) + + total = re.findall(r'HugePages_Total:\s+\w+', stdout) + free = re.findall(r'HugePages_Free:\s+\w+', stdout) + size = re.findall(r'Hugepagesize:\s+\w+\s+\w+', stdout) + memtotal = re.findall(r'MemTotal:\s+\w+\s+\w+', stdout) + memfree = re.findall(r'MemFree:\s+\w+\s+\w+', stdout) + + total = total[0].split(':')[1].lstrip() + free = free[0].split(':')[1].lstrip() + size = size[0].split(':')[1].lstrip() + memtotal = memtotal[0].split(':')[1].lstrip() + memfree = memfree[0].split(':')[1].lstrip() + return total, free, size, memtotal, memfree + + def show_huge_pages(self): + """ + Print the current huge page configuration + + """ + + node = self._node + hugepages = node['hugepages'] + print " {:30}: {}".format("Total System Memory", + hugepages['memtotal']) + print " {:30}: {}".format("Total Free Memory", + hugepages['memfree']) + print " {:30}: {}".format("Actual Huge Page Total", + hugepages['actual_total']) + print " {:30}: {}".format("Configured Huge Page Total", + hugepages['total']) + print " {:30}: {}".format("Huge Pages Free", hugepages['free']) + print " {:30}: {}".format("Huge Page Size", hugepages['size']) + + def get_huge_page_config(self): + """ + Returns the huge page config. + + :returns: The map max count and shmmax + """ + + total = self._node['hugepages']['total'] + max_map_count = int(total) * 2 + 1024 + shmmax = int(total) * 2 * 1024 * 1024 + return max_map_count, shmmax + + def __init__(self, node): + self._node = node diff --git a/extras/vpp_config/vpplib/VppPCIUtil.py b/extras/vpp_config/vpplib/VppPCIUtil.py new file mode 100644 index 00000000000..829d66ae374 --- /dev/null +++ b/extras/vpp_config/vpplib/VppPCIUtil.py @@ -0,0 +1,330 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""VPP PCI Utility libraries""" + +import re + +from vpplib.VPPUtil import VPPUtil + +DPDK_SCRIPT = "/vpp/vpp-config/scripts/dpdk-devbind.py" + +# PCI Device id regular expresssion +PCI_DEV_ID_REGEX = '[0-9A-Fa-f]+:[0-9A-Fa-f]+:[0-9A-Fa-f]+.[0-9A-Fa-f]+' + + +class VppPCIUtil(object): + """ + PCI Utilities + + """ + + @staticmethod + def _create_device_list(device_string): + """ + Returns a list of PCI devices + + :param device_string: The devices string from dpdk_devbind + :returns: The device list + :rtype: dictionary + """ + + devices = {} + + ids = re.findall(PCI_DEV_ID_REGEX, device_string) + descriptions = re.findall(r'\'([\s\S]*?)\'', device_string) + unused = re.findall(r'unused=[\w,]+', device_string) + + for i, j in enumerate(ids): + device = {'description': descriptions[i]} + if unused: + device['unused'] = unused[i].split('=')[1].split(',') + + cmd = 'ls /sys/bus/pci/devices/{}/driver/module/drivers'. \ + format(ids[i]) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret == 0: + device['driver'] = stdout.split(':')[1].rstrip('\n') + + cmd = 'cat /sys/bus/pci/devices/{}/numa_node'.format(ids[i]) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed {} {}'. + format(cmd, stderr, stdout)) + numa_node = stdout.rstrip('\n') + if numa_node == '-1': + device['numa_node'] = '0' + else: + device['numa_node'] = numa_node + + interfaces = [] + device['interfaces'] = [] + cmd = 'ls /sys/bus/pci/devices/{}/net'.format(ids[i]) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret == 0: + interfaces = stdout.rstrip('\n').split() + device['interfaces'] = interfaces + + l2_addrs = [] + for intf in interfaces: + cmd = 'cat /sys/bus/pci/devices/{}/net/{}/address'.format( + ids[i], intf) + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed {} {}'. + format(cmd, stderr, stdout)) + + l2_addrs.append(stdout.rstrip('\n')) + + device['l2addr'] = l2_addrs + + devices[ids[i]] = device + + return devices + + def __init__(self, node): + self._node = node + self._dpdk_devices = {} + self._kernel_devices = {} + self._other_devices = {} + self._crypto_dpdk_devices = {} + self._crypto_kernel_devices = {} + self._crypto_other_devices = {} + self._link_up_devices = {} + + def get_all_devices(self): + """ + Returns a list of all the devices + + """ + + node = self._node + rootdir = node['rootdir'] + dpdk_script = rootdir + DPDK_SCRIPT + cmd = dpdk_script + ' --status' + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'.format( + cmd, + node['host'], + stderr)) + + # Get the network devices using the DPDK + # First get everything after using DPDK + stda = stdout.split('Network devices using DPDK-compatible driver')[1] + # Then get everything before using kernel driver + using_dpdk = stda.split('Network devices using kernel driver')[0] + self._dpdk_devices = self._create_device_list(using_dpdk) + + # Get the network devices using the kernel + stda = stdout.split('Network devices using kernel driver')[1] + using_kernel = stda.split('Other network devices')[0] + self._kernel_devices = self._create_device_list(using_kernel) + + # Get the other network devices + stda = stdout.split('Other network devices')[1] + other = stda.split('Crypto devices using DPDK-compatible driver')[0] + self._other_devices = self._create_device_list(other) + + # Get the crypto devices using the DPDK + stda = stdout.split('Crypto devices using DPDK-compatible driver')[1] + crypto_using_dpdk = stda.split('Crypto devices using kernel driver')[0] + self._crypto_dpdk_devices = self._create_device_list( + crypto_using_dpdk) + + # Get the network devices using the kernel + stda = stdout.split('Crypto devices using kernel driver')[1] + crypto_using_kernel = stda.split('Other crypto devices')[0] + self._crypto_kernel_devices = self._create_device_list( + crypto_using_kernel) + + # Get the other network devices + crypto_other = stdout.split('Other crypto devices')[1] + self._crypto_other_devices = self._create_device_list(crypto_other) + + # Get the devices used by the kernel + for devk in self._kernel_devices.items(): + dvid = devk[0] + device = devk[1] + for i in device['interfaces']: + cmd = "ip addr show " + i + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {}'.format( + cmd, + node['host'], + stderr)) + lstate = re.findall(r'state \w+', stdout)[0].split(' ')[1] + + # Take care of the links that are UP + if lstate == 'UP': + device['linkup'] = True + self._link_up_devices[dvid] = device + + for devl in self._link_up_devices.items(): + dvid = devl[0] + del self._kernel_devices[dvid] + + def get_dpdk_devices(self): + """ + Returns a list the dpdk devices + + """ + return self._dpdk_devices + + def get_kernel_devices(self): + """ + Returns a list the kernel devices + + """ + return self._kernel_devices + + def get_other_devices(self): + """ + Returns a list the other devices + + """ + return self._other_devices + + def get_crypto_dpdk_devices(self): + """ + Returns a list the crypto dpdk devices + + """ + return self._crypto_dpdk_devices + + def get_crypto_kernel_devices(self): + """ + Returns a list the crypto kernel devices + + """ + return self._crypto_kernel_devices + + def get_crypto_other_devices(self): + """ + Returns a list the crypto other devices + + """ + return self._crypto_other_devices + + def get_link_up_devices(self): + """ + Returns a list the link up devices + + """ + return self._link_up_devices + + @staticmethod + def vpp_create_interface(interfaces, device_id, device): + """ + Create an interface using the device is and device + + """ + + name = 'port' + str(len(interfaces)) + interfaces[name] = {} + interfaces[name]['pci_address'] = device_id + interfaces[name]['numa_node'] = device['numa_node'] + if 'l2addr' in device: + l2_addrs = device['l2addr'] + for i, j in enumerate(l2_addrs): + if i > 0: + mname = 'mac_address' + str(i + 1) + interfaces[name][mname] = l2_addrs[i] + else: + interfaces[name]['mac_address'] = l2_addrs[i] + + @staticmethod + def show_vpp_devices(devices, show_interfaces=True, show_header=True): + """ + show the vpp devices specified in the argument + + :param devices: A list of devices + :param show_interfaces: show the kernel information + :type devices: dict + :type show_interfaces: bool + """ + + + if show_interfaces: + header = "{:15} {:25} {:50}".format("PCI ID", + "Kernel Interface(s)", + "Description") + else: + header = "{:15} {:50}".format("PCI ID", + "Description") + dashseparator = ("-" * (len(header) - 2)) + + + if show_header == True: + print header + print dashseparator + for dit in devices.items(): + dvid = dit[0] + device = dit[1] + if show_interfaces: + interfaces = device['interfaces'] + interface = '' + for i, j in enumerate(interfaces): + if i > 0: + interface += ',' + interfaces[i] + else: + interface = interfaces[i] + + print "{:15} {:25} {:50}".format( + dvid, interface, device['description']) + else: + print "{:15} {:50}".format( + dvid, device['description']) + + @staticmethod + def unbind_vpp_device(node, device_id): + """ + unbind the device specified + + :param node: Node dictionary with cpuinfo. + :param device_id: The device id + :type node: dict + :type device_id: string + """ + + + rootdir = node['rootdir'] + dpdk_script = rootdir + DPDK_SCRIPT + cmd = dpdk_script + ' -u ' + ' ' + device_id + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'.format( + cmd, node['host'], + stdout, stderr)) + + @staticmethod + def bind_vpp_device(node, driver, device_id): + """ + bind the device specified + + :param node: Node dictionary with cpuinfo. + :param driver: The driver + :param device_id: The device id + :type node: dict + :type driver: string + :type device_id: string + """ + + rootdir = node['rootdir'] + dpdk_script = rootdir + DPDK_SCRIPT + cmd = dpdk_script + ' -b ' + driver + ' ' + device_id + (ret, stdout, stderr) = VPPUtil.exec_command(cmd) + if ret != 0: + raise RuntimeError('{} failed on node {} {} {}'.format( + cmd, node['host'], stdout, stderr)) diff --git a/extras/vpp_config/vpplib/__init__.py b/extras/vpp_config/vpplib/__init__.py new file mode 100644 index 00000000000..54b42722be7 --- /dev/null +++ b/extras/vpp_config/vpplib/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +__init__ file for directory lib +""" diff --git a/extras/vpp_config/vpplib/constants.py b/extras/vpp_config/vpplib/constants.py new file mode 100644 index 00000000000..051a21cf023 --- /dev/null +++ b/extras/vpp_config/vpplib/constants.py @@ -0,0 +1,48 @@ +# Copyright (c) 2016 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Constants used in CSIT.""" + + +class Constants(object): + """Constants used in CSIT.""" + + # OpenVPP testing directory location at topology nodes + REMOTE_FW_DIR = '/tmp/openvpp-testing' + + # shell scripts location + RESOURCES_LIB_SH = 'resources/libraries/bash' + + # vat templates location + RESOURCES_TPL_VAT = 'resources/templates/vat' + + # OpenVPP VAT binary name + VAT_BIN_NAME = 'vpp_api_test' + + # QEMU version to install + QEMU_INSTALL_VERSION = 'qemu-2.5.0' + + # QEMU install directory + QEMU_INSTALL_DIR = '/opt/qemu-2.5.0' + + # Honeycomb directory location at topology nodes: + REMOTE_HC_DIR = '/opt/honeycomb' + + # Honeycomb persistence files location + REMOTE_HC_PERSIST = '/var/lib/honeycomb/persist' + + # Honeycomb templates location + RESOURCES_TPL_HC = 'resources/templates/honeycomb' + + # ODL Client Restconf listener port + ODL_PORT = 8181 -- cgit 1.2.3-korg