diff options
724 files changed, 71528 insertions, 41 deletions
diff --git a/.gitignore b/.gitignore index e57e2953a..e86ef4036 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,139 @@ -debian/.debhelper/ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +.idea/ +.idea +.idea/* +*.iml + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Autogenerated files +templates-cfg/* +templates-op/* +tests/templates/* + +# Debian packaging debian/files -debian/vyos-smoketest.debhelper.log -debian/vyos-smoketest.substvars -debian/vyos-smoketest/ +debian/tmp +debian/debhelper-build-stamp +debian/.debhelper/ +debian/vyos-1x +debian/vyos-1x-vmware +debian/vyos-1x-smoketest +debian/*.postinst.debhelper +debian/*.prerm.debhelper +debian/*.substvars + +# Sonar Cloud +.scannerwork +/.vs + +# SlickEdit +*.vpj +*.vpw +*.vpwhist +*.vtg + +# VIM +*.swp + +# vyos-1x JSON version +data/component-versions.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..d8177a5f5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,12 @@ +# Contributing to VyOS + +You wan't to help us improve VyOS? This is awesome. We accept any kind of Pull +Requests on GitHub. To make the life of the maintainers and you as future +contributor (or maybe maintainer) much easier we have come up with some basic +rules. Instead of copy/pasting or maintaining two instances of how to contribute +to VyOS you can find the entire process documented in our online documentation: +https://docs.vyos.io/en/latest/contributing/development.html + +Also this guide might not be complete so any PR is much appreciated. + +It might also worth browsing our blog: https://blog.vyos.io diff --git a/LICENSE.GPL b/LICENSE.GPL new file mode 100644 index 000000000..23cb79033 --- /dev/null +++ b/LICENSE.GPL @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/LICENSE.LGPL b/LICENSE.LGPL new file mode 100644 index 000000000..4362b4915 --- /dev/null +++ b/LICENSE.LGPL @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! @@ -1,7 +1,148 @@ +TMPL_DIR := templates-cfg +OP_TMPL_DIR := templates-op +BUILD_DIR := build +DATA_DIR := data +CFLAGS := + +src = $(wildcard interface-definitions/*.xml.in) +obj = $(src:.xml.in=.xml) + +%.xml: %.xml.in + @echo Generating $(BUILD_DIR)/$@ from $< + # -ansi This turns off certain features of GCC that are incompatible + # with ISO C90. Without this regexes containing '/' as in an URL + # won't work + # -x c By default GCC guesses the input language from its file extension, + # thus XML is unknown. Force it to C language + # -E Stop after the preprocessing stage + # -undef Do not predefine any system-specific or GCC-specific macros. + # -nostdinc Do not search the standard system directories for header files + # -P Inhibit generation of linemarkers in the output from the + # preprocessor + @$(CC) -x c-header -E -undef -nostdinc -P -I$(CURDIR)/interface-definitions -o $(BUILD_DIR)/$@ -c $< + +$(BUILD_DIR): + install -d -m 0755 $(BUILD_DIR)/interface-definitions + install -d -m 0755 $(BUILD_DIR)/op-mode-definitions + +.PHONY: interface_definitions +.ONESHELL: +interface_definitions: $(BUILD_DIR) $(obj) + mkdir -p $(TMPL_DIR) + + find $(BUILD_DIR)/interface-definitions -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-templates {} $(CURDIR)/schema/interface_definition.rng $(TMPL_DIR) || exit 1 + + # XXX: delete top level node.def's that now live in other packages + rm -f $(TMPL_DIR)/firewall/node.def + rm -f $(TMPL_DIR)/interfaces/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif-s/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif-s/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/l2tpv3/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/openvpn/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/tunnel/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/tunnel/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/wirelessmodem/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/protocols/node.def + rm -rf $(TMPL_DIR)/protocols/nbgp + rm -rf $(TMPL_DIR)/protocols/isis + rm -f $(TMPL_DIR)/protocols/static/node.def + rm -f $(TMPL_DIR)/system/node.def + rm -f $(TMPL_DIR)/vpn/node.def + rm -f $(TMPL_DIR)/vpn/ipsec/node.def + +.PHONY: op_mode_definitions +.ONESHELL: +op_mode_definitions: + mkdir -p $(OP_TMPL_DIR) + + find $(CURDIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1 + + # XXX: delete top level op mode node.def's that now live in other packages + rm -f $(OP_TMPL_DIR)/add/node.def + rm -f $(OP_TMPL_DIR)/clear/node.def + rm -f $(OP_TMPL_DIR)/clear/interfaces/node.def + rm -f $(OP_TMPL_DIR)/set/node.def + rm -f $(OP_TMPL_DIR)/show/node.def + rm -f $(OP_TMPL_DIR)/show/interfaces/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/bgp/node.def + rm -f $(OP_TMPL_DIR)/show/ipv6/route/node.def + rm -f $(OP_TMPL_DIR)/restart/node.def + rm -f $(OP_TMPL_DIR)/monitor/node.def + rm -f $(OP_TMPL_DIR)/generate/node.def + rm -f $(OP_TMPL_DIR)/show/system/node.def + rm -f $(OP_TMPL_DIR)/show/vpn/node.def + rm -f $(OP_TMPL_DIR)/delete/node.def + rm -f $(OP_TMPL_DIR)/reset/vpn/node.def + + # XXX: ping must be able to recursivly call itself as the + # options are provided from the script itself + ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/ + +.PHONY: component_versions +.ONESHELL: +component_versions: $(BUILD_DIR) $(obj) + $(CURDIR)/scripts/build-component-versions $(BUILD_DIR)/interface-definitions $(DATA_DIR) + .PHONY: all +all: clean interface_definitions op_mode_definitions component_versions -all: - # Install is just xcopy +.PHONY: clean +clean: + rm -rf $(BUILD_DIR) + rm -rf $(TMPL_DIR) + rm -rf $(OP_TMPL_DIR) + +.PHONY: test +test: + set -e; python3 -m compileall -q . + PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose + +.PHONY: sonar +sonar: + sonar-scanner -X -Dsonar.login=${SONAR_TOKEN} + +.PHONY: docs +.ONESHELL: +docs: + sphinx-apidoc -o sphinx/source/ python/ + cd sphinx/ + PYTHONPATH=../python make html deb: dpkg-buildpackage -uc -us -tc -b + +.PHONY: schema +schema: + trang -I rnc -O rng schema/interface_definition.rnc schema/interface_definition.rng + trang -I rnc -O rng schema/op-mode-definition.rnc schema/op-mode-definition.rng diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..5bb9fb73d --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] +lxml = "*" +pylint = "*" +nose = "*" +coverage = "*" + +[packages] +vyos = {file = "./python"} +jinja2 = "*" + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 000000000..eae5ea9fe --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,248 @@ +{ + "_meta": { + "hash": { + "sha256": "5564acff86bcf8ef717129935c288ffed2631f1d795d1c44d0af36779593b89b" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "index": "pypi", + "version": "==2.10.1" + }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "version": "==1.1.0" + }, + "vyos": { + "file": "./python" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "coverage": { + "hashes": [ + "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", + "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", + "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", + "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", + "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", + "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", + "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", + "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", + "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", + "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", + "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", + "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", + "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", + "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", + "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", + "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", + "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", + "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", + "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", + "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", + "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", + "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", + "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", + "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", + "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", + "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", + "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", + "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", + "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", + "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", + "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + ], + "index": "pypi", + "version": "==4.5.2" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "lxml": { + "hashes": [ + "sha256:0dd6589fa75d369ba06d2b5f38dae107f76ea127f212f6a7bee134f6df2d1d21", + "sha256:1afbac344aa68c29e81ab56c1a9411c3663157b5aee5065b7fa030b398d4f7e0", + "sha256:1baad9d073692421ad5dbbd81430aba6c7f5fdc347f03537ae046ddf2c9b2297", + "sha256:1d8736421a2358becd3edf20260e41a06a0bf08a560480d3a5734a6bcbacf591", + "sha256:1e1d9bddc5afaddf0de76246d3f2152f961697ad7439c559f179002682c45801", + "sha256:1f179dc8b2643715f020f4d119d5529b02cd794c1c8f305868b73b8674d2a03f", + "sha256:241fb7bdf97cb1df1edfa8f0bcdfd80525d4023dac4523a241907c8b2f44e541", + "sha256:2f9765ee5acd3dbdcdc0d0c79309e01f7c16bc8d39b49250bf88de7b46daaf58", + "sha256:312e1e1b1c3ce0c67e0b8105317323e12807955e8186872affb667dbd67971f6", + "sha256:3273db1a8055ca70257fd3691c6d2c216544e1a70b673543e15cc077d8e9c730", + "sha256:34dfaa8c02891f9a246b17a732ca3e99c5e42802416628e740a5d1cb2f50ff49", + "sha256:3aa3f5288af349a0f3a96448ebf2e57e17332d99f4f30b02093b7948bd9f94cc", + "sha256:51102e160b9d83c1cc435162d90b8e3c8c93b28d18d87b60c56522d332d26879", + "sha256:56115fc2e2a4140e8994eb9585119a1ae9223b506826089a3ba753a62bd194a6", + "sha256:69d83de14dbe8fe51dccfd36f88bf0b40f5debeac763edf9f8325180190eba6e", + "sha256:99fdce94aeaa3ccbdfcb1e23b34273605c5853aa92ec23d84c84765178662c6c", + "sha256:a7c0cd5b8a20f3093ee4a67374ccb3b8a126743b15a4d759e2a1bf098faac2b2", + "sha256:abe12886554634ed95416a46701a917784cb2b4c77bfacac6916681d49bbf83d", + "sha256:b4f67b5183bd5f9bafaeb76ad119e977ba570d2b0e61202f534ac9b5c33b4485", + "sha256:bdd7c1658475cc1b867b36d5c4ed4bc316be8d3368abe03d348ba906a1f83b0e", + "sha256:c6f24149a19f611a415a51b9bc5f17b6c2f698e0d6b41ffb3fa9f24d35d05d73", + "sha256:d1e111b3ab98613115a208c1017f266478b0ab224a67bc8eac670fa0bad7d488", + "sha256:d6520aa965773bbab6cb7a791d5895b00d02cf9adc93ac2bf4edb9ac1a6addc5", + "sha256:dd185cde2ccad7b649593b0cda72021bc8a91667417001dbaf24cd746ecb7c11", + "sha256:de2e5b0828a9d285f909b5d2e9d43f1cf6cf21fe65bc7660bdaa1780c7b58298", + "sha256:f726444b8e909c4f41b4fde416e1071cf28fa84634bfb4befdf400933b6463af" + ], + "index": "pypi", + "version": "==4.3.0" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "nose": { + "hashes": [ + "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac", + "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a", + "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98" + ], + "index": "pypi", + "version": "==1.3.7" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "typed-ast": { + "hashes": [ + "sha256:023625bfa9359e29bd6e24cac2a4503495b49761d48a5f1e38333fc4ac4d93fe", + "sha256:07591f7a5fdff50e2e566c4c1e9df545c75d21e27d98d18cb405727ed0ef329c", + "sha256:153e526b0f4ffbfada72d0bb5ffe8574ba02803d2f3a9c605c8cf99dfedd72a2", + "sha256:3ad2bdcd46a4a1518d7376e9f5016d17718a9ed3c6a3f09203d832f6c165de4a", + "sha256:3ea98c84df53ada97ee1c5159bb3bc784bd734231235a1ede14c8ae0775049f7", + "sha256:51a7141ccd076fa561af107cfb7a8b6d06a008d92451a1ac7e73149d18e9a827", + "sha256:52c93cd10e6c24e7ac97e8615da9f224fd75c61770515cb323316c30830ddb33", + "sha256:6344c84baeda3d7b33e157f0b292e4dd53d05ddb57a63f738178c01cac4635c9", + "sha256:64699ca1b3bd5070bdeb043e6d43bc1d0cebe08008548f4a6bee782b0ecce032", + "sha256:74903f2e56bbffe29282ef8a5487d207d10be0f8513b41aff787d954a4cf91c9", + "sha256:7891710dba83c29ee2bd51ecaa82f60f6bede40271af781110c08be134207bf2", + "sha256:91976c56224e26c256a0de0f76d2004ab885a29423737684b4f7ebdd2f46dde2", + "sha256:9bad678a576ecc71f25eba9f1e3fd8d01c28c12a2834850b458428b3e855f062", + "sha256:b4726339a4c180a8b6ad9d8b50d2b6dc247e1b79b38fe2290549c98e82e4fd15", + "sha256:ba36f6aa3f8933edf94ea35826daf92cbb3ec248b89eccdc053d4a815d285357", + "sha256:bbc96bde544fd19e9ef168e4dfa5c3dfe704bfa78128fa76f361d64d6b0f731a", + "sha256:c0c927f1e44469056f7f2dada266c79b577da378bbde3f6d2ada726d131e4824", + "sha256:c0f9a3708008aa59f560fa1bd22385e05b79b8e38e0721a15a8402b089243442", + "sha256:f0bf6f36ff9c5643004171f11d2fdc745aa3953c5aacf2536a0685db9ceb3fb1", + "sha256:f5be39a0146be663cbf210a4d95c3c58b2d7df7b043c9047c5448e358f0550a2", + "sha256:fcd198bf19d9213e5cbf2cde2b9ef20a9856e716f76f9476157f90ae6de06cc6" + ], + "markers": "python_version < '3.7' and implementation_name == 'cpython'", + "version": "==1.2.0" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} @@ -1,11 +1,63 @@ -vyos-smoketest -============== +# vyos-1x: VyOS 1.2.0+ configuration scripts and data -This is a set of scripts and test data for sanity checking VyOS builds. +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vyos%3Avyos-1x&metric=coverage)](https://sonarcloud.io/component_measures?id=vyos%3Avyos-1x&metric=coverage) +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_shield) -The main entry point is /usr/bin/vyos-smoketest +VyOS 1.1.x had its codebase split into way too many submodules for no good reason, which made it hard +to navigate or write meaningful changelogs. As the code undergoes rewrite in the new style in VyOS 1.2.0+, +we consolidate the rewritten code in this package. -It will try to check for common things that break such as kernel modules not loading, -and print a test report. +If you just want to build a VyOS image, the repository you want is [vyos-build](https://github.com/vyos/vyos-build). +If you also want to contribute to VyOS, read on. -It also comes with a huge reference config that has almost every feature set. +## Package layout + +``` +interface-definitions # Configuration interface (i.e. conf mode command) definitions +op-mode-definitions # Operational command definitions +src + conf_mode/ # Configuration mode scripts + op_mode/ # Operational mode scripts + completion/ # Completion helpers + validators/ # Value validators + helpers/ # Misc helpers + migration-scripts # Migration scripts + tests/ # Unit tests + +python/ # Python modules + +scripts/ # Build-time scripts +schema/ # XML schemas +``` + +## Interface/command definitions + +Raw node.def files for the old backend are no longer written by hand or generated by custom sciprts. +They are all now produced from a unified XML format that supports a strict subset of the old backend +features. In particular, it intentionally does not support embedded shell scripts, default values, +and value "types", instead delegating those tasks to external scripts. + +Configuration interface definitions must conform to the schema found in schema/interface_definition.rng +and operational command definitions must conform to schema/op-mode-definition.rng +Schema checks are performed at build time, so a package with malformed interface definitions will not build. + +## Configuration scripts + +The guidelines in a nutshell: + +* Use separate functions for retrieving configuration data, validating it, and generating taret config +* Use a template processor when the format is more complex than just one line (jinja2 and pystache are acceptable options) + +## Tests + +Tests are executed at build time, you can also execute them by hand with: + +``` +pipenv install +pipenv shell +make test +``` + + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_large)
\ No newline at end of file diff --git a/data/templates/accel-ppp/chap-secrets.ipoe.tmpl b/data/templates/accel-ppp/chap-secrets.ipoe.tmpl new file mode 100644 index 000000000..a7d899354 --- /dev/null +++ b/data/templates/accel-ppp/chap-secrets.ipoe.tmpl @@ -0,0 +1,18 @@ +# username server password acceptable local IP addresses shaper +{% for interface in auth_interfaces -%} +{% for mac in interface.mac -%} +{% if mac.rate_upload and mac.rate_download -%} +{% if mac.vlan_id -%} +{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} * {{ mac.rate_download }}/{{ mac.rate_upload }} +{% else -%} +{{ interface.name }} * {{ mac.address | lower }} * {{ mac.rate_download }}/{{ mac.rate_upload }} +{% endif -%} +{% else -%} +{% if mac.vlan_id -%} +{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} * +{% else -%} +{{ interface.name }} * {{ mac.address | lower }} * +{% endif -%} +{% endif -%} +{% endfor -%} +{% endfor -%} diff --git a/data/templates/accel-ppp/chap-secrets.tmpl b/data/templates/accel-ppp/chap-secrets.tmpl new file mode 100644 index 000000000..dd00d7bd0 --- /dev/null +++ b/data/templates/accel-ppp/chap-secrets.tmpl @@ -0,0 +1,10 @@ +# username server password acceptable local IP addresses shaper +{% for user in local_users %} +{% if user.state == 'enabled' %} +{% if user.upload and user.download %} +{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} {{ user.download }} / {{ user.upload }} +{% else %} +{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} +{% endif %} +{% endif %} +{% endfor %} diff --git a/data/templates/accel-ppp/ipoe.config.tmpl b/data/templates/accel-ppp/ipoe.config.tmpl new file mode 100644 index 000000000..fca520efa --- /dev/null +++ b/data/templates/accel-ppp/ipoe.config.tmpl @@ -0,0 +1,111 @@ +### generated by ipoe.py ### +[modules] +log_syslog +ipoe +shaper +ipv6pool +ipv6_nd +ipv6_dhcp +ippool +{% if auth_mode == 'radius' %} +radius +{% elif auth_mode == 'local' %} +chap-secrets +{% endif %} + +[core] +thread-count={{ thread_cnt }} + +[log] +syslog=accel-ipoe,daemon +copy=1 +level=5 + +[ipoe] +verbose=1 +{% for interface in interfaces %} +{% if interface.vlan_mon %} +interface=re:{{ interface.name }}\.\d+,{% else %}interface={{ interface.name }},{% endif %}shared={{ interface.shared }},mode={{ interface.mode }},ifcfg={{ interface.ifcfg }},range={{ interface.range }},start={{ interface.sess_start }},ipv6=1 +{% endfor %} +{% if auth_mode == 'noauth' %} +noauth=1 +{% elif auth_mode == 'local' %} +username=ifname +password=csid +{% endif %} + +{%- for interface in interfaces %} +{% if (interface.shared == '0') and (interface.vlan_mon) %} +vlan-mon={{ interface.name }},{{ interface.vlan_mon | join(',') }} +{% endif %} +{% endfor %} + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if dnsv6 %} +[ipv6-dns] +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} +{% endif %} + +[ipv6-nd] +verbose=1 + +[ipv6-dhcp] +verbose=1 + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} +{% endfor %} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} +{% endfor %} +{% endif %} + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets={{ chap_secrets_file }} +{% elif auth_mode == 'radius' %} +[radius] +verbose=1 +{% for r in radius_server %} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} +{% endfor -%} + +acct-timeout={{ radius_acct_tmo }} +timeout={{ radius_timeout }} +max-try={{ radius_max_try }} +{% if radius_nas_id %} +nas-identifier={{ radius_nas_id }} +{% endif -%} +{% if radius_nas_ip %} +nas-ip-address={{ radius_nas_ip }} +{% endif -%} +{% if radius_source_address %} +bind={{ radius_source_address }} +{% endif -%} + +{% if radius_dynamic_author %} +dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} +{% endif -%} + +{% if radius_shaper_attr %} +[shaper] +verbose=1 +attr={{ radius_shaper_attr }} +{% if radius_shaper_vendor %} +vendor={{ radius_shaper_vendor }} +{% endif -%} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2002 diff --git a/data/templates/accel-ppp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl new file mode 100644 index 000000000..b9131684d --- /dev/null +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -0,0 +1,148 @@ +### generated by accel_l2tp.py ### +[modules] +log_syslog +l2tp +chap-secrets +{% for proto in auth_proto: %} +{{proto}} +{% endfor%} + +{% if auth_mode == 'radius' %} +radius +{% endif -%} + +ippool +shaper +ipv6pool +ipv6_nd +ipv6_dhcp + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-l2tp,daemon +copy=1 +level=5 + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if dnsv6 %} +[ipv6-dns] +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} +{% endif %} + +{% if wins %} +[wins] +{% for server in wins -%} +wins{{ loop.index }}={{ server }} +{% endfor -%} +{% endif %} + +[l2tp] +verbose=1 +ifname=l2tp%d +ppp-max-mtu={{ mtu }} +mppe={{ ppp_mppe }} +{% if outside_addr %} +bind={{ outside_addr }} +{% endif %} +{% if lns_shared_secret %} +secret={{ lns_shared_secret }} +{% endif %} + +[client-ip-range] +0.0.0.0/0 + +{% if client_ip_pool or client_ip_subnets %} +[ip-pool] +{% if client_ip_pool %} +{{ client_ip_pool }} +{% endif -%} +{% if client_ip_subnets %} +{% for sn in client_ip_subnets %} +{{sn}} +{% endfor -%} +{% endif %} +{% endif %} +{% if gateway_address %} +gw-ip-address={{ gateway_address }} +{% endif %} + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets={{ chap_secrets_file }} +{% elif auth_mode == 'radius' %} +[radius] +verbose=1 +{% for r in radius_server %} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} +{% endfor -%} + +acct-timeout={{ radius_acct_tmo }} +timeout={{ radius_timeout }} +max-try={{ radius_max_try }} + +{% if radius_nas_id %} +nas-identifier={{ radius_nas_id }} +{% endif -%} +{% if radius_nas_ip %} +nas-ip-address={{ radius_nas_ip }} +{% endif -%} +{% if radius_source_address %} +bind={{ radius_source_address }} +{% endif -%} +{% endif %} +{% if gateway_address %} +gw-ip-address={{ gateway_address }} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +single-session=replace +lcp-echo-timeout={{ ppp_echo_timeout }} +lcp-echo-interval={{ ppp_echo_interval }} +lcp-echo-failure={{ ppp_echo_failure }} +{% if ccp_disable %} +ccp=0 +{% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} +{% endfor %} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} +{% endfor %} +{% endif %} + +{% if client_ipv6_delegate_prefix %} +[ipv6-dhcp] +verbose=1 +{% endif %} + +{% if radius_shaper_attr %} +[shaper] +verbose=1 +attr={{ radius_shaper_attr }} +{% if radius_shaper_vendor %} +vendor={{ radius_shaper_vendor }} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2004 +sessions-columns=ifname,username,calling-sid,ip,{{ ip6_column | join(',') }}{{ ',' if ip6_column }}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl new file mode 100644 index 000000000..5ad628fde --- /dev/null +++ b/data/templates/accel-ppp/pppoe.config.tmpl @@ -0,0 +1,204 @@ +### generated by accel_pppoe.py ### +[modules] +log_syslog +pppoe +{% if auth_mode == 'radius' %} +radius +{% endif %} +chap-secrets +ippool +{% if ppp_ipv6 != 'deny' %} +ipv6pool +ipv6_nd +ipv6_dhcp +{% endif %} +{% for proto in auth_proto: %} +{{proto}} +{% endfor%} +shaper +{% if snmp %} +net-snmp +{% endif %} +{% if limits %} +connlimit +{% endif %} + +[core] +thread-count={{ thread_cnt }} + +[log] +syslog=accel-pppoe,daemon +copy=1 +level=5 + +{% if snmp == 'enable-ma' %} +[snmp] +master=1 +{% endif %} + +[client-ip-range] +disable + +{% if ppp_gw %} +[ip-pool] +gw-ip-address={{ ppp_gw }} +{% if client_ip_pool %} +{{ client_ip_pool }} +{% endif -%} +{% if client_ip_subnets %} +{% for subnet in client_ip_subnets %} +{{ subnet }} +{% endfor %} +{% endif %} +{% endif %} + +{% if client_ipv6_pool %} +[ipv6-nd] +AdvAutonomousFlag=1 + +[ipv6-pool] +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} +{% endfor %} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} +{% endfor %} +{% endif %} + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if dnsv6 %} +[ipv6-dns] +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} +{% endif %} + +{% if wins %} +[wins] +{% for server in wins -%} +wins{{ loop.index }}={{ server }} +{% endfor -%} +{% endif %} + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets={{ chap_secrets_file }} +{% elif auth_mode == 'radius' %} +[radius] +verbose=1 +{% for r in radius_server %} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} +{% endfor -%} + +acct-timeout={{ radius_acct_tmo }} +timeout={{ radius_timeout }} +max-try={{ radius_max_try }} + +{% if radius_nas_id %} +nas-identifier={{ radius_nas_id }} +{% endif -%} +{% if radius_nas_ip %} +nas-ip-address={{ radius_nas_ip }} +{% endif -%} +{% if radius_source_address %} +bind={{ radius_source_address }} +{% endif -%} + + +{% if radius_dynamic_author %} +dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} +{% endif -%} +{% endif %} +{% if ppp_gw %} +gw-ip-address={{ ppp_gw }} +{% endif %} + +{% if sesscrtl != 'disable' %} +[common] +single-session={{ sesscrtl }} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +{% if ppp_ccp %} +ccp=1 +{% else %} +ccp=0 +{% endif %} +{% if ppp_min_mtu %} +min-mtu={{ ppp_min_mtu }} +{% else %} +min-mtu={{ mtu }} +{% endif %} +{% if ppp_mru %} +mru={{ ppp_mru }} +{% endif %} +mppe={{ ppp_mppe }} +lcp-echo-interval={{ ppp_echo_interval }} +lcp-echo-timeout={{ ppp_echo_timeout }} +lcp-echo-failure={{ ppp_echo_failure }} +{% if ppp_ipv4 %} +ipv4={{ ppp_ipv4 }} +{% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + +{% if ppp_ipv6 %} +ipv6={{ ppp_ipv6 }} +{% if ppp_ipv6_intf_id %} +ipv6-intf-id={{ ppp_ipv6_intf_id }} +{% endif %} +{% if ppp_ipv6_peer_intf_id %} +ipv6-peer-intf-id={{ ppp_ipv6_peer_intf_id }} +{% endif %} +{% if ppp_ipv6_accept_peer_intf_id %} +ipv6-accept-peer-intf-id={{ ppp_ipv6_accept_peer_intf_id }} +{% endif %} +{% endif %} +mtu={{ mtu }} + +[pppoe] +verbose=1 +ac-name={{ concentrator }} + +{% if interfaces %} +{% for interface in interfaces %} +interface={{ interface.name }} +{% if interface.vlans %} +vlan-mon={{ interface.name }},{{ interface.vlans | join(',') }} +interface=re:{{ interface.name }}\.\d+ +{% endif %} +{% endfor -%} +{% endif -%} + +{% if svc_name %} +service-name={{ svc_name|join(',') }} +{% endif -%} + +{% if pado_delay %} +pado-delay={{ pado_delay }} +{% endif %} + +{% if limits_burst or limits_connections or limits_connections %} +[connlimit] +{% if limits_connections %} +limit={{ limits_connections }} +{% endif %} +{% if limits_burst %} +burst={{ limits_burst }} +{% endif %} +{% if limits_timeout %} +timeout={{ limits_timeout }} +{% endif %} +{% endif %} + +[cli] +tcp=127.0.0.1:2001 diff --git a/data/templates/accel-ppp/pptp.config.tmpl b/data/templates/accel-ppp/pptp.config.tmpl new file mode 100644 index 000000000..e0f2c6da9 --- /dev/null +++ b/data/templates/accel-ppp/pptp.config.tmpl @@ -0,0 +1,89 @@ +### generated by accel_pptp.py ### +[modules] +log_syslog +pptp +ippool +{% if auth_mode == 'local' %} +chap-secrets +{% elif auth_mode == 'radius' %} +radius +{% endif -%} +{% for proto in auth_proto %} +{{proto}} +{% endfor %} + +[core] +thread-count={{ thread_cnt }} + +[log] +syslog=accel-pptp,daemon +copy=1 +level=5 + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if wins %} +[wins] +{% for server in wins -%} +wins{{ loop.index }}={{ server }} +{% endfor -%} +{% endif %} + + +[pptp] +ifname=pptp%d +{% if outside_addr %} +bind={{ outside_addr }} +{% endif %} +verbose=1 +ppp-max-mtu={{mtu}} +mppe={{ ppp_mppe }} +echo-interval=10 +echo-failure=3 + + +[client-ip-range] +0.0.0.0/0 + +[ip-pool] +tunnel={{ client_ip_pool }} +gw-ip-address={{ gw_ip }} + +[ppp] +verbose=5 +check-ip=1 +single-session=replace + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets={{ chap_secrets_file }} +{% elif auth_mode == 'radius' %} +[radius] +verbose=1 +{% for r in radius_server %} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} +{% endfor -%} + +acct-timeout={{ radius_acct_tmo }} +timeout={{ radius_timeout }} +max-try={{ radius_max_try }} + +{% if radius_nas_id %} +nas-identifier={{ radius_nas_id }} +{% endif -%} +{% if radius_nas_ip %} +nas-ip-address={{ radius_nas_ip }} +{% endif -%} +{% if radius_source_address %} +bind={{ radius_source_address }} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2003 + diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl new file mode 100644 index 000000000..c9e4a1d7d --- /dev/null +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -0,0 +1,146 @@ +### generated by vpn_sstp.py ### +[modules] +log_syslog +sstp +shaper +{% if auth_mode == 'local' %} +chap-secrets +{% elif auth_mode == 'radius' %} +radius +{% endif -%} +ippool +ipv6pool +ipv6_nd +ipv6_dhcp + +{% for proto in auth_proto %} +{{proto}} +{% endfor %} + +[core] +thread-count={{thread_cnt}} + +[common] +single-session=replace + +[log] +syslog=accel-sstp,daemon +copy=1 +level=5 + +[client-ip-range] +disable + +[sstp] +verbose=1 +ifname=sstp%d +accept=ssl +ssl-ca-file={{ ssl_ca }} +ssl-pemfile={{ ssl_cert }} +ssl-keyfile={{ ssl_key }} + +{% if client_ip_pool %} +[ip-pool] +gw-ip-address={{ client_gateway }} +{% for subnet in client_ip_pool %} +{{ subnet }} +{% endfor %} +{% endif %} + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if dnsv6 %} +[ipv6-dns] +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} +{% endif %} + + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets={{ chap_secrets_file }} +{% elif auth_mode == 'radius' %} +[radius] +verbose=1 +{% for r in radius_server %} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} +{% endfor -%} + +acct-timeout={{ radius_acct_tmo }} +timeout={{ radius_timeout }} +max-try={{ radius_max_try }} + +{% if radius_nas_id %} +nas-identifier={{ radius_nas_id }} +{% endif -%} +{% if radius_nas_ip %} +nas-ip-address={{ radius_nas_ip }} +{% endif -%} +{% if radius_source_address %} +bind={{ radius_source_address }} +{% endif -%} + + +{% if radius_dynamic_author %} +dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} +{% endif -%} +{% endif %} +{% if client_gateway %} +gw-ip-address={{ client_gateway }} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +{% if mtu %} +mtu={{ mtu }} +{% endif -%} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + +{% if ppp_mppe %} +mppe={{ ppp_mppe }} +{% endif -%} +{% if ppp_echo_interval %} +lcp-echo-interval={{ ppp_echo_interval }} +{% endif -%} +{% if ppp_echo_failure %} +lcp-echo-failure={{ ppp_echo_failure }} +{% endif -%} +{% if ppp_echo_timeout %} +lcp-echo-timeout={{ ppp_echo_timeout }} +{% endif %} + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} +{% endfor %} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} +{% endfor %} +{% endif %} + +{% if client_ipv6_delegate_prefix %} +[ipv6-dhcp] +verbose=1 +{% endif %} + +{% if radius_shaper_attr %} +[shaper] +verbose=1 +attr={{ radius_shaper_attr }} +{% if radius_shaper_vendor %} +vendor={{ radius_shaper_vendor }} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2005 diff --git a/data/templates/bcast-relay/udp-broadcast-relay.tmpl b/data/templates/bcast-relay/udp-broadcast-relay.tmpl new file mode 100644 index 000000000..d0c7d8bf9 --- /dev/null +++ b/data/templates/bcast-relay/udp-broadcast-relay.tmpl @@ -0,0 +1,7 @@ +### Autogenerated by bcast_relay.py ### + +# UDP broadcast relay configuration for instance {{ id }} +{%- if description %} +# Comment: {{ description }} +{% endif %} +DAEMON_ARGS="{{ '-s ' + address if address is defined }} {{ instance }} {{ port }} {{ interface | join(' ') }}" diff --git a/data/templates/conserver/conserver.conf.tmpl b/data/templates/conserver/conserver.conf.tmpl new file mode 100644 index 000000000..4e7b5d8d7 --- /dev/null +++ b/data/templates/conserver/conserver.conf.tmpl @@ -0,0 +1,37 @@ +### Autogenerated by service_console-server.py ### + +# See https://www.conserver.com/docs/conserver.cf.man.html for additional options + +config * { + primaryport 3109; + daemonmode false; +} + +default * { + motd "VyOS Console Server"; + rw *; +} + +## +## list of consoles we serve +## +{% for key, value in device.items() %} +{# Depending on our USB serial console we could require a path adjustment #} +{% set path = '/dev' if key.startswith('ttyS') else '/dev/serial/by-bus' %} +console {{ key }} { + master localhost; + type device; + device {{ path }}/{{ key }}; + baud {{ value.speed }}; + parity {{ value.parity }}; + options {{ "!" if value.stop_bits == "1" }}cstopb; +} +{% endfor %} + +## +## list of clients we allow +## +access * { + trusted localhost; + allowed localhost; +} diff --git a/data/templates/dhcp-client/daemon-options.tmpl b/data/templates/dhcp-client/daemon-options.tmpl new file mode 100644 index 000000000..290aefa49 --- /dev/null +++ b/data/templates/dhcp-client/daemon-options.tmpl @@ -0,0 +1,4 @@ +### Autogenerated by interface.py ### + +DHCLIENT_OPTS="-nw -cf /var/lib/dhcp/dhclient_{{ifname}}.conf -pf /var/lib/dhcp/dhclient_{{ifname}}.pid -lf /var/lib/dhcp/dhclient_{{ifname}}.leases {{ifname}}" + diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl new file mode 100644 index 000000000..8a44a9761 --- /dev/null +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -0,0 +1,19 @@ +### Autogenerated by interface.py ### + +option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; +timeout 60; +retry 300; + +interface "{{ ifname }}" { + send host-name "{{ dhcp_options.host_name }}"; +{% if dhcp_options.client_id is defined and dhcp_options.client_id is not none %} + send dhcp-client-identifier "{{ dhcp_options.client_id }}"; +{% endif %} +{% if dhcp_options.vendor_class_id is defined and dhcp_options.vendor_class_id is not none %} + send vendor-class-identifier "{{ dhcp_options.vendor_class_id }}"; +{% endif %} + request subnet-mask, broadcast-address, routers, domain-name-servers, + rfc3442-classless-static-routes, domain-name, interface-mtu; + require subnet-mask; +} + diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl new file mode 100644 index 000000000..68f668117 --- /dev/null +++ b/data/templates/dhcp-client/ipv6.tmpl @@ -0,0 +1,57 @@ +### Autogenerated by interface.py ### + +# man https://www.unix.com/man-page/debian/5/dhcp6c.conf/ +interface {{ ifname }} { +{% if address is defined and 'dhcpv6' in address %} + request domain-name-servers; + request domain-name; +{% if dhcpv6_options is defined and dhcpv6_options.parameters_only is defined %} + information-only; +{% endif %} +{% if dhcpv6_options is not defined or dhcpv6_options.temporary is not defined %} + send ia-na 0; # non-temporary address +{% endif %} +{% if dhcpv6_options is defined and dhcpv6_options.rapid_commit is defined %} + send rapid-commit; # wait for immediate reply instead of advertisements +{% endif %} +{% endif %} +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} +{% for pd in dhcpv6_options.pd %} + send ia-pd {{ pd }}; # prefix delegation #{{ pd }} +{% endfor %} +{% endif %} +}; + +{% if address is defined and 'dhcpv6' in address %} +{% if dhcpv6_options is not defined or dhcpv6_options.temporary is not defined %} +id-assoc na 0 { + # Identity association for non temporary address +}; +{% endif %} +{% endif %} + +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} +{% for pd in dhcpv6_options.pd %} +id-assoc pd {{ pd }} { +{# length got a default value #} + prefix ::/{{ dhcpv6_options.pd[pd].length }} infinity; +{% set sla_len = 64 - dhcpv6_options.pd[pd].length|int %} +{% set count = namespace(value=0) %} +{% for interface in dhcpv6_options.pd[pd].interface if dhcpv6_options.pd[pd].interface is defined %} + prefix-interface {{ interface }} { + sla-len {{ sla_len }}; +{% if dhcpv6_options.pd[pd].interface[interface].sla_id is defined and dhcpv6_options.pd[pd].interface[interface].sla_id is not none %} + sla-id {{ dhcpv6_options.pd[pd].interface[interface].sla_id }}; +{% else %} + sla-id {{ count.value }}; +{% endif %} +{% if dhcpv6_options.pd[pd].interface[interface].address is defined and dhcpv6_options.pd[pd].interface[interface].address is not none %} + ifid {{ dhcpv6_options.pd[pd].interface[interface].address }}; +{% endif %} + }; +{% set count.value = count.value + 1 %} +{% endfor %} +}; +{% endfor %} +{% endif %} + diff --git a/data/templates/dhcp-relay/config.tmpl b/data/templates/dhcp-relay/config.tmpl new file mode 100644 index 000000000..b223807cf --- /dev/null +++ b/data/templates/dhcp-relay/config.tmpl @@ -0,0 +1,4 @@ +### Autogenerated by dhcp_relay.py ### + +# Defaults for isc-dhcp-relay6.service +OPTIONS="{{ options | join(' ') }} -i {{ interface | join(' -i ') }} {{ server | join(' ') }}" diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl new file mode 100644 index 000000000..5f5129451 --- /dev/null +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -0,0 +1,195 @@ + +### Autogenerated by dhcp_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html +# +# log-facility local7; + +{% if hostfile_update %} +on release { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} + +on expiry { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} +{% endif %} +{%- if host_decl_name %} +use-host-decl-names on; +{%- endif %} +ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %}; +{% if static_route -%} +option rfc3442-static-route code 121 = array of integer 8; +option windows-static-route code 249 = array of integer 8; +{%- endif %} +{% if wpad -%} +option wpad-url code 252 = text; +{% endif %} + +{%- if global_parameters %} +# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated +{%- for param in global_parameters %} +{{ param }} +{%- endfor -%} +{%- endif %} + +# Failover configuration +{% for network in shared_network %} +{%- if not network.disabled -%} +{%- for subnet in network.subnet %} +{%- if subnet.failover_name -%} +failover peer "{{ subnet.failover_name }}" { +{%- if subnet.failover_status == 'primary' %} + primary; + mclt 1800; + split 128; +{%- elif subnet.failover_status == 'secondary' %} + secondary; +{%- endif %} + address {{ subnet.failover_local_addr }}; + port 520; + peer address {{ subnet.failover_peer_addr }}; + peer port 520; + max-response-delay 30; + max-unacked-updates 10; + load balance max seconds 3; +} +{% endif -%} +{% endfor -%} +{% endif -%} +{% endfor %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {%- if network.authoritative %} + authoritative; + {%- endif %} + {%- if network.network_parameters %} + # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated + {%- for param in network.network_parameters %} + {{ param }} + {%- endfor %} + {%- endif %} + {%- for subnet in network.subnet %} + subnet {{ subnet.address }} netmask {{ subnet.netmask }} { + {%- if subnet.dns_server %} + option domain-name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.domain_search %} + option domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.ntp_server %} + option ntp-servers {{ subnet.ntp_server | join(', ') }}; + {%- endif %} + {%- if subnet.pop_server %} + option pop-server {{ subnet.pop_server | join(', ') }}; + {%- endif %} + {%- if subnet.smtp_server %} + option smtp-server {{ subnet.smtp_server | join(', ') }}; + {%- endif %} + {%- if subnet.time_server %} + option time-servers {{ subnet.time_server | join(', ') }}; + {%- endif %} + {%- if subnet.wins_server %} + option netbios-name-servers {{ subnet.wins_server | join(', ') }}; + {%- endif %} + {%- if subnet.static_route %} + option rfc3442-static-route {{ subnet.static_route }}{% if subnet.rfc3442_default_router %}, {{ subnet.rfc3442_default_router }}{% endif %}; + option windows-static-route {{ subnet.static_route }}; + {%- endif %} + {%- if subnet.ip_forwarding %} + option ip-forwarding true; + {%- endif -%} + {%- if subnet.default_router %} + option routers {{ subnet.default_router }}; + {%- endif -%} + {%- if subnet.server_identifier %} + option dhcp-server-identifier {{ subnet.server_identifier }}; + {%- endif -%} + {%- if subnet.domain_name %} + option domain-name "{{ subnet.domain_name }}"; + {%- endif -%} + {%- if subnet.subnet_parameters %} + # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated + {%- for param in subnet.subnet_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + {%- if subnet.tftp_server %} + option tftp-server-name "{{ subnet.tftp_server }}"; + {%- endif -%} + {%- if subnet.bootfile_name %} + option bootfile-name "{{ subnet.bootfile_name }}"; + filename "{{ subnet.bootfile_name }}"; + {%- endif -%} + {%- if subnet.bootfile_server %} + next-server {{ subnet.bootfile_server }}; + {%- endif -%} + {%- if subnet.time_offset %} + option time-offset {{ subnet.time_offset }}; + {%- endif -%} + {%- if subnet.wpad_url %} + option wpad-url "{{ subnet.wpad_url }}"; + {%- endif -%} + {%- if subnet.client_prefix_length %} + option subnet-mask {{ subnet.client_prefix_length }}; + {%- endif -%} + {% if subnet.lease %} + default-lease-time {{ subnet.lease }}; + max-lease-time {{ subnet.lease }}; + {%- endif -%} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { + {%- if host.ip_address %} + fixed-address {{ host.ip_address }}; + {%- endif %} + hardware ethernet {{ host.mac_address }}; + {%- if host.static_parameters %} + # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated + {%- for param in host.static_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + } + {%- endif %} + {%- endfor %} + {%- if subnet.failover_name %} + pool { + failover peer "{{ subnet.failover_name }}"; + deny dynamic bootp clients; + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + } + {%- else %} + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- endif %} + } + {%- endfor %} + on commit { + set shared-networkname = "{{ network.name }}"; + {% if hostfile_update -%} + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".", leased-address); + set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); + {%- endif %} + } +} +{%- endif %} +{% endfor %} diff --git a/data/templates/dhcpv6-relay/config.tmpl b/data/templates/dhcpv6-relay/config.tmpl new file mode 100644 index 000000000..55035ae6c --- /dev/null +++ b/data/templates/dhcpv6-relay/config.tmpl @@ -0,0 +1,4 @@ +### Autogenerated by dhcpv6_relay.py ### + +# Defaults for isc-dhcp-relay6.service +OPTIONS="-l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}" diff --git a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl new file mode 100644 index 000000000..ff7822b0d --- /dev/null +++ b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl @@ -0,0 +1,81 @@ +### Autogenerated by dhcpv6_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html + +log-facility local7; +{%- if preference %} +option dhcp6.preference {{ preference }}; +{%- endif %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {%- for subnet in network.subnet %} + subnet6 {{ subnet.network }} { + {%- for range in subnet.range6_prefix %} + range6 {{ range.prefix }}{{ " temporary" if range.temporary }}; + {%- endfor %} + {%- for range in subnet.range6 %} + range6 {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- if subnet.domain_search %} + option dhcp6.domain-search "{{ subnet.domain_search | join('", "') }}"; + {%- endif %} + {%- if subnet.lease_def %} + default-lease-time {{ subnet.lease_def }}; + {%- endif %} + {%- if subnet.lease_max %} + max-lease-time {{ subnet.lease_max }}; + {%- endif %} + {%- if subnet.lease_min %} + min-lease-time {{ subnet.lease_min }}; + {%- endif %} + {%- if subnet.dns_server %} + option dhcp6.name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.nis_domain %} + option dhcp6.nis-domain-name "{{ subnet.nis_domain }}"; + {%- endif %} + {%- if subnet.nis_server %} + option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }}; + {%- endif %} + {%- if subnet.nisp_domain %} + option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}"; + {%- endif %} + {%- if subnet.nisp_server %} + option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }}; + {%- endif %} + {%- if subnet.sip_address %} + option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; + {%- endif %} + {%- if subnet.sip_hostname %} + option dhcp6.sip-servers-names "{{ subnet.sip_hostname | join('", "') }}"; + {%- endif %} + {%- if subnet.sntp_server %} + option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; + {%- endif %} + {%- for prefix in subnet.prefix_delegation %} + prefix6 {{ prefix.start }} {{ prefix.stop }} /{{ prefix.length }}; + {%- endfor %} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {{ network.name }}_{{ host.name }} { + {%- if host.client_identifier %} + host-identifier option dhcp6.client-id {{ host.client_identifier }}; + {%- endif %} + {%- if host.ipv6_address %} + fixed-address6 {{ host.ipv6_address }}; + {%- endif %} + } + {%- endif %} + {%- endfor %} + } + {%- endfor %} + on commit { + set shared-networkname = "{{ network.name }}"; + } +} +{%- endif %} +{% endfor %} diff --git a/data/templates/dns-forwarding/recursor.conf.lua.tmpl b/data/templates/dns-forwarding/recursor.conf.lua.tmpl new file mode 100644 index 000000000..e2506238d --- /dev/null +++ b/data/templates/dns-forwarding/recursor.conf.lua.tmpl @@ -0,0 +1,9 @@ +-- Autogenerated by VyOS (dns_forwarding.py) -- +-- Do not edit, your changes will get overwritten -- + +-- Load DNSSEC root keys from dns-root-data package. +dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua") + +-- Load lua from vyos-hostsd -- +dofile("recursor.vyos-hostsd.conf.lua") + diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl new file mode 100644 index 000000000..d233b8abc --- /dev/null +++ b/data/templates/dns-forwarding/recursor.conf.tmpl @@ -0,0 +1,33 @@ +### Autogenerated by dns_forwarding.py ### + +# XXX: pdns recursor doesn't like whitespace near entry separators, +# especially in the semicolon-separated lists of name servers. +# Please be careful if you edit the template. + +# Non-configurable defaults +daemon=yes +threads=1 +allow-from={{ allow_from | join(',') }} +log-common-errors=yes +non-local-bind=yes +query-local-address=0.0.0.0 +query-local-address6=:: +lua-config-file=recursor.conf.lua + +# cache-size +max-cache-entries={{ cache_size }} + +# negative TTL for NXDOMAIN +max-negative-ttl={{ negative_ttl }} + +# ignore-hosts-file +export-etc-hosts={{ export_hosts_file }} + +# listen-address +local-address={{ listen_address | join(',') }} + +# dnssec +dnssec={{ dnssec }} + +forward-zones-file=recursor.forward-zones.conf + diff --git a/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl new file mode 100644 index 000000000..de5eaee00 --- /dev/null +++ b/data/templates/dns-forwarding/recursor.forward-zones.conf.tmpl @@ -0,0 +1,28 @@ +# Autogenerated by VyOS (vyos-hostsd) +# Do not edit, your changes will get overwritten + +# dot zone (catch-all): '+' indicates recursion is desired +# (same as forward-zones-recurse) +{#- the code below ensures the order of nameservers is determined first by #} +{#- the order of tags, then by the order of nameservers within that tag #} +{%- set n = namespace(dot_zone_ns='') %} +{%- for tag in name_server_tags_recursor %} +{%- set ns = '' %} +{%- if tag in name_servers %} +{%- set ns = ns + name_servers[tag]|join(', ') %} +{%- set n.dot_zone_ns = (n.dot_zone_ns, ns)|join(', ') if n.dot_zone_ns != '' else ns %} +{%- endif %} +# {{ tag }}: {{ ns }} +{%- endfor %} + +{%- if n.dot_zone_ns %} ++.={{ n.dot_zone_ns }} +{%- endif %} + +{% if forward_zones -%} +# zones added via 'service dns forwarding domain' +{%- for zone, zonedata in forward_zones.items() %} +{% if zonedata['recursion-desired'] %}+{% endif %}{{ zone }}={{ zonedata['nslist']|join(', ') }} +{%- endfor %} +{%- endif %} + diff --git a/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl new file mode 100644 index 000000000..b0d99d9ae --- /dev/null +++ b/data/templates/dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl @@ -0,0 +1,24 @@ +-- Autogenerated by VyOS (vyos-hostsd) -- +-- Do not edit, your changes will get overwritten -- + +{% if hosts -%} +-- from 'system static-host-mapping' and DHCP server +{%- for tag, taghosts in hosts.items() %} +{%- for host, hostprops in taghosts.items() %} +addNTA("{{ host }}.", "{{ tag }}") +{%- for a in hostprops['aliases'] %} +addNTA("{{ a }}.", "{{ tag }} alias") +{%- endfor %} +{%- endfor %} +{%- endfor %} +{%- endif %} + +{% if forward_zones -%} +-- from 'service dns forwarding domain' +{%- for zone, zonedata in forward_zones.items() %} +{%- if zonedata['addNTA'] %} +addNTA("{{ zone }}", "static") +{%- endif %} +{%- endfor %} +{%- endif %} + diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl new file mode 100644 index 000000000..9c7219230 --- /dev/null +++ b/data/templates/dynamic-dns/ddclient.conf.tmpl @@ -0,0 +1,46 @@ +### Autogenerated by dynamic_dns.py ### +daemon=1m +syslog=yes +ssl=yes + +{% for interface in interfaces -%} + +# +# ddclient configuration for interface "{{ interface.interface }}": +# +{% if interface.web_url -%} +use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %} +{% else -%} +use=if, if={{ interface.interface }} +{% endif -%} + +{% for rfc in interface.rfc2136 -%} +{% for record in rfc.record %} +# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }} +server={{ rfc.server }} +protocol=nsupdate +password={{ rfc.keyfile }} +ttl={{ rfc.ttl }} +zone={{ rfc.zone }} +{{ record }} +{% endfor -%} +{% endfor -%} + +{% for srv in interface.service %} +{% for host in srv.host %} +# DynDNS provider configuration for {{ host }} +protocol={{ srv.protocol }}, +max-interval=28d, +login={{ srv.login }}, +password='{{ srv.password }}', +{% if srv.server -%} +server={{ srv.server }}, +{% endif -%} +{% if srv.zone -%} +zone={{ srv.zone }}, +{% endif -%} +{{ host }} +{% endfor %} +{% endfor %} + +{% endfor %} diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl new file mode 100644 index 000000000..0c29f536b --- /dev/null +++ b/data/templates/firewall/nftables-nat.tmpl @@ -0,0 +1,157 @@ +#!/usr/sbin/nft -f + +# Start with clean NAT table +flush table nat + +{% if helper_functions == 'remove' %} +{# NAT if going to be disabled - remove rules and targets from nftables #} + +{% set base_command = "delete rule ip raw" %} +{{ base_command }} PREROUTING handle {{ pre_ct_ignore }} +{{ base_command }} OUTPUT handle {{ out_ct_ignore }} +{{ base_command }} PREROUTING handle {{ pre_ct_conntrack }} +{{ base_command }} OUTPUT handle {{ out_ct_conntrack }} + +delete chain ip raw NAT_CONNTRACK + +{% elif helper_functions == 'add' %} +{# NAT if enabled - add targets to nftables #} +add chain ip raw NAT_CONNTRACK +add rule ip raw NAT_CONNTRACK counter accept + +{% set base_command = "add rule ip raw" %} + +{{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER +{{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER +{{ base_command }} PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK +{{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK +{% endif %} + +{% macro nat_rule(rule, chain) %} +{% set src_addr = "ip saddr " + rule.source_address if rule.source_address %} +{% set dst_addr = "ip daddr " + rule.dest_address if rule.dest_address %} + +{# negated port groups need special treatment, move != in front of { } group #} +{% if rule.source_port.startswith('!=') %} +{% set src_port = "sport != { " + rule.source_port.replace('!=','') +" }" if rule.source_port %} +{% else %} +{% set src_port = "sport { " + rule.source_port +" }" if rule.source_port %} +{% endif %} + +{# negated port groups need special treatment, move != in front of { } group #} +{% if rule.dest_port.startswith('!=') %} +{% set dst_port = "dport != { " + rule.dest_port.replace('!=','') +" }" if rule.dest_port %} +{% else %} +{% set dst_port = "dport { " + rule.dest_port +" }" if rule.dest_port %} +{% endif %} + +{% set comment = "DST-NAT-" + rule.number %} + +{% if chain == "PREROUTING" %} +{% set interface = " iifname \"" + rule.interface_in + "\"" if rule.interface_in is defined and rule.interface_in != 'any' else '' %} +{% set trns_addr = "dnat to " + rule.translation_address %} + +{% elif chain == "POSTROUTING" %} +{% set interface = " oifname \"" + rule.interface_out + "\"" if rule.interface_out is defined and rule.interface_out != 'any' else '' %} +{% if rule.translation_address == 'masquerade' %} +{% set trns_addr = rule.translation_address %} +{% if rule.translation_port %} +{% set trns_addr = trns_addr + " to " %} +{% endif %} +{% else %} +{% set trns_addr = "snat to " + rule.translation_address %} +{% endif %} +{% endif %} +{% set trns_port = ":" + rule.translation_port if rule.translation_port %} + +{% if rule.protocol == "tcp_udp" %} +{% set protocol = "tcp" %} +{% set comment = comment + " tcp_udp" %} +{% else %} +{% set protocol = rule.protocol %} +{% endif %} + +{% if rule.log %} +{% set base_log = "[NAT-DST-" + rule.number %} +{% if rule.exclude %} +{% set log = base_log + "-EXCL]" %} +{% elif rule.translation_address == 'masquerade' %} +{% set log = base_log + "-MASQ]" %} +{% else %} +{% set log = base_log + "]" %} +{% endif %} +{% endif %} + +{% if rule.exclude %} +{# rule has been marked as "exclude" thus we simply return here #} +{% set trns_addr = "return" %} +{% set trns_port = "" %} +{% endif %} + +{% set output = "add rule ip nat " + chain + interface %} + +{% if protocol != "all" %} +{% set output = output + " ip protocol " + protocol %} +{% endif %} + +{% if src_addr %} +{% set output = output + " " + src_addr %} +{% endif %} +{% if src_port %} +{% set output = output + " " + protocol + " " + src_port %} +{% endif %} + +{% if dst_addr %} +{% set output = output + " " + dst_addr %} +{% endif %} +{% if dst_port %} +{% set output = output + " " + protocol + " " + dst_port %} +{% endif %} + +{# Count packets #} +{% set output = output + " counter" %} + +{# Special handling of log option, we must repeat the entire rule before the #} +{# NAT translation options are added, this is essential #} +{% if log %} +{% set log_output = output + " log prefix \"" + log + "\" comment \"" + comment + "\"" %} +{% endif %} + +{% if trns_addr %} +{% set output = output + " " + trns_addr %} +{% endif %} + +{% if trns_port %} +{# Do not add a whitespace here, translation port must be directly added after IP address #} +{# e.g. 192.0.2.10:3389 #} +{% set output = output + trns_port %} +{% endif %} + +{% if comment %} +{% set output = output + " comment \"" + comment + "\"" %} +{% endif %} + +{{ log_output if log_output }} +{{ output }} + +{# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #} +{% if rule.protocol == "tcp_udp" %} +{# Beware of trailing whitespace, without it the comment tcp_udp will be changed to udp_udp #} +{{ log_output | replace("tcp ", "udp ") if log_output }} +{{ output | replace("tcp ", "udp ") }} +{% endif %} +{% endmacro %} + +# +# Destination NAT rules build up here +# +{% for rule in destination if not rule.disabled -%} +{{ nat_rule(rule, 'PREROUTING') }} +{% endfor %} + +# +# Source NAT rules build up here +# +{% for rule in source if not rule.disabled -%} +{{ nat_rule(rule, 'POSTROUTING') }} +{% endfor %} diff --git a/data/templates/frr/bfd.frr.tmpl b/data/templates/frr/bfd.frr.tmpl new file mode 100644 index 000000000..7df4bfd01 --- /dev/null +++ b/data/templates/frr/bfd.frr.tmpl @@ -0,0 +1,16 @@ +! +bfd +{% for peer in old_peers -%} + no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} +{% endfor -%} +! +{% for peer in new_peers -%} + peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} + detect-multiplier {{ peer.multiplier }} + receive-interval {{ peer.rx_interval }} + transmit-interval {{ peer.tx_interval }} + {% if peer.echo_mode %}echo-mode{% endif %} + {% if peer.echo_interval != '' %}echo-interval {{ peer.echo_interval }}{% endif %} + {% if not peer.shutdown %}no {% endif %}shutdown +{% endfor -%} +! diff --git a/data/templates/frr/bgp.frr.tmpl b/data/templates/frr/bgp.frr.tmpl new file mode 100644 index 000000000..cdf4cb4fe --- /dev/null +++ b/data/templates/frr/bgp.frr.tmpl @@ -0,0 +1 @@ +! diff --git a/data/templates/frr/igmp.frr.tmpl b/data/templates/frr/igmp.frr.tmpl new file mode 100644 index 000000000..de4696c1f --- /dev/null +++ b/data/templates/frr/igmp.frr.tmpl @@ -0,0 +1,41 @@ +! +{% for iface in old_ifaces -%} +interface {{ iface }} +{% for group in old_ifaces[iface].gr_join -%} +{% if old_ifaces[iface].gr_join[group] -%} +{% for source in old_ifaces[iface].gr_join[group] -%} +no ip igmp join {{ group }} {{ source }} +{% endfor -%} +{% else -%} +no ip igmp join {{ group }} +{% endif -%} +{% endfor -%} +no ip igmp +! +{% endfor -%} +{% for iface in ifaces -%} +interface {{ iface }} +{% if ifaces[iface].version -%} +ip igmp version {{ ifaces[iface].version }} +{% else -%} +{# IGMP default version 3 #} +ip igmp +{% endif -%} +{% if ifaces[iface].query_interval -%} +ip igmp query-interval {{ ifaces[iface].query_interval }} +{% endif -%} +{% if ifaces[iface].query_max_resp_time -%} +ip igmp query-max-response-time {{ ifaces[iface].query_max_resp_time }} +{% endif -%} +{% for group in ifaces[iface].gr_join -%} +{% if ifaces[iface].gr_join[group] -%} +{% for source in ifaces[iface].gr_join[group] -%} +ip igmp join {{ group }} {{ source }} +{% endfor -%} +{% else -%} +ip igmp join {{ group }} +{% endif -%} +{% endfor -%} +! +{% endfor -%} +! diff --git a/data/templates/frr/ldpd.frr.tmpl b/data/templates/frr/ldpd.frr.tmpl new file mode 100644 index 000000000..dbaa917e8 --- /dev/null +++ b/data/templates/frr/ldpd.frr.tmpl @@ -0,0 +1,70 @@ +! +{% if mpls_ldp -%} +mpls ldp +{% if old_router_id -%} +no router-id {{ old_router_id }} +{% endif -%} +{% if router_id -%} +router-id {{ router_id }} +{% endif -%} +{% for neighbor_id in old_ldp.neighbors -%} +no neighbor {{neighbor_id}} password {{old_ldp.neighbors[neighbor_id].password}} +{% endfor -%} +{% for neighbor_id in ldp.neighbors -%} +neighbor {{neighbor_id}} password {{ldp.neighbors[neighbor_id].password}} +{% endfor -%} +address-family ipv4 +label local allocate host-routes +{% if old_ldp.d_transp_ipv4 -%} +no discovery transport-address {{ old_ldp.d_transp_ipv4 }} +{% endif -%} +{% if ldp.d_transp_ipv4 -%} +discovery transport-address {{ ldp.d_transp_ipv4 }} +{% endif -%} +{% if old_ldp.hello_holdtime -%} +no discovery hello holdtime {{ old_ldp.hello_holdtime }} +{% endif -%} +{% if ldp.hello_holdtime -%} +discovery hello holdtime {{ ldp.hello_holdtime }} +{% endif -%} +{% if old_ldp.hello_interval -%} +no discovery hello interval {{ old_ldp.hello_interval }} +{% endif -%} +{% if ldp.hello_interval -%} +discovery hello interval {{ ldp.hello_interval }} +{% endif -%} +{% for interface in old_ldp.interfaces -%} +no interface {{interface}} +{% endfor -%} +{% for interface in ldp.interfaces -%} +interface {{interface}} +{% endfor -%} +! +! +exit-address-family +! +{% if ldp.d_transp_ipv6 -%} +address-family ipv6 +label local allocate host-routes +{% if old_ldp.d_transp_ipv6 -%} +no discovery transport-address {{ old_ldp.d_transp_ipv6 }} +{% endif -%} +{% if ldp.d_transp_ipv6 -%} +discovery transport-address {{ ldp.d_transp_ipv6 }} +{% endif -%} +{% for interface in old_ldp.interfaces -%} +no interface {{interface}} +{% endfor -%} +{% for interface in ldp.interfaces -%} +interface {{interface}} +{% endfor -%} +! +exit-address-family +{% else -%} +no address-family ipv6 +{% endif -%} +! +{% else -%} +no mpls ldp +{% endif -%} +! diff --git a/data/templates/frr/pimd.frr.tmpl b/data/templates/frr/pimd.frr.tmpl new file mode 100644 index 000000000..1d1532c60 --- /dev/null +++ b/data/templates/frr/pimd.frr.tmpl @@ -0,0 +1,34 @@ +! +{% for rp_addr in old_pim.rp -%} +{% for group in old_pim.rp[rp_addr] -%} +no ip pim rp {{ rp_addr }} {{ group }} +{% endfor -%} +{% endfor -%} +{% if old_pim.rp_keep_alive -%} +no ip pim rp keep-alive-timer {{ old_pim.rp_keep_alive }} +{% endif -%} +{% for iface in old_pim.ifaces -%} +interface {{ iface }} +no ip pim +! +{% endfor -%} +{% for iface in pim.ifaces -%} +interface {{ iface }} +ip pim +{% if pim.ifaces[iface].dr_prio -%} +ip pim drpriority {{ pim.ifaces[iface].dr_prio }} +{% endif -%} +{% if pim.ifaces[iface].hello -%} +ip pim hello {{ pim.ifaces[iface].hello }} +{% endif -%} +! +{% endfor -%} +{% for rp_addr in pim.rp -%} +{% for group in pim.rp[rp_addr] -%} +ip pim rp {{ rp_addr }} {{ group }} +{% endfor -%} +{% endfor -%} +{% if pim.rp_keep_alive -%} +ip pim rp keep-alive-timer {{ pim.rp_keep_alive }} +{% endif -%} +! diff --git a/data/templates/frr/rip.frr.tmpl b/data/templates/frr/rip.frr.tmpl new file mode 100644 index 000000000..60bc686bd --- /dev/null +++ b/data/templates/frr/rip.frr.tmpl @@ -0,0 +1,143 @@ +! +{% if rip_conf -%} +router rip +{% if old_default_distance -%} +no distance {{old_default_distance}} +{% endif -%} +{% if default_distance -%} +distance {{default_distance}} +{% endif -%} +{% if old_default_originate -%} +no default-information originate +{% endif -%} +{% if default_originate -%} +default-information originate +{% endif -%} +{% if old_rip.default_metric -%} +no default-metric {{old_rip.default_metric}} +{% endif -%} +{% if rip.default_metric -%} +default-metric {{rip.default_metric}} +{% endif -%} +{% for protocol in old_rip.redist -%} +{% if old_rip.redist[protocol]['metric'] and old_rip.redist[protocol]['route_map'] -%} +no redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} route-map {{rip.redist[protocol]['route_map']}} +{% elif old_rip.redist[protocol]['metric'] -%} +no redistribute {{protocol}} metric {{old_rip.redist[protocol]['metric']}} +{% elif old_rip.redist[protocol]['route_map'] -%} +no redistribute {{protocol}} route-map {{old_rip.redist[protocol]['route_map']}} +{% else -%} +no redistribute {{protocol}} +{% endif -%} +{% endfor -%} +{% for protocol in rip.redist -%} +{% if rip.redist[protocol]['metric'] and rip.redist[protocol]['route_map'] -%} +redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} route-map {{rip.redist[protocol]['route_map']}} +{% elif rip.redist[protocol]['metric'] -%} +redistribute {{protocol}} metric {{rip.redist[protocol]['metric']}} +{% elif rip.redist[protocol]['route_map'] -%} +redistribute {{protocol}} route-map {{rip.redist[protocol]['route_map']}} +{% else -%} +redistribute {{protocol}} +{% endif -%} +{% endfor -%} +{% for iface in old_rip.distribute -%} +{% if old_rip.distribute[iface].iface_access_list_in -%} +no distribute-list {{old_rip.distribute[iface].iface_access_list_in}} in {{iface}} +{% endif -%} +{% if old_rip.distribute[iface].iface_access_list_out -%} +no distribute-list {{old_rip.distribute[iface].iface_access_list_out}} out {{iface}} +{% endif -%} +{% if old_rip.distribute[iface].iface_prefix_list_in -%} +no distribute-list prefix {{old_rip.distribute[iface].iface_prefix_list_in}} in {{iface}} +{% endif -%} +{% if old_rip.distribute[iface].iface_prefix_list_out -%} +no distribute-list prefix {{old_rip.distribute[iface].iface_prefix_list_out}} out {{iface}} +{% endif -%} +{% endfor -%} +{% for iface in rip.distribute -%} +{% if rip.distribute[iface].iface_access_list_in -%} +distribute-list {{rip.distribute[iface].iface_access_list_in}} in {{iface}} +{% endif -%} +{% if rip.distribute[iface].iface_access_list_out -%} +distribute-list {{rip.distribute[iface].iface_access_list_out}} out {{iface}} +{% endif -%} +{% if rip.distribute[iface].iface_prefix_list_in -%} +distribute-list prefix {{rip.distribute[iface].iface_prefix_list_in}} in {{iface}} +{% endif -%} +{% if rip.distribute[iface].iface_prefix_list_out -%} +distribute-list prefix {{rip.distribute[iface].iface_prefix_list_out}} out {{iface}} +{% endif -%} +{% endfor -%} +{% if old_rip.dist_acl_in -%} +no distribute-list {{old_rip.dist_acl_in}} in +{% endif -%} +{% if rip.dist_acl_in -%} +distribute-list {{rip.dist_acl_in}} in +{% endif -%} +{% if old_rip.dist_acl_out -%} +no distribute-list {{old_rip.dist_acl_out}} out +{% endif -%} +{% if rip.dist_acl_out -%} +distribute-list {{rip.dist_acl_out}} out +{% endif -%} +{% if old_rip.dist_prfx_in -%} +no distribute-list prefix {{old_rip.dist_prfx_in}} in +{% endif -%} +{% if rip.dist_prfx_in -%} +distribute-list prefix {{rip.dist_prfx_in}} in +{% endif -%} +{% if old_rip.dist_prfx_out -%} +no distribute-list prefix {{old_rip.dist_prfx_out}} out +{% endif -%} +{% if rip.dist_prfx_out -%} +distribute-list prefix {{rip.dist_prfx_out}} out +{% endif -%} +{% for network in old_rip.networks -%} +no network {{network}} +{% endfor -%} +{% for network in rip.networks -%} +network {{network}} +{% endfor -%} +{% for iface in old_rip.ifaces -%} +no network {{iface}} +{% endfor -%} +{% for iface in rip.ifaces -%} +network {{iface}} +{% endfor -%} +{% for neighbor in old_rip.neighbors -%} +no neighbor {{neighbor}} +{% endfor -%} +{% for neighbor in rip.neighbors -%} +neighbor {{neighbor}} +{% endfor -%} +{% for net in rip.net_distance -%} +{% if rip.net_distance[net].access_list and rip.net_distance[net].distance -%} +distance {{rip.net_distance[net].distance}} {{net}} {{rip.net_distance[net].access_list}} +{% else -%} +distance {{rip.net_distance[net].distance}} {{net}} +{% endif -%} +{% endfor -%} +{% for passive_iface in old_rip.passive_iface -%} +no passive-interface {{passive_iface}} +{% endfor -%} +{% for passive_iface in rip.passive_iface -%} +passive-interface {{passive_iface}} +{% endfor -%} +{% for route in old_rip.route -%} +no route {{route}} +{% endfor -%} +{% for route in rip.route -%} +route {{route}} +{% endfor -%} +{% if old_rip.timer_update or old_rip.timer_timeout or old_rip.timer_garbage -%} +no timers basic +{% endif -%} +{% if rip.timer_update or rip.timer_timeout or rip.timer_garbage -%} +timers basic {{rip.timer_update}} {{rip.timer_timeout}} {{rip.timer_garbage}} +{% endif -%} +! +{% else -%} +no router rip +! +{% endif -%} diff --git a/data/templates/frr/static_mcast.frr.tmpl b/data/templates/frr/static_mcast.frr.tmpl new file mode 100644 index 000000000..86d619ab0 --- /dev/null +++ b/data/templates/frr/static_mcast.frr.tmpl @@ -0,0 +1,20 @@ +! +{% for route_gr in old_mroute -%} +{% for nh in old_mroute[route_gr] -%} +{% if old_mroute[route_gr][nh] -%} +no ip mroute {{ route_gr }} {{ nh }} {{ old_mroute[route_gr][nh] }} +{% else -%} +no ip mroute {{ route_gr }} {{ nh }} +{% endif -%} +{% endfor -%} +{% endfor -%} +{% for route_gr in mroute -%} +{% for nh in mroute[route_gr] -%} +{% if mroute[route_gr][nh] -%} +ip mroute {{ route_gr }} {{ nh }} {{ mroute[route_gr][nh] }} +{% else -%} +ip mroute {{ route_gr }} {{ nh }} +{% endif -%} +{% endfor -%} +{% endfor -%} +! diff --git a/data/templates/getty/serial-getty.service.tmpl b/data/templates/getty/serial-getty.service.tmpl new file mode 100644 index 000000000..0183eae7d --- /dev/null +++ b/data/templates/getty/serial-getty.service.tmpl @@ -0,0 +1,37 @@ +[Unit] +Description=Serial Getty on %I +Documentation=man:agetty(8) man:systemd-getty-generator(8) +Documentation=http://0pointer.de/blog/projects/serial-console.html +BindsTo=dev-%i.device +After=dev-%i.device systemd-user-sessions.service plymouth-quit-wait.service getty-pre.target +After=vyos-router.service + +# If additional gettys are spawned during boot then we should make +# sure that this is synchronized before getty.target, even though +# getty.target didn't actually pull it in. +Before=getty.target +IgnoreOnIsolate=yes + +# IgnoreOnIsolate causes issues with sulogin, if someone isolates +# rescue.target or starts rescue.service from multi-user.target or +# graphical.target. +Conflicts=rescue.service +Before=rescue.service + +[Service] +# The '-o' option value tells agetty to replace 'login' arguments with an +# option to preserve environment (-p), followed by '--' for safety, and then +# the entered username. +ExecStart=-/sbin/agetty -o '-p -- \\u' --keep-baud {{ speed }} %I $TERM +Type=idle +Restart=always +UtmpIdentifier=%I +TTYPath=/dev/%I +TTYReset=yes +TTYVHangup=yes +KillMode=process +IgnoreSIGPIPE=no +SendSIGHUP=yes + +[Install] +WantedBy=getty.target diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl new file mode 100644 index 000000000..a20be45ae --- /dev/null +++ b/data/templates/https/nginx.default.tmpl @@ -0,0 +1,69 @@ +### Autogenerated by https.py ### +# Default server configuration +# +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$server_name$request_uri; +} + +{% for server in server_block_list %} +server { + + # SSL configuration + # +{% if server.address == '*' %} + listen {{ server.port }} ssl; + listen [::]:{{ server.port }} ssl; +{% else %} + listen {{ server.address }}:{{ server.port }} ssl; +{% endif %} + +{% for name in server.name %} + server_name {{ name }}; +{% endfor %} + +{% if server.certbot %} + ssl_certificate {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/fullchain.pem; + ssl_certificate_key {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/privkey.pem; + include {{ server.certbot_dir }}/options-ssl-nginx.conf; + ssl_dhparam {{ server.certbot_dir }}/ssl-dhparams.pem; +{% elif server.vyos_cert %} + include {{ server.vyos_cert.conf }}; +{% else %} + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + include snippets/snakeoil.conf; +{% endif %} + + # proxy settings for HTTP API, if enabled; 503, if not + location ~ /(retrieve|configure|config-file|image|generate|show) { +{% if server.api %} + proxy_pass http://localhost:{{ server.api.port }}; + proxy_read_timeout 600; + proxy_buffering off; +{% else %} + return 503; +{% endif %} + } + + error_page 501 502 503 =200 @50*_json; + +{% if api_set %} + location @50*_json { + default_type application/json; + return 200 '{"error": "service https api unavailable at this proxy address: set service https api-restrict virtual-host"}'; + } +{% else %} + location @50*_json { + default_type application/json; + return 200 '{"error": "Start service in configuration mode: set service https api"}'; + } +{% endif %} + +} + +{% endfor %} diff --git a/data/templates/ids/fastnetmon.tmpl b/data/templates/ids/fastnetmon.tmpl new file mode 100644 index 000000000..71a1b2bd7 --- /dev/null +++ b/data/templates/ids/fastnetmon.tmpl @@ -0,0 +1,60 @@ +# enable this option if you want to send logs to local syslog facility +logging:local_syslog_logging = on + +# list of all your networks in CIDR format +networks_list_path = /etc/networks_list + +# list networks in CIDR format which will be not monitored for attacks +white_list_path = /etc/networks_whitelist + +# Enable/Disable any actions in case of attack +enable_ban = on + +## How many packets will be collected from attack traffic +ban_details_records_count = 500 + +## How long (in seconds) we should keep an IP in blocked state +## If you set 0 here it completely disables unban capability +ban_time = 1900 + +# Check if the attack is still active, before triggering an unban callback with this option +# If the attack is still active, check each run of the unban watchdog +unban_only_if_attack_finished = on + +# enable per subnet speed meters +# For each subnet, list track speed in bps and pps for both directions +enable_subnet_counters = off + +{% if "mirror" in mode %} +mirror_afpacket = on +{% endif -%} + +{% if "in" in direction %} +process_incoming_traffic = on +{% endif -%} +{% if "out" in direction %} +process_outgoing_traffic = on +{% endif -%} +{% for th in threshold %} +{% if th == "fps" %} +ban_for_flows = on +threshold_flows = {{ threshold[th] }} +{% endif -%} +{% if th == "mbps" %} +ban_for_bandwidth = on +threshold_mbps = {{ threshold[th] }} +{% endif -%} +{% if th == "pps" %} +ban_for_pps = on +threshold_pps = {{ threshold[th] }} +{% endif -%} +{% endfor -%} + +{% if listen_interface %} +{% set value = listen_interface if listen_interface is string else listen_interface | join(',') %} +interfaces = {{ value }} +{% endif -%} + +{% if alert_script %} +notify_script_path = {{ alert_script }} +{% endif -%} diff --git a/data/templates/ids/fastnetmon_networks_list.tmpl b/data/templates/ids/fastnetmon_networks_list.tmpl new file mode 100644 index 000000000..d58990053 --- /dev/null +++ b/data/templates/ids/fastnetmon_networks_list.tmpl @@ -0,0 +1,7 @@ +{% if network is string %} +{{ network }} +{% else %} +{% for net in network %} +{{ net }} +{% endfor %} +{% endif %} diff --git a/data/templates/igmp-proxy/igmpproxy.conf.tmpl b/data/templates/igmp-proxy/igmpproxy.conf.tmpl new file mode 100644 index 000000000..c7fc5cef5 --- /dev/null +++ b/data/templates/igmp-proxy/igmpproxy.conf.tmpl @@ -0,0 +1,37 @@ +######################################################## +# +# autogenerated by igmp_proxy.py +# +# The configuration file must define one upstream +# interface, and one or more downstream interfaces. +# +# If multicast traffic originates outside the +# upstream subnet, the "altnet" option can be +# used in order to define legal multicast sources. +# (Se example...) +# +# The "quickleave" should be used to avoid saturation +# of the upstream link. The option should only +# be used if it's absolutely nessecary to +# accurately imitate just one Client. +# +######################################################## + +{% if not disable_quickleave -%} +quickleave +{% endif -%} + +{% for interface in interfaces %} +# Configuration for {{ interface.name }} ({{ interface.role }} interface) +{% if interface.role == 'disabled' -%} +phyint {{ interface.name }} disabled +{%- else -%} +phyint {{ interface.name }} {{ interface.role }} ratelimit 0 threshold {{ interface.threshold }} +{%- endif -%} +{%- for subnet in interface.alt_subnet %} + altnet {{ subnet }} +{%- endfor %} +{%- for subnet in interface.whitelist %} + whitelist {{ subnet }} +{%- endfor %} +{% endfor %} diff --git a/data/templates/ipsec/charon.tmpl b/data/templates/ipsec/charon.tmpl new file mode 100644 index 000000000..4d710921e --- /dev/null +++ b/data/templates/ipsec/charon.tmpl @@ -0,0 +1,342 @@ +# Options for the charon IKE daemon. +charon { + + # Accept unencrypted ID and HASH payloads in IKEv1 Main Mode. + # accept_unencrypted_mainmode_messages = no + + # Maximum number of half-open IKE_SAs for a single peer IP. + # block_threshold = 5 + + # Whether Certicate Revocation Lists (CRLs) fetched via HTTP or LDAP should + # be saved under a unique file name derived from the public key of the + # Certification Authority (CA) to /etc/ipsec.d/crls (stroke) or + # /etc/swanctl/x509crl (vici), respectively. + # cache_crls = no + + # Whether relations in validated certificate chains should be cached in + # memory. + # cert_cache = yes + + # Send Cisco Unity vendor ID payload (IKEv1 only). + # cisco_unity = no + + # Close the IKE_SA if setup of the CHILD_SA along with IKE_AUTH failed. + # close_ike_on_child_failure = no + + # Number of half-open IKE_SAs that activate the cookie mechanism. + # cookie_threshold = 10 + + # Delete CHILD_SAs right after they got successfully rekeyed (IKEv1 only). + # delete_rekeyed = no + + # Use ANSI X9.42 DH exponent size or optimum size matched to cryptographic + # strength. + # dh_exponent_ansi_x9_42 = yes + + # Use RTLD_NOW with dlopen when loading plugins and IMV/IMCs to reveal + # missing symbols immediately. + # dlopen_use_rtld_now = no + + # DNS server assigned to peer via configuration payload (CP). + # dns1 = + + # DNS server assigned to peer via configuration payload (CP). + # dns2 = + + # Enable Denial of Service protection using cookies and aggressiveness + # checks. + # dos_protection = yes + + # Compliance with the errata for RFC 4753. + # ecp_x_coordinate_only = yes + + # Free objects during authentication (might conflict with plugins). + # flush_auth_cfg = no + + # Whether to follow IKEv2 redirects (RFC 5685). + # follow_redirects = yes + + # Maximum size (complete IP datagram size in bytes) of a sent IKE fragment + # when using proprietary IKEv1 or standardized IKEv2 fragmentation, defaults + # to 1280 (use 0 for address family specific default values, which uses a + # lower value for IPv4). If specified this limit is used for both IPv4 and + # IPv6. + # fragment_size = 1280 + + # Name of the group the daemon changes to after startup. + # group = + + # Timeout in seconds for connecting IKE_SAs (also see IKE_SA_INIT DROPPING). + # half_open_timeout = 30 + + # Enable hash and URL support. + # hash_and_url = no + + # Allow IKEv1 Aggressive Mode with pre-shared keys as responder. + # i_dont_care_about_security_and_use_aggressive_mode_psk = no + + # Whether to ignore the traffic selectors from the kernel's acquire events + # for IKEv2 connections (they are not used for IKEv1). + # ignore_acquire_ts = no + + # A space-separated list of routing tables to be excluded from route + # lookups. + # ignore_routing_tables = + + # Maximum number of IKE_SAs that can be established at the same time before + # new connection attempts are blocked. + # ikesa_limit = 0 + + # Number of exclusively locked segments in the hash table. + # ikesa_table_segments = 1 + + # Size of the IKE_SA hash table. + # ikesa_table_size = 1 + + # Whether to close IKE_SA if the only CHILD_SA closed due to inactivity. + # inactivity_close_ike = no + + # Limit new connections based on the current number of half open IKE_SAs, + # see IKE_SA_INIT DROPPING in strongswan.conf(5). + # init_limit_half_open = 0 + + # Limit new connections based on the number of queued jobs. + # init_limit_job_load = 0 + + # Causes charon daemon to ignore IKE initiation requests. + # initiator_only = no + + # Install routes into a separate routing table for established IPsec + # tunnels. + install_routes = {{ install_routes }} + + # Install virtual IP addresses. + # install_virtual_ip = yes + + # The name of the interface on which virtual IP addresses should be + # installed. + # install_virtual_ip_on = + + # Check daemon, libstrongswan and plugin integrity at startup. + # integrity_test = no + + # A comma-separated list of network interfaces that should be ignored, if + # interfaces_use is specified this option has no effect. + # interfaces_ignore = + + # A comma-separated list of network interfaces that should be used by + # charon. All other interfaces are ignored. + # interfaces_use = + + # NAT keep alive interval. + # keep_alive = 20s + + # Plugins to load in the IKE daemon charon. + # load = + + # Determine plugins to load via each plugin's load option. + # load_modular = no + + # Initiate IKEv2 reauthentication with a make-before-break scheme. + # make_before_break = no + + # Maximum number of IKEv1 phase 2 exchanges per IKE_SA to keep state about + # and track concurrently. + # max_ikev1_exchanges = 3 + + # Maximum packet size accepted by charon. + # max_packet = 10000 + + # Enable multiple authentication exchanges (RFC 4739). + # multiple_authentication = yes + + # WINS servers assigned to peer via configuration payload (CP). + # nbns1 = + + # WINS servers assigned to peer via configuration payload (CP). + # nbns2 = + + # UDP port used locally. If set to 0 a random port will be allocated. + # port = 500 + + # UDP port used locally in case of NAT-T. If set to 0 a random port will be + # allocated. Has to be different from charon.port, otherwise a random port + # will be allocated. + # port_nat_t = 4500 + + # Prefer locally configured proposals for IKE/IPsec over supplied ones as + # responder (disabling this can avoid keying retries due to + # INVALID_KE_PAYLOAD notifies). + # prefer_configured_proposals = yes + + # By default public IPv6 addresses are preferred over temporary ones (RFC + # 4941), to make connections more stable. Enable this option to reverse + # this. + # prefer_temporary_addrs = no + + # Process RTM_NEWROUTE and RTM_DELROUTE events. + # process_route = yes + + # Delay in ms for receiving packets, to simulate larger RTT. + # receive_delay = 0 + + # Delay request messages. + # receive_delay_request = yes + + # Delay response messages. + # receive_delay_response = yes + + # Specific IKEv2 message type to delay, 0 for any. + # receive_delay_type = 0 + + # Size of the AH/ESP replay window, in packets. + # replay_window = 32 + + # Base to use for calculating exponential back off, see IKEv2 RETRANSMISSION + # in strongswan.conf(5). + # retransmit_base = 1.8 + + # Timeout in seconds before sending first retransmit. + # retransmit_timeout = 4.0 + + # Number of times to retransmit a packet before giving up. + # retransmit_tries = 5 + + # Interval in seconds to use when retrying to initiate an IKE_SA (e.g. if + # DNS resolution failed), 0 to disable retries. + # retry_initiate_interval = 0 + + # Initiate CHILD_SA within existing IKE_SAs (always enabled for IKEv1). + # reuse_ikesa = yes + + # Numerical routing table to install routes to. + # routing_table = + + # Priority of the routing table. + # routing_table_prio = + + # Delay in ms for sending packets, to simulate larger RTT. + # send_delay = 0 + + # Delay request messages. + # send_delay_request = yes + + # Delay response messages. + # send_delay_response = yes + + # Specific IKEv2 message type to delay, 0 for any. + # send_delay_type = 0 + + # Send strongSwan vendor ID payload + # send_vendor_id = no + + # Whether to enable Signature Authentication as per RFC 7427. + # signature_authentication = yes + + # Whether to enable constraints against IKEv2 signature schemes. + # signature_authentication_constraints = yes + + # Number of worker threads in charon. + # threads = 16 + + # Name of the user the daemon changes to after startup. + # user = + + crypto_test { + + # Benchmark crypto algorithms and order them by efficiency. + # bench = no + + # Buffer size used for crypto benchmark. + # bench_size = 1024 + + # Number of iterations to test each algorithm. + # bench_time = 50 + + # Test crypto algorithms during registration (requires test vectors + # provided by the test-vectors plugin). + # on_add = no + + # Test crypto algorithms on each crypto primitive instantiation. + # on_create = no + + # Strictly require at least one test vector to enable an algorithm. + # required = no + + # Whether to test RNG with TRUE quality; requires a lot of entropy. + # rng_true = no + + } + + host_resolver { + + # Maximum number of concurrent resolver threads (they are terminated if + # unused). + # max_threads = 3 + + # Minimum number of resolver threads to keep around. + # min_threads = 0 + + } + + leak_detective { + + # Includes source file names and line numbers in leak detective output. + # detailed = yes + + # Threshold in bytes for leaks to be reported (0 to report all). + # usage_threshold = 10240 + + # Threshold in number of allocations for leaks to be reported (0 to + # report all). + # usage_threshold_count = 0 + + } + + processor { + + # Section to configure the number of reserved threads per priority class + # see JOB PRIORITY MANAGEMENT in strongswan.conf(5). + priority_threads { + + } + + } + + # Section containing a list of scripts (name = path) that are executed when + # the daemon is started. + start-scripts { + + } + + # Section containing a list of scripts (name = path) that are executed when + # the daemon is terminated. + stop-scripts { + + } + + tls { + + # List of TLS encryption ciphers. + # cipher = + + # List of TLS key exchange methods. + # key_exchange = + + # List of TLS MAC algorithms. + # mac = + + # List of TLS cipher suites. + # suites = + + } + + x509 { + + # Discard certificates with unsupported or unknown critical extensions. + # enforce_critical = yes + + } + +} + diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl new file mode 100644 index 000000000..d0b60765b --- /dev/null +++ b/data/templates/ipsec/ipsec.conf.tmpl @@ -0,0 +1,3 @@ +{{delim_ipsec_l2tp_begin}} +include {{ipsec_ra_conn_file}} +{{delim_ipsec_l2tp_end}} diff --git a/data/templates/ipsec/ipsec.secrets.tmpl b/data/templates/ipsec/ipsec.secrets.tmpl new file mode 100644 index 000000000..55c010a3b --- /dev/null +++ b/data/templates/ipsec/ipsec.secrets.tmpl @@ -0,0 +1,7 @@ +{{delim_ipsec_l2tp_begin}} +{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} +{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}" +{% elif ipsec_l2tp_auth_mode == 'x509' %} +: RSA {{server_key_file_copied}} +{% endif%} +{{delim_ipsec_l2tp_end}} diff --git a/data/templates/ipsec/remote-access.tmpl b/data/templates/ipsec/remote-access.tmpl new file mode 100644 index 000000000..fae48232f --- /dev/null +++ b/data/templates/ipsec/remote-access.tmpl @@ -0,0 +1,28 @@ +{{delim_ipsec_l2tp_begin}} +conn {{ra_conn_name}} + type=transport + left={{outside_addr}} + leftsubnet=%dynamic[/1701] + rightsubnet=%dynamic + mark_in=%unique + auto=add + ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! + dpddelay=15 + dpdtimeout=45 + dpdaction=clear + esp=aes256-sha1,3des-sha1! + rekey=no +{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} + authby=secret + leftauth=psk + rightauth=psk +{% elif ipsec_l2tp_auth_mode == 'x509' %} + authby=rsasig + leftrsasigkey=%cert + rightrsasigkey=%cert + rightca=%same + leftcert={{server_cert_file_copied}} +{% endif %} + ikelifetime={{ipsec_l2tp_ike_lifetime}} + keylife={{ipsec_l2tp_lifetime}} +{{delim_ipsec_l2tp_end}} diff --git a/data/templates/lcd/LCDd.conf.tmpl b/data/templates/lcd/LCDd.conf.tmpl new file mode 100644 index 000000000..6cf6a440f --- /dev/null +++ b/data/templates/lcd/LCDd.conf.tmpl @@ -0,0 +1,132 @@ +### Autogenerted by system-display.py ## + +# LCDd.conf -- configuration file for the LCDproc server daemon LCDd +# +# This file contains the configuration for the LCDd server. +# +# The format is ini-file-like. It is divided into sections that start at +# markers that look like [section]. Comments are all line-based comments, +# and are lines that start with '#' or ';'. +# +# The server has a 'central' section named [server]. For the menu there is +# a section called [menu]. Further each driver has a section which +# defines how the driver acts. +# +# The drivers are activated by specifying them in a driver= line in the +# server section, like: +# +# Driver=curses +# +# This tells LCDd to use the curses driver. +# The first driver that is loaded and is capable of output defines the +# size of the display. The default driver to use is curses. +# If the driver is specified using the -d <driver> command line option, +# the Driver= options in the config file are ignored. +# +# The drivers read their own options from the respective sections. + +## Server section with all kinds of settings for the LCDd server ## +[server] + +# Where can we find the driver modules ? +# NOTE: Always place a slash as last character ! +DriverPath=/usr/lib/x86_64-linux-gnu/lcdproc/ + +# Tells the server to load the given drivers. Multiple lines can be given. +# The name of the driver is case sensitive and determines the section +# where to look for further configuration options of the specific driver +# as well as the name of the dynamic driver module to load at runtime. +# The latter one can be changed by giving a File= directive in the +# driver specific section. +# +# The following drivers are supported: +# bayrad, CFontz, CFontzPacket, curses, CwLnx, ea65, EyeboxOne, futaba, +# g15, glcd, glcdlib, glk, hd44780, icp_a106, imon, imonlcd,, IOWarrior, +# irman, joy, lb216, lcdm001, lcterm, linux_input, lirc, lis, MD8800, +# mdm166a, ms6931, mtc_s16209x, MtxOrb, mx5000, NoritakeVFD, +# Olimex_MOD_LCD1x9, picolcd, pyramid, rawserial, sdeclcd, sed1330, +# sed1520, serialPOS, serialVFD, shuttleVFD, sli, stv5730, svga, t6963, +# text, tyan, ula200, vlsys_m428, xosd, yard2LCD + +{% if model is defined %} +{% if model.startswith('cfa-') %} +Driver=CFontzPacket +{% elif model == 'sdec' %} +Driver=sdeclcd +{% endif %} +{% endif %} + +# Tells the driver to bind to the given interface. [default: 127.0.0.1] +Bind=127.0.0.1 + +# Listen on this specified port. [default: 13666] +Port=13666 + +# Sets the reporting level; defaults to warnings and errors only. +# [default: 2; legal: 0-5] +ReportLevel=3 + +# Should we report to syslog instead of stderr? [default: no; legal: yes, no] +ReportToSyslog=yes + +# User to run as. LCDd will drop its root privileges and run as this user +# instead. [default: nobody] +User=nobody + +# The server will stay in the foreground if set to yes. +# [default: no, legal: yes, no] +Foreground=yes + +# Hello message: each entry represents a display line; default: builtin +Hello="Starting VyOS..." + +# GoodBye message: each entry represents a display line; default: builtin +GoodBye="VyOS shutdown..." + +# Sets the interval in microseconds for updating the display. +# [default: 125000 meaning 8Hz] +FrameInterval=500000 # 2 updates per second + +# Sets the default time in seconds to displays a screen. [default: 4] +WaitTime=1 + +# If set to no, LCDd will start with screen rotation disabled. This has the +# same effect as if the ToggleRotateKey had been pressed. Rotation will start +# if the ToggleRotateKey is pressed. Note that this setting does not turn off +# priority sorting of screens. [default: on; legal: on, off] +AutoRotate=on + +# If yes, the the serverscreen will be rotated as a usual info screen. If no, +# it will be a background screen, only visible when no other screens are +# active. The special value 'blank' is similar to no, but only a blank screen +# is displayed. [default: on; legal: on, off, blank] +ServerScreen=blank + +# Set master backlight setting. If set to 'open' a client may control the +# backlight for its own screens (only). [default: open; legal: off, open, on] +Backlight=on + +# Set master heartbeat setting. If set to 'open' a client may control the +# heartbeat for its own screens (only). [default: open; legal: off, open, on] +Heartbeat=off + +# set title scrolling speed [default: 10; legal: 0-10] +TitleSpeed=10 + +{% if model is defined and model is not none %} +{% if model.startswith('cfa-') %} +## CrystalFontz packet driver (for CFA533, CFA631, CFA633 & CFA635) ## +[CFontzPacket] +Model={{ model.split('-')[1] }} +Device={{ device }} +Contrast=350 +Brightness=500 +OffBrightness=50 +Reboot=yes +USB=yes +{% elif model == 'sdec' %} +## SDEC driver for Lanner, Watchguard, Sophos sppliances ## +[sdeclcd] +# No options +{% endif %} +{% endif %} diff --git a/data/templates/lcd/lcdproc.conf.tmpl b/data/templates/lcd/lcdproc.conf.tmpl new file mode 100644 index 000000000..c79f3cd0d --- /dev/null +++ b/data/templates/lcd/lcdproc.conf.tmpl @@ -0,0 +1,60 @@ +### autogenerated by system-lcd.py ### + +# LCDproc client configuration file + +[lcdproc] +Server=127.0.0.1 +Port=13666 + +# set reporting level +ReportLevel=3 + +# report to to syslog ? +ReportToSyslog=true + +Foreground=yes + +[CPU] +Active=true +OnTime=1 +OffTime=2 +ShowInvisible=false + +[SMP-CPU] +Active=false + +[Memory] +Active=false + +[Load] +Active=false + +[Uptime] +Active=true + +[ProcSize] +Active=false + +[Disk] +Active=false + +[About] +Active=false + +[TimeDate] +Active=true +TimeFormat="%H:%M:%S" + +[OldTime] +Active=false + +[BigClock] +Active=false + +[MiniClock] +Active=false + +# Display the title bar in two-line mode. Note that with four lines or more +# the title is always shown. [default: true; legal: true, false] +ShowTitle=false + diff --git a/data/templates/lldp/lldpd.tmpl b/data/templates/lldp/lldpd.tmpl new file mode 100644 index 000000000..3db955b48 --- /dev/null +++ b/data/templates/lldp/lldpd.tmpl @@ -0,0 +1,3 @@ +### Autogenerated by lldp.py ### +DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}" + diff --git a/data/templates/lldp/vyos.conf.tmpl b/data/templates/lldp/vyos.conf.tmpl new file mode 100644 index 000000000..e724f42c6 --- /dev/null +++ b/data/templates/lldp/vyos.conf.tmpl @@ -0,0 +1,20 @@ +### Autogenerated by lldp.py ### + +configure system platform VyOS +configure system description "VyOS {{ options.description }}" +{% if options.listen_on -%} +configure system interface pattern "{{ ( options.listen_on | select('equalto','all') | map('replace','all','*') | list + options.listen_on | select('equalto','!all') | map('replace','!all','!*') | list + options.listen_on | reject('equalto','all') | reject('equalto','!all') | list ) | unique | join(",") }}" +{%- endif %} +{% if options.mgmt_addr -%} +configure system ip management pattern {{ options.mgmt_addr | join(",") }} +{%- endif %} +{%- for loc in location -%} +{%- if loc.elin %} +configure ports {{ loc.name }} med location elin "{{ loc.elin }}" +{%- endif %} +{%- if loc.coordinate_based %} +configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %} +{%- endif %} + + +{% endfor %} diff --git a/data/templates/macsec/wpa_supplicant.conf.tmpl b/data/templates/macsec/wpa_supplicant.conf.tmpl new file mode 100644 index 000000000..1731bf160 --- /dev/null +++ b/data/templates/macsec/wpa_supplicant.conf.tmpl @@ -0,0 +1,89 @@ +# autogenerated by interfaces-macsec.py + +# see full documentation: +# https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf + +# For UNIX domain sockets (default on Linux and BSD): This is a directory that +# will be created for UNIX domain sockets for listening to requests from +# external programs (CLI/GUI, etc.) for status information and configuration. +# The socket file will be named based on the interface name, so multiple +# wpa_supplicant processes can be run at the same time if more than one +# interface is used. +# /var/run/wpa_supplicant is the recommended directory for sockets and by +# default, wpa_cli will use it when trying to connect with wpa_supplicant. +ctrl_interface=/run/wpa_supplicant + +# Note: When using MACsec, eapol_version shall be set to 3, which is +# defined in IEEE Std 802.1X-2010. +eapol_version=3 + +# No need to scan for access points in MACsec mode +ap_scan=0 + +# EAP fast re-authentication +fast_reauth=1 + +network={ + key_mgmt=NONE + + # Note: When using wired authentication (including MACsec drivers), + # eapol_flags must be set to 0 for the authentication to be completed + # successfully. + eapol_flags=0 + + # macsec_policy: IEEE 802.1X/MACsec options + # This determines how sessions are secured with MACsec (only for MACsec + # drivers). + # 0: MACsec not in use (default) + # 1: MACsec enabled - Should secure, accept key server's advice to + # determine whether to use a secure session or not. + macsec_policy=1 + + # macsec_integ_only: IEEE 802.1X/MACsec transmit mode + # This setting applies only when MACsec is in use, i.e., + # - macsec_policy is enabled + # - the key server has decided to enable MACsec + # 0: Encrypt traffic (default) + # 1: Integrity only + macsec_integ_only={{ '0' if security is defined and security.encrypt is defined else '1' }} + +{% if security is defined %} +{% if security.encrypt is defined %} + # mka_cak, mka_ckn, and mka_priority: IEEE 802.1X/MACsec pre-shared key mode + # This allows to configure MACsec with a pre-shared key using a (CAK,CKN) pair. + # In this mode, instances of wpa_supplicant can act as MACsec peers. The peer + # with lower priority will become the key server and start distributing SAKs. + # mka_cak (CAK = Secure Connectivity Association Key) takes a 16-byte (128-bit) + # hex-string (32 hex-digits) or a 32-byte (256-bit) hex-string (64 hex-digits) + # mka_ckn (CKN = CAK Name) takes a 1..32-bytes (8..256 bit) hex-string + # (2..64 hex-digits) + mka_cak={{ security.mka.cak }} + mka_ckn={{ security.mka.ckn }} + + # mka_priority (Priority of MKA Actor) is in 0..255 range with 255 being + # default priority + mka_priority={{ security.mka.priority }} +{% endif %} + +{% if security.replay_window is defined %} + # macsec_replay_protect: IEEE 802.1X/MACsec replay protection + # This setting applies only when MACsec is in use, i.e., + # - macsec_policy is enabled + # - the key server has decided to enable MACsec + # 0: Replay protection disabled (default) + # 1: Replay protection enabled + macsec_replay_protect=1 + + # macsec_replay_window: IEEE 802.1X/MACsec replay protection window + # This determines a window in which replay is tolerated, to allow receipt + # of frames that have been misordered by the network. + # This setting applies only when MACsec replay protection active, i.e., + # - macsec_replay_protect is enabled + # - the key server has decided to enable MACsec + # 0: No replay window, strict check (default) + # 1..2^32-1: number of packets that could be misordered + macsec_replay_window={{ security.replay_window }} +{% endif %} +{% endif %} +} + diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl new file mode 100644 index 000000000..80f4ab047 --- /dev/null +++ b/data/templates/mdns-repeater/mdns-repeater.tmpl @@ -0,0 +1,2 @@ +### Autogenerated by mdns_repeater.py ### +DAEMON_ARGS="{{ interface | join(' ') }}" diff --git a/data/templates/netflow/uacctd.conf.tmpl b/data/templates/netflow/uacctd.conf.tmpl new file mode 100644 index 000000000..d8615566f --- /dev/null +++ b/data/templates/netflow/uacctd.conf.tmpl @@ -0,0 +1,69 @@ +# Genereated from VyOS configuration +daemonize: true +promisc: false +pidfile: /var/run/uacctd.pid +uacctd_group: 2 +uacctd_nl_size: 2097152 +snaplen: {{ snaplen }} +aggregate: in_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows +plugin_pipe_size: {{ templatecfg['plugin_pipe_size'] }} +plugin_buffer_size: {{ templatecfg['plugin_buffer_size'] }} +{%- if templatecfg['syslog-facility'] != none %} +syslog: {{ templatecfg['syslog-facility'] }} +{%- endif %} +{%- if templatecfg['disable-imt'] == none %} +imt_path: /tmp/uacctd.pipe +imt_mem_pools_number: 169 +{%- endif %} +plugins: +{%- if templatecfg['netflow']['servers'] != none -%} + {% for server in templatecfg['netflow']['servers'] %} + {%- if loop.last -%}nfprobe[nf_{{ server['address'] }}]{%- else %}nfprobe[nf_{{ server['address'] }}],{%- endif %} + {%- endfor -%} + {% set plugins_presented = true %} +{%- endif %} +{%- if templatecfg['sflow']['servers'] != none -%} + {% if plugins_presented -%} + {%- for server in templatecfg['sflow']['servers'] -%} + ,sfprobe[sf_{{ server['address'] }}] + {%- endfor %} + {%- else %} + {%- for server in templatecfg['sflow']['servers'] %} + {%- if loop.last -%}sfprobe[sf_{{ server['address'] }}]{%- else %}sfprobe[sf_{{ server['address'] }}],{%- endif %} + {%- endfor %} + {%- endif -%} + {% set plugins_presented = true %} +{%- endif %} +{%- if templatecfg['disable-imt'] == none %} + {%- if plugins_presented -%},memory{%- else %}memory{%- endif %} +{%- endif %} +{%- if templatecfg['netflow']['servers'] != none %} +{%- for server in templatecfg['netflow']['servers'] %} +nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} +nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }} +{%- if templatecfg['netflow']['engine-id'] != none %} +nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }} +{%- endif %} +{%- if templatecfg['netflow']['max-flows'] != none %} +nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }} +{%- endif %} +{%- if templatecfg['netflow']['sampling-rate'] != none %} +sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }} +{%- endif %} +{%- if templatecfg['netflow']['source-ip'] != none %} +nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }} +{%- endif %} +{%- if templatecfg['netflow']['timeout_string'] != '' %} +nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }} +{%- endif %} +{%- endfor %} +{%- endif %} +{%- if templatecfg['sflow']['servers'] != none %} +{%- for server in templatecfg['sflow']['servers'] %} +sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} +sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }} +{%- if templatecfg['sflow']['sampling-rate'] != none %} +sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }} +{%- endif %} +{%- endfor %} +{% endif %} diff --git a/data/templates/ntp/ntp.conf.tmpl b/data/templates/ntp/ntp.conf.tmpl new file mode 100644 index 000000000..6ef0c0f2c --- /dev/null +++ b/data/templates/ntp/ntp.conf.tmpl @@ -0,0 +1,47 @@ +### Autogenerated by ntp.py ### + +# +# Non-configurable defaults +# +driftfile /var/lib/ntp/ntp.drift +# By default, only allow ntpd to query time sources, ignore any incoming requests +restrict default noquery nopeer notrap nomodify +# Local users have unrestricted access, allowing reconfiguration via ntpdc +restrict 127.0.0.1 +restrict -6 ::1 + +# +# Configurable section +# +{% if server %} +{% for srv in server %} +{% set options = '' %} +{% set options = options + 'noselect ' if server[srv].noselect is defined else '' %} +{% set options = options + 'preempt ' if server[srv].preempt is defined else '' %} +{% set options = options + 'prefer ' if server[srv].prefer is defined else '' %} +server {{ srv | replace('_', '-') }} iburst {{ options }} +{% endfor %} +{% endif %} + +{% if allow_clients is defined and allow_clients.address is defined %} +# Allowed clients configuration +{% if allow_clients.address is string %} +restrict {{ allow_clients.address|address_from_cidr }} mask {{ allow_clients.address|netmask_from_cidr }} nomodify notrap nopeer +{% else %} +{% for address in allow_clients.address %} +restrict {{ address|address_from_cidr }} mask {{ address|netmask_from_cidr }} nomodify notrap nopeer +{% endfor %} +{% endif %} +{% endif %} + +{% if listen_address %} +# NTP should listen on configured addresses only +interface ignore wildcard +{% if listen_address is string %} +interface listen {{ listen_address }} +{% else %} +{% for address in listen_address %} +interface listen {{ address }} +{% endfor %} +{% endif %} +{% endif %} diff --git a/data/templates/ntp/override.conf.tmpl b/data/templates/ntp/override.conf.tmpl new file mode 100644 index 000000000..466638e5a --- /dev/null +++ b/data/templates/ntp/override.conf.tmpl @@ -0,0 +1,11 @@ +{% set vrf_command = '/sbin/ip vrf exec ' + vrf + ' ' if vrf is defined else '' %} +[Unit] +StartLimitIntervalSec=0 +After=vyos-router.service + +[Service] +ExecStart= +ExecStart={{vrf_command}}/usr/lib/ntp/ntp-systemd-wrapper +Restart=on-failure +RestartSec=10 + diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl new file mode 100644 index 000000000..6aaeff693 --- /dev/null +++ b/data/templates/ocserv/ocserv_config.tmpl @@ -0,0 +1,82 @@ +### generated by vpn_anyconnect.py ### + +tcp-port = {{ listen_ports.tcp }} +udp-port = {{ listen_ports.udp }} + +run-as-user = nobody +run-as-group = daemon + +{% if "radius" in authentication.mode %} +auth = "radius [config=/run/ocserv/radiusclient.conf]" +{% else %} +auth = "plain[/run/ocserv/ocpasswd]" +{% endif %} + +{% if ssl.cert_file %} +server-cert = {{ ssl.cert_file }} +{% endif %} + +{% if ssl.key_file %} +server-key = {{ ssl.key_file }} +{% endif %} + +{% if ssl.ca_cert_file %} +ca-cert = {{ ssl.ca_cert_file }} +{% endif %} + +socket-file = /run/ocserv/ocserv.socket +occtl-socket-file = /run/ocserv/occtl.socket +use-occtl = true +isolate-workers = true +keepalive = 300 +dpd = 60 +mobile-dpd = 300 +switch-to-tcp-timeout = 30 +tls-priorities = "NORMAL:%SERVER_PRECEDENCE:%COMPAT:-RSA:-VERS-SSL3.0:-ARCFOUR-128" +auth-timeout = 240 +idle-timeout = 1200 +mobile-idle-timeout = 1800 +min-reauth-time = 3 +cookie-timeout = 300 +rekey-method = ssl +try-mtu-discovery = true +cisco-client-compat = true +dtls-legacy = true + + +# The name to use for the tun device +device = sslvpn + +# An alternative way of specifying the network: +{% if network_settings %} +# DNS settings +{% if network_settings.name_server is string %} +dns = {{ network_settings.name_server }} +{% else %} +{% for dns in network_settings.name_server %} +dns = {{ dns }} +{% endfor %} +{% endif %} +# IPv4 network pool +{% if network_settings.client_ip_settings %} +{% if network_settings.client_ip_settings.subnet %} +ipv4-network = {{ network_settings.client_ip_settings.subnet }} +{% endif %} +{% endif %} +# IPv6 network pool +{% if network_settings.client_ipv6_pool %} +{% if network_settings.client_ipv6_pool.prefix %} +ipv6-network = {{ network_settings.client_ipv6_pool.prefix }} +ipv6-subnet-prefix = {{ network_settings.client_ipv6_pool.mask }} +{% endif %} +{% endif %} +{% endif %} + +{% if network_settings.push_route is string %} +route = {{ network_settings.push_route }} +{% else %} +{% for route in network_settings.push_route %} +route = {{ route }} +{% endfor %} +{% endif %} + diff --git a/data/templates/ocserv/ocserv_passwd.tmpl b/data/templates/ocserv/ocserv_passwd.tmpl new file mode 100644 index 000000000..ffadb4860 --- /dev/null +++ b/data/templates/ocserv/ocserv_passwd.tmpl @@ -0,0 +1,6 @@ +#<username>:<group>:<hash> +{% for user in username if username is defined %} +{% if not "disable" in username[user] %} +{{ user }}:*:{{ username[user].hash }} +{% endif %} +{% endfor %}
\ No newline at end of file diff --git a/data/templates/ocserv/radius_conf.tmpl b/data/templates/ocserv/radius_conf.tmpl new file mode 100644 index 000000000..2d19306a0 --- /dev/null +++ b/data/templates/ocserv/radius_conf.tmpl @@ -0,0 +1,22 @@ +### generated by cpn_anyconnect.py ### +nas-identifier VyOS +{% for srv in server %} +{% if not "disable" in server[srv] %} +{% if "port" in server[srv] %} +authserver {{ srv }}:{{server[srv]["port"]}} +{% else %} +authserver {{ srv }} +{% endif %} +{% endif %} +{% endfor %} +radius_timeout {{ timeout }} +{% if source_address %} +bindaddr {{ source_address }} +{% else %} +bindaddr * +{% endif %} +servers /run/ocserv/radius_servers +dictionary /etc/radcli/dictionary +default_realm +radius_retries 3 +#
\ No newline at end of file diff --git a/data/templates/ocserv/radius_servers.tmpl b/data/templates/ocserv/radius_servers.tmpl new file mode 100644 index 000000000..ba21fa074 --- /dev/null +++ b/data/templates/ocserv/radius_servers.tmpl @@ -0,0 +1,7 @@ +### generated by cpn_anyconnect.py ### +# server key +{% for srv in server %} +{% if not "disable" in server[srv] %} +{{ srv }} {{ server[srv].key }} +{% endif %} +{% endfor %} diff --git a/data/templates/openvpn/client.conf.tmpl b/data/templates/openvpn/client.conf.tmpl new file mode 100644 index 000000000..508d8da94 --- /dev/null +++ b/data/templates/openvpn/client.conf.tmpl @@ -0,0 +1,35 @@ +### Autogenerated by interfaces-openvpn.py ### + +{% if ip -%} +ifconfig-push {{ ip[0] }} {{ remote_netmask }} +{% endif -%} + +{% for route in push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for net in subnet -%} +iroute {{ net }} +{% endfor -%} + +{# ipv6_remote is only set when IPv6 server is enabled #} +{% if ipv6_remote -%} +# IPv6 + +{%- if ipv6_ip %} +ifconfig-ipv6-push {{ ipv6_ip[0] }} {{ ipv6_remote }} +{%- endif %} + +{%- for route6 in ipv6_push_route %} +push "route-ipv6 {{ route6 }}" +{%- endfor %} + +{%- for net6 in ipv6_subnet %} +iroute {{ net6 }} +{%- endfor %} + +{% endif -%} + +{% if disable -%} +disable +{% endif -%} diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl new file mode 100644 index 000000000..401f8e04b --- /dev/null +++ b/data/templates/openvpn/server.conf.tmpl @@ -0,0 +1,262 @@ +### Autogenerated by interfaces-openvpn.py ### +# +# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage +# for individual keyword definition + +{% if description -%} +# {{ description }} + +{% endif -%} + +verb 3 + +user {{ uid }} +group {{ gid }} + +dev-type {{ type }} +dev {{ intf }} +persist-key +iproute /usr/libexec/vyos/system/unpriv-ip + +proto {{ protocol_real }} + +{%- if local_host %} +local {{ local_host }} +{%- endif %} + +{%- if mode == 'server' and protocol == 'udp' and not local_host %} +multihome +{%- endif %} + +{%- if local_port %} +lport {{ local_port }} +{%- endif %} + +{% if remote_port -%} +rport {{ remote_port }} +{% endif %} + +{%- if remote_host %} +{%- for remote in remote_host -%} +remote {{ remote }} +{% endfor -%} +{% endif -%} + +{% if shared_secret_file %} +secret {{ shared_secret_file }} +{%- endif %} + +{%- if persistent_tunnel %} +persist-tun +{%- endif %} + +{%- if redirect_gateway %} +push "redirect-gateway {{ redirect_gateway }}" +{%- endif %} + +{%- if compress_lzo %} +compress lzo +{%- endif %} + +{% if 'client' in mode -%} +# +# OpenVPN Client mode +# +client +nobind + +{% elif 'server' in mode -%} +# +# OpenVPN Server mode +# + +{%- if server_topology %} +topology {% if server_topology == 'point-to-point' %}p2p{% else %}{{ server_topology }}{% endif %} +{%- endif %} + +{%- if is_bridge_member %} +mode server +tls-server +{%- else %} +server {{ server_subnet[0] }} nopool +{%- endif %} + +{%- if server_pool %} +ifconfig-pool {{ server_pool_start }} {{ server_pool_stop }}{% if server_pool_netmask %} {{ server_pool_netmask }}{% endif %} +{%- endif %} + +{%- if server_max_conn %} +max-clients {{ server_max_conn }} +{%- endif %} + +{%- if client %} +client-config-dir /run/openvpn/ccd/{{ intf }} +{%- endif %} + +{%- if server_reject_unconfigured %} +ccd-exclusive +{%- endif %} + +keepalive {{ ping_interval }} {{ ping_restart }} +management /run/openvpn/openvpn-mgmt-intf unix + +{% for route in server_push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for ns in server_dns_nameserver -%} +push "dhcp-option DNS {{ ns }}" +{% endfor -%} + +{%- if server_domain -%} +push "dhcp-option DOMAIN {{ server_domain }}" +{% endif -%} + +{%- if server_ipv6_local %} +# IPv6 +push "tun-ipv6" +ifconfig-ipv6 {{ server_ipv6_local }}/{{ server_ipv6_prefixlen }} {{ server_ipv6_remote }} + +{%- if server_ipv6_pool %} +ifconfig-ipv6-pool {{ server_ipv6_pool_base }}/{{ server_ipv6_pool_prefixlen }} +{%- endif %} + +{%- for route6 in server_ipv6_push_route %} +push "route-ipv6 {{ route6 }}" +{%- endfor %} + +{%- for ns6 in server_ipv6_dns_nameserver %} +push "dhcp-option DNS6 {{ ns6 }}" +{%- endfor %} + +{%- endif %} + +{% else -%} +# +# OpenVPN site-2-site mode +# +ping {{ ping_interval }} +ping-restart {{ ping_restart }} + +{% if local_address_subnet -%} +ifconfig {{ local_address[0] }} {{ local_address_subnet }} +{%- elif remote_address -%} +ifconfig {{ local_address[0] }} {{ remote_address[0] }} +{%- endif %} + +{% if ipv6_local_address -%} +ifconfig-ipv6 {{ ipv6_local_address[0] }} {{ ipv6_remote_address[0] }} +{%- endif %} + +{% endif -%} + +{% if tls -%} +# TLS options +{%- if tls_ca_cert %} +ca {{ tls_ca_cert }} +{%- endif %} + +{%- if tls_cert %} +cert {{ tls_cert }} +{%- endif %} + +{%- if tls_key %} +key {{ tls_key }} +{%- endif %} + +{%- if tls_crypt %} +tls-crypt {{ tls_crypt }} +{%- endif %} + +{%- if tls_crl %} +crl-verify {{ tls_crl }} +{%- endif %} + +{%- if tls_version_min %} +tls-version-min {{tls_version_min}} +{%- endif %} + +{%- if tls_dh %} +dh {{ tls_dh }} +{%- endif %} + +{%- if tls_auth %} +tls-auth {{tls_auth}} +{%- endif %} + +{%- if tls_role %} +{%- if 'active' in tls_role %} +tls-client +{%- elif 'passive' in tls_role %} +tls-server +{%- endif %} +{%- endif %} + +{%- endif %} + +# Encryption options +{%- if encryption %} +{% if encryption == 'des' -%} +cipher des-cbc +{%- elif encryption == '3des' -%} +cipher des-ede3-cbc +{%- elif encryption == 'bf128' -%} +cipher bf-cbc +keysize 128 +{%- elif encryption == 'bf256' -%} +cipher bf-cbc +keysize 25 +{%- elif encryption == 'aes128gcm' -%} +cipher aes-128-gcm +{%- elif encryption == 'aes128' -%} +cipher aes-128-cbc +{%- elif encryption == 'aes192gcm' -%} +cipher aes-192-gcm +{%- elif encryption == 'aes192' -%} +cipher aes-192-cbc +{%- elif encryption == 'aes256gcm' -%} +cipher aes-256-gcm +{%- elif encryption == 'aes256' -%} +cipher aes-256-cbc +{%- endif -%} +{%- endif %} + +{%- if ncp_ciphers %} +ncp-ciphers {{ncp_ciphers}} +{%- endif %} +{%- if disable_ncp %} +ncp-disable +{%- endif %} + +{% if hash -%} +auth {{ hash }} +{%- endif -%} + +{%- if auth %} +auth-user-pass {{ auth_user_pass_file }} +auth-retry nointeract +{%- endif %} + +# DEPRECATED This option will be removed in OpenVPN 2.5 +# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this: +# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old +# behaviour was to remap any character other than alphanumeric, underscore ('_'), +# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject +# string as returned by the tls_id environmental variable, could additionally +# contain colon (':') or equal ('='). When using the --compat-names option, this +# old formatting and remapping will be re-enabled again. This is purely implemented +# for compatibility reasons when using older plug-ins or scripts which does not +# handle the new formatting or UTF-8 characters. +# +# See https://phabricator.vyos.net/T1512 +compat-names + +{% if options -%} +# +# Custom options added by user (not validated) +# + +{% for option in options -%} +{{ option }} +{% endfor -%} +{%- endif %} diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl new file mode 100644 index 000000000..c2d0cd09a --- /dev/null +++ b/data/templates/pppoe/ip-down.script.tmpl @@ -0,0 +1,36 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up +if [ "$6" != "{{ ifname }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ ifname }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +{% if connect_on_demand is not defined %} +# See https://phabricator.vyos.net/T2248. Determine if we are enslaved to a +# VRF, this is needed to properly insert the default route. +VRF_NAME="" +if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*)) + # Remove upper_ prefix from result string + VRF=${VRF#"upper_"} + # Populate variable to run in VR context + VRF_NAME="vrf ${VRF_NAME}" +fi + +# Always delete default route when interface goes down +vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ ifname }} ${VRF_NAME}" +{% if ipv6_enable %} +vtysh -c "conf t" ${VRF_NAME} -c "no ipv6 route ::/0 {{ ifname }} ${VRF_NAME}" +{% endif %} +{% endif %} + +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} +# Stop wide dhcpv6 client +systemctl stop dhcp6c@{{ ifname }}.service +{% endif %} diff --git a/data/templates/pppoe/ip-pre-up.script.tmpl b/data/templates/pppoe/ip-pre-up.script.tmpl new file mode 100644 index 000000000..cf85ed067 --- /dev/null +++ b/data/templates/pppoe/ip-pre-up.script.tmpl @@ -0,0 +1,18 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up +if [ "$6" != "{{ ifname }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ ifname }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +echo "{{ description }}" > /sys/class/net/{{ ifname }}/ifalias + +{% if vrf -%} +logger -t pppd[$DIALER_PID] "configuring dialer interface $6 for VRF {{ vrf }}" +ip link set dev {{ ifname }} master {{ vrf }} +{% endif %} diff --git a/data/templates/pppoe/ip-up.script.tmpl b/data/templates/pppoe/ip-up.script.tmpl new file mode 100644 index 000000000..568e21c4e --- /dev/null +++ b/data/templates/pppoe/ip-up.script.tmpl @@ -0,0 +1,49 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up +if [ "$6" != "{{ ifname }}" ]; then + exit +fi + +{% if connect_on_demand is not defined %} +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ ifname }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +{% if default_route != 'none' -%} +# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved +# to a VRF, this is needed to properly insert the default route. + +SED_OPT="^ip route" +VRF_NAME="" +if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*)) + # Remove upper_ prefix from result string + VRF=${VRF#"upper_"} + # generate new SED command + SED_OPT="vrf ${VRF}" + # generate vtysh option + VRF_NAME="vrf ${VRF}" +fi + +{% if default_route == 'auto' -%} +# Only insert a new default route if there is no default route configured +routes=$(vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | wc -l) +if [ "$routes" -ne 0 ]; then + exit 1 +fi + +{% elif default_route == 'force' -%} +# Retrieve current static default routes and remove it from the routing table +vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | while read route ; do + vtysh -c "conf t" ${VTY_OPT} -c "no ${route} ${VRF_NAME}" +done +{% endif %} + +# Add default route to default or VRF routing table +vtysh -c "conf t" ${VTY_OPT} -c "ip route 0.0.0.0/0 {{ ifname }} ${VRF_NAME}" +logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} ${VRF_NAME}" +{% endif %} +{% endif %} diff --git a/data/templates/pppoe/ipv6-up.script.tmpl b/data/templates/pppoe/ipv6-up.script.tmpl new file mode 100644 index 000000000..d0a62478c --- /dev/null +++ b/data/templates/pppoe/ipv6-up.script.tmpl @@ -0,0 +1,83 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up + +if [ "$6" != "{{ ifname }}" ]; then + exit +fi + +{% if ipv6 is defined and ipv6.address is defined and ipv6.address.autoconf is defined -%} +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ ifname }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" +logger -t pppd[$DIALER_PID] "configuring interface {{ ifname }} via {{ source_interface }}" + +# Configure interface-specific Host/Router behaviour. +# Note: It is recommended to have the same setting on all interfaces; mixed +# router/host scenarios are rather uncommon. Possible values are: +# +# 0 Forwarding disabled +# 1 Forwarding enabled +# +echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/forwarding + +# Accept Router Advertisements; autoconfigure using them. +# +# It also determines whether or not to transmit Router +# Solicitations. If and only if the functional setting is to +# accept Router Advertisements, Router Solicitations will be +# transmitted. Possible values are: +# +# 0 Do not accept Router Advertisements. +# 1 Accept Router Advertisements if forwarding is disabled. +# 2 Overrule forwarding behaviour. Accept Router Advertisements +# even if forwarding is enabled. +# +echo 2 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra + +# Autoconfigure addresses using Prefix Information in Router Advertisements. +echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/autoconf +{% endif %} + +{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %} +# Start wide dhcpv6 client +systemctl start dhcp6c@{{ ifname }}.service +{% endif %} + +{% if default_route != 'none' -%} +# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved +# to a VRF, this is needed to properly insert the default route. + +SED_OPT="^ipv6 route" +VRF_NAME="" +if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*)) + # Remove upper_ prefix from result string + VRF=${VRF#"upper_"} + # generate new SED command + SED_OPT="vrf ${VRF}" + # generate vtysh option + VRF_NAME="vrf ${VRF}" +fi + +{% if default_route == 'auto' -%} +# Only insert a new default route if there is no default route configured +routes=$(vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep ::/0 | wc -l) +if [ "$routes" -ne 0 ]; then + exit 1 +fi + +{% elif default_route == 'force' -%} +# Retrieve current static default routes and remove it from the routing table +vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep ::/0 | while read route ; do + vtysh -c "conf t" ${VTY_OPT} -c "no ${route} ${VRF_NAME}" +done +{% endif %} + +# Add default route to default or VRF routing table +vtysh -c "conf t" ${VTY_OPT} -c "ipv6 route ::/0 {{ ifname }} ${VRF_NAME}" +logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} ${VRF_NAME}" +{% endif %} + diff --git a/data/templates/pppoe/peer.tmpl b/data/templates/pppoe/peer.tmpl new file mode 100644 index 000000000..e909843a5 --- /dev/null +++ b/data/templates/pppoe/peer.tmpl @@ -0,0 +1,76 @@ +### Autogenerated by interfaces-pppoe.py ### + +{% if description %} +# {{ description }} +{% endif %} + +# Require peer to provide the local IP address if it is not +# specified explicitly in the config file. +noipdefault + +# Don't show the password in logfiles: +hide-password + +# Standard Link Control Protocol (LCP) parameters: +lcp-echo-interval 20 +lcp-echo-failure 3 + +# RFC 2516, paragraph 7 mandates that the following options MUST NOT be +# requested and MUST be rejected if requested by the peer: +# Address-and-Control-Field-Compression (ACFC) +noaccomp + +# Asynchronous-Control-Character-Map (ACCM) +default-asyncmap + +# Override any connect script that may have been set in /etc/ppp/options. +connect /bin/true + +# Don't try to authenticate the remote node +noauth + +# Don't try to proxy ARP for the remote endpoint. User can set proxy +# arp entries up manually if they wish. More importantly, having +# the "proxyarp" parameter set disables the "defaultroute" option. +noproxyarp + +# Unlimited connection attempts +maxfail 0 + +plugin rp-pppoe.so +{{ source_interface }} +persist +ifname {{ ifname }} +ipparam {{ ifname }} +debug +mtu {{ mtu }} +mru {{ mtu }} + +{% if authentication is defined %} +{{ "user " + authentication.user if authentication.user is defined }} +{{ "password " + authentication.password if authentication.password is defined }} +{% endif %} + +{{ "usepeerdns" if no_peer_dns is not defined }} + +{% if ipv6 is defined and ipv6.enable is defined -%} ++ipv6 +ipv6cp-use-ipaddr +{% endif %} + +{% if service_name is defined -%} +rp_pppoe_service "{{ service_name }}" +{% endif %} + +{% if connect_on_demand is defined %} +demand +# See T2249. PPP default route options should only be set when in on-demand +# mode. As soon as we are not in on-demand mode the default-route handling is +# passed to the ip-up.d/ip-down.s scripts which is required for VRF support. +{% if 'auto' in default_route -%} +defaultroute +{% elif 'force' in default_route -%} +defaultroute +replacedefaultroute +{% endif %} +{% endif %} diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl new file mode 100644 index 000000000..cebfc54b5 --- /dev/null +++ b/data/templates/router-advert/radvd.conf.tmpl @@ -0,0 +1,47 @@ +### Autogenerated by service_router-advert.py ### + +{% if interface is defined and interface is not none %} +{% for iface in interface %} +interface {{ iface }} { + IgnoreIfMissing on; +{% if interface[iface].default_preference is defined and interface[iface].default_preference is not none %} + AdvDefaultPreference {{ interface[iface].default_preference }}; +{% endif %} +{% if interface[iface].managed_flag is defined and interface[iface].managed_flag is not none %} + AdvManagedFlag {{ 'on' if interface[iface].managed_flag is defined else 'off' }}; +{% endif %} +{% if interface[iface].interval.max is defined and interface[iface].interval.max is not none %} + MaxRtrAdvInterval {{ interface[iface].interval.max }}; +{% endif %} +{% if interface[iface].interval.min is defined and interface[iface].interval.min is not none %} + MinRtrAdvInterval {{ interface[iface].interval.min }}; +{% endif %} +{% if interface[iface].reachable_time is defined and interface[iface].reachable_time is not none %} + AdvReachableTime {{ interface[iface].reachable_time }}; +{% endif %} + AdvIntervalOpt {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; + AdvSendAdvert {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; +{% if interface[iface].default_lifetime is defined %} + AdvDefaultLifetime {{ interface[iface].default_lifetime }}; +{% endif %} +{% if interface[iface].link_mtu is defined %} + AdvLinkMTU {{ interface[iface].link_mtu }}; +{% endif %} + AdvOtherConfigFlag {{ 'on' if interface[iface].other_config_flag is defined else 'off' }}; + AdvRetransTimer {{ interface[iface].retrans_timer }}; + AdvCurHopLimit {{ interface[iface].hop_limit }}; +{% for prefix in interface[iface].prefix %} + prefix {{ prefix }} { + AdvAutonomous {{ 'off' if interface[iface].prefix[prefix].no_autonomous_flag is defined else 'on' }}; + AdvValidLifetime {{ interface[iface].prefix[prefix].valid_lifetime }}; + AdvOnLink {{ 'off' if interface[iface].prefix[prefix].no_on_link_flag is defined else 'on' }}; + AdvPreferredLifetime {{ interface[iface].prefix[prefix].preferred_lifetime }}; + }; +{% endfor %} +{% if interface[iface].name_server is defined %} + RDNSS {{ interface[iface].name_server | join(" ") }} { + }; +{% endif %} +}; +{% endfor -%} +{% endif %} diff --git a/data/templates/rsyslog/rsyslog.conf b/data/templates/rsyslog/rsyslog.conf new file mode 100644 index 000000000..ab60fc0f0 --- /dev/null +++ b/data/templates/rsyslog/rsyslog.conf @@ -0,0 +1,59 @@ +# /etc/rsyslog.conf Configuration file for rsyslog. +# + +################# +#### MODULES #### +################# + +$ModLoad imuxsock # provides support for local system logging +$ModLoad imklog # provides kernel logging support (previously done by rklogd) +#$ModLoad immark # provides --MARK-- message capability + +$OmitLocalLogging off +$SystemLogSocketName /run/systemd/journal/syslog + +$KLogPath /proc/kmsg + +# provides UDP syslog reception +#$ModLoad imudp +#$UDPServerRun 514 + +# provides TCP syslog reception +#$ModLoad imtcp +#$InputTCPServerRun 514 + +########################### +#### GLOBAL DIRECTIVES #### +########################### + +# +# Use traditional timestamp format. +# To enable high precision timestamps, comment out the following line. +# +$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat + +# Filter duplicated messages +$RepeatedMsgReduction on + +# +# Set the default permissions for all log files. +# +$FileOwner root +$FileGroup adm +$FileCreateMode 0640 +$DirCreateMode 0755 +$Umask 0022 + + +# +# Include all config files in /etc/rsyslog.d/ +# +$IncludeConfig /etc/rsyslog.d/*.conf + +############### +#### RULES #### +############### +# Emergencies are sent to everybody logged in. + +*.emerg :omusrmsg:* + diff --git a/data/templates/salt-minion/minion.tmpl b/data/templates/salt-minion/minion.tmpl new file mode 100644 index 000000000..9369573a4 --- /dev/null +++ b/data/templates/salt-minion/minion.tmpl @@ -0,0 +1,59 @@ +### Autogenerated by salt-minion.py ### + +##### Primary configuration settings ##### +########################################## + +# The hash_type is the hash to use when discovering the hash of a file on +# the master server. The default is sha256, but md5, sha1, sha224, sha384 and +# sha512 are also supported. +# +# WARNING: While md5 and sha1 are also supported, do not use them due to the +# high chance of possible collisions and thus security breach. +# +# Prior to changing this value, the master should be stopped and all Salt +# caches should be cleared. +hash_type: {{ hash }} + +##### Logging settings ##### +########################################## +# The location of the minion log file +# The minion log can be sent to a regular file, local path name, or network +# location. Remote logging works best when configured to use rsyslogd(8) (e.g.: +# ``file:///dev/log``), with rsyslogd(8) configured for network logging. The URI +# format is: <file|udp|tcp>://<host|socketpath>:<port-if-required>/<log-facility> +log_file: file:///dev/log + +# The level of messages to send to the console. +# One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. +# +# The following log levels are considered INSECURE and may log sensitive data: +# ['garbage', 'trace', 'debug'] +# +# Default: 'warning' +log_level: {{ log_level }} + +# Set the location of the salt master server, if the master server cannot be +# resolved, then the minion will fail to start. +master: +{% for host in master -%} +- {{ host }} +{% endfor %} + +# The user to run salt +user: {{ user }} + +# The directory to store the pki information in +pki_dir: /config/salt/pki/minion + +# Explicitly declare the id for this minion to use, if left commented the id +# will be the hostname as returned by the python call: socket.getfqdn() +# Since salt uses detached ids it is possible to run multiple minions on the +# same machine but with different ids, this can be useful for salt compute +# clusters. +id: {{ salt_id }} + + +# The number of minutes between mine updates. +mine_interval: {{ interval }} + +verify_master_pubkey_sign: {{ verify_master_pubkey_sign }} diff --git a/data/templates/snmp/etc.snmp.conf.tmpl b/data/templates/snmp/etc.snmp.conf.tmpl new file mode 100644 index 000000000..6e4c6f063 --- /dev/null +++ b/data/templates/snmp/etc.snmp.conf.tmpl @@ -0,0 +1,4 @@ +### Autogenerated by snmp.py ### +{% if trap_source %} +clientaddr {{ trap_source }} +{% endif %} diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl new file mode 100644 index 000000000..278506350 --- /dev/null +++ b/data/templates/snmp/etc.snmpd.conf.tmpl @@ -0,0 +1,115 @@ +### Autogenerated by snmp.py ### + +# non configurable defaults +sysObjectID 1.3.6.1.4.1.44641 +sysServices 14 +master agentx +agentXPerms 0777 0777 +pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias +smuxpeer .1.3.6.1.2.1.83 +smuxpeer .1.3.6.1.2.1.157 +smuxsocket localhost + +# linkUp/Down configure the Event MIB tables to monitor +# the ifTable for network interfaces being taken up or down +# for making internal queries to retrieve any necessary information +iquerySecName {{ vyos_user }} + +# Modified from the default linkUpDownNotification +# to include more OIDs and poll more frequently +notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus +notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus +monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2 +monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 + +######################## +# configurable section # +######################## + +# Default system description is VyOS version +sysDescr VyOS {{ version }} + +{% if description %} +# Description +SysDescr {{ description }} +{% endif %} + +# Listen +agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %} + +# SNMP communities +{% for c in communities %} +{% if c.network_v4 %} +{% for network in c.network_v4 %} +{{ c.authorization }}community {{ c.name }} {{ network }} +{% endfor %} +{% elif not c.has_source %} +{{ c.authorization }}community {{ c.name }} +{% endif %} +{% if c.network_v6 %} +{% for network in c.network_v6 %} +{{ c.authorization }}community6 {{ c.name }} {{ network }} +{% endfor %} +{% elif not c.has_source %} +{{ c.authorization }}community6 {{ c.name }} +{% endif %} +{% endfor %} + +{% if contact %} +# system contact information +SysContact {{ contact }} +{% endif %} + +{% if location %} +# system location information +SysLocation {{ location }} +{% endif %} + +{% if smux_peers %} +# additional smux peers +{% for sp in smux_peers %} +smuxpeer {{ sp }} +{% endfor %} +{% endif %} + +{% if trap_targets %} +# if there is a problem - tell someone! +{% for trap in trap_targets %} +trap2sink {{ trap.target }}{{ ":" + trap.port if trap.port is defined }} {{ trap.community }} +{% endfor %} +{% endif %} + +{% if v3_enabled %} +# +# SNMPv3 stuff goes here +# +# views +{% for view in v3_views %} +{% for oid in view.oids %} +view {{ view.name }} included .{{ oid.oid }} +{% endfor %} +{% endfor %} + +# access +# context sec.model sec.level match read write notif +{% for group in v3_groups %} +access {{ group.name }} "" usm {{ group.seclevel }} exact {{ group.view }} {% if group.mode == 'ro' %}none{% else %}{{ group.view }}{% endif %} none +{% endfor %} + +# trap-target +{% for t in v3_traps %} +trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} +{% endfor %} + +# group +{% for u in v3_users %} +group {{ u.group }} usm {{ u.name }} +{% endfor %} +{% endif %} + +{% if script_ext %} +# extension scripts +{% for ext in script_ext|sort(attribute='name') %} +extend {{ ext.name }} {{ ext.script }} +{% endfor %} +{% endif %} diff --git a/data/templates/snmp/override.conf.tmpl b/data/templates/snmp/override.conf.tmpl new file mode 100644 index 000000000..e6302a9e1 --- /dev/null +++ b/data/templates/snmp/override.conf.tmpl @@ -0,0 +1,13 @@ +{% set vrf_command = '/sbin/ip vrf exec ' + vrf + ' ' if vrf is defined else '' %} +[Unit] +StartLimitIntervalSec=0 +After=vyos-router.service + +[Service] +Environment= +Environment="MIBSDIR=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/mibs/site:/usr/share/snmp/mibs:/usr/share/mibs/iana:/usr/share/mibs/ietf:/usr/share/mibs/netsnmp" +ExecStart= +ExecStart={{vrf_command}}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp -I -ipCidrRouteTable,inetCidrRouteTable -f -p /run/snmpd.pid +Restart=on-failure +RestartSec=10 + diff --git a/data/templates/snmp/usr.snmpd.conf.tmpl b/data/templates/snmp/usr.snmpd.conf.tmpl new file mode 100644 index 000000000..9c0337fa8 --- /dev/null +++ b/data/templates/snmp/usr.snmpd.conf.tmpl @@ -0,0 +1,6 @@ +### Autogenerated by snmp.py ### +{%- for u in v3_users %} +{{ u.mode }}user {{ u.name }} +{%- endfor %} + +rwuser {{ vyos_user }} diff --git a/data/templates/snmp/var.snmpd.conf.tmpl b/data/templates/snmp/var.snmpd.conf.tmpl new file mode 100644 index 000000000..6cbc687ef --- /dev/null +++ b/data/templates/snmp/var.snmpd.conf.tmpl @@ -0,0 +1,14 @@ +### Autogenerated by snmp.py ### +# user +{%- for u in v3_users %} +{%- if u.authOID == 'none' %} +createUser {{ u.name }} +{%- else %} +usmUser 1 3 0x{{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} 0x{{ u.authMasterKey }} {{ u.privOID }} 0x{{ u.privMasterKey }} 0x +{%- endif %} +{%- endfor %} + +createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +{%- if v3_engineid %} +oldEngineID 0x{{ v3_engineid }} +{%- endif %} diff --git a/data/templates/ssh/override.conf.tmpl b/data/templates/ssh/override.conf.tmpl new file mode 100644 index 000000000..843aa927b --- /dev/null +++ b/data/templates/ssh/override.conf.tmpl @@ -0,0 +1,11 @@ +{% set vrf_command = '/sbin/ip vrf exec ' + vrf + ' ' if vrf is defined else '' %} +[Unit] +StartLimitIntervalSec=0 +After=vyos-router.service +ConditionPathExists={{config_file}} + +[Service] +ExecStart= +ExecStart={{vrf_command}}/usr/sbin/sshd -f {{config_file}} -D $SSHD_OPTS +RestartSec=10 + diff --git a/data/templates/ssh/sshd_config.tmpl b/data/templates/ssh/sshd_config.tmpl new file mode 100644 index 000000000..4fde24255 --- /dev/null +++ b/data/templates/ssh/sshd_config.tmpl @@ -0,0 +1,114 @@ +### Autogenerated by ssh.py ### + +# https://linux.die.net/man/5/sshd_config + +# +# Non-configurable defaults +# +Protocol 2 +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key +SyslogFacility AUTH +LoginGraceTime 120 +StrictModes yes +PubkeyAuthentication yes +IgnoreRhosts yes +HostbasedAuthentication no +PermitEmptyPasswords no +ChallengeResponseAuthentication no +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes +Banner /etc/issue.net +Subsystem sftp /usr/lib/openssh/sftp-server +UsePAM yes +PermitRootLogin no + +# +# User configurable section +# + +# Look up remote host name and check that the resolved host name for the remote IP +# address maps back to the very same IP address. +UseDNS {{ "no" if disable_host_validation is defined else "yes" }} + +# Specifies the port number that sshd(8) listens on +{% if port is string %} +Port {{ port }} +{% else %} +{% for value in port %} +Port {{ value }} +{% endfor %} +{% endif %} + +# Gives the verbosity level that is used when logging messages from sshd +LogLevel {{ loglevel | upper }} + +# Specifies whether password authentication is allowed +PasswordAuthentication {{ "no" if disable_password_authentication is defined else "yes" }} + +{% if listen_address %} +# Specifies the local addresses sshd should listen on +{% if listen_address is string %} +ListenAddress {{ listen_address }} +{% else %} +{% for address in listen_address %} +ListenAddress {{ address }} +{% endfor %} +{% endif %} +{% endif %} + +{% if ciphers %} +# Specifies the ciphers allowed for protocol version 2 +{% set value = ciphers if ciphers is string else ciphers | join(',') %} +Ciphers {{ value }} +{% endif %} + +{% if mac %} +# Specifies the available MAC (message authentication code) algorithms +{% set value = mac if mac is string else mac | join(',') %} +MACs {{ value }} +{% endif %} + +{% if key_exchange %} +# Specifies the available Key Exchange algorithms +{% set value = key_exchange if key_exchange is string else key_exchange | join(',') %} +KexAlgorithms {{ value }} +{% endif %} + +{% if access_control is defined %} +{% if access_control.allow is defined %} +{% if access_control.allow.user is defined %} +# If specified, login is allowed only for user names that match +{% set value = access_control.allow.user if access_control.allow.user is string else access_control.allow.user | join(' ') %} +AllowUsers {{ value }} +{% endif %} +{% if access_control.allow.group is defined %} +# If specified, login is allowed only for users whose primary group or supplementary group list matches +{% set value = access_control.allow.group if access_control.allow.group is string else access_control.allow.group | join(' ') %} +AllowGroups {{ value }} +{% endif %} +{% endif %} +{% if access_control.deny is defined %} +{% if access_control.deny.user is defined %} +# Login is disallowed for user names that match +{% set value = access_control.deny.user if access_control.deny.user is string else access_control.deny.user | join(' ') %} +DenyUsers {{ value }} +{% endif %} +{% if access_control.deny.group is defined %} +# Login is disallowed for users whose primary group or supplementary group list matches +{% set value = access_control.deny.group if access_control.deny.group is string else access_control.deny.group | join(' ') %} +DenyGroups {{ value }} +{% endif %} +{% endif %} +{% endif %} + +{% if client_keepalive_interval %} +# Sets a timeout interval in seconds after which if no data has been received from the client, +# sshd(8) will send a message through the encrypted channel to request a response from the client +ClientAliveInterval {{ client_keepalive_interval }} +{% endif %} diff --git a/data/templates/syslog/logrotate.tmpl b/data/templates/syslog/logrotate.tmpl new file mode 100644 index 000000000..f758265e4 --- /dev/null +++ b/data/templates/syslog/logrotate.tmpl @@ -0,0 +1,12 @@ +{% for file in files %} +{{files[file]['log-file']}} { + missingok + notifempty + create + rotate {{files[file]['max-files']}} + size={{files[file]['max-size']//1024}}k + postrotate + invoke-rc.d rsyslog rotate > /dev/null + endscript +} +{% endfor %} diff --git a/data/templates/syslog/rsyslog.conf.tmpl b/data/templates/syslog/rsyslog.conf.tmpl new file mode 100644 index 000000000..bc3f7667b --- /dev/null +++ b/data/templates/syslog/rsyslog.conf.tmpl @@ -0,0 +1,44 @@ +## generated by syslog.py ## +## file based logging +{% if files['global']['marker'] -%} +$ModLoad immark +{% if files['global']['marker-interval'] %} +$MarkMessagePeriod {{files['global']['marker-interval']}} +{% endif %} +{% endif -%} +{% if files['global']['preserver_fqdn'] -%} +$PreserveFQDN on +{% endif -%} +{% for file in files %} +$outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}} +{{files[file]['selectors']}} :omfile:${{file}} +{% endfor %} +{% if console %} +## console logging +{% for con in console %} +{{console[con]['selectors']}} /dev/console +{% endfor %} +{% endif %} +{% if hosts %} +## remote logging +{% for host in hosts %} +{% if hosts[host]['proto'] == 'tcp' %} +{% if hosts[host]['port'] %} +{{hosts[host]['selectors']}} @@{{host}}:{{hosts[host]['port']}} +{% else %} +{{hosts[host]['selectors']}} @@{{host}} +{% endif %} +{% else %} +{% if hosts[host]['port'] %} +{{hosts[host]['selectors']}} @{{host}}:{{hosts[host]['port']}} +{% else %} +{{hosts[host]['selectors']}} @{{host}} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} +{% if user %} +{% for u in user %} +{{user[u]['selectors']}} :omusrmsg:{{u}} +{% endfor %} +{% endif %} diff --git a/data/templates/system-login/pam_radius_auth.conf.tmpl b/data/templates/system-login/pam_radius_auth.conf.tmpl new file mode 100644 index 000000000..ec2d6df95 --- /dev/null +++ b/data/templates/system-login/pam_radius_auth.conf.tmpl @@ -0,0 +1,16 @@ +# Automatically generated by system-login.py +# RADIUS configuration file +{% if radius_server %} +# server[:port] shared_secret timeout source_ip +{% for s in radius_server|sort(attribute='priority') if not s.disabled %} +{% set addr_port = s.address + ":" + s.port %} +{{ "%-22s" | format(addr_port) }} {{ "%-25s" | format(s.key) }} {{ "%-10s" | format(s.timeout) }} {{ radius_source_address if radius_source_address }} +{% endfor %} + +priv-lvl 15 +mapped_priv_user radius_priv_user + +{% if radius_vrf %} +vrf-name {{ radius_vrf }} +{% endif %} +{% endif %} diff --git a/data/templates/system/curlrc.tmpl b/data/templates/system/curlrc.tmpl new file mode 100644 index 000000000..3e5ce801c --- /dev/null +++ b/data/templates/system/curlrc.tmpl @@ -0,0 +1,8 @@ +{% if http_client is defined %} +{% if http_client.source_interface is defined %} +--interface "{{ http_client.source_interface }}" +{% endif %} +{% if http_client.source_address is defined %} +--interface "{{ http_client.source_address }}" +{% endif %} +{% endif %} diff --git a/data/templates/system/ssh_config.tmpl b/data/templates/system/ssh_config.tmpl new file mode 100644 index 000000000..509bd5479 --- /dev/null +++ b/data/templates/system/ssh_config.tmpl @@ -0,0 +1,3 @@ +{% if ssh_client is defined and ssh_client.source_address is defined and ssh_client.source_address is not none %}
+BindAddress {{ ssh_client.source_address }}
+{% endif %}
diff --git a/data/templates/tftp-server/default.tmpl b/data/templates/tftp-server/default.tmpl new file mode 100644 index 000000000..18fee35d1 --- /dev/null +++ b/data/templates/tftp-server/default.tmpl @@ -0,0 +1,2 @@ +### Autogenerated by tftp_server.py ### +DAEMON_ARGS="--listen --user tftp --address {% for a in listen-%}{{ a }}{% endfor %}{% if allow_upload %} --create --umask 000{% endif %} --secure {{ directory }}" diff --git a/data/templates/vrf/vrf.conf.tmpl b/data/templates/vrf/vrf.conf.tmpl new file mode 100644 index 000000000..761b0bb6f --- /dev/null +++ b/data/templates/vrf/vrf.conf.tmpl @@ -0,0 +1,8 @@ +### Autogenerated by vrf.py ### +# +# Routing table ID to name mapping reference + +# id vrf name comment +{% for vrf in vrf_add -%} +{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} +{% endfor -%} diff --git a/data/templates/vrrp/daemon.tmpl b/data/templates/vrrp/daemon.tmpl new file mode 100644 index 000000000..c9dbea72d --- /dev/null +++ b/data/templates/vrrp/daemon.tmpl @@ -0,0 +1,5 @@ +# Autogenerated by VyOS +# Options to pass to keepalived + +# DAEMON_ARGS are appended to the keepalived command-line +DAEMON_ARGS="--snmp" diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl new file mode 100644 index 000000000..08b821f70 --- /dev/null +++ b/data/templates/vrrp/keepalived.conf.tmpl @@ -0,0 +1,97 @@ +# Autogenerated by VyOS +# Do not edit this file, all your changes will be lost +# on next commit or reboot + +global_defs { + dynamic_interfaces + script_user root + notify_fifo /run/keepalived_notify_fifo + notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py +} + +{% for group in groups -%} + +{% if group.health_check_script -%} +vrrp_script healthcheck_{{ group.name }} { + script "{{ group.health_check_script }}" + interval {{ group.health_check_interval }} + fall {{ group.health_check_count }} + rise 1 + +} +{% endif %} + +vrrp_instance {{ group.name }} { + {% if group.description -%} + # {{ group.description }} + {% endif -%} + + state BACKUP + interface {{ group.interface }} + virtual_router_id {{ group.vrid }} + priority {{ group.priority }} + advert_int {{ group.advertise_interval }} + + {% if group.preempt -%} + preempt_delay {{ group.preempt_delay }} + {% else -%} + nopreempt + {% endif -%} + + {% if group.peer_address -%} + unicast_peer { {{ group.peer_address }} } + {% endif -%} + + {% if group.hello_source -%} + {%- if group.peer_address -%} + unicast_src_ip {{ group.hello_source }} + {%- else -%} + mcast_src_ip {{ group.hello_source }} + {%- endif %} + {% endif -%} + + {% if group.use_vmac and group.peer_address -%} + use_vmac {{group.interface}}v{{group.vrid}} + vmac_xmit_base + {% elif group.use_vmac -%} + use_vmac {{group.interface}}v{{group.vrid}} + {% endif -%} + + {% if group.auth_password -%} + authentication { + auth_pass "{{ group.auth_password }}" + auth_type {{ group.auth_type }} + } + {% endif -%} + + virtual_ipaddress { + {% for addr in group.virtual_addresses -%} + {{ addr }} + {% endfor -%} + } + + {% if group.health_check_script -%} + track_script { + healthcheck_{{ group.name }} + } + {% endif -%} +} + +{% endfor -%} + +{% for sync_group in sync_groups -%} +vrrp_sync_group {{ sync_group.name }} { + group { + {% for member in sync_group.members -%} + {{ member }} + {% endfor -%} + } + + {% if sync_group.conntrack_sync -%} + notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}" + notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}" + notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}" + {% endif -%} +} + +{% endfor -%} diff --git a/data/templates/vyos-hostsd/hosts.tmpl b/data/templates/vyos-hostsd/hosts.tmpl new file mode 100644 index 000000000..566f9a5dd --- /dev/null +++ b/data/templates/vyos-hostsd/hosts.tmpl @@ -0,0 +1,26 @@ +### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +# Local host +127.0.0.1 localhost +127.0.1.1 {{ host_name }}{% if domain_name %}.{{ domain_name }} {{ host_name }}{% endif %} + +# The following lines are desirable for IPv6 capable hosts +::1 localhost ip6-localhost ip6-loopback +fe00::0 ip6-localnet +ff00::0 ip6-mcastprefix +ff02::1 ip6-allnodes +ff02::2 ip6-allrouters + +{% if hosts -%} +# From 'system static-host-mapping' and DHCP server +{%- for tag, taghosts in hosts.items() %} +# {{ tag }} +{%- for host, hostprops in taghosts.items() %} +{%- if hostprops['address'] %} +{{ hostprops['address'] }} {{ host }}{% for a in hostprops['aliases'] %} {{ a }}{% endfor %} +{%- endif %} +{%- endfor %} +{%- endfor %} +{%- endif %} + diff --git a/data/templates/vyos-hostsd/resolv.conf.tmpl b/data/templates/vyos-hostsd/resolv.conf.tmpl new file mode 100644 index 000000000..b920b2e5f --- /dev/null +++ b/data/templates/vyos-hostsd/resolv.conf.tmpl @@ -0,0 +1,26 @@ +### Autogenerated by VyOS ### +### Do not edit, your changes will get overwritten ### + +{#- the code below ensures the order of nameservers is determined first by #} +{# the order of tags, then by the order of nameservers within that tag #} + +{%- for tag in name_server_tags_system %} +{%- if tag in name_servers %} +# {{ tag }} +{%- for ns in name_servers[tag] %} +nameserver {{ ns }} +{%- endfor %} +{%- endif %} +{%- endfor %} + +{%- if domain_name %} +domain {{ domain_name }} +{%- endif %} + +{% for tag in name_server_tags_system %} +{%- if tag in search_domains %} +# {{ tag }} +search {{ search_domains[tag]|join(' ') }} +{%- endif %} +{%- endfor %} + diff --git a/data/templates/wifi/cfg80211.conf.tmpl b/data/templates/wifi/cfg80211.conf.tmpl new file mode 100644 index 000000000..91df57aab --- /dev/null +++ b/data/templates/wifi/cfg80211.conf.tmpl @@ -0,0 +1 @@ +{{ 'options cfg80211 ieee80211_regdom=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/crda.tmpl b/data/templates/wifi/crda.tmpl new file mode 100644 index 000000000..6cd125e37 --- /dev/null +++ b/data/templates/wifi/crda.tmpl @@ -0,0 +1 @@ +{{ 'REGDOMAIN=' + regdom if regdom is defined }} diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl new file mode 100644 index 000000000..765668c57 --- /dev/null +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -0,0 +1,687 @@ +### Autogenerated by interfaces-wireless.py ### +{% if description %} +# Description: {{ description }} +# User-friendly description of device; up to 32 octets encoded in UTF-8 +device_name={{ description | truncate(32, True) }} +{% endif %} + +# AP netdevice name (without 'ap' postfix, i.e., wlan0 uses wlan0ap for +# management frames with the Host AP driver); wlan0 with many nl80211 drivers +# Note: This attribute can be overridden by the values supplied with the '-i' +# command line parameter. +interface={{ ifname }} + +# Driver interface type (hostap/wired/none/nl80211/bsd); +# default: hostap). nl80211 is used with all Linux mac80211 drivers. +# Use driver=none if building hostapd as a standalone RADIUS server that does +# not control any wireless/wired driver. +driver=nl80211 + +# Levels (minimum value for logged events): +# 0 = verbose debugging +# 1 = debugging +# 2 = informational messages +# 3 = notification +# 4 = warning +logger_syslog=-1 +logger_syslog_level=0 +logger_stdout=-1 +logger_stdout_level=0 + +{% if country_code %} +# Country code (ISO/IEC 3166-1). Used to set regulatory domain. +# Set as needed to indicate country in which device is operating. +# This can limit available channels and transmit power. +country_code={{ country_code }} + +# Enable IEEE 802.11d. This advertises the country_code and the set of allowed +# channels and transmit power levels based on the regulatory limits. The +# country_code setting must be configured with the correct country for +# IEEE 802.11d functions. +ieee80211d=1 +{% endif %} + +{% if ssid %} +# SSID to be used in IEEE 802.11 management frames +ssid={{ ssid }} +{% endif %} + +{% if channel %} +# Channel number (IEEE 802.11) +# (default: 0, i.e., not set) +# Please note that some drivers do not use this value from hostapd and the +# channel will need to be configured separately with iwconfig. +# +# If CONFIG_ACS build option is enabled, the channel can be selected +# automatically at run time by setting channel=acs_survey or channel=0, both of +# which will enable the ACS survey based algorithm. +channel={{ channel }} +{% endif %} + +{% if mode %} +# Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz), +# g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used +# with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this +# needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs +# to be set to hw_mode=a. When using ACS (see channel parameter), a +# special value "any" can be used to indicate that any support band can be used. +# This special case is currently supported only with drivers with which +# offloaded ACS is used. +{% if 'n' in mode %} +hw_mode=g +{% elif 'ac' in mode %} +hw_mode=a +ieee80211h=1 +ieee80211ac=1 +{% else %} +hw_mode={{ mode }} +{% endif %} +{% endif %} + +# ieee80211w: Whether management frame protection (MFP) is enabled +# 0 = disabled (default) +# 1 = optional +# 2 = required +{% if 'disabled' in mgmt_frame_protection %} +ieee80211w=0 +{% elif 'optional' in mgmt_frame_protection %} +ieee80211w=1 +{% elif 'required' in mgmt_frame_protection %} +ieee80211w=2 +{% endif %} + +{% if capabilities is defined and capabilities.ht is defined %} +# ht_capab: HT capabilities (list of flags) +# LDPC coding capability: [LDPC] = supported +# Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary +# channel below the primary channel; [HT40+] = both 20 MHz and 40 MHz +# with secondary channel above the primary channel +# (20 MHz only if neither is set) +# Note: There are limits on which channels can be used with HT40- and +# HT40+. Following table shows the channels that may be available for +# HT40- and HT40+ use per IEEE 802.11n Annex J: +# freq HT40- HT40+ +# 2.4 GHz 5-13 1-7 (1-9 in Europe/Japan) +# 5 GHz 40,48,56,64 36,44,52,60 +# (depending on the location, not all of these channels may be available +# for use) +# Please note that 40 MHz channels may switch their primary and secondary +# channels if needed or creation of 40 MHz channel maybe rejected based +# on overlapping BSSes. These changes are done automatically when hostapd +# is setting up the 40 MHz channel. +# Spatial Multiplexing (SM) Power Save: [SMPS-STATIC] or [SMPS-DYNAMIC] +# (SMPS disabled if neither is set) +# HT-greenfield: [GF] (disabled if not set) +# Short GI for 20 MHz: [SHORT-GI-20] (disabled if not set) +# Short GI for 40 MHz: [SHORT-GI-40] (disabled if not set) +# Tx STBC: [TX-STBC] (disabled if not set) +# Rx STBC: [RX-STBC1] (one spatial stream), [RX-STBC12] (one or two spatial +# streams), or [RX-STBC123] (one, two, or three spatial streams); Rx STBC +# disabled if none of these set +# HT-delayed Block Ack: [DELAYED-BA] (disabled if not set) +# Maximum A-MSDU length: [MAX-AMSDU-7935] for 7935 octets (3839 octets if not +# set) +# DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) +# 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) +# L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) +{% set output = '' %} +{% set output = output + '[40-INTOLERANT]' if capabilities.ht.fourtymhz_incapable is defined else '' %} +{% set output = output + '[DELAYED-BA]' if capabilities.ht.delayed_block_ack is defined else '' %} +{% set output = output + '[DSSS_CCK-40]' if capabilities.ht.dsss_cck_40 is defined else '' %} +{% set output = output + '[GF]' if capabilities.ht.greenfield is defined else '' %} +{% set output = output + '[LDPC]' if capabilities.ht.ldpc is defined else '' %} +{% set output = output + '[LSIG-TXOP-PROT]' if capabilities.ht.lsig_protection is defined else '' %} +{% set output = output + '[TX-STBC]' if capabilities.ht.stbc.tx is defined else '' %} +{% set output = output + '[RX-STBC-' + capabilities.ht.stbc.rx | upper + ']' if capabilities.ht.stbc.tx is defined else '' %} +{% set output = output + '[MAX-AMSDU-' + capabilities.ht.max_amsdu + ']' if capabilities.ht.max_amsdu is defined else '' %} +{% set output = output + '[SMPS-' + capabilities.ht.smps | upper + ']' if capabilities.ht.smps is defined else '' %} + +{% if capabilities.ht.channel_set_width is defined %} +{% for csw in capabilities.ht.channel_set_width %} +{% set output = output + '[' + csw | upper + ']' %} +{% endfor %} +{% endif %} + +{% if capabilities.ht.short_gi is defined %} +{% for short_gi in capabilities.ht.short_gi %} +{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} +{% endfor %} +{% endif %} + +ht_capab={{ output }} + +{% if capabilities.ht.auto_powersave is defined %} +# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] +# Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) +uapsd_advertisement_enabled=1 +{% endif %} + +{% endif %} + +# Required for full HT and VHT functionality +wme_enabled=1 + + +{% if capabilities is defined and capabilities.require_ht is defined %} +# Require stations to support HT PHY (reject association if they do not) +require_ht=1 +{% endif %} + +{% if capabilities is defined and capabilities.vht is defined %} +# vht_capab: VHT capabilities (list of flags) +# +# vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] +# Indicates maximum MPDU length +# 0 = 3895 octets (default) +# 1 = 7991 octets +# 2 = 11454 octets +# 3 = reserved +# +# supported_chan_width: [VHT160] [VHT160-80PLUS80] +# Indicates supported Channel widths +# 0 = 160 MHz & 80+80 channel widths are not supported (default) +# 1 = 160 MHz channel width is supported +# 2 = 160 MHz & 80+80 channel widths are supported +# 3 = reserved +# +# Rx LDPC coding capability: [RXLDPC] +# Indicates support for receiving LDPC coded pkts +# 0 = Not supported (default) +# 1 = Supported +# +# Short GI for 80 MHz: [SHORT-GI-80] +# Indicates short GI support for reception of packets transmitted with TXVECTOR +# params format equal to VHT and CBW = 80Mhz +# 0 = Not supported (default) +# 1 = Supported +# +# Short GI for 160 MHz: [SHORT-GI-160] +# Indicates short GI support for reception of packets transmitted with TXVECTOR +# params format equal to VHT and CBW = 160Mhz +# 0 = Not supported (default) +# 1 = Supported +# +# Tx STBC: [TX-STBC-2BY1] +# Indicates support for the transmission of at least 2x1 STBC +# 0 = Not supported (default) +# 1 = Supported +# +# Rx STBC: [RX-STBC-1] [RX-STBC-12] [RX-STBC-123] [RX-STBC-1234] +# Indicates support for the reception of PPDUs using STBC +# 0 = Not supported (default) +# 1 = support of one spatial stream +# 2 = support of one and two spatial streams +# 3 = support of one, two and three spatial streams +# 4 = support of one, two, three and four spatial streams +# 5,6,7 = reserved +# +# SU Beamformer Capable: [SU-BEAMFORMER] +# Indicates support for operation as a single user beamformer +# 0 = Not supported (default) +# 1 = Supported +# +# SU Beamformee Capable: [SU-BEAMFORMEE] +# Indicates support for operation as a single user beamformee +# 0 = Not supported (default) +# 1 = Supported +# +# Compressed Steering Number of Beamformer Antennas Supported: +# [BF-ANTENNA-2] [BF-ANTENNA-3] [BF-ANTENNA-4] +# Beamformee's capability indicating the maximum number of beamformer +# antennas the beamformee can support when sending compressed beamforming +# feedback +# If SU beamformer capable, set to maximum value minus 1 +# else reserved (default) +# +# Number of Sounding Dimensions: +# [SOUNDING-DIMENSION-2] [SOUNDING-DIMENSION-3] [SOUNDING-DIMENSION-4] +# Beamformer's capability indicating the maximum value of the NUM_STS parameter +# in the TXVECTOR of a VHT NDP +# If SU beamformer capable, set to maximum value minus 1 +# else reserved (default) +# +# MU Beamformer Capable: [MU-BEAMFORMER] +# Indicates support for operation as an MU beamformer +# 0 = Not supported or sent by Non-AP STA (default) +# 1 = Supported +# +# VHT TXOP PS: [VHT-TXOP-PS] +# Indicates whether or not the AP supports VHT TXOP Power Save Mode +# or whether or not the STA is in VHT TXOP Power Save mode +# 0 = VHT AP doesn't support VHT TXOP PS mode (OR) VHT STA not in VHT TXOP PS +# mode +# 1 = VHT AP supports VHT TXOP PS mode (OR) VHT STA is in VHT TXOP power save +# mode +# +# +HTC-VHT Capable: [HTC-VHT] +# Indicates whether or not the STA supports receiving a VHT variant HT Control +# field. +# 0 = Not supported (default) +# 1 = supported +# +# Maximum A-MPDU Length Exponent: [MAX-A-MPDU-LEN-EXP0]..[MAX-A-MPDU-LEN-EXP7] +# Indicates the maximum length of A-MPDU pre-EOF padding that the STA can recv +# This field is an integer in the range of 0 to 7. +# The length defined by this field is equal to +# 2 pow(13 + Maximum A-MPDU Length Exponent) -1 octets +# +# VHT Link Adaptation Capable: [VHT-LINK-ADAPT2] [VHT-LINK-ADAPT3] +# Indicates whether or not the STA supports link adaptation using VHT variant +# HT Control field +# If +HTC-VHTcapable is 1 +# 0 = (no feedback) if the STA does not provide VHT MFB (default) +# 1 = reserved +# 2 = (Unsolicited) if the STA provides only unsolicited VHT MFB +# 3 = (Both) if the STA can provide VHT MFB in response to VHT MRQ and if the +# STA provides unsolicited VHT MFB +# Reserved if +HTC-VHTcapable is 0 +# +# Rx Antenna Pattern Consistency: [RX-ANTENNA-PATTERN] +# Indicates the possibility of Rx antenna pattern change +# 0 = Rx antenna pattern might change during the lifetime of an association +# 1 = Rx antenna pattern does not change during the lifetime of an association +# +# Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN] +# Indicates the possibility of Tx antenna pattern change +# 0 = Tx antenna pattern might change during the lifetime of an association +# 1 = Tx antenna pattern does not change during the lifetime of an + +{% if capabilities.vht.center_channel_freq.freq_1 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 42 gives center freq 5.210 GHz +# which is channel 42 in 5G band +vht_oper_centr_freq_seg0_idx={{ capabilities.vht.center_channel_freq.freq_1 }} +{% endif %} + +{% if capabilities.vht.center_channel_freq.freq_2 is defined %} +# center freq = 5 GHz + (5 * index) +# So index 159 gives center freq 5.795 GHz +# which is channel 159 in 5G band +vht_oper_centr_freq_seg1_idx={{ capabilities.vht.center_channel_freq.freq_2 }} +{% endif %} + +{% if capabilities.vht.channel_set_width is defined %} +vht_oper_chwidth={{ capabilities.vht.channel_set_width }} +{% endif %} + +{% set output = '' %} +{% set output = output + '[TX-STBC-2BY1]' if capabilities.vht.stbc.tx is defined else '' %} +{% set output = output + '[RXLDPC]' if capabilities.vht.ldpc is defined else '' %} +{% set output = output + '[VHT-TXOP-PS]' if capabilities.vht.tx_powersave is defined else '' %} +{% set output = output + '[HTC-VHT]' if capabilities.vht.vht_cf is defined else '' %} +{% set output = output + '[RX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} +{% set output = output + '[TX-ANTENNA-PATTERN]' if capabilities.vht.antenna_pattern_fixed is defined else '' %} + +{% set output = output + '[RX-STBC-' + capabilities.vht.stbc.rx + ']' if capabilities.vht.stbc.rx is defined else '' %} +{% set output = output + '[MAX-MPDU-' + capabilities.vht.max_mpdu + ']' if capabilities.vht.max_mpdu is defined else '' %} +{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} +{% set output = output + '[MAX-A-MPDU-LEN-EXP-' + capabilities.vht.max_mpdu_exp + ']' if capabilities.vht.max_mpdu_exp is defined else '' %} + +{% set output = output + '[VHT160]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '2' else '' %} +{% set output = output + '[VHT160-80PLUS80]' if capabilities.vht.max_mpdu_exp is defined and capabilities.vht.max_mpdu_exp == '3' else '' %} +{% set output = output + '[VHT-LINK-ADAPT2]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'unsolicited' else '' %} +{% set output = output + '[VHT-LINK-ADAPT3]' if capabilities.vht.link_adaptation is defined and capabilities.vht.link_adaptation == 'both' else '' %} + +{% if capabilities.vht.short_gi is defined %} +{% for short_gi in capabilities.vht.short_gi %} +{% set output = output + '[SHORT-GI-' + short_gi | upper + ']' %} +{% endfor %} +{% endif %} + +{% if capabilities.vht.beamform %} +{% for beamform in capabilities.vht.beamform %} +{% set output = output + '[SU-BEAMFORMER]' if beamform == 'single-user-beamformer' else '' %} +{% set output = output + '[SU-BEAMFORMEE]' if beamform == 'single-user-beamformee' else '' %} +{% set output = output + '[MU-BEAMFORMER]' if beamform == 'multi-user-beamformer' else '' %} +{% set output = output + '[MU-BEAMFORMEE]' if beamform == 'multi-user-beamformee' else '' %} +{% endfor %} +{% endif %} + +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 %} +{% if capabilities.vht.beamform %} +{% if beamform == 'single-user-beamformer' %} +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 6 %} +{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count|int -1 + ']' %} +{% endif %} +{% endif %} +{% if capabilities.vht.antenna_count is defined and capabilities.vht.antenna_count|int > 1 and capabilities.vht.antenna_count|int < 5 %} +{% set output = output + '[BF-ANTENNA-' + capabilities.vht.antenna_count + ']' %} +{% set output = output + '[SOUNDING-DIMENSION-' + capabilities.vht.antenna_count+ ']' %} +{% endif %} +{% endif %} +{% endif %} + +vht_capab={{ output }} +{% endif %} + +# ieee80211n: Whether IEEE 802.11n (HT) is enabled +# 0 = disabled (default) +# 1 = enabled +# Note: You will also need to enable WMM for full HT functionality. +# Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. +{% if capabilities is defined and capabilities.require_vht is defined %} +ieee80211n=0 +# Require stations to support VHT PHY (reject association if they do not) +require_vht=1 +{% else %} +{% if 'n' in mode or 'ac' in mode %} +ieee80211n=1 +{% else %} +ieee80211n=0 +{% endif %} +{% endif %} + +{% if disable_broadcast_ssid is defined %} +# Send empty SSID in beacons and ignore probe request frames that do not +# specify full SSID, i.e., require stations to know SSID. +# default: disabled (0) +# 1 = send empty (length=0) SSID in beacon and ignore probe request for +# broadcast SSID +# 2 = clear SSID (ASCII 0), but keep the original length (this may be required +# with some clients that do not support empty SSID) and ignore probe +# requests for broadcast SSID +ignore_broadcast_ssid=1 +{% endif %} + +# Station MAC address -based authentication +# Please note that this kind of access control requires a driver that uses +# hostapd to take care of management frame processing and as such, this can be +# used with driver=hostap or driver=nl80211, but not with driver=atheros. +# 0 = accept unless in deny list +# 1 = deny unless in accept list +# 2 = use external RADIUS server (accept/deny lists are searched first) +macaddr_acl=0 + +{% if max_stations is defined %} +# Maximum number of stations allowed in station table. New stations will be +# rejected after the station table is full. IEEE 802.11 has a limit of 2007 +# different association IDs, so this number should not be larger than that. +# (default: 2007) +max_num_sta={{ max_stations }} +{% endif %} + +{% if isolate_stations is defined %} +# Client isolation can be used to prevent low-level bridging of frames between +# associated stations in the BSS. By default, this bridging is allowed. +ap_isolate=1 +{% endif %} + +{% if reduce_transmit_power is defined %} +# Add Power Constraint element to Beacon and Probe Response frames +# This config option adds Power Constraint element when applicable and Country +# element is added. Power Constraint element is required by Transmit Power +# Control. This can be used only with ieee80211d=1. +# Valid values are 0..255. +local_pwr_constraint={{ reduce_transmit_power }} +{% endif %} + +{% if expunge_failing_stations is defined %} +# Disassociate stations based on excessive transmission failures or other +# indications of connection loss. This depends on the driver capabilities and +# may not be available with all drivers. +disassoc_low_ack=1 +{% endif %} + + +{% if security is defined and security.wep is defined %} +# IEEE 802.11 specifies two authentication algorithms. hostapd can be +# configured to allow both of these or only one. Open system authentication +# should be used with IEEE 802.1X. +# Bit fields of allowed authentication algorithms: +# bit 0 = Open System Authentication +# bit 1 = Shared Key Authentication (requires WEP) +auth_algs=2 + +# WEP rekeying (disabled if key lengths are not set or are set to 0) +# Key lengths for default/broadcast and individual/unicast keys: +# 5 = 40-bit WEP (also known as 64-bit WEP with 40 secret bits) +# 13 = 104-bit WEP (also known as 128-bit WEP with 104 secret bits) +wep_key_len_broadcast=5 +wep_key_len_unicast=5 + +# Static WEP key configuration +# +# The key number to use when transmitting. +# It must be between 0 and 3, and the corresponding key must be set. +# default: not set +wep_default_key=0 + +# The WEP keys to use. +# A key may be a quoted string or unquoted hexadecimal digits. +# The key length should be 5, 13, or 16 characters, or 10, 26, or 32 +# digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or +# 128-bit (152-bit) WEP is used. +# Only the default key must be supplied; the others are optional. +{% if security.wep.key is defined %} +{% for key in sec_wep_key %} +wep_key{{ loop.index -1 }}={{ security.wep.key }} +{% endfor %} +{% endif %} + + +{% elif security is defined and security.wpa is defined %} +##### WPA/IEEE 802.11i configuration ########################################## + +# Enable WPA. Setting this variable configures the AP to require WPA (either +# WPA-PSK or WPA-RADIUS/EAP based on other configuration). For WPA-PSK, either +# wpa_psk or wpa_passphrase must be set and wpa_key_mgmt must include WPA-PSK. +# Instead of wpa_psk / wpa_passphrase, wpa_psk_radius might suffice. +# For WPA-RADIUS/EAP, ieee8021x must be set (but without dynamic WEP keys), +# RADIUS authentication server must be configured, and WPA-EAP must be included +# in wpa_key_mgmt. +# This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0) +# and/or WPA2 (full IEEE 802.11i/RSN): +# bit0 = WPA +# bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) +{% if security.wpa.mode is defined %} +{% if security.wpa.mode == 'both' %} +wpa=3 +{% elif security.wpa.mode == 'wpa2' %} +wpa=2 +{% elif security.wpa.mode == 'wpa' %} +wpa=1 +{% endif %} +{% endif %} + +{% if security.wpa.cipher is defined %} +# Set of accepted cipher suites (encryption algorithms) for pairwise keys +# (unicast packets). This is a space separated list of algorithms: +# CCMP = AES in Counter mode with CBC-MAC (CCMP-128) +# TKIP = Temporal Key Integrity Protocol +# CCMP-256 = AES in Counter mode with CBC-MAC with 256-bit key +# GCMP = Galois/counter mode protocol (GCMP-128) +# GCMP-256 = Galois/counter mode protocol with 256-bit key +# Group cipher suite (encryption algorithm for broadcast and multicast frames) +# is automatically selected based on this configuration. If only CCMP is +# allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise, +# TKIP will be used as the group cipher. The optional group_cipher parameter can +# be used to override this automatic selection. + +{% if security.wpa.mode is defined and security.wpa.mode == 'wpa2' %} +# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) +{% if security.wpa.cipher is string %} +rsn_pairwise={{ security.wpa.cipher }} +{% else %} +rsn_pairwise={{ security.wpa.cipher | join(" ") }} +{% endif %} +{% else %} +# Pairwise cipher for WPA (v1) (default: TKIP) +{% if security.wpa.cipher is string %} +wpa_pairwise={{ security.wpa.cipher }} +{% else %} +wpa_pairwise={{ security.wpa.cipher | join(" ") }} +{% endif %} +{% endif %} +{% endif %} + +{% if security.wpa.group_cipher is defined %} +# Optional override for automatic group cipher selection +# This can be used to select a specific group cipher regardless of which +# pairwise ciphers were enabled for WPA and RSN. It should be noted that +# overriding the group cipher with an unexpected value can result in +# interoperability issues and in general, this parameter is mainly used for +# testing purposes. +{% if security.wpa.group_cipher is string %} +group_cipher={{ security.wpa.group_cipher }} +{% else %} +group_cipher={{ security.wpa.group_cipher | join(" ") }} +{% endif %} +{% endif %} + +{% if security.wpa.passphrase is defined %} +# IEEE 802.11 specifies two authentication algorithms. hostapd can be +# configured to allow both of these or only one. Open system authentication +# should be used with IEEE 802.1X. +# Bit fields of allowed authentication algorithms: +# bit 0 = Open System Authentication +# bit 1 = Shared Key Authentication (requires WEP) +auth_algs=1 + +# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit +# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase +# (8..63 characters) that will be converted to PSK. This conversion uses SSID +# so the PSK changes when ASCII passphrase is used and the SSID is changed. +wpa_passphrase={{ security.wpa.passphrase }} + +# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The +# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be +# added to enable SHA256-based stronger algorithms. +# WPA-PSK = WPA-Personal / WPA2-Personal +# WPA-PSK-SHA256 = WPA2-Personal using SHA256 +wpa_key_mgmt=WPA-PSK + +{% elif security.wpa.radius is defined %} +##### IEEE 802.1X-2004 related configuration ################################## +# Require IEEE 802.1X authorization +ieee8021x=1 + +# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The +# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be +# added to enable SHA256-based stronger algorithms. +# WPA-EAP = WPA-Enterprise / WPA2-Enterprise +# WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 +wpa_key_mgmt=WPA-EAP + +{% if security.wpa.radius.server is defined %} +# RADIUS client forced local IP address for the access point +# Normally the local IP address is determined automatically based on configured +# IP addresses, but this field can be used to force a specific address to be +# used, e.g., when the device has multiple IP addresses. +# The own IP address of the access point (used as NAS-IP-Address) +{% if security.wpa.radius.source_address is defined %} +radius_client_addr={{ security.wpa.radius.source_address }} +own_ip_addr={{ security.wpa.radius.source_address }} +{% else %} +own_ip_addr=127.0.0.1 +{% endif %} + +{% for radius in security.wpa.radius.server if not radius.disabled %} +# RADIUS authentication server +auth_server_addr={{ radius.server }} +auth_server_port={{ radius.port }} +auth_server_shared_secret={{ radius.key }} + +{% if radius.acc_port %} +# RADIUS accounting server +acct_server_addr={{ radius.server }} +acct_server_port={{ radius.acc_port }} +acct_server_shared_secret={{ radius.key }} +{% endif %} +{% endfor %} +{% else %} +# Open system +auth_algs=1 +{% endif %} +{% endif %} +{% endif %} + +# TX queue parameters (EDCF / bursting) +# tx_queue_<queue name>_<param> +# queues: data0, data1, data2, data3 +# (data0 is the highest priority queue) +# parameters: +# aifs: AIFS (default 2) +# cwmin: cwMin (1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, +# 16383, 32767) +# cwmax: cwMax (same values as cwMin, cwMax >= cwMin) +# burst: maximum length (in milliseconds with precision of up to 0.1 ms) for +# bursting +# +# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): +# These parameters are used by the access point when transmitting frames +# to the clients. +# +# Low priority / AC_BK = background +tx_queue_data3_aifs=7 +tx_queue_data3_cwmin=15 +tx_queue_data3_cwmax=1023 +tx_queue_data3_burst=0 +# Note: for IEEE 802.11b mode: cWmin=31 cWmax=1023 burst=0 +# +# Normal priority / AC_BE = best effort +tx_queue_data2_aifs=3 +tx_queue_data2_cwmin=15 +tx_queue_data2_cwmax=63 +tx_queue_data2_burst=0 +# Note: for IEEE 802.11b mode: cWmin=31 cWmax=127 burst=0 +# +# High priority / AC_VI = video +tx_queue_data1_aifs=1 +tx_queue_data1_cwmin=7 +tx_queue_data1_cwmax=15 +tx_queue_data1_burst=3.0 +# Note: for IEEE 802.11b mode: cWmin=15 cWmax=31 burst=6.0 +# +# Highest priority / AC_VO = voice +tx_queue_data0_aifs=1 +tx_queue_data0_cwmin=3 +tx_queue_data0_cwmax=7 +tx_queue_data0_burst=1.5 + +# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): +# for 802.11a or 802.11g networks +# These parameters are sent to WMM clients when they associate. +# The parameters will be used by WMM clients for frames transmitted to the +# access point. +# +# note - txop_limit is in units of 32microseconds +# note - acm is admission control mandatory flag. 0 = admission control not +# required, 1 = mandatory +# note - Here cwMin and cmMax are in exponent form. The actual cw value used +# will be (2^n)-1 where n is the value given here. The allowed range for these +# wmm_ac_??_{cwmin,cwmax} is 0..15 with cwmax >= cwmin. +# +wmm_enabled=1 + +# Low priority / AC_BK = background +wmm_ac_bk_cwmin=4 +wmm_ac_bk_cwmax=10 +wmm_ac_bk_aifs=7 +wmm_ac_bk_txop_limit=0 +wmm_ac_bk_acm=0 +# Note: for IEEE 802.11b mode: cWmin=5 cWmax=10 +# +# Normal priority / AC_BE = best effort +wmm_ac_be_aifs=3 +wmm_ac_be_cwmin=4 +wmm_ac_be_cwmax=10 +wmm_ac_be_txop_limit=0 +wmm_ac_be_acm=0 +# Note: for IEEE 802.11b mode: cWmin=5 cWmax=7 +# +# High priority / AC_VI = video +wmm_ac_vi_aifs=2 +wmm_ac_vi_cwmin=3 +wmm_ac_vi_cwmax=4 +wmm_ac_vi_txop_limit=94 +wmm_ac_vi_acm=0 +# Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188 +# +# Highest priority / AC_VO = voice +wmm_ac_vo_aifs=2 +wmm_ac_vo_cwmin=2 +wmm_ac_vo_cwmax=3 +wmm_ac_vo_txop_limit=47 +wmm_ac_vo_acm=0 + diff --git a/data/templates/wifi/wpa_supplicant.conf.tmpl b/data/templates/wifi/wpa_supplicant.conf.tmpl new file mode 100644 index 000000000..9ddad35fd --- /dev/null +++ b/data/templates/wifi/wpa_supplicant.conf.tmpl @@ -0,0 +1,9 @@ +# WPA supplicant config +network={ + ssid="{{ ssid }}" +{% if security is defined and security.wpa is defined and security.wpa.passphrase is defined %} + psk="{{ security.wpa.passphrase }}" +{% else %} + key_mgmt=NONE +{% endif %} +} diff --git a/data/templates/wwan/chat.tmpl b/data/templates/wwan/chat.tmpl new file mode 100644 index 000000000..a3395c057 --- /dev/null +++ b/data/templates/wwan/chat.tmpl @@ -0,0 +1,6 @@ +ABORT 'NO DIAL TONE' ABORT 'NO ANSWER' ABORT 'NO CARRIER' ABORT DELAYED +'' AT +OK ATZ +OK 'AT+CGDCONT=1,"IP","{{ apn }}"' +OK ATD*99# +CONNECT '' diff --git a/data/templates/wwan/ip-down.script.tmpl b/data/templates/wwan/ip-down.script.tmpl new file mode 100644 index 000000000..9dc15ea99 --- /dev/null +++ b/data/templates/wwan/ip-down.script.tmpl @@ -0,0 +1,27 @@ +#!/bin/sh + +# Script parameters will be like: +# wlm0 /dev/serial/by-bus/usb0b1.3p1.3 115200 10.100.118.91 10.64.64.64 wlm0 + +# Only applicable for Wireless Modems (WWAN) +if [ -z $(echo $2 | egrep "(ttyS[0-9]+|usb[0-9]+b.*)$") ]; then + exit 0 +fi + +# Determine if we are running inside a VRF or not, required for proper routing table +# NOTE: the down script can not be properly templated as we need the VRF name, +# which is not present on deletion, thus we read it from the operating system. +if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*)) + # Remove upper_ prefix from result string + VRF_NAME=${VRF#"upper_"} + # Remove default route from VRF routing table + vtysh -c "conf t" -c "vrf ${VRF_NAME}" -c "no ip route 0.0.0.0/0 {{ ifname }}" +else + # Remove default route from GRT (global routing table) + vtysh -c "conf t" -c "no ip route 0.0.0.0/0 {{ ifname }}" +fi + +DIALER_PID=$(cat /var/run/{{ ifname }}.pid) +logger -t pppd[$DIALER_PID] "removed default route via {{ ifname }} metric {{ backup.distance }}" diff --git a/data/templates/wwan/ip-pre-up.script.tmpl b/data/templates/wwan/ip-pre-up.script.tmpl new file mode 100644 index 000000000..efc065bad --- /dev/null +++ b/data/templates/wwan/ip-pre-up.script.tmpl @@ -0,0 +1,23 @@ +#!/bin/sh +# As WWAN is an "on demand" interface we need to re-configure it when it +# becomes 'up' + +ipparam=$6 + +# device name and metric are received using ipparam +device=`echo "$ipparam"|awk '{ print $1 }'` + +if [ "$device" != "{{ ifname }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ ifname }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +echo "{{ description }}" > /sys/class/net/{{ ifname }}/ifalias + +{% if vrf -%} +logger -t pppd[$DIALER_PID] "configuring interface {{ ifname }} for VRF {{ vrf }}" +ip link set dev {{ ifname }} master {{ vrf }} +{% endif %} diff --git a/data/templates/wwan/ip-up.script.tmpl b/data/templates/wwan/ip-up.script.tmpl new file mode 100644 index 000000000..2603a0286 --- /dev/null +++ b/data/templates/wwan/ip-up.script.tmpl @@ -0,0 +1,25 @@ +#!/bin/sh + +# Script parameters will be like: +# wlm0 /dev/serial/by-bus/usb0b1.3p1.3 115200 10.100.118.91 10.64.64.64 wlm0 + +# Only applicable for Wireless Modems (WWAN) +if [ -z $(echo $2 | egrep "(ttyS[0-9]+|usb[0-9]+b.*)$") ]; then + exit 0 +fi + +# Determine if we are running inside a VRF or not, required for proper routing table +if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*)) + # Remove upper_ prefix from result string + VRF_NAME=${VRF#"upper_"} + # Remove default route from VRF routing table + vtysh -c "conf t" -c "vrf ${VRF_NAME}" -c "ip route 0.0.0.0/0 {{ ifname }} {{ backup.distance }}" +else + # Remove default route from GRT (global routing table) + vtysh -c "conf t" -c "ip route 0.0.0.0/0 {{ ifname }} {{ backup.distance }}" +fi + +DIALER_PID=$(cat /var/run/{{ ifname }}.pid) +logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} metric {{ backup.distance }} ${VRF_NAME}" diff --git a/data/templates/wwan/peer.tmpl b/data/templates/wwan/peer.tmpl new file mode 100644 index 000000000..aa759f741 --- /dev/null +++ b/data/templates/wwan/peer.tmpl @@ -0,0 +1,27 @@ +### Autogenerated by interfaces-wirelessmodem.py ### + +{{ "# description: " + description if description is defined }} +ifname {{ ifname }} +ipparam {{ ifname }} +linkname {{ ifname }} +{{ "usepeerdns" if no_peer_dns is defined }} +# physical device +{{ device }} +lcp-echo-failure 0 +115200 +debug +debug +mtu {{ mtu }} +mru {{ mtu }} +nodefaultroute +ipcp-max-failure 4 +ipcp-accept-local +ipcp-accept-remote +noauth +crtscts +lock +persist +{{ "demand" if ondemand is defined }} + +connect '/usr/sbin/chat -v -t6 -f /etc/ppp/peers/chat.{{ ifname }}' + diff --git a/debian/changelog b/debian/changelog index 2b24a8f9e..fba9d77d0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,10 @@ -vyos-smoketest (1.0.0) unstable; urgency=medium +vyos-1x (1.0.0) unstable; urgency=medium - * Initial release + * Dummy changelog entry for vyos-1x repository + This is a internal VyOS package and the VyOS package process does not use + the debian package changelog for its changes, please refer to the + GitHub commitlog and the vyos release-notes for more details. + The correct verion number of this package is auto-generated by GIT + on build-time - -- Daniil Baturin <daniil@baturin.org> Tue, 02 Jul 2019 22:29:53 +0200 + -- Runar Borge <runar@borge.nu> Sat, 9 May 2020 22:00:00 +0100 diff --git a/debian/control b/debian/control index ada46a4c1..65808de86 100644 --- a/debian/control +++ b/debian/control @@ -1,22 +1,129 @@ -Source: vyos-smoketest +Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers <maintainers@vyos.net> -Build-Depends: debhelper (>= 9), - quilt, +Build-Depends: + debhelper (>= 9), + fakeroot, + libvyosconfig0 (>= 0.0.7), python3, - python3-setuptools, - quilt, + python3-coverage, python3-lxml, python3-nose, - python3-coverage + python3-setuptools, + python3-xmltodict, + quilt, + whois Standards-Version: 3.9.6 -Package: vyos-smoketest +Package: vyos-1x Architecture: all -Depends: python3, +Depends: + accel-ppp, + beep, + bmon, + bsdmainutils, + conntrack, + conserver-client, + conserver-server, + crda, + cron, + dbus, + dropbear, + easy-rsa, + etherwake, + fastnetmon, + file, + frr, + frr-pythontools, + hostapd (>= 0.6.8), + hvinfo, + igmpproxy, + ipaddrcheck, + iperf, + iperf3, + iputils-arping, + isc-dhcp-client, + isc-dhcp-relay, + isc-dhcp-server, + iw, + keepalived (>=2.0.5), + lcdproc, + libatomic1, + libndp-tools, + libpam-radius-auth (>= 1.5.0), + libvyosconfig0, + lldpd, + lm-sensors, + lsscsi, + mdns-repeater, + mtr-tiny, + nftables (>= 0.9.3), + nginx-light, + ntp, + ntpdate, + ocserv, + openssh-server, + openvpn, + openvpn-auth-ldap, + openvpn-auth-radius, + pciutils, + pdns-recursor, + pmacct (>= 1.6.0), + pppoe, + procps, + python3, + python3-certbot-nginx, ${python3:Depends}, - ${misc:Depends}, - vyos-1x + python3-flask, + python3-hurry.filesize, + python3-isc-dhcp-leases, + python3-jinja2, + python3-jmespath, + python3-netaddr, + python3-netifaces, + python3-psutil, + python3-pystache, + python3-pyudev, + python3-six, + python3-tabulate, + python3-vici (>= 5.7.2), + python3-voluptuous, + python3-waitress, + python3-xmltodict, + python3-zmq, + radvd, + salt-minion, + snmp, + snmpd, + ssl-cert, + systemd, + tcpdump, + tcptraceroute, + telnet, + tftpd-hpa, + traceroute, + udp-broadcast-relay, + usb-modeswitch, + usbutils, + vyos-utils, + wide-dhcpv6-client, + wireguard, + wireless-regdb, + wpasupplicant (>= 0.6.7) +Description: VyOS configuration scripts and data + VyOS configuration scripts, interface definitions, and everything + +Package: vyos-1x-vmware +Architecture: amd64 i386 +Depends: + vyos-1x, + open-vm-tools +Description: VyOS configuration scripts and data for VMware + Adds configuration files required for VyOS running on VMware hosts. + +Package: vyos-1x-smoketest +Architecture: all +Depends: + vyos-1x Description: VyOS build sanity checking toolkit - VyOS build sanity checking toolkit diff --git a/debian/copyright b/debian/copyright index a3260a7a1..20704c47c 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,7 +1,7 @@ This package was debianized by Daniil Baturin <daniil@baturin.org> on -Tue, 02 Jul 2019 22:24:20 +0200 +Thu, 17 Aug 2017 20:17:04 -0400 -It's original content from the GIT repository <http://github.com/vyos/vyos-smoketest> +It's original content from the GIT repository <http://github.com/vyos/vyos-1x> Upstream Author: @@ -9,13 +9,13 @@ Upstream Author: Copyright: - Copyright (C) 2019 VyOS maintainers and contributors + Copyright (C) 2017 VyOS maintainers and contributors All Rights Reserved. License: This program is free software; you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by +it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. @@ -26,10 +26,10 @@ General Public License for more details. A copy of the GNU General Public License is available as `/usr/share/common-licenses/GPL' in the Debian GNU/Linux distribution -or on the World Wide Web at `http://www.gnu.org/copyleft/lgpl.html'. +or on the World Wide Web at `http://www.gnu.org/copyleft/gpl.html'. You can also obtain it by writing to the Free Software Foundation, Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. -The Debian packaging is (C) 2019, Daniil Baturin <daniil@baturin.org> and +The Debian packaging is (C) 2017, Daniil Baturin <daniil@baturin.org> and is licensed under the GPL, see above. diff --git a/debian/lintian-overrides b/debian/lintian-overrides new file mode 100644 index 000000000..6c5d67159 --- /dev/null +++ b/debian/lintian-overrides @@ -0,0 +1,6 @@ +# It's FSH compliant! +vyos-1x: file-in-unusual-dir usr/libexec/* +vyos-1x: non-standard-dir-in-usr usr/libexec/ + +# Nothing we can do about that right now +vyos-1x: dir-or-file-in-opt diff --git a/debian/rules b/debian/rules index 526fb8537..58bafd333 100755 --- a/debian/rules +++ b/debian/rules @@ -1,25 +1,101 @@ #!/usr/bin/make -f -DIR := debian/vyos-smoketest -VYOS_BIN_DIR := usr/bin/ +DIR := debian/tmp +VYOS_SBIN_DIR := usr/sbin +VYOS_BIN_DIR := usr/bin VYOS_LIBEXEC_DIR := usr/libexec/vyos -VYOS_DATA_DIR := /usr/share/vyos -VYOS_CFG_TMPL_DIR := /opt/vyatta/share/vyatta-cfg/templates -VYOS_OP_TMPL_DIR := /opt/vyatta/share/vyatta-op/templates +VYOS_DATA_DIR := usr/share/vyos +VYOS_CFG_TMPL_DIR := opt/vyatta/share/vyatta-cfg/templates +VYOS_OP_TMPL_DIR := opt/vyatta/share/vyatta-op/templates -MIGRATION_SCRIPTS_DIR := /opt/vyatta/etc/config-migrate/migrate/ +MIGRATION_SCRIPTS_DIR := opt/vyatta/etc/config-migrate/migrate +SYSTEM_SCRIPTS_DIR := usr/libexec/vyos/system +SERVICES_DIR := usr/libexec/vyos/services %: dh $@ --with python3, --with quilt +override_dh_gencontrol: + dh_gencontrol -- -v$(shell (git describe --tags --long --match 'vyos/*' --dirty 2>/dev/null || echo 0.0-no.git.tag) | sed -E 's%vyos/%%' | sed -E 's%-dirty%+dirty%') + override_dh_auto_build: make all override_dh_auto_install: + dh_auto_install + + # convert the XML to dictionaries + env PYTHONPATH=python python3 python/vyos/xml/generate.py + + cd python; python3 setup.py install --install-layout=deb --root ../$(DIR); cd .. + + # Install scripts + mkdir -p $(DIR)/$(VYOS_SBIN_DIR) + mkdir -p $(DIR)/$(VYOS_BIN_DIR) + cp -r src/utils/* $(DIR)/$(VYOS_BIN_DIR) + + # Install conf mode scripts + mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/conf_mode + cp -r src/conf_mode/* $(DIR)/$(VYOS_LIBEXEC_DIR)/conf_mode + + # Install op mode scripts + mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/op_mode + cp -r src/op_mode/* $(DIR)/$(VYOS_LIBEXEC_DIR)/op_mode + + # Install validators + mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/validators + cp -r src/validators/* $(DIR)/$(VYOS_LIBEXEC_DIR)/validators + + # Install completion helpers + mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/completion + cp -r src/completion/* $(DIR)/$(VYOS_LIBEXEC_DIR)/completion + + # Install helper scripts + cp -r src/helpers/* $(DIR)/$(VYOS_LIBEXEC_DIR)/ + + # Install migration scripts + mkdir -p $(DIR)/$(MIGRATION_SCRIPTS_DIR) + cp -r src/migration-scripts/* $(DIR)/$(MIGRATION_SCRIPTS_DIR) + + # Install system scripts + mkdir -p $(DIR)/$(SYSTEM_SCRIPTS_DIR) + cp -r src/system/* $(DIR)/$(SYSTEM_SCRIPTS_DIR) + + # Install system services + mkdir -p $(DIR)/$(SERVICES_DIR) + cp -r src/services/* $(DIR)/$(SERVICES_DIR) + + # Install configuration command definitions + mkdir -p $(DIR)/$(VYOS_CFG_TMPL_DIR) + cp -r templates-cfg/* $(DIR)/$(VYOS_CFG_TMPL_DIR) + + # Install operational command definitions + mkdir -p $(DIR)/$(VYOS_OP_TMPL_DIR) + cp -r templates-op/* $(DIR)/$(VYOS_OP_TMPL_DIR) + + # Install data files + mkdir -p $(DIR)/$(VYOS_DATA_DIR) + cp -r data/* $(DIR)/$(VYOS_DATA_DIR) + + # Install etc configuration files + mkdir -p $(DIR)/etc + cp -r src/etc/* $(DIR)/etc + + # Install PAM configuration snippets + mkdir -p $(DIR)/usr/share/pam-configs + cp -r src/pam-configs/* $(DIR)/usr/share/pam-configs + + # Install systemd service units + mkdir -p $(DIR)/lib/systemd/system + cp -r src/systemd/* $(DIR)/lib/systemd/system + + # Make directory for generated configuration file + mkdir -p $(DIR)/etc/vyos + # Install smoke test scripts mkdir -p $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke/ - cp -r scripts/* $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke + cp -r smoketest/scripts/* $(DIR)/$(VYOS_LIBEXEC_DIR)/tests/smoke # Install system programs mkdir -p $(DIR)/$(VYOS_BIN_DIR) - cp -r bin/* $(DIR)/$(VYOS_BIN_DIR) + cp -r smoketest/bin/* $(DIR)/$(VYOS_BIN_DIR) diff --git a/debian/vyos-1x-smoketest.install b/debian/vyos-1x-smoketest.install new file mode 100644 index 000000000..fdf949557 --- /dev/null +++ b/debian/vyos-1x-smoketest.install @@ -0,0 +1,2 @@ +usr/bin/vyos-smoketest +usr/libexec/vyos/tests/smoke diff --git a/debian/vyos-1x-vmware.install b/debian/vyos-1x-vmware.install new file mode 100644 index 000000000..715015621 --- /dev/null +++ b/debian/vyos-1x-vmware.install @@ -0,0 +1 @@ +etc/vmware-tools diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install new file mode 100644 index 000000000..6cd678442 --- /dev/null +++ b/debian/vyos-1x.install @@ -0,0 +1,23 @@ +etc/dhcp +etc/ppp +etc/rsyslog.d +etc/systemd +etc/sysctl.d +etc/udev +etc/vyos +lib/ +opt/ +usr/bin/initial-setup +usr/bin/vyos-config-file-query +usr/bin/vyos-config-to-commands +usr/bin/vyos-config-to-json +usr/bin/vyos-hostsd-client +usr/lib +usr/libexec/vyos/completion +usr/libexec/vyos/conf_mode +usr/libexec/vyos/op_mode +usr/libexec/vyos/services +usr/libexec/vyos/system +usr/libexec/vyos/validators +usr/libexec/vyos/*.py +usr/share diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst new file mode 100644 index 000000000..dc129cb54 --- /dev/null +++ b/debian/vyos-1x.postinst @@ -0,0 +1,32 @@ +#!/bin/sh -e +if ! deb-systemd-helper --quiet was-enabled salt-minion.service; then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper disable salt-minion.service >/dev/null || true +fi + +if [ -x "/etc/init.d/salt-minion" ]; then + update-rc.d -f salt-minion remove >/dev/null +fi + +# Add minion user for salt-minion +if ! grep -q '^minion' /etc/passwd; then + adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg --gecos "salt minion user" --shell /bin/vbash minion + adduser --quiet minion frrvty + adduser --quiet minion sudo + adduser --quiet minion adm + adduser --quiet minion dip + adduser --quiet minion disk + adduser --quiet minion users +fi + +# add hostsd group for vyos-hostsd +if ! grep -q '^hostsd' /etc/group; then + addgroup --quiet --system hostsd +fi + +# add dhcpd user for dhcp-server +if ! grep -q '^dhcpd' /etc/passwd; then + adduser --quiet --system --disabled-login --no-create-home --home /run/dhcp-server dhcpd + adduser --quiet dhcpd hostsd +fi diff --git a/interface-definitions/arp.xml.in b/interface-definitions/arp.xml.in new file mode 100644 index 000000000..b72f025a8 --- /dev/null +++ b/interface-definitions/arp.xml.in @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="static"> + <children> + <tagNode name="arp" owner="${vyos_conf_scripts_dir}/arp.py"> + <properties> + <help>Static ARP translation</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 destination address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="hwaddr"> + <properties> + <help>mac address to translate to</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/bcast-relay.xml.in b/interface-definitions/bcast-relay.xml.in new file mode 100644 index 000000000..96ce16639 --- /dev/null +++ b/interface-definitions/bcast-relay.xml.in @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<!-- UDP broadcast relay configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="broadcast-relay" owner="${vyos_conf_scripts_dir}/bcast_relay.py"> + <properties> + <help>UDP broadcast relay service</help> + <priority>990</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Globally disable broadcast relay service</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="id"> + <properties> + <help>Unique ID for each UDP port to forward</help> + <valueHelp> + <format>1-99</format> + <description>Numerical ID #</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-99"/> + </constraint> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Disable broadcast relay service instance</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="address"> + <properties> + <help>Set source IP of forwarded packets, otherwise original senders address is used</help> + <valueHelp> + <format>ipv4</format> + <description>Optional source address for forwarded packets</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Description</help> + </properties> + </leafNode> + <leafNode name="interface"> + <properties> + <help>Interface to repeat UDP broadcasts to [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Destination or source port to listen and retransmit on [REQUIRED]</help> + <valueHelp> + <format>1-65535</format> + <description>UDP port to listen on</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/cron.xml.in b/interface-definitions/cron.xml.in new file mode 100644 index 000000000..2d4921bf0 --- /dev/null +++ b/interface-definitions/cron.xml.in @@ -0,0 +1,75 @@ +<?xml version="1.0"?> + +<!-- Cron configuration --> + +<interfaceDefinition> + <node name="system"> + <children> + <node name="task-scheduler"> + <properties> + <help>Task scheduler settings</help> + </properties> + <children> + <tagNode name="task" owner="${vyos_conf_scripts_dir}/task_scheduler.py"> + <properties> + <help>Scheduled task</help> + <valueHelp> + <format><string></format> + <description>Task name</description> + </valueHelp> + <priority>999</priority> + </properties> + <children> + <leafNode name="crontab-spec"> + <properties> + <help>UNIX crontab time specification string</help> + </properties> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Execution interval</help> + <valueHelp> + <format><minutes></format> + <description>Execution interval in minutes</description> + </valueHelp> + <valueHelp> + <format><minutes>m</format> + <description>Execution interval in minutes</description> + </valueHelp> + <valueHelp> + <format><hours>h</format> + <description>Execution interval in hours</description> + </valueHelp> + <valueHelp> + <format><days>d</format> + <description>Execution interval in days</description> + </valueHelp> + <constraint> + <regex>[1-9]([0-9]*)([mhd]{0,1})</regex> + </constraint> + </properties> + </leafNode> + <node name="executable"> + <properties> + <help>Executable path and arguments</help> + </properties> + <children> + <leafNode name="path"> + <properties> + <help>Path to executable</help> + </properties> + </leafNode> + <leafNode name="arguments"> + <properties> + <help>Arguments passed to the executable</help> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcp-relay.xml.in b/interface-definitions/dhcp-relay.xml.in new file mode 100644 index 000000000..b83402aa1 --- /dev/null +++ b/interface-definitions/dhcp-relay.xml.in @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<!-- DHCP relay configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcp-relay" owner="${vyos_conf_scripts_dir}/dhcp_relay.py"> + <properties> + <help>Host Configuration Protocol (DHCP) relay agent</help> + <priority>910</priority> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>DHCP relay interface [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -b</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <node name="relay-options"> + <properties> + <help>Relay options</help> + </properties> + <children> + <leafNode name="hop-count"> + <properties> + <help>Policy to discard packets that have reached specified hop-count</help> + <valueHelp> + <format>1-255</format> + <description>Hop count (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>hop-count must be a value between 1 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="max-size"> + <properties> + <help>Maximum packet size to send to a DHCPv4/BOOTP server</help> + <valueHelp> + <format>64-1400</format> + <description>Maximum packet size (default: 576)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 64-1400"/> + </constraint> + <constraintErrorMessage>max-size must be a value between 64 and 1400</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="relay-agents-packets"> + <properties> + <help>Policy to handle incoming DHCPv4 packets which already contain relay agent options (default: forward)</help> + <completionHelp> + <list>append replace forward discard</list> + </completionHelp> + <valueHelp> + <format>append</format> + <description>append own relay options to packet</description> + </valueHelp> + <valueHelp> + <format>replace</format> + <description>replace existing agent option field</description> + </valueHelp> + <valueHelp> + <format>forward</format> + <description>forward packet unchanged</description> + </valueHelp> + <valueHelp> + <format>discard</format> + <description>discard packet (default action if giaddr not set in packet)</description> + </valueHelp> + <constraint> + <regex>(append|replace|forward|discard)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="server"> + <properties> + <help>DHCP server address</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server IPv4 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in new file mode 100644 index 000000000..e8bdff3df --- /dev/null +++ b/interface-definitions/dhcp-server.xml.in @@ -0,0 +1,467 @@ +<?xml version="1.0"?> +<!-- DHCP server configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcp-server" owner="${vyos_conf_scripts_dir}/dhcp_server.py"> + <properties> + <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help> + <priority>911</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCP server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dynamic-dns-update"> + <properties> + <help>DHCP server to dynamically update the Domain Name System (DNS)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="global-parameters"> + <properties> + <help>Additional global parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <leafNode name="hostfile-update"> + <properties> + <help>Enable DHCP server updating /etc/hosts (per client lease)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="host-decl-name"> + <properties> + <help>Instruct server to use host declaration name for forward DNS name</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="shared-network-name"> + <properties> + <help>DHCP shared network name [REQUIRED]</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + <children> + <leafNode name="authoritative"> + <properties> + <help>Option to make DHCP server authoritative for this physical network</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Shared-network-name description</help> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCP configuration for shared-network</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="shared-network-parameters"> + <properties> + <help>Additional shared-network parameters for DHCP server. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <tagNode name="subnet"> + <properties> + <help>DHCP subnet for shared network</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="bootfile-name"> + <properties> + <help>Bootstrap file name</help> + </properties> + </leafNode> + <leafNode name="bootfile-server"> + <properties> + <help>Server (IP address or domain name) from which the initial + boot file is to be loaded</help> + </properties> + </leafNode> + <leafNode name="client-prefix-length"> + <properties> + <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> + <valueHelp> + <format>0-32</format> + <description>DHCP client prefix length must be 0 to 32</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-router"> + <properties> + <help>IP address of default router</help> + <valueHelp> + <format>ipv4</format> + <description>Default router IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dns-server"> + <properties> + <help>DNS server IPv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>DNS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="domain-name"> + <properties> + <help>Client domain name</help> + </properties> + </leafNode> + <leafNode name="domain-search"> + <properties> + <help>Client domain search</help> + <multi/> + </properties> + </leafNode> + <leafNode name="exclude"> + <properties> + <help>IP address to exclude from DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="failover"> + <properties> + <help>DHCP failover parameters</help> + </properties> + <children> + <leafNode name="local-address"> + <properties> + <help>IP address for failover peer to connect [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to exclude from lease range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="name"> + <properties> + <help>DHCP failover peer name [REQUIRED]</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid failover peer name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="peer-address"> + <properties> + <help>IP address of failover peer [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of failover peer</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="status"> + <properties> + <help>DHCP failover peer status (primary|secondary) [REQUIRED]</help> + <completionHelp> + <list>primary secondary</list> + </completionHelp> + <constraint> + <regex>(primary|secondary)</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="ip-forwarding"> + <properties> + <help>Enable IP forwarding on client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="lease"> + <properties> + <help>Lease timeout in seconds (default: 86400)</help> + <valueHelp> + <format>0-4294967295</format> + <description>DHCP lease time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>DHCP lease time must be between 0 and 4294967295 (49 days)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="ntp-server"> + <properties> + <help>IP address of NTP server</help> + <valueHelp> + <format>ipv4</format> + <description>NTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="pop-server"> + <properties> + <help>IP address of POP3 server</help> + <valueHelp> + <format>ipv4</format> + <description>POP3 server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="server-identifier"> + <properties> + <help>Address for DHCP server identifier</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server identifier IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="smtp-server"> + <properties> + <help>IP address of SMTP server</help> + <valueHelp> + <format>ipv4</format> + <description>SMTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="range"> + <properties> + <help>DHCP lease range</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid DHCP lease range name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address for DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 start address of pool</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address for DHCP lease range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 end address of pool</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="static-mapping"> + <properties> + <help>Name of static mapping</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable static mapping</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ip-address"> + <properties> + <help>Fixed IP address of static mapping</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address used in static mapping</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mac-address"> + <properties> + <help>MAC address of static mapping [REQUIRED]</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>MAC address used in static mapping [REQUIRED]</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="static-mapping-parameters"> + <properties> + <help>Additional static-mapping parameters for DHCP server. + Will be placed inside the "host" block of the mapping. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <node name="static-route"> + <properties> + <help>Classless static route</help> + </properties> + <children> + <leafNode name="destination-subnet"> + <properties> + <help>Destination subnet [REQUIRED]</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="router"> + <properties> + <help>IP address of router to be used to reach the destination subnet [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of router</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="subnet-parameters"> + <properties> + <help>Additional subnet parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <leafNode name="tftp-server-name"> + <properties> + <help>TFTP server name</help> + </properties> + </leafNode> + <leafNode name="time-offset"> + <properties> + <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help> + <valueHelp> + <format>[-]N</format> + <description>Time offset (number, may be negative)</description> + </valueHelp> + <constraint> + <regex>-?[0-9]+</regex> + </constraint> + <constraintErrorMessage>Invalid time offset value</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="time-server"> + <properties> + <help>IP address of time server</help> + <valueHelp> + <format>ipv4</format> + <description>Time server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wins-server"> + <properties> + <help>IP address for Windows Internet Name Service (WINS) server</help> + <valueHelp> + <format>ipv4</format> + <description>WINS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wpad-url"> + <properties> + <help>Web Proxy Autodiscovery (WPAD) URL</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcpv6-relay.xml.in b/interface-definitions/dhcpv6-relay.xml.in new file mode 100644 index 000000000..0beb09d05 --- /dev/null +++ b/interface-definitions/dhcpv6-relay.xml.in @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<!-- DHCPv6 relay configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcpv6-relay" owner="${vyos_conf_scripts_dir}/dhcpv6_relay.py"> + <properties> + <help>DHCPv6 Relay Agent parameters</help> + <priority>900</priority> + </properties> + <children> + <tagNode name="listen-interface"> + <properties> + <help>Interface for DHCPv6 Relay Agent to listen for requests</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address on listen-interface listen for requests on</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address on listen interface</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="max-hop-count"> + <properties> + <help>Maximum hop count for which requests will be processed</help> + <valueHelp> + <format>1-255</format> + <description>Hop count (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>max-hop-count must be a value between 1 and 255</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="upstream-interface"> + <properties> + <help>Interface for DHCPv6 Relay Agent forward requests out</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address to forward requests to</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of the DHCP server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="use-interface-id-option"> + <properties> + <help>Option to set DHCPv6 interface-ID option</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in new file mode 100644 index 000000000..4073b46b2 --- /dev/null +++ b/interface-definitions/dhcpv6-server.xml.in @@ -0,0 +1,344 @@ +<?xml version="1.0"?> +<!-- DHCPv6 server configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/dhcpv6_server.py"> + <properties> + <help>DHCP for IPv6 (DHCPv6) server</help> + <priority>900</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCPv6 server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="preference"> + <properties> + <help>Preference of this DHCPv6 server compared with others</help> + <valueHelp> + <format>0-255</format> + <description>DHCPv6 server preference (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="shared-network-name"> + <properties> + <help>DHCPv6 shared network name [REQUIRED]</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid DHCPv6 shared network name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable DHCPv6 configuration for shared-network</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="subnet"> + <properties> + <help>IPv6 DHCP subnet for this shared network [REQUIRED]</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <node name="address-range"> + <properties> + <help>Parameters setting ranges for assigning IPv6 addresses</help> + </properties> + <children> + <tagNode name="prefix"> + <properties> + <help>IPv6 prefix defining range of addresses to assign</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="temporary"> + <properties> + <help>Address range will be used for temporary addresses</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="start"> + <properties> + <help>First in range of consecutive IPv6 addresses to assign</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="stop"> + <properties> + <help>Last in range of consecutive IPv6 addresses</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="domain-search"> + <properties> + <help>Domain name for client to search</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid domain name. May only contain letters, numbers and .-_</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <node name="lease-time"> + <properties> + <help>Parameters relating to the lease time</help> + </properties> + <children> + <leafNode name="default"> + <properties> + <help>Default time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>DHCPv6 valid lifetime</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="maximum"> + <properties> + <help>Maximum time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>Maximum lease time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="minimum"> + <properties> + <help>Minimum time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>Minimum lease time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="name-server"> + <properties> + <help>IPv6 address of a Recursive DNS Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of DNS name server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nis-domain"> + <properties> + <help>NIS domain name for client to use</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid NIS domain name</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nis-server"> + <properties> + <help>IPv6 address of a NIS Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nisplus-domain"> + <properties> + <help>NIS+ domain name for client to use</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid NIS+ domain name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nisplus-server"> + <properties> + <help>IPv6 address of a NIS+ Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS+ server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="prefix-delegation"> + <properties> + <help>Parameters relating to IPv6 prefix delegation</help> + </properties> + <children> + <tagNode name="start"> + <properties> + <help>First in range of IPv6 addresses to be used in prefix delegation</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address used in prefix delegation</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="prefix-length"> + <properties> + <help>Length in bits of prefixes to be delegated</help> + <valueHelp> + <format>0-255</format> + <description>DHCPv6 server preference (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last in range of IPv6 addresses to be used in prefix delegation</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address used in prefix delegation</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="sip-server"> + <properties> + <help>IPv6 address of SIP server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of SIP server</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>FQDN of SIP server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="fqdn"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="sntp-server"> + <properties> + <help>IPv6 address of an SNTP server for client to use</help> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="static-mapping"> + <properties> + <help>Name of static mapping</help> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + <constraintErrorMessage>Invalid static mapping name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable static mapping</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="identifier"> + <properties> + <help>Client identifier (DUID) for this static mapping</help> + <valueHelp> + <format>h[[:h]...]</format> + <description>DUID: colon-separated hex list (as used by isc-dhcp option dhcpv6.client-id)</description> + </valueHelp> + <constraint> + <regex>([0-9A-Fa-f]{1,2}[:])*([0-9A-Fa-f]{1,2})</regex> + </constraint> + <constraintErrorMessage>Invalid DUID, must be in the format h[[:h]...]</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="ipv6-address"> + <properties> + <help>Client IPv6 address for this static mapping</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address for this static mapping</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in new file mode 100644 index 000000000..3b5843b53 --- /dev/null +++ b/interface-definitions/dns-domain-name.xml.in @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<!-- host-name configuration --> +<interfaceDefinition> + <node name="system"> + <children> + <leafNode name="name-server" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>Domain Name Servers (DNS) used by the system (resolv.conf)</help> + <priority>400</priority> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="name-servers-dhcp" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>Interfaces whose DHCP client nameservers will be used by the system (resolv.conf)</help> + <priority>400</priority> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="host-name" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>System host name (default: vyos)</help> + <constraint> + <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="domain-name" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>System domain name</help> + <constraint> + <regex>[A-Za-z0-9][-.A-Za-z0-9]*</regex> + </constraint> + </properties> + </leafNode> + <node name="domain-search" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>Domain Name Server (DNS) domain completion order</help> + <priority>400</priority> + </properties> + <children> + <leafNode name="domain"> + <properties> + <help>DNS domain completion order</help> + <constraint> + <regex>[-a-zA-Z0-9.]+$</regex> + </constraint> + <constraintErrorMessage>Invalid domain name</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="static-host-mapping" owner="${vyos_conf_scripts_dir}/host_name.py"> + <properties> + <help>Map host names to addresses</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="host-name"> + <properties> + <help>Host name for static address mapping</help> + <constraint> + <regex>[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$</regex> + </constraint> + <constraintErrorMessage>invalid hostname</constraintErrorMessage> + </properties> + <children> + <leafNode name="alias"> + <properties> + <help>Alias for this address</help> + <constraint> + <regex>.{1,63}$</regex> + </constraint> + <constraintErrorMessage>invalid alias hostname, needs to be between 1 and 63 charactes</constraintErrorMessage> + <multi /> + </properties> + </leafNode> + <leafNode name="inet"> + <properties> + <help>IP Address [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in new file mode 100644 index 000000000..143c04ef6 --- /dev/null +++ b/interface-definitions/dns-dynamic.xml.in @@ -0,0 +1,242 @@ +<?xml version="1.0"?> +<!-- Dynamic DNS configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dns"> + <properties> + <help>Domain Name System related services</help> + </properties> + <children> + <node name="dynamic" owner="${vyos_conf_scripts_dir}/dynamic_dns.py"> + <properties> + <help>Dynamic DNS</help> + <priority>919</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Interface to send DDNS updates for [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="rfc2136"> + <properties> + <help>RFC2136 Update name</help> + </properties> + <children> + <leafNode name="key"> + <properties> + <help>File containing the secret key shared with remote DNS server [REQUIRED]</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="record"> + <properties> + <help>Record to be updated [REQUIRED]</help> + <multi/> + </properties> + </leafNode> + <leafNode name="server"> + <properties> + <help>Server to be updated [REQUIRED]</help> + </properties> + </leafNode> + <leafNode name="ttl"> + <properties> + <help>Time To Live (default: 600)</help> + <valueHelp> + <format>1-86400</format> + <description>DNS forwarding cache size</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-86400"/> + </constraint> + </properties> + </leafNode> + <leafNode name="zone"> + <properties> + <help>Zone to be updated [REQUIRED]</help> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="service"> + <properties> + <help>Service being used for Dynamic DNS [REQUIRED]</help> + <completionHelp> + <list><custom> afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit</list> + </completionHelp> + <valueHelp> + <format><custom></format> + <description>Service with a custom name</description> + </valueHelp> + <valueHelp> + <format>afraid</format> + <description>afraid.org Services</description> + </valueHelp> + <valueHelp> + <format>changeip</format> + <description>changeip.com Services</description> + </valueHelp> + <valueHelp> + <format>cloudflare</format> + <description>cloudflare.com Services</description> + </valueHelp> + <valueHelp> + <format>dnspark</format> + <description>dnspark.com Services</description> + </valueHelp> + <valueHelp> + <format>dslreports</format> + <description>dslreports.com Services</description> + </valueHelp> + <valueHelp> + <format>dyndns</format> + <description>dyndns.com Services</description> + </valueHelp> + <valueHelp> + <format>easydns</format> + <description>easydns.com Services</description> + </valueHelp> + <valueHelp> + <format>namecheap</format> + <description>namecheap.com Services</description> + </valueHelp> + <valueHelp> + <format>noip</format> + <description>noip.com Services</description> + </valueHelp> + <valueHelp> + <format>sitelutions</format> + <description>sitelutions.com Services</description> + </valueHelp> + <valueHelp> + <format>zoneedit</format> + <description>zoneedit.com Services</description> + </valueHelp> + <constraint> + <regex>^(custom|afraid|changeip|cloudflare|dnspark|dslreports|dyndns|easydns|namecheap|noip|sitelutions|zoneedit|\w+)$</regex> + </constraint> + <constraintErrorMessage>You can use only predefined list of services or word characters (_, a-z, A-Z, 0-9) as service name</constraintErrorMessage> + </properties> + <children> + <leafNode name="host-name"> + <properties> + <help>Hostname registered with DDNS service [REQUIRED]</help> + <multi/> + </properties> + </leafNode> + <leafNode name="login"> + <properties> + <help>Login for DDNS service [REQUIRED]</help> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for DDNS service [REQUIRED]</help> + </properties> + </leafNode> + <leafNode name="protocol"> + <properties> + <help>ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM]</help> + <completionHelp> + <list>changeip cloudflare dnspark dslreports1 dyndns2 easydns namecheap noip sitelutions zoneedit1</list> + </completionHelp> + <valueHelp> + <format>changeip</format> + <description>changeip protocol</description> + </valueHelp> + <valueHelp> + <format>cloudflare</format> + <description>cloudflare protocol</description> + </valueHelp> + <valueHelp> + <format>dnspark</format> + <description>dnspark protocol</description> + </valueHelp> + <valueHelp> + <format>dslreports1</format> + <description>dslreports1 protocol</description> + </valueHelp> + <valueHelp> + <format>dyndns2</format> + <description>dyndns2 protocol</description> + </valueHelp> + <valueHelp> + <format>easydns</format> + <description>easydns protocol</description> + </valueHelp> + <valueHelp> + <format>namecheap</format> + <description>namecheap protocol</description> + </valueHelp> + <valueHelp> + <format>noip</format> + <description>noip protocol</description> + </valueHelp> + <valueHelp> + <format>sitelutions</format> + <description>sitelutions protocol</description> + </valueHelp> + <valueHelp> + <format>zoneedit1</format> + <description>zoneedit1 protocol</description> + </valueHelp> + <constraint> + <regex>(changeip|cloudflare|dnspark|dslreports1|dyndns2|easydns|namecheap|noip|sitelutions|zoneedit1)</regex> + </constraint> + <constraintErrorMessage>Please choose from the list of allowed protocols</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="server"> + <properties> + <help>Server to send DDNS update to [REQUIRED FOR CUSTOM]</help> + <valueHelp> + <format>IPv4</format> + <description>IP address of DDNS server</description> + </valueHelp> + <valueHelp> + <format>FQDN</format> + <description>Hostname of DDNS server</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="zone"> + <properties> + <help>DNS zone to update (only available with CloudFlare)</help> + </properties> + </leafNode> + </children> + </tagNode> + <node name="use-web"> + <properties> + <help>Web check used for obtaining the external IP address</help> + </properties> + <children> + <leafNode name="skip"> + <properties> + <help>Skip everything before this on the given URL</help> + </properties> + </leafNode> + <leafNode name="url"> + <properties> + <help>URL to obtain the current external IP address</help> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in new file mode 100644 index 000000000..aaf8bb27d --- /dev/null +++ b/interface-definitions/dns-forwarding.xml.in @@ -0,0 +1,189 @@ +<?xml version="1.0"?> +<!-- DNS forwarder configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="dns"> + <properties> + <help>Domain Name System related services</help> + </properties> + <children> + <node name="forwarding" owner="${vyos_conf_scripts_dir}/dns_forwarding.py"> + <properties> + <help>DNS forwarding</help> + <priority>918</priority> + </properties> + <children> + <leafNode name="cache-size"> + <properties> + <help>DNS forwarding cache size</help> + <valueHelp> + <format>0-10000</format> + <description>DNS forwarding cache size</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-10000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dhcp"> + <properties> + <help>Interfaces whose DHCP client nameservers to forward requests to</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="dnssec"> + <properties> + <help>DNSSEC mode</help> + <completionHelp> + <list>off process-no-validate process log-fail validate</list> + </completionHelp> + <valueHelp> + <format>off</format> + <description>No DNSSEC processing whatsoever!</description> + </valueHelp> + <valueHelp> + <format>process-no-validate</format> + <description>Respond with DNSSEC records to clients that ask for it. No validation done at all!</description> + </valueHelp> + <valueHelp> + <format>process</format> + <description>Respond with DNSSEC records to clients that ask for it. Validation for clients that request it.</description> + </valueHelp> + <valueHelp> + <format>log-fail</format> + <description>Similar behaviour to process, but validate RRSIGs on responses and log bogus responses.</description> + </valueHelp> + <valueHelp> + <format>validate</format> + <description>Full blown DNSSEC validation. Send SERVFAIL to clients on bogus responses.</description> + </valueHelp> + <constraint> + <regex>(off|process-no-validate|process|log-fail|validate)</regex> + </constraint> + </properties> + </leafNode> + <tagNode name="domain"> + <properties> + <help>Domain to forward to a custom DNS server</help> + </properties> + <children> + <leafNode name="server"> + <properties> + <help>Domain Name Server (DNS) to forward queries to</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="addnta"> + <properties> + <help>Add NTA (negative trust anchor) for this domain (must be set if the domain does not support DNSSEC)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="recursion-desired"> + <properties> + <help>Set the "recursion desired" bit in requests to the upstream nameserver</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="ignore-hosts-file"> + <properties> + <help>Do not use local /etc/hosts file in name resolution</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="allow-from"> + <properties> + <help>Networks allowed to query this server</help> + <valueHelp> + <format>ipv4net</format> + <description>IP address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="listen-address"> + <properties> + <help>Addresses to listen for DNS queries [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="negative-ttl"> + <properties> + <help>Maximum amount of time negative entries are cached</help> + <valueHelp> + <format>0-7200</format> + <description>Seconds to cache NXDOMAIN entries</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-7200"/> + </constraint> + </properties> + </leafNode> + <leafNode name="name-server"> + <properties> + <help>Domain Name Servers (DNS) addresses [OPTIONAL]</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="system"> + <properties> + <help>Use system name servers</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/firewall-options.xml.in b/interface-definitions/firewall-options.xml.in new file mode 100644 index 000000000..defd44f06 --- /dev/null +++ b/interface-definitions/firewall-options.xml.in @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="firewall"> + <children> + <node name="options"> + <properties> + <help>Firewall options/Packet manipulation</help> + <priority>990</priority> + </properties> + <children> + <tagNode name="interface" owner="${vyos_conf_scripts_dir}/firewall_options.py"> + <properties> + <help>Interface clamping options</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Disable this rule</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="adjust-mss"> + <properties> + <help>Adjust MSS for IPv4 transit packets</help> + <valueHelp> + <format>500-1460</format> + <description>TCP Maximum segment size in bytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 500-1460"/> + </constraint> + </properties> + </leafNode> + <leafNode name="adjust-mss6"> + <properties> + <help>Adjust MSS for IPv6 transit packets</help> + <valueHelp> + <format>1280-1492</format> + <description>TCP Maximum segment size in bytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1280-1492"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in new file mode 100644 index 000000000..239269235 --- /dev/null +++ b/interface-definitions/flow-accounting-conf.xml.in @@ -0,0 +1,431 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- flow-accounting configuration --> +<interfaceDefinition> + <node name="system"> + <children> + <node name="flow-accounting" owner="${vyos_conf_scripts_dir}/flow_accounting_conf.py"> + <properties> + <help>Flow accounting settings</help> + <priority>990</priority> + </properties> + <children> + <leafNode name="buffer-size"> + <properties> + <help>Buffer size</help> + <valueHelp> + <format>0-4294967295</format> + <description>Buffer size in MiB</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295" /> + </constraint> + </properties> + </leafNode> + <leafNode name="disable-imt"> + <properties> + <help>Disable in memory table plugin</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="syslog-facility"> + <properties> + <help>Syslog facility for flow-accounting</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <constraint> + <regex>auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all</regex> + </constraint> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>Routing protocols (local7)</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Authentication and authorization</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="interface"> + <properties> + <help>Interface for flow-accounting [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <node name="netflow"> + <properties> + <help>NetFlow settings</help> + </properties> + <children> + <leafNode name="engine-id"> + <properties> + <help>NetFlow engine-id</help> + <valueHelp> + <format>0-255 or 0-255:0-255</format> + <description>NetFlow engine-id for v5</description> + </valueHelp> + <valueHelp> + <format>0-4294967295</format> + <description>NetFlow engine-id for v9 / IPFIX</description> + </valueHelp> + <constraint> + <regex>(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$|^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="max-flows"> + <properties> + <help>NetFlow maximum flows</help> + <valueHelp> + <format>0-4294967295</format> + <description>NetFlow maximum flows</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295" /> + </constraint> + </properties> + </leafNode> + <leafNode name="sampling-rate"> + <properties> + <help>NetFlow sampling-rate</help> + <valueHelp> + <format>0-4294967295</format> + <description>Sampling rate (1 in N packets)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295" /> + </constraint> + </properties> + </leafNode> + <leafNode name="source-ip"> + <properties> + <help>IPv4 or IPv6 source address of NetFlow packets</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source address of NetFlow packets</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 source address of NetFlow packets</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="version"> + <properties> + <help>NetFlow version to export</help> + <completionHelp> + <list>5 9 10</list> + </completionHelp> + <valueHelp> + <format>5</format> + <description>NetFlow version 5</description> + </valueHelp> + <valueHelp> + <format>9</format> + <description>NetFlow version 9 (default)</description> + </valueHelp> + <valueHelp> + <format>10</format> + <description>Internet Protocol Flow Information Export (IPFIX)</description> + </valueHelp> + </properties> + </leafNode> + <tagNode name="server"> + <properties> + <help>Server to export NetFlow [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 server to export NetFlow</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 server to export NetFlow</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="port"> + <properties> + <help>NetFlow port number</help> + <valueHelp> + <format>1025-65535</format> + <description>NetFlow port number (default 2055)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1025-65535" /> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <node name="timeout"> + <properties> + <help>NetFlow timeout values</help> + </properties> + <children> + <leafNode name="expiry-interval"> + <properties> + <help>Expiry scan interval</help> + <valueHelp> + <format>0-2147483647</format> + <description>Expiry scan interval (default 60)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + <leafNode name="flow-generic"> + <properties> + <help>Generic flow timeout value</help> + <valueHelp> + <format>0-2147483647</format> + <description>Generic flow timeout in seconds (default 3600)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + <leafNode name="icmp"> + <properties> + <help>ICMP timeout value</help> + <valueHelp> + <format>0-2147483647</format> + <description>ICMP timeout in seconds (default 300)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + <leafNode name="max-active-life"> + <properties> + <help>Max active timeout value</help> + <valueHelp> + <format>0-2147483647</format> + <description>Max active timeout in seconds (default 604800)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + <leafNode name="tcp-fin"> + <properties> + <help>TCP finish timeout value</help> + <valueHelp> + <format>0-2147483647</format> + <description>TCP FIN timeout in seconds (default 300)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + <leafNode name="tcp-generic"> + <properties> + <help>TCP generic timeout value</help> + <valueHelp> + <format>0-2147483647</format> + <description>TCP generic timeout in seconds (default 3600)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + <leafNode name="tcp-rst"> + <properties> + <help>TCP reset timeout value</help> + <valueHelp> + <format>0-2147483647</format> + <description>TCP RST timeout in seconds (default 120)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + <leafNode name="udp"> + <properties> + <help>UDP timeout value</help> + <valueHelp> + <format>0-2147483647</format> + <description>UDP timeout in seconds (default 300)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-2147483647" /> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <node name="sflow"> + <properties> + <help>sFlow settings</help> + </properties> + <children> + <leafNode name="agent-address"> + <properties> + <help>sFlow agent IPv4 address</help> + <valueHelp> + <format>auto</format> + <description>auto select sFlow agent-address (default)</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>sFlow IPv4 agent address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <regex>auto$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="sampling-rate"> + <properties> + <help>sFlow sampling-rate</help> + <valueHelp> + <format>0-4294967295</format> + <description>Sampling rate (1 in N packets)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295" /> + </constraint> + </properties> + </leafNode> + <tagNode name="server"> + <properties> + <help>Server to export sFlow [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 server to export sFlow</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 server to export sFlow</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="port"> + <properties> + <help>sFlow port number</help> + <valueHelp> + <format>1025-65535</format> + <description>sFlow port number (default 6343)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1025-65535" /> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in new file mode 100644 index 000000000..9bb96f1f0 --- /dev/null +++ b/interface-definitions/https.xml.in @@ -0,0 +1,174 @@ +<?xml version="1.0"?> +<!-- HTTPS configuration --> +<interfaceDefinition> + <syntaxVersion component='https' version='2'></syntaxVersion> + <node name="service"> + <children> + <node name="https" owner="${vyos_conf_scripts_dir}/https.py"> + <properties> + <help>HTTPS configuration</help> + <priority>1001</priority> + </properties> + <children> + <tagNode name="virtual-host"> + <properties> + <help>Identifier for virtual host</help> + <constraint> + <regex>[a-zA-Z0-9-_.:]{1,255}</regex> + </constraint> + <constraintErrorMessage>illegal characters in identifier or identifier longer than 255 characters</constraintErrorMessage> + </properties> + <children> + <leafNode name="listen-address"> + <properties> + <help>Address to listen for HTTPS requests</help> + <valueHelp> + <format>ipv4</format> + <description>HTTPS IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>HTTPS IPv6 address</description> + </valueHelp> + <valueHelp> + <format>'*'</format> + <description>any</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + <regex>\*$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name='listen-port'> + <properties> + <help>Port to listen for HTTPS requests; default 443</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="server-name"> + <properties> + <help>Server names: exact, wildcard, or regex</help> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <node name="api" owner="${vyos_conf_scripts_dir}/http-api.py"> + <properties> + <help>VyOS HTTP API configuration</help> + <priority>1002</priority> + </properties> + <children> + <leafNode name="port"> + <properties> + <help>Port for HTTP API service</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <node name="keys"> + <properties> + <help>HTTP API keys</help> + </properties> + <children> + <tagNode name="id"> + <properties> + <help>HTTP API id</help> + </properties> + <children> + <leafNode name="key"> + <properties> + <help>HTTP API plaintext key</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="strict"> + <properties> + <help>Enforce strict path checking</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="debug"> + <properties> + <help>Debug</help> + <valueless/> + <hidden/> + </properties> + </leafNode> + </children> + </node> + <node name="api-restrict"> + <properties> + <help>Restrict api proxy to subset of virtual hosts</help> + </properties> + <children> + <leafNode name="virtual-host"> + <properties> + <help>Restrict proxy to virtual host(s)</help> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="certificates"> + <properties> + <help>TLS certificates</help> + </properties> + <children> + <node name="system-generated-certificate" owner="${vyos_conf_scripts_dir}/vyos_cert.py"> + <properties> + <help>Use an automatically generated self-signed certificate</help> + </properties> + <children> + <leafNode name="lifetime"> + <properties> + <help>Lifetime in days; default is 365</help> + <valueHelp> + <format>1-65535</format> + <description>Number of days</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + <node name="certbot" owner="${vyos_conf_scripts_dir}/le_cert.py"> + <properties> + <help>Request or apply a letsencrypt certificate for domain-name</help> + </properties> + <children> + <leafNode name="domain-name"> + <properties> + <help>Domain name(s) for which to obtain certificate</help> + <multi/> + </properties> + </leafNode> + <leafNode name="email"> + <properties> + <help>Email address to associate with certificate</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/igmp-proxy.xml.in b/interface-definitions/igmp-proxy.xml.in new file mode 100644 index 000000000..74fec6b48 --- /dev/null +++ b/interface-definitions/igmp-proxy.xml.in @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<!-- IGMP Proxy configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="igmp-proxy" owner="${vyos_conf_scripts_dir}/igmp_proxy.py"> + <properties> + <help>Internet Group Management Protocol (IGMP) proxy parameters</help> + <priority>740</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable IGMP proxy</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-quickleave"> + <properties> + <help>Option to disable "quickleave"</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="interface"> + <properties> + <help>Interface for IGMP proxy [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="alt-subnet"> + <properties> + <help>Unicast source networks allowed for multicast traffic to be proxyed</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="role"> + <properties> + <help>Role of this IGMP interface</help> + <completionHelp> + <list>upstream downstream disabled</list> + </completionHelp> + <valueHelp> + <format>upstream</format> + <description>Upstream interface (only 1 allowed)</description> + </valueHelp> + <valueHelp> + <format>downstream</format> + <description>Downstream interface(s) (default)</description> + </valueHelp> + <valueHelp> + <format>disabled</format> + <description>Disabled interface</description> + </valueHelp> + <constraint> + <regex>(upstream|downstream|disabled)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="threshold"> + <properties> + <help>TTL threshold</help> + <valueHelp> + <format>1-255</format> + <description>TTL threshold for the interfaces (default: 1)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>threshold must be between 1 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="whitelist"> + <properties> + <help>Group to whitelist</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/include/accel-auth-mode.xml.i b/interface-definitions/include/accel-auth-mode.xml.i new file mode 100644 index 000000000..e719112db --- /dev/null +++ b/interface-definitions/include/accel-auth-mode.xml.i @@ -0,0 +1,19 @@ +<leafNode name="mode"> + <properties> + <help>Authentication mode used by this server</help> + <valueHelp> + <format>local</format> + <description>Use local username/password configuration</description> + </valueHelp> + <valueHelp> + <format>radius</format> + <description>Use RADIUS server for user autentication</description> + </valueHelp> + <constraint> + <regex>(local|radius)</regex> + </constraint> + <completionHelp> + <list>local radius</list> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/accel-client-ipv6-pool.xml.in b/interface-definitions/include/accel-client-ipv6-pool.xml.in new file mode 100644 index 000000000..455ada6ef --- /dev/null +++ b/interface-definitions/include/accel-client-ipv6-pool.xml.in @@ -0,0 +1,59 @@ +<node name="client-ipv6-pool"> + <properties> + <help>Pool of client IPv6 addresses</help> + </properties> + <children> + <tagNode name="prefix"> + <properties> + <help>Pool of addresses used to assign to clients</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="mask"> + <properties> + <help>Prefix length used for individual client</help> + <valueHelp> + <format><48-128></format> + <description>Client prefix length (default: 64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 48-128"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="delegate"> + <properties> + <help>Subnet used to delegate prefix through DHCPv6-PD (RFC3633)</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="delegation-prefix"> + <properties> + <help>Prefix length delegated to client</help> + <valueHelp> + <format><32-64></format> + <description>Delegated prefix length</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 32-64"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> diff --git a/interface-definitions/include/accel-name-server.xml.in b/interface-definitions/include/accel-name-server.xml.in new file mode 100644 index 000000000..82ed6771d --- /dev/null +++ b/interface-definitions/include/accel-name-server.xml.in @@ -0,0 +1,18 @@ +<leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS) propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> +</leafNode> diff --git a/interface-definitions/include/accel-radius-additions.xml.in b/interface-definitions/include/accel-radius-additions.xml.in new file mode 100644 index 000000000..e37b68514 --- /dev/null +++ b/interface-definitions/include/accel-radius-additions.xml.in @@ -0,0 +1,125 @@ +<node name="radius"> + <children> + <tagNode name="server"> + <children> + <leafNode name="acct-port"> + <properties> + <help>Accounting port</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 1813)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="fail-time"> + <properties> + <help>Mark server unavailable for <n> seconds on failure</help> + <valueHelp> + <format>0-600</format> + <description>Fail time penalty</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-600"/> + </constraint> + <constraintErrorMessage>Fail time must be between 0 and 600 seconds</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="timeout"> + <properties> + <help>Timeout in seconds to wait response from RADIUS server</help> + <valueHelp> + <format>1-60</format> + <description>Timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-60"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 60 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="acct-timeout"> + <properties> + <help>Timeout for Interim-Update packets, terminate session afterwards (default 3 seconds)</help> + <valueHelp> + <format>0-60</format> + <description>Timeout in seconds, 0 to keep active</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-60"/> + </constraint> + <constraintErrorMessage>Timeout must be between 0 and 60 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="max-try"> + <properties> + <help>Number of tries to send Access-Request/Accounting-Request queries</help> + <valueHelp> + <format>1-20</format> + <description>Maximum tries</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-20"/> + </constraint> + <constraintErrorMessage>Maximum tries must be between 1 and 20</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nas-identifier"> + <properties> + <help>NAS-Identifier attribute sent to RADIUS</help> + </properties> + </leafNode> + <leafNode name="nas-ip-address"> + <properties> + <help>NAS-IP-Address attribute sent to RADIUS</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>NAS-IP-Address attribute</description> + </valueHelp> + </properties> + </leafNode> + <node name="dynamic-author"> + <properties> + <help>Dynamic Authorization Extension/Change of Authorization server</help> + </properties> + <children> + <leafNode name="server"> + <properties> + <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address for aynamic authorization server</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> + <valueHelp> + <format>number</format> + <description>TCP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Shared secret for Dynamic Authorization Extension server</help> + </properties> + </leafNode> + </children> + </node> + </children> +</node> diff --git a/interface-definitions/include/accel-wins-server.xml.i b/interface-definitions/include/accel-wins-server.xml.i new file mode 100644 index 000000000..461a65ddf --- /dev/null +++ b/interface-definitions/include/accel-wins-server.xml.i @@ -0,0 +1,13 @@ +<leafNode name="wins-server"> + <properties> + <help>Windows Internet Name Service (WINS) servers propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> +</leafNode> diff --git a/interface-definitions/include/address-ipv4-ipv6-dhcp.xml.i b/interface-definitions/include/address-ipv4-ipv6-dhcp.xml.i new file mode 100644 index 000000000..cca824d89 --- /dev/null +++ b/interface-definitions/include/address-ipv4-ipv6-dhcp.xml.i @@ -0,0 +1,29 @@ +<leafNode name="address"> + <properties> + <help>IP address</help> + <completionHelp> + <list>dhcp dhcpv6</list> + </completionHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>dhcp</format> + <description>Dynamic Host Configuration Protocol</description> + </valueHelp> + <valueHelp> + <format>dhcpv6</format> + <description>Dynamic Host Configuration Protocol for IPv6</description> + </valueHelp> + <constraint> + <validator name="ip-host"/> + <regex>(dhcp|dhcpv6)</regex> + </constraint> + <multi/> + </properties> +</leafNode> diff --git a/interface-definitions/include/address-ipv4-ipv6.xml.i b/interface-definitions/include/address-ipv4-ipv6.xml.i new file mode 100644 index 000000000..a891085bd --- /dev/null +++ b/interface-definitions/include/address-ipv4-ipv6.xml.i @@ -0,0 +1,17 @@ +<leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-host"/> + </constraint> + <multi/> + </properties> +</leafNode> diff --git a/interface-definitions/include/bgp-afi-aggregate-address.xml.i b/interface-definitions/include/bgp-afi-aggregate-address.xml.i new file mode 100644 index 000000000..050ee0074 --- /dev/null +++ b/interface-definitions/include/bgp-afi-aggregate-address.xml.i @@ -0,0 +1,12 @@ +<leafNode name="as-set"> + <properties> + <help>Generate AS-set path information for this aggregate address</help> + <valueless/> + </properties> +</leafNode> +<leafNode name="summary-only"> + <properties> + <help>Announce the aggregate summary network only</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/bgp-afi-redistribute-metric-route-map.xml.i b/interface-definitions/include/bgp-afi-redistribute-metric-route-map.xml.i new file mode 100644 index 000000000..9b3f7a008 --- /dev/null +++ b/interface-definitions/include/bgp-afi-redistribute-metric-route-map.xml.i @@ -0,0 +1,17 @@ +<leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format><1-4294967295></format> + <description>Metric for redistributed routes</description> + </valueHelp> + </properties> +</leafNode> +<leafNode name="route-map"> + <properties> + <help>Route map to filter redistributed routes</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/bgp-neighbor-afi-ipv4-unicast.xml.i b/interface-definitions/include/bgp-neighbor-afi-ipv4-unicast.xml.i new file mode 100644 index 000000000..74afb8851 --- /dev/null +++ b/interface-definitions/include/bgp-neighbor-afi-ipv4-unicast.xml.i @@ -0,0 +1,285 @@ +<node name="ipv4-unicast"> + <properties> + <help>IPv4 BGP neighbor parameters</help> + </properties> + <children> + <node name="allowas-in"> + <properties> + <help>Accept a IPv4-route that contains the local-AS in the as-path</help> + </properties> + <children> + <leafNode name="number"> + <properties> + <help>Number of occurrences of AS number</help> + <valueHelp> + <format><1-10></format> + <description>Number of times AS is allowed in path</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-10"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="as-override"> + <properties> + <help>AS for routes sent to this neighbor to be the local AS</help> + <valueless/> + </properties> + </leafNode> + <node name="attribute-unchanged"> + <properties> + <help>BGP attributes are sent unchanged (IPv4)</help> + </properties> + <children> + <leafNode name="as-path"> + <properties> + <help>Send AS path unchanged (IPv4)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="med"> + <properties> + <help>Send multi-exit discriminator unchanged (IPv4)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="next-hop"> + <properties> + <help>Send nexthop unchanged (IPv4)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="capability"> + <properties> + <help>Advertise capabilities to this neighbor (IPv4)</help> + </properties> + <children> + <node name="orf"> + <properties> + <help>Advertise ORF capability to this neighbor</help> + </properties> + <children> + <node name="prefix-list"> + <properties> + <help>Advertise prefix-list ORF capability to this neighbor</help> + </properties> + <children> + <leafNode name="receive"> + <properties> + <help>Capability to receive the ORF</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="send"> + <properties> + <help>Capability to send the ORF</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="default-originate"> + <properties> + <help>Send default IPv4-route to this neighbor</help> + </properties> + <children> + <leafNode name="route-map"> + <properties> + <help>IPv4-Route-map to specify criteria of the default</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="distribute-list"> + <properties> + <help>Access-list to filter IPv4-route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Access-list to filter outgoing IPv4-route updates to this neighbor</help> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter outgoing IPv4-route updates to this neighbor</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Access-list to filter incoming IPv4-route updates from this neighbor</help> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter incoming IPv4-route updates from this neighbor</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="filter-list"> + <properties> + <help>As-path-list to filter IPv4-route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>As-path-list to filter outgoing IPv4-route updates to this neighbor</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>As-path-list to filter incoming IPv4-route updates from this neighbor</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="maximum-prefix"> + <properties> + <help>Maximum number of IPv4-prefixes to accept from this neighbor</help> + <valueHelp> + <format><1-4294967295></format> + <description>Prefix limit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <node name="nexthop-self"> + <properties> + <help>Nexthop for IPv4-routes sent to this neighbor to be the local router</help> + </properties> + <children> + <leafNode name="force"> + <properties> + <help>Set the next hop to self for reflected routes</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="prefix-list"> + <properties> + <help>IPv4-Prefix-list to filter route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>IPv4-Prefix-list to filter outgoing route updates to this neighbor</help> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>IPv4-Prefix-list to filter incoming route updates from this neighbor</help> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="remove-private-as"> + <properties> + <help>Remove private AS numbers from AS path in outbound IPv4-route updates</help> + <valueless/> + </properties> + </leafNode> + <node name="route-map"> + <properties> + <help>Route-map to filter IPv4-route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>IPv4-Route-map to filter outgoing route updates to this neighbor</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>IPv4-Route-map to filter incoming route updates from this neighbor</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="route-reflector-client"> + <properties> + <help>Neighbor as a IPv4-route reflector client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="route-server-client"> + <properties> + <help>Neighbor is IPv4-route server client</help> + <valueless/> + </properties> + </leafNode> + <node name="soft-reconfiguration"> + <properties> + <help>Soft reconfiguration for neighbor (IPv4)</help> + </properties> + <children> + <leafNode name="inbound"> + <properties> + <help>Inbound soft reconfiguration for this neighbor [REQUIRED]</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="unsuppress-map"> + <properties> + <help>Route-map to selectively unsuppress suppressed IPv4-routes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="weight"> + <properties> + <help>Default weight for routes from this neighbor</help> + <valueHelp> + <format><1-65535></format> + <description>Weight for routes from this neighbor</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/bgp-neighbor-afi-ipv6-unicast.xml.i b/interface-definitions/include/bgp-neighbor-afi-ipv6-unicast.xml.i new file mode 100644 index 000000000..e95cb6dd8 --- /dev/null +++ b/interface-definitions/include/bgp-neighbor-afi-ipv6-unicast.xml.i @@ -0,0 +1,322 @@ +<node name="ipv6-unicast"> + <properties> + <help>IPv6 BGP neighbor parameters</help> + </properties> + <children> + <node name="allowas-in"> + <properties> + <help>Accept a IPv6-route that contains the local-AS in the as-path</help> + </properties> + <children> + <leafNode name="number"> + <properties> + <help>Number of occurrences of AS number</help> + <valueHelp> + <format><1-10></format> + <description>Number of times AS is allowed in path</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-10"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="as-override"> + <properties> + <help>AS for routes sent to this neighbor to be the local AS</help> + <valueless/> + </properties> + </leafNode> + <node name="attribute-unchanged"> + <properties> + <help>BGP attributes are sent unchanged</help> + </properties> + <children> + <leafNode name="as-path"> + <properties> + <help>Send AS path unchanged</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="med"> + <properties> + <help>Send multi-exit discriminator unchanged</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="next-hop"> + <properties> + <help>Send nexthop unchanged</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="capability"> + <properties> + <help>Advertise capabilities to this neighbor (IPv6)</help> + </properties> + <children> + <node name="orf"> + <properties> + <help>Advertise ORF capability to this neighbor</help> + </properties> + <children> + <node name="prefix-list"> + <properties> + <help>Advertise prefix-list ORF capability to this neighbor</help> + </properties> + <children> + <leafNode name="receive"> + <properties> + <help>Capability to receive the ORF</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="send"> + <properties> + <help>Capability to send the ORF</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="default-originate"> + <properties> + <help>Send default IPv6-route to this neighbor</help> + </properties> + <children> + <leafNode name="route-map"> + <properties> + <help>Route-map to specify criteria of the default</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="disable-send-community"> + <properties> + <help>Disable sending community attributes to this neighbor</help> + </properties> + <children> + <leafNode name="extended"> + <properties> + <help>Disable sending extended community attributes to this neighbor</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="standard"> + <properties> + <help>Disable sending standard community attributes to this neighbor</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="distribute-list"> + <properties> + <help>Access-list to filter route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Access-list to filter outgoing route updates to this neighbor</help> + <completionHelp> + <path>policy access-list6</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter outgoing route updates to this neighbor</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Access-list to filter incoming route updates from this neighbor</help> + <completionHelp> + <path>policy access-list6</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter incoming route updates from this neighbor</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="filter-list"> + <properties> + <help>As-path-list to filter route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>As-path-list to filter outgoing route updates to this neighbor</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>As-path-list to filter incoming route updates from this neighbor</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="maximum-prefix"> + <properties> + <help>Maximum number of prefixes to accept from this neighbor</help> + <valueHelp> + <format><1-4294967295></format> + <description>Prefix limit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <node name="nexthop-local"> + <properties> + <help>Nexthop attributes</help> + </properties> + <children> + <leafNode name="unchanged"> + <properties> + <help>Leave link-local nexthop unchanged for this peer</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="nexthop-self"> + <properties> + <help>Nexthop for IPv6-routes sent to this neighbor to be the local router</help> + </properties> + <children> + <leafNode name="force"> + <properties> + <help>Set the next hop to self for reflected routes</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="peer-group"> + <properties> + <help>IPv6 peer group for this peer</help> + </properties> + </leafNode> + <node name="prefix-list"> + <properties> + <help>Prefix-list to filter route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Prefix-list to filter outgoing route updates to this neighbor</help> + <completionHelp> + <path>policy prefix-list6</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Prefix-list to filter incoming route updates from this neighbor</help> + <completionHelp> + <path>policy prefix-list6</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="remove-private-as"> + <properties> + <help>Remove private AS numbers from AS path in outbound route updates</help> + <valueless/> + </properties> + </leafNode> + <node name="route-map"> + <properties> + <help>Route-map to filter route updates to/from this neighbor</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Route-map to filter outgoing route updates to this neighbor</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Route-map to filter incoming route updates from this neighbor</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="route-reflector-client"> + <properties> + <help>Neighbor as a IPv6-route reflector client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="route-server-client"> + <properties> + <help>Neighbor is IPv6-route server client</help> + <valueless/> + </properties> + </leafNode> + <node name="soft-reconfiguration"> + <properties> + <help>Soft reconfiguration for neighbor (IPv6)</help> + </properties> + <children> + <leafNode name="inbound"> + <properties> + <help>Inbound soft reconfiguration for this neighbor [REQUIRED]</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="unsuppress-map"> + <properties> + <help>Route-map to selectively unsuppress suppressed IPv6-routes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="weight"> + <properties> + <help>Default weight for routes from this neighbor</help> + <valueHelp> + <format><1-65535></format> + <description>Weight for routes from this neighbor</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/bgp-peer-group-afi-ipv4-unicast.xml.i b/interface-definitions/include/bgp-peer-group-afi-ipv4-unicast.xml.i new file mode 100644 index 000000000..df051ace5 --- /dev/null +++ b/interface-definitions/include/bgp-peer-group-afi-ipv4-unicast.xml.i @@ -0,0 +1,301 @@ +<node name="ipv4-unicast"> + <properties> + <help>IPv4 BGP peer group parameters</help> + </properties> + <children> + <node name="allowas-in"> + <properties> + <help>Accept a route that contains the local-AS in the as-path</help> + </properties> + <children> + <leafNode name="number"> + <properties> + <help>Number of occurrences of AS number</help> + <valueHelp> + <format><1-10></format> + <description>Number of times AS is allowed in path</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-10"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="attribute-unchanged"> + <properties> + <help>BGP attributes are sent unchanged</help> + </properties> + <children> + <leafNode name="as-path"> + <properties> + <help>Send AS path unchanged</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="med"> + <properties> + <help>Send multi-exit discriminator unchanged</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="next-hop"> + <properties> + <help>Send nexthop unchanged</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="capability"> + <properties> + <help>Advertise capabilities to this peer-group</help> + </properties> + <children> + <leafNode name="dynamic"> + <properties> + <help>Advertise dynamic capability to this peer-group</help> + <valueless/> + </properties> + </leafNode> + <node name="orf"> + <properties> + <help>Advertise ORF capability to this peer-group</help> + </properties> + <children> + <node name="prefix-list"> + <properties> + <help>Advertise prefix-list ORF capability to this peer-group</help> + </properties> + <children> + <leafNode name="receive"> + <properties> + <help>Capability to receive the ORF</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="send"> + <properties> + <help>Capability to send the ORF</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="default-originate"> + <properties> + <help>Send default route to this peer-group</help> + </properties> + <children> + <leafNode name="route-map"> + <properties> + <help>Route-map to specify criteria of the default</help> + </properties> + </leafNode> + </children> + </node> + <node name="disable-send-community"> + <properties> + <help>Disable sending community attributes to this peer-group</help> + </properties> + <children> + <leafNode name="extended"> + <properties> + <help>Disable sending extended community attributes to this peer-group</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="standard"> + <properties> + <help>Disable sending standard community attributes to this peer-group</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="distribute-list"> + <properties> + <help>Access-list to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Access-list to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter outgoing route updates to this peer-group</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Access-list to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter incoming route updates from this peer-group</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="filter-list"> + <properties> + <help>As-path-list to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>As-path-list to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>As-path-list to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="maximum-prefix"> + <properties> + <help>Maximum number of prefixes to accept from this peer-group</help> + <valueHelp> + <format><1-4294967295></format> + <description>Prefix limit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <node name="nexthop-self"> + <properties> + <help>Nexthop for routes sent to this peer-group to be the local router</help> + </properties> + <children> + <leafNode name="force"> + <properties> + <help>Set the next hop to self for reflected routes</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="prefix-list"> + <properties> + <help>Prefix-list to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Prefix-list to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Prefix-list to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="remove-private-as"> + <properties> + <help>Remove private AS numbers from AS path in outbound route updates</help> + <valueless/> + </properties> + </leafNode> + <node name="route-map"> + <properties> + <help>Route-map to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Route-map to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Route-map to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="route-reflector-client"> + <properties> + <help>Peer-group as a route reflector client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="route-server-client"> + <properties> + <help>Peer-group as route server client</help> + <valueless/> + </properties> + </leafNode> + <node name="soft-reconfiguration"> + <properties> + <help>Soft reconfiguration for peer-group</help> + </properties> + <children> + <leafNode name="inbound"> + <properties> + <help>Inbound soft reconfiguration for this peer-group [REQUIRED]</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="unsuppress-map"> + <properties> + <help>Route-map to selectively unsuppress suppressed routes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="weight"> + <properties> + <help>Default weight for routes from this peer-group</help> + <valueHelp> + <format><1-65535></format> + <description>Weight for routes from this peer-group</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/bgp-peer-group-afi-ipv6-unicast.xml.i b/interface-definitions/include/bgp-peer-group-afi-ipv6-unicast.xml.i new file mode 100644 index 000000000..a381e02f0 --- /dev/null +++ b/interface-definitions/include/bgp-peer-group-afi-ipv6-unicast.xml.i @@ -0,0 +1,317 @@ +<node name="ipv6-unicast"> + <properties> + <help>IPv6 BGP neighbor parameters</help> + </properties> + <children> + <node name="allowas-in"> + <properties> + <help>Accept a IPv6-route that contains the local-AS in the as-path</help> + </properties> + <children> + <leafNode name="number"> + <properties> + <help>Number of occurrences of AS number</help> + <valueHelp> + <format><1-10></format> + <description>Number of times AS is allowed in path</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-10"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="attribute-unchanged"> + <properties> + <help>BGP attributes are sent unchanged</help> + </properties> + <children> + <leafNode name="as-path"> + <properties> + <help>Send AS path unchanged</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="med"> + <properties> + <help>Send multi-exit discriminator unchanged</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="next-hop"> + <properties> + <help>Send nexthop unchanged</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="capability"> + <properties> + <help>Advertise capabilities to this peer-group</help> + </properties> + <children> + <leafNode name="dynamic"> + <properties> + <help>Advertise dynamic capability to this peer-group</help> + <valueless/> + </properties> + </leafNode> + <node name="orf"> + <properties> + <help>Advertise ORF capability to this peer-group</help> + </properties> + <children> + <node name="prefix-list"> + <properties> + <help>Advertise prefix-list ORF capability to this peer-group</help> + </properties> + <children> + <leafNode name="receive"> + <properties> + <help>Capability to receive the ORF</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="send"> + <properties> + <help>Capability to send the ORF</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="default-originate"> + <properties> + <help>Send default route to this peer-group</help> + </properties> + <children> + <leafNode name="route-map"> + <properties> + <help>Route-map to specify criteria of the default</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <node name="disable-send-community"> + <properties> + <help>Disable sending community attributes to this peer-group</help> + </properties> + <children> + <leafNode name="extended"> + <properties> + <help>Disable sending extended community attributes to this peer-group</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="standard"> + <properties> + <help>Disable sending standard community attributes to this peer-group</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="distribute-list"> + <properties> + <help>Access-list to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Access-list to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy access-list6</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter outgoing route updates to this peer-group</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Access-list to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy access-list6</path> + </completionHelp> + <valueHelp> + <format><1-65535></format> + <description>Access-list to filter incoming route updates from this peer-group</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="filter-list"> + <properties> + <help>As-path-list to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>As-path-list to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>As-path-list to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="maximum-prefix"> + <properties> + <help>Maximum number of prefixes to accept from this peer-group</help> + <valueHelp> + <format><1-4294967295></format> + <description>Prefix limit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <node name="nexthop-local"> + <properties> + <help>Nexthop attributes</help> + </properties> + <children> + <leafNode name="unchanged"> + <properties> + <help>Leave link-local nexthop unchanged for this peer</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="nexthop-self"> + <properties> + <help>Nexthop for routes sent to this peer-group to be the local router</help> + </properties> + <children> + <leafNode name="force"> + <properties> + <help>Set the next hop to self for reflected routes</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="prefix-list"> + <properties> + <help>Prefix-list to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Prefix-list to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy prefix-list6</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Prefix-list to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy prefix-list6</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="remove-private-as"> + <properties> + <help>Remove private AS numbers from AS path in outbound route updates</help> + <valueless/> + </properties> + </leafNode> + <node name="route-map"> + <properties> + <help>Route-map to filter route updates to/from this peer-group</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>Route-map to filter outgoing route updates to this peer-group</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="import"> + <properties> + <help>Route-map to filter incoming route updates from this peer-group</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="route-reflector-client"> + <properties> + <help>Peer-group as a route reflector client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="route-server-client"> + <properties> + <help>Peer-group as route server client</help> + <valueless/> + </properties> + </leafNode> + <node name="soft-reconfiguration"> + <properties> + <help>Soft reconfiguration for peer-group</help> + </properties> + <children> + <leafNode name="inbound"> + <properties> + <help>Inbound soft reconfiguration for this peer-group [REQUIRED]</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="unsuppress-map"> + <properties> + <help>Route-map to selectively unsuppress suppressed routes</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="weight"> + <properties> + <help>Default weight for routes from this peer-group</help> + <valueHelp> + <format><1-65535></format> + <description>Weight for routes from this peer-group</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/dhcp-options.xml.i b/interface-definitions/include/dhcp-options.xml.i new file mode 100644 index 000000000..9989291fc --- /dev/null +++ b/interface-definitions/include/dhcp-options.xml.i @@ -0,0 +1,22 @@ +<node name="dhcp-options"> + <properties> + <help>DHCP client settings/options</help> + </properties> + <children> + <leafNode name="client-id"> + <properties> + <help>DHCP client identifier</help> + </properties> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>DHCP client host name (overrides system host name)</help> + </properties> + </leafNode> + <leafNode name="vendor-class-id"> + <properties> + <help>DHCP client vendor type</help> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/dhcpv6-options.xml.i b/interface-definitions/include/dhcpv6-options.xml.i new file mode 100644 index 000000000..b0a806806 --- /dev/null +++ b/interface-definitions/include/dhcpv6-options.xml.i @@ -0,0 +1,86 @@ +<node name="dhcpv6-options"> + <properties> + <help>DHCPv6 client settings/options</help> + </properties> + <children> + <leafNode name="parameters-only"> + <properties> + <help>Acquire only config parameters, no address</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="pd"> + <properties> + <help>DHCPv6 prefix delegation interface statement</help> + <valueHelp> + <format>instance number</format> + <description>Prefix delegation instance (>= 0)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--non-negative"/> + </constraint> + </properties> + <children> + <leafNode name="length"> + <properties> + <help>Request IPv6 prefix length from peer</help> + <valueHelp> + <format>32-64</format> + <description>Length of delegated prefix</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 32-64"/> + </constraint> + </properties> + <defaultValue>64</defaultValue> + </leafNode> + <tagNode name="interface"> + <properties> + <help>Delegate IPv6 prefix from provider to this interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>Local interface address assigned to interface</help> + <valueHelp> + <format>>0</format> + <description>Used to form IPv6 interface address (default: EUI-64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--non-negative"/> + </constraint> + </properties> + </leafNode> + <leafNode name="sla-id"> + <properties> + <help>Interface site-Level aggregator (SLA)</help> + <valueHelp> + <format>0-128</format> + <description>Decimal integer which fits in the length of SLA IDs</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-128"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <leafNode name="rapid-commit"> + <properties> + <help>Wait for immediate reply instead of advertisements</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="temporary"> + <properties> + <help>IPv6 temporary address</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/interface-arp-cache-timeout.xml.i b/interface-definitions/include/interface-arp-cache-timeout.xml.i new file mode 100644 index 000000000..e65321158 --- /dev/null +++ b/interface-definitions/include/interface-arp-cache-timeout.xml.i @@ -0,0 +1,14 @@ +<leafNode name="arp-cache-timeout"> + <properties> + <help>ARP cache entry timeout in seconds</help> + <valueHelp> + <format>1-86400</format> + <description>ARP cache entry timout in seconds (default 30)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-86400"/> + </constraint> + <constraintErrorMessage>ARP cache entry timeout must be between 1 and 86400 seconds</constraintErrorMessage> + </properties> + <defaultValue>30</defaultValue> +</leafNode> diff --git a/interface-definitions/include/interface-description.xml.i b/interface-definitions/include/interface-description.xml.i new file mode 100644 index 000000000..961533e26 --- /dev/null +++ b/interface-definitions/include/interface-description.xml.i @@ -0,0 +1,9 @@ +<leafNode name="description"> + <properties> + <help>Interface specific description</help> + <constraint> + <regex>.{1,256}$</regex> + </constraint> + <constraintErrorMessage>Description too long (limit 256 characters)</constraintErrorMessage> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-disable-arp-filter.xml.i b/interface-definitions/include/interface-disable-arp-filter.xml.i new file mode 100644 index 000000000..ec3f51b2d --- /dev/null +++ b/interface-definitions/include/interface-disable-arp-filter.xml.i @@ -0,0 +1,6 @@ +<leafNode name="disable-arp-filter"> + <properties> + <help>Disable ARP filter on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-disable-link-detect.xml.i b/interface-definitions/include/interface-disable-link-detect.xml.i new file mode 100644 index 000000000..619cd03b0 --- /dev/null +++ b/interface-definitions/include/interface-disable-link-detect.xml.i @@ -0,0 +1,6 @@ +<leafNode name="disable-link-detect"> + <properties> + <help>Ignore link state changes</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-disable.xml.i b/interface-definitions/include/interface-disable.xml.i new file mode 100644 index 000000000..7bd3df5da --- /dev/null +++ b/interface-definitions/include/interface-disable.xml.i @@ -0,0 +1,6 @@ +<leafNode name="disable"> + <properties> + <help>Administratively disable interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-enable-arp-accept.xml.i b/interface-definitions/include/interface-enable-arp-accept.xml.i new file mode 100644 index 000000000..69f26b322 --- /dev/null +++ b/interface-definitions/include/interface-enable-arp-accept.xml.i @@ -0,0 +1,6 @@ +<leafNode name="enable-arp-accept"> + <properties> + <help>Enable ARP accept on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-enable-arp-announce.xml.i b/interface-definitions/include/interface-enable-arp-announce.xml.i new file mode 100644 index 000000000..8d51874c1 --- /dev/null +++ b/interface-definitions/include/interface-enable-arp-announce.xml.i @@ -0,0 +1,6 @@ +<leafNode name="enable-arp-announce"> + <properties> + <help>Enable ARP announce on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-enable-arp-ignore.xml.i b/interface-definitions/include/interface-enable-arp-ignore.xml.i new file mode 100644 index 000000000..9adc0f17e --- /dev/null +++ b/interface-definitions/include/interface-enable-arp-ignore.xml.i @@ -0,0 +1,6 @@ +<leafNode name="enable-arp-ignore"> + <properties> + <help>Enable ARP ignore on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-enable-proxy-arp.xml.i b/interface-definitions/include/interface-enable-proxy-arp.xml.i new file mode 100644 index 000000000..14ab08875 --- /dev/null +++ b/interface-definitions/include/interface-enable-proxy-arp.xml.i @@ -0,0 +1,6 @@ +<leafNode name="enable-proxy-arp"> + <properties> + <help>Enable proxy-arp on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-hw-id.xml.i b/interface-definitions/include/interface-hw-id.xml.i new file mode 100644 index 000000000..318ddd1c4 --- /dev/null +++ b/interface-definitions/include/interface-hw-id.xml.i @@ -0,0 +1,12 @@ +<leafNode name="hw-id"> + <properties> + <help>Associate Ethernet Interface with given Media Access Control (MAC) address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware Media Access Control (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-ipv4.xml.i b/interface-definitions/include/interface-ipv4.xml.i new file mode 100644 index 000000000..15932a9d3 --- /dev/null +++ b/interface-definitions/include/interface-ipv4.xml.i @@ -0,0 +1,11 @@ +<node name="ip"> + <properties> + <help>IPv4 routing parameters</help> + </properties> + <children> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + </children> +</node> diff --git a/interface-definitions/include/interface-ipv6.xml.i b/interface-definitions/include/interface-ipv6.xml.i new file mode 100644 index 000000000..23362f75a --- /dev/null +++ b/interface-definitions/include/interface-ipv6.xml.i @@ -0,0 +1,10 @@ +<node name="ipv6"> + <properties> + <help>IPv6 routing parameters</help> + </properties> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> +</node> diff --git a/interface-definitions/include/interface-mac.xml.i b/interface-definitions/include/interface-mac.xml.i new file mode 100644 index 000000000..7b2456236 --- /dev/null +++ b/interface-definitions/include/interface-mac.xml.i @@ -0,0 +1,12 @@ +<leafNode name="mac"> + <properties> + <help>Media Access Control (MAC) address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-mtu-1200-9000.xml.i b/interface-definitions/include/interface-mtu-1200-9000.xml.i new file mode 100644 index 000000000..de48db65e --- /dev/null +++ b/interface-definitions/include/interface-mtu-1200-9000.xml.i @@ -0,0 +1,14 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>1200-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1200-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 1200 and 9000</constraintErrorMessage> + </properties> + <defaultValue>1500</defaultValue> +</leafNode> diff --git a/interface-definitions/include/interface-mtu-1450-9000.xml.i b/interface-definitions/include/interface-mtu-1450-9000.xml.i new file mode 100644 index 000000000..d15987394 --- /dev/null +++ b/interface-definitions/include/interface-mtu-1450-9000.xml.i @@ -0,0 +1,14 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>1450-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1450-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 1450 and 9000</constraintErrorMessage> + </properties> + <defaultValue>1500</defaultValue> +</leafNode> diff --git a/interface-definitions/include/interface-mtu-64-8024.xml.i b/interface-definitions/include/interface-mtu-64-8024.xml.i new file mode 100644 index 000000000..e60867e35 --- /dev/null +++ b/interface-definitions/include/interface-mtu-64-8024.xml.i @@ -0,0 +1,14 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>64-8024</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 64-8024"/> + </constraint> + <constraintErrorMessage>MTU must be between 64 and 8024</constraintErrorMessage> + </properties> + <defaultValue>1500</defaultValue> +</leafNode> diff --git a/interface-definitions/include/interface-mtu-68-1500.xml.i b/interface-definitions/include/interface-mtu-68-1500.xml.i new file mode 100644 index 000000000..d47efd2c9 --- /dev/null +++ b/interface-definitions/include/interface-mtu-68-1500.xml.i @@ -0,0 +1,14 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>68-1500</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-1500"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 1500</constraintErrorMessage> + </properties> + <defaultValue>1500</defaultValue> +</leafNode> diff --git a/interface-definitions/include/interface-mtu-68-9000.xml.i b/interface-definitions/include/interface-mtu-68-9000.xml.i new file mode 100644 index 000000000..8fae2043c --- /dev/null +++ b/interface-definitions/include/interface-mtu-68-9000.xml.i @@ -0,0 +1,14 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>68-9000</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-9000"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 9000</constraintErrorMessage> + </properties> + <defaultValue>1500</defaultValue> +</leafNode> diff --git a/interface-definitions/include/interface-proxy-arp-pvlan.xml.i b/interface-definitions/include/interface-proxy-arp-pvlan.xml.i new file mode 100644 index 000000000..7e72b3800 --- /dev/null +++ b/interface-definitions/include/interface-proxy-arp-pvlan.xml.i @@ -0,0 +1,6 @@ +<leafNode name="proxy-arp-pvlan"> + <properties> + <help>Enable private VLAN proxy ARP on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-vrf.xml.i b/interface-definitions/include/interface-vrf.xml.i new file mode 100644 index 000000000..355e7f0f3 --- /dev/null +++ b/interface-definitions/include/interface-vrf.xml.i @@ -0,0 +1,12 @@ +<leafNode name="vrf"> + <properties> + <help>VRF instance name</help> + <valueHelp> + <format>text</format> + <description>VRF instance name</description> + </valueHelp> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/ipv6-address.xml.i b/interface-definitions/include/ipv6-address.xml.i new file mode 100644 index 000000000..34f54e4c1 --- /dev/null +++ b/interface-definitions/include/ipv6-address.xml.i @@ -0,0 +1,29 @@ +<node name="address"> + <children> + <leafNode name="autoconf"> + <properties> + <help>Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="eui64"> + <properties> + <help>Prefix for IPv6 address with MAC-based EUI-64</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 network and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="no-default-link-local"> + <properties> + <help>Remove the default link-local address from the interface</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/ipv6-disable-forwarding.xml.i b/interface-definitions/include/ipv6-disable-forwarding.xml.i new file mode 100644 index 000000000..3f90c7e34 --- /dev/null +++ b/interface-definitions/include/ipv6-disable-forwarding.xml.i @@ -0,0 +1,6 @@ +<leafNode name="disable-forwarding"> + <properties> + <help>Disable IPv6 forwarding on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i b/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i new file mode 100644 index 000000000..728187560 --- /dev/null +++ b/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i @@ -0,0 +1,16 @@ +<leafNode name="dup-addr-detect-transmits"> + <properties> + <help>Number of NS messages to send while performing DAD (default: 1)</help> + <valueHelp> + <format>1-n</format> + <description>Number of NS messages to send while performing DAD</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Disable Duplicate Address Dectection (DAD)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--non-negative"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/isis-redistribute-ipv4.xml.i b/interface-definitions/include/isis-redistribute-ipv4.xml.i new file mode 100644 index 000000000..f90900da1 --- /dev/null +++ b/interface-definitions/include/isis-redistribute-ipv4.xml.i @@ -0,0 +1,82 @@ +<node name="level-1"> + <properties> + <help>Redistribute into level-1</help> + </properties> + <children> + <leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format><0-16777215></format> + <description>ISIS default metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + <tagNode name="route-map"> + <properties> + <help>Route map reference</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + <children> + <leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format><0-16777215></format> + <description>ISIS default metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> +<node name="level-2"> + <properties> + <help>Redistribute into level-2</help> + </properties> + <children> + <leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format><0-16777215></format> + <description>ISIS default metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + <tagNode name="route-map"> + <properties> + <help>Route map reference</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + <children> + <leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format><0-16777215></format> + <description>ISIS default metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> diff --git a/interface-definitions/include/nat-address.xml.i b/interface-definitions/include/nat-address.xml.i new file mode 100644 index 000000000..933dae07b --- /dev/null +++ b/interface-definitions/include/nat-address.xml.i @@ -0,0 +1,37 @@ +<leafNode name="address"> + <properties> + <help>IP address, subnet, or range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to match</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <valueHelp> + <format>ipv4range</format> + <description>IPv4 address range to match</description> + </valueHelp> + <valueHelp> + <format>!ipv4</format> + <description>Match everything except the specified address</description> + </valueHelp> + <valueHelp> + <format>!ipv4net</format> + <description>Match everything except the specified prefix</description> + </valueHelp> + <valueHelp> + <format>!ipv4range</format> + <description>Match everything except the specified range</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + <validator name="ipv4-range"/> + <validator name="ipv4-address-exclude"/> + <validator name="ipv4-prefix-exclude"/> + <validator name="ipv4-range-exclude"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/nat-interface.xml.i b/interface-definitions/include/nat-interface.xml.i new file mode 100644 index 000000000..c49483297 --- /dev/null +++ b/interface-definitions/include/nat-interface.xml.i @@ -0,0 +1,9 @@ +<leafNode name="outbound-interface"> + <properties> + <help>Outbound interface of NAT traffic</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/nat-port.xml.i b/interface-definitions/include/nat-port.xml.i new file mode 100644 index 000000000..24803ae05 --- /dev/null +++ b/interface-definitions/include/nat-port.xml.i @@ -0,0 +1,17 @@ +<leafNode name="port"> + <properties> + <help>Port number</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <valueHelp> + <format>start-end</format> + <description>Numbered port range (e.g., 1001-1005)</description> + </valueHelp> + <valueHelp> + <format> </format> + <description>\n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005'</description> + </valueHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i new file mode 100644 index 000000000..a2d058479 --- /dev/null +++ b/interface-definitions/include/nat-rule.xml.i @@ -0,0 +1,303 @@ +<tagNode name="rule"> + <properties> + <help>Rule number for NAT</help> + <valueHelp> + <format>1-999999</format> + <description>Number for this NAT rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + <leafNode name="description"> + <properties> + <help>Rule description</help> + </properties> + </leafNode> + <node name="destination"> + <properties> + <help>NAT destination parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + <leafNode name="disable"> + <properties> + <help>Disable NAT rule</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="exclude"> + <properties> + <help>Exclude packets matching this rule from NAT</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="log"> + <properties> + <help>NAT rule logging</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="protocol"> + <properties> + <help>Protocol to NAT</help> + <completionHelp> + <list>all ip hopopt icmp igmp ggp ipencap st tcp egp igp pup udp tcp_udp hmp xns-idp rdp iso-tp4 dccp xtp ddp idpr-cmtp ipv6 ipv6-route ipv6-frag idrp rsvp gre esp ah skip ipv6-icmp ipv6-nonxt ipv6-opts rspf vmtp eigrp ospf ax.25 ipip etherip encap 99 pim ipcomp vrrp l2tp isis sctp fc mobility-header udplite mpls-in-ip manet hip shim6 wesp rohc</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All IP protocols</description> + </valueHelp> + <valueHelp> + <format>ip</format> + <description>Internet Protocol, pseudo protocol number</description> + </valueHelp> + <valueHelp> + <format>hopopt</format> + <description>IPv6 Hop-by-Hop Option [RFC1883]</description> + </valueHelp> + <valueHelp> + <format>icmp</format> + <description>internet control message protocol</description> + </valueHelp> + <valueHelp> + <format>igmp</format> + <description>Internet Group Management</description> + </valueHelp> + <valueHelp> + <format>ggp</format> + <description>gateway-gateway protocol</description> + </valueHelp> + <valueHelp> + <format>ipencap</format> + <description>IP encapsulated in IP (officially IP)</description> + </valueHelp> + <valueHelp> + <format>st</format> + <description>ST datagram mode</description> + </valueHelp> + <valueHelp> + <format>tcp</format> + <description>transmission control protocol</description> + </valueHelp> + <valueHelp> + <format>egp</format> + <description>exterior gateway protocol</description> + </valueHelp> + <valueHelp> + <format>igp</format> + <description>any private interior gateway (Cisco)</description> + </valueHelp> + <valueHelp> + <format>pup</format> + <description>PARC universal packet protocol</description> + </valueHelp> + <valueHelp> + <format>udp</format> + <description>user datagram protocol</description> + </valueHelp> + <valueHelp> + <format>tcp_udp</format> + <description>Both TCP and UDP</description> + </valueHelp> + <valueHelp> + <format>hmp</format> + <description>host monitoring protocol</description> + </valueHelp> + <valueHelp> + <format>xns-idp</format> + <description>Xerox NS IDP</description> + </valueHelp> + <valueHelp> + <format>rdp</format> + <description>"reliable datagram" protocol</description> + </valueHelp> + <valueHelp> + <format>iso-tp4</format> + <description>ISO Transport Protocol class 4 [RFC905]</description> + </valueHelp> + <valueHelp> + <format>dccp</format> + <description>Datagram Congestion Control Prot. [RFC4340]</description> + </valueHelp> + <valueHelp> + <format>xtp</format> + <description>Xpress Transfer Protocol</description> + </valueHelp> + <valueHelp> + <format>ddp</format> + <description>Datagram Delivery Protocol</description> + </valueHelp> + <valueHelp> + <format>idpr-cmtp</format> + <description>IDPR Control Message Transport</description> + </valueHelp> + <valueHelp> + <format>Ipv6</format> + <description>Internet Protocol, version 6</description> + </valueHelp> + <valueHelp> + <format>ipv6-route</format> + <description>Routing Header for IPv6</description> + </valueHelp> + <valueHelp> + <format>ipv6-frag</format> + <description>Fragment Header for IPv6</description> + </valueHelp> + <valueHelp> + <format>idrp</format> + <description>Inter-Domain Routing Protocol</description> + </valueHelp> + <valueHelp> + <format>rsvp</format> + <description>Reservation Protocol</description> + </valueHelp> + <valueHelp> + <format>gre</format> + <description>General Routing Encapsulation</description> + </valueHelp> + <valueHelp> + <format>esp</format> + <description>Encap Security Payload [RFC2406]</description> + </valueHelp> + <valueHelp> + <format>ah</format> + <description>Authentication Header [RFC2402]</description> + </valueHelp> + <valueHelp> + <format>skip</format> + <description>SKIP</description> + </valueHelp> + <valueHelp> + <format>ipv6-icmp</format> + <description>ICMP for IPv6</description> + </valueHelp> + <valueHelp> + <format>ipv6-nonxt</format> + <description>No Next Header for IPv6</description> + </valueHelp> + <valueHelp> + <format>ipv6-opts</format> + <description>Destination Options for IPv6</description> + </valueHelp> + <valueHelp> + <format>rspf</format> + <description>Radio Shortest Path First (officially CPHB)</description> + </valueHelp> + <valueHelp> + <format>vmtp</format> + <description>Versatile Message Transport</description> + </valueHelp> + <valueHelp> + <format>eigrp</format> + <description>Enhanced Interior Routing Protocol (Cisco)</description> + </valueHelp> + <valueHelp> + <format>ospf</format> + <description>Open Shortest Path First IGP</description> + </valueHelp> + <valueHelp> + <format>ax.25</format> + <description>AX.25 frames</description> + </valueHelp> + <valueHelp> + <format>ipip</format> + <description>IP-within-IP Encapsulation Protocol</description> + </valueHelp> + <valueHelp> + <format>etherip</format> + <description>Ethernet-within-IP Encapsulation [RFC3378]</description> + </valueHelp> + <valueHelp> + <format>encap</format> + <description>Yet Another IP encapsulation [RFC1241]</description> + </valueHelp> + <valueHelp> + <format>99</format> + <description>Any private encryption scheme</description> + </valueHelp> + <valueHelp> + <format>pim</format> + <description>Protocol Independent Multicast</description> + </valueHelp> + <valueHelp> + <format>ipcomp</format> + <description>IP Payload Compression Protocol</description> + </valueHelp> + <valueHelp> + <format>vrrp</format> + <description>Virtual Router Redundancy Protocol [RFC5798]</description> + </valueHelp> + <valueHelp> + <format>l2tp</format> + <description>Layer Two Tunneling Protocol [RFC2661]</description> + </valueHelp> + <valueHelp> + <format>isis</format> + <description>IS-IS over IPv4</description> + </valueHelp> + <valueHelp> + <format>sctp</format> + <description>Stream Control Transmission Protocol</description> + </valueHelp> + <valueHelp> + <format>fc</format> + <description>Fibre Channel</description> + </valueHelp> + <valueHelp> + <format>mobility-header</format> + <description>Mobility Support for IPv6 [RFC3775]</description> + </valueHelp> + <valueHelp> + <format>udplite</format> + <description>UDP-Lite [RFC3828]</description> + </valueHelp> + <valueHelp> + <format>mpls-in-ip</format> + <description>MPLS-in-IP [RFC4023]</description> + </valueHelp> + <valueHelp> + <format>manet</format> + <description>MANET Protocols [RFC5498]</description> + </valueHelp> + <valueHelp> + <format>hip</format> + <description>Host Identity Protocol</description> + </valueHelp> + <valueHelp> + <format>shim6</format> + <description>Shim6 Protocol</description> + </valueHelp> + <valueHelp> + <format>wesp</format> + <description>Wrapped Encapsulating Security Payload</description> + </valueHelp> + <valueHelp> + <format>rohc</format> + <description>Robust Header Compression</description> + </valueHelp> + <valueHelp> + <format>0-255</format> + <description>IP protocol number</description> + </valueHelp> + <constraint> + <validator name="ip-protocol"/> + </constraint> + </properties> + </leafNode> + <node name="source"> + <properties> + <help>NAT source parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + </children> +</tagNode> diff --git a/interface-definitions/include/nat-translation-port.xml.i b/interface-definitions/include/nat-translation-port.xml.i new file mode 100644 index 000000000..93de471e3 --- /dev/null +++ b/interface-definitions/include/nat-translation-port.xml.i @@ -0,0 +1,13 @@ +<leafNode name="port"> + <properties> + <help>Port number</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <valueHelp> + <format><start>-<end></format> + <description>Numbered port range (e.g., 1001-1005)</description> + </valueHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/port-number.xml.i b/interface-definitions/include/port-number.xml.i new file mode 100644 index 000000000..29d2f55fd --- /dev/null +++ b/interface-definitions/include/port-number.xml.i @@ -0,0 +1,12 @@ +<leafNode name="port"> + <properties> + <help>Port number used to establish connection</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/radius-server.xml.i b/interface-definitions/include/radius-server.xml.i new file mode 100644 index 000000000..047728233 --- /dev/null +++ b/interface-definitions/include/radius-server.xml.i @@ -0,0 +1,56 @@ +<node name="radius"> + <properties> + <help>RADIUS based user authentication</help> + </properties> + <children> + <leafNode name="source-address"> + <properties> + <help>RADIUS client source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source-address of RADIUS queries</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <tagNode name="server"> + <properties> + <help>RADIUS server configuration</help> + <valueHelp> + <format>ipv4</format> + <description>RADIUS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Temporary disable this server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Shared secret key</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Authentication port</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 1812)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> diff --git a/interface-definitions/include/rip-redistribute.xml.i b/interface-definitions/include/rip-redistribute.xml.i new file mode 100644 index 000000000..d94dfa5a8 --- /dev/null +++ b/interface-definitions/include/rip-redistribute.xml.i @@ -0,0 +1,24 @@ +<leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format><1-16></format> + <description>Redistribute route metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-16"/> + </constraint> + </properties> +</leafNode> +<leafNode name="route-map"> + <properties> + <help>Route map reference</help> + <valueHelp> + <format><text></format> + <description>Route map reference</description> + </valueHelp> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/source-address-ipv4-ipv6.xml.i b/interface-definitions/include/source-address-ipv4-ipv6.xml.i new file mode 100644 index 000000000..6d2d77c95 --- /dev/null +++ b/interface-definitions/include/source-address-ipv4-ipv6.xml.i @@ -0,0 +1,17 @@ +<leafNode name="source-address"> + <properties> + <help>IPv4/IPv6 source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source-address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 source-address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/source-interface-ethernet.xml.i b/interface-definitions/include/source-interface-ethernet.xml.i new file mode 100644 index 000000000..ad90bc4ac --- /dev/null +++ b/interface-definitions/include/source-interface-ethernet.xml.i @@ -0,0 +1,12 @@ +<leafNode name="source-interface"> + <properties> + <help>Physical interface the traffic will go through</help> + <valueHelp> + <format>interface</format> + <description>Physical interface used for traffic forwarding</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t ethernet</script> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/source-interface.xml.i b/interface-definitions/include/source-interface.xml.i new file mode 100644 index 000000000..ae579c2a6 --- /dev/null +++ b/interface-definitions/include/source-interface.xml.i @@ -0,0 +1,12 @@ +<leafNode name="source-interface"> + <properties> + <help>Physical interface used for connection</help> + <valueHelp> + <format>interface</format> + <description>Physical interface used for connection</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i new file mode 100644 index 000000000..a6d7c81ce --- /dev/null +++ b/interface-definitions/include/vif-s.xml.i @@ -0,0 +1,67 @@ +<tagNode name="vif-s"> + <properties> + <help>QinQ TAG-S Virtual Local Area Network (VLAN) ID</help> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + <leafNode name="ethertype"> + <properties> + <help>Set Ethertype</help> + <completionHelp> + <list>0x88A8 0x8100</list> + </completionHelp> + <valueHelp> + <format>0x88A8</format> + <description>802.1ad</description> + </valueHelp> + <valueHelp> + <format>0x8100</format> + <description>802.1q</description> + </valueHelp> + <constraint> + <regex>(0x88A8|0x8100)</regex> + </constraint> + <constraintErrorMessage>Ethertype must be 0x88A8 or 0x8100</constraintErrorMessage> + </properties> + </leafNode> + <node name="ip"> + <children> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + </children> + </node> + #include <include/interface-mac.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + <tagNode name="vif-c"> + <properties> + <help>QinQ TAG-C Virtual Local Area Network (VLAN) ID</help> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-mac.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + #include <include/interface-vrf.xml.i> + </children> + </tagNode> + </children> +</tagNode> diff --git a/interface-definitions/include/vif.xml.i b/interface-definitions/include/vif.xml.i new file mode 100644 index 000000000..5a4e52122 --- /dev/null +++ b/interface-definitions/include/vif.xml.i @@ -0,0 +1,65 @@ +<tagNode name="vif"> + <properties> + <help>Virtual Local Area Network (VLAN) ID</help> + <valueHelp> + <format>0-4094</format> + <description>Virtual Local Area Network (VLAN) ID</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="egress-qos"> + <properties> + <help>VLAN egress QoS</help> + <completionHelp> + <script>echo Format for qos mapping, e.g.: '0:1 1:6 7:6'</script> + </completionHelp> + <constraint> + <regex>[:0-7 ]+$</regex> + </constraint> + <constraintErrorMessage>QoS mapping should be in the format of '0:7 2:3' with numbers 0-9</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="ingress-qos"> + <properties> + <help>VLAN ingress QoS</help> + <completionHelp> + <script>echo Format for qos mapping '0:1 1:6 7:6'</script> + </completionHelp> + <constraint> + <regex>[:0-7 ]+$</regex> + </constraint> + <constraintErrorMessage>QoS mapping should be in the format of '0:7 2:3' with numbers 0-9</constraintErrorMessage> + </properties> + </leafNode> + <node name="ip"> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + #include <include/interface-enable-proxy-arp.xml.i> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + #include <include/interface-mac.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + </children> +</tagNode> diff --git a/interface-definitions/intel_qat.xml.in b/interface-definitions/intel_qat.xml.in new file mode 100644 index 000000000..812484184 --- /dev/null +++ b/interface-definitions/intel_qat.xml.in @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="acceleration" owner="${vyos_conf_scripts_dir}/intel_qat.py"> + <properties> + <help>Acceleration components</help> + <priority>50</priority> + </properties> + <children> + <leafNode name="qat"> + <properties> + <help>Enable Intel QAT (Quick Assist Technology) for cryptographic acceleration</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in new file mode 100644 index 000000000..7d658f6a0 --- /dev/null +++ b/interface-definitions/interfaces-bonding.xml.in @@ -0,0 +1,174 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="bonding" owner="${vyos_conf_scripts_dir}/interfaces-bonding.py"> + <properties> + <help>Bonding Interface/Link Aggregation</help> + <priority>320</priority> + <constraint> + <regex>^bond[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage> + <valueHelp> + <format>bondN</format> + <description>Bonding interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + <node name="arp-monitor"> + <properties> + <help>ARP link monitoring parameters</help> + </properties> + <children> + <leafNode name="interval"> + <properties> + <help>ARP link monitoring interval</help> + <valueHelp> + <format>0-4294967295</format> + <description>Specifies the ARP link monitoring frequency in milliseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="target"> + <properties> + <help>IP address used for ARP monitoring</help> + <valueHelp> + <format>ipv4</format> + <description>Specify IPv4 address of ARP requests when interval is enabled</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="hash-policy"> + <properties> + <help>Bonding transmit hash policy</help> + <completionHelp> + <list>layer2 layer2+3 layer3+4</list> + </completionHelp> + <valueHelp> + <format>layer2</format> + <description>use MAC addresses to generate the hash (802.3ad, default)</description> + </valueHelp> + <valueHelp> + <format>layer2+3</format> + <description>combine MAC address and IP address to make hash</description> + </valueHelp> + <valueHelp> + <format>layer3+4</format> + <description>combine IP address and port to make hash</description> + </valueHelp> + <constraint> + <regex>(layer2\+3|layer3\+4|layer2)</regex> + </constraint> + <constraintErrorMessage>hash-policy must be layer2 layer2+3 or layer3+4</constraintErrorMessage> + </properties> + <defaultValue>layer2</defaultValue> + </leafNode> + <node name="ip"> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + #include <include/interface-enable-proxy-arp.xml.i> + #include <include/interface-proxy-arp-pvlan.xml.i> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + #include <include/interface-mac.xml.i> + <leafNode name="mode"> + <properties> + <help>Bonding mode</help> + <completionHelp> + <list>802.3ad active-backup broadcast round-robin transmit-load-balance adaptive-load-balance xor-hash</list> + </completionHelp> + <valueHelp> + <format>802.3ad</format> + <description>IEEE 802.3ad Dynamic link aggregation (Default)</description> + </valueHelp> + <valueHelp> + <format>active-backup</format> + <description>Fault tolerant: only one slave in the bond is active</description> + </valueHelp> + <valueHelp> + <format>broadcast</format> + <description>Fault tolerant: transmits everything on all slave interfaces</description> + </valueHelp> + <valueHelp> + <format>round-robin</format> + <description>Load balance: transmit packets in sequential order</description> + </valueHelp> + <valueHelp> + <format>transmit-load-balance</format> + <description>Load balance: adapts based on transmit load and speed</description> + </valueHelp> + <valueHelp> + <format>adaptive-load-balance</format> + <description>Load balance: adapts based on transmit and receive plus ARP</description> + </valueHelp> + <valueHelp> + <format>xor-hash</format> + <description>Distribute based on MAC address</description> + </valueHelp> + <constraint> + <regex>(802.3ad|active-backup|broadcast|round-robin|transmit-load-balance|adaptive-load-balance|xor-hash)</regex> + </constraint> + <constraintErrorMessage>mode must be 802.3ad, active-backup, broadcast, round-robin, transmit-load-balance, adaptive-load-balance, or xor</constraintErrorMessage> + </properties> + <defaultValue>802.3ad</defaultValue> + </leafNode> + <node name="member"> + <properties> + <help>Bridge member interfaces</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Member interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --bondable</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + #include <include/interface-mtu-68-9000.xml.i> + <leafNode name="primary"> + <properties> + <help>Primary device interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --bondable</script> + </completionHelp> + </properties> + </leafNode> + #include <include/vif-s.xml.i> + #include <include/vif.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in new file mode 100644 index 000000000..92356d696 --- /dev/null +++ b/interface-definitions/interfaces-bridge.xml.in @@ -0,0 +1,184 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="bridge" owner="${vyos_conf_scripts_dir}/interfaces-bridge.py"> + <properties> + <help>Bridge Interface</help> + <priority>489</priority> + <constraint> + <regex>^br[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Bridge interface must be named brN</constraintErrorMessage> + <valueHelp> + <format>brN</format> + <description>Bridge interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + <leafNode name="aging"> + <properties> + <help>MAC address aging interval</help> + <valueHelp> + <format>0</format> + <description>Disable MAC address learning (always flood)</description> + </valueHelp> + <valueHelp> + <format>10-1000000</format> + <description>MAC address aging time in seconds (default: 300)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 10-1000000"/> + </constraint> + </properties> + <defaultValue>300</defaultValue> + </leafNode> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="forwarding-delay"> + <properties> + <help>Forwarding delay</help> + <valueHelp> + <format>0-200</format> + <description>Spanning Tree Protocol forwarding delay in seconds (default 15)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-200"/> + </constraint> + <constraintErrorMessage>Forwarding delay must be between 0 and 200 seconds</constraintErrorMessage> + </properties> + <defaultValue>14</defaultValue> + </leafNode> + <leafNode name="hello-time"> + <properties> + <help>Hello packet advertisment interval</help> + <valueHelp> + <format>1-10</format> + <description>Spanning Tree Protocol hello advertisement interval in seconds (default 2)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-10"/> + </constraint> + <constraintErrorMessage>Bridge Hello interval must be between 1 and 10 seconds</constraintErrorMessage> + </properties> + <defaultValue>2</defaultValue> + </leafNode> + <node name="igmp"> + <properties> + <help>Internet Group Management Protocol (IGMP) settings</help> + </properties> + <children> + <leafNode name="querier"> + <properties> + <help>Enable IGMP querier</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="ip"> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + #include <include/interface-disable-arp-filter.xml.i> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + #include <include/interface-mac.xml.i> + <leafNode name="max-age"> + <properties> + <help>Interval at which neighbor bridges are removed</help> + <valueHelp> + <format>1-40</format> + <description>Bridge maximum aging time in seconds (default 20)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-40"/> + </constraint> + <constraintErrorMessage>Bridge max aging value must be between 1 and 40 seconds</constraintErrorMessage> + </properties> + <defaultValue>20</defaultValue> + </leafNode> + <node name="member"> + <properties> + <help>Bridge member interfaces</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Member interface name</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --bridgeable</script> + </completionHelp> + </properties> + <children> + <leafNode name="cost"> + <properties> + <help>Bridge port cost</help> + <valueHelp> + <format>1-65535</format> + <description>Path cost value for Spanning Tree Protocol</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Path cost value must be between 1 and 65535</constraintErrorMessage> + </properties> + <defaultValue>100</defaultValue> + </leafNode> + <leafNode name="priority"> + <properties> + <help>Bridge port priority</help> + <valueHelp> + <format>0-63</format> + <description>Bridge port priority</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-63"/> + </constraint> + <constraintErrorMessage>Port priority value must be between 0 and 63</constraintErrorMessage> + </properties> + <defaultValue>32</defaultValue> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="priority"> + <properties> + <help>Priority for this bridge</help> + <valueHelp> + <format>0-65535</format> + <description>Bridge priority (default 32768)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + <constraintErrorMessage>Bridge priority must be between 0 and 65535 (multiples of 4096)</constraintErrorMessage> + </properties> + <defaultValue>32768</defaultValue> + </leafNode> + <leafNode name="stp"> + <properties> + <help>Enable spanning tree protocol</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in new file mode 100644 index 000000000..135adfc10 --- /dev/null +++ b/interface-definitions/interfaces-dummy.xml.in @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="dummy" owner="${vyos_conf_scripts_dir}/interfaces-dummy.py"> + <properties> + <help>Dummy Interface</help> + <priority>300</priority> + <constraint> + <regex>^dum[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Dummy interface must be named dumN</constraintErrorMessage> + <valueHelp> + <format>dumN</format> + <description>Dummy interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in new file mode 100644 index 000000000..e8f3f09f1 --- /dev/null +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -0,0 +1,277 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="ethernet" owner="${vyos_conf_scripts_dir}/interfaces-ethernet.py"> + <properties> + <help>Ethernet Interface</help> + <priority>318</priority> + <constraint> + <regex>^((eth|lan)[0-9]+|(eno|ens|enp|enx).+)$</regex> + </constraint> + <constraintErrorMessage>Invalid Ethernet interface name</constraintErrorMessage> + <valueHelp> + <format>ethN</format> + <description>Ethernet interface name</description> + </valueHelp> + <valueHelp> + <format>en[ospx]N</format> + <description>Ethernet interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + <leafNode name="disable-flow-control"> + <properties> + <help>Disable Ethernet flow control (pause frames)</help> + <valueless/> + </properties> + </leafNode> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="duplex"> + <properties> + <help>Duplex mode</help> + <completionHelp> + <list>auto half full</list> + </completionHelp> + <valueHelp> + <format>auto</format> + <description>Auto negotiation (default)</description> + </valueHelp> + <valueHelp> + <format>half</format> + <description>Half duplex</description> + </valueHelp> + <valueHelp> + <format>full</format> + <description>Full duplex</description> + </valueHelp> + <constraint> + <regex>(auto|half|full)</regex> + </constraint> + <constraintErrorMessage>duplex must be auto, half or full</constraintErrorMessage> + </properties> + <defaultValue>auto</defaultValue> + </leafNode> + #include <include/interface-hw-id.xml.i> + <node name="ip"> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + #include <include/interface-enable-proxy-arp.xml.i> + #include <include/interface-proxy-arp-pvlan.xml.i> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + #include <include/interface-mac.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + <node name="offload-options"> + <properties> + <help>Configurable offload options</help> + </properties> + <children> + <leafNode name="generic-receive"> + <properties> + <help>Configure GRO (generic receive offload)</help> + <completionHelp> + <list>on off</list> + </completionHelp> + <valueHelp> + <format>on</format> + <description>Enable GRO (generic receive offload)</description> + </valueHelp> + <valueHelp> + <format>off</format> + <description>Disable GRO (generic receive offload)</description> + </valueHelp> + <constraint> + <regex>(on|off)</regex> + </constraint> + <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="generic-segmentation"> + <properties> + <help>Configure GSO (generic segmentation offload)</help> + <completionHelp> + <list>on off</list> + </completionHelp> + <valueHelp> + <format>on</format> + <description>Enable GSO (generic segmentation offload)</description> + </valueHelp> + <valueHelp> + <format>off</format> + <description>Disable GSO (generic segmentation offload)</description> + </valueHelp> + <constraint> + <regex>(on|off)</regex> + </constraint> + <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="scatter-gather"> + <properties> + <help>Configure scatter-gather option</help> + <completionHelp> + <list>on off</list> + </completionHelp> + <valueHelp> + <format>on</format> + <description>Enable scatter-gather</description> + </valueHelp> + <valueHelp> + <format>off</format> + <description>Disable scatter-gather</description> + </valueHelp> + <constraint> + <regex>(on|off)</regex> + </constraint> + <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tcp-segmentation"> + <properties> + <help>Configure TSO (TCP segmentation offloading)</help> + <completionHelp> + <list>on off</list> + </completionHelp> + <valueHelp> + <format>on</format> + <description>Enable TSO (TCP segmentation offloading)</description> + </valueHelp> + <valueHelp> + <format>off</format> + <description>Disable TSO (TCP segmentation offloading)</description> + </valueHelp> + <constraint> + <regex>(on|off)</regex> + </constraint> + <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="udp-fragmentation"> + <properties> + <help>Configure UDP fragmentation offloading</help> + <completionHelp> + <list>on off</list> + </completionHelp> + <valueHelp> + <format>on</format> + <description>Enable UDP fragmentation offloading</description> + </valueHelp> + <valueHelp> + <format>off</format> + <description>Disable UDP fragmentation offloading</description> + </valueHelp> + <constraint> + <regex>(on|off)</regex> + </constraint> + <constraintErrorMessage>Must be either 'on' or 'off'</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="smp-affinity"> + <properties> + <help>CPU interrupt affinity mask</help> + <completionHelp> + <list>auto 10 100 1000 2500 5000 10000</list> + </completionHelp> + <valueHelp> + <format>auto</format> + <description>Auto negotiation (default)</description> + </valueHelp> + <valueHelp> + <format>hex</format> + <description>Bitmask representing CPUs that this NIC will interrupt</description> + </valueHelp> + <valueHelp> + <format>hex,hex</format> + <description>Bitmasks representing CPUs for interrupt and receive processing</description> + </valueHelp> + <constraint> + <regex>(auto)</regex> + <regex>[0-9a-f]+(|,[0-9a-f]+)$</regex> + </constraint> + <constraintErrorMessage>IRQ affinity mask must be hex value or auto</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="speed"> + <properties> + <help>Link speed</help> + <completionHelp> + <list>auto 10 100 1000 2500 5000 10000 25000 40000 50000 100000</list> + </completionHelp> + <valueHelp> + <format>auto</format> + <description>Auto negotiation (default)</description> + </valueHelp> + <valueHelp> + <format>10</format> + <description>10 Mbit/sec</description> + </valueHelp> + <valueHelp> + <format>100</format> + <description>100 Mbit/sec</description> + </valueHelp> + <valueHelp> + <format>1000</format> + <description>1 Gbit/sec</description> + </valueHelp> + <valueHelp> + <format>2500</format> + <description>2.5 Gbit/sec</description> + </valueHelp> + <valueHelp> + <format>5000</format> + <description>5 Gbit/sec</description> + </valueHelp> + <valueHelp> + <format>10000</format> + <description>10 Gbit/sec</description> + </valueHelp> + <valueHelp> + <format>25000</format> + <description>25 Gbit/sec</description> + </valueHelp> + <valueHelp> + <format>40000</format> + <description>40 Gbit/sec</description> + </valueHelp> + <valueHelp> + <format>50000</format> + <description>50 Gbit/sec</description> + </valueHelp> + <valueHelp> + <format>100000</format> + <description>100 Gbit/sec</description> + </valueHelp> + <constraint> + <regex>(auto|10|100|1000|2500|5000|10000|25000|40000|50000|100000)</regex> + </constraint> + <constraintErrorMessage>Speed must be auto, 10, 100, 1000, 2500, 5000, 10000, 25000, 40000, 50000 or 100000</constraintErrorMessage> + </properties> + <defaultValue>auto</defaultValue> + </leafNode> + #include <include/vif-s.xml.i> + #include <include/vif.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in new file mode 100644 index 000000000..31a3ebb7a --- /dev/null +++ b/interface-definitions/interfaces-geneve.xml.in @@ -0,0 +1,60 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="geneve" owner="${vyos_conf_scripts_dir}/interfaces-geneve.py"> + <properties> + <help>Generic Network Virtualization Encapsulation (GENEVE) Interface</help> + <priority>460</priority> + <constraint> + <regex>^gnv[0-9]+$</regex> + </constraint> + <constraintErrorMessage>GENEVE interface must be named gnvN</constraintErrorMessage> + <valueHelp> + <format>gnvN</format> + <description>GENEVE interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + <node name="ip"> + <properties> + <help>IPv4 routing parameters</help> + </properties> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-enable-proxy-arp.xml.i> + </children> + </node> + #include <include/interface-mtu-1450-9000.xml.i> + <leafNode name="remote"> + <properties> + <help>Remote address of GENEVE tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Remote address of GENEVE tunnel</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="vni"> + <properties> + <help>Virtual Network Identifier</help> + <valueHelp> + <format>0-16777214</format> + <description>GENEVE virtual network identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777214"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in new file mode 100644 index 000000000..3a878ad76 --- /dev/null +++ b/interface-definitions/interfaces-l2tpv3.xml.in @@ -0,0 +1,161 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="l2tpv3" owner="${vyos_conf_scripts_dir}/interfaces-l2tpv3.py"> + <properties> + <help>Layer 2 Tunnel Protocol Version 3 (L2TPv3) Interface</help> + <priority>485</priority> + <constraint> + <regex>^l2tpeth[0-9]+$</regex> + </constraint> + <constraintErrorMessage>L2TPv3 interface must be named l2tpethN</constraintErrorMessage> + <valueHelp> + <format>l2tpethN</format> + <description>L2TPv3 interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-description.xml.i> + <leafNode name="destination-port"> + <properties> + <help>UDP destination port for L2TPv3 tunnel (default: 5000)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>5000</defaultValue> + </leafNode> + #include <include/interface-disable.xml.i> + <leafNode name="encapsulation"> + <properties> + <help>Encapsulation type (default: UDP)</help> + <completionHelp> + <list>udp ip</list> + </completionHelp> + <valueHelp> + <format>udp</format> + <description>UDP encapsulation</description> + </valueHelp> + <valueHelp> + <format>ip</format> + <description>IP encapsulation</description> + </valueHelp> + <constraint> + <regex>(udp|ip)</regex> + </constraint> + <constraintErrorMessage>Encapsulation must be UDP or IP</constraintErrorMessage> + </properties> + <defaultValue>udp</defaultValue> + </leafNode> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="local-ip"> + <properties> + <help>Local IP address for L2TPv3 tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 address of tunnel</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 address of tunnel</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + #include <include/interface-mtu-68-9000.xml.i> + <leafNode name="peer-session-id"> + <properties> + <help>Peer session identifier</help> + <valueHelp> + <format>1-429496729</format> + <description>L2TPv3 peer session identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-429496729"/> + </constraint> + </properties> + </leafNode> + <leafNode name="peer-tunnel-id"> + <properties> + <help>Peer tunnel identifier</help> + <valueHelp> + <format>1-429496729</format> + <description>L2TPv3 peer tunnel identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-429496729"/> + </constraint> + </properties> + </leafNode> + <leafNode name="remote-ip"> + <properties> + <help>Remote IP address for L2TPv3 tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Remote IPv4 address of tunnel</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Remote IPv6 address of tunnel</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="session-id"> + <properties> + <help>Session identifier</help> + <valueHelp> + <format>1-429496729</format> + <description>L2TPv3 session identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-429496729"/> + </constraint> + </properties> + </leafNode> + <leafNode name="source-port"> + <properties> + <help>UDP source port for L2TPv3 tunnel (default: 5000)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>5000</defaultValue> + </leafNode> + <leafNode name="tunnel-id"> + <properties> + <help>Local tunnel identifier</help> + <valueHelp> + <format>1-429496729</format> + <description>L2TPv3 local tunnel identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-429496729"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-loopback.xml.in b/interface-definitions/interfaces-loopback.xml.in new file mode 100644 index 000000000..97d5bab90 --- /dev/null +++ b/interface-definitions/interfaces-loopback.xml.in @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="loopback" owner="${vyos_conf_scripts_dir}/interfaces-loopback.py"> + <properties> + <help>Loopback Interface</help> + <priority>300</priority> + <constraint> + <regex>^lo$</regex> + </constraint> + <constraintErrorMessage>Loopback interface must be named lo</constraintErrorMessage> + <valueHelp> + <format>lo</format> + <description>Loopback interface</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-description.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in new file mode 100644 index 000000000..dfef387d2 --- /dev/null +++ b/interface-definitions/interfaces-macsec.xml.in @@ -0,0 +1,116 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="macsec" owner="${vyos_conf_scripts_dir}/interfaces-macsec.py"> + <properties> + <help>MACsec Interface (802.1ae)</help> + <priority>319</priority> + <constraint> + <regex>^macsec[0-9]+$</regex> + </constraint> + <constraintErrorMessage>MACsec interface must be named macsecN</constraintErrorMessage> + <valueHelp> + <format>macsecN</format> + <description>MACsec interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + <node name="security"> + <properties> + <help>Security/Encryption Settings</help> + </properties> + <children> + <leafNode name="cipher"> + <properties> + <help>Cipher suite used</help> + <completionHelp> + <list>gcm-aes-128</list> + </completionHelp> + <valueHelp> + <format>gcm-aes-128</format> + <description>Galois/Counter Mode of AES cipher with 128-bit key (default)</description> + </valueHelp> + <constraint> + <regex>(gcm-aes-128)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="encrypt"> + <properties> + <help>Enable optional MACsec encryption</help> + <valueless/> + </properties> + </leafNode> + <node name="mka"> + <properties> + <help>MACsec Key Agreement protocol (MKA)</help> + </properties> + <children> + <leafNode name="cak"> + <properties> + <help>Secure Connectivity Association Key</help> + <valueHelp> + <format>key</format> + <description>16-byte (128-bit) hex-string (32 hex-digits)</description> + </valueHelp> + <constraint> + <regex>^[A-Fa-f0-9]{32}$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="ckn"> + <properties> + <help>Secure Connectivity Association Key Name</help> + <valueHelp> + <format>key</format> + <description>32-byte (256-bit) hex-string (64 hex-digits)</description> + </valueHelp> + <constraint> + <regex>^[A-Fa-f0-9]{64}$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="priority"> + <properties> + <help>Priority of MACsec Key Agreement protocol (MKA) actor (default: 255)</help> + <valueHelp> + <format>0-255</format> + <description>MACsec Key Agreement protocol (MKA) priority</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255" /> + </constraint> + </properties> + <defaultValue>255</defaultValue> + </leafNode> + </children> + </node> + <leafNode name="replay-window"> + <properties> + <help>IEEE 802.1X/MACsec replay protection window</help> + <valueHelp> + <format>0</format> + <description>No replay window, strict check</description> + </valueHelp> + <valueHelp> + <format>1-4294967295</format> + <description>Number of packets that could be misordered</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295" /> + </constraint> + </properties> + </leafNode> + </children> + </node> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + #include <include/source-interface-ethernet.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in new file mode 100644 index 000000000..905c76507 --- /dev/null +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -0,0 +1,808 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="openvpn" owner="${vyos_conf_scripts_dir}/interfaces-openvpn.py"> + <properties> + <help>OpenVPN Tunnel Interface</help> + <priority>460</priority> + <constraint> + <regex>^vtun[0-9]+$</regex> + </constraint> + <constraintErrorMessage>OpenVPN tunnel interface must be named vtunN</constraintErrorMessage> + <valueHelp> + <format>vtunN</format> + <description>OpenVPN interface name</description> + </valueHelp> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Authentication options</help> + </properties> + <children> + <leafNode name="password"> + <properties> + <help>OpenVPN password used for authentication</help> + </properties> + </leafNode> + <leafNode name="username"> + <properties> + <help>OpenVPN username used for authentication</help> + </properties> + </leafNode> + </children> + </node> + #include <include/interface-description.xml.i> + <leafNode name="device-type"> + <properties> + <help>OpenVPN interface device-type</help> + <completionHelp> + <list>tun tap</list> + </completionHelp> + <valueHelp> + <format>tun</format> + <description>TUN device, required for OSI layer 3</description> + </valueHelp> + <valueHelp> + <format>tap</format> + <description>TAP device, required for OSI layer 2</description> + </valueHelp> + <constraint> + <regex>(tun|tap)</regex> + </constraint> + </properties> + </leafNode> + #include <include/interface-disable.xml.i> + <node name="encryption"> + <properties> + <help>Data Encryption settings</help> + </properties> + <children> + <leafNode name="cipher"> + <properties> + <help>Standard Data Encryption Algorithm</help> + <completionHelp> + <list>des 3des bf128 bf256 aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> + </completionHelp> + <valueHelp> + <format>des</format> + <description>DES algorithm</description> + </valueHelp> + <valueHelp> + <format>3des</format> + <description>DES algorithm with triple encryption</description> + </valueHelp> + <valueHelp> + <format>bf128</format> + <description>Blowfish algorithm with 128-bit key</description> + </valueHelp> + <valueHelp> + <format>bf256</format> + <description>Blowfish algorithm with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>aes128</format> + <description>AES algorithm with 128-bit key CBC</description> + </valueHelp> + <valueHelp> + <format>aes128gcm</format> + <description>AES algorithm with 128-bit key GCM</description> + </valueHelp> + <valueHelp> + <format>aes192</format> + <description>AES algorithm with 192-bit key CBC</description> + </valueHelp> + <valueHelp> + <format>aes192gcm</format> + <description>AES algorithm with 192-bit key GCM</description> + </valueHelp> + <valueHelp> + <format>aes256</format> + <description>AES algorithm with 256-bit key CBC</description> + </valueHelp> + <valueHelp> + <format>aes256gcm</format> + <description>AES algorithm with 256-bit key GCM</description> + </valueHelp> + <constraint> + <regex>(des|3des|bf128|bf256|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="ncp-ciphers"> + <properties> + <help>Cipher negotiation list for use in server or client mode</help> + <completionHelp> + <list>des 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> + </completionHelp> + <valueHelp> + <format>des</format> + <description>DES algorithm</description> + </valueHelp> + <valueHelp> + <format>3des</format> + <description>DES algorithm with triple encryption</description> + </valueHelp> + <valueHelp> + <format>aes128</format> + <description>AES algorithm with 128-bit key CBC</description> + </valueHelp> + <valueHelp> + <format>aes128gcm</format> + <description>AES algorithm with 128-bit key GCM</description> + </valueHelp> + <valueHelp> + <format>aes192</format> + <description>AES algorithm with 192-bit key CBC</description> + </valueHelp> + <valueHelp> + <format>aes192gcm</format> + <description>AES algorithm with 192-bit key GCM</description> + </valueHelp> + <valueHelp> + <format>aes256</format> + <description>AES algorithm with 256-bit key CBC</description> + </valueHelp> + <valueHelp> + <format>aes256gcm</format> + <description>AES algorithm with 256-bit key GCM</description> + </valueHelp> + <constraint> + <regex>(des|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="disable-ncp"> + <properties> + <help>Disable support for ncp-ciphers</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="hash"> + <properties> + <help>Hashing Algorithm</help> + <completionHelp> + <list>md5 sha1 sha256 sha384 sha512</list> + </completionHelp> + <valueHelp> + <format>md5</format> + <description>MD5 algorithm</description> + </valueHelp> + <valueHelp> + <format>sha1</format> + <description>SHA-1 algorithm</description> + </valueHelp> + <valueHelp> + <format>sha256</format> + <description>SHA-256 algorithm</description> + </valueHelp> + <valueHelp> + <format>sha384</format> + <description>SHA-384 algorithm</description> + </valueHelp> + <valueHelp> + <format>sha512</format> + <description>SHA-512 algorithm</description> + </valueHelp> + <constraint> + <regex>(md5|sha1|sha256|sha384|sha512)</regex> + </constraint> + </properties> + </leafNode> + <node name="keep-alive"> + <properties> + <help>Keepalive helper options</help> + </properties> + <children> + <leafNode name="failure-count"> + <properties> + <help>Maximum number of keepalive packet failures [default 6]</help> + <valueHelp> + <format>0-1000</format> + <description>Maximum number of keepalive packet failures</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-1000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Keepalive packet interval (seconds) [default 10]</help> + <valueHelp> + <format>0-600</format> + <description>Keepalive packet interval (seconds)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-600"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <tagNode name="local-address"> + <properties> + <help>Local IP address of tunnel (IPv4 or IPv6)</help> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + <children> + <leafNode name="subnet-mask"> + <properties> + <help>Subnet-mask for local IP address of tunnel (IPv4 only)</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="local-host"> + <properties> + <help>Local IP address to accept connections (all if not set)</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="local-port"> + <properties> + <help>Local port number to accept connections</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mode"> + <properties> + <help>OpenVPN mode of operation</help> + <completionHelp> + <list>site-to-site client server</list> + </completionHelp> + <valueHelp> + <format>site-to-site</format> + <description>Site-to-site mode</description> + </valueHelp> + <valueHelp> + <format>client</format> + <description>Client in client-server mode</description> + </valueHelp> + <valueHelp> + <format>server</format> + <description>Server in client-server mode</description> + </valueHelp> + <constraint> + <regex>(site-to-site|client|server)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="openvpn-option"> + <properties> + <help>Additional OpenVPN options. You must + use the syntax of openvpn.conf in this text-field. Using this + without proper knowledge may result in a crashed OpenVPN server. + Check system log to look for errors.</help> + <multi/> + </properties> + </leafNode> + <leafNode name="persistent-tunnel"> + <properties> + <help>Do not close and reopen interface (TUN/TAP device) on client restarts</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="protocol"> + <properties> + <help>OpenVPN communication protocol</help> + <completionHelp> + <list>udp tcp-passive tcp-active</list> + </completionHelp> + <valueHelp> + <format>udp</format> + <description>UDP</description> + </valueHelp> + <valueHelp> + <format>tcp-passive</format> + <description>TCP and accepts connections passively</description> + </valueHelp> + <valueHelp> + <format>tcp-active</format> + <description>TCP and initiates connections actively</description> + </valueHelp> + <constraint> + <regex>(udp|tcp-passive|tcp-active)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="remote-address"> + <properties> + <help>IP address of remote end of tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Remote end IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Remote end IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="remote-host"> + <properties> + <help>Remote host to connect to (dynamic if not set)</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of remote host</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of remote host</description> + </valueHelp> + <valueHelp> + <format>txt</format> + <description>Hostname of remote host</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="remote-port"> + <properties> + <help>Remote port number to connect to</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <node name="replace-default-route"> + <properties> + <help>OpenVPN tunnel to be used as the default route</help> + </properties> + <children> + <leafNode name="local"> + <properties> + <help>Tunnel endpoints are on the same subnet</help> + </properties> + </leafNode> + </children> + </node> + <node name="server"> + <properties> + <help>Server-mode options</help> + </properties> + <children> + <tagNode name="client"> + <properties> + <help>Client-specific settings</help> + <valueHelp> + <format>name</format> + <description>Client common-name in the certificate</description> + </valueHelp> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable client connection</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ip"> + <properties> + <help>IP address of the client</help> + <valueHelp> + <format>ipv4</format> + <description>Client IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Client IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="push-route"> + <properties> + <help>Route to be pushed to the client</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 network and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="subnet"> + <properties> + <help>Subnet belonging to the client (iroute)</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network and prefix length belonging to the client</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 network and prefix length belonging to the client</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <node name="client-ip-pool"> + <properties> + <help>Pool of client IPv4 addresses</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Disable client IP pool</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="start"> + <properties> + <help>First IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="subnet-mask"> + <properties> + <help>Subnet mask pushed to dynamic clients. + If not set the server subnet mask will be used. + Only used with topology subnet or device type tap. + Not used with bridged interfaces.</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 subnet mask</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + <node name="client-ipv6-pool"> + <properties> + <help>Pool of client IPv6 addresses</help> + </properties> + <children> + <leafNode name="base"> + <properties> + <help>Client IPv6 pool base address with optional prefix length</help> + <valueHelp> + <format>ipv6net</format> + <description>Client IPv6 pool base address with optional prefix length (defaults: base = server subnet + 0x1000, prefix length = server prefix length)</description> + </valueHelp> + <constraint> + <validator name="ipv6"/> + </constraint> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable client IPv6 pool</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="domain-name"> + <properties> + <help>DNS suffix to be pushed to all clients</help> + <valueHelp> + <format>txt</format> + <description>Domain Name Server suffix</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="max-connections"> + <properties> + <help>Number of maximum client connections</help> + <valueHelp> + <format>1-4096</format> + <description>Number of concurrent clients</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4096"/> + </constraint> + </properties> + </leafNode> + <leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS)</help> + <valueHelp> + <format>ipv4</format> + <description>DNS server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>DNS server IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="push-route"> + <properties> + <help>Route to be pushed to all clients</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 network and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="reject-unconfigured-clients"> + <properties> + <help>Reject connections from clients that are not explicitly configured</help> + </properties> + </leafNode> + <leafNode name="subnet"> + <properties> + <help>Server-mode subnet (from which client IPs are allocated)</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 network and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="topology"> + <properties> + <help>Topology for clients</help> + <completionHelp> + <list>net30 point-to-point subnet</list> + </completionHelp> + <valueHelp> + <format>net30</format> + <description>net30 topology (default)</description> + </valueHelp> + <valueHelp> + <format>point-to-point</format> + <description>Point-to-point topology</description> + </valueHelp> + <valueHelp> + <format>subnet</format> + <description>Subnet topology</description> + </valueHelp> + <constraint> + <regex>(subnet|point-to-point|net30)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="shared-secret-key-file"> + <properties> + <help>File containing the secret key shared with remote end of tunnel</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <node name="tls"> + <properties> + <help>Transport Layer Security (TLS) options</help> + </properties> + <children> + <leafNode name="auth-file"> + <properties> + <help>File containing tls static key for tls-auth</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ca-cert-file"> + <properties> + <help>File containing certificate for Certificate Authority (CA)</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="cert-file"> + <properties> + <help>File containing certificate for this host</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="crl-file"> + <properties> + <help>File containing certificate revocation list (CRL) for this host</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dh-file"> + <properties> + <help>File containing Diffie Hellman parameters (server only)</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="key-file"> + <properties> + <help>Private key for this host</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="crypt-file"> + <properties> + <help>File containing encryption key to authenticate control channel</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="tls-version-min"> + <properties> + <help>Specify the minimum required TLS version</help> + <completionHelp> + <list>1.0 1.1 1.2</list> + </completionHelp> + <valueHelp> + <format>1.0</format> + <description>TLS v1.0</description> + </valueHelp> + <valueHelp> + <format>1.1</format> + <description>TLS v1.1</description> + </valueHelp> + <valueHelp> + <format>1.2</format> + <description>TLS v1.2</description> + </valueHelp> + <constraint> + <regex>(1.0|1.1|1.2)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="role"> + <properties> + <help>Private key for this host</help> + <completionHelp> + <list>active passive</list> + </completionHelp> + <valueHelp> + <format>active</format> + <description>Initiate TLS negotiation actively</description> + </valueHelp> + <valueHelp> + <format>passive</format> + <description>Waiting for TLS connections passively</description> + </valueHelp> + <constraint> + <regex>(active|passive)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="use-lzo-compression"> + <properties> + <help>Use fast LZO compression on this TUN/TAP interface</help> + <valueless/> + </properties> + </leafNode> + #include <include/interface-vrf.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in new file mode 100644 index 000000000..8a6c61312 --- /dev/null +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -0,0 +1,164 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py"> + <properties> + <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> + <priority>321</priority> + <constraint> + <regex>^pppoe[0-9]+$</regex> + </constraint> + <constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage> + <valueHelp> + <format>pppoeN</format> + <description>PPPoE dialer interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="access-concentrator"> + <properties> + <help>Access concentrator name (only connect to this concentrator)</help> + <constraint> + <regex>[a-zA-Z0-9]+$</regex> + </constraint> + <constraintErrorMessage>Access concentrator name must be composed of uppper and lower case letters or numbers only</constraintErrorMessage> + </properties> + </leafNode> + <node name="authentication"> + <properties> + <help>Authentication settings</help> + </properties> + <children> + <leafNode name="user"> + <properties> + <help>User name</help> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password</help> + </properties> + </leafNode> + </children> + </node> + <leafNode name="connect-on-demand"> + <properties> + <help>Automatic establishment of PPPOE connection when traffic is sent</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="default-route"> + <properties> + <help>Default route insertion behaviour (default: auto)</help> + <completionHelp> + <list>auto none force</list> + </completionHelp> + <constraint> + <regex>(auto|none|force)</regex> + </constraint> + <constraintErrorMessage>PPPoE default-route option must be 'auto', 'none', or 'force'</constraintErrorMessage> + <valueHelp> + <format>auto</format> + <description>Automatically install a default route</description> + </valueHelp> + <valueHelp> + <format>none</format> + <description>Do not install a default route</description> + </valueHelp> + <valueHelp> + <format>force</format> + <description>Replace existing default route</description> + </valueHelp> + </properties> + <defaultValue>auto</defaultValue> + </leafNode> + #include <include/dhcpv6-options.xml.i> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="idle-timeout"> + <properties> + <help>Delay before disconnecting idle session (in seconds)</help> + <valueHelp> + <format>n</format> + <description>Idle timeout in seconds</description> + </valueHelp> + </properties> + </leafNode> + <node name="ipv6"> + <children> + <node name="address"> + <properties> + <help>IPv6 address configuration modes</help> + </properties> + <children> + <leafNode name="autoconf"> + <properties> + <help>Enable Stateless Address Autoconfiguration (SLAAC)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="enable"> + <properties> + <help>Activate IPv6 support on this connection</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="source-interface"> + <properties> + <help>Physical Interface used for this PPPoE session</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + </leafNode> + <leafNode name="local-address"> + <properties> + <help>IPv4 address of local end of the PPPoE link</help> + <valueHelp> + <format>ipv4</format> + <description>Address of local end of the PPPoE link</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + #include <include/interface-mtu-68-1500.xml.i> + <leafNode name="no-peer-dns"> + <properties> + <help>Do not use DNS servers provided by the peer</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="remote-address"> + <properties> + <help>IPv4 address of remote end of the PPPoE link</help> + <valueHelp> + <format>ipv4</format> + <description>Address of remote end of the PPPoE link</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="service-name"> + <properties> + <help>Service name, only connect to access concentrators advertising this</help> + <constraint> + <regex>[a-zA-Z0-9]+$</regex> + </constraint> + <constraintErrorMessage>Service name must be composed of uppper and lower case letters or numbers only</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in new file mode 100644 index 000000000..4382db598 --- /dev/null +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -0,0 +1,82 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="pseudo-ethernet" owner="${vyos_conf_scripts_dir}/interfaces-pseudo-ethernet.py"> + <properties> + <help>Pseudo Ethernet</help> + <priority>321</priority> + <constraint> + <regex>^peth[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Pseudo Ethernet interface must be named pethN</constraintErrorMessage> + <valueHelp> + <format>pethN</format> + <description>Pseudo Ethernet interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <node name="ip"> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + #include <include/interface-enable-proxy-arp.xml.i> + #include <include/interface-proxy-arp-pvlan.xml.i> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + #include <include/source-interface-ethernet.xml.i> + #include <include/interface-mac.xml.i> + <leafNode name="mode"> + <properties> + <help>Receive mode (default: private)</help> + <completionHelp> + <list>private vepa bridge passthru</list> + </completionHelp> + <valueHelp> + <format>private</format> + <description>No communication with other pseudo-devices</description> + </valueHelp> + <valueHelp> + <format>vepa</format> + <description>Virtual Ethernet Port Aggregator reflective relay</description> + </valueHelp> + <valueHelp> + <format>bridge</format> + <description>Simple bridge between pseudo-devices</description> + </valueHelp> + <valueHelp> + <format>passthru</format> + <description>Promicious mode passthrough of underlying device</description> + </valueHelp> + <constraint> + <regex>(private|vepa|bridge|passthru)</regex> + </constraint> + <constraintErrorMessage>mode must be private, vepa, bridge or passthru</constraintErrorMessage> + </properties> + <defaultValue>private</defaultValue> + </leafNode> + #include <include/interface-mtu-68-9000.xml.i> + #include <include/vif-s.xml.i> + #include <include/vif.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in new file mode 100644 index 000000000..64520ce99 --- /dev/null +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -0,0 +1,283 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="tunnel" owner="${vyos_conf_scripts_dir}/interfaces-tunnel.py"> + <properties> + <help>Tunnel interface</help> + <priority>380</priority> + <constraint> + <regex>^tun[0-9]+$</regex> + </constraint> + <constraintErrorMessage>tunnel interface must be named tunN</constraintErrorMessage> + <valueHelp> + <format>tunN</format> + <description>Tunnel interface name</description> + </valueHelp> + </properties> + <children> + #include <include/interface-description.xml.i> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-vrf.xml.i> + #include <include/interface-mtu-64-8024.xml.i> + #include <include/interface-ipv4.xml.i> + #include <include/interface-ipv6.xml.i> + <leafNode name="local-ip"> + <properties> + <help>Local IP address for this tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 address for this tunnel</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 address for this tunnel [NOTICE: unavailable for mGRE tunnels]</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_local.py</script> + </completionHelp> + <constraint> + <!-- does it need fixing/changing to be more restrictive ? --> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="remote-ip"> + <properties> + <help>Remote IP address for this tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Remote IPv4 address for this tunnel</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Remote IPv6 address for this tunnel</description> + </valueHelp> + <constraint> + <!-- does it need fixing/changing to be more restrictive ? --> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="source-interface"> + <properties> + <help>Physical Interface used for underlaying traffic</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + <leafNode name="6rd-prefix"> + <properties> + <help>6rd network prefix</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="6rd-relay-prefix"> + <properties> + <help>6rd relay prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix of interface for 6rd</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dhcp-interface"> + <properties> + <help>dhcp interface</help> + <valueHelp> + <format>interface</format> + <description>DHCP interface that supplies the local IP address for this tunnel</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <constraint> + <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="encapsulation"> + <properties> + <help>Encapsulation of this tunnel interface</help> + <completionHelp> + <list>gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre</list> + </completionHelp> + <valueHelp> + <format>gre-bridge</format> + <description>Generic Routing Encapsulation bridge interface</description> + </valueHelp> + <valueHelp> + <format>ipip</format> + <description>IP in IP encapsulation</description> + </valueHelp> + <valueHelp> + <format>sit</format> + <description>Simple Internet Transition encapsulation</description> + </valueHelp> + <valueHelp> + <format>ipip6</format> + <description>IP in IP6 encapsulation</description> + </valueHelp> + <valueHelp> + <format>ip6ip6</format> + <description>IP6 in IP6 encapsulation</description> + </valueHelp> + <valueHelp> + <format>ip6gre</format> + <description>GRE over IPv6 network</description> + </valueHelp> + <constraint> + <regex>(gre|gre-bridge|ipip|sit|ipip6|ip6ip6|ip6gre)</regex> + </constraint> + <constraintErrorMessage>Must be one of 'gre' 'gre-bridge' 'ipip' 'sit' 'ipip6' 'ip6ip6' 'ip6gre'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="multicast"> + <properties> + <help>Multicast operation over tunnel</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable Multicast</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable Multicast (default)</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + <constraintErrorMessage>Must be 'disable' or 'enable'</constraintErrorMessage> + </properties> + </leafNode> + <node name="parameters"> + <properties> + <help>Tunnel parameters</help> + </properties> + <children> + <node name="ip"> + <properties> + <help>IPv4 specific tunnel parameters</help> + </properties> + <children> + <leafNode name="ttl"> + <properties> + <help>Time to live field</help> + <valueHelp> + <format>0-255</format> + <description>Time to live (default 255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>TTL must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tos"> + <properties> + <help>Type of Service (TOS)</help> + <valueHelp> + <format>0-99</format> + <description>Type of Service (TOS)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-99"/> + </constraint> + <constraintErrorMessage>TOS must be between 0 and 99</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Tunnel key</help> + <valueHelp> + <format>0-4294967295</format> + <description>Tunnel key</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>key must be between 0-4294967295</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <node name="ipv6"> + <properties> + <help>IPv6 specific tunnel parameters</help> + </properties> + <children> + <leafNode name="encaplimit"> + <properties> + <help>Encaplimit field</help> + <valueHelp> + <format>0-255</format> + <description>Encaplimit (default 4)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>key must be between 0-255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="flowlabel"> + <properties> + <help>Flowlabel</help> + <valueHelp> + <format>0x0-0x0FFFFF</format> + <description>Tunnel key, 'inherit' or hex value</description> + </valueHelp> + <constraint> + <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,5})</regex> + </constraint> + <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="hoplimit"> + <properties> + <help>Hoplimit</help> + <valueHelp> + <format>0-255</format> + <description>Hoplimit (default 64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>hoplimit must be between 0-255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tclass"> + <properties> + <help>Traffic class (Tclass)</help> + <valueHelp> + <format>0x0-0x0FFFFF</format> + <description>Traffic class, 'inherit' or hex value</description> + </valueHelp> + <constraint> + <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,2})</regex> + </constraint> + <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in new file mode 100644 index 000000000..8529f6885 --- /dev/null +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="vxlan" owner="${vyos_conf_scripts_dir}/interfaces-vxlan.py"> + <properties> + <help>Virtual Extensible LAN (VXLAN) Interface</help> + <priority>460</priority> + <constraint> + <regex>^vxlan[0-9]+$</regex> + </constraint> + <constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage> + <valueHelp> + <format>vxlanN</format> + <description>VXLAN interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + <leafNode name="group"> + <properties> + <help>Multicast group address for VXLAN interface</help> + <valueHelp> + <format>ipv4</format> + <description>Multicast IPv4 group address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Multicast IPv6 group address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <node name="ip"> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + #include <include/interface-enable-proxy-arp.xml.i> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="source-address"> + <properties> + <help>VXLAN source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source-address of VXLAN tunnel</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + #include <include/source-interface.xml.i> + #include <include/interface-mtu-1200-9000.xml.i> + <leafNode name="remote"> + <properties> + <help>Remote address of VXLAN tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Remote IPv4 address of VXLAN tunnel</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Remote IPv6 address of VXLAN tunnel</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Destination port of VXLAN tunnel (default: 8472)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>8472</defaultValue> + </leafNode> + <leafNode name="vni"> + <properties> + <help>Virtual Network Identifier</help> + <valueHelp> + <format>0-16777214</format> + <description>VXLAN virtual network identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777214"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in new file mode 100644 index 000000000..981bce826 --- /dev/null +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -0,0 +1,124 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="wireguard" owner="${vyos_conf_scripts_dir}/interfaces-wireguard.py"> + <properties> + <help>WireGuard Interface</help> + <priority>459</priority> + <constraint> + <regex>^wg[0-9]+$</regex> + </constraint> + <constraintErrorMessage>WireGuard interface must be named wgN</constraintErrorMessage> + <valueHelp> + <format>wgN</format> + <description>WireGuard interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + #include <include/port-number.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + <leafNode name="fwmark"> + <properties> + <help>A 32-bit fwmark value set on all outgoing packets</help> + <valueHelp> + <format>number</format> + <description>value which marks the packet for QoS/shaper</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="private-key"> + <properties> + <help>Private key to use on that interface</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + <defaultValue>default</defaultValue> + </leafNode> + <tagNode name="peer"> + <properties> + <help>peer alias</help> + <constraint> + <regex>[^ ]{1,100}$</regex> + </constraint> + <constraintErrorMessage>peer alias too long (limit 100 characters)</constraintErrorMessage> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>disables peer</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="pubkey"> + <properties> + <help>base64 encoded public key</help> + <constraint> + <regex>[0-9a-zA-Z\+/]{43}=$</regex> + </constraint> + <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="preshared-key"> + <properties> + <help>base64 encoded preshared key</help> + <constraint> + <regex>[0-9a-zA-Z\+/]{43}=$</regex> + </constraint> + <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="allowed-ips"> + <properties> + <help>IP addresses allowed to traverse the peer</help> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="address"> + <properties> + <help>IP address of tunnel remote end</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to listen for incoming connections</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + #include <include/port-number.xml.i> + <leafNode name="persistent-keepalive"> + <properties> + <help>Interval to send keepalive messages</help> + <valueHelp> + <format>1-65535</format> + <description>Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in new file mode 100644 index 000000000..6f0ec9e71 --- /dev/null +++ b/interface-definitions/interfaces-wireless.xml.in @@ -0,0 +1,800 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="wireless" owner="${vyos_conf_scripts_dir}/interfaces-wireless.py"> + <properties> + <help>Wireless (WiFi/WLAN) Network Interface</help> + <priority>400</priority> + <constraint> + <regex>^wlan[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Wireless interface must be named wlanN</constraintErrorMessage> + <valueHelp> + <format>wlanN</format> + <description>Wireless (WiFi/WLAN) interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6-dhcp.xml.i> + <node name="capabilities"> + <properties> + <help>HT and VHT capabilities for your card</help> + </properties> + <children> + <node name="ht"> + <properties> + <help>HT (High Throughput) settings</help> + </properties> + <children> + <leafNode name="40mhz-incapable"> + <properties> + <help>40MHz intolerance, use 20MHz only!</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="auto-powersave"> + <properties> + <help>Enable WMM-PS unscheduled automatic power aave delivery [U-APSD]</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="channel-set-width"> + <properties> + <help>Supported channel set width</help> + <completionHelp> + <list>ht20 ht40+ ht40-</list> + </completionHelp> + <valueHelp> + <format>ht20</format> + <description>Supported channel set width both 20 MHz only</description> + </valueHelp> + <valueHelp> + <format>ht40+</format> + <description>Supported channel set width both 20 MHz and 40 MHz with secondary channel above primary channel</description> + </valueHelp> + <valueHelp> + <format>ht40-</format> + <description>Supported channel set width both 20 MHz and 40 MHz with secondary channel below primary channel</description> + </valueHelp> + <constraint> + <regex>(ht20|ht40\+|ht40-)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="delayed-block-ack"> + <properties> + <help>Enable HT-delayed block ack</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dsss-cck-40"> + <properties> + <help>Enable DSSS_CCK-40</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="greenfield"> + <properties> + <help>Enable HT-greenfield</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ldpc"> + <properties> + <help>Enable LDPC coding capability</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="lsig-protection"> + <properties> + <help>Enable L-SIG TXOP protection capability</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="max-amsdu"> + <properties> + <help>Set maximum A-MSDU length</help> + <completionHelp> + <list>3839 7935</list> + </completionHelp> + <valueHelp> + <format>3839</format> + <description>Set maximum A-MSDU length to 3839 octets</description> + </valueHelp> + <valueHelp> + <format>7935</format> + <description>Set maximum A-MSDU length to 7935 octets</description> + </valueHelp> + <constraint> + <regex>(3839|7935)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="short-gi"> + <properties> + <help>Short GI capabilities</help> + <completionHelp> + <list>20 40</list> + </completionHelp> + <valueHelp> + <format>20</format> + <description>Short GI for 20 MHz</description> + </valueHelp> + <valueHelp> + <format>40</format> + <description>Short GI for 40 MHz</description> + </valueHelp> + <constraint> + <regex>(20|40)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="smps"> + <properties> + <help>Spatial Multiplexing Power Save (SMPS) settings</help> + <completionHelp> + <list>static dynamic</list> + </completionHelp> + <valueHelp> + <format>static</format> + <description>STATIC Spatial Multiplexing (SM) Power Save</description> + </valueHelp> + <valueHelp> + <format>dynamic</format> + <description>DYNAMIC Spatial Multiplexing (SM) Power Save</description> + </valueHelp> + <constraint> + <regex>(static|dynamic)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="stbc"> + <properties> + <help>Support for sending and receiving PPDU using STBC (Space Time Block Coding)</help> + </properties> + <children> + <leafNode name="rx"> + <properties> + <help>Enable receiving PPDU using STBC (Space Time Block Coding)</help> + <valueHelp> + <format>[1-3]+</format> + <description>Number of spacial streams that can use RX STBC</description> + </valueHelp> + <constraint> + <regex>[1-3]+</regex> + </constraint> + <constraintErrorMessage>Invalid capability item</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tx"> + <properties> + <help>Enable sending PPDU using STBC (Space Time Block Coding)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="require-ht"> + <properties> + <help>Require stations to support HT PHY (reject association if they do not)</help> + <completionHelp> + <script>echo If you reject non-HT, you also disable 802.11g</script> + </completionHelp> + <valueless/> + </properties> + </leafNode> + <leafNode name="require-vht"> + <properties> + <help>Require stations to support VHT PHY (reject association if they do not)</help> + <completionHelp> + <script>echo If you reject non-VHT, you also disable 802.11n</script> + </completionHelp> + <valueless/> + </properties> + </leafNode> + <node name="vht"> + <properties> + <help>VHT (Very High Throughput) settings</help> + </properties> + <children> + <leafNode name="antenna-count"> + <properties> + <help>Number of antennas on this card</help> + <valueHelp> + <format>1-8</format> + <description>Number of antennas for this card</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-8"/> + </constraint> + </properties> + </leafNode> + <leafNode name="antenna-pattern-fixed"> + <properties> + <help>Set if antenna pattern does not change during the lifetime of an association</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="beamform"> + <properties> + <help>Beamforming capabilities</help> + <completionHelp> + <list>single-user-beamformer single-user-beamformee multi-user-beamformer multi-user-beamformee</list> + </completionHelp> + <valueHelp> + <format>single-user-beamformer</format> + <description>Support for operation as single user beamformer</description> + </valueHelp> + <valueHelp> + <format>single-user-beamformee</format> + <description>Support for operation as single user beamformee</description> + </valueHelp> + <valueHelp> + <format>multi-user-beamformer</format> + <description>Support for operation as multi user beamformer</description> + </valueHelp> + <valueHelp> + <format>multi-user-beamformee</format> + <description>Support for operation as multi user beamformee</description> + </valueHelp> + <constraint> + <regex>(single-user-beamformer|single-user-beamformee|multi-user-beamformer|multi-user-beamformee)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="center-channel-freq"> + <properties> + <help>VHT operating channel center frequency</help> + </properties> + <children> + <leafNode name="freq-1"> + <properties> + <help>VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes)</help> + <valueHelp> + <format><34-173></format> + <description>5Ghz (802.11 a/h/j/n/ac) center channel index (use 42 for primary 80MHz channel 36)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 34-173"/> + </constraint> + <constraintErrorMessage>Channel center value must be between 34 and 173</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="freq-2"> + <properties> + <help>VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode)</help> + <valueHelp> + <format>34-173</format> + <description>5Ghz (802.11 a/h/j/n/ac) center channel index (use 58 for primary 80MHz channel 52)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 34-173"/> + </constraint> + <constraintErrorMessage>Channel center value must be between 34 and 173</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="channel-set-width"> + <properties> + <help>VHT operating Channel width</help> + <completionHelp> + <list>0 1 2 3</list> + </completionHelp> + <valueHelp> + <format>0</format> + <description>20 or 40 MHz channel width (default)</description> + </valueHelp> + <valueHelp> + <format>1</format> + <description>80 MHz channel width</description> + </valueHelp> + <valueHelp> + <format>2</format> + <description>160 MHz channel width</description> + </valueHelp> + <valueHelp> + <format>3</format> + <description>80+80 MHz channel width</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-3"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ldpc"> + <properties> + <help>Enable LDPC (Low Density Parity Check) coding capability</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="link-adaptation"> + <properties> + <help>VHT link adaptation capabilities</help> + <completionHelp> + <list>unsolicited both</list> + </completionHelp> + <valueHelp> + <format>unsolicited</format> + <description>Station provides only unsolicited VHT MFB</description> + </valueHelp> + <valueHelp> + <format>both</format> + <description>Station can provide VHT MFB in response to VHT MRQ and unsolicited VHT MFB</description> + </valueHelp> + <constraint> + <regex>(unsolicited|both)</regex> + </constraint> + <constraintErrorMessage>Invalid capability item</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="max-mpdu-exp"> + <properties> + <help>Set the maximum length of A-MPDU pre-EOF padding that the station can receive</help> + <valueHelp> + <format><0-7></format> + <description>Maximum length of A-MPDU pre-EOF padding = 2 pow(13 + x) -1 octets</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-7"/> + </constraint> + </properties> + </leafNode> + <leafNode name="max-mpdu"> + <properties> + <help>Increase Maximum MPDU length to 7991 or 11454 octets (otherwise: 3895 octets)</help> + <completionHelp> + <list>7991 11454</list> + </completionHelp> + <valueHelp> + <format>7991</format> + <description>ncrease Maximum MPDU length to 7991 octets</description> + </valueHelp> + <valueHelp> + <format>11454</format> + <description>ncrease Maximum MPDU length to 11454 octets</description> + </valueHelp> + <constraint> + <regex>(7991|11454)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="short-gi"> + <properties> + <help>Short GI capabilities</help> + <completionHelp> + <list>80 160</list> + </completionHelp> + <valueHelp> + <format>80</format> + <description>Short GI for 80 MHz</description> + </valueHelp> + <valueHelp> + <format>160</format> + <description>Short GI for 160 MHz</description> + </valueHelp> + <constraint> + <regex>(80|160)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="stbc"> + <properties> + <help>Support for sending and receiving PPDU using STBC (Space Time Block Coding)</help> + </properties> + <children> + <leafNode name="rx"> + <properties> + <help>Enable receiving PPDU using STBC (Space Time Block Coding)</help> + <valueHelp> + <format>[1-4]+</format> + <description>Number of spacial streams that can use RX STBC</description> + </valueHelp> + <constraint> + <regex>[1-4]+</regex> + </constraint> + <constraintErrorMessage>Invalid capability item</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tx"> + <properties> + <help>Enable sending PPDU using STBC (Space Time Block Coding)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="tx-powersave"> + <properties> + <help>Enable VHT TXOP Power Save Mode</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="vht-cf"> + <properties> + <help>Station supports receiving VHT variant HT Control field</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="channel"> + <properties> + <help>Wireless radio channel (use 0 for ACS auto channel selection)</help> + <valueHelp> + <format><1-14></format> + <description>2.4Ghz (802.11 b/g/n) Channel</description> + </valueHelp> + <valueHelp> + <format><0,34-173></format> + <description>5Ghz (802.11 a/h/j/n/ac) Channel</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 1-14 --range 34-173"/> + </constraint> + </properties> + </leafNode> + #include <include/interface-description.xml.i> + #include <include/dhcp-options.xml.i> + #include <include/dhcpv6-options.xml.i> + <leafNode name="disable-broadcast-ssid"> + <properties> + <help>Disable broadcast of SSID from access-point</help> + <valueless/> + </properties> + </leafNode> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="expunge-failing-stations"> + <properties> + <help>Disassociate stations based on excessive transmission failures</help> + <valueless/> + </properties> + </leafNode> + <node name="ip"> + <children> + #include <include/interface-arp-cache-timeout.xml.i> + #include <include/interface-disable-arp-filter.xml.i> + #include <include/interface-enable-arp-accept.xml.i> + #include <include/interface-enable-arp-announce.xml.i> + #include <include/interface-enable-arp-ignore.xml.i> + #include <include/interface-enable-proxy-arp.xml.i> + #include <include/interface-proxy-arp-pvlan.xml.i> + </children> + </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + #include <include/interface-hw-id.xml.i> + <leafNode name="isolate-stations"> + <properties> + <help>Isolate stations on the AP so they cannot see each other</help> + <valueless/> + </properties> + </leafNode> + #include <include/interface-mac.xml.i> + <leafNode name="max-stations"> + <properties> + <help>Maximum number of wireless radio stations. Excess stations will be rejected upon authentication request.</help> + <valueHelp> + <format><1-2007></format> + <description>Number of allowed stations</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2007"/> + </constraint> + <constraintErrorMessage>Number of stations must be between 1 and 2007</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="mgmt-frame-protection"> + <properties> + <help>Management Frame Protection (MFP) according to IEEE 802.11w</help> + <completionHelp> + <list>disabled optional required</list> + </completionHelp> + <valueHelp> + <format>disabled</format> + <description>no MFP (hostapd default)</description> + </valueHelp> + <valueHelp> + <format>optional</format> + <description>MFP optional</description> + </valueHelp> + <valueHelp> + <format>required</format> + <description>MFP enforced</description> + </valueHelp> + <constraint> + <regex>(disabled|optional|required)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="mode"> + <properties> + <help>Wireless radio mode</help> + <completionHelp> + <list>a b g n ac</list> + </completionHelp> + <valueHelp> + <format>a</format> + <description>802.11a - 54 Mbits/sec</description> + </valueHelp> + <valueHelp> + <format>b</format> + <description>802.11b - 11 Mbits/sec</description> + </valueHelp> + <valueHelp> + <format>g</format> + <description>802.11g - 54 Mbits/sec (default)</description> + </valueHelp> + <valueHelp> + <format>n</format> + <description>802.11n - 600 Mbits/sec</description> + </valueHelp> + <valueHelp> + <format>ac</format> + <description>802.11ac - 1300 Mbits/sec</description> + </valueHelp> + <constraint> + <regex>^(a|b|g|n|ac)$</regex> + </constraint> + </properties> + <defaultValue>g</defaultValue> + </leafNode> + <leafNode name="physical-device"> + <properties> + <help>Wireless physical device</help> + <completionHelp> + <script>${vyos_completion_dir}/list_wireless_phys.sh</script> + </completionHelp> + <constraint> + <validator name="wireless-phy"/> + </constraint> + </properties> + </leafNode> + <leafNode name="reduce-transmit-power"> + <properties> + <help>Transmission power reduction in dBm</help> + <valueHelp> + <format><0-255></format> + <description>TX power reduction in dBm</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>dBm value must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <node name="security"> + <properties> + <help>Wireless security settings</help> + </properties> + <children> + <node name="wep"> + <properties> + <help>Wired Equivalent Privacy (WEP) parameters</help> + </properties> + <children> + <leafNode name="key"> + <properties> + <help>WEP encryption key</help> + <valueHelp> + <format><hexdigits></format> + <description>Wired Equivalent Privacy key</description> + </valueHelp> + <constraint> + <regex>([a-fA-F0-9]{10}|[a-fA-F0-9]{26}|[a-fA-F0-9]{32})</regex> + </constraint> + <constraintErrorMessage>Invalid WEP key</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="wpa"> + <properties> + <help>Wifi Protected Access (WPA) parameters</help> + </properties> + <children> + <leafNode name="cipher"> + <properties> + <help>Cipher suite for WPA unicast packets</help> + <completionHelp> + <list>GCMP-256 GCMP CCMP-256 CCMP TKIP</list> + </completionHelp> + <valueHelp> + <format>GCMP-256</format> + <description>AES in Galois/counter mode with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>GCMP</format> + <description>AES in Galois/counter mode with 128-bit key</description> + </valueHelp> + <valueHelp> + <format>CCMP-256</format> + <description>AES in Counter mode with CBC-MAC with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>CCMP</format> + <description>AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] (supported on all WPA2 APs)</description> + </valueHelp> + <valueHelp> + <format>TKIP</format> + <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> + </valueHelp> + <constraint> + <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex> + </constraint> + <constraintErrorMessage>Invalid cipher selection</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="group-cipher"> + <properties> + <help>Cipher suite for WPA multicast and broadcast packets</help> + <completionHelp> + <list>GCMP-256 GCMP CCMP-256 CCMP TKIP</list> + </completionHelp> + <valueHelp> + <format>GCMP-256</format> + <description>AES in Galois/counter mode with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>GCMP</format> + <description>AES in Galois/counter mode with 128-bit key</description> + </valueHelp> + <valueHelp> + <format>CCMP-256</format> + <description>AES in Counter mode with CBC-MAC with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>CCMP</format> + <description>AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] (supported on all WPA2 APs)</description> + </valueHelp> + <valueHelp> + <format>TKIP</format> + <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> + </valueHelp> + <constraint> + <regex>^(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)$</regex> + </constraint> + <constraintErrorMessage>Invalid group cipher selection</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="mode"> + <properties> + <help>WPA mode</help> + <completionHelp> + <list>wpa wpa2 both</list> + </completionHelp> + <valueHelp> + <format>wpa</format> + <description>WPA (IEEE 802.11i/D3.0)</description> + </valueHelp> + <valueHelp> + <format>wpa2</format> + <description>WPA2 (full IEEE 802.11i/RSN)</description> + </valueHelp> + <valueHelp> + <format>both</format> + <description>Allow both WPA and WPA2</description> + </valueHelp> + <constraint> + <regex>^(wpa|wpa2|both)$</regex> + </constraint> + <constraintErrorMessage>Unknown WPA mode</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="passphrase"> + <properties> + <help>WPA personal shared pass phrase. If you are + using special characters in the WPA passphrase then single + quotes are required.</help> + <valueHelp> + <format><text></format> + <description>Passphrase of at least 8 but not more than 63 printable characters</description> + </valueHelp> + <constraint> + <regex>.{8,63}$</regex> + </constraint> + <constraintErrorMessage>Invalid WPA pass phrase, must be 8 to 63 printable characters!</constraintErrorMessage> + </properties> + </leafNode> + #include <include/radius-server.xml.i> + <node name="radius"> + <children> + <tagNode name="server"> + <children> + <leafNode name="accounting"> + <properties> + <help>Enable RADIUS server to receive accounting info</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + <leafNode name="ssid"> + <properties> + <help>Wireless access-point service set identifier (SSID)</help> + <constraint> + <regex>.{1,32}$</regex> + </constraint> + <constraintErrorMessage>Invalid SSID</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Wireless device type for this interface</help> + <completionHelp> + <list>access-point station monitor</list> + </completionHelp> + <valueHelp> + <format>access-point</format> + <description>Access-point forwards packets between other nodes</description> + </valueHelp> + <valueHelp> + <format>station</format> + <description>Connects to another access point</description> + </valueHelp> + <valueHelp> + <format>monitor</format> + <description>Passively monitor all packets on the frequency/channel</description> + </valueHelp> + <constraint> + <regex>^(access-point|station|monitor)$</regex> + </constraint> + <constraintErrorMessage>Type must be access-point, station or monitor</constraintErrorMessage> + </properties> + <defaultValue>monitor</defaultValue> + </leafNode> + #include <include/vif.xml.i> + #include <include/vif-s.xml.i> + </children> + </tagNode> + </children> + </node> + <node name="system"> + <children> + <leafNode name="wifi-regulatory-domain" owner="${vyos_conf_scripts_dir}/system-wifi-regdom.py"> + <properties> + <help>Wireless regulatory domain (mandatory)</help> + <priority>305</priority> + <completionHelp> + <list>US EU JP DE UK CN</list> + </completionHelp> + <valueHelp> + <format><code%gt;</format> + <description>Country code (ISO/IEC 3166-1)</description> + </valueHelp> + <constraint> + <regex>[A-Z][A-Z]$</regex> + </constraint> + <constraintErrorMessage>invalid country code</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-wirelessmodem.xml.in b/interface-definitions/interfaces-wirelessmodem.xml.in new file mode 100644 index 000000000..d375b808d --- /dev/null +++ b/interface-definitions/interfaces-wirelessmodem.xml.in @@ -0,0 +1,93 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="wirelessmodem" owner="${vyos_conf_scripts_dir}/interfaces-wirelessmodem.py"> + <properties> + <help>Wireless Modem (WWAN) Interface</help> + <priority>350</priority> + <constraint> + <regex>^wlm[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Wireless Modem interface must be named wlmN</constraintErrorMessage> + <valueHelp> + <format>wlmN</format> + <description>Wireless modem interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="apn"> + <properties> + <help>Access Point Name (APN)</help> + </properties> + </leafNode> + <node name="backup"> + <properties> + <help>Insert backup default route</help> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Distance backup default route</help> + <valueHelp> + <format>1-255</format> + <description>Distance of the backup route (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>Must be between (1-255)</constraintErrorMessage> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + </children> + </node> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="device"> + <properties> + <help>Serial device </help> + <completionHelp> + <script>ls -1 /dev | grep ttyS</script> + <script>if [ -d /dev/serial/by-bus ]; then ls -1 /dev/serial/by-bus; fi</script> + </completionHelp> + <valueHelp> + <format>ttySXX</format> + <description>TTY device name, regular serial port</description> + </valueHelp> + <valueHelp> + <format>usbNbXpY</format> + <description>TTY device name, USB based</description> + </valueHelp> + <constraint> + <regex>^(ttyS[0-9]+|usb[0-9]+b.*)$</regex> + </constraint> + </properties> + </leafNode> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="no-peer-dns"> + <properties> + <help>Do not use peer supplied DNS server information</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ondemand"> + <properties> + <help>Only dial when traffic is available</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/ipsec-settings.xml.in b/interface-definitions/ipsec-settings.xml.in new file mode 100644 index 000000000..bc54baa27 --- /dev/null +++ b/interface-definitions/ipsec-settings.xml.in @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpn"> + <children> + <node name="ipsec"> + <children> + <node name="options" owner="${vyos_conf_scripts_dir}/ipsec-settings.py"> + <properties> + <help>Global IPsec settings</help> + </properties> + <children> + <leafNode name="disable-route-autoinstall"> + <properties> + <valueless/> + <help>Do not automatically install routes to remote networks</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/lldp.xml.in b/interface-definitions/lldp.xml.in new file mode 100644 index 000000000..8f6629d81 --- /dev/null +++ b/interface-definitions/lldp.xml.in @@ -0,0 +1,191 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="lldp" owner="${vyos_conf_scripts_dir}/lldp.py"> + <properties> + <help>LLDP settings</help> + <priority>985</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Location data for interface</help> + <valueHelp> + <format>all</format> + <description>Location data all interfaces</description> + </valueHelp> + <valueHelp> + <format><intf></format> + <description>Location data for a specific interface</description> + </valueHelp> + <completionHelp> + <script>${vyatta_sbindir}/vyatta-interfaces.pl --show all</script> + <list>all</list> + </completionHelp> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Disable lldp on this interface</help> + <valueless/> + </properties> + </leafNode> + <node name="location"> + <properties> + <help>LLDP-MED location data [REQUIRED]</help> + </properties> + <children> + <node name="coordinate-based"> + <properties> + <help>Coordinate based location</help> + </properties> + <children> + <leafNode name="altitude"> + <properties> + <help>Altitude in meters</help> + <valueHelp> + <format>[+-]<meters></format> + <description>Altitude in meters</description> + </valueHelp> + <constraintErrorMessage>Altitude should be a positive or negative number</constraintErrorMessage> + <constraint> + <validator name="numeric"/> + </constraint> + </properties> + </leafNode> + <leafNode name="datum"> + <properties> + <help>Coordinate datum type</help> + <valueHelp> + <format>WGS84</format> + <description>WGS84 (default)</description> + </valueHelp> + <valueHelp> + <format>NAD83</format> + <description>NAD83</description> + </valueHelp> + <valueHelp> + <format>MLLW</format> + <description>NAD83/MLLW</description> + </valueHelp> + <completionHelp> + <list>WGS84 NAD83 MLLW</list> + </completionHelp> + <constraintErrorMessage>Datum should be WGS84, NAD83, or MLLW</constraintErrorMessage> + <constraint> + <regex>^(WGS84|NAD83|MLLW)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="latitude"> + <properties> + <help>Latitude [REQUIRED]</help> + <valueHelp> + <format><latitude></format> + <description>Latitude (example "37.524449N")</description> + </valueHelp> + <constraintErrorMessage>Latitude should be a number followed by S or N</constraintErrorMessage> + <constraint> + <regex>(\d+)(\.\d+)?[nNsS]$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="longitude"> + <properties> + <help>Longitude [REQUIRED]</help> + <valueHelp> + <format><longitude></format> + <description>Longitude (example "122.267255W")</description> + </valueHelp> + <constraintErrorMessage>Longiture should be a number followed by E or W</constraintErrorMessage> + <constraint> + <regex>(\d+)(\.\d+)?[eEwW]$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="elin"> + <properties> + <help>ECS ELIN (Emergency location identifier number)</help> + <valueHelp> + <format>0-9999999999</format> + <description>Emergency Call Service ELIN number (between 10-25 numbers)</description> + </valueHelp> + <constraint> + <regex>[0-9]{10,25}$</regex> + </constraint> + <constraintErrorMessage>ELIN number must be between 10-25 numbers</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + <node name="legacy-protocols"> + <properties> + <help>Legacy (vendor specific) protocols</help> + </properties> + <children> + <leafNode name="cdp"> + <properties> + <help>Listen for CDP for Cisco routers/switches</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="edp"> + <properties> + <help>Listen for EDP for Extreme routers/switches</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="fdp"> + <properties> + <help>Listen for FDP for Foundry routers/switches</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="sonmp"> + <properties> + <help>Listen for SONMP for Nortel routers/switches</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="management-address"> + <properties> + <help>Management IP Address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 Management Address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 Management Address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="snmp"> + <properties> + <help>SNMP parameters for LLDP</help> + </properties> + <children> + <leafNode name="enable"> + <properties> + <help>Enable SNMP queries of the LLDP database</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in new file mode 100644 index 000000000..8a14f4d25 --- /dev/null +++ b/interface-definitions/nat.xml.in @@ -0,0 +1,180 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="nat" owner="${vyos_conf_scripts_dir}/nat.py"> + <properties> + <help>Network Address Translation (NAT) parameters</help> + <priority>220</priority> + </properties> + <children> + <node name="destination"> + <properties> + <help>Destination NAT settings</help> + </properties> + <children> + #include <include/nat-rule.xml.i> + <tagNode name="rule"> + <children> + <leafNode name="inbound-interface"> + <properties> + <help>Inbound interface of NAT traffic</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + <node name="translation"> + <properties> + <help>Inside NAT IP (destination NAT only)</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address, subnet, or range</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to match</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <valueHelp> + <format>ipv4range</format> + <description>IPv4 address range to match</description> + </valueHelp> + <!-- TODO: add general iptables constraint script --> + </properties> + </leafNode> + #include <include/nat-translation-port.xml.i> + </children> + </node> + </children> + </tagNode> + </children> + </node> + <node name="nptv6"> + <properties> + <help>IPv6-to-IPv6 Network Prefix Translation Settings</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>NPTv6 rule number</help> + <valueHelp> + <format>1-999999</format> + <description>Number for this rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + <leafNode name="description"> + <properties> + <help>Rule description</help> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable NAT rule</help> + <valueless/> + </properties> + </leafNode> + #include <include/nat-interface.xml.i> + <node name="source"> + <properties> + <help>IPv6 source prefix options</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>IPv6 prefix to be translated</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="translation"> + <properties> + <help>Translated IPv6 prefix options</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>IPv6 prefix to translate to</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + <node name="source"> + <properties> + <help>Source NAT settings</help> + </properties> + <children> + #include <include/nat-rule.xml.i> + <tagNode name="rule"> + <children> + #include <include/nat-interface.xml.i> + <node name="translation"> + <properties> + <help>Outside NAT IP (source NAT only)</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address, subnet, or range</help> + <completionHelp> + <list>masquerade</list> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to match</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <valueHelp> + <format>ipv4range</format> + <description>IPv4 address range to match</description> + </valueHelp> + <valueHelp> + <format>masquerade</format> + <description>NAT to the primary address of outbound-interface</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv4-address"/> + <validator name="ipv4-range"/> + <regex>(masquerade)</regex> + </constraint> + </properties> + </leafNode> + #include <include/nat-translation-port.xml.i> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in new file mode 100644 index 000000000..485487a42 --- /dev/null +++ b/interface-definitions/ntp.xml.in @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<!-- NTP configuration --> +<interfaceDefinition> + <node name="system"> + <children> + <node name="ntp" owner="${vyos_conf_scripts_dir}/ntp.py"> + <properties> + <help>Network Time Protocol (NTP) configuration</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="server"> + <properties> + <help>Network Time Protocol (NTP) server</help> + </properties> + <children> + <leafNode name="noselect"> + <properties> + <help>Marks the server as unused</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="preempt"> + <properties> + <help>Specifies the association as preemptable rather than the default persistent</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="prefer"> + <properties> + <help>Marks the server as preferred</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <node name="allow-clients"> + <properties> + <help>Network Time Protocol (NTP) server options</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IP address</help> + <valueHelp> + <format>ipv4net</format> + <description>IP address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="listen-address"> + <properties> + <help>Addresses to listen for NTP queries</help> + <valueHelp> + <format>ipv4</format> + <description>Network Time Protocol (NTP) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Network Time Protocol (NTP) IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + #include <include/interface-vrf.xml.i> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-bfd.xml.in b/interface-definitions/protocols-bfd.xml.in new file mode 100644 index 000000000..8900e7955 --- /dev/null +++ b/interface-definitions/protocols-bfd.xml.in @@ -0,0 +1,140 @@ +<?xml version="1.0"?> +<!-- Bidirectional Forwarding Detection (BFD) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="bfd" owner="${vyos_conf_scripts_dir}/protocols_bfd.py"> + <properties> + <help>Bidirectional Forwarding Detection (BFD)</help> + <priority>820</priority> + </properties> + <children> + <tagNode name="peer"> + <properties> + <help>Configures a new BFD peer to listen and talk to</help> + <valueHelp> + <format>ipv4</format> + <description>BFD peer IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>BFD peer IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <node name="source"> + <properties> + <help>Bind listener to specified interface/address, mandatory for IPv6</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Local interface to bind our peer listener to</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + <leafNode name="address"> + <properties> + <help>Local address to bind our peer listener to</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 address used to connect to the peer</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 address used to connect to the peer</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="interval"> + <properties> + <help>Configure timer intervals</help> + </properties> + <children> + <leafNode name="receive"> + <properties> + <help>Minimum interval of receiving control packets</help> + <valueHelp> + <format>10-60000</format> + <description>Interval in milliseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-60000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="transmit"> + <properties> + <help>Minimum interval of transmitting control packets</help> + <valueHelp> + <format>10-60000</format> + <description>Interval in milliseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-60000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="multiplier"> + <properties> + <help>Multiplier to determine packet loss</help> + <valueHelp> + <format>2-255</format> + <description>Remote transmission interval will be multiplied by this value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 2-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="echo-interval"> + <properties> + <help>Echo receive transmission interval</help> + <valueHelp> + <format>10-60000</format> + <description>The minimal echo receive transmission interval that this system is capable of handling</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-60000"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="shutdown"> + <properties> + <help>Disable this peer</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="multihop"> + <properties> + <help>Allow this BFD peer to not be directly connected</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="echo-mode"> + <properties> + <help>Enables the echo transmission mode</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-bgp.xml.in b/interface-definitions/protocols-bgp.xml.in new file mode 100644 index 000000000..3a4600753 --- /dev/null +++ b/interface-definitions/protocols-bgp.xml.in @@ -0,0 +1,1205 @@ +<?xml version="1.0"?> +<!-- Border Gateway Protocol (BGP) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <tagNode name="nbgp" owner="${vyos_conf_scripts_dir}/protocols_bgp.py"> + <properties> + <help>Border Gateway Protocol (BGP) parameters</help> + <valueHelp> + <format><1-4294967294></format> + <description>AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + </constraint> + <priority>820</priority> + </properties> + <children> + <node name="address-family"> + <properties> + <help>BGP address-family parameters</help> + </properties> + <children> + <node name="ipv4-unicast"> + <properties> + <help>IPv4 BGP settings</help> + </properties> + <children> + <tagNode name="aggregate-address"> + <properties> + <help>BGP aggregate network</help> + <valueHelp> + <format>ipv4net</format> + <description>BGP aggregate network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + #include <include/bgp-afi-aggregate-address.xml.i> + </children> + </tagNode> + <tagNode name="network"> + <properties> + <help>BGP network</help> + <valueHelp> + <format>ipv4net</format> + <description>BGP network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="backdoor"> + <properties> + <help>Network as a backdoor route</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="route-map"> + <properties> + <help>Route-map to modify route attributes</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </tagNode> + <node name="redistribute"> + <properties> + <help>Redistribute routes from other protocols into BGP</help> + </properties> + <children> + <node name="connected"> + <properties> + <help>Redistribute connected routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="kernel"> + <properties> + <help>Redistribute kernel routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="ospf"> + <properties> + <help>Redistribute OSPF routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="rip"> + <properties> + <help>Redistribute RIP routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="static"> + <properties> + <help>Redistribute static routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <leafNode name="table"> + <properties> + <help>Redistribute non-main Kernel Routing Table</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <node name="ipv6-unicast"> + <properties> + <help>IPv6 BGP settings</help> + </properties> + <children> + <tagNode name="aggregate-address"> + <properties> + <help>BGP aggregate network</help> + <valueHelp> + <format>ipv6net</format> + <description>Aggregate network</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + #include <include/bgp-afi-aggregate-address.xml.i> + </children> + </tagNode> + <tagNode name="network"> + <properties> + <help>BGP network</help> + <valueHelp> + <format>ipv6net</format> + <description>Aggregate network</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="path-limit"> + <properties> + <help>AS-path hopcount limit</help> + <valueHelp> + <format><0-255></format> + <description>AS path hop count limit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="route-map"> + <properties> + <help>Route-map to modify route attributes</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + </children> + </tagNode> + <node name="redistribute"> + <properties> + <help>Redistribute routes from other protocols into BGP</help> + </properties> + <children> + <node name="connected"> + <properties> + <help>Redistribute connected routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="kernel"> + <properties> + <help>Redistribute kernel routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="ospf"> + <properties> + <help>Redistribute OSPF routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="rip"> + <properties> + <help>Redistribute RIP routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <node name="static"> + <properties> + <help>Redistribute static routes into BGP</help> + </properties> + <children> + #include <include/bgp-afi-redistribute-metric-route-map.xml.i> + </children> + </node> + <leafNode name="table"> + <properties> + <help>Redistribute non-main Kernel Routing Table</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="maximum-paths"> + <properties> + <help>BGP multipaths</help> + </properties> + <children> + <leafNode name="ebgp"> + <properties> + <help>Maximum ebgp multipaths</help> + <valueHelp> + <format><1-255></format> + <description>EBGP multipaths</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ibgp"> + <properties> + <help>Maximum ibgp multipaths</help> + <valueHelp> + <format><1-255></format> + <description>EBGP multipaths</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <tagNode name="neighbor"> + <properties> + <help>BGP neighbor</help> + <valueHelp> + <format>ipv4</format> + <description>BGP neighbor IP address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>BGP neighbor IPv6 address</description> + </valueHelp> + <valueHelp> + <format><interface></format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex> + </constraint> + </properties> + <children> + <node name="address-family"> + <properties> + <help>Parameters relating to IPv4 or IPv6 routes</help> + </properties> + <children> + #include <include/bgp-neighbor-afi-ipv4-unicast.xml.i> + #include <include/bgp-neighbor-afi-ipv6-unicast.xml.i> + </children> + </node> + <leafNode name="advertisement-interval"> + <properties> + <help>Minimum interval for sending routing updates</help> + <valueHelp> + <format><0-600></format> + <description>Advertisement interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-600"/> + </constraint> + </properties> + </leafNode> + <node name="bfd"> + <properties> + <help>Enable Bidirectional Forwarding Detection (BFD) support</help> + </properties> + <children> + <leafNode name="check-control-plane-failure"> + <properties> + <help>Allow to write CBIT independence in BFD outgoing packets and read both C-BIT value of BFD and lookup BGP peer status</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="capability"> + <properties> + <help>Advertise capabilities to this neighbor</help> + </properties> + <children> + <leafNode name="dynamic"> + <properties> + <help>Advertise dynamic capability to this neighbor</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="extended-nexthop"> + <properties> + <help>Advertise extended-nexthop capability to this neighbor</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="description"> + <properties> + <help>Description for this neighbor</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-capability-negotiation"> + <properties> + <help>Disable capability negotiation with this neighbor</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-connected-check"> + <properties> + <help>Disable check to see if eBGP peer address is a connected route</help> + <valueless/> + </properties> + </leafNode> + <node name="disable-send-community"> + <properties> + <help>Disable sending community attributes to this neighbor (IPv4)</help> + </properties> + <children> + <leafNode name="extended"> + <properties> + <help>Disable sending extended community attributes to this neighbor (IPv4)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="standard"> + <properties> + <help>Disable sending standard community attributes to this neighbor (IPv4)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="ebgp-multihop"> + <properties> + <help>Allow this EBGP neighbor to not be on a directly connected network</help> + <valueHelp> + <format><1-255></format> + <description>Number of hops</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <node name="interface"> + <properties> + <help>Interface parameters</help> + </properties> + <children> + <leafNode name="peer-group"> + <properties> + <help>Peer group for this peer</help> + </properties> + </leafNode> + <leafNode name="remote-as"> + <properties> + <help>Neighbor BGP AS number [REQUIRED]</help> + <completionHelp> + <list>external internal</list> + </completionHelp> + <valueHelp> + <format><1-4294967294></format> + <description>Neighbor AS number</description> + </valueHelp> + <valueHelp> + <format>external</format> + <description>Any AS different from the local AS</description> + </valueHelp> + <valueHelp> + <format>internal</format> + <description>Neighbor AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + <regex>(external|internal)</regex> + </constraint> + <constraintErrorMessage>Invalid ASN value</constraintErrorMessage> + </properties> + </leafNode> + <node name="v6only"> + <properties> + <help>Enable BGP with v6 link-local only</help> + </properties> + <children> + <leafNode name="peer-group"> + <properties> + <help>Peer group for this peer</help> + </properties> + </leafNode> + <leafNode name="remote-as"> + <properties> + <help>Neighbor BGP AS number [REQUIRED]</help> + <completionHelp> + <list>external internal</list> + </completionHelp> + <valueHelp> + <format><1-4294967294></format> + <description>Neighbor AS number</description> + </valueHelp> + <valueHelp> + <format>external</format> + <description>Any AS different from the local AS</description> + </valueHelp> + <valueHelp> + <format>internal</format> + <description>Neighbor AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + <regex>(external|internal)</regex> + </constraint> + <constraintErrorMessage>Invalid ASN value</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <tagNode name="local-as"> + <properties> + <help>Local AS number</help> + <valueHelp> + <format><1-4294967294></format> + <description>Local AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + </constraint> + </properties> + <children> + <leafNode name="no-prepend"> + <properties> + <help>Disable prepending local-as to updates from EBGP peers</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="override-capability"> + <properties> + <help>Ignore capability negotiation with specified neighbor</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="passive"> + <properties> + <help>Do not initiate a session with this neighbor</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>BGP MD5 password</help> + </properties> + </leafNode> + <leafNode name="peer-group"> + <properties> + <help>IPv4 peer group for this peer</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Neighbor BGP port</help> + <valueHelp> + <format><1-65535></format> + <description>Neighbor BGP port number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="remote-as"> + <properties> + <help>Neighbor BGP AS number [REQUIRED]</help> + <completionHelp> + <list>external internal</list> + </completionHelp> + <valueHelp> + <format><1-4294967294></format> + <description>Neighbor AS number</description> + </valueHelp> + <valueHelp> + <format>external</format> + <description>Any AS different from the local AS</description> + </valueHelp> + <valueHelp> + <format>internal</format> + <description>Neighbor AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + <regex>(external|internal)</regex> + </constraint> + <constraintErrorMessage>Invalid ASN value</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="shutdown"> + <properties> + <help>Administratively shut down neighbor</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="strict-capability-match"> + <properties> + <help>Enable strict capability negotiation</help> + <valueless/> + </properties> + </leafNode> + <node name="timers"> + <properties> + <help>Neighbor timers</help> + </properties> + <children> + <leafNode name="connect"> + <properties> + <help>BGP connect timer for this neighbor</help> + <valueHelp> + <format><1-65535></format> + <description>Connect timer in seconds</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Disable connect timer</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="holdtime"> + <properties> + <help>BGP hold timer for this neighbor</help> + <valueHelp> + <format><1-65535></format> + <description>Hold timer in seconds</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Hold timer disabled</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="keepalive"> + <properties> + <help>BGP keepalive interval for this neighbor</help> + <valueHelp> + <format><1-65535></format> + <description>Keepalive interval in seconds (default 60)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="ttl-security"> + <properties> + <help>Ttl security mechanism for this BGP peer</help> + </properties> + <children> + <leafNode name="hops"> + <properties> + <help>Number of the maximum number of hops to the BGP peer</help> + <valueHelp> + <format><1-254></format> + <description>Number of hops</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-254"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="update-source"> + <!-- Need to check format interfaces --> + <properties> + <help>Source IP of routing updates</help> + <valueHelp> + <format>ipv4</format> + <description>IP address of route source</description> + </valueHelp> + <valueHelp> + <format><interface></format> + <description>Interface as route source</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <node name="parameters"> + <properties> + <help>BGP parameters</help> + </properties> + <children> + <leafNode name="always-compare-med"> + <properties> + <help>Always compare MEDs from different neighbors</help> + <valueless/> + </properties> + </leafNode> + <node name="bestpath"> + <properties> + <help>Default bestpath selection mechanism</help> + </properties> + <children> + <node name="as-path"> + <properties> + <help>AS-path attribute comparison parameters</help> + </properties> + <children> + <leafNode name="confed"> + <properties> + <help>Compare AS-path lengths including confederation sets and sequences</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ignore"> + <properties> + <help>Ignore AS-path length in selecting a route</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="multipath-relax"> + <properties> + <help>Allow load sharing across routes that have different AS paths (but same length)</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="compare-routerid"> + <properties> + <help>Compare the router-id for identical EBGP paths</help> + <valueless/> + </properties> + </leafNode> + <node name="med"> + <properties> + <help>MED attribute comparison parameters</help> + </properties> + <children> + <leafNode name="confed"> + <properties> + <help>Compare MEDs among confederation paths</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="missing-as-worst"> + <properties> + <help>Treat missing route as a MED as the least preferred one</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="cluster-id"> + <properties> + <help>Route-reflector cluster-id</help> + <valueHelp> + <format>ipv4</format> + <description>Route-reflector cluster-id</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <node name="confederation"> + <properties> + <help>AS confederation parameters</help> + </properties> + <children> + <leafNode name="identifier"> + <properties> + <help>Confederation AS identifier [REQUIRED]</help> + <valueHelp> + <format><1-4294967294></format> + <description>Confederation AS id</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + </constraint> + </properties> + </leafNode> + <leafNode name="peers"> + <properties> + <help>Peer ASs in the BGP confederation</help> + <valueHelp> + <format><1-4294967294></format> + <description>Peer AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="dampening"> + <properties> + <help>Enable route-flap dampening</help> + </properties> + <children> + <leafNode name="half-life"> + <properties> + <help>Half-life time for dampening [REQUIRED]</help> + <valueHelp> + <format><1-45></format> + <description>Half-life penalty in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-45"/> + </constraint> + </properties> + </leafNode> + <leafNode name="max-suppress-time"> + <properties> + <help>Maximum duration to suppress a stable route [REQUIRED]</help> + <valueHelp> + <format><1-255></format> + <description>Maximum suppress duration in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="re-use"> + <properties> + <help>Time to start reusing a route [REQUIRED]</help> + <valueHelp> + <format><1-20000></format> + <description>Re-use time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-20000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="start-suppress-time"> + <properties> + <help>When to start suppressing a route [REQUIRED]</help> + <valueHelp> + <format><1-20000></format> + <description>Start-suppress-time</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-20000"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="default"> + <properties> + <help>BGP defaults</help> + </properties> + <children> + <leafNode name="local-pref"> + <properties> + <help>Default local preference</help> + <valueHelp> + <format><0-4294967295></format> + <description>Local preference</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="no-ipv4-unicast"> + <properties> + <help>Deactivate IPv4 unicast for a peer by default</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="deterministic-med"> + <properties> + <help>Compare MEDs between different peers in the same AS</help> + <valueless/> + </properties> + </leafNode> + <node name="distance"> + <properties> + <help>Administratives distances for BGP routes</help> + </properties> + <children> + <node name="global"> + <properties> + <help>Global administratives distances for BGP routes</help> + </properties> + <children> + <leafNode name="external"> + <properties> + <help>Administrative distance for external BGP routes</help> + <valueHelp> + <format><1-255></format> + <description>Administrative distance for external BGP routes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="internal"> + <properties> + <help>Administrative distance for internal BGP routes</help> + <valueHelp> + <format><1-255></format> + <description>Administrative distance for internal BGP routes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="local"> + <properties> + <help>Administrative distance for local BGP routes</help> + <valueHelp> + <format><1-255></format> + <description>Administrative distance for internal BGP routes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <tagNode name="prefix"> + <properties> + <help>Administrative distance for a specific BGP prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>Administrative distance for a specific BGP prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Administrative distance for prefix</help> + <valueHelp> + <format><1-255></format> + <description>Administrative distance for external BGP routes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="enforce-first-as"> + <properties> + <help>Require first AS in the path to match peer AS number</help> + <valueless/> + </properties> + </leafNode> + <node name="graceful-restart"> + <properties> + <help>Graceful restart capability parameters</help> + </properties> + <children> + <leafNode name="stalepath-time"> + <properties> + <help>Maximum time to hold onto restarting neighbors stale paths</help> + <valueHelp> + <format><1-3600></format> + <description>Hold time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="log-neighbor-changes"> + <properties> + <help>Log neighbor up/down changes and reset reason</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="network-import-check"> + <properties> + <help>Enable IGP route check for network statements</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="no-client-to-client-reflection"> + <properties> + <help>Disable client to client route reflection</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="no-fast-external-failover"> + <properties> + <help>Disable immediate session reset on peer link down event</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="router-id"> + <properties> + <help>BGP router id</help> + <valueHelp> + <format>ipv4</format> + <description>BGP router id</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <tagNode name="peer-group"> + <properties> + <help>BGP peer-group</help> + </properties> + <children> + <node name="address-family"> + <properties> + <help>BGP peer-group address-family parameters</help> + </properties> + <children> + #include <include/bgp-peer-group-afi-ipv4-unicast.xml.i> + #include <include/bgp-peer-group-afi-ipv6-unicast.xml.i> + </children> + </node> + <leafNode name="bfd"> + <properties> + <help>Enable Bidirectional Forwarding Detection (BFD) support</help> + <valueless/> + </properties> + </leafNode> + <node name="capability"> + <properties> + <help>Advertise capabilities to this peer-group</help> + </properties> + <children> + <leafNode name="dynamic"> + <properties> + <help>Advertise dynamic capability to this peer-group</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="extended-nexthop"> + <properties> + <help>Advertise extended-nexthop capability to this neighbor</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="description"> + <properties> + <help>Description for this peer-group</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-capability-negotiation"> + <properties> + <help>Disable capability negotiation with this peer-group</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-connected-check"> + <properties> + <help>Disable check to see if eBGP peer address is a connected route</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ebgp-multihop"> + <properties> + <help>Allow this EBGP peer-group to not be on a directly connected network</help> + <valueHelp> + <format><1-255></format> + <description>Number of hops</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <tagNode name="local-as"> + <properties> + <help>Local AS number [REQUIRED]</help> + <valueHelp> + <format><1-4294967294></format> + <description>Local AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + </constraint> + </properties> + <children> + <leafNode name="no-prepend"> + <properties> + <help>Disable prepending local-as to updates from EBGP peers</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="override-capability"> + <properties> + <help>Ignore capability negotiation with specified peer-group</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="passive"> + <properties> + <help>Do not intiate a session with this peer-group</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>BGP MD5 password</help> + </properties> + </leafNode> + <leafNode name="remote-as"> + <properties> + <help>Neighbor BGP AS number [REQUIRED]</help> + <completionHelp> + <list>external internal</list> + </completionHelp> + <valueHelp> + <format><1-4294967294></format> + <description>Neighbor AS number</description> + </valueHelp> + <valueHelp> + <format>external</format> + <description>Any AS different from the local AS</description> + </valueHelp> + <valueHelp> + <format>internal</format> + <description>Neighbor AS number</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967294"/> + <regex>(external|internal)</regex> + </constraint> + <constraintErrorMessage>Invalid ASN value</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="shutdown"> + <properties> + <help>Administratively shut down peer-group</help> + <valueless/> + </properties> + </leafNode> + <node name="ttl-security"> + <properties> + <help>Ttl security mechanism</help> + </properties> + <children> + <leafNode name="hops"> + <properties> + <help>Number of the maximum number of hops to the BGP peer</help> + <valueHelp> + <format><1-254></format> + <description>Number of hops</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-254"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="update-source"> + <!-- Need to check format interfaces --> + <properties> + <help>Source IP of routing updates</help> + <valueHelp> + <format>ipv4</format> + <description>IP address of route source</description> + </valueHelp> + <valueHelp> + <format><interface></format> + <description>Interface as route source</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="route-map"> + <properties> + <help>Filter routes installed in local route map</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + </leafNode> + <node name="timers"> + <properties> + <help>BGP protocol timers</help> + </properties> + <children> + <leafNode name="holdtime"> + <properties> + <help>BGP holdtime interval</help> + <valueHelp> + <format><4-65535></format> + <description>Hold-time in seconds (default 180)</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Do not hold routes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="keepalive"> + <properties> + <help>Keepalive interval</help> + <valueHelp> + <format><1-65535></format> + <description>Keep-alive time in seconds (default 60)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-igmp.xml.in b/interface-definitions/protocols-igmp.xml.in new file mode 100644 index 000000000..a9b11e1a3 --- /dev/null +++ b/interface-definitions/protocols-igmp.xml.in @@ -0,0 +1,88 @@ +<?xml version="1.0"?> +<!-- Internet Group Management Protocol (IGMP) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="igmp" owner="${vyos_conf_scripts_dir}/protocols_igmp.py"> + <properties> + <help>Internet Group Management Protocol (IGMP)</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IGMP interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="join"> + <properties> + <help>IGMP join multicast group</help> + <valueHelp> + <format>ipv4</format> + <description>Multicast group address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="source"> + <properties> + <help>Source address</help> + <valueHelp> + <format>ipv4</format> + <description>Source address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="version"> + <properties> + <help>IGMP version</help> + <valueHelp> + <format>2-3</format> + <description>IGMP version</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 2-3"/> + </constraint> + </properties> + </leafNode> + <leafNode name="query-interval"> + <properties> + <help>IGMP host query interval</help> + <valueHelp> + <format>1-1800</format> + <description>Query interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1800"/> + </constraint> + </properties> + </leafNode> + <leafNode name="query-max-response-time"> + <properties> + <help>IGMP max query response time</help> + <valueHelp> + <format>10-250</format> + <description>Query response value in deci-seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-250"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-isis.xml.in b/interface-definitions/protocols-isis.xml.in new file mode 100644 index 000000000..988231108 --- /dev/null +++ b/interface-definitions/protocols-isis.xml.in @@ -0,0 +1,552 @@ +<?xml version="1.0"?> +<!-- Protocol IS-IS configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <tagNode name="isis" owner="${vyos_conf_scripts_dir}/protocols_isis.py"> + <properties> + <help>Intermediate System to Intermediate System (ISIS)</help> + <valueHelp> + <format>text(TAG)</format> + <description>ISO Routing area tag</description> + </valueHelp> + </properties> + <children> + <node name="area-password"> + <properties> + <help>Configure the authentication password for an area</help> + </properties> + <children> + <leafNode name="plaintext-password"> + <properties> + <help>Plain-text authentication type</help> + <valueHelp> + <format><text></format> + <description>Level-wide password</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="md5"> + <properties> + <help>MD5 authentication type</help> + <valueHelp> + <format><md5></format> + <description>Level-wide password</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + <node name="default-information"> + <properties> + <help>Control distribution of default information</help> + </properties> + <children> + <node name="originate"> + <properties> + <help>Distribute a default route</help> + </properties> + <children> + <node name="ipv4"> + <properties> + <help>Distribute default route for IPv4</help> + </properties> + <children> + <leafNode name="level-1"> + <properties> + <help>Distribute default route into level-1</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="level-2"> + <properties> + <help>Distribute default route into level-2</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="ipv6"> + <properties> + <help>Distribute default route for IPv6</help> + </properties> + <children> + <leafNode name="level-1"> + <properties> + <help>Distribute default route into level-1</help> + <completionHelp> + <list>always</list> + </completionHelp> + <valueHelp> + <format>always</format> + <description>Always advertise default route</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="level-2"> + <properties> + <help>Distribute default route into level-2</help> + <completionHelp> + <list>always</list> + </completionHelp> + <valueHelp> + <format>always</format> + <description>Always advertise default route</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="domain-password"> + <properties> + <help>Set the authentication password for a routing domain</help> + </properties> + <children> + <leafNode name="plaintext-password"> + <properties> + <help>Plain-text authentication type</help> + <valueHelp> + <format><text></format> + <description>Level-wide password</description> + </valueHelp> + </properties> + </leafNode> + <!-- <leafNode name="md5"> + <properties> + <help>MD5 authentication type</help> + <valueHelp> + <format><md5></format> + <description>Level-wide password</description> + </valueHelp> + </properties> + </leafNode> --> + </children> + </node> + <leafNode name="dynamic-hostname"> + <properties> + <help>Dynamic hostname for IS-IS</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="level"> + <properties> + <help>IS-IS level number</help> + <completionHelp> + <list>level-1 level-1-2 level-2</list> + </completionHelp> + <valueHelp> + <format>level-1</format> + <description>Act as a station router</description> + </valueHelp> + <valueHelp> + <format>level-1-2</format> + <description>Act as both a station and an area router</description> + </valueHelp> + <valueHelp> + <format>level-2</format> + <description>Act as an area router</description> + </valueHelp> + <constraint> + <regex>(level-1|level-1-2|level-2)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="lsp-gen-interval"> + <properties> + <help>Minimum interval between regenerating same LSP</help> + <valueHelp> + <format><1-120></format> + <description>Minimum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-120"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lsp-mtu"> + <properties> + <help>Configure the maximum size of generated LSPs</help> + <valueHelp> + <format><128-4352></format> + <description>Maximum size of generated LSPs</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 128-4352"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lsp-refresh-interval"> + <properties> + <help>LSP refresh interval</help> + <valueHelp> + <format><1-65235></format> + <description>LSP refresh interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65235"/> + </constraint> + </properties> + </leafNode> + <leafNode name="max-lsp-lifetime"> + <properties> + <help>Maximum LSP lifetime</help> + <valueHelp> + <format><350-65535></format> + <description>LSP lifetime in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="metric-style"> + <properties> + <help>Use old-style (ISO 10589) or new-style packet formats</help> + <completionHelp> + <list>narrow transition wide</list> + </completionHelp> + <valueHelp> + <format>narrow</format> + <description>Use old style of TLVs with narrow metric</description> + </valueHelp> + <valueHelp> + <format>transition</format> + <description>Send and accept both styles of TLVs during transition</description> + </valueHelp> + <valueHelp> + <format>wide</format> + <description>Use new style of TLVs to carry wider metric</description> + </valueHelp> + <constraint> + <regex>(narrow|transition|wide)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="net"> + <properties> + <help>A Network Entity Title for this process (ISO only)</help> + <valueHelp> + <format>XX.XXXX. ... .XXX.XX</format> + <description>Network entity title (NET)</description> + </valueHelp> + <constraint> + <regex>[a-fA-F0-9]{2}(\.[a-fA-F0-9]{4}){3,9}\.[a-fA-F0-9]{2}</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="purge-originator"> + <properties> + <help>Use the RFC 6232 purge-originator</help> + <valueless/> + </properties> + </leafNode> + <node name="redistribute"> + <properties> + <help>Redistribute information from another routing protocol</help> + </properties> + <children> + <node name="ipv4"> + <properties> + <help>Redistribute IPv4 routes</help> + </properties> + <children> + <node name="bgp"> + <properties> + <help>Border Gateway Protocol (BGP)</help> + </properties> + <children> + #include <include/isis-redistribute-ipv4.xml.i> + </children> + </node> + <node name="connected"> + <properties> + <help>Redistribute connected routes into ISIS</help> + </properties> + <children> + #include <include/isis-redistribute-ipv4.xml.i> + </children> + </node> + <node name="kernel"> + <properties> + <help>Redistribute kernel routes into ISIS</help> + </properties> + <children> + #include <include/isis-redistribute-ipv4.xml.i> + </children> + </node> + <node name="ospf"> + <properties> + <help>Redistribute OSPF routes into ISIS</help> + </properties> + <children> + #include <include/isis-redistribute-ipv4.xml.i> + </children> + </node> + <node name="rip"> + <properties> + <help>Redistribute RIP routes into ISIS</help> + </properties> + <children> + #include <include/isis-redistribute-ipv4.xml.i> + </children> + </node> + <node name="static"> + <properties> + <help>Redistribute static routes into ISIS</help> + </properties> + <children> + #include <include/isis-redistribute-ipv4.xml.i> + </children> + </node> + </children> + </node> + </children> + </node> + <leafNode name="set-attached-bit"> + <properties> + <help>Set attached bit to identify as L1/L2 router for inter-area traffic</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="set-overload-bit"> + <properties> + <help>Set overload bit to avoid any transit traffic</help> + <valueless/> + </properties> + </leafNode> + <node name="spf-delay-ietf"> + <properties> + <help>IETF SPF delay algorithm</help> + </properties> + <children> + <leafNode name="init-delay"> + <properties> + <help>Delay used while in QUIET state</help> + <valueHelp> + <format><0-60000></format> + <description>Delay used while in QUIET state (in ms)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-60000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="short-delay"> + <properties> + <help>Delay used while in SHORT_WAIT state</help> + <valueHelp> + <format><0-60000></format> + <description>Delay used while in SHORT_WAIT state (in ms)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-60000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="long-delay"> + <properties> + <help>Delay used while in LONG_WAIT</help> + <valueHelp> + <format><0-60000></format> + <description>Delay used while in LONG_WAIT state (in ms)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-60000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="holddown"> + <properties> + <help>Time with no received IGP events before considering IGP stable</help> + <valueHelp> + <format><0-60000></format> + <description>Time with no received IGP events before considering IGP stable (in ms)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-60000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="time-to-learn"> + <properties> + <help>Maximum duration needed to learn all the events related to a single failure</help> + <valueHelp> + <format><0-60000></format> + <description>Maximum duration needed to learn all the events related to a single failure (in ms)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-60000"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="spf-interval"> + <properties> + <help>Minimum interval between SPF calculations</help> + <valueHelp> + <format><1-120></format> + <description>Minimum interval between consecutive SPFs in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-120"/> + </constraint> + </properties> + </leafNode> + <tagNode name="interface"> + <!-- (config-if)# ip router isis WORD (same as name of IS-IS process) + if any section of "interface" pesent --> + <properties> + <help>Interface params</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="bfd"> + <properties> + <help>Enable BFD support</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="circuit-type"> + <properties> + <help>Configure circuit type for interface</help> + <completionHelp> + <list>level-1 level-1-2 level-2-only</list> + </completionHelp> + <valueHelp> + <format>level-1</format> + <description>Level-1 only adjacencies are formed</description> + </valueHelp> + <valueHelp> + <format>level-1-2</format> + <description>Level-1-2 adjacencies are formed</description> + </valueHelp> + <valueHelp> + <format>level-2-only</format> + <description>Level-2 only adjacencies are formed</description> + </valueHelp> + <constraint> + <regex>(level-1|level-1-2|level-2-only)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="hello-padding"> + <properties> + <help>Add padding to IS-IS hello packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="hello-interval"> + <properties> + <help>Set Hello interval</help> + <valueHelp> + <format><1-600></format> + <description>Set Hello interval</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-600"/> + </constraint> + </properties> + </leafNode> + <leafNode name="hello-multiplier"> + <properties> + <help>Set Hello interval</help> + <valueHelp> + <format><2-100></format> + <description>Set multiplier for Hello holding time</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 2-100"/> + </constraint> + </properties> + </leafNode> + <leafNode name="metric"> + <properties> + <help>Set default metric for circuit</help> + <valueHelp> + <format><0-16777215></format> + <description>Default metric value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + <node name="network"> + <properties> + <help>Set network type</help> + </properties> + <children> + <leafNode name="point-to-point"> + <properties> + <help>point-to-point network type</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="passive"> + <properties> + <help>Configure the passive mode for interface</help> + <valueless/> + </properties> + </leafNode> + <node name="password"> + <properties> + <help>Configure the authentication password for a circuit</help> + </properties> + <children> + <leafNode name="plaintext-password"> + <properties> + <help>Plain-text authentication type</help> + <valueHelp> + <format><text></format> + <description>Circuit password</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="priority"> + <properties> + <help>Set priority for Designated Router election</help> + <valueHelp> + <format><0-127></format> + <description>Priority value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-127"/> + </constraint> + </properties> + </leafNode> + <leafNode name="psnp-interval"> + <properties> + <help>Set PSNP interval in seconds</help> + <valueHelp> + <format><0-127></format> + <description>Priority value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-127"/> + </constraint> + </properties> + </leafNode> + <leafNode name="three-way-handshake"> + <properties> + <help>Enable/Disable three-way handshake</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in new file mode 100644 index 000000000..3e9edbf72 --- /dev/null +++ b/interface-definitions/protocols-mpls.xml.in @@ -0,0 +1,122 @@ +<?xml version="1.0"?> +<!-- Multiprotocol Label Switching (MPLS) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="mpls" owner="${vyos_conf_scripts_dir}/protocols_mpls.py"> + <properties> + <help>Multiprotocol Label Switching (MPLS)</help> + <priority>299</priority> + </properties> + <children> + <node name="ldp"> + <properties> + <help>LDP options</help> + </properties> + <children> + <leafNode name="router-id"> + <properties> + <help>x.x.x.x Label Switch Router (LSR) id</help> + <valueHelp> + <format>ipv4</format> + <description>LSR ipv4 id</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <tagNode name="neighbor"> + <properties> + <help>LDP Id of neighbor</help> + <valueHelp> + <format>ipv4</format> + <description>neighbor IPv4 id</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="password"> + <properties> + <help>Peer password</help> + </properties> + </leafNode> + </children> + </tagNode> + <node name="discovery"> + <properties> + <help>Discovery parameters</help> + <valueHelp> + <format>ipv4</format> + <description>Discovery parameters</description> + </valueHelp> + </properties> + <children> + <leafNode name="hello-holdtime"> + <properties> + <help>Hello holdtime</help> + <valueHelp> + <format>1-65535</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="hello-interval"> + <properties> + <help>Hello interval</help> + <valueHelp> + <format>1-65535</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="transport-ipv4-address"> + <properties> + <help>Transport ipv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 bind as transport</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="transport-ipv6-address"> + <properties> + <help>Transport ipv6 address</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 bind as transport</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="interface"> + <properties> + <help>Listen interface for LDP</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-multicast.xml.in b/interface-definitions/protocols-multicast.xml.in new file mode 100644 index 000000000..a06f2b287 --- /dev/null +++ b/interface-definitions/protocols-multicast.xml.in @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<!-- Multicast static routing configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="static"> + <children> + <node name="multicast" owner="${vyos_conf_scripts_dir}/protocols_static_multicast.py"> + <properties> + <help>Multicast static route</help> + </properties> + <children> + <tagNode name="route"> + <properties> + <help>Configure static unicast route into MRIB for multicast RPF lookup</help> + <valueHelp> + <format>ipv4net</format> + <description>Network</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + <children> + <tagNode name="next-hop"> + <properties> + <help>Nexthop IPv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>Nexthop IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Distance value for this route</help> + <valueHelp> + <format>1-255</format> + <description>Distance for this route</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="interface-route"> + <properties> + <help>Multicast interface based route</help> + <valueHelp> + <format>ipv4net</format> + <description>Network</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + <children> + <tagNode name="next-hop-interface"> + <properties> + <help>Next-hop interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Distance value for this route</help> + <valueHelp> + <format>1-255</format> + <description>Distance for this route</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-pim.xml.in b/interface-definitions/protocols-pim.xml.in new file mode 100644 index 000000000..6152045a7 --- /dev/null +++ b/interface-definitions/protocols-pim.xml.in @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<!-- Protocol Independent Multicast (PIM) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="pim" owner="${vyos_conf_scripts_dir}/protocols_pim.py"> + <properties> + <help>Protocol Independent Multicast (PIM)</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>PIM interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="dr-priority"> + <properties> + <help>Designated Router Election Priority</help> + <valueHelp> + <format>1-4294967295</format> + <description>Value of the new DR Priority</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="hello"> + <properties> + <help>Hello Interval</help> + <valueHelp> + <format>1-180</format> + <description>Hello Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-180"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <node name="rp"> + <properties> + <help>Rendezvous Point</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Rendezvous Point address</help> + <valueHelp> + <format>ipv4</format> + <description>Rendezvous Point address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="group"> + <properties> + <help>Group Address range</help> + <valueHelp> + <format>ipv4net</format> + <description>Group Address range RFC 3171</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="keep-alive-timer"> + <properties> + <help>Keep alive Timer</help> + <valueHelp> + <format>31-60000</format> + <description>Keep alive Timer in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 31-60000"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-rip.xml.in b/interface-definitions/protocols-rip.xml.in new file mode 100644 index 000000000..107f0e0d5 --- /dev/null +++ b/interface-definitions/protocols-rip.xml.in @@ -0,0 +1,406 @@ +<!-- Routing Information Protocol (RIP) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="rip" owner="${vyos_conf_scripts_dir}/protocols_rip.py"> + <properties> + <help>Routing Information Protocol (RIP) parameters</help> + </properties> + <children> + <leafNode name="default-distance"> + <properties> + <help>Administrative distance</help> + <valueHelp> + <format><1-255></format> + <description>Administrative distance</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <node name="default-information"> + <properties> + <help>Control distribution of default route</help> + </properties> + <children> + <leafNode name="originate"> + <properties> + <help>Distribute a default route</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="default-metric"> + <properties> + <help>Metric of redistributed routes</help> + <valueHelp> + <format><1-16></format> + <description>Redistributed routes metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-16"/> + </constraint> + </properties> + </leafNode> + <node name="distribute-list"> + <properties> + <help>Filter networks in routing updates</help> + </properties> + <children> + <node name="access-list"> + <properties> + <help>Access-list</help> + </properties> + <children> + <leafNode name="in"> + <properties> + <help>Access list to apply to input packets</help> + <valueHelp> + <format><0-4294967295></format> + <description>Access list to apply to input packets</description> + </valueHelp> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="out"> + <properties> + <help>Access list to apply to output packets</help> + <valueHelp> + <format><0-4294967295></format> + <description>Access list to apply to output packets</description> + </valueHelp> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <tagNode name="interface"> + <properties> + <help>Apply filtering to an interface</help> + <valueHelp> + <format><text></format> + <description>Apply filtering to an interface</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <node name="access-list"> + <properties> + <help>Access list</help> + </properties> + <children> + <leafNode name="in"> + <properties> + <help>Access list to apply to input packets</help> + <valueHelp> + <format><0-4294967295></format> + <description>Access list to apply to input packets</description> + </valueHelp> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="out"> + <properties> + <help>Access list to apply to output packets</help> + <valueHelp> + <format><0-4294967295></format> + <description>Access list to apply to output packets</description> + </valueHelp> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="prefix-list"> + <properties> + <help>Prefix-list</help> + </properties> + <children> + <leafNode name="in"> + <properties> + <help>Prefix-list to apply to input packets</help> + <valueHelp> + <format><text></format> + <description>Prefix-list to apply to input packets</description> + </valueHelp> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="out"> + <properties> + <help>Prefix-list to apply to output packets</help> + <valueHelp> + <format><text></format> + <description>Prefix-list to apply to output packets</description> + </valueHelp> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + <node name="prefix-list"> + <properties> + <help>Prefix-list</help> + </properties> + <children> + <leafNode name="in"> + <properties> + <help>Prefix-list to apply to input packets</help> + <valueHelp> + <format><text></format> + <description>Prefix-list to apply to input packets</description> + </valueHelp> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="out"> + <properties> + <help>Prefix-list to apply to output packets</help> + <valueHelp> + <format><text></format> + <description>Prefix-list to apply to output packets</description> + </valueHelp> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="interface"> + <properties> + <help>Interface name</help> + <valueHelp> + <format><text></format> + <description>Apply filtering to an interface</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="neighbor"> + <properties> + <help>Neighbor router</help> + <valueHelp> + <format>ipv4</format> + <description>Neighbor router</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="network"> + <properties> + <help>RIP network</help> + <valueHelp> + <format>ipv4net</format> + <description>RIP network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="network-distance"> + <properties> + <help>Source network</help> + <valueHelp> + <format>ipv4net</format> + <description>Source network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="access-list"> + <properties> + <help>Access list</help> + <valueHelp> + <format><text></format> + <description>Access list</description> + </valueHelp> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="distance"> + <properties> + <help>Administrative distance for network</help> + <valueHelp> + <format><1-255></format> + <description>Administrative distance</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="passive-interface"> + <properties> + <help>Passive interface</help> + <valueHelp> + <format><text></format> + <description>Suppress routing updates on interface</description> + </valueHelp> + <valueHelp> + <format>default</format> + <description>Suppress routing updates on all interfaces by default</description> + </valueHelp> + <completionHelp> + <list>default</list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <node name="redistribute"> + <properties> + <help>Redistribute information from another routing protocol</help> + </properties> + <children> + <node name="bgp"> + <properties> + <help>Redistribute BGP routes</help> + </properties> + <children> + #include <include/rip-redistribute.xml.i> + </children> + </node> + <node name="connected"> + <properties> + <help>Redistribute connected routes</help> + </properties> + <children> + #include <include/rip-redistribute.xml.i> + </children> + </node> + <node name="kernel"> + <properties> + <help>Redistribute kernel routes</help> + </properties> + <children> + #include <include/rip-redistribute.xml.i> + </children> + </node> + <node name="ospf"> + <properties> + <help>Redistribute OSPF routes</help> + </properties> + <children> + #include <include/rip-redistribute.xml.i> + </children> + </node> + <node name="static"> + <properties> + <help>Redistribute static routes</help> + </properties> + <children> + #include <include/rip-redistribute.xml.i> + </children> + </node> + </children> + </node> + <leafNode name="route"> + <properties> + <help>RIP static route</help> + <valueHelp> + <format>ipv4net</format> + <description>RIP static route</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="timers"> + <properties> + <help>RIP timer values</help> + </properties> + <children> + <leafNode name="garbage-collection"> + <properties> + <help>Garbage collection timer</help> + <valueHelp> + <format><5-2147483647></format> + <description>Garbage colletion time (default 120)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 5-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="timeout"> + <properties> + <help>Routing information timeout timer</help> + <valueHelp> + <format><5-2147483647></format> + <description>Routing information timeout timer (default 180)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 5-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="update"> + <properties> + <help>Routing table update timer</help> + <valueHelp> + <format><5-2147483647></format> + <description>Routing table update timer in seconds (default 30)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 5-2147483647"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/salt-minion.xml.in b/interface-definitions/salt-minion.xml.in new file mode 100644 index 000000000..97f882a6a --- /dev/null +++ b/interface-definitions/salt-minion.xml.in @@ -0,0 +1,67 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="salt-minion" owner="${vyos_conf_scripts_dir}/salt-minion.py"> + <properties> + <help>Salt Minion</help> + <priority>500</priority> + </properties> + <children> + <leafNode name="hash"> + <properties> + <help>Hash used when discovering file on master server (default: sha256)</help> + <completionHelp> + <list>md5 sha1 sha224 sha256 sha384 sha512</list> + </completionHelp> + <constraint> + <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="master"> + <properties> + <help>The hostname or IP address of the master.</help> + <valueHelp> + <format>ipv4</format> + <description>Remote syslog server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Remote syslog server FQDN</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid FQDN or IP address</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="id"> + <properties> + <help>Explicitly declare ID for this minion to use (default: hostname)</help> + </properties> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Interval in minutes between updates (default: 60)</help> + <valueHelp> + <format><1-1440></format> + <description>Update interval in minutes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1440"/> + </constraint> + </properties> + </leafNode> + <leafNode name="master-key"> + <properties> + <help>URL with signature of master for auth reply verification</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service-ids-ddos-protection.xml.in b/interface-definitions/service-ids-ddos-protection.xml.in new file mode 100644 index 000000000..93d4cc682 --- /dev/null +++ b/interface-definitions/service-ids-ddos-protection.xml.in @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="ids"> + <properties> + <help>Intrusion Detection System</help> + </properties> + <children> + <node name="ddos-protection" owner="${vyos_conf_scripts_dir}/service_ids_fastnetmon.py"> + <properties> + <help>FastNetMon detection and protection parameters</help> + <priority>731</priority> + </properties> + <children> + <leafNode name="alert-script"> + <properties> + <help>Path to fastnetmon alert script</help> + </properties> + </leafNode> + <leafNode name="direction"> + <properties> + <help>Direction for processing traffic</help> + <completionHelp> + <list>in out</list> + </completionHelp> + <constraint> + <regex>(in|out)</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="listen-interface"> + <properties> + <help>Listen interface for mirroring traffic</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <node name="mode"> + <properties> + <help>Traffic capture modes</help> + </properties> + <children> + <!-- Future modes "mirror" "netflow" "combine (both)" --> + <leafNode name="mirror"> + <properties> + <help>Listen mirrored traffic mode</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="network"> + <properties> + <help>Define monitoring networks</help> + <valueHelp> + <format>ipv4net</format> + <description>Processed network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="threshold"> + <properties> + <help>Attack limits thresholds</help> + </properties> + <children> + <leafNode name="fps"> + <properties> + <help>Flows per second</help> + <valueHelp> + <format><0-4294967294></format> + <description>Flows per second</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967294"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mbps"> + <properties> + <help>Megabits per second</help> + <valueHelp> + <format><0-4294967294></format> + <description>Megabits per second</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967294"/> + </constraint> + </properties> + </leafNode> + <leafNode name="pps"> + <properties> + <help>Packets per second</help> + <valueHelp> + <format><0-4294967294></format> + <description>Packets per second</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967294"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_console-server.xml.in b/interface-definitions/service_console-server.xml.in new file mode 100644 index 000000000..59a9fe237 --- /dev/null +++ b/interface-definitions/service_console-server.xml.in @@ -0,0 +1,93 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="console-server" owner="${vyos_conf_scripts_dir}/service_console-server.py"> + <properties> + <help>Serial Console Server</help> + <priority>990</priority> + </properties> + <children> + <tagNode name="device"> + <properties> + <help>System serial interface name (ttyS or ttyUSB)</help> + <completionHelp> + <script>ls -1 /dev | grep ttyS</script> + <script>ls -1 /dev/serial/by-bus</script> + </completionHelp> + <valueHelp> + <format>ttySxxx</format> + <description>Regular serial interface</description> + </valueHelp> + <valueHelp> + <format>usbxbxpx</format> + <description>USB based serial interface</description> + </valueHelp> + <constraint> + <regex>^(ttyS\d+|usb\d+b.*p.*)$</regex> + </constraint> + </properties> + <children> + #include <include/interface-description.xml.i> + <leafNode name="speed"> + <properties> + <help>Serial port baud rate</help> + <completionHelp> + <list>300 1200 2400 4800 9600 19200 38400 57600 115200</list> + </completionHelp> + <constraint> + <regex>(300|1200|2400|4800|9600|19200|38400|57600|115200)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="data-bits"> + <properties> + <help>Serial port data bits (default: 8)</help> + <completionHelp> + <list>7 8</list> + </completionHelp> + <constraint> + <regex>(7|8)</regex> + </constraint> + </properties> + <defaultValue>8</defaultValue> + </leafNode> + <leafNode name="stop-bits"> + <properties> + <help>Serial port stop bits (default: 1)</help> + <completionHelp> + <list>1 2</list> + </completionHelp> + <constraint> + <regex>(1|2)</regex> + </constraint> + </properties> + <defaultValue>1</defaultValue> + </leafNode> + <leafNode name="parity"> + <properties> + <help>Parity setting (default: none)</help> + <completionHelp> + <list>even odd none</list> + </completionHelp> + <constraint> + <regex>(even|odd|none)</regex> + </constraint> + </properties> + <defaultValue>none</defaultValue> + </leafNode> + <node name="ssh"> + <properties> + <help>SSH remote access to this console</help> + </properties> + <children> + #include <include/port-number.xml.i> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in new file mode 100644 index 000000000..9ee5d5156 --- /dev/null +++ b/interface-definitions/service_ipoe-server.xml.in @@ -0,0 +1,208 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="ipoe-server" owner="${vyos_conf_scripts_dir}/service_ipoe-server.py"> + <properties> + <help>Internet Protocol over Ethernet (IPoE) Server</help> + <priority>900</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Network interface to server IPoE</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="network-mode"> + <properties> + <help>Network Layer IPoE serves on</help> + <completionHelp> + <list>L2 L3</list> + </completionHelp> + <constraint> + <regex>(L2|L3)</regex> + </constraint> + <valueHelp> + <format>L2</format> + <description>client share the same subnet</description> + </valueHelp> + <valueHelp> + <format>L3</format> + <description>clients are behind this router</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="network"> + <properties> + <help>Enables clients to share the same network or each client has its own vlan</help> + <completionHelp> + <list>shared vlan</list> + </completionHelp> + <constraint> + <regex>(shared|vlan)</regex> + </constraint> + <valueHelp> + <format>shared</format> + <description>Multiple clients share the same network</description> + </valueHelp> + <valueHelp> + <format>vlan</format> + <description>One VLAN per client</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="client-subnet"> + <properties> + <help>Client address pool</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + <node name="external-dhcp"> + <properties> + <help>DHCP requests will be forwarded</help> + </properties> + <children> + <leafNode name="dhcp-relay"> + <properties> + <help>DHCP Server the request will be redirected to.</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of the DHCP Server</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="giaddr"> + <properties> + <help>address of the relay agent (Relay Agent IP Address)</help> + </properties> + </leafNode> + </children> + </node> + <leafNode name="vlan-id"> + <properties> + <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> + <constraint> + <validator name="numeric" argument="--range 1-4096"/> + </constraint> + <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="vlan-range"> + <properties> + <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> + <constraint> + <regex>(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})</regex> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + #include <include/accel-name-server.xml.in> + #include <include/accel-client-ipv6-pool.xml.in> + <node name="authentication"> + <properties> + <help>Client authentication methods</help> + </properties> + <children> + <leafNode name="mode"> + <properties> + <help>Authetication mode</help> + <completionHelp> + <list>local radius noauth</list> + </completionHelp> + <constraint> + <regex>(local|radius|noauth)</regex> + </constraint> + <valueHelp> + <format>local</format> + <description>Authentication based on local definition</description> + </valueHelp> + <valueHelp> + <format>radius</format> + <description>Authentication based on a RADIUS server</description> + </valueHelp> + <valueHelp> + <format>noauth</format> + <description>Authentication disabled</description> + </valueHelp> + </properties> + </leafNode> + <tagNode name="interface"> + <properties> + <help>Network interface the client mac will appear on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="mac-address"> + <properties> + <help>Client mac address allowed to receive an IP address</help> + <valueHelp> + <format>h:h:h:h:h:h</format> + <description>Hardware (MAC) address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + </constraint> + </properties> + <children> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="upload"> + <properties> + <help>Upload bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="download"> + <properties> + <help>Download bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="vlan-id"> + <properties> + <help>VLAN-ID of the client network</help> + <constraint> + <validator name="numeric" argument="--range 1-4096"/> + </constraint> + <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_mdns-repeater.xml.in b/interface-definitions/service_mdns-repeater.xml.in new file mode 100644 index 000000000..e21b1b27c --- /dev/null +++ b/interface-definitions/service_mdns-repeater.xml.in @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="mdns"> + <properties> + <help>Multicast DNS (mDNS) parameters</help> + </properties> + <children> + <node name="repeater" owner="${vyos_conf_scripts_dir}/service_mdns-repeater.py"> + <properties> + <help>mDNS repeater configuration</help> + <priority>990</priority> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Disable mDNS repeater service</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="interface"> + <properties> + <help>Interface to repeat mDNS advertisements [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in new file mode 100644 index 000000000..605f47b37 --- /dev/null +++ b/interface-definitions/service_pppoe-server.xml.in @@ -0,0 +1,491 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="pppoe-server" owner="${vyos_conf_scripts_dir}/service_pppoe-server.py"> + <properties> + <help>Point to Point over Ethernet (PPPoE) Server</help> + <priority>900</priority> + </properties> + <children> + <node name="snmp"> + <properties> + <help>Enable SNMP</help> + </properties> + <children> + <leafNode name="master-agent"> + <properties> + <help>enable SNMP master agent mode</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + <leafNode name="access-concentrator"> + <properties> + <help>Access concentrator name</help> + <constraint> + <regex>[a-zA-Z0-9]{1,100}</regex> + </constraint> + <constraintErrorMessage>access-concentrator name limited to alphanumerical characters only (max. 100)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="session-control"> + <properties> + <help>control sessions count</help> + <constraint> + <regex>(deny|disable)</regex> + </constraint> + <constraintErrorMessage>Invalid value</constraintErrorMessage> + <valueHelp> + <format>disable</format> + <description>Disables session control</description> + </valueHelp> + <valueHelp> + <format>deny</format> + <description>Deny second session authorization</description> + </valueHelp> + <completionHelp> + <list>deny disable</list> + </completionHelp> + </properties> + </leafNode> + <node name="authentication"> + <properties> + <help>Authentication for remote access PPPoE Server</help> + </properties> + <children> + <node name="local-users"> + <properties> + <help>Local user authentication for PPPoE server</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a PPPoE Server user</help> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + <leafNode name="static-ip"> + <properties> + <help>Static client IP address</help> + </properties> + </leafNode> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="upload"> + <properties> + <help>Upload bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="download"> + <properties> + <help>Download bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + #include <include/accel-auth-mode.xml.i> + #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> + <node name="radius"> + <children> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="attribute"> + <properties> + <help>Specifies which radius attribute contains rate information. (default is Filter-Id)</help> + </properties> + </leafNode> + <leafNode name="vendor"> + <properties> + <help>Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius)</help> + </properties> + </leafNode> + <leafNode name="enable"> + <properties> + <help>Enables Bandwidth shaping via RADIUS</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="protocols"> + <properties> + <help>Authentication protocol</help> + <valueHelp> + <format>pap</format> + <description>Allow PAP authentication [Password Authentication Protocol]</description> + </valueHelp> + <valueHelp> + <format>chap</format> + <description>Allow CHAP authentication [Challenge Handshake Authentication Protocol]</description> + </valueHelp> + <valueHelp> + <format>mschap</format> + <description>Allow MS-CHAP authentication [Microsoft Challenge Handshake Authentication Protocol, Version 1]</description> + </valueHelp> + <valueHelp> + <format>mschap-v2</format> + <description>Allow MS-CHAPv2 authentication [Microsoft Challenge Handshake Authentication Protocol, Version 2]</description> + </valueHelp> + <constraint> + <regex>(pap|chap|mschap|mschap-v2)</regex> + </constraint> + <completionHelp> + <list>pap chap mschap mschap-v2</list> + </completionHelp> + <multi /> + </properties> + </leafNode> + </children> + </node> + <node name="client-ip-pool"> + <properties> + <help>Pool of client IP addresses (must be within a /24)</help> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="subnet"> + <properties> + <help>Client IP subnet (CIDR notation)</help> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage> + <multi /> + </properties> + </leafNode> + </children> + </node> + #include <include/accel-client-ipv6-pool.xml.in> + #include <include/accel-name-server.xml.in> + <tagNode name="interface"> + <properties> + <help>interface(s) to listen on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="vlan-id"> + <properties> + <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> + <constraint> + <validator name="numeric" argument="--range 1-4096"/> + </constraint> + <constraintErrorMessage>VLAN ID needs to be between 1 and 4096</constraintErrorMessage> + <multi /> + </properties> + </leafNode> + <leafNode name="vlan-range"> + <properties> + <help>VLAN monitor for the automatic creation of vlans (user per vlan)</help> + <constraint> + <regex>(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})-(409[0-6]|40[0-8][0-9]|[1-3][0-9]{3}|[1-9][0-9]{0,2})</regex> + </constraint> + <multi /> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="local-ip"> + <properties> + <help>local gateway address</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU) - default 1492</help> + <constraint> + <validator name="numeric" argument="--range 128-16384"/> + </constraint> + </properties> + </leafNode> + <node name="limits"> + <properties> + <help>Limits the connection rate from a single source</help> + </properties> + <children> + <leafNode name="connection-limit"> + <properties> + <help>Acceptable rate of connections (e.g. 1/min, 60/sec)</help> + <constraint> + <regex>[0-9]+\/(min|sec)$</regex> + </constraint> + <constraintErrorMessage>illegal value</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="burst"> + <properties> + <help>Burst count</help> + </properties> + </leafNode> + <leafNode name="timeout"> + <properties> + <help>Timeout in seconds</help> + </properties> + </leafNode> + </children> + </node> + <leafNode name="service-name"> + <properties> + <help>Service name</help> + <constraint> + <regex>[a-zA-Z0-9\-]{1,100}</regex> + </constraint> + <constraintErrorMessage>servicename can contain aplhanumerical characters and dashes only (max. 100)</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + #include <include/accel-wins-server.xml.i> + <node name="ppp-options"> + <properties> + <help>Advanced protocol options</help> + </properties> + <children> + <leafNode name="min-mtu"> + <properties> + <help>Minimum acceptable MTU (68-65535)</help> + <constraint> + <validator name="numeric" argument="--range 68-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mru"> + <properties> + <help>Preferred MRU (68-65535)</help> + <constraint> + <validator name="numeric" argument="--range 68-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ccp"> + <properties> + <help>CCP negotiation (default disabled)</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="mppe"> + <properties> + <help>Specifies MPPE negotiation preference. (default prefer mppe)</help> + <completionHelp> + <list>deny prefer require</list> + </completionHelp> + <valueHelp> + <format>deny</format> + <description>Deny MPPE</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>Ask client for MPPE - do not fail on reject</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Ask client for MPPE - drop connection on reject</description> + </valueHelp> + <constraint> + <regex>^(deny|prefer|require)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-interval"> + <properties> + <help>LCP echo-requests/sec</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-failure"> + <properties> + <help>Maximum number of Echo-Requests may be sent without valid reply</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-timeout"> + <properties> + <help>Timeout in seconds to wait for any peer activity. If this option specified it turns on adaptive lcp echo functionality and "lcp-echo-failure" is not used.</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ipv4"> + <properties> + <help>IPv4 (IPCP) negotiation algorithm</help> + <constraint> + <regex>(deny|allow|prefer|require)</regex> + </constraint> + <constraintErrorMessage>invalid value</constraintErrorMessage> + <valueHelp> + <format>deny</format> + <description>Do not negotiate IPv4</description> + </valueHelp> + <valueHelp> + <format>allow</format> + <description>Negotiate IPv4 only if client requests</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>Ask client for IPv4 negotiation, do not fail if it rejects</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Require IPv4 negotiation</description> + </valueHelp> + <completionHelp> + <list>deny allow prefer require</list> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6"> + <properties> + <help>IPv6 (IPCP6) negotiation algorithm</help> + <constraint> + <regex>(deny|allow|prefer|require)</regex> + </constraint> + <constraintErrorMessage>invalid value</constraintErrorMessage> + <valueHelp> + <format>deny</format> + <description>Do not negotiate IPv6</description> + </valueHelp> + <valueHelp> + <format>allow</format> + <description>Negotiate IPv6 only if client requests</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>Ask client for IPv6 negotiation, do not fail if it rejects</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>Require IPv6 negotiation</description> + </valueHelp> + <completionHelp> + <list>deny allow prefer require</list> + </completionHelp> + </properties> + </leafNode> + <leafNode name="ipv6-intf-id"> + <properties> + <help>Fixed or random interface identifier for IPv6</help> + <valueHelp> + <format>random</format> + <description>Random interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>x:x:x:x</format> + <description>specify interface identifier for IPv6</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="ipv6-peer-intf-id"> + <properties> + <help>Peer interface identifier for IPv6</help> + <valueHelp> + <format>x:x:x:x</format> + <description>Interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>random</format> + <description>Use a random interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>Calculate interface identifier from IPv4 address, for example 192:168:0:1</description> + </valueHelp> + <valueHelp> + <format>calling-sid</format> + <description>Calculate interface identifier from calling-station-id</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="ipv6-accept-peer-intf-id"> + <properties> + <help>Accept peer interface identifier</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + <tagNode name="pado-delay"> + <properties> + <help>PADO delays</help> + <valueHelp> + <format>1-999999</format> + <description>Number in ms</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Invalid PADO delay</constraintErrorMessage> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Number of sessions</help> + <valueHelp> + <format>1-999999</format> + <description>Number of sessions</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Invalid number of delayed sessions</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in new file mode 100644 index 000000000..5a472fc9a --- /dev/null +++ b/interface-definitions/service_router-advert.xml.in @@ -0,0 +1,273 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="router-advert" owner="${vyos_conf_scripts_dir}/service_router-advert.py"> + <properties> + <help>IPv6 Router Advertisements (RAs) service</help> + <priority>900</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Interface to send DDNS updates for [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="hop-limit"> + <properties> + <help>Set Hop Count field of the IP header for outgoing packets (default: 64)</help> + <valueHelp> + <format>1-255</format> + <description>Value should represent current diameter of the Internet</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Unspecified (by this router)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Hop count must be between 0 and 255</constraintErrorMessage> + </properties> + <defaultValue>64</defaultValue> + </leafNode> + <leafNode name="default-lifetime"> + <properties> + <help>Lifetime associated with the default router in units of seconds</help> + <valueHelp> + <format>4-9000</format> + <description>Router Lifetime in seconds</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Not a default router</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 4-9000"/> + </constraint> + <constraintErrorMessage>Default router livetime bust be 0 or between 4 and 9000</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-preference"> + <properties> + <help>Preference associated with the default router,</help> + <completionHelp> + <list>low medium high</list> + </completionHelp> + <valueHelp> + <format>low</format> + <description>Default router has low preference</description> + </valueHelp> + <valueHelp> + <format>medium</format> + <description>Default router has medium preference (default)</description> + </valueHelp> + <valueHelp> + <format>high</format> + <description>Default router has high preference</description> + </valueHelp> + <constraint> + <regex>^(low|medium|high)$</regex> + </constraint> + <constraintErrorMessage>Default preference must be low, medium or high</constraintErrorMessage> + </properties> + <defaultValue>medium</defaultValue> + </leafNode> + <leafNode name="dnssl"> + <properties> + <help>DNS search list</help> + <multi/> + </properties> + </leafNode> + <leafNode name="link-mtu"> + <properties> + <help>Link MTU value placed in RAs, exluded in RAs if unset</help> + <valueHelp> + <format>1280-9000</format> + <description>Link MTU value in RAs</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1280-9000"/> + </constraint> + <constraintErrorMessage>Link MTU must be between 1280 and 9000</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="managed-flag"> + <properties> + <help>Hosts use the administered (stateful) protocol for address autoconfiguration in addition to any addresses autoconfigured using SLAAC</help> + <valueless/> + </properties> + </leafNode> + <node name="interval"> + <properties> + <help>Set interval between unsolicited multicast RAs</help> + </properties> + <children> + <leafNode name="max"> + <properties> + <help>Maximum interval between unsolicited multicast RAs (default: 600)</help> + <valueHelp> + <format>4-1800</format> + <description>Maximum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 4-1800"/> + </constraint> + <constraintErrorMessage>Maximum interval must be between 4 and 1800 seconds</constraintErrorMessage> + </properties> + <defaultValue>600</defaultValue> + </leafNode> + <leafNode name="min"> + <properties> + <help>Minimum interval between unsolicited multicast RAs</help> + <valueHelp> + <format>3-1350</format> + <description>Minimum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 3-1350"/> + </constraint> + <constraintErrorMessage>Minimum interval must be between 3 and 1350 seconds</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="name-server"> + <properties> + <help>IPv6 address of recursive DNS server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of DNS name server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="other-config-flag"> + <properties> + <help>Hosts use the administered (stateful) protocol for autoconfiguration of other (non-address) information</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="prefix"> + <properties> + <help>IPv6 prefix to be advertised in Router Advertisements (RAs)</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix to be advertized</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="no-autonomous-flag"> + <properties> + <help>Prefix can not be used for stateless address auto-configuration</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="no-on-link-flag"> + <properties> + <help>Prefix can not be used for on-link determination</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="preferred-lifetime"> + <properties> + <help>Time in seconds that the prefix will remain preferred (default 4 hours)</help> + <completionHelp> + <list>infinity</list> + </completionHelp> + <valueHelp> + <format>0-4294967295</format> + <description>Time in seconds that the prefix will remain preferred</description> + </valueHelp> + <valueHelp> + <format>infinity</format> + <description>Prefix will remain preferred forever</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + <regex>^(infinity)$</regex> + </constraint> + </properties> + <defaultValue>14400</defaultValue> + </leafNode> + <leafNode name="valid-lifetime"> + <properties> + <help>Time in seconds that the prefix will remain valid (default: 30 days)</help> + <completionHelp> + <list>infinity</list> + </completionHelp> + <valueHelp> + <format>1-4294967295</format> + <description>Time in seconds that the prefix will remain valid</description> + </valueHelp> + <valueHelp> + <format>infinity</format> + <description>Prefix will remain preferred forever</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + <regex>(infinity)</regex> + </constraint> + </properties> + <defaultValue>2592000</defaultValue> + </leafNode> + </children> + </tagNode> + <leafNode name="reachable-time"> + <properties> + <help>Time, in milliseconds, that a node assumes a neighbor is reachable after having received a reachability confirmation</help> + <valueHelp> + <format>1-3600000</format> + <description>Reachable Time value in RAs (in milliseconds)</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Reachable Time unspecified by this router</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 1-3600000"/> + </constraint> + <constraintErrorMessage>Reachable time must be 0 or between 1 and 3600000 milliseconds</constraintErrorMessage> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="retrans-timer"> + <properties> + <help>Time in milliseconds between retransmitted Neighbor Solicitation messages</help> + <valueHelp> + <format>1-4294967295</format> + <description>Minimum interval in milliseconds</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Time, in milliseconds, between retransmitted Neighbor Solicitation messages</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 1-4294967295"/> + </constraint> + <constraintErrorMessage>Retransmit interval must be 0 or between 1 and 4294967295 milliseconds</constraintErrorMessage> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="no-send-advert"> + <properties> + <help>Do not send router adverts</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in new file mode 100644 index 000000000..2fe8ce583 --- /dev/null +++ b/interface-definitions/snmp.xml.in @@ -0,0 +1,631 @@ +<?xml version="1.0"?> +<!-- SNMP forwarder configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="snmp" owner="${vyos_conf_scripts_dir}/snmp.py"> + <properties> + <help>Simple Network Management Protocol (SNMP)</help> + <priority>980</priority> + </properties> + <children> + <tagNode name="community"> + <properties> + <help>Community name</help> + <constraint> + <regex>^[a-zA-Z0-9\-_]{1,100}$</regex> + </constraint> + <constraintErrorMessage>Community string is limited to alphanumerical characters only with a total lenght of 100</constraintErrorMessage> + </properties> + <children> + <leafNode name="authorization"> + <properties> + <help>Authorization type (default: 'ro')</help> + <completionHelp> + <list>ro rw</list> + </completionHelp> + <valueHelp> + <format>ro</format> + <description>read only</description> + </valueHelp> + <valueHelp> + <format>rw</format> + <description>read write</description> + </valueHelp> + <constraint> + <regex>^(ro|rw)$</regex> + </constraint> + <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="client"> + <properties> + <help>IP address of SNMP client allowed to contact system</help> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="network"> + <properties> + <help>Subnet of SNMP client(s) allowed to contact system</help> + <valueHelp> + <format>ipv4net</format> + <description>IP address and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="contact"> + <properties> + <help>Contact information</help> + <constraint> + <regex>^.{1,255}$</regex> + </constraint> + <constraintErrorMessage>Contact information is limited to 255 characters or less</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="description"> + <properties> + <help>Description information</help> + <constraint> + <regex>^.{1,255}$</regex> + </constraint> + <constraintErrorMessage>Description is limited to 255 characters or less</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="listen-address"> + <properties> + <help>IP address to listen for incoming SNMP requests</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to listen for incoming SNMP requests</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to listen for incoming SNMP requests</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="port"> + <properties> + <help>Port for SNMP service (default: '161')</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="location"> + <properties> + <help>Location information</help> + <constraint> + <regex>^.{1,255}$</regex> + </constraint> + <constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="smux-peer"> + <properties> + <help>Register a subtree for SMUX-based processing</help> + <valueHelp> + <format>oid</format> + <description>Object Identifier</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="trap-source"> + <properties> + <help>SNMP trap source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <tagNode name="trap-target"> + <properties> + <help>Address of trap target</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <leafNode name="community"> + <properties> + <help>Community used when sending trap information</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Destination port used for trap notification</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + <node name="v3"> + <properties> + <help>Simple Network Management Protocol (SNMP) v3</help> + </properties> + <children> + <leafNode name="engineid"> + <properties> + <help>Specifies the EngineID that uniquely identify an agent (e.g. 000000000000000000000002)</help> + <constraint> + <regex>^([0-9a-f][0-9a-f]){1,18}$</regex> + </constraint> + <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="group"> + <properties> + <help>Specifies the group with name groupname</help> + </properties> + <children> + <leafNode name="mode"> + <properties> + <help>Define group access permission (default: 'ro')</help> + <completionHelp> + <list>ro rw</list> + </completionHelp> + <valueHelp> + <format>ro</format> + <description>read only</description> + </valueHelp> + <valueHelp> + <format>rw</format> + <description>read write</description> + </valueHelp> + <constraint> + <regex>^(ro|rw)$</regex> + </constraint> + <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="seclevel"> + <properties> + <help>Security levels</help> + <completionHelp> + <list>noauth auth priv</list> + </completionHelp> + <valueHelp> + <format>noauth</format> + <description>Messages not authenticated and not encrypted (noAuthNoPriv)</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Messages are authenticated but not encrypted (authNoPriv)</description> + </valueHelp> + <valueHelp> + <format>priv</format> + <description>Messages are authenticated and encrypted (authPriv)</description> + </valueHelp> + <constraint> + <regex>^(noauth|auth|priv)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="view"> + <properties> + <help>Defines the name of view</help> + <completionHelp> + <path>service snmp v3 view</path> + </completionHelp> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="trap-target"> + <properties> + <help>Defines SNMP target for inform or traps for IP</help> + <valueHelp> + <format>ipv4</format> + <description>IP address of trap target</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of trap target</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + <children> + <node name="auth"> + <properties> + <help>Defines the privacy</help> + </properties> + <children> + <leafNode name="encrypted-password"> + <properties> + <help>Defines the encrypted key for authentication</help> + <constraint> + <regex>^[0-9a-f]*$</regex> + </constraint> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="plaintext-password"> + <properties> + <help>Defines the clear text key for authentication</help> + <constraint> + <regex>^.{8,}$</regex> + </constraint> + <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Defines the protocol used for authentication (default: 'md5')</help> + <completionHelp> + <list>md5 sha</list> + </completionHelp> + <valueHelp> + <format>md5</format> + <description>Message Digest 5</description> + </valueHelp> + <valueHelp> + <format>sha</format> + <description>Secure Hash Algorithm</description> + </valueHelp> + <constraint> + <regex>^(md5|sha)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="port"> + <properties> + <help>Specifies TCP/UDP port of destination SNMP traps/informs (default: '162')</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage> + </properties> + </leafNode> + <node name="privacy"> + <properties> + <help>Defines the privacy</help> + </properties> + <children> + <leafNode name="encrypted-password"> + <properties> + <help>Defines the encrypted key for privacy protocol</help> + <constraint> + <regex>^[0-9a-f]*$</regex> + </constraint> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="plaintext-password"> + <properties> + <help>Defines the clear text key for privacy protocol</help> + <constraint> + <regex>^.{8,}$</regex> + </constraint> + <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Defines the protocol for privacy (default: 'des')</help> + <completionHelp> + <list>des aes</list> + </completionHelp> + <valueHelp> + <format>des</format> + <description>Data Encryption Standard</description> + </valueHelp> + <valueHelp> + <format>aes</format> + <description>Advanced Encryption Standard</description> + </valueHelp> + <constraint> + <regex>^(des|aes)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="protocol"> + <properties> + <help>Defines protocol for notification between TCP and UDP</help> + <completionHelp> + <list>tcp udp</list> + </completionHelp> + <valueHelp> + <format>tcp</format> + <description>Use Transmission Control Protocol for notifications</description> + </valueHelp> + <valueHelp> + <format>udp</format> + <description>Use User Datagram Protocol for notifications</description> + </valueHelp> + <constraint> + <regex>^(tcp|udp)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Specifies the type of notification between inform and trap (default: 'inform')</help> + <completionHelp> + <list>inform trap</list> + </completionHelp> + <valueHelp> + <format>inform</format> + <description>Use INFORM</description> + </valueHelp> + <valueHelp> + <format>trap</format> + <description>Use TRAP</description> + </valueHelp> + <constraint> + <regex>^(inform|trap)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="user"> + <properties> + <help>Defines username for authentication</help> + <completionHelp> + <path>service snmp v3 user</path> + </completionHelp> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="user"> + <properties> + <help>Specifies the user with name username</help> + <constraint> + <regex>[^\(\)\|\-]+$</regex> + </constraint> + <constraintErrorMessage>Illegal characters in name</constraintErrorMessage> + </properties> + <children> + <node name="auth"> + <properties> + <help>Specifies the auth</help> + </properties> + <children> + <leafNode name="encrypted-password"> + <properties> + <help>Defines the encrypted key for authentication</help> + <constraint> + <regex>^[0-9a-f]*$</regex> + </constraint> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="plaintext-password"> + <properties> + <help>Defines the clear text key for authentication</help> + <constraint> + <regex>^.{8,}$</regex> + </constraint> + <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Defines the protocol used for authentication (default: 'md5')</help> + <completionHelp> + <list>md5 sha</list> + </completionHelp> + <valueHelp> + <format>md5</format> + <description>Message Digest 5</description> + </valueHelp> + <valueHelp> + <format>sha</format> + <description>Secure Hash Algorithm</description> + </valueHelp> + <constraint> + <regex>^(md5|sha)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="group"> + <properties> + <help>Specifies group for user name</help> + <completionHelp> + <path>service snmp v3 group</path> + </completionHelp> + </properties> + </leafNode> + <leafNode name="mode"> + <properties> + <help>Define users access permission (default: 'ro')</help> + <completionHelp> + <list>ro rw</list> + </completionHelp> + <valueHelp> + <format>ro</format> + <description>read only</description> + </valueHelp> + <valueHelp> + <format>rw</format> + <description>read write</description> + </valueHelp> + <constraint> + <regex>^(ro|rw)$</regex> + </constraint> + <constraintErrorMessage>Authorization type must be either 'rw' or 'ro'</constraintErrorMessage> + </properties> + </leafNode> + <node name="privacy"> + <properties> + <help>Defines the privacy</help> + </properties> + <children> + <leafNode name="encrypted-password"> + <properties> + <help>Defines the encrypted key for privacy protocol</help> + <constraint> + <regex>^[0-9a-f]*$</regex> + </constraint> + <constraintErrorMessage>Encrypted key must only contain hex digits</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="plaintext-password"> + <properties> + <help>Defines the clear text key for privacy protocol</help> + <constraint> + <regex>^.{8,}$</regex> + </constraint> + <constraintErrorMessage>Key must contain 8 or more characters</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Defines the protocol for privacy (default: 'des')</help> + <completionHelp> + <list>des aes</list> + </completionHelp> + <valueHelp> + <format>des</format> + <description>Data Encryption Standard</description> + </valueHelp> + <valueHelp> + <format>aes</format> + <description>Advanced Encryption Standard</description> + </valueHelp> + <constraint> + <regex>^(des|aes)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + <tagNode name="view"> + <properties> + <help>Specifies the view with name viewname</help> + <constraint> + <regex>[^\(\)\|\-]+$</regex> + </constraint> + <constraintErrorMessage>Illegal characters in name</constraintErrorMessage> + </properties> + <children> + <tagNode name="oid"> + <properties> + <help>Specifies the oid</help> + <constraint> + <regex>^[0-9]+(\.[0-9]+)*$</regex> + </constraint> + <constraintErrorMessage>OID must start from a number</constraintErrorMessage> + </properties> + <children> + <leafNode name="exclude"> + <properties> + <help>Exclude is an optional argument</help> + </properties> + </leafNode> + <leafNode name="mask"> + <properties> + <help>Defines a bit-mask that is indicating which subidentifiers of the associated subtree OID should be regarded as significant</help> + <constraint> + <regex>^[0-9a-f]{2}([\.:][0-9a-f]{2})*$</regex> + </constraint> + <constraintErrorMessage>MASK is a list of hex octets, separated by '.' or ':'</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + <node name="script-extensions"> + <properties> + <help>SNMP script extensions</help> + </properties> + <children> + <tagNode name="extension-name"> + <properties> + <help>Extension name</help> + <constraint> + <regex>^[a-z0-9\.\-\_]+</regex> + </constraint> + <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> + </properties> + <children> + <leafNode name="script"> + <properties> + <help>Script location and name</help> + <completionHelp> + <script>ls /config/user-data</script> + </completionHelp> + <constraint> + <regex>^[a-z0-9\.\-\_\/]+</regex> + </constraint> + <constraintErrorMessage>Script extension contains invalid characters</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + #include <include/interface-vrf.xml.i> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in new file mode 100644 index 000000000..d253c2f34 --- /dev/null +++ b/interface-definitions/ssh.xml.in @@ -0,0 +1,207 @@ +<?xml version="1.0"?> +<!--SSH configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="ssh" owner="${vyos_conf_scripts_dir}/ssh.py"> + <properties> + <help>Secure Shell (SSH)</help> + <priority>500</priority> + </properties> + <children> + <node name="access-control"> + <properties> + <help>SSH user/group access controls. Directives are processed + in the following order: deny-users, allow-users, deny-groups and + allow-groups.</help> + </properties> + <children> + <node name="allow"> + <properties> + <help>Allow user/group SSH access</help> + </properties> + <children> + <leafNode name="group"> + <properties> + <help>Allow members of a group to login</help> + <constraint> + <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="user"> + <properties> + <help>Allow specific users to login</help> + <constraint> + <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + </children> + </node> + <node name="deny"> + <properties> + <help>Deny user/group SSH access</help> + </properties> + <children> + <leafNode name="group"> + <properties> + <help>Disallow members of a group to login</help> + <constraint> + <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="user"> + <properties> + <help>Disallow specific users to login</help> + <constraint> + <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="ciphers"> + <properties> + <help>Allowed ciphers</help> + <completionHelp> + <!-- generated by ssh -Q cipher | tr '\n' ' ' as this will not change dynamically --> + <list>3des-cbc aes128-cbc aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com</list> + </completionHelp> + <constraint> + <regex>^(3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com)$</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="disable-host-validation"> + <properties> + <help>Disable IP Address to Hostname lookup</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable-password-authentication"> + <properties> + <help>Disable password-based authentication</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="key-exchange"> + <properties> + <help>Allowed key exchange (KEX) algorithms</help> + <completionHelp> + <!-- generated by ssh -Q kex | tr '\n' ' ' as this will not change dynamically --> + <list>diffie-hellman-group1-sha1 diffie-hellman-group14-sha1 diffie-hellman-group14-sha256 diffie-hellman-group16-sha512 diffie-hellman-group18-sha512 diffie-hellman-group-exchange-sha1 diffie-hellman-group-exchange-sha256 ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 curve25519-sha256 curve25519-sha256@libssh.org</list> + </completionHelp> + <multi/> + <constraint> + <regex>^(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="listen-address"> + <properties> + <help>Local addresses SSH service should listen on</help> + <valueHelp> + <format>ipv4</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to listen for incoming connections</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="loglevel"> + <properties> + <help>Log level</help> + <completionHelp> + <list>quiet fatal error info verbose</list> + </completionHelp> + <valueHelp> + <format>quiet</format> + <description>stay silent</description> + </valueHelp> + <valueHelp> + <format>fatal</format> + <description>log fatals only</description> + </valueHelp> + <valueHelp> + <format>error</format> + <description>log errors and fatals only</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>default log level</description> + </valueHelp> + <valueHelp> + <format>verbose</format> + <description>enable logging of failed login attempts</description> + </valueHelp> + <constraint> + <regex>^(quiet|fatal|error|info|verbose)$</regex> + </constraint> + </properties> + <defaultValue>INFO</defaultValue> + </leafNode> + <leafNode name="mac"> + <properties> + <help>Allowed message authentication code (MAC) algorithms</help> + <completionHelp> + <!-- generated by ssh -Q mac | tr '\n' ' ' as this will not change dynamically --> + <list>hmac-sha1 hmac-sha1-96 hmac-sha2-256 hmac-sha2-512 hmac-md5 hmac-md5-96 umac-64@openssh.com umac-128@openssh.com hmac-sha1-etm@openssh.com hmac-sha1-96-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-md5-etm@openssh.com hmac-md5-96-etm@openssh.com umac-64-etm@openssh.com umac-128-etm@openssh.com</list> + </completionHelp> + <constraint> + <regex>^(hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com)$</regex> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for SSH service</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <multi/> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>22</defaultValue> + </leafNode> + <leafNode name="client-keepalive-interval"> + <properties> + <help>Enable transmission of keepalives from server to client</help> + <valueHelp> + <format>1-65535</format> + <description>Time interval in seconds for keepalive message</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + #include <include/interface-vrf.xml.i> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-console.xml.in b/interface-definitions/system-console.xml.in new file mode 100644 index 000000000..71e63d0cb --- /dev/null +++ b/interface-definitions/system-console.xml.in @@ -0,0 +1,90 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="console" owner="${vyos_conf_scripts_dir}/system_console.py"> + <properties> + <help>Serial console configuration</help> + <priority>100</priority> + </properties> + <children> + <tagNode name="device"> + <properties> + <help>Serial console device name</help> + <completionHelp> + <script>ls -1 /dev | grep -e ttyS -e hvc</script> + <script>if [ -d /dev/serial/by-bus ]; then ls -1 /dev/serial/by-bus; fi</script> + </completionHelp> + <valueHelp> + <format>ttySN</format> + <description>TTY device name, regular serial port</description> + </valueHelp> + <valueHelp> + <format>usbNbXpY</format> + <description>TTY device name, USB based</description> + </valueHelp> + <valueHelp> + <format>hvcN</format> + <description>Xen console</description> + </valueHelp> + <constraint> + <regex>^(ttyS[0-9]+|hvc[0-9]+|usb[0-9]+b.*)$</regex> + </constraint> + </properties> + <children> + <leafNode name="speed"> + <properties> + <help>Console baud rate</help> + <completionHelp> + <list>1200 2400 4800 9600 19200 38400 57600 115200</list> + </completionHelp> + <valueHelp> + <format>1200</format> + <description>1200 bps</description> + </valueHelp> + <valueHelp> + <format>2400</format> + <description>2400 bps</description> + </valueHelp> + <valueHelp> + <format>4800</format> + <description>4800 bps</description> + </valueHelp> + <valueHelp> + <format>9600</format> + <description>9600 bps</description> + </valueHelp> + <valueHelp> + <format>19200</format> + <description>19200 bps</description> + </valueHelp> + <valueHelp> + <format>38400</format> + <description>38400 bps</description> + </valueHelp> + <valueHelp> + <format>57600</format> + <description>57600 bps</description> + </valueHelp> + <valueHelp> + <format>115200</format> + <description>115200 bps</description> + </valueHelp> + <constraint> + <regex>(1200|2400|4800|9600|19200|38400|57600|115200)</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="powersave"> + <properties> + <help>Enable screen blank powersaving on VGA console</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-ip.xml.in b/interface-definitions/system-ip.xml.in new file mode 100644 index 000000000..14b3b8a07 --- /dev/null +++ b/interface-definitions/system-ip.xml.in @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="ip" owner="${vyos_conf_scripts_dir}/system-ip.py"> + <properties> + <help>IPv4 Settings</help> + <priority>400</priority> + </properties> + <children> + <node name="arp"> + <properties> + <help>Parameters for ARP cache</help> + </properties> + <children> + <leafNode name="table-size"> + <properties> + <help>Maximum number of entries to keep in the ARP cache</help> + <completionHelp> + <list>1024 2048 4096 8192 16384 32768</list> + </completionHelp> + <constraint> + <regex>(1024|2048|4096|8192|16384|32768)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="disable-forwarding"> + <properties> + <help>Disable IPv4 forwarding on all interfaces</help> + <valueless/> + </properties> + </leafNode> + <node name="multipath"> + <properties> + <help>IPv4 multipath settings</help> + </properties> + <children> + <leafNode name="ignore-unreachable-nexthops"> + <properties> + <help>Ignore next hops that are not in the ARP table</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="layer4-hashing"> + <properties> + <help>Use layer 4 information for ECMP hashing</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-ipv6.xml.in b/interface-definitions/system-ipv6.xml.in new file mode 100644 index 000000000..47fbeb4e1 --- /dev/null +++ b/interface-definitions/system-ipv6.xml.in @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="ipv6" owner="${vyos_conf_scripts_dir}/system-ipv6.py"> + <properties> + <help>IPv6 Settings</help> + <priority>290</priority> + </properties> + <children> + <leafNode name="disable-forwarding"> + <properties> + <help>Disable IPv6 forwarding on all interfaces</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <help>Disable assignment of IPv6 addresses on all interfaces</help> + <valueless/> + </properties> + </leafNode> + <node name="multipath"> + <properties> + <help>IPv4 multipath settings</help> + </properties> + <children> + <leafNode name="layer4-hashing"> + <properties> + <help>Use layer 4 information for ECMP hashing</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="neighbor"> + <properties> + <help>Parameters for Neighbor cache</help> + </properties> + <children> + <leafNode name="table-size"> + <properties> + <help>Maximum number of entries to keep in the Neighbor cache</help> + <completionHelp> + <list>1024 2048 4096 8192 16384 32768</list> + </completionHelp> + <constraint> + <regex>(1024|2048|4096|8192|16384|32768)</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="strict-dad"> + <properties> + <help>Disable IPv6 operation on interface when DAD fails on LL addr</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-lcd.xml.in b/interface-definitions/system-lcd.xml.in new file mode 100644 index 000000000..36116ae1b --- /dev/null +++ b/interface-definitions/system-lcd.xml.in @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="lcd" owner="${vyos_conf_scripts_dir}/system_lcd.py"> + <properties> + <help>System LCD display</help> + <priority>100</priority> + </properties> + <children> + <leafNode name="model"> + <properties> + <help>Model of the display attached to this system [REQUIRED]</help> + <completionHelp> + <list>cfa-533 cfa-631 cfa-633 cfa-635 sdec</list> + </completionHelp> + <valueHelp> + <format>cfa-533</format> + <description>Crystalfontz CFA-533</description> + </valueHelp> + <valueHelp> + <format>cfa-631</format> + <description>Crystalfontz CFA-631</description> + </valueHelp> + <valueHelp> + <format>cfa-633</format> + <description>Crystalfontz CFA-633</description> + </valueHelp> + <valueHelp> + <format>cfa-635</format> + <description>Crystalfontz CFA-635</description> + </valueHelp> + <valueHelp> + <format>sdec</format> + <description>Lanner, Watchguard, Nexcom NSA, Sophos UTM appliances</description> + </valueHelp> + <constraint> + <regex>^(cfa-533|cfa-631|cfa-633|cfa-635|sdec)$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="device"> + <properties> + <help>Physical device used by LCD display</help> + <completionHelp> + <script>ls -1 /dev | grep ttyS</script> + <script>if [ -d /dev/serial/by-bus ]; then ls -1 /dev/serial/by-bus; fi</script> + </completionHelp> + <valueHelp> + <format>ttySXX</format> + <description>TTY device name, regular serial port</description> + </valueHelp> + <valueHelp> + <format>usbNbXpY</format> + <description>TTY device name, USB based</description> + </valueHelp> + <constraint> + <regex>^(ttyS[0-9]+|usb[0-9]+b.*)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-login-banner.xml.in b/interface-definitions/system-login-banner.xml.in new file mode 100644 index 000000000..c4bb14bd6 --- /dev/null +++ b/interface-definitions/system-login-banner.xml.in @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py"> + <properties> + <help>System User Login Configuration</help> + <priority>400</priority> + </properties> + <children> + <node name="banner" owner="${vyos_conf_scripts_dir}/system-login-banner.py"> + <properties> + <help>System login banners</help> + </properties> + <children> + <leafNode name="post-login"> + <properties> + <help>System loging banner post-login</help> + </properties> + </leafNode> + <leafNode name="pre-login"> + <properties> + <help>System loging banner pre-login</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in new file mode 100644 index 000000000..812a50c8a --- /dev/null +++ b/interface-definitions/system-login.xml.in @@ -0,0 +1,152 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="login" owner="${vyos_conf_scripts_dir}/system-login.py"> + <properties> + <help>System User Login Configuration</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="user"> + <properties> + <help>Local user account information</help> + <constraint> + <regex>[a-zA-Z0-9\-_\.]{1,100}</regex> + </constraint> + <constraintErrorMessage>Username contains illegal characters or\nexceeds 100 character limitation.</constraintErrorMessage> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Password authentication</help> + </properties> + <children> + <leafNode name="encrypted-password"> + <properties> + <help>Encrypted password</help> + <constraint> + <regex>(\*|\!)</regex> + <regex>[a-zA-Z0-9\.\/]{13}$</regex> + <regex>\$1\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{22}</regex> + <regex>\$5\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{43}</regex> + <regex>\$6\$[a-zA-Z0-9\./]*\$[a-zA-Z0-9\./]{86}</regex> + </constraint> + <constraintErrorMessage>Invalid encrypted password for $VAR(../../@).</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="plaintext-password"> + <properties> + <help>Plaintext password used for encryption</help> + </properties> + </leafNode> + <tagNode name="public-keys"> + <properties> + <help>Remote access public keys</help> + <valueHelp> + <format>>identifier<</format> + <description>Key identifier used by ssh-keygen (usually of form user@host)</description> + </valueHelp> + </properties> + <children> + <leafNode name="key"> + <properties> + <help>Public key value (base64-encoded)</help> + </properties> + </leafNode> + <leafNode name="options"> + <properties> + <help>Optional public key options</help> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help></help> + <completionHelp> + <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list> + </completionHelp> + <valueHelp> + <format>ssh-dss</format> + <description/> + </valueHelp> + <valueHelp> + <format>ssh-rsa</format> + <description/> + </valueHelp> + <valueHelp> + <format>ecdsa-sha2-nistp256</format> + <description/> + </valueHelp> + <valueHelp> + <format>ecdsa-sha2-nistp384</format> + <description/> + </valueHelp> + <valueHelp> + <format>ssh-ed25519</format> + <description/> + </valueHelp> + <constraint> + <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="full-name"> + <properties> + <help>Full name of the user (use quotes for names with spaces)</help> + <constraint> + <regex>[^:]*$</regex> + </constraint> + <constraintErrorMessage>Cannot use ':' in full name</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="home-directory"> + <properties> + <help>Home directory</help> + </properties> + </leafNode> + </children> + </tagNode> + #include <include/radius-server.xml.i> + <node name="radius"> + <children> + <tagNode name="server"> + <children> + <leafNode name="timeout"> + <properties> + <help>Session timeout</help> + <valueHelp> + <format>1-30</format> + <description>Session timeout in seconds (default: 2)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-30"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="priority"> + <properties> + <help>Server priority</help> + <valueHelp> + <format>1-255</format> + <description>Server priority (default: 255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + #include <include/interface-vrf.xml.i> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-options.xml.in b/interface-definitions/system-options.xml.in new file mode 100644 index 000000000..a5fec10db --- /dev/null +++ b/interface-definitions/system-options.xml.in @@ -0,0 +1,68 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="options" owner="${vyos_conf_scripts_dir}/system-options.py"> + <properties> + <help>System Options</help> + <priority>9999</priority> + </properties> + <children> + <leafNode name="beep-if-fully-booted"> + <properties> + <help>plays sound via system speaker when you can login</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ctrl-alt-del-action"> + <properties> + <help>Ctrl-Alt-Delete action</help> + <completionHelp> + <list>ignore reboot poweroff</list> + </completionHelp> + <valueHelp> + <format>ignore</format> + <description>Ignore Ctrl-Alt-Delete</description> + </valueHelp> + <valueHelp> + <format>reboot</format> + <description>Reboot VyOS</description> + </valueHelp> + <valueHelp> + <format>poweroff</format> + <description>Poweroff VyOS</description> + </valueHelp> + <constraint> + <regex>^(ignore|reboot|poweroff)$</regex> + </constraint> + <constraintErrorMessage>Must be ignore, reboot, or poweroff</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="reboot-on-panic"> + <properties> + <help>Reboot system on kernel panic</help> + <valueless/> + </properties> + </leafNode> + <node name="http-client"> + <properties> + <help>Global options used for HTTP client</help> + </properties> + <children> + #include <include/source-interface.xml.i> + #include <include/source-address-ipv4-ipv6.xml.i> + </children> + </node> + <node name="ssh-client"> + <properties> + <help>Global options used for SSH client</help> + </properties> + <children> + #include <include/source-address-ipv4-ipv6.xml.i> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-proxy.xml.in b/interface-definitions/system-proxy.xml.in new file mode 100644 index 000000000..540fa97e3 --- /dev/null +++ b/interface-definitions/system-proxy.xml.in @@ -0,0 +1,43 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="proxy" owner="${vyos_conf_scripts_dir}/system-proxy.py"> + <properties> + <help>Sets a proxy for system wide use</help> + </properties> + <children> + <leafNode name="url"> + <properties> + <help>Proxy URL</help> + <constraint> + <regex>http:\/\/[a-z0-9\.]+$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Proxy port</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="username"> + <properties> + <help>Proxy username</help> + <constraint> + <regex>[a-z0-9-_\.]{1,100}$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Proxy password</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-syslog.xml.in b/interface-definitions/system-syslog.xml.in new file mode 100644 index 000000000..194cdb851 --- /dev/null +++ b/interface-definitions/system-syslog.xml.in @@ -0,0 +1,949 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="syslog" owner="${vyos_conf_scripts_dir}/system-syslog.py"> + <properties> + <help>System logging</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="user"> + <properties> + <help>Logging to specific terminal of given user</help> + <constraint> + <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex> + </constraint> + <constraintErrorMessage>illegal characters in user</constraintErrorMessage> + <valueHelp> + <format>username</format> + <description>user login name</description> + </valueHelp> + </properties> + <children> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <constraint> + <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex> + </constraint> + <constraintErrorMessage>Invalid facility type</constraintErrorMessage> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <constraint> + <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex> + </constraint> + <constraintErrorMessage>Invalid loglevel</constraintErrorMessage> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="host"> + <properties> + <help>Logging to a remote host</help> + <constraint> + <validator name="ip-address"/> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid host (FQDN or IP address)</constraintErrorMessage> + <valueHelp> + <format>ipv4</format> + <description>Remote syslog server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Remote syslog server FQDN</description> + </valueHelp> + </properties> + <children> + <leafNode name="port"> + <properties> + <help>Destination port</help> + <valueHelp> + <format>1-65535</format> + <description>Destination port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Invalid destination port value</constraintErrorMessage> + </properties> + </leafNode> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <constraint> + <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex> + </constraint> + <constraintErrorMessage>Invalid facility type</constraintErrorMessage> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="protocol"> + <properties> + <help>syslog communication protocol</help> + <valueHelp> + <format>udp</format> + <description>send log messages to remote syslog server over udp</description> + </valueHelp> + <valueHelp> + <format>tcp</format> + <description>send log messages to remote syslog server over tcp</description> + </valueHelp> + <completionHelp> + <list>udp tcp</list> + </completionHelp> + <constraint> + <regex>(udp|tcp)</regex> + </constraint> + <constraintErrorMessage>invalid protocol name</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <constraint> + <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex> + </constraint> + <constraintErrorMessage>Invalid loglevel</constraintErrorMessage> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <node name="global"> + <properties> + <help>Logging to system standard location</help> + </properties> + <children> + <node name="archive"> + <properties> + <help>Log file size and rotation characteristics</help> + </properties> + <children> + <leafNode name="file"> + <properties> + <help>Number of saved files (default is 5)</help> + <constraint> + <regex>[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>Size of log files (in kbytes, default is 256)</help> + <constraint> + <regex>[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in size</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <constraint> + <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex> + </constraint> + <constraintErrorMessage>Invalid facility type</constraintErrorMessage> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <constraint> + <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex> + </constraint> + <constraintErrorMessage>Invalid loglevel</constraintErrorMessage> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + <node name="marker"> + <properties> + <help>mark messages sent to syslog</help> + </properties> + <children> + <leafNode name="interval"> + <properties> + <help>time interval how often a mark message is being sent in seconds (default: 1200)</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name ="preserve-fqdn"> + <properties> + <help>uses FQDN for logging</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + <tagNode name="file"> + <properties> + <help>Logging to a file</help> + <constraint> + <regex>[a-zA-Z0-9\-_.]{1,255}</regex> + </constraint> + <constraintErrorMessage>illegal characters in filename or filename longer than 255 characters</constraintErrorMessage> + </properties> + <children> + <node name="archive"> + <properties> + <help>Log file size and rotation characteristics</help> + </properties> + <children> + <leafNode name="file"> + <properties> + <help>Number of saved files (default is 5)</help> + <constraint> + <regex>[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>Size of log files (in kbytes, default is 256)</help> + <constraint> + <regex>[0-9]+</regex> + </constraint> + <constraintErrorMessage>illegal characters in size</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <constraint> + <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex> + </constraint> + <constraintErrorMessage>Invalid facility type</constraintErrorMessage> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <constraint> + <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex> + </constraint> + <constraintErrorMessage>Invalid loglevel</constraintErrorMessage> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <node name="console"> + <properties> + <help>logging to serial console</help> + </properties> + <children> + <tagNode name="facility"> + <properties> + <help>Facility for logging</help> + <completionHelp> + <list>auth authpriv cron daemon kern lpr mail mark news protocols security syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7 all</list> + </completionHelp> + <constraint> + <regex>(auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7|all)</regex> + </constraint> + <constraintErrorMessage>Invalid facility type</constraintErrorMessage> + <valueHelp> + <format>all</format> + <description>All facilities excluding "mark"</description> + </valueHelp> + <valueHelp> + <format>auth</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>authpriv</format> + <description>Non-system authorization</description> + </valueHelp> + <valueHelp> + <format>cron</format> + <description>Cron daemon</description> + </valueHelp> + <valueHelp> + <format>daemon</format> + <description>System daemons</description> + </valueHelp> + <valueHelp> + <format>kern</format> + <description>Kernel</description> + </valueHelp> + <valueHelp> + <format>lpr</format> + <description>Line printer spooler</description> + </valueHelp> + <valueHelp> + <format>mail</format> + <description>Mail subsystem</description> + </valueHelp> + <valueHelp> + <format>mark</format> + <description>Timestamp</description> + </valueHelp> + <valueHelp> + <format>news</format> + <description>USENET subsystem</description> + </valueHelp> + <valueHelp> + <format>protocols</format> + <description>depricated will be set to local7</description> + </valueHelp> + <valueHelp> + <format>security</format> + <description>depricated will be set to auth</description> + </valueHelp> + <valueHelp> + <format>syslog</format> + <description>Authentication and authorization</description> + </valueHelp> + <valueHelp> + <format>user</format> + <description>Application processes</description> + </valueHelp> + <valueHelp> + <format>uucp</format> + <description>UUCP subsystem</description> + </valueHelp> + <valueHelp> + <format>local0</format> + <description>Local facility 0</description> + </valueHelp> + <valueHelp> + <format>local1</format> + <description>Local facility 1</description> + </valueHelp> + <valueHelp> + <format>local2</format> + <description>Local facility 2</description> + </valueHelp> + <valueHelp> + <format>local3</format> + <description>Local facility 3</description> + </valueHelp> + <valueHelp> + <format>local4</format> + <description>Local facility 4</description> + </valueHelp> + <valueHelp> + <format>local5</format> + <description>Local facility 5</description> + </valueHelp> + <valueHelp> + <format>local6</format> + <description>Local facility 6</description> + </valueHelp> + <valueHelp> + <format>local7</format> + <description>Local facility 7</description> + </valueHelp> + </properties> + <children> + <leafNode name="level"> + <properties> + <help>Logging level</help> + <completionHelp> + <list>emerg alert crit err warning notice info debug all</list> + </completionHelp> + <constraint> + <regex>(emerg|alert|crit|err|warning|notice|info|debug|all)</regex> + </constraint> + <constraintErrorMessage>Invalid loglevel</constraintErrorMessage> + <valueHelp> + <format>emerg</format> + <description>Emergency messages</description> + </valueHelp> + <valueHelp> + <format>alert</format> + <description>Urgent messages</description> + </valueHelp> + <valueHelp> + <format>crit</format> + <description>Critical messages</description> + </valueHelp> + <valueHelp> + <format>err</format> + <description>Error messages</description> + </valueHelp> + <valueHelp> + <format>warning</format> + <description>Warning messages</description> + </valueHelp> + <valueHelp> + <format>notice</format> + <description>Messages for further investigation</description> + </valueHelp> + <valueHelp> + <format>info</format> + <description>Informational messages</description> + </valueHelp> + <valueHelp> + <format>debug</format> + <description>Debug messages</description> + </valueHelp> + <valueHelp> + <format>all</format> + <description>Log everything</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-time-zone.xml.in b/interface-definitions/system-time-zone.xml.in new file mode 100644 index 000000000..ff815c9d3 --- /dev/null +++ b/interface-definitions/system-time-zone.xml.in @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <leafNode name="time-zone" owner="${vyos_conf_scripts_dir}/system-timezone.py"> + <properties> + <help>Local time zone (default UTC)</help> + <priority>100</priority> + <completionHelp> + <script>find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/:: | sort</script> + </completionHelp> + <constraint> + <validator name="timezone" argument="--validate"/> + </constraint> + </properties> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/tftp-server.xml.in b/interface-definitions/tftp-server.xml.in new file mode 100644 index 000000000..2874b034c --- /dev/null +++ b/interface-definitions/tftp-server.xml.in @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<!-- TFTP configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="tftp-server" owner="${vyos_conf_scripts_dir}/tftp_server.py"> + <properties> + <help>Trivial File Transfer Protocol (TFTP) server</help> + <priority>990</priority> + </properties> + <children> + <leafNode name="directory"> + <properties> + <help>Folder containing files served by TFTP [REQUIRED]</help> + </properties> + </leafNode> + <leafNode name="allow-upload"> + <properties> + <help>Allow TFTP file uploads</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for TFTP service</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 69)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="listen-address"> + <properties> + <help>Addresses for TFTP server to listen [REQUIRED]</help> + <valueHelp> + <format>ipv4</format> + <description>TFTP IPv4 listen address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>TFTP IPv6 listen address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vpn_anyconnect.xml.in b/interface-definitions/vpn_anyconnect.xml.in new file mode 100644 index 000000000..e74326986 --- /dev/null +++ b/interface-definitions/vpn_anyconnect.xml.in @@ -0,0 +1,258 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpn"> + <children> + <node name="anyconnect" owner="${vyos_conf_scripts_dir}/vpn_anyconnect.py"> + <properties> + <help>SSL VPN AnyConnect</help> + <priority>901</priority> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Authentication for remote access SSL VPN Server</help> + </properties> + <children> + <leafNode name="mode"> + <properties> + <help>Authentication mode used by this server</help> + <valueHelp> + <format>local</format> + <description>Use local username/password configuration</description> + </valueHelp> + <valueHelp> + <format>radius</format> + <description>Use RADIUS server for user autentication</description> + </valueHelp> + <constraint> + <regex>(local|radius)</regex> + </constraint> + <completionHelp> + <list>local radius</list> + </completionHelp> + </properties> + </leafNode> + <node name="local-users"> + <properties> + <help>Local user authentication for SSL VPN server</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a SSL VPN Server user</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + #include <include/radius-server.xml.i> + <node name="radius"> + <children> + <leafNode name="timeout"> + <properties> + <help>Session timeout</help> + <valueHelp> + <format>1-30</format> + <description>Session timeout in seconds (default: 2)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-30"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 30 seconds</constraintErrorMessage> + </properties> + <defaultValue>2</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + <node name="listen-ports"> + <properties> + <help>SSL Certificate, SSL Key and CA (/config/auth)</help> + </properties> + <children> + <leafNode name="tcp"> + <properties> + <help>tcp port number to accept connections (default: 443)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 443)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>443</defaultValue> + </leafNode> + <leafNode name="udp"> + <properties> + <help>udp port number to accept connections (default: 443)</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 443)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>443</defaultValue> + </leafNode> + </children> + </node> + <node name="ssl"> + <properties> + <help>SSL Certificate, SSL Key and CA (/config/auth)</help> + </properties> + <children> + <leafNode name="ca-cert-file"> + <properties> + <help>Certificate Authority certificate</help> + <completionHelp> + <script>ls /config/auth</script> + </completionHelp> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config"/> + </constraint> + </properties> + </leafNode> + <leafNode name="cert-file"> + <properties> + <help>Server Certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config"/> + </constraint> + </properties> + </leafNode> + <leafNode name="key-file"> + <properties> + <help>Privat Key of the Server Certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="network-settings"> + <properties> + <help>Network settings</help> + </properties> + <children> + <leafNode name="push-route"> + <properties> + <help>Route to be pushed to the client</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 network and prefix length</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 network and prefix length</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="client-ip-settings"> + <properties> + <help>Client IP pools settings</help> + </properties> + <children> + <leafNode name="subnet"> + <properties> + <help>Client IP subnet (CIDR notation)</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <node name="client-ipv6-pool"> + <properties> + <help>Pool of client IPv6 addresses</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>Pool of addresses used to assign to clients</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="mask"> + <properties> + <help>Prefix length used for individual client</help> + <valueHelp> + <format><48-128></format> + <description>Client prefix length (default: 64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 48-128"/> + </constraint> + </properties> + <defaultValue>64</defaultValue> + </leafNode> + </children> + </node> + <leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS) propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> +</node> +</interfaceDefinition> diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in new file mode 100644 index 000000000..702ef8b5a --- /dev/null +++ b/interface-definitions/vpn_l2tp.xml.in @@ -0,0 +1,457 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpn"> + <children> + <node name="l2tp" owner="${vyos_conf_scripts_dir}/vpn_l2tp.py"> + <properties> + <help>L2TP Virtual Private Network (VPN)</help> + </properties> + <children> + <node name="remote-access"> + <properties> + <help>Remote access L2TP VPN</help> + </properties> + <children> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <constraint> + <validator name="numeric" argument="--range 128-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="outside-address"> + <properties> + <help>External IP address to which VPN clients will connect</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="gateway-address"> + <properties> + <help>Gatway address uses as client tunnel termination point</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + #include <include/accel-name-server.xml.in> + <node name="lns"> + <properties> + <help>L2TP Network Server (LNS)</help> + </properties> + <children> + <leafNode name="shared-secret"> + <properties> + <help>Tunnel password used to authenticate the client (LAC)</help> + </properties> + </leafNode> + </children> + </node> + <leafNode name="ccp-disable"> + <properties> + <help>Disable Compression Control Protocol (CCP)</help> + <valueless /> + </properties> + </leafNode> + <node name="ipsec-settings"> + <properties> + <help>Internet Protocol Security (IPsec) for remote access L2TP VPN</help> + </properties> + <children> + <node name="authentication"> + <properties> + <help>IPsec authentication settings</help> + </properties> + <children> + <leafNode name="mode"> + <properties> + <help>Authentication mode for IPsec</help> + <valueHelp> + <format>pre-shared-secret</format> + <description>Use pre-shared secret for IPsec authentication</description> + </valueHelp> + <valueHelp> + <format>x509</format> + <description>Use X.509 certificate for IPsec authentication</description> + </valueHelp> + <constraint> + <regex>(pre-shared-secret|x509)</regex> + </constraint> + <completionHelp> + <list>pre-shared-secret x509</list> + </completionHelp> + </properties> + </leafNode> + <leafNode name="pre-shared-secret"> + <properties> + <help>Pre-shared secret for IPsec</help> + </properties> + </leafNode> + <node name="x509"> + <properties> + <help>X.509 certificate</help> + </properties> + <children> + <leafNode name="ca-cert-file"> + <properties> + <help>File containing the X.509 certificate for the Certificate Authority (CA)</help> + <valueHelp> + <format><text></format> + <description>File in /config/auth</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="crl-file"> + <properties> + <help>File containing the X.509 Certificate Revocation List (CRL)</help> + <valueHelp> + <format><text></format> + <description>File in /config/auth</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="server-cert-file"> + <properties> + <help>File containing the X.509 certificate for the remote access VPN server (this host)</help> + <valueHelp> + <format><text></format> + <description>File in /config/auth</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="server-key-file"> + <properties> + <help>File containing the private key for the X.509 certificate for the remote access VPN server (this host)</help> + <valueHelp> + <format><text></format> + <description>File in /config/auth</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="server-key-password"> + <properties> + <help>Password that protects the private key</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="ike-lifetime"> + <properties> + <help>IKE lifetime</help> + <valueHelp> + <format><30-86400></format> + <description>IKE lifetime in seconds (default 3600)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 30-86400"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lifetime"> + <properties> + <help>ESP lifetime</help> + <valueHelp> + <format><30-86400></format> + <description>IKE lifetime in seconds (default 3600)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 30-86400"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + #include <include/accel-wins-server.xml.i> + <node name="client-ip-pool"> + <properties> + <help>Pool of client IP addresses (must be within a /24)</help> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address in the pool (will be used as gateway address)</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="subnet"> + <properties> + <help>Client IP subnet (CIDR notation)</help> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 subnet address</description> + </valueHelp> + <multi /> + </properties> + </leafNode> + </children> + </node> + #include <include/accel-client-ipv6-pool.xml.in> + <leafNode name="description"> + <properties> + <help>Description for L2TP remote-access settings</help> + </properties> + </leafNode> + <leafNode name="dhcp-interface"> + <properties> + <help>DHCP interface to listen on</help> + </properties> + </leafNode> + <leafNode name="idle"> + <properties> + <help>PPP idle timeout</help> + <valueHelp> + <format><30-86400></format> + <description>PPP idle timeout in seconds (default 1800)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 30-86400"/> + </constraint> + </properties> + </leafNode> + <node name="authentication"> + <properties> + <help>Authentication for remote access L2TP VPN</help> + </properties> + <children> + <leafNode name="require"> + <properties> + <help>Authentication protocol for remote access peer L2TP VPN</help> + <valueHelp> + <format>pap</format> + <description>Require the peer to authenticate itself using PAP [Password Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>chap</format> + <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>mschap</format> + <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>mschap-v2</format> + <description>Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2].</description> + </valueHelp> + <constraint> + <regex>(pap|chap|mschap|mschap-v2)</regex> + </constraint> + <completionHelp> + <list>pap chap mschap mschap-v2</list> + </completionHelp> + <multi /> + </properties> + </leafNode> + <leafNode name="mppe"> + <properties> + <help>Specifies mppe negotioation preference. (default require mppe 128-bit stateless</help> + <valueHelp> + <format>deny</format> + <description>deny mppe</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>Ask client for mppe, if it rejects do not fail</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>ask client for mppe, if it rejects drop connection</description> + </valueHelp> + <constraint> + <regex>(deny|prefer|require)</regex> + </constraint> + <completionHelp> + <list>deny prefer require</list> + </completionHelp> + </properties> + </leafNode> + #include <include/accel-auth-mode.xml.i> + <node name="local-users"> + <properties> + <help>Local user authentication for remote access L2TP VPN</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a L2TP Server user</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + <leafNode name="static-ip"> + <properties> + <help>Static client IP address</help> + </properties> + </leafNode> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="upload"> + <properties> + <help>Upload bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="download"> + <properties> + <help>Download bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + #include <include/radius-server.xml.i> + <node name="radius"> + <children> + <tagNode name="server"> + <children> + <leafNode name="fail-time"> + <properties> + <help>Mark server unavailable for <n> seconds on failure</help> + <valueHelp> + <format>0-600</format> + <description>Fail time penalty</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-600"/> + </constraint> + <constraintErrorMessage>Fail time must be between 0 and 600 seconds</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="timeout"> + <properties> + <help>Timeout to wait response from server (seconds)</help> + </properties> + </leafNode> + <leafNode name="acct-timeout"> + <properties> + <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help> + </properties> + </leafNode> + <leafNode name="max-try"> + <properties> + <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help> + </properties> + </leafNode> + <leafNode name="nas-identifier"> + <properties> + <help>Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests.</help> + </properties> + </leafNode> + <node name="dae-server"> + <properties> + <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + <children> + <leafNode name="ip-address"> + <properties> + <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + </leafNode> + <leafNode name="secret"> + <properties> + <help>Secret for Dynamic Authorization Extension server (DM/CoA)</help> + </properties> + </leafNode> + </children> + </node> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="attribute"> + <properties> + <help>Specifies which radius attribute contains rate information. (default is Filter-Id)</help> + </properties> + </leafNode> + <leafNode name="vendor"> + <properties> + <help>Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius)</help> + </properties> + </leafNode> + <leafNode name="enable"> + <properties> + <help>Enables Bandwidth shaping via RADIUS</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="ppp-options"> + <properties> + <help>Advanced protocol options</help> + </properties> + <children> + <leafNode name="lcp-echo-interval"> + <properties> + <help>LCP echo-requests/sec</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-failure"> + <properties> + <help>Maximum number of Echo-Requests may be sent without valid reply</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn_pptp.xml.in new file mode 100644 index 000000000..032455b4d --- /dev/null +++ b/interface-definitions/vpn_pptp.xml.in @@ -0,0 +1,165 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpn"> + <children> + <node name="pptp" owner="${vyos_conf_scripts_dir}/vpn_pptp.py"> + <properties> + <help>Point to Point Tunneling Protocol (PPTP) Virtual Private Network (VPN)</help> + </properties> + <children> + <node name="remote-access"> + <properties> + <help>Remote access PPTP VPN</help> + </properties> + <children> + <leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <constraint> + <validator name="numeric" argument="--range 128-16384"/> + </constraint> + </properties> + </leafNode> + <leafNode name="outside-address"> + <properties> + <help>External IP address to which VPN clients will connect</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS) propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + #include <include/accel-wins-server.xml.i> + <node name="client-ip-pool"> + <properties> + <help>Pool of client IP addresses (must be within a /24)</help> + </properties> + <children> + <leafNode name="start"> + <properties> + <help>First IP address in the pool (will be used as gateway address)</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last IP address in the pool</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="gateway-address"> + <properties> + <help>Gatway address uses as client tunnel termination point</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <node name="authentication"> + <properties> + <help>Authentication for remote access PPTP VPN</help> + </properties> + <children> + <leafNode name="require"> + <properties> + <help>Authentication protocol for remote access peer PPTP VPN</help> + <valueHelp> + <format>pap</format> + <description>Require the peer to authenticate itself using PAP [Password Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>chap</format> + <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>mschap</format> + <description>Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol].</description> + </valueHelp> + <valueHelp> + <format>mschap-v2</format> + <description>Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2].</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="mppe"> + <properties> + <help>Specifies mppe negotioation preference. (default require mppe 128-bit stateless</help> + <valueHelp> + <format>deny</format> + <description>deny mppe</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>ask client for mppe, if it rejects do not fail</description> + </valueHelp> + <valueHelp> + <format>require</format> + <description>ask client for mppe, if it rejects drop connection</description> + </valueHelp> + <constraint> + <regex>(deny|prefer|require)</regex> + </constraint> + <completionHelp> + <list>deny prefer require</list> + </completionHelp> + </properties> + </leafNode> + #include <include/accel-auth-mode.xml.i> + <node name="local-users"> + <properties> + <help>Local user authentication for remote access PPTP VPN</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a PPTP Server user</help> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + <leafNode name="static-ip"> + <properties> + <help>Static client IP address</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in new file mode 100644 index 000000000..f0c93b882 --- /dev/null +++ b/interface-definitions/vpn_sstp.xml.in @@ -0,0 +1,273 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vpn"> + <children> + <node name="sstp" owner="${vyos_conf_scripts_dir}/vpn_sstp.py"> + <properties> + <help>Secure Socket Tunneling Protocol (SSTP) server</help> + <priority>901</priority> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Authentication for remote access SSTP Server</help> + </properties> + <children> + <node name="local-users"> + <properties> + <help>Local user authentication for SSTP server</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>User name for authentication</help> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Option to disable a SSTP Server user</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="password"> + <properties> + <help>Password for authentication</help> + </properties> + </leafNode> + <leafNode name="static-ip"> + <properties> + <help>Static client IP address</help> + </properties> + </leafNode> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="upload"> + <properties> + <help>Upload bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="download"> + <properties> + <help>Download bandwidth limit in kbits/sec</help> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + #include <include/accel-auth-mode.xml.i> + <leafNode name="protocols"> + <properties> + <help>Authentication protocol for remote access peer SSTP VPN</help> + <completionHelp> + <list>pap chap mschap mschap-v2</list> + </completionHelp> + <valueHelp> + <format>pap</format> + <description>Authentication via PAP (Password Authentication Protocol)</description> + </valueHelp> + <valueHelp> + <format>chap</format> + <description>Authentication via CHAP (Challenge Handshake Authentication Protocol)</description> + </valueHelp> + <valueHelp> + <format>mschap</format> + <description>Authentication via MS-CHAP (Microsoft Challenge Handshake Authentication Protocol)</description> + </valueHelp> + <valueHelp> + <format>mschap-v2</format> + <description>Authentication via MS-CHAPv2 (Microsoft Challenge Handshake Authentication Protocol, version 2)</description> + </valueHelp> + <constraint> + <regex>(pap|chap|mschap|mschap-v2)</regex> + </constraint> + <multi /> + </properties> + </leafNode> + #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> + <node name="radius"> + <children> + <node name="rate-limit"> + <properties> + <help>Upload/Download speed limits</help> + </properties> + <children> + <leafNode name="attribute"> + <properties> + <help>Specifies RADIUS attribute containing rate information (default 'Filter-Id')</help> + </properties> + </leafNode> + <leafNode name="vendor"> + <properties> + <help>Specifies vendor dictionary (needs to be in /usr/share/accel-ppp/radius)</help> + </properties> + </leafNode> + <leafNode name="enable"> + <properties> + <help>Enable RADIUS bandwidth shaping</help> + <valueless /> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="ssl"> + <properties> + <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help> + </properties> + <children> + <leafNode name="ca-cert-file"> + <properties> + <help>Certificate Authority certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="cert-file"> + <properties> + <help>Server Certificate</help> + <completionHelp> + <script>ls /config</script> + </completionHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="key-file"> + <properties> + <help>Privat Key of the Server Certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="network-settings"> + <properties> + <help>Network settings</help> + </properties> + <children> + <node name="client-ip-settings"> + <properties> + <help>Client IP pools and gateway setting</help> + </properties> + <children> + <leafNode name="subnet"> + <properties> + <help>Client IP subnet (CIDR notation)</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage> + <multi /> + </properties> + </leafNode> + <leafNode name="gateway-address"> + <properties> + <help>Gateway IP address</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <constraintErrorMessage>invalid IPv4 address</constraintErrorMessage> + <valueHelp> + <format>ipv4</format> + <description>Default Gateway send to the client</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + #include <include/accel-client-ipv6-pool.xml.in> + #include <include/accel-name-server.xml.in> + #include <include/interface-mtu-68-1500.xml.i> + </children> + </node> + <node name="ppp-settings"> + <properties> + <help>PPP (Point-to-Point Protocol) settings</help> + </properties> + <children> + <leafNode name="mppe"> + <properties> + <help>Specifies mppe negotiation preferences</help> + <completionHelp> + <list>require prefer deny</list> + </completionHelp> + <constraint> + <regex>(^require|prefer|deny)</regex> + </constraint> + <valueHelp> + <format>require</format> + <description>send mppe request, if client rejects, drop the connection</description> + </valueHelp> + <valueHelp> + <format>prefer</format> + <description>send mppe request, if client rejects continue</description> + </valueHelp> + <valueHelp> + <format>deny</format> + <description>drop all mppe</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="lcp-echo-interval"> + <properties> + <help>LCP echo-requests/sec</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-failure"> + <properties> + <help>Maximum number of Echo-Requests may be sent without valid reply</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="lcp-echo-timeout"> + <properties> + <help>Timeout in seconds to wait for any peer activity. If this option specified it turns on adaptive lcp echo functionality and "lcp-echo-failure" is not used.</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> +</node> +</interfaceDefinition> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in new file mode 100644 index 000000000..159f4ea3e --- /dev/null +++ b/interface-definitions/vrf.xml.in @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vrf" owner="${vyos_conf_scripts_dir}/vrf.py"> + <properties> + <help>Virtual Routing and Forwarding</help> + <!-- must be before any interface creation --> + <priority>60</priority> + </properties> + <children> + <leafNode name="bind-to-all"> + <properties> + <help>Enable binding services to all VRFs</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="name"> + <properties> + <help>VRF instance name</help> + <constraint> + <validator name="vrf-name"/> + </constraint> + <constraintErrorMessage>VRF instance name must be 15 characters or less and can not\nbe named as regular network interfaces.\n</constraintErrorMessage> + <valueHelp> + <format>name</format> + <description>Instance name</description> + </valueHelp> + </properties> + <children> + <leafNode name="table"> + <properties> + <help>Routing table associated with this instance</help> + <valueHelp> + <format>100-2147483647</format> + <description>Routing table ID</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 100-2147483647"/> + </constraint> + <constraintErrorMessage>VRF routing table must be in range from 100 to 2147483647</constraintErrorMessage> + </properties> + </leafNode> + #include <include/interface-description.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in new file mode 100644 index 000000000..120c7d218 --- /dev/null +++ b/interface-definitions/vrrp.xml.in @@ -0,0 +1,302 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="high-availability"> + <properties> + <help>High availability settings</help> + </properties> + <children> + <node name="vrrp" owner="${vyos_conf_scripts_dir}/vrrp.py"> + <properties> + <priority>800</priority> <!-- after all interfaces and conntrack-sync --> + <help>Virtual Router Redundancy Protocol settings</help> + </properties> + <children> + <tagNode name="group"> + <properties> + <help>VRRP group</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Network interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + </leafNode> + <leafNode name="advertise-interval"> + <properties> + <help>Advertise interval</help> + <valueHelp> + <format>1-255</format> + <description>Advertise interval in seconds (default: 1)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + <node name="authentication"> + <properties> + <help>VRRP authentication</help> + </properties> + <children> + <leafNode name="password"> + <properties> + <help>VRRP password</help> + <valueHelp> + <format>text</format> + <description>Password string (up to 8 characters)</description> + </valueHelp> + <constraint> + <regex>.{1,8}</regex> + </constraint> + <constraintErrorMessage>Password must not be longer than 8 characters</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>Authentication type</help> + <completionHelp> + <list>plaintext-password ah</list> + </completionHelp> + <constraint> + <regex>(plaintext-password|ah)</regex> + </constraint> + <constraintErrorMessage>Authentication type must be plaintext-password or ah</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="description"> + <properties> + <help>Group description</help> + </properties> + </leafNode> + <leafNode name="disable"> + <properties> + <valueless/> + <help>Disable VRRP group</help> + </properties> + </leafNode> + <node name="health-check"> + <properties> + <help>Health check script</help> + </properties> + <children> + <leafNode name="failure-count"> + <properties> + <help>Health check failure count required for transition to fault (default: 3)</help> + <constraint> + <validator name="numeric" argument="--positive" /> + </constraint> + </properties> + </leafNode> + <leafNode name="interval"> + <properties> + <help>Health check execution interval in seconds (default: 60)</help> + <constraint> + <validator name="numeric" argument="--positive"/> + </constraint> + </properties> + </leafNode> + <leafNode name="script"> + <properties> + <help>Health check script file</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="hello-source-address"> + <properties> + <help>VRRP hello source address (IPv4 or IPv6)</help> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <valueHelp> + <format><IPv4|IPv6></format> + <description>IPv4 or IPv6 hello source address</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="peer-address"> + <properties> + <help>Unicast VRRP peer address (IPv4 or IPv6)</help> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <valueHelp> + <format><IPv4|IPv6></format> + <description>IPv4 or IPv6 unicast peer address</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="no-preempt"> + <properties> + <valueless/> + <help>Disable master preemption</help> + </properties> + </leafNode> + <leafNode name="preempt-delay"> + <properties> + <help>Preempt delay (in seconds)</help> + <constraint> + <validator name="numeric" argument="--range 0-1000"/> + </constraint> + </properties> + </leafNode> + <leafNode name="priority"> + <properties> + <help>Router priority</help> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <valueHelp> + <format>1-255</format> + <description>Router priority (default: 100)</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="rfc3768-compatibility"> + <properties> + <valueless/> + <help>Use VRRP virtual MAC address as per RFC3768</help> + </properties> + </leafNode> + <node name="transition-script"> + <properties> + <help>VRRP transition scripts</help> + </properties> + <children> + <leafNode name="master"> + <properties> + <help>Script to run on VRRP state transition to master</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="backup"> + <properties> + <help>Script to run on VRRP state transition to backup</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="fault"> + <properties> + <help>Script to run on VRRP state transition to fault</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Script to run on VRRP state transition to stop</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="virtual-address"> + <properties> + <multi/> + <help>Virtual address (IPv4 or IPv6, but they must not be mixed in one group)</help> + <constraint> + <validator name="ipv4-host"/> + <validator name="ipv6-host"/> + </constraint> + <constraintErrorMessage>Virtual address must be a valid IPv4 or IPv6 address with prefix length (e.g. 192.0.2.3/24 or 2001:db8:ff::10/64)</constraintErrorMessage> + <valueHelp> + <format><IPv4|IPv6></format> + <description>IPv4 or IPv6 virtual address</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="vrid"> + <properties> + <help>Virtual router identifier</help> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <valueHelp> + <format>1-255</format> + <description>Virtual router identifier</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="sync-group"> + <properties> + <help>VRRP sync group</help> + </properties> + <children> + <leafNode name="member"> + <properties> + <multi/> + <help>Sync group member</help> + <valueHelp> + <format>text</format> + <description>VRRP group name</description> + </valueHelp> + <completionHelp> + <path>high-availability vrrp group</path> + </completionHelp> + </properties> + </leafNode> + <node name="transition-script"> + <properties> + <help>VRRP transition scripts</help> + </properties> + <children> + <leafNode name="master"> + <properties> + <help>Script to run on VRRP state transition to master</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="backup"> + <properties> + <help>Script to run on VRRP state transition to backup</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="fault"> + <properties> + <help>Script to run on VRRP state transition to fault</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Script to run on VRRP state transition to stop</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/add-system-image.xml b/op-mode-definitions/add-system-image.xml new file mode 100644 index 000000000..3dc1c67ab --- /dev/null +++ b/op-mode-definitions/add-system-image.xml @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="add"> + <children> + <node name="system"> + <properties> + <help>Add item to a system facility</help> + </properties> + <children> + <tagNode name="image"> + <properties> + <help>Add a new image to the system</help> + <completionHelp> + <list>/path/to/vyos-image.iso http://example.com/vyos-image.iso</list> + </completionHelp> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}"</command> + <children> + <tagNode name="vrf"> + <properties> + <help>Download image via specified VRF</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}"</command> + <children> + <tagNode name="username"> + <properties> + <help>Username for authentication</help> + </properties> + <children> + <tagNode name="password"> + <properties> + <help>Password to use with authentication</help> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --vrf "${6}" --username "${8}" --password "${10}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="username"> + <properties> + <help>Username for authentication</help> + </properties> + <children> + <tagNode name="password"> + <properties> + <help>Password to use with authentication</help> + </properties> + <command>sudo ${vyatta_sbindir}/install-image --url "${4}" --username "${6}" --password "${8}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/anyconnect.xml b/op-mode-definitions/anyconnect.xml new file mode 100644 index 000000000..7e8cdd35b --- /dev/null +++ b/op-mode-definitions/anyconnect.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="anyconnect-server"> + <properties> + <help>show anyconnect-server information</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active anyconnect server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/anyconnect-control.py --action="show_sessions"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/configure.xml b/op-mode-definitions/configure.xml new file mode 100644 index 000000000..3dd5a0f45 --- /dev/null +++ b/op-mode-definitions/configure.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="configure"> + <properties> + <help>Enter configuration mode</help> + </properties> + <command>if [ `id -u` == 0 ]; then + echo "You are attempting to enter configuration mode as root." + echo "It may have unintended consequences and render your system" + echo "unusable until restart." + echo "Please do it as an administrator level VyOS user instead." + else + if grep -q -e '^overlay.*/filesystem.squashfs' /proc/mounts; then + echo "WARNING: You are currently configuring a live-ISO environment, changes will not persist until installed" + fi + history -w + export _OFR_CONFIGURE=ok + newgrp vyattacfg + unset _OFR_CONFIGURE + _vyatta_op_do_key_bindings + history -r + fi</command> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/connect.xml b/op-mode-definitions/connect.xml new file mode 100644 index 000000000..1ec62949a --- /dev/null +++ b/op-mode-definitions/connect.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="connect"> + <properties> + <help>Establish connection</help> + </properties> + <children> + <tagNode name="console"> + <properties> + <help>Connect to device attached to serial console server</help> + <completionHelp> + <path>service console-server device</path> + </completionHelp> + </properties> + <command>/usr/bin/console "$3"</command> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Bring up a connection-oriented network interface</help> + <completionHelp> + <path>interfaces pppoe</path> + <path>interfaces wirelessmodem</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/date.xml b/op-mode-definitions/date.xml new file mode 100644 index 000000000..15a69dbd9 --- /dev/null +++ b/op-mode-definitions/date.xml @@ -0,0 +1,64 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="date"> + <properties> + <help>Show system time and date</help> + </properties> + <command>/bin/date</command> + <children> + <node name="utc"> + <properties> + <help>Show system date and time as Coordinated Universal Time</help> + </properties> + <command>/bin/date -u</command> + <children> + <leafNode name="maya"> + <properties> + <help>Show UTC date in Maya calendar format</help> + </properties> + <command>${vyos_op_scripts_dir}/maya_date.py $(date +%s)</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="set"> + <children> + <tagNode name="date"> + <properties> + <help>Set system date and time</help> + <completionHelp> + <list><MMDDhhmm> <MMDDhhmmYY> <MMDDhhmmCCYY> <MMDDhhmmCCYY.ss></list> + </completionHelp> + </properties> + <command>/bin/date "$3"</command> + </tagNode> + <node name="date"> + <properties> + <help>Set system date and time</help> + </properties> + <children> + <node name="ntp"> + <properties> + <help>Set system date and time from NTP server (default: 0.pool.ntp.org)</help> + </properties> + <command>/usr/sbin/ntpdate -u 0.pool.ntp.org</command> + </node> + <tagNode name="ntp"> + <properties> + <help>Set system date and time from NTP server</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ntp_servers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/ntpdate -u "$4"</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml new file mode 100644 index 000000000..48752cfd5 --- /dev/null +++ b/op-mode-definitions/dhcp.xml @@ -0,0 +1,203 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="dhcp"> + <properties> + <help>Show DHCP (Dynamic Host Configuration Protocol) information</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Show DHCP server information</help> + </properties> + <children> + <node name="leases"> + <properties> + <help>Show DHCP server leases</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases</command> + <children> + <tagNode name="pool"> + <properties> + <help>Show DHCP server leases for a specific pool</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed pool</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $6</command> + </tagNode> + <tagNode name="sort"> + <properties> + <help>Show DHCP server leases sorted by the specified key</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed sort</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --sort $6</command> + </tagNode> + <tagNode name="state"> + <properties> + <help>Show DHCP server leases with a specific state (can be multiple, comma-separated)</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed state</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --state $(echo $6 | tr , " ")</command> + </tagNode> + </children> + </node> + <node name="statistics"> + <properties> + <help>Show DHCP server statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics</command> + <children> + <tagNode name="pool"> + <properties> + <help>Show DHCP server statistics for a specific pool</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed pool</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $6</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Show DHCPv6 (IPv6 Dynamic Host Configuration Protocol) information</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Show DHCPv6 server information</help> + </properties> + <children> + <node name="leases"> + <properties> + <help>Show DHCPv6 server leases</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases</command> + <children> + <tagNode name="pool"> + <properties> + <help>Show DHCPv6 server leases for a specific pool</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed pool</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --pool $6</command> + </tagNode> + <tagNode name="sort"> + <properties> + <help>Show DHCPv6 server leases sorted by the specified key</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed sort</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --sort $6</command> + </tagNode> + <tagNode name="state"> + <properties> + <help>Show DHCPv6 server leases with a specific state (can be multiple, comma-separated)</help> + <completionHelp> + <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed state</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $(echo $6 | tr , " ")</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="dhcp"> + <properties> + <help>Restart DHCP processes</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo systemctl restart isc-dhcp-server.service</command> + </node> + <node name="relay-agent"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv4</command> + </node> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Restart DHCPv6 processes</help> + </properties> + <children> + <node name="server"> + <properties> + <help>Restart the DHCPv6 server process</help> + </properties> + <command>sudo systemctl restart isc-dhcp-server6.service</command> + </node> + <node name="relay-agent"> + <properties> + <help>Restart the DHCP server process</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_dhcp_relay.py --ipv6</command> + </node> + </children> + </node> + </children> + </node> + <node name="renew"> + <properties> + <help>Renew specified variable</help> + </properties> + <children> + <node name="dhcp"> + <properties> + <help>Renew DHCP client lease</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Renew DHCP client lease for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>sudo systemctl restart "dhclient@$4.service"</command> + </tagNode> + </children> + </node> + <node name="dhcpv6"> + <properties> + <help>Renew DHCPv6 client lease</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Renew DHCPv6 client lease for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>sudo systemctl restart "dhcp6c@$4.service"</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/disconnect.xml b/op-mode-definitions/disconnect.xml new file mode 100644 index 000000000..bf2c37b89 --- /dev/null +++ b/op-mode-definitions/disconnect.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="disconnect"> + <properties> + <help>Take down a connection</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Take down a connection-oriented network interface</help> + <completionHelp> + <path>interfaces pppoe</path> + <path>interfaces wirelessmodem</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/disks.xml b/op-mode-definitions/disks.xml new file mode 100644 index 000000000..fb39c4f3c --- /dev/null +++ b/op-mode-definitions/disks.xml @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="format"> + <properties> + <help>Format a device</help> + </properties> + <children> + <tagNode name="disk"> + <properties> + <help>Format a disk drive</help> + <completionHelp> + <script>${vyos_completion_dir}/list_disks.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="like"> + <properties> + <help>Format this disk the same as another disk</help> + <completionHelp> + <script>${vyos_completion_dir}/list_disks.py --exclude ${COMP_WORDS[2]}</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/format_disk.py --target $3 --proto $5</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + + <node name="show"> + <children> + <tagNode name="disk"> + <properties> + <help>Show status of disk device</help> + <completionHelp> + <script>${vyos_completion_dir}/list_disks.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="format"> + <properties> + <help>Show disk drive formatting</help> + </properties> + <command>${vyos_op_scripts_dir}/show_disk_format.sh $3</command> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dns-dynamic.xml b/op-mode-definitions/dns-dynamic.xml new file mode 100644 index 000000000..9c37874fb --- /dev/null +++ b/op-mode-definitions/dns-dynamic.xml @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="log"> + <children> + <node name="dns"> + <children> + <node name="dynamic"> + <properties> + <help>Show log for dynamic DNS</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "ddclient"</command> + </node> + </children> + </node> + </children> + </node> + <node name="dns"> + <properties> + <help>Show DNS information</help> + </properties> + <children> + <node name="dynamic"> + <properties> + <help>Show Dynamic DNS information</help> + </properties> + <children> + <leafNode name="status"> + <properties> + <help>Show Dynamic DNS status</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --status</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="dns"> + <children> + <node name="dynamic"> + <properties> + <help>Restart Dynamic DNS service</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command> + </node> + </children> + </node> + </children> + </node> + <node name="update"> + <properties> + <help>Update data for a service</help> + </properties> + <children> + <node name="dns"> + <properties> + <help>Update DNS information</help> + </properties> + <children> + <node name="dynamic"> + <properties> + <help>Update Dynamic DNS information</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/dns-forwarding.xml b/op-mode-definitions/dns-forwarding.xml new file mode 100644 index 000000000..23de97704 --- /dev/null +++ b/op-mode-definitions/dns-forwarding.xml @@ -0,0 +1,94 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="log"> + <children> + <node name="dns"> + <properties> + <help>Show log for Domain Name Service (DNS)</help> + </properties> + <children> + <node name="forwarding"> + <properties> + <help>Show log for DNS Forwarding</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "pdns_recursor"</command> + </node> + </children> + </node> + </children> + </node> + <node name="dns"> + <properties> + <help>Show DNS information</help> + </properties> + <children> + <node name="forwarding"> + <properties> + <help>Show DNS forwarding information</help> + </properties> + <children> + <leafNode name="statistics"> + <properties> + <help>Show DNS forwarding statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_statistics.py</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="dns"> + <properties> + <help>Restart a DNS service</help> + </properties> + <children> + <leafNode name="forwarding"> + <properties> + <help>Restart DNS forwarding service</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_restart.sh</command> + </leafNode> + </children> + </node> + </children> + </node> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="dns"> + <properties> + <help>Reset a DNS service state</help> + </properties> + <children> + <node name="forwarding"> + <properties> + <help>Reset DNS forwarding cache</help> + </properties> + <children> + <tagNode name="domain"> + <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py $5</command> + <properties> + <help>Reset DNS forwarding cache for a domain</help> + </properties> + </tagNode> + <leafNode name="all"> + <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py --all</command> + <properties> + <help>Reset DNS forwarding cache</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/flow-accounting-op.xml b/op-mode-definitions/flow-accounting-op.xml new file mode 100644 index 000000000..912805d59 --- /dev/null +++ b/op-mode-definitions/flow-accounting-op.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- flow-accounting op mode commands --> +<interfaceDefinition> + <node name="show"> + <children> + <node name="flow-accounting"> + <properties> + <help>Show flow accounting statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show flow accounting statistics for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4</command> + <children> + <tagNode name="host"> + <properties> + <help>Show flow accounting statistics for specified interface/host</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4 --host $6</command> + </tagNode> + <tagNode name="port"> + <properties> + <help>Show flow accounting statistics for specified interface/port</help> + <completionHelp> + <list>1-65535</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4 --ports $6</command> + </tagNode> + <tagNode name="top"> + <properties> + <help>Show top N flows for specified interface</help> + <completionHelp> + <list>1-100</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action show --interface $4 --top $6</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <leafNode name="flow-accounting"> + <properties> + <help>Restart flow-accounting service</help> + </properties> + <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action restart</command> + </leafNode> + </children> + </node> + <node name="clear"> + <children> + <node name="flow-accounting"> + <properties> + <help>Clear flow accounting</help> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear flow accounting statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/flow_accounting_op.py --action clear</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-arp.xml b/op-mode-definitions/force-arp.xml new file mode 100644 index 000000000..c7bcad413 --- /dev/null +++ b/op-mode-definitions/force-arp.xml @@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <properties> + <help>Force an operation</help> + </properties> + <children> + <node name="arp"> + <properties> + <help>Send gratuitous ARP request or reply</help> + </properties> + <children> + <node name="reply"> + <properties> + <help>Send gratuitous ARP reply</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Send gratuitous ARP reply on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Send gratuitous ARP reply for specified address</help> + </properties> + <command>sudo /usr/bin/arping -I $5 -c 1 -A $7</command> + <children> + <tagNode name="count"> + <properties> + <help>Send specified number of ARP replies</help> + </properties> + <command>sudo /usr/bin/arping -I $5 -c $9 -A $7</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + <node name="request"> + <properties> + <help>Send gratuitous ARP request</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Send gratuitous ARP request on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Send gratuitous ARP request for specified address</help> + </properties> + <command>sudo /usr/bin/arping -I $5 -c 1 -U $7</command> + <children> + <tagNode name="count"> + <properties> + <help>Send specified number of ARP requests</help> + </properties> + <command>sudo /usr/bin/arping -I $5 -c $9 -U $7</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-ipv6-nd.xml b/op-mode-definitions/force-ipv6-nd.xml new file mode 100644 index 000000000..49de097f6 --- /dev/null +++ b/op-mode-definitions/force-ipv6-nd.xml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <node name="ipv6-nd"> + <properties> + <help>IPv6 Neighbor Discovery</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IPv6 Neighbor Discovery on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>IPv6 address of node to lookup</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/ndisc6 -m "$6" "$4"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-ipv6-rd.xml b/op-mode-definitions/force-ipv6-rd.xml new file mode 100644 index 000000000..8c901af25 --- /dev/null +++ b/op-mode-definitions/force-ipv6-rd.xml @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <node name="ipv6-rd"> + <properties> + <help>IPv6 Router Discovery</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IPv6 Router Discovery on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>/usr/bin/rdisc6 "$4"</command> + <children> + <tagNode name="address"> + <properties> + <help>IPv6 address of target</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/rdisc6 -m "$6" "$4"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/generate-macsec-key.xml b/op-mode-definitions/generate-macsec-key.xml new file mode 100644 index 000000000..40d2b9061 --- /dev/null +++ b/op-mode-definitions/generate-macsec-key.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="macsec"> + <properties> + <help>Generate MACsec Key</help> + </properties> + <children> + <node name="mka-cak"> + <properties> + <help>Generate MACsec connectivity association key (CAK)</help> + </properties> + <command>/usr/bin/hexdump -n 16 -e '4/4 "%08x" 1 "\n"' /dev/random</command> + </node> + <node name="mka-ckn"> + <properties> + <help>Generate MACsec connectivity association name (CKN)</help> + </properties> + <command>/usr/bin/hexdump -n 32 -e '8/4 "%08x" 1 "\n"' /dev/random</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/generate-ssh-server-key.xml b/op-mode-definitions/generate-ssh-server-key.xml new file mode 100644 index 000000000..a6ebf1b78 --- /dev/null +++ b/op-mode-definitions/generate-ssh-server-key.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <properties> + <help>Generate an object</help> + </properties> + <children> + <node name="ssh-server-key"> + <properties> + <help>Regenerate the host SSH keys and restart the SSH server</help> + </properties> + <command>${vyos_op_scripts_dir}/generate_ssh_server_key.py</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/igmp-proxy.xml b/op-mode-definitions/igmp-proxy.xml new file mode 100644 index 000000000..8533138d7 --- /dev/null +++ b/op-mode-definitions/igmp-proxy.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="restart"> + <children> + <node name="igmp-proxy"> + <properties> + <help>Restart the IGMP proxy process</help> + </properties> + <command>sudo systemctl restart igmpproxy.service</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ipoe-server.xml b/op-mode-definitions/ipoe-server.xml new file mode 100644 index 000000000..c20d3aa2a --- /dev/null +++ b/op-mode-definitions/ipoe-server.xml @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <children> + <node name="ipoe-server"> + <properties> + <help>Clear ipoe-server sessions or process</help> + </properties> + <children> + <node name="session"> + <properties> + <help>Clear ipoe-server session</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>Clear ipoe-server session by username</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ipoe.py --selector="username"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ipoe-control.py --action="terminate" --selector="username" --target="$5"</command> + </tagNode> + <tagNode name="sid"> + <properties> + <help>Clear ipoe-server session by Session ID</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ipoe.py --selector="sid"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ipoe-control.py --action="terminate" --selector="sid" --target="$5"</command> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Clear ipoe-server session by interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ipoe.py --selector="ifname"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ipoe-control.py --action="terminate" --selector="if" --target="$5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="ipoe-server"> + <properties> + <help>show ipoe-server status</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active IPoE server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/ipoe-control.py --action="show_sessions"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show IPoE server statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/ipoe-control.py --action="show_stat"</command> + </leafNode> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <leafNode name="ipoe-server"> + <properties> + <help>show ipoe-server status</help> + </properties> + <command>${vyos_op_scripts_dir}/ipoe-control.py --action="restart"</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ipv4-route.xml b/op-mode-definitions/ipv4-route.xml new file mode 100644 index 000000000..1bda3ac11 --- /dev/null +++ b/op-mode-definitions/ipv4-route.xml @@ -0,0 +1,87 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <properties> + <help>Show system information</help> + </properties> + <children> + <node name="ip"> + <properties> + <help>Show IPv4 information</help> + </properties> + <children> + <leafNode name="groups"> + <properties> + <help>Show IP multicast group membership</help> + </properties> + <command>netstat -gn4</command> + </leafNode> + </children> + </node> + </children> + </node> + + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="ip"> + <properties> + <help>Reset Internet Protocol (IP) parameters</help> + </properties> + <children> + <node name="arp"> + <properties> + <help>Reset Address Resolution Protocol (ARP) cache</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Reset ARP cache for an IPv4 address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>sudo /sbin/ip neigh flush to "$5"</command> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Reset ARP cache for interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>sudo /sbin/ip neigh flush dev "$5"</command> + </tagNode> + </children> + </node> + + <node name="route"> + <properties> + <help>Reset IP route</help> + </properties> + <children> + <leafNode name= "cache"> + <properties> + <help>Flush the kernel route cache</help> + </properties> + <command>sudo /sbin/ip route flush cache</command> + </leafNode> + + <tagNode name="cache"> + <properties> + <help>Flush the kernel route cache for a given route</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>sudo /sbin/ip route flush cache "$5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ipv6-route.xml b/op-mode-definitions/ipv6-route.xml new file mode 100644 index 000000000..fbf6489ba --- /dev/null +++ b/op-mode-definitions/ipv6-route.xml @@ -0,0 +1,133 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <properties> + <help>Show system information</help> + </properties> + <children> + <node name="ipv6"> + <properties> + <help>Show IPv6 routing information</help> + </properties> + <children> + <leafNode name="groups"> + <properties> + <help>Show IPv6 multicast group membership</help> + </properties> + <command>netstat -gn6</command> + </leafNode> + + <leafNode name="neighbors"> + <properties> + <help>Show IPv6 Neighbor Discovery (ND) information</help> + </properties> + <command>ip -f inet6 neigh list</command> + </leafNode> + + <node name="route"> + <properties> + <help>Show IPv6 routes</help> + </properties> + <children> + <node name="cache"> + <properties> + <help>Show kernel IPv6 route cache</help> + </properties> + <command>ip -s -f inet6 route list cache</command> + </node> + <tagNode name="cache"> + <properties> + <help>Show kernel IPv6 route cache for a given route</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>ip -s -f inet6 route list cache $5</command> + </tagNode> + <node name="forward"> + <properties> + <help>Show kernel IPv6 route table</help> + </properties> + <command>ip -f inet6 route list</command> + </node> + <tagNode name="forward"> + <properties> + <help>Show kernel IPv6 route table for a given route</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>ip -s -f inet6 route list $5</command> + </tagNode> + </children> + </node> + + </children> + </node> + </children> + </node> + + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="ipv6"> + <properties> + <help>Reset Internet Protocol version 6 (IPv6) parameters</help> + </properties> + <children> + <node name="neighbors"> + <properties> + <help>Reset IPv6 Neighbor Discovery (ND) cache</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Reset ND cache for an IPv6 address</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ip -f inet6 neigh flush to "$5"</command> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Reset IPv6 ND cache for interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>sudo ip -f inet6 neigh flush dev "$5"</command> + </tagNode> + </children> + </node> + + <node name="route"> + <properties> + <help>Reset IPv6 route</help> + </properties> + <children> + <leafNode name= "cache"> + <properties> + <help>Flush the kernel IPv6 route cache</help> + </properties> + <command>sudo ip -f inet6 route flush cache</command> + </leafNode> + + <tagNode name="cache"> + <properties> + <help>Flush the kernel IPv6 route cache for a given route</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>sudo ip -f inet6 route flush cache "$5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/l2tp-server.xml b/op-mode-definitions/l2tp-server.xml new file mode 100644 index 000000000..3e96b9365 --- /dev/null +++ b/op-mode-definitions/l2tp-server.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="l2tp-server"> + <properties> + <help>Show L2TP server information</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active L2TP server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="l2tp" --action="show sessions"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show L2TP server statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="l2tp" --action="show stat"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/lldp.xml b/op-mode-definitions/lldp.xml new file mode 100644 index 000000000..297ccf1f4 --- /dev/null +++ b/op-mode-definitions/lldp.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="lldp"> + <properties> + <help>Show LLDP (Link Layer Discovery Protocol)</help> + </properties> + <children> + <node name="neighbors"> + <properties> + <help>Show LLDP neighbors</help> + </properties> + <command>${vyos_op_scripts_dir}/lldp_op.py --all</command> + <children> + <node name="detail"> + <properties> + <help>Show LLDP neighbor details</help> + </properties> + <command>${vyos_op_scripts_dir}/lldp_op.py --detail</command> + </node> + <tagNode name="interface"> + <properties> + <help>Show LLDP for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/lldp_op.py --interface $5</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-bandwidth-test.xml b/op-mode-definitions/monitor-bandwidth-test.xml new file mode 100644 index 000000000..d1e459b17 --- /dev/null +++ b/op-mode-definitions/monitor-bandwidth-test.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="bandwidth-test"> + <properties> + <help>Initiate or wait for bandwidth test</help> + </properties> + <children> + <leafNode name="accept"> + <properties> + <help>Wait for bandwidth test connections (port TCP/5001)</help> + </properties> + <command>iperf -s</command> + </leafNode> + <tagNode name="initiate"> + <properties> + <help>Initiate a bandwidth test to specified host (port TCP/5001)</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>iperf -c $4</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-bandwidth.xml b/op-mode-definitions/monitor-bandwidth.xml new file mode 100644 index 000000000..9af0a9e70 --- /dev/null +++ b/op-mode-definitions/monitor-bandwidth.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="bandwidth"> + <properties> + <help>Monitor interface bandwidth in real time</help> + </properties> + <children> + <tagNode name="interface"> + <command>bmon -b -p $4</command> + <properties> + <help>Monitor bandwidth usage on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-log.xml b/op-mode-definitions/monitor-log.xml new file mode 100644 index 000000000..99efe5306 --- /dev/null +++ b/op-mode-definitions/monitor-log.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="log"> + <properties> + <help>Monitor last lines of messages file</help> + </properties> + <command>tail --follow=name /var/log/messages</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/monitor-ndp.xml b/op-mode-definitions/monitor-ndp.xml new file mode 100644 index 000000000..1ac6ce39b --- /dev/null +++ b/op-mode-definitions/monitor-ndp.xml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="ndp"> + <properties> + <help>Monitor the NDP information received by the router through the device</help> + </properties> + <command>sudo ndptool monitor</command> + <children> + <tagNode name="interface"> + <command>sudo ndptool monitor --ifname=$4</command> + <properties> + <help>Monitor ndp protocol on specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="type"> + <command>sudo ndptool monitor --ifname=$4 --msg-type=$6</command> + <properties> + <help>Monitor specific types of NDP protocols</help> + <completionHelp> + <list>rs ra ns na</list> + </completionHelp> + </properties> + </tagNode> + </children> + </tagNode> + <tagNode name="type"> + <command>sudo ndptool monitor --msg-type=$4</command> + <properties> + <help>Monitor specific types of NDP protocols</help> + <completionHelp> + <list>rs ra ns na</list> + </completionHelp> + </properties> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/nat.xml b/op-mode-definitions/nat.xml new file mode 100644 index 000000000..f6c0fa748 --- /dev/null +++ b/op-mode-definitions/nat.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="nat"> + <properties> + <help>Show Network Address Translation (NAT) information</help> + </properties> + <children> + <node name="source"> + <properties> + <help>Show source Network Address Translation (NAT) information</help> + </properties> + <children> + <node name="rules"> + <properties> + <help>Show configured source NAT rules</help> + </properties> + <command>echo To be migrated to Python - https://phabricator.vyos.net/T2459</command> + </node> + <node name="statistics"> + <properties> + <help>Show statistics for configured source NAT rules</help> + </properties> + <command>${vyos_op_scripts_dir}/show_nat_statistics.py --source</command> + </node> + <node name="translations"> + <properties> + <help>Show active source NAT translations</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Show active source NAT translations for an IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose --ipaddr="$6"</command> + </tagNode> + <node name="detail"> + <properties> + <help>Show active source NAT translations detail</help> + </properties> + <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose</command> + </node> + </children> + <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source</command> + </node> + </children> + </node> + <node name="destination"> + <properties> + <help>Show destination Network Address Translation (NAT) information</help> + </properties> + <children> + <node name="rules"> + <properties> + <help>Show configured destination NAT rules</help> + </properties> + <command>echo To be migrated to Python - https://phabricator.vyos.net/T2459</command> + </node> + <node name="statistics"> + <properties> + <help>Show statistics for configured destination NAT rules</help> + </properties> + <command>${vyos_op_scripts_dir}/show_nat_statistics.py --destination</command> + </node> + <node name="translations"> + <properties> + <help>Show active destination NAT translations</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Show active NAT destination translations for an IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose --ipaddr="$6"</command> + </tagNode> + <node name="detail"> + <properties> + <help>Show active destination NAT translations detail</help> + </properties> + <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose</command> + </node> + </children> + <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml new file mode 100644 index 000000000..b9cb06dca --- /dev/null +++ b/op-mode-definitions/openvpn.xml @@ -0,0 +1,140 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="openvpn"> + <properties> + <help>OpenVPN key generation tool</help> + </properties> + <children> + <tagNode name="key"> + <properties> + <help>Generate shared-secret key with specified file name</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command> + result=1; + key_path=$4 + full_path= + + # Prepend /config/auth if the path is not absolute + if echo $key_path | egrep -ve '^/.*' > /dev/null; then + full_path=/config/auth/$key_path + else + full_path=$key_path + fi + + key_dir=`dirname $full_path` + if [ ! -d $key_dir ]; then + echo "Directory $key_dir does not exist!" + exit 1 + fi + + echo "Generating OpenVPN key to $full_path" + sudo /usr/sbin/openvpn --genkey --secret "$full_path" + result=$? + if [ $result = 0 ]; then + echo "Your new local OpenVPN key has been generated" + fi + /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path" + </command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="openvpn"> + <children> + <tagNode name="client"> + <properties> + <help>Reset specified OpenVPN client</help> + <completionHelp> + <script>sudo ${vyos_completion_dir}/list_openvpn_clients.py --all</script> + </completionHelp> + </properties> + <command>echo kill $4 | socat - UNIX-CONNECT:/run/openvpn/openvpn-mgmt-intf > /dev/null</command> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Reset OpenVPN process on interface</help> + <completionHelp> + <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_openvpn.py $4</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <node name="openvpn"> + <properties> + <help>Show OpenVPN interface information</help> + </properties> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed OpenVPN interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=openvpn --action=show</command> + </leafNode> + </children> + </node> + <tagNode name="openvpn"> + <properties> + <help>Show OpenVPN interface information</help> + <completionHelp> + <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf=$4</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of specified OpenVPN interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + <node name="openvpn"> + <properties> + <help>Show OpenVPN information</help> + </properties> + <children> + <leafNode name="client"> + <properties> + <help>Show tunnel status for OpenVPN client interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client</command> + </leafNode> + <leafNode name="server"> + <properties> + <help>Show tunnel status for OpenVPN server interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server</command> + </leafNode> + <leafNode name="site-to-site"> + <properties> + <help>Show tunnel status for OpenVPN site-to-site interfaces</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/ping.xml b/op-mode-definitions/ping.xml new file mode 100644 index 000000000..4c25a59ab --- /dev/null +++ b/op-mode-definitions/ping.xml @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <tagNode name="ping"> + <properties> + <help>Send Internet Control Message Protocol (ICMP) echo request</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ping.py ${@:2}</command> + <children> + <leafNode name="node.tag"> + <properties> + <help>Ping options</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/ping.py --get-options "${COMP_WORDS[@]}"</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ping.py ${@:2}</command> + </leafNode> + </children> + </tagNode> +</interfaceDefinition> diff --git a/op-mode-definitions/poweroff.xml b/op-mode-definitions/poweroff.xml new file mode 100644 index 000000000..b4163bcb9 --- /dev/null +++ b/op-mode-definitions/poweroff.xml @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="poweroff"> + <properties> + <help>Poweroff the system</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --poweroff</command> + <children> + <leafNode name="now"> + <properties> + <help>Poweroff the system without confirmation</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff</command> + </leafNode> + <leafNode name="cancel"> + <properties> + <help>Cancel a pending poweroff</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --cancel</command> + </leafNode> + <tagNode name="in"> + <properties> + <help>Poweroff in X minutes</help> + <completionHelp> + <list><Minutes></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $4</command> + </tagNode> + <tagNode name="at"> + <properties> + <help>Poweroff at a specific time</help> + <completionHelp> + <list><HH:MM></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3</command> + <children> + <tagNode name="date"> + <properties> + <help>Poweroff at a specific date</help> + <completionHelp> + <list><DDMMYYYY> <DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --poweroff $3 $5</command> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/pppoe-server.xml b/op-mode-definitions/pppoe-server.xml new file mode 100644 index 000000000..5ac9d9497 --- /dev/null +++ b/op-mode-definitions/pppoe-server.xml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="pppoe-server"> + <properties> + <help>Show pppoe-server status</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active PPPoE server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="show sessions"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show PPPoE server statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="show stat"</command> + </leafNode> + <leafNode name="interfaces"> + <properties> + <help>Show interfaces where pppoe-server listens on</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="pppoe interface show"</command> + </leafNode> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <leafNode name="pppoe-server"> + <properties> + <help>Restarts pppoe-server</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="restart"</command> + </leafNode> + </children> + </node> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="pppoe-server"> + <properties> + <help>Reset PPPoE server sessions</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Terminate all pppoe-server users</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="terminate all"</command> + </leafNode> + <tagNode name="interface"> + <properties> + <help>Terminate a ppp interface</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="terminate if $4"</command> + </tagNode> + <tagNode name="username"> + <properties> + <help>Terminate specified users</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="terminate username $4"</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="set"> + <children> + <node name="pppoe-server"> + <properties> + <help>Set PPPoE server maintenance mode</help> + </properties> + <children> + <node name="maintenance-mode"> + <properties> + <help>Set PPPoE server maintenance mode</help> + </properties> + <children> + <leafNode name="enable"> + <properties> + <help>Deny new connections and stop to serve pppoe after disconnect last session</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="shutdown soft"</command> + </leafNode> + <leafNode name="cancel"> + <properties> + <help>Cancel maintenance mode</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pppoe" --action="shutdown cancel"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/pptp-server.xml b/op-mode-definitions/pptp-server.xml new file mode 100644 index 000000000..59be68611 --- /dev/null +++ b/op-mode-definitions/pptp-server.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="pptp-server"> + <properties> + <help>Show PPTP server information</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active PPTP server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pptp" --action="show sessions"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show PPTP server statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="pptp" --action="show stat"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reboot.xml b/op-mode-definitions/reboot.xml new file mode 100644 index 000000000..2c8daec5d --- /dev/null +++ b/op-mode-definitions/reboot.xml @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reboot"> + <properties> + <help>Reboot the system</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --reboot</command> + <children> + <leafNode name="now"> + <properties> + <help>Reboot the system without confirmation</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot</command> + </leafNode> + <leafNode name="cancel"> + <properties> + <help>Cancel a pending reboot</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --cancel</command> + </leafNode> + <tagNode name="in"> + <properties> + <help>Reboot in X minutes</help> + <completionHelp> + <list><Minutes></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $4</command> + </tagNode> + <tagNode name="at"> + <properties> + <help>Reboot at a specific time</help> + <completionHelp> + <list><HH:MM></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3</command> + <children> + <tagNode name="date"> + <properties> + <help>Reboot at a specific date</help> + <completionHelp> + <list><DDMMYYYY> <DD/MM/YYYY> <DD.MM.YYYY> <DD:MM:YYYY></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/powerctrl.py --yes --reboot $3 $5</command> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reset-conntrack.xml b/op-mode-definitions/reset-conntrack.xml new file mode 100644 index 000000000..827ba4af4 --- /dev/null +++ b/op-mode-definitions/reset-conntrack.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="conntrack"> + <properties> + <help>Reset all currently tracked connections</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/clear_conntrack.py</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reset-ip-igmp.xml b/op-mode-definitions/reset-ip-igmp.xml new file mode 100644 index 000000000..143553d33 --- /dev/null +++ b/op-mode-definitions/reset-ip-igmp.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <children> + <node name="ip"> + <children> + <node name="igmp"> + <properties> + <help>IGMP clear commands</help> + </properties> + <children> + <leafNode name="interfaces"> + <properties> + <help>Reset IGMP interfaces</help> + </properties> + <command>/usr/bin/vtysh -c "clear ip igmp interfaces"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reset-ip-multicast.xml b/op-mode-definitions/reset-ip-multicast.xml new file mode 100644 index 000000000..d610add16 --- /dev/null +++ b/op-mode-definitions/reset-ip-multicast.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <children> + <node name="ip"> + <children> + <node name="multicast"> + <properties> + <help>IP multicast routing table</help> + </properties> + <children> + <leafNode name="route"> + <properties> + <help>Clear multicast routing table</help> + </properties> + <command>/usr/bin/vtysh -c "clear ip mroute"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reset-vpn.xml b/op-mode-definitions/reset-vpn.xml new file mode 100644 index 000000000..ae553c272 --- /dev/null +++ b/op-mode-definitions/reset-vpn.xml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="vpn"> + <properties> + <help>Reset Virtual Private Network (VPN) information</help> + </properties> + <children> + <node name="remote-access"> + <properties> + <help>Reset remote access VPN connections</help> + </properties> + <children> + <node name="all"> + <properties> + <help>Terminate all user's current remote access VPN session(s)</help> + </properties> + <children> + <node name="protocol"> + <properties> + <help>Terminate specified user's current remote access VPN session(s) with specified protocol</help> + </properties> + <children> + <leafNode name="l2tp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with L2TP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="l2tp"</command> + </leafNode> + <leafNode name="pptp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with PPTP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="pptp"</command> + </leafNode> + <leafNode name="sstp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with SSTP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users" --protocol="sstp"</command> + </leafNode> + </children> + </node> + </children> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="all_users"</command> + </node> + <tagNode name="interface"> + <properties> + <help>Terminate a remote access VPN interface</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --interface="$5"</command> + </tagNode> + <tagNode name="user"> + <properties> + <help>Terminate specified user's current remote access VPN session(s)</help> + </properties> + <children> + <node name="protocol"> + <properties> + <help>Terminate specified user's current remote access VPN session(s) with specified protocol</help> + </properties> + <children> + <leafNode name="l2tp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with L2TP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="l2tp"</command> + </leafNode> + <leafNode name="pptp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with PPTP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="pptp"</command> + </leafNode> + <leafNode name="sstp"> + <properties> + <help>Terminate all user's current remote access VPN session(s) with SSTP protocol</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5" --protocol="sstp"</command> + </leafNode> + </children> + </node> + </children> + <command>sudo ${vyos_op_scripts_dir}/reset_vpn.py --username="$5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/restart-frr.xml b/op-mode-definitions/restart-frr.xml new file mode 100644 index 000000000..96ad1a650 --- /dev/null +++ b/op-mode-definitions/restart-frr.xml @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="restart"> + <children> + <node name="frr"> + <properties> + <help>Restart FRRouting daemons</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart</command> + <children> + <leafNode name="bfdd"> + <properties> + <help>Restart Bidirectional Forwarding Detection daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bfdd</command> + </leafNode> + <leafNode name="bgpd"> + <properties> + <help>Restart Border Gateway Protocol daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon bgpd</command> + </leafNode> + <leafNode name="ospfd"> + <properties> + <help>Restart OSPFv2 daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospfd</command> + </leafNode> + <leafNode name="ospf6d"> + <properties> + <help>Restart OSPFv3 daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ospf6d</command> + </leafNode> + <leafNode name="ripd"> + <properties> + <help>Restart Routing Information Protocol daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripd</command> + </leafNode> + <leafNode name="ripngd"> + <properties> + <help>Restart RIPng daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon ripngd</command> + </leafNode> + <leafNode name="staticd"> + <properties> + <help>Restart Static Route daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon staticd</command> + </leafNode> + <leafNode name="zebra"> + <properties> + <help>Restart IP routing manager daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart_frr.py --action restart --daemon zebra</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-acceleration.xml b/op-mode-definitions/show-acceleration.xml new file mode 100644 index 000000000..d0dcea2d6 --- /dev/null +++ b/op-mode-definitions/show-acceleration.xml @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="system"> + <properties> + <help>Show system information</help> + </properties> + <children> + <node name="acceleration"> + <properties> + <help>Acceleration components</help> + </properties> + <children> + <node name="qat"> + <properties> + <help>Intel QAT (Quick Assist Technology) Devices</help> + </properties> + <children> + <tagNode name="device"> + <properties> + <help>Show QAT information for a given acceleration device</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/show_acceleration.py --dev_list</script> + </completionHelp> + </properties> + <children> + <node name="flows"> + <properties> + <help>Intel QAT flows</help> + </properties> + <command>${vyos_op_scripts_dir}/show_acceleration.py --flow --dev $6</command> + </node> + <node name="config"> + <properties> + <help>Intel QAT configuration</help> + </properties> + <command>${vyos_op_scripts_dir}/show_acceleration.py --conf --dev $6</command> + </node> + </children> + </tagNode> + <node name="status"> + <properties> + <help>Intel QAT status</help> + </properties> + <command>${vyos_op_scripts_dir}/show_acceleration.py --status</command> + </node> + <node name="interrupts"> + <properties> + <help>Intel QAT interrupts</help> + </properties> + <command>${vyos_op_scripts_dir}/show_acceleration.py --interrupts</command> + </node> + </children> + <command>${vyos_op_scripts_dir}/show_acceleration.py --hw</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-bridge.xml b/op-mode-definitions/show-bridge.xml new file mode 100644 index 000000000..8c1f7c398 --- /dev/null +++ b/op-mode-definitions/show-bridge.xml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <leafNode name="bridge"> + <properties> + <help>Show bridging information</help> + </properties> + <command>/sbin/brctl show</command> + </leafNode> + <tagNode name="bridge"> + <properties> + <help>Show bridge information for a given bridge interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script> + </completionHelp> + </properties> + <command>/sbin/brctl show $3</command> + <children> + <leafNode name="macs"> + <properties> + <help>Show bridge Media Access Control (MAC) address table</help> + </properties> + <command>/sbin/brctl showmacs $3</command> + </leafNode> + <leafNode name="spanning-tree"> + <properties> + <help>Show bridge spanning tree information</help> + </properties> + <command>/sbin/brctl showstp $3</command> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-configuration.xml b/op-mode-definitions/show-configuration.xml new file mode 100644 index 000000000..318942ab0 --- /dev/null +++ b/op-mode-definitions/show-configuration.xml @@ -0,0 +1,37 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="configuration"> + <properties> + <help>Show available saved configurations</help> + </properties> + <!-- no admin check --> + <command>cli-shell-api showCfg --show-active-only --show-hide-secrets</command> + <children> + <node name="all"> + <properties> + <help>Show running configuration (including default values)</help> + </properties> + <!-- no admin check --> + <command>cli-shell-api showCfg --show-show-defaults --show-active-only --show-hide-secrets</command> + </node> + <node name="commands"> + <properties> + <help> Show running configuration as set commands </help> + </properties> + <!-- no admin check --> + <command>cli-shell-api showCfg --show-active-only | vyos-config-to-commands</command> + </node> + <node name="files"> + <properties> + <help> Show available saved configurations </help> + </properties> + <!-- no admin check --> + <command>${vyos_op_scripts_dir}/show_configuration_files.sh</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-console-server.xml b/op-mode-definitions/show-console-server.xml new file mode 100644 index 000000000..77a7f3376 --- /dev/null +++ b/op-mode-definitions/show-console-server.xml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="log"> + <children> + <leafNode name="console-server"> + <properties> + <help>Show log for serial console server</help> + </properties> + <command>/usr/bin/journalctl -u conserver-server.service</command> + </leafNode> + </children> + </node> + <node name="console-server"> + <properties> + <help>Show Console-Server information</help> + </properties> + <children> + <leafNode name="ports"> + <properties> + <help>Examine console ports and configured baud rates</help> + </properties> + <command>/usr/bin/console -x</command> + </leafNode> + <leafNode name="user"> + <properties> + <help>Show users on various consoles</help> + </properties> + <command>/usr/bin/console -u</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-environment.xml b/op-mode-definitions/show-environment.xml new file mode 100644 index 000000000..95b658785 --- /dev/null +++ b/op-mode-definitions/show-environment.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="environment"> + <properties> + <help>Show current system environmental conditions</help> + </properties> + <children> + <leafNode name="sensors"> + <properties> + <help>Show hardware monitoring results</help> + </properties> + <!-- Linux always adds "hypervisor" to CPU flags --> + <command>if ! grep -q hypervisor /proc/cpuinfo; then ${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_sensors.py; else echo "VyOS running under hypervisor, no sensors available"; fi</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-hardware.xml b/op-mode-definitions/show-hardware.xml new file mode 100644 index 000000000..c3ff3a60f --- /dev/null +++ b/op-mode-definitions/show-hardware.xml @@ -0,0 +1,94 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="hardware"> + <properties> + <help>Show system hardware details</help> + </properties> + <children> + <node name="cpu"> + <properties> + <help>Show CPU info</help> + </properties> + <command>lscpu</command> + <children> + <node name="detail"> + <properties> + <help> Show system CPU details</help> + </properties> + <command>cat /proc/cpuinfo</command> + </node> + <node name="summary"> + <properties> + <help>Show CPU's on system</help> + </properties> + <command>${vyos_op_scripts_dir}/cpu_summary.py</command> + </node> + </children> + </node> + <node name="dmi"> + <properties> + <help>Show system DMI details</help> + </properties> + <command>${vyatta_bindir}/vyatta-show-dmi</command> + </node> + <node name="mem"> + <properties> + <help>Show system RAM details</help> + </properties> + <command>cat /proc/meminfo</command> + </node> + <node name="pci"> + <properties> + <help>Show system PCI bus details</help> + </properties> + <command>lspci</command> + <children> + <node name="detail"> + <properties> + <help>Show verbose system PCI bus details</help> + </properties> + <command>lspci -vvv</command> + </node> + </children> + </node> + <node name="scsi"> + <properties> + <help>Show SCSI device information</help> + </properties> + <command>lsscsi</command> + <children> + <node name="detail"> + <properties> + <help>Show detailed SCSI device information</help> + </properties> + <command>lsscsi -vvv</command> + </node> + </children> + </node> + <node name="usb"> + <properties> + <help>Show peripherals connected to the USB bus</help> + </properties> + <command>/usr/bin/lsusb -t</command> + <children> + <node name="detail"> + <properties> + <help>Show detailed USB bus information</help> + </properties> + <command>/usr/bin/lsusb -v</command> + </node> + <leafNode name="serial"> + <properties> + <help>Show information about connected USB serial ports</help> + </properties> + <command>${vyos_op_scripts_dir}/show_usb_serial.py</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-history.xml b/op-mode-definitions/show-history.xml new file mode 100644 index 000000000..7fb286264 --- /dev/null +++ b/op-mode-definitions/show-history.xml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="history"> + <properties> + <help>Show command history</help> + </properties> + <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show recent command history</help> + </properties> + <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history 20</command> + </leafNode> + </children> + </node> + + <tagNode name="history"> + <properties> + <help>Show last N commands in history</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>HISTTIMEFORMAT='%FT%T%z ' HISTFILE="$HOME/.bash_history" \set -o history; history $3</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-host.xml b/op-mode-definitions/show-host.xml new file mode 100644 index 000000000..eee1288a1 --- /dev/null +++ b/op-mode-definitions/show-host.xml @@ -0,0 +1,44 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="host"> + <properties> + <help>Show host information</help> + </properties> + <children> + <leafNode name="date"> + <properties> + <help>Show host current date</help> + </properties> + <command>/bin/date</command> + </leafNode> + <leafNode name="domain"> + <properties> + <help>Show domain name</help> + </properties> + <command>/bin/domainname -d</command> + </leafNode> + <leafNode name="name"> + <properties> + <help>Show host name</help> + </properties> + <command>/bin/hostname</command> + </leafNode> + <tagNode name="lookup"> + <properties> + <help>Lookup host information for hostname|IPv4 address</help> + </properties> + <command>/usr/bin/host $4</command> + </tagNode> + <leafNode name="os"> + <properties> + <help>Show host operating system details</help> + </properties> + <command>/bin/uname -a</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-bonding.xml b/op-mode-definitions/show-interfaces-bonding.xml new file mode 100644 index 000000000..568b215af --- /dev/null +++ b/op-mode-definitions/show-interfaces-bonding.xml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="bonding"> + <properties> + <help>Show bonding interface information</help> + <completionHelp> + <path>interfaces bonding</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified bonding interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + <tagNode name="vif"> + <properties> + <help>Show specified virtual network interface (vif) information</help> + <completionHelp> + <path>interfaces bonding ${COMP_WORDS[3]} vif</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of specified virtual network interface (vif) information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <node name="bonding"> + <properties> + <help>Show bonding interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bonding --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed bonding interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bonding --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-bridge.xml b/op-mode-definitions/show-interfaces-bridge.xml new file mode 100644 index 000000000..85fde95b5 --- /dev/null +++ b/op-mode-definitions/show-interfaces-bridge.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="bridge"> + <properties> + <help>Show bridge interface information</help> + <completionHelp> + <path>interfaces bridge</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified bridge interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="bridge"> + <properties> + <help>Show bridge interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed bridge interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-dummy.xml b/op-mode-definitions/show-interfaces-dummy.xml new file mode 100644 index 000000000..7c24c6921 --- /dev/null +++ b/op-mode-definitions/show-interfaces-dummy.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="dummy"> + <properties> + <help>Show dummy interface information</help> + <completionHelp> + <path>interfaces dummy</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified dummy interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="dummy"> + <properties> + <help>Show dummy interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed dummy interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-ethernet.xml b/op-mode-definitions/show-interfaces-ethernet.xml new file mode 100644 index 000000000..bdcfa55f1 --- /dev/null +++ b/op-mode-definitions/show-interfaces-ethernet.xml @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="ethernet"> + <properties> + <help>Show ethernet interface information</help> + <completionHelp> + <path>interfaces ethernet</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + <leafNode name="identify"> + <properties> + <help>Visually identify specified ethernet interface</help> + </properties> + <command>echo "Blinking interface $4 for 30 seconds."; /sbin/ethtool --identify "$4" 30</command> + </leafNode> + <node name="physical"> + <properties> + <help>Show physical device information for specified ethernet interface</help> + </properties> + <command>/sbin/ethtool "$4"; /sbin/ethtool -i "$4"</command> + <children> + <leafNode name="offload"> + <properties> + <help>Show physical device offloading capabilities</help> + </properties> + <command>/sbin/ethtool -k "$4" | sed -e 1d -e '/fixed/d' -e 's/^\t*//g' -e 's/://' | column -t -s' '</command> + </leafNode> + </children> + </node> + <leafNode name="statistics"> + <properties> + <help>Show physical device statistics for specified ethernet interface</help> + </properties> + <command>/sbin/ethtool -S "$4"</command> + </leafNode> + <leafNode name="transceiver"> + <properties> + <help>Show transceiver information from modules (e.g SFP+, QSFP)</help> + </properties> + <command>/sbin/ethtool -m "$4"</command> + </leafNode> + <tagNode name="vif"> + <properties> + <help>Show specified virtual network interface (vif) information</help> + <completionHelp> + <path>interfaces ethernet ${COMP_WORDS[3]} vif</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of specified virtual network interface (vif) information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <node name="ethernet"> + <properties> + <help>Show ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-input.xml b/op-mode-definitions/show-interfaces-input.xml new file mode 100644 index 000000000..15e8203e5 --- /dev/null +++ b/op-mode-definitions/show-interfaces-input.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="input"> + <properties> + <help>Show input interface information</help> + <completionHelp> + <path>interfaces input</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified input interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="input"> + <properties> + <help>Show input interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=input --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed input interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=input --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-l2tpv3.xml b/op-mode-definitions/show-interfaces-l2tpv3.xml new file mode 100644 index 000000000..60fee34a1 --- /dev/null +++ b/op-mode-definitions/show-interfaces-l2tpv3.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="l2tpv3"> + <properties> + <help>Show L2TPv3 interface information</help> + <completionHelp> + <path>interfaces l2tpv3</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified L2TPv3 interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="l2tpv3"> + <properties> + <help>Show L2TPv3 interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=l2tpv3 --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed L2TPv3 interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=l2tpv3 --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-loopback.xml b/op-mode-definitions/show-interfaces-loopback.xml new file mode 100644 index 000000000..b30b57909 --- /dev/null +++ b/op-mode-definitions/show-interfaces-loopback.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="loopback"> + <properties> + <help>Show loopback interface information</help> + <completionHelp> + <path>interfaces loopback</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified dummy interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="loopback"> + <properties> + <help>Show loopback interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=loopback --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed dummy interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-macsec.xml b/op-mode-definitions/show-interfaces-macsec.xml new file mode 100644 index 000000000..6aeab66af --- /dev/null +++ b/op-mode-definitions/show-interfaces-macsec.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <node name="macsec"> + <properties> + <help>Show MACsec interface information</help> + <completionHelp> + <path>interfaces macsec</path> + </completionHelp> + </properties> + <command>/usr/sbin/ip macsec show</command> + </node> + <tagNode name="macsec"> + <properties> + <help>Show specified MACsec interface information</help> + <completionHelp> + <path>interfaces macsec</path> + </completionHelp> + </properties> + <command>/usr/sbin/ip macsec show $4</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml new file mode 100644 index 000000000..393ca912f --- /dev/null +++ b/op-mode-definitions/show-interfaces-pppoe.xml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="pppoe"> + <properties> + <help>Show PPPoE interface information</help> + <completionHelp> + <path>interfaces pppoe</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="log"> + <properties> + <help>Show specified PPPoE interface log</help> + </properties> + <command>/usr/bin/journalctl -u "ppp@$4".service</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show specified PPPoE interface statistics</help> + <completionHelp> + <path>interfaces pppoe</path> + </completionHelp> + </properties> + <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command> + </leafNode> + </children> + </tagNode> + <node name="pppoe"> + <properties> + <help>Show PPPoE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed PPPoE interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml new file mode 100644 index 000000000..195944745 --- /dev/null +++ b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="pseudo-ethernet"> + <properties> + <help>Show pseudo-ethernet/MACvlan interface information</help> + <completionHelp> + <path>interfaces pseudo-ethernet</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified pseudo-ethernet/MACvlan interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="pseudo-ethernet"> + <properties> + <help>Show pseudo-ethernet/MACvlan interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pseudo-ethernet --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed pseudo-ethernet/MACvlan interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pseudo-ethernet --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-tunnel.xml b/op-mode-definitions/show-interfaces-tunnel.xml new file mode 100644 index 000000000..416de0299 --- /dev/null +++ b/op-mode-definitions/show-interfaces-tunnel.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="tunnel"> + <properties> + <help>Show tunnel interface information</help> + <completionHelp> + <path>interfaces tunnel</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified tunnel interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="tunnel"> + <properties> + <help>Show tunnel interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=tunnel --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed tunnel interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=tunnel --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-vti.xml b/op-mode-definitions/show-interfaces-vti.xml new file mode 100644 index 000000000..f51be2d19 --- /dev/null +++ b/op-mode-definitions/show-interfaces-vti.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="vti"> + <properties> + <help>Show vti interface information</help> + <completionHelp> + <path>interfaces vti</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified vti interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="vti"> + <properties> + <help>Show vti interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vti --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed vti interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vti --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-vxlan.xml b/op-mode-definitions/show-interfaces-vxlan.xml new file mode 100644 index 000000000..4e3cb93cd --- /dev/null +++ b/op-mode-definitions/show-interfaces-vxlan.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="vxlan"> + <properties> + <help>Show VXLAN interface information</help> + <completionHelp> + <path>interfaces vxlan</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified VXLAN interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="vxlan"> + <properties> + <help>Show VXLAN interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vxlan --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed VXLAN interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vxlan --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-wirelessmodem.xml b/op-mode-definitions/show-interfaces-wirelessmodem.xml new file mode 100644 index 000000000..c0ab9c66f --- /dev/null +++ b/op-mode-definitions/show-interfaces-wirelessmodem.xml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="wirelessmodem"> + <properties> + <help>Show Wireless Modem (WWAN) interface information</help> + <completionHelp> + <path>interfaces wirelessmodem</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="log"> + <properties> + <help>Show specified WWAN interface log</help> + </properties> + <command>/usr/bin/journalctl -u "ppp@$4".service</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show specified WWAN interface statistics</help> + <completionHelp> + <path>interfaces wirelessmodem</path> + </completionHelp> + </properties> + <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command> + </leafNode> + </children> + </tagNode> + <node name="wirelessmodem"> + <properties> + <help>Show Wireless Modem (WWAN) interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed Wireless Modem (WWAN( interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces.xml b/op-mode-definitions/show-interfaces.xml new file mode 100644 index 000000000..39b0f0a2c --- /dev/null +++ b/op-mode-definitions/show-interfaces.xml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <properties> + <help>Show network interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show-brief</command> + <children> + <leafNode name="counters"> + <properties> + <help>Show network interface counters</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show-count</command> + </leafNode> + <leafNode name="detail"> + <properties> + <help>Show detailed information of all interfaces</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-access-paths-prefix-community-lists.xml b/op-mode-definitions/show-ip-access-paths-prefix-community-lists.xml new file mode 100644 index 000000000..a5ec65c94 --- /dev/null +++ b/op-mode-definitions/show-ip-access-paths-prefix-community-lists.xml @@ -0,0 +1,116 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <properties> + <help>Show IPv4 routing information</help> + </properties> + <children> + <leafNode name="access-list"> + <properties> + <help>Show all IP access-lists</help> + </properties> + <command>/usr/bin/vtysh -c "show ip access-list"</command> + </leafNode> + <tagNode name="access-list"> + <properties> + <help>Show all IP access-lists</help> + <completionHelp> + <path>policy access-list</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip access-list $4"</command> + </tagNode> + <leafNode name="as-path-access-list"> + <properties> + <help>Show all as-path-access-lists</help> + </properties> + <command>/usr/bin/vtysh -c "show ip as-path-access-list"</command> + </leafNode> + <tagNode name="as-path-access-list"> + <properties> + <help>Show all as-path-access-lists</help> + <completionHelp> + <path>policy as-path-list</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip as-path-access-list $4"</command> + </tagNode> + <leafNode name="community-list"> + <properties> + <help>Show IP community-lists</help> + </properties> + <command>/usr/bin/vtysh -c "show bgp community-list"</command> + </leafNode> + <tagNode name="community-list"> + <properties> + <help>Show IP community-lists</help> + <completionHelp> + <path>policy community-list</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show bgp community-list $4 detail"</command> + </tagNode> + <leafNode name="extcommunity-list"> + <properties> + <help>Show extended IP community-lists</help> + </properties> + <command>/usr/bin/vtysh -c "show bgp extcommunity-list"</command> + </leafNode> + <tagNode name="extcommunity-list"> + <properties> + <help>Show extended IP community-lists</help> + <completionHelp> + <path>policy extcommunity-list</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show bgp extcommunity-list $4 detail"</command> + </tagNode> + <leafNode name="forwarding"> + <properties> + <help>Show IP forwarding status</help> + </properties> + <command>/usr/bin/vtysh -c "show ip forwarding"</command> + </leafNode> + <leafNode name="large-community-list"> + <properties> + <help>Show IP large-community-lists</help> + </properties> + <command>/usr/bin/vtysh -c "show bgp large-community-list"</command> + </leafNode> + <tagNode name="large-community-list"> + <properties> + <help>Show IP large-community-lists</help> + <completionHelp> + <path>policy large-community-list</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show bgp large-community-list $4 detail"</command> + </tagNode> + <leafNode name="prefix-list"> + <properties> + <help>Show all IP prefix-lists</help> + </properties> + <command>/usr/bin/vtysh -c "show ip prefix-list"</command> + </leafNode> + <tagNode name="prefix-list"> + <properties> + <help>Show all IP prefix-lists</help> + <completionHelp> + <path>policy prefix-list</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip prefix-list $4"</command> + </tagNode> + <leafNode name="protocol"> + <properties> + <help>Show IP route-maps per protocol</help> + </properties> + <command>/usr/bin/vtysh -c "show ip protocol"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-bgp.xml b/op-mode-definitions/show-ip-bgp.xml new file mode 100644 index 000000000..5eb2ae56e --- /dev/null +++ b/op-mode-definitions/show-ip-bgp.xml @@ -0,0 +1,342 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="bgp"> + <properties> + <help>Show Border Gateway Protocol (BGP) information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp"</command> + <children> + <leafNode name="attribute-info"> + <properties> + <help>Show BGP attribute information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp attribute-info"</command> + </leafNode> + <leafNode name="cidr-only"> + <properties> + <help>Display only routes with non-natural netmasks</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp cidr-only"</command> + </leafNode> + <node name="community"> + <properties> + <help>Show BGP routes matching the communities</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp community"</command> + </node> + <tagNode name="community"> + <properties> + <help>Display routes matching the specified communities</help> + <completionHelp> + <list><AA:NN> local-AS no-advertise no-export</list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp community $5"</command> + </tagNode> + <leafNode name="community-info"> + <properties> + <help>List all bgp community information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp community-info"</command> + </leafNode> + <tagNode name="community-list"> + <properties> + <help>Show BGP routes matching specified community list</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp community-list $5"</command> + <children> + <leafNode name="exact-match"> + <properties> + <help>Show BGP routes exactly matching specified community list</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp community-list $5 exact-match"</command> + </leafNode> + </children> + </tagNode> + <leafNode name="dampened-paths"> + <properties> + <help>Show dampened BGP paths</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp dampening dampened-paths"</command> + </leafNode> + <tagNode name="filter-list"> + <properties> + <help>Show BGP information for specified word</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp filter-list $5"</command> + </tagNode> + <leafNode name="flap-statistics"> + <properties> + <help>Show flap statistics of routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp dampening flap-statistics"</command> + </leafNode> + <node name="ipv4"> + <properties> + <help>Show BGP IPv4 information</help> + </properties> + <children> + <node name="unicast"> + <properties> + <help>Show BGP IPv4 unicast information</help> + </properties> + <children> + <leafNode name="cidr-only"> + <properties> + <help>Display only routes with non-natural netmasks</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast cidr-only"</command> + </leafNode> + <node name="community"> <!-- START new code --> + <properties> + <help>Show BGP routes matching the communities</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community"</command> + </node> + <tagNode name="community"> + <properties> + <help>Display routes matching the specified communities</help> + <completionHelp> + <list><AA:NN> local-AS no-advertise no-export</list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community $7"</command> + </tagNode> + <tagNode name="community-list"> + <properties> + <help>Show BGP routes matching specified community list</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community-list $7"</command> + <children> + <leafNode name="exact-match"> + <properties> + <help>Show BGP routes exactly matching specified community list</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast community-list $7 exact-match"</command> + </leafNode> + </children> + </tagNode> + <tagNode name="filter-list"> + <properties> + <help>Show BGP information for specified word</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp filter-list $5"</command> + </tagNode> + <tagNode name="neighbors"> + <properties> + <help>Show detailed BGP IPv4 unicast neighbor information</help> + <completionHelp> + <script>vtysh -c "show ip bgp ipv4 unicast summary" | awk '{print $1}' | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"</script> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbors $7"</command> + <children> + <leafNode name="advertised-routes"> + <properties> + <help>Show routes advertised to a BGP neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 advertised-routes"</command> + </leafNode> + <leafNode name="prefix-counts"> + <properties> + <help>Show detailed prefix count information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 prefix-counts"</command> + </leafNode> + <leafNode name="received-routes"> + <properties> + <help>Show the received routes from neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 received-routes"</command> + </leafNode> + <leafNode name="routes"> + <properties> + <help>Show routes learned from neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast neighbor $7 routes"</command> + </leafNode> + </children> + </tagNode> + <leafNode name="paths"> + <properties> + <help>Show BGP path information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast paths"</command> + </leafNode> + <tagNode name="prefix-list"> + <properties> + <help>Show BGP routes matching the specified prefix list</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast prefix-list $7"</command> + </tagNode> + <tagNode name="regexp"> + <properties> + <help>Show BGP routes matching the specified AS path regular expression</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp ipv4 unicast regexp $5"</command> + </tagNode> + <tagNode name="route-map"> + <properties> + <help>Show BGP routes matching the specified route map</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp route-map $5"</command> + </tagNode> + <leafNode name="summary"> + <properties> + <help>Show summary of BGP information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp summary"</command> + </leafNode> + </children> + </node> + <tagNode name="unicast"> + <properties> + <help>Show BGP information for specified IP address or prefix</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp $6"</command> + </tagNode> + </children> + </node> + <node name="large-community"> + <properties> + <help>Show BGP routes matching the specified large-communities</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp large-community"</command> + </node> + <leafNode name="large-community-info"> + <properties> + <help>Show BGP large-community information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp large-community-info"</command> + </leafNode> + <tagNode name="large-community-list"> + <properties> + <help>Show BGP routes matching the specified large-community list</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp large-community-list $5"</command> + </tagNode> + <leafNode name="memory"> + <properties> + <help>Show BGP memory usage</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp memory"</command> + </leafNode> + <tagNode name="neighbors"> + <properties> + <help>Show detailed BGP IPv4 unicast neighbor information</help> + <completionHelp> + <script>vtysh -c "show ip bgp summary" | awk '{print $1}' | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b"</script> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbors $5"</command> + <children> + <leafNode name="advertised-routes"> + <properties> + <help>Show routes advertised to a BGP neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 advertised-routes"</command> + </leafNode> + <leafNode name="dampened-routes"> + <properties> + <help>Show dampened routes received from BGP neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 dampened-routes"</command> + </leafNode> + <leafNode name="flap-statistics"> + <properties> + <help>Show flap statistics of the routes learned from BGP neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 flap-statistics"</command> + </leafNode> + <leafNode name="prefix-counts"> + <properties> + <help>Show detailed prefix count information for BGP neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 prefix-counts"</command> + </leafNode> + <node name="received"> + <properties> + <help>Show information received from BGP neighbor</help> + </properties> + <children> + <leafNode name="prefix-filter"> + <properties> + <help>Show prefixlist filter</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 received prefix-filter"</command> + </leafNode> + </children> + </node> + <leafNode name="received-routes"> + <properties> + <help>Show received routes from BGP neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 received-routes"</command> + </leafNode> + <leafNode name="routes"> + <properties> + <help>Show routes learned from BGP neighbor</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp neighbor $5 routes"</command> + </leafNode> + </children> + </tagNode> + <leafNode name="paths"> + <properties> + <help>Show BGP path information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp paths"</command> + </leafNode> + <tagNode name="prefix-list"> + <properties> + <help>Show BGP routes matching the specified prefix list</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp prefix-list $5"</command> + </tagNode> + <tagNode name="regexp"> + <properties> + <help>Show BGP routes matching the specified AS path regular expression</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp regexp $5"</command> + </tagNode> + <tagNode name="route-map"> + <properties> + <help>Show BGP routes matching the specified route map</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp route-map $5"</command> + </tagNode> + <leafNode name="statistics"> + <properties> + <help>Show summary of BGP information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp statistics"</command> + </leafNode> + <leafNode name="summary"> + <properties> + <help>Show summary of BGP information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp summary"</command> + </leafNode> + </children> + </node> + <tagNode name="bgp"> + <properties> + <help>Show BGP information for specified IP address or prefix</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip bgp $4"</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-igmp.xml b/op-mode-definitions/show-ip-igmp.xml new file mode 100644 index 000000000..b8f2f9107 --- /dev/null +++ b/op-mode-definitions/show-ip-igmp.xml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="igmp"> + <properties> + <help>Show IGMP (Internet Group Management Protocol) information</help> + </properties> + <children> + <leafNode name="groups"> + <properties> + <help>IGMP groups information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp groups"</command> + </leafNode> + <leafNode name="interfaces"> + <properties> + <help>IGMP interfaces information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp interface"</command> + </leafNode> + <leafNode name="join"> + <properties> + <help>IGMP static join information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp join"</command> + </leafNode> + <leafNode name="sources"> + <properties> + <help>IGMP sources information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp sources"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>IGMP statistics</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp statistics"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-multicast.xml b/op-mode-definitions/show-ip-multicast.xml new file mode 100644 index 000000000..5331d2e35 --- /dev/null +++ b/op-mode-definitions/show-ip-multicast.xml @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="multicast"> + <properties> + <help>Show IP multicast</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Show multicast interfaces</help> + </properties> + <command>if ps -C igmpproxy &>/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --interface; else echo IGMP proxy not configured; fi</command> + </leafNode> + <leafNode name="mfc"> + <properties> + <help>Show multicast fowarding cache</help> + </properties> + <command>if ps -C igmpproxy &>/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --mfc; else echo IGMP proxy not configured; fi</command> + </leafNode> + <leafNode name="summary"> + <properties> + <help>IP multicast information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip multicast"</command> + </leafNode> + <leafNode name="route"> + <properties> + <help>IP multicast routing table</help> + </properties> + <command>/usr/bin/vtysh -c "show ip mroute"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-ospf.xml b/op-mode-definitions/show-ip-ospf.xml new file mode 100644 index 000000000..99441d185 --- /dev/null +++ b/op-mode-definitions/show-ip-ospf.xml @@ -0,0 +1,579 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <properties> + <help>Show IPv4 routing information</help> + </properties> + <children> + <node name="ospf"> + <properties> + <help>Show IPv4 Open Shortest Path First (OSPF) routing information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf"</command> + <children> + <leafNode name="border-routers"> + <properties> + <help>Show IPv4 OSPF border-routers information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf border-routers"</command> + </leafNode> + <node name="database"> + <properties> + <help>Show IPv4 OSPF database information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database"</command> + <children> + <node name="asbr-summary"> + <properties> + <help>Show IPv4 OSPF ASBR summary database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF ASBR summary database for given address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF ASBR summary database for given address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="asbr-summary"> + <properties> + <help>Show IPv4 OSPF ASBR summary database information of given address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF ASBR summary database of given address for given advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database asbr-summary $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show summary of self-originate IPv4 OSPF ASBR database</help> + </properties> + <command>show ip ospf database asbr-summary $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <node name="external"> + <properties> + <help>Show IPv4 OSPF external database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database external"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF external database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database external adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF external database for specified IP address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="external"> + <properties> + <help>Show IPv4 OSPF external database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database external"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF external database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database external $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF external database</help> + </properties> + <command>show ip ospf database external $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <leafNode name="max-age"> + <properties> + <help>Show IPv4 OSPF max-age database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database max-age"</command> + </leafNode> + <node name="network"> + <properties> + <help>Show IPv4 OSPF network database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database network"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF network database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database network adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF network database for given address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="network"> + <properties> + <help>Show IPv4 OSPF network database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database network"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF network database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database network $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF network database</help> + </properties> + <command>show ip ospf database network $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <node name="nssa-external"> + <properties> + <help>Show IPv4 OSPF NSSA external database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database nssa-external"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF NSSA external database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database nssa-external adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF NSSA external database for specified IP address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="nssa-external"> + <properties> + <help>Show IPv4 OSPF NSSA external database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database nssa-external"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF NSSA external database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database nssa-external $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF NSSA external database</help> + </properties> + <command>show ip ospf database nssa-external $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <node name="opaque-area"> + <properties> + <help>Show IPv4 OSPF opaque-area database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-area"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-area database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-area adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-area database for specified IP address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="opaque-area"> + <properties> + <help>Show IPv4 OSPF opaque-area database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-area"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-area database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-area $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF opaque-area database</help> + </properties> + <command>show ip ospf database opaque-area $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <node name="opaque-as"> + <properties> + <help>Show IPv4 OSPF opaque-as database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-as"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-as database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-as adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-as database for specified IP address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="opaque-as"> + <properties> + <help>Show IPv4 OSPF opaque-as database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-as"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-as database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-as $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF opaque-as database</help> + </properties> + <command>show ip ospf database opaque-as $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <node name="opaque-link"> + <properties> + <help>Show IPv4 OSPF opaque-link database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-link"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-link database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-link adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-link database for specified IP address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="opaque-link"> + <properties> + <help>Show IPv4 OSPF opaque-link database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-link"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF opaque-link database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database opaque-link $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF opaque-link database</help> + </properties> + <command>show ip ospf database opaque-link $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <node name="router"> + <properties> + <help>Show IPv4 OSPF router database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database router"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF router database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database router adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF router database for specified IP address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="router"> + <properties> + <help>Show IPv4 OSPF router database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database router"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF router database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database router $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF router database</help> + </properties> + <command>show ip ospf database router $6 self-originate</command> + </leafNode> + </children> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show IPv4 OSPF self-originate database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database self-originate"</command> + </leafNode> + <node name="summary"> + <properties> + <help>Show summary of IPv4 OSPF database</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database summary"</command> + <children> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF summary database for specified IP address of advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database summary adv-router $7"</command> + </tagNode> + <node name="adv-router"> + <properties> + <help>Show IPv4 OSPF summary database for specified IP address of advertised router</help> + </properties> + </node> + </children> + </node> + <tagNode name="summary"> + <properties> + <help>Show IPv4 OSPF summary database information of specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database summary"</command> + <children> + <node name="adv-router"> + <properties> + <help>Show advertising router link states</help> + </properties> + </node> + <tagNode name="adv-router"> + <properties> + <help>Show IPv4 OSPF summary database of specified IP address for specified advertised router</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf database summary $6 adv-router $8"</command> + </tagNode> + <leafNode name="self-originate"> + <properties> + <help>Show self-originate IPv4 OSPF summary database</help> + </properties> + <command>show ip ospf database summary $6 self-originate</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + <node name="interface"> + <properties> + <help>Show IPv4 OSPF interface information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf interface"</command> + </node> + <tagNode name="interface"> + <properties> + <help>Show IPv4 OSPF information for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf interface $5"</command> + </tagNode> + <node name="neighbor"> + <properties> + <help>Show IPv4 OSPF neighbor information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf neighbor"</command> + <children> + <tagNode name="address"> + <properties> + <help>Show IPv4 OSPF neighbor information for specified IP address</help> + <completionHelp> + <list><x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf neighbor $6"</command> + </tagNode> + <node name="detail"> + <properties> + <help>Show detailed IPv4 OSPF neighbor information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf neighbor detail"</command> + </node> + </children> + </node> + <tagNode name="neighbor"> + <properties> + <help>Show IPv4 OSPF neighbor information for specified IP address or interface</help> + <completionHelp> + <list><x.x.x.x></list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf neighbor $5"</command> + </tagNode> + <leafNode name="route"> + <properties> + <help>Show IPv4 OSPF route information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip ospf route"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-pim.xml b/op-mode-definitions/show-ip-pim.xml new file mode 100644 index 000000000..3f4edc779 --- /dev/null +++ b/op-mode-definitions/show-ip-pim.xml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="pim"> + <properties> + <help>Show PIM (Protocol Independent Multicast) information</help> + </properties> + <children> + <leafNode name="interfaces"> + <properties> + <help>PIM interfaces information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim interface"</command> + </leafNode> + <leafNode name="join"> + <properties> + <help>PIM join information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim join"</command> + </leafNode> + <leafNode name="neighbor"> + <properties> + <help>PIM neighbor information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim neighbor"</command> + </leafNode> + <leafNode name="nexthop"> + <properties> + <help>PIM cached nexthop rpf information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim nexthop"</command> + </leafNode> + <leafNode name="state"> + <properties> + <help>PIM state information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim state"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>PIM statistics</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim statistics"</command> + </leafNode> + <leafNode name="rp"> + <properties> + <help>PIM RP (Rendevous Point) information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim rp-info"</command> + </leafNode> + <leafNode name="rpf"> + <properties> + <help>PIM cached source rpf information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim rpf"</command> + </leafNode> + <leafNode name="upstream"> + <properties> + <help>PIM upstream information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim upstream"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-ports.xml b/op-mode-definitions/show-ip-ports.xml new file mode 100644 index 000000000..a74b68ffc --- /dev/null +++ b/op-mode-definitions/show-ip-ports.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <leafNode name="ports"> + <properties> + <help>Show IP ports in use by various system services</help> + </properties> + <command>sudo /usr/bin/netstat -tulnp</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-rip.xml b/op-mode-definitions/show-ip-rip.xml new file mode 100644 index 000000000..b61ab10a7 --- /dev/null +++ b/op-mode-definitions/show-ip-rip.xml @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <properties> + <help>Show IPv4 routing information</help> + </properties> + <children> + <node name="rip"> + <properties> + <help>Show Routing Information Protocol (RIP) information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip rip"</command> + <children> + <leafNode name="status"> + <properties> + <help>Show RIP protocol status</help> + </properties> + <command>/usr/bin/vtysh -c "show ip rip status"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-route.xml b/op-mode-definitions/show-ip-route.xml new file mode 100644 index 000000000..d12d132c0 --- /dev/null +++ b/op-mode-definitions/show-ip-route.xml @@ -0,0 +1,160 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <properties> + <help>Show IPv4 routing information</help> + </properties> + <children> + <node name="route"> + <properties> + <help>Show IP routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route"</command> + <children> + <leafNode name="bgp"> + <properties> + <help>Show IP BGP routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route bgp"</command> + </leafNode> + <node name="cache"> + <properties> + <help>Show kernel route cache</help> + </properties> + <command>ip -s route list cache</command> + </node> + <tagNode name="cache"> + <properties> + <help>Show kernel route cache for a given route</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>ip -s route list cache $5</command> + </tagNode> + <leafNode name="connected"> + <properties> + <help>Show IP connected routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route connected"</command> + </leafNode> + <node name="forward"> + <properties> + <help>Show kernel route table</help> + </properties> + <command>ip route list</command> + </node> + <tagNode name="forward"> + <properties> + <help>Show kernel route table for a given route</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>ip -s route list $5</command> + </tagNode> + <leafNode name="kernel"> + <properties> + <help>Show IP kernel routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route kernel"</command> + </leafNode> + <leafNode name="ospf"> + <properties> + <help>Show IP OSPF routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route ospf"</command> + </leafNode> + <leafNode name="rip"> + <properties> + <help>Show IP RIP routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route rip"</command> + </leafNode> + <leafNode name="static"> + <properties> + <help>Show IP static routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route static"</command> + </leafNode> + <leafNode name="summary"> + <properties> + <help>Show IP routes summary</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route summary"</command> + </leafNode> + <leafNode name="supernets-only"> + <properties> + <help>Show IP supernet routes</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route supernets-only"</command> + </leafNode> + <node name="table"> + <properties> + <help>Show IP routes in policy table</help> + </properties> + </node> + <tagNode name="table"> + <properties> + <help>Show IP routes in policy table</help> + <completionHelp> + <list><1-200></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip route table $5"</command> + </tagNode> + <node name="tag"> + <properties> + <help>Show only routes with tag</help> + </properties> + </node> + <tagNode name="tag"> + <properties> + <help>Tag value</help> + <completionHelp> + <list><1-4294967295></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip route tag $5"</command> + </tagNode> + <node name="vrf"> + <properties> + <help>Show IP routes in VRF</help> + </properties> + </node> + <tagNode name="vrf"> + <properties> + <help>Show IP routes in VRF</help> + <completionHelp> + <list><vrf></list> + <path>vrf name</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip route vrf $5"</command> + </tagNode> + </children> + </node> + <tagNode name="route"> + <properties> + <help>Show IP routes of specified IP address or prefix</help> + <completionHelp> + <list><x.x.x.x> <x.x.x.x/x></list> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show ip route $4"</command> + <children> + <leafNode name="longer-prefixes"> + <properties> + <help>Show longer prefixes of routes for specified IP address or prefix</help> + </properties> + <command>/usr/bin/vtysh -c "show ip route $4"</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ipv6-bgp.xml b/op-mode-definitions/show-ipv6-bgp.xml new file mode 100644 index 000000000..67a8c8658 --- /dev/null +++ b/op-mode-definitions/show-ipv6-bgp.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ipv6"> + <children> + <node name="bgp"> + <children> + <tagNode name="route-map"> + <properties> + <help>Show BGP routes matching the specified route map</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show bgp ipv6 route-map $5"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-license.xml b/op-mode-definitions/show-license.xml new file mode 100644 index 000000000..2ce11567d --- /dev/null +++ b/op-mode-definitions/show-license.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <leafNode name="license"> + <properties> + <help>Show VyOS license information</help> + </properties> + <command>less $_vyatta_less_options --prompt=".license, page %dt of %D" -- ${vyatta_sysconfdir}/LICENSE</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-log.xml b/op-mode-definitions/show-log.xml new file mode 100644 index 000000000..0c4da647b --- /dev/null +++ b/op-mode-definitions/show-log.xml @@ -0,0 +1,218 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="log"> + <properties> + <help>Show contents of current master log file</help> + </properties> + <command>/bin/journalctl</command> + <children> + <leafNode name="all"> + <properties> + <help>Show contents of all master log files</help> + </properties> + <command>eval $(lesspipe); less $_vyatta_less_options --prompt=".log?m, file %i of %m., page %dt of %D" -- `printf "%s\n" /var/log/messages* | sort -nr`</command> + </leafNode> + <leafNode name="authorization"> + <properties> + <help>Show listing of authorization attempts</help> + </properties> + <command>/bin/journalctl -q SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command> + </leafNode> + <leafNode name="cluster"> + <properties> + <help>Show log for Cluster</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e heartbeat -e cl_status -e mach_down -e ha_log</command> + </leafNode> + <leafNode name="conntrack-sync"> + <properties> + <help>Show log for Conntrack-sync</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | grep -e conntrackd</command> + </leafNode> + <leafNode name="dhcp"> + <properties> + <help>Show log for Dynamic Host Control Protocol (DHCP)</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep dhcpd</command> + </leafNode> + <node name="firewall"> + <properties> + <help>Show log for Firewall</help> + </properties> + <children> + <tagNode name="ipv6-name"> + <properties> + <help>Show log for a specified firewall (IPv6)</help> + <completionHelp> + <path>firewall ipv6-name</path> + </completionHelp> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | egrep "\[$5-([0-9]+|default)-[ADR]\]"</command> + <children> + <tagNode name="rule"> + <properties> + <help>Show log for a rule in the specified firewall</help> + <completionHelp> + <path>firewall ipv6-name ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "\[$5-$7-[ADR]\]"</command> + </tagNode> + </children> + </tagNode> + <tagNode name="name"> + <properties> + <help>Show log for a specified firewall (IPv4)</help> + <completionHelp> + <path>firewall name</path> + </completionHelp> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr ) | egrep "\[$5-([0-9]+|default)-[ADR]\]"</command> + <children> + <tagNode name="rule"> + <properties> + <help>Show log for a rule in the specified firewall</help> + <completionHelp> + <path>firewall name ${COMP_WORDS[4]} rule</path> + </completionHelp> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | egrep "\[$5-$7-[ADR]\]"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + <leafNode name="https"> + <properties> + <help>Show log for HTTPs</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e nginx</command> + </leafNode> + <tagNode name="image"> + <properties> + <help>Show contents of master log file for image</help> + <completionHelp> + <script>compgen -f /lib/live/mount/persistence/boot/ | grep -v grub | sed -e s@/lib/live/mount/persistence/boot/@@</script> + </completionHelp> + </properties> + <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/messages</command> + <children> + <leafNode name="all"> + <properties> + <help>Show contents of all master log files for image</help> + </properties> + <command>eval $(lesspipe); less $_vyatta_less_options --prompt=".log?m, file %i of %m., page %dt of %D" -- `printf "%s\n" /lib/live/mount/persistence/boot/$4/rw/var/log/messages* | sort -nr`</command> + </leafNode> + <leafNode name="authorization"> + <properties> + <help>Show listing of authorization attempts for image</help> + </properties> + <command>less $_vyatta_less_options --prompt=".log, page %dt of %D" -- /lib/live/mount/persistence/boot/$4/rw/var/log/auth.log</command> + </leafNode> + <tagNode name="tail"> + <properties> + <help>Show last changes to messages</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>tail -n "$6" /lib/live/mount/persistence/boot/$4/rw/var/log/messages | ${VYATTA_PAGER:-cat}</command> + </tagNode> + </children> + </tagNode> + <leafNode name="lldp"> + <properties> + <help>Show log for LLDP</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e lldpd</command> + </leafNode> + <leafNode name="nat"> + <properties> + <help>Show log for Network Address Translation (NAT)</help> + </properties> + <command>egrep -i "kernel:.*\[NAT-[A-Z]{3,}-[0-9]+(-MASQ)?\]" $(find /var/log -maxdepth 1 -type f -name messages\* | sort -t. -k2nr)</command> + </leafNode> + <leafNode name="nat"> + <properties> + <help>Show log for OpenVPN</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e openvpn</command> + </leafNode> + <leafNode name="snmp"> + <properties> + <help>Show log for Simple Network Monitoring Protocol (SNMP)</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e snmpd</command> + </leafNode> + <tagNode name="tail"> + <properties> + <help>Show last n changes to messages</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>tail -n "$4" /var/log/messages | ${VYATTA_PAGER:-cat}</command> + </tagNode> + <node name="tail"> + <properties> + <help>Show last 10 lines of /var/log/messages file</help> + </properties> + <command>tail -n 10 /var/log/messages</command> + </node> + <node name="vpn"> + <properties> + <help>Show log for Virtual Private Network (VPN)</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Show log for ALL</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon -e accel -e pptpd -e ppp</command> + </leafNode> + <leafNode name="ipsec"> + <properties> + <help>Show log for IPSec</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon</command> + </leafNode> + <leafNode name="l2tp"> + <properties> + <help>Show log for L2TP</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e remote-access-aaa-win -e remote-access-zzz-mac -e accel-l2tp -e ppp</command> + </leafNode> + <leafNode name="pptp"> + <properties> + <help>Show log for PPTP</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-pptp -e ppp</command> + </leafNode> + <leafNode name="sstp"> + <properties> + <help>Show log for SSTP</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e accel-sstp -e ppp</command> + </leafNode> + </children> + </node> + <leafNode name="vrrp"> + <properties> + <help>Show log for Virtual Router Redundancy Protocol (VRRP)</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e Keepalived_vrrp</command> + </leafNode> + <leafNode name="webproxy"> + <properties> + <help>Show log for Webproxy</help> + </properties> + <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "squid"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-login.xml b/op-mode-definitions/show-login.xml new file mode 100644 index 000000000..6d8c782c4 --- /dev/null +++ b/op-mode-definitions/show-login.xml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="login"> + <properties> + <help>Show current login credentials</help> + </properties> + <command>${vyos_op_scripts_dir}/show_current_user.sh</command> + <children> + <leafNode name="groups"> + <properties> + <help>Show current login group information</help> + </properties> + <command>/usr/bin/id -Gn</command> + </leafNode> + <leafNode name="level"> + <properties> + <help>Show current login level</help> + </properties> + <command>if [ -n "$VYATTA_USER_LEVEL_DIR" ]; then basename $VYATTA_USER_LEVEL_DIR; fi</command> + </leafNode> + <leafNode name="user"> + <properties> + <help>Show current login user id</help> + </properties> + <command>/usr/bin/id -un</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-monitoring.xml b/op-mode-definitions/show-monitoring.xml new file mode 100644 index 000000000..2651b3438 --- /dev/null +++ b/op-mode-definitions/show-monitoring.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <leafNode name="monitoring"> + <properties> + <help>Show currently monitored services</help> + </properties> + <command>vtysh -c "show debugging"</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-mpls.xml b/op-mode-definitions/show-mpls.xml new file mode 100644 index 000000000..3f160b6dc --- /dev/null +++ b/op-mode-definitions/show-mpls.xml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="mpls"> + <properties> + <help>Show Multiprotocol Label Switching (MPLS)</help> + </properties> + <children> + <node name="ldp"> + <properties> + <help>Label Distribution Protocol (LDP)</help> + </properties> + <children> + <node name="binding"> + <properties> + <help>Label Information Base</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp binding"</command> + </node> + <node name="discovery"> + <properties> + <help>Discovery hello information</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp discovery"</command> + </node> + <node name="interface"> + <properties> + <help>LDP interface information</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp interface"</command> + </node> + <node name="neighbor"> + <properties> + <help>LDP neighbor information</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp neighbor"</command> + <children> + <node name="detail"> + <properties> + <help>Show neighbor detail</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp neighbor detail"</command> + </node> + </children> + </node> + </children> + </node> + <node name="pseudowire"> + <properties> + <help>Show MPLS pseudowire interfaces</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls pseudowires"</command> + </node> + <node name="table"> + <properties> + <help>Show MPLS table</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls table"</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ntp.xml b/op-mode-definitions/show-ntp.xml new file mode 100644 index 000000000..b7f0acdf8 --- /dev/null +++ b/op-mode-definitions/show-ntp.xml @@ -0,0 +1,31 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ntp"> + <properties> + <help>Show peer status of NTP daemon</help> + </properties> + <command>if ps -C ntpd &>/dev/null; then ntpq -n -c peers; else echo NTP daemon disabled; fi</command> + <children> + <tagNode name="server"> + <properties> + <help>Show date and time of specified NTP server</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ntp_servers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/ntpdate -q "$4"</command> + </tagNode> + <node name="info"> + <properties> + <help>Show NTP operational summary</help> + </properties> + <command>if ps -C ntpd &>/dev/null; then ntpq -n -c sysinfo; ntpq -n -c kerninfo; else echo NTP daemon disabled; fi</command> + </node> + + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-poweroff.xml b/op-mode-definitions/show-poweroff.xml new file mode 100644 index 000000000..1fd2afcc3 --- /dev/null +++ b/op-mode-definitions/show-poweroff.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <leafNode name="poweroff"> + <properties> + <help>Show scheduled poweroff</help> + </properties> + <command>${vyos_op_scripts_dir}/powerctrl.py --check</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-protocols-bfd.xml b/op-mode-definitions/show-protocols-bfd.xml new file mode 100644 index 000000000..4483b39bf --- /dev/null +++ b/op-mode-definitions/show-protocols-bfd.xml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="protocols"> + <properties> + <help>Show protocol specific information</help> + </properties> + <children> + <node name="bfd"> + <children> + <node name="peer"> + <properties> + <help>Show all Bidirectional Forwarding Detection (BFD) peer status</help> + </properties> + <command>/usr/bin/vtysh -c "show bfd peers"</command> + <children> + <leafNode name="counters"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help> + </properties> + <command>/usr/bin/vtysh -c "show bfd peers counters"</command> + </leafNode> + </children> + </node> + <tagNode name="peer"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD) peer status</help> + <completionHelp> + <script>/usr/bin/vtysh -c "show bfd peers" | awk '/[:blank:]*peer/ { printf "%s\n", $2 }'</script> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("/usr/bin/vtysh -c \"show bfd " peer "\"") }'</command> + <children> + <leafNode name="counters"> + <properties> + <help>Show Bidirectional Forwarding Detection (BFD) peer counters</help> + </properties> + <command>/usr/bin/vtysh -c "show bfd peers" | awk -v BFD_PEER=$5 'BEGIN { regex = sprintf("(peer %s.*)vrf", BFD_PEER) } { if (match($0, regex, bfd_peer_value)) peer=bfd_peer_value[1] } END { if (peer) system("/usr/bin/vtysh -c \"show bfd " peer " counters\"") }'</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-protocols-static.xml b/op-mode-definitions/show-protocols-static.xml new file mode 100644 index 000000000..1211a7fe5 --- /dev/null +++ b/op-mode-definitions/show-protocols-static.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="protocols"> + <children> + <node name="static"> + <children> + <node name="arp"> + <properties> + <help>Show Address Resolution Protocol (ARP) information</help> + </properties> + <command>/usr/sbin/arp -e -n</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show Address Resolution Protocol (ARP) cache for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -b</script> + </completionHelp> + </properties> + <command>/usr/sbin/arp -e -n -i "$6"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml new file mode 100644 index 000000000..8bf394552 --- /dev/null +++ b/op-mode-definitions/show-raid.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <tagNode name="raid"> + <properties> + <help>Show status of RAID set</help> + <completionHelp> + <script>${vyos_completion_dir}/list_raidset.sh</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_raid.sh $3</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-reboot.xml b/op-mode-definitions/show-reboot.xml new file mode 100644 index 000000000..c85966bcb --- /dev/null +++ b/op-mode-definitions/show-reboot.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <leafNode name="reboot"> + <properties> + <help>Show scheduled reboot</help> + </properties> + <command>${vyos_op_scripts_dir}/powerctrl.py --check</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-route-map.xml b/op-mode-definitions/show-route-map.xml new file mode 100644 index 000000000..0e376757b --- /dev/null +++ b/op-mode-definitions/show-route-map.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="route-map"> + <properties> + <help>Show route-map information</help> + </properties> + <command>/usr/bin/vtysh -c "show route-map"</command> + </node> + <tagNode name="route-map"> + <properties> + <help>Show specified route-map information</help> + <completionHelp> + <path>policy route-map</path> + </completionHelp> + </properties> + <command>/usr/bin/vtysh -c "show route-map $3"</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-rpki.xml b/op-mode-definitions/show-rpki.xml new file mode 100644 index 000000000..d68c3b862 --- /dev/null +++ b/op-mode-definitions/show-rpki.xml @@ -0,0 +1,32 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="rpki"> + <properties> + <help>Show RPKI information</help> + </properties> + <children> + <leafNode name="cache-connection"> + <properties> + <help>Show RPKI cache connections</help> + </properties> + <command>/usr/bin/vtysh -c "show rpki cache-connection"</command> + </leafNode> + <leafNode name="cache-server"> + <properties> + <help>Show RPKI cache servers information</help> + </properties> + <command>/usr/bin/vtysh -c "show rpki cache-server"</command> + </leafNode> + <leafNode name="prefix-table"> + <properties> + <help>Show RPKI-validated prefixes</help> + </properties> + <command>/usr/bin/vtysh -c "show rpki prefix-table"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-system.xml b/op-mode-definitions/show-system.xml new file mode 100644 index 000000000..1b98b559b --- /dev/null +++ b/op-mode-definitions/show-system.xml @@ -0,0 +1,183 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="system"> + <properties> + <help>Show system information</help> + </properties> + <children> + <node name="connections"> + <properties> + <help>Show active network connections on the system</help> + </properties> + <command>netstat -an</command> + <children> + <node name="tcp"> + <properties> + <help>Show TCP connection information</help> + </properties> + <command>ss -t -r</command> + <children> + <leafNode name="all"> + <properties> + <help>Show all TCP connections</help> + </properties> + <command>ss -t -a</command> + </leafNode> + <leafNode name="numeric"> + <properties> + <help>Show TCP connection without resolving names</help> + </properties> + <command>ss -t -n</command> + </leafNode> + </children> + </node> + <node name="udp"> + <properties> + <help>Show UDP socket information</help> + </properties> + <command>ss -u -a -r</command> + <children> + <leafNode name="numeric"> + <properties> + <help>Show UDP socket information without resolving names</help> + </properties> + <command>ss -u -a -n</command> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="cpu"> + <properties> + <help>Show CPU information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_cpu.py</command> + </leafNode> + <leafNode name= "integrity"> + <properties> + <help>Checks overall system integrity</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/system_integrity.py</command> + </leafNode> + <leafNode name="kernel-messages"> + <properties> + <help>Show messages in kernel ring buffer</help> + </properties> + <command>sudo dmesg</command> + </leafNode> + <node name="login"> + <properties> + <help>Show user accounts</help> + </properties> + <children> + <node name="users"> + <properties> + <help>Show user account information</help> + </properties> + <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py</command> + <children> + <leafNode name="all"> + <properties> + <help>Show information about all accounts</help> + </properties> + <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py all</command> + </leafNode> + <leafNode name="locked"> + <properties> + <help>Show information about locked accounts</help> + </properties> + <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py locked</command> + </leafNode> + <leafNode name="other"> + <properties> + <help>Show information about non VyOS user accounts</help> + </properties> + <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py other</command> + </leafNode> + <leafNode name="vyos"> + <properties> + <help>Show information about VyOS user accounts</help> + </properties> + <command>${vyos_libexec_dir}/vyos-sudo.py ${vyos_op_scripts_dir}/show_users.py vyos</command> + </leafNode> + </children> + </node> + </children> + </node> + <node name="memory"> + <properties> + <help>Show system memory usage</help> + </properties> + <command>${vyos_op_scripts_dir}/show_ram.sh</command> + <children> + <leafNode name="cache"> + <properties> + <help>Show kernel cache information</help> + </properties> + <command>sudo slabtop -o</command> + </leafNode> + <leafNode name="detail"> + <properties> + <help>Show detailed system memory usage</help> + </properties> + <command>cat /proc/meminfo</command> + </leafNode> + <leafNode name="routing-daemons"> + <properties> + <help>Show memory usage of all routing protocols</help> + </properties> + <command>/usr/bin/vtysh -c "show memory"</command> + </leafNode> + </children> + </node> + <node name="processes"> + <properties> + <help>Show system processes</help> + </properties> + <command>ps ax</command> + <children> + <leafNode name="extensive"> + <properties> + <help>Show extensive process info</help> + </properties> + <command>top -b -n1</command> + </leafNode> + <leafNode name="summary"> + <properties> + <help>Show summary of system processes</help> + </properties> + <command>uptime</command> + </leafNode> + <leafNode name="tree"> + <properties> + <help>Show process tree</help> + </properties> + <command>ps -ejH</command> + </leafNode> + </children> + </node> + <leafNode name="routing-daemons"> + <properties> + <help>Show Quagga routing daemons</help> + </properties> + <command>/usr/bin/vtysh -c "show daemons"</command> + </leafNode> + <leafNode name="storage"> + <properties> + <help>Show filesystem usage</help> + </properties> + <command>df -h -x squashfs</command> + </leafNode> + <leafNode name="uptime"> + <properties> + <help>Show how long the system has been up</help> + </properties> + <command>uptime</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-table.xml b/op-mode-definitions/show-table.xml new file mode 100644 index 000000000..b093a5de7 --- /dev/null +++ b/op-mode-definitions/show-table.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <leafNode name="table"> + <properties> + <help>Show routing tables</help> + </properties> + <command>/usr/bin/vtysh -c "show zebra router table summary"</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-users.xml b/op-mode-definitions/show-users.xml new file mode 100644 index 000000000..a026e47e7 --- /dev/null +++ b/op-mode-definitions/show-users.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="users"> + <properties> + <help>Show user information</help> + </properties> + <command>who -H</command> + <children> + <node name="recent"> + <properties> + <help>Show 10 recently logged in users</help> + </properties> + <command>last -aF -n 10 | sed -e 's/^wtmp begins/Displaying logins since/'</command> + </node> + <tagNode name="recent"> + <properties> + <help>Show specified number of recently logged in users</help> + <completionHelp> + <list>NUMBER</list> + </completionHelp> + </properties> + <command>last -aF -n $4 | sed -e 's/^wtmp begins/Displaying logins since/'</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-version.xml b/op-mode-definitions/show-version.xml new file mode 100644 index 000000000..aae5bb008 --- /dev/null +++ b/op-mode-definitions/show-version.xml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="version"> + <properties> + <help>Show system version information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_version.py</command> + <children> + <leafNode name="funny"> + <properties> + <help>Show system version and some fun stuff</help> + </properties> + <command>${vyos_op_scripts_dir}/show_version.py --funny</command> + </leafNode> + <leafNode name="all"> + <properties> + <help>Show system version and versions of all packages</help> + </properties> + <command>${vyos_op_scripts_dir}/show_version.py --all</command> + </leafNode> + <leafNode name="quagga"> + <properties> + <help>Show Quagga version information</help> + </properties> + <command>/usr/bin/vtysh -c "show version"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-vpn.xml b/op-mode-definitions/show-vpn.xml new file mode 100644 index 000000000..0e7fc38e9 --- /dev/null +++ b/op-mode-definitions/show-vpn.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vpn"> + <properties> + <help>Show active remote access Virtual Private Network (VPN) sessions</help> + </properties> + <children> + <leafNode name="remote-access"> + <properties> + <help>Show active VPN server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/show_vpn_ra.py</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml new file mode 100644 index 000000000..438e7c334 --- /dev/null +++ b/op-mode-definitions/show-vrf.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vrf"> + <properties> + <help>Show VRF information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_vrf.py -e</command> + </node> + <tagNode name="vrf"> + <properties> + <help>Show information on specific VRF instance</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command> + <children> + <leafNode name="processes"> + <properties> + <help>Shows all process ids associated with VRF</help> + </properties> + <command>/usr/sbin/ip vrf pids "$3"</command> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/snmp.xml b/op-mode-definitions/snmp.xml new file mode 100644 index 000000000..a0a47da40 --- /dev/null +++ b/op-mode-definitions/snmp.xml @@ -0,0 +1,111 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="snmp"> + <properties> + <help>Show status of SNMP on localhost</help> + </properties> + <children> + <tagNode name="community"> + <properties> + <help>Show status of SNMP community</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/snmp.py --allowed</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/snmp.py --community="$4"</command> + <children> + <tagNode name="host"> + <properties> + <help>Show status of SNMP on remote host</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp.py --community="$4" --host "$6"</command> + </tagNode> + </children> + </tagNode> + <node name="mib"> + <properties> + <help>Show SNMP MIB information</help> + </properties> + <children> + <node name="ifmib"> + <properties> + <help>Show all SNMP interfaces MIB information</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp_ifmib.py</command> + <children> + <tagNode name="ifAlias"> + <properties> + <help>Show SNMP ifAlias for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifalias="$6"</command> + </tagNode> + <tagNode name="ifDescr"> + <properties> + <help>Show SNMP ifDescr for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifdescr="$6"</command> + </tagNode> + <tagNode name="ifIndex"> + <properties> + <help>Show SNMP ifDescr for specified interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/snmp_ifmib.py --ifindex="$6"</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="v3"> + <properties> + <help>Show SNMP v3 status on localhost</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp_v3.py --all</command> + <children> + <leafNode name="certificates"> + <properties> + <help>Show TSM certificates</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp_v3_showcerts.sh</command> + </leafNode> + <leafNode name="group"> + <properties> + <help>Show the list of configured groups</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp_v3.py --group</command> + </leafNode> + <leafNode name="trap-target"> + <properties> + <help>Show the list of configured targets</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp_v3.py --trap</command> + </leafNode> + <leafNode name="user"> + <properties> + <help>Show the list of configured users</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp_v3.py --user</command> + </leafNode> + <leafNode name="view"> + <properties> + <help>Show the list of configured views</help> + </properties> + <command>${vyos_op_scripts_dir}/snmp_v3.py --view</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/sstp-server.xml b/op-mode-definitions/sstp-server.xml new file mode 100644 index 000000000..03dfc4262 --- /dev/null +++ b/op-mode-definitions/sstp-server.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="sstp-server"> + <properties> + <help>Show SSTP server information</help> + </properties> + <children> + <leafNode name="sessions"> + <properties> + <help>Show active SSTP server sessions</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="sstp" --action="show sessions"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show SSTP server statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/ppp-server-ctrl.py --proto="sstp" --action="show stat"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/telnet.xml b/op-mode-definitions/telnet.xml new file mode 100644 index 000000000..c5bb6d283 --- /dev/null +++ b/op-mode-definitions/telnet.xml @@ -0,0 +1,30 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="telnet"> + <properties> + <help>Telnet to a node</help> + </properties> + <children> + <tagNode name="to"> + <properties> + <help>Telnet to a host</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/telnet $3</command> + <children> + <tagNode name="port"> + <properties> + <help>Telnet to a host:port</help> + <completionHelp> + <list><0-65535></list> + </completionHelp> + </properties> + <command>/usr/bin/telnet $3 $5</command> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/terminal.xml b/op-mode-definitions/terminal.xml new file mode 100644 index 000000000..9c4e629cb --- /dev/null +++ b/op-mode-definitions/terminal.xml @@ -0,0 +1,122 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="clear"> + <properties> + <help>Clear system information</help> + </properties> + <children> + <node name="console"> + <properties> + <help>Clear screen</help> + </properties> + <command>/usr/bin/clear</command> + </node> + </children> + </node> + <node name="reset"> + <properties> + <help>Reset a service</help> + </properties> + <children> + <node name="terminal"> + <properties> + <help>Reset terminal</help> + </properties> + <command>/usr/bin/reset</command> + </node> + </children> + </node> + <node name="set"> + <properties> + <help>Set operational options</help> + </properties> + <children> + <tagNode name="builtin"> + <properties> + <help>Bash builtin set command</help> + <completionHelp> + <list><OPTION></list> + </completionHelp> + </properties> + <command>builtin $3</command> + </tagNode> + + <node name="console"> + <properties> + <help>Control console behaviors</help> + </properties> + <children> + <leafNode name="keymap"> + <properties> + <help>Reconfigure console keyboard layout</help> + </properties> + <command>sudo dpkg-reconfigure -f dialog keyboard-configuration && sudo systemctl restart keyboard-setup</command> + </leafNode> + </children> + </node> + + <node name="terminal"> + <properties> + <help>Control terminal behaviors</help> + </properties> + <children> + + <node name="key"> + <properties> + <help>Set key behaviors</help> + </properties> + <children> + <tagNode name="query-help"> + <properties> + <help>Enable/disable getting help using question mark (default enabled)</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/toggle_help_binding.sh $5</command> + </tagNode> + </children> + </node> + + <node name="pager"> + <properties> + <help>Set terminal pager to default (less)</help> + </properties> + <command>VYATTA_PAGER=${_vyatta_default_pager}</command> + </node> + <tagNode name="pager"> + <properties> + <help>Set terminal pager</help> + <completionHelp> + <list><PROGRAM></list> + </completionHelp> + </properties> + <command>VYATTA_PAGER=$4</command> + </tagNode> + + <tagNode name="length"> + <properties> + <help>Set terminal to given number of rows (0 disables paging)</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>if [ "$4" -eq 0 ]; then VYATTA_PAGER=cat; else VYATTA_PAGER=${_vyatta_default_pager}; stty rows $4; fi</command> + </tagNode> + + <tagNode name="width"> + <properties> + <help>Set terminal to given number of columns</help> + <completionHelp> + <list><NUMBER></list> + </completionHelp> + </properties> + <command>stty columns $4</command> + </tagNode> + </children> + </node> + </children> + </node> + + +</interfaceDefinition> diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml new file mode 100644 index 000000000..1b619ed43 --- /dev/null +++ b/op-mode-definitions/traceroute.xml @@ -0,0 +1,227 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <tagNode name="traceroute"> + <properties> + <help>Track network path to node</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/traceroute "$2"</command> + </tagNode> + <node name="traceroute"> + <properties> + <help>Track network path to node</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <children> + <tagNode name="ipv4"> + <properties> + <help>Explicitly use IPv4 when tracing the path</help> + <completionHelp> + <list><hostname> <x.x.x.x></list> + </completionHelp> + </properties> + <command>/usr/bin/traceroute -4 "$3"</command> + <children> + <node name="tcp"> + <properties> + <help>Route tracing and port detection using TCP</help> + </properties> + <command>sudo /usr/bin/tcptraceroute "$3" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/bin/tcptraceroute "$3" $6</command> + </tagNode> + </children> + </node> + </children> + </tagNode> + <tagNode name="ipv6"> + <properties> + <help>Explicitly use IPv6 when tracing the path</help> + <completionHelp> + <list><hostname> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/traceroute -6 "$3"</command> + <children> + <node name="tcp"> + <properties> + <help>Use TCP/IPv6 packets to perform a traceroute</help> + </properties> + <command>sudo /usr/bin/tcptraceroute6 "$3" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/bin/tcptraceroute6 "$3" $6</command> + </tagNode> + </children> + </node> + </children> + </tagNode> + <tagNode name="vrf"> + <properties> + <help>Track network path to specified node via given VRF</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <children> + <!-- we need an empty tagNode to pass in a plain fqdn/ip address and + let traceroute decide how to handle this parameter --> + <tagNode name=""> + <properties> + <help>Track network path to specified node via given VRF</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute "$4"</command> + </tagNode> + <tagNode name="ipv4"> + <properties> + <help>Explicitly use IPv4 when tracing the path via given VRF</help> + <completionHelp> + <list><hostname> <x.x.x.x></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -4 "$5"</command> + <children> + <node name="tcp"> + <properties> + <help>Route tracing and port detection using TCP</help> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute "$5" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute "$5" $8</command> + </tagNode> + </children> + </node> + </children> + </tagNode> + <tagNode name="ipv6"> + <properties> + <help>Explicitly use IPv6 when tracing the path via given VRF</help> + <completionHelp> + <list><hostname> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -6 "$5"</command> + <children> + <node name="tcp"> + <properties> + <help>Use TCP/IPv6 packets to perform a traceroute</help> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute6 "$5" </command> + <children> + <tagNode name="port"> + <properties> + <help>TCP port to connect to for path tracing</help> + <completionHelp> + <list>0-65535</list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/tcptraceroute6 "$5" $8</command> + </tagNode> + </children> + </node> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + <node name="monitor"> + <children> + <tagNode name="traceroute"> + <properties> + <help>Monitor path to destination in realtime</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>/usr/bin/mtr "$3"</command> + </tagNode> + <node name="traceroute"> + <children> + <tagNode name="ipv4"> + <properties> + <help>IPv4 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>/usr/bin/mtr -4 "$4"</command> + </tagNode> + <tagNode name="ipv6"> + <properties> + <help>IPv6 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>/usr/bin/mtr -6 "$4"</command> + </tagNode> + <tagNode name="vrf"> + <properties> + <help>Monitor path to destination in realtime via given VRF</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <children> + <tagNode name="ipv4"> + <properties> + <help>IPv4 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr -4 "$6"</command> + </tagNode> + <tagNode name="ipv6"> + <properties> + <help>IPv6 fully qualified domain name (FQDN)</help> + <completionHelp> + <list><fqdn></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr -6 "$6"</command> + </tagNode> + <tagNode name=""> + <properties> + <help>Track network path to specified node via given VRF</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr "$5"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/traffic-dump.xml b/op-mode-definitions/traffic-dump.xml new file mode 100644 index 000000000..6d86f7423 --- /dev/null +++ b/op-mode-definitions/traffic-dump.xml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="traffic"> + <properties> + <help>Monitor traffic dumps</help> + </properties> + <children> + <tagNode name="interface"> + <command>sudo tcpdump -i $4</command> + <properties> + <help>Monitor traffic dump from an interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_dumpable_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="filter"> + <command>sudo tcpdump -n -i $4 "${@:6}"</command> + <properties> + <help>Monitor traffic matching filter conditions</help> + </properties> + </tagNode> + <tagNode name="save"> + <command>sudo tcpdump -n -i $4 -w $6</command> + <properties> + <help>Save traffic dump from an interface to a file</help> + </properties> + <children> + <tagNode name="filter"> + <command>sudo tcpdump -n -i $4 -w $6 "${@:8}"</command> + <properties> + <help>Save a dump of traffic matching filter conditions to a file</help> + </properties> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/vrrp.xml b/op-mode-definitions/vrrp.xml new file mode 100644 index 000000000..856fb440d --- /dev/null +++ b/op-mode-definitions/vrrp.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vrrp"> + <properties> + <help>Show VRRP (Virtual Router Redundancy Protocol) information</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vrrp.py --summary</command> + <children> + <node name="statistics"> + <properties> + <help>Show VRRP statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vrrp.py --statistics</command> + </node> + <node name="detail"> + <properties> + <help>Show detailed VRRP state information</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/vrrp.py --data</command> + </node> + </children> + </node> + </children> + </node> + <node name="restart"> + <children> + <node name="vrrp"> + <properties> + <help>Restart the VRRP (Virtual Router Redundancy Protocol) process</help> + </properties> + <command>sudo systemctl restart keepalived.service</command> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/wake-on-lan.xml b/op-mode-definitions/wake-on-lan.xml new file mode 100644 index 000000000..1a9b88596 --- /dev/null +++ b/op-mode-definitions/wake-on-lan.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="wake-on-lan"> + <properties> + <help>Send Wake-On-LAN (WOL) Magic Packet</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Interface where the station is connected</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="host"> + <properties> + <help>Station (MAC) address to wake up</help> + </properties> + <command>sudo /usr/sbin/etherwake -i "$3" "$5"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/wireguard.xml b/op-mode-definitions/wireguard.xml new file mode 100644 index 000000000..a7bfa36a3 --- /dev/null +++ b/op-mode-definitions/wireguard.xml @@ -0,0 +1,138 @@ +<?xml version="1.0"?> +<!-- wireguard key management --> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="wireguard"> + <properties> + <help>wireguard key generation utility</help> + </properties> + <children> + <leafNode name="default-keypair"> + <properties> + <help>generates the wireguard default-keypair</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey</command> + </leafNode> + <leafNode name="preshared-key"> + <properties> + <help>generate a wireguard preshared key</help> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --genpsk</command> + </leafNode> + <tagNode name="named-keypairs"> + <properties> + <help>Generates named wireguard keypairs</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4"</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="wireguard"> + <properties> + <help>Show wireguard properties</help> + </properties> + <children> + <node name="keypairs"> + <properties> + <help>Shows named wireguard keys</help> + </properties> + <children> + <tagNode name="pubkey"> + <properties> + <help>Show wireguard private named key</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5"</command> + </tagNode> + <tagNode name="privkey"> + <properties> + <help>Show wireguard public named key</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5"</command> + </tagNode> + </children> + </node> + </children> + </node> + <node name="interfaces"> + <children> + <tagNode name="wireguard"> + <properties> + <help>show wireguard interface information</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard.py --showinterface "$4"</command> + <children> + <leafNode name="allowed-ips"> + <properties> + <help>show all allowed-ips for the specified interface</help> + </properties> + <command>sudo wg show "$4" allowed-ips</command> + </leafNode> + <leafNode name="endpoints"> + <properties> + <help>show all endpoints for the specified interface</help> + </properties> + <command>sudo wg show "$4" endpoints</command> + </leafNode> + <leafNode name="peers"> + <properties> + <help>show all peer IDs for the specified interface</help> + </properties> + <command>sudo wg show "$4" peers</command> + </leafNode> + <!-- more commands upon request --> + </children> + </tagNode> + <node name="wireguard"> + <properties> + <help>Show wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="delete"> + <children> + <node name="wireguard"> + <properties> + <help>Delete wireguard properties</help> + </properties> + <children> + <tagNode name="keypair"> + <properties> + <help>Delete a wireguard keypair</help> + <completionHelp> + <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4"</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> + diff --git a/op-mode-definitions/wireless.xml b/op-mode-definitions/wireless.xml new file mode 100644 index 000000000..a3a9d1f55 --- /dev/null +++ b/op-mode-definitions/wireless.xml @@ -0,0 +1,119 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="clear"> + <children> + <node name="interfaces"> + <children> + <node name="wireless"> + <properties> + <help>Clear wireless interface information</help> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear all wireless interface counters</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_interfaces.py --action=clear --intf-type="$3"</command> + </leafNode> + </children> + </node> + <tagNode name="wireless"> + <properties> + <help>Clear interface information for a given wireless interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type wireless</script> + </completionHelp> + </properties> + <children> + <leafNode name="counters"> + <properties> + <help>Clear all wireless interface counters</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_interfaces.py --action=clear --intf="$4"</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <node name="wireless"> + <properties> + <help>Show wireless interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireless --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed wireless interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireless --action=show</command> + </leafNode> + <leafNode name="info"> + <properties> + <help>Show wireless interface configuration</help> + </properties> + <command>${vyos_op_scripts_dir}/show_wireless.py --brief</command> + </leafNode> + </children> + </node> + <tagNode name="wireless"> + <properties> + <help>Show specified wireless interface information</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type wireless</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified wireless interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + <node name="scan"> + <properties> + <help>Show summary of the specified wireless interface information</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/show_wireless.py --scan "$4"</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed scan results</help> + </properties> + <command>sudo /sbin/iw dev "$4" scan ap-force</command> + </leafNode> + </children> + </node> + <leafNode name="stations"> + <properties> + <help>Show specified wireless interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_wireless.py --stations "$4"</command> + </leafNode> + <tagNode name="vif"> + <properties> + <help>Show specified virtual network interface (vif) information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of specified virtual network interface (vif) information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 000000000..e2d28bd6b --- /dev/null +++ b/python/setup.py @@ -0,0 +1,27 @@ +import os +from setuptools import setup + +def packages(directory): + return [ + _[0].replace('/','.') + for _ in os.walk(directory) + if os.path.isfile(os.path.join(_[0], '__init__.py')) + ] + +setup( + name = "vyos", + version = "1.3.0", + author = "VyOS maintainers and contributors", + author_email = "maintainers@vyos.net", + description = ("VyOS configuration libraries."), + license = "LGPLv2+", + keywords = "vyos", + url = "http://www.vyos.io", + packages = packages('vyos'), + long_description="VyOS configuration libraries", + classifiers=[ + "Development Status :: 4 - Beta", + "Topic :: Utilities", + "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", + ], +) diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py new file mode 100644 index 000000000..e3e14fdd8 --- /dev/null +++ b/python/vyos/__init__.py @@ -0,0 +1 @@ +from .base import ConfigError diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py new file mode 100644 index 000000000..510ab7f46 --- /dev/null +++ b/python/vyos/airbag.py @@ -0,0 +1,181 @@ +# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import sys +from datetime import datetime + +from vyos import debug +from vyos.logger import syslog +from vyos.version import get_version +from vyos.version import get_full_version_data + + +def enable(log=True): + if log: + _intercepting_logger() + _intercepting_exceptions() + + +_noteworthy = [] + + +def noteworthy(msg): + """ + noteworthy can be use to take note things which we may not want to + report to the user may but be worth including in bug report + if something goes wrong later on + """ + _noteworthy.append(msg) + + +# emulate a file object +class _IO(object): + def __init__(self, std, log): + self.std = std + self.log = log + + def write(self, message): + self.std.write(message) + for line in message.split('\n'): + s = line.rstrip() + if s: + self.log(s) + + def flush(self): + self.std.flush() + + def close(self): + pass + + +# The function which will be used to report information +# to users when an exception is unhandled +def bug_report(dtype, value, trace): + from traceback import format_exception + + sys.stdout.flush() + sys.stderr.flush() + + information = get_full_version_data() + trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n') + note = '' + if _noteworthy: + note = 'noteworthy:\n' + note += '\n'.join(_noteworthy) + + information.update({ + 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'trace': trace, + 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + 'note': note, + }) + + sys.stdout.write(INTRO.format(**information)) + sys.stdout.flush() + + sys.stderr.write(FAULT.format(**information)) + sys.stderr.flush() + + +# define an exception handler to be run when an exception +# reach the end of __main__ and was not intercepted +def _intercepter(dtype, value, trace): + bug_report(dtype, value, trace) + if debug.enabled('developer'): + import pdb + pdb.pm() + + +def _intercepting_logger(_singleton=[False]): + skip = _singleton.pop() + _singleton.append(True) + if skip: + return + + # log to syslog any message sent to stderr + sys.stderr = _IO(sys.stderr, syslog.critical) + + +# lists as default arguments in function is normally dangerous +# as they will keep any modification performed, unless this is +# what you want to do (in that case to only run the code once) +def _intercepting_exceptions(_singleton=[False]): + skip = _singleton.pop() + _singleton.append(True) + if skip: + return + + # install the handler to replace the default behaviour + # which just prints the exception trace on screen + sys.excepthook = _intercepter + + +# Messages to print +# if the key before the value has not time, syslog takes that as the source of the message + +FAULT = """\ +Report Time: {date} +Image Version: VyOS {version} +Release Train: {release_train} + +Built by: {built_by} +Built on: {built_on} +Build UUID: {build_uuid} +Build Commit ID: {build_git} + +Architecture: {system_arch} +Boot via: {boot_via} +System type: {system_type} + +Hardware vendor: {hardware_vendor} +Hardware model: {hardware_model} +Hardware S/N: {hardware_serial} +Hardware UUID: {hardware_uuid} + +{trace} +{note} +""" + +INTRO = """\ +VyOS had an issue completing a command. + +We are sorry that you encountered a problem while using VyOS. +There are a few things you can do to help us (and yourself): +{instructions} + +When reporting problems, please include as much information as possible: +- do not obfuscate any data (feel free to contact us privately if your + business policy requires it) +- and include all the information presented below + +""" + +COMMUNITY = """\ +- Make sure you are running the latest version of the code available at + https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso +- Consult the forum to see how to handle this issue + https://forum.vyos.io +- Join our community on slack where our users exchange help and advice + https://vyos.slack.com +""".strip() + +SUPPORTED = """\ +- Make sure you are running the latest stable version of VyOS + the code is available at https://downloads.vyos.io/?dir=release/current +- Contact us using the online help desk + https://support.vyos.io/ +- Join our community on slack where our users exchange help and advice + https://vyos.slack.com +""".strip() diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py new file mode 100644 index 000000000..66b5f4a74 --- /dev/null +++ b/python/vyos/authutils.py @@ -0,0 +1,41 @@ +# authutils -- miscelanneous functions for handling passwords and publis keys +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import re + +from vyos.util import cmd + + +def make_password_hash(password): + """ Makes a password hash for /etc/shadow using mkpasswd """ + + mkpassword = 'mkpasswd --method=sha-512 --stdin' + return cmd(mkpassword, input=password, timeout=5) + +def split_ssh_public_key(key_string, defaultname=""): + """ Splits an SSH public key into its components """ + + key_string = key_string.strip() + parts = re.split(r'\s+', key_string) + + if len(parts) == 3: + key_type, key_data, key_name = parts[0], parts[1], parts[2] + else: + key_type, key_data, key_name = parts[0], parts[1], defaultname + + if key_type not in ['ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519']: + raise ValueError("Bad key type \'{0}\', must be one of must be one of ssh-rsa, ssh-dss, ecdsa-sha2-nistp<256|384|521> or ssh-ed25519".format(key_type)) + + return({"type": key_type, "data": key_data, "name": key_name}) diff --git a/python/vyos/base.py b/python/vyos/base.py new file mode 100644 index 000000000..4e23714e5 --- /dev/null +++ b/python/vyos/base.py @@ -0,0 +1,18 @@ +# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +class ConfigError(Exception): + pass diff --git a/python/vyos/certbot_util.py b/python/vyos/certbot_util.py new file mode 100644 index 000000000..df42d4780 --- /dev/null +++ b/python/vyos/certbot_util.py @@ -0,0 +1,58 @@ +# certbot_util -- adaptation of certbot_nginx name matching functions for VyOS +# https://github.com/certbot/certbot/blob/master/LICENSE.txt + +from certbot_nginx import parser + +NAME_RANK = 0 +START_WILDCARD_RANK = 1 +END_WILDCARD_RANK = 2 +REGEX_RANK = 3 + +def _rank_matches_by_name(server_block_list, target_name): + """Returns a ranked list of server_blocks that match target_name. + Adapted from the function of the same name in + certbot_nginx.NginxConfigurator + """ + matches = [] + for server_block in server_block_list: + name_type, name = parser.get_best_match(target_name, + server_block['name']) + if name_type == 'exact': + matches.append({'vhost': server_block, + 'name': name, + 'rank': NAME_RANK}) + elif name_type == 'wildcard_start': + matches.append({'vhost': server_block, + 'name': name, + 'rank': START_WILDCARD_RANK}) + elif name_type == 'wildcard_end': + matches.append({'vhost': server_block, + 'name': name, + 'rank': END_WILDCARD_RANK}) + elif name_type == 'regex': + matches.append({'vhost': server_block, + 'name': name, + 'rank': REGEX_RANK}) + + return sorted(matches, key=lambda x: x['rank']) + +def _select_best_name_match(matches): + """Returns the best name match of a ranked list of server_blocks. + Adapted from the function of the same name in + certbot_nginx.NginxConfigurator + """ + if not matches: + return None + elif matches[0]['rank'] in [START_WILDCARD_RANK, END_WILDCARD_RANK]: + rank = matches[0]['rank'] + wildcards = [x for x in matches if x['rank'] == rank] + return max(wildcards, key=lambda x: len(x['name']))['vhost'] + else: + return matches[0]['vhost'] + +def choose_server_block(server_block_list, target_name): + matches = _rank_matches_by_name(server_block_list, target_name) + server_blocks = [x for x in [_select_best_name_match(matches)] + if x is not None] + return server_blocks + diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py new file mode 100644 index 000000000..90b458aae --- /dev/null +++ b/python/vyos/component_versions.py @@ -0,0 +1,57 @@ +# Copyright 2017 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +""" +The version data looks like: + +/* Warning: Do not remove the following line. */ +/* === vyatta-config-version: +"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1" +=== */ +/* Release version: 1.2.0-rolling+201806131737 */ +""" + +import re + +def get_component_version(string_line): + """ + Get component version dictionary from string + return empty dictionary if string contains no config information + or raise error if component version string malformed + """ + return_value = {} + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line): + + if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line): + raise ValueError("malformed configuration string: " + str(string_line)) + + for pair in re.findall(r'([\w,-]+)@(\d+)', string_line): + if pair[0] in return_value.keys(): + raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"") + return_value[pair[0]] = int(pair[1]) + + return return_value + + +def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'): + """ + Get component version dictionary parsing config file line by line + """ + f = open(config_file_name, 'r') + for line_in_config in f: + component_version = get_component_version(line_in_config) + if component_version: + return component_version + raise ValueError("no config string in file:", config_file_name) diff --git a/python/vyos/config.py b/python/vyos/config.py new file mode 100644 index 000000000..884d6d947 --- /dev/null +++ b/python/vyos/config.py @@ -0,0 +1,454 @@ +# Copyright 2017, 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +""" +A library for reading VyOS running config data. + +This library is used internally by all config scripts of VyOS, +but its API should be considered stable and safe to use +in user scripts. + +Note that this module will not work outside VyOS. + +Node taxonomy +############# + +There are multiple types of config tree nodes in VyOS, each requires +its own set of operations. + +*Leaf nodes* (such as "address" in interfaces) can have values, but cannot +have children. +Leaf nodes can have one value, multiple values, or no values at all. + +For example, "system host-name" is a single-value leaf node, +"system name-server" is a multi-value leaf node (commonly abbreviated "multi node"), +and "system ip disable-forwarding" is a valueless leaf node. + +Non-leaf nodes cannot have values, but they can have child nodes. They are divided into +two classes depending on whether the names of their children are fixed or not. +For example, under "system", the names of all valid child nodes are predefined +("login", "name-server" etc.). + +To the contrary, children of the "system task-scheduler task" node can have arbitrary names. +Such nodes are called *tag nodes*. This terminology is confusing but we keep using it for lack +of a better word. No one remembers if the "tag" in "task Foo" is "task" or "Foo", +but the distinction is irrelevant in practice. + +Configuration modes +################### + +VyOS has two distinct modes: operational mode and configuration mode. When a user logins, +the CLI is in the operational mode. In this mode, only the running (effective) config is accessible for reading. + +When a user enters the "configure" command, a configuration session is setup. Every config session +has its *proposed* (or *session*) config built on top of the current running config. When changes are commited, if commit succeeds, +the proposed config is merged into the running config. + +In configuration mode, "base" functions like `exists`, `return_value` return values from the session config, +while functions prefixed "effective" return values from the running config. + +In operational mode, all functions return values from the running config. + +""" + +import re +import json +from copy import deepcopy + +import vyos.util +import vyos.configtree +from vyos.configsource import ConfigSource, ConfigSourceSession + +class Config(object): + """ + The class of config access objects. + + Internally, in the current implementation, this object is *almost* stateless, + the only state it keeps is relative *config path* for convenient access to config + subtrees. + """ + def __init__(self, session_env=None, config_source=None): + if config_source is None: + self._config_source = ConfigSourceSession(session_env) + else: + if not isinstance(config_source, ConfigSource): + raise TypeError("config_source not of type ConfigSource") + self._config_source = config_source + + self._level = [] + self._dict_cache = {} + (self._running_config, + self._session_config) = self._config_source.get_configtree_tuple() + + def _make_path(self, path): + # Backwards-compatibility stuff: original implementation used string paths + # libvyosconfig paths are lists, but since node names cannot contain whitespace, + # splitting at whitespace is reasonably safe. + # It may cause problems with exists() when it's used for checking values, + # since values may contain whitespace. + if isinstance(path, str): + path = re.split(r'\s+', path) + elif isinstance(path, list): + pass + else: + raise TypeError("Path must be a whitespace-separated string or a list") + return (self._level + path) + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config tree path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``exists("name-server")`` is equivalent to calling + ``exists("system name-server"`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + # Make sure there's always a space between default path (level) + # and path supplied as method argument + # XXX: for small strings in-place concatenation is not a problem + if isinstance(path, str): + if path: + self._level = re.split(r'\s+', path) + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def get_level(self): + """ + Gets the current edit level. + + Returns: + str: current edit level + """ + return(self._level.copy()) + + def exists(self, path): + """ + Checks if a node with given path exists in the running or proposed config + + Returns: + True if node exists, False otherwise + + Note: + This function cannot be used outside a configuration sessions. + In operational mode scripts, use ``exists_effective``. + """ + if not self._session_config: + return False + if self._session_config.exists(self._make_path(path)): + return True + else: + # libvyosconfig exists() works only for _nodes_, not _values_ + # libvyattacfg one also worked for values, so we emulate that case here + if isinstance(path, str): + path = re.split(r'\s+', path) + path_without_value = path[:-1] + path_str = " ".join(path_without_value) + try: + value = self._session_config.return_value(self._make_path(path_str)) + return (value == path[-1]) + except vyos.configtree.ConfigTreeError: + # node doesn't exist at all + return False + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + return self._config_source.session_changed() + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + return self._config_source.in_session() + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + return self._config_source.show_config(path, default, effective) + + def get_cached_dict(self, effective=False): + cached = self._dict_cache.get(effective, {}) + if cached: + config_dict = cached + else: + config_dict = {} + + if effective: + if self._running_config: + config_dict = json.loads((self._running_config).to_json()) + else: + if self._session_config: + config_dict = json.loads((self._session_config).to_json()) + + self._dict_cache[effective] = config_dict + + return config_dict + + def get_config_dict(self, path=[], effective=False, key_mangling=None, get_first_key=False): + """ + Args: + path (str list): Configuration tree path, can be empty + effective=False: effective or session config + key_mangling=None: mangle dict keys according to regex and replacement + get_first_key=False: if k = path[:-1], return sub-dict d[k] instead of {k: d[k]} + + Returns: a dict representation of the config under path + """ + config_dict = self.get_cached_dict(effective) + + config_dict = vyos.util.get_sub_dict(config_dict, self._make_path(path), get_first_key) + + if key_mangling: + if not (isinstance(key_mangling, tuple) and \ + (len(key_mangling) == 2) and \ + isinstance(key_mangling[0], str) and \ + isinstance(key_mangling[1], str)): + raise ValueError("key_mangling must be a tuple of two strings") + else: + config_dict = vyos.util.mangle_dict_keys(config_dict, key_mangling[0], key_mangling[1]) + else: + config_dict = deepcopy(config_dict) + + return config_dict + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + self._config_source.set_level(self.get_level) + return self._config_source.is_multi(path) + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + self._config_source.set_level(self.get_level) + return self._config_source.is_tag(path) + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + self._config_source.set_level(self.get_level) + return self._config_source.is_leaf(path) + + def return_value(self, path, default=None): + """ + Retrieve a value of single-value leaf node in the running or proposed config + + Args: + path (str): Configuration tree path + default (str): Default value to return if node does not exist + + Returns: + str: Node value, if it has any + None: if node is valueless *or* if it doesn't exist + + Note: + Due to the issue with treatment of valueless nodes by this function, + valueless nodes should be checked with ``exists`` instead. + + This function cannot be used outside a configuration session. + In operational mode scripts, use ``return_effective_value``. + """ + if self._session_config: + try: + value = self._session_config.return_value(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + value = None + else: + value = None + + if not value: + return(default) + else: + return(value) + + def return_values(self, path, default=[]): + """ + Retrieve all values of a multi-value leaf node in the running or proposed config + + Args: + path (str): Configuration tree path + + Returns: + str list: Node values, if it has any + []: if node does not exist + + Note: + This function cannot be used outside a configuration session. + In operational mode scripts, use ``return_effective_values``. + """ + if self._session_config: + try: + values = self._session_config.return_values(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + values = [] + else: + values = [] + + if not values: + return(default.copy()) + else: + return(values) + + def list_nodes(self, path, default=[]): + """ + Retrieve names of all children of a tag node in the running or proposed config + + Args: + path (str): Configuration tree path + + Returns: + string list: child node names + + """ + if self._session_config: + try: + nodes = self._session_config.list_nodes(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + nodes = [] + else: + nodes = [] + + if not nodes: + return(default.copy()) + else: + return(nodes) + + def exists_effective(self, path): + """ + Check if a node exists in the running (effective) config + + Args: + path (str): Configuration tree path + + Returns: + True if node exists in the running config, False otherwise + + Note: + This function is safe to use in operational mode. In configuration mode, + it ignores uncommited changes. + """ + if self._running_config: + return(self._running_config.exists(self._make_path(path))) + + return False + + def return_effective_value(self, path, default=None): + """ + Retrieve a values of a single-value leaf node in a running (effective) config + + Args: + path (str): Configuration tree path + default (str): Default value to return if node does not exist + + Returns: + str: Node value + """ + if self._running_config: + try: + value = self._running_config.return_value(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + value = None + else: + value = None + + if not value: + return(default) + else: + return(value) + + def return_effective_values(self, path, default=[]): + """ + Retrieve all values of a multi-value node in a running (effective) config + + Args: + path (str): Configuration tree path + + Returns: + str list: A list of values + """ + if self._running_config: + try: + values = self._running_config.return_values(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + values = [] + else: + values = [] + + if not values: + return(default.copy()) + else: + return(values) + + def list_effective_nodes(self, path, default=[]): + """ + Retrieve names of all children of a tag node in the running config + + Args: + path (str): Configuration tree path + + Returns: + str list: child node names + """ + if self._running_config: + try: + nodes = self._running_config.list_nodes(self._make_path(path)) + except vyos.configtree.ConfigTreeError: + nodes = [] + else: + nodes = [] + + if not nodes: + return(default.copy()) + else: + return(nodes) diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py new file mode 100644 index 000000000..bd8624ced --- /dev/null +++ b/python/vyos/configdict.py @@ -0,0 +1,314 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +""" +A library for retrieving value dicts from VyOS configs in a declarative fashion. +""" +import os + +from enum import Enum +from copy import deepcopy + +from vyos import ConfigError + +def retrieve_config(path_hash, base_path, config): + """ + Retrieves a VyOS config as a dict according to a declarative description + + The description dict, passed in the first argument, must follow this format: + ``field_name : <path, type, [inner_options_dict]>``. + + Supported types are: ``str`` (for normal nodes), + ``list`` (returns a list of strings, for multi nodes), + ``bool`` (returns True if valueless node exists), + ``dict`` (for tag nodes, returns a dict indexed by node names, + according to description in the third item of the tuple). + + Args: + path_hash (dict): Declarative description of the config to retrieve + base_path (list): A base path to prepend to all option paths + config (vyos.config.Config): A VyOS config object + + Returns: + dict: config dict + """ + config_hash = {} + + for k in path_hash: + + if type(path_hash[k]) != tuple: + raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k]))) + if len(path_hash[k]) < 2: + raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k)) + + path = path_hash[k][0] + if type(path) != list: + raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path))) + + typ = path_hash[k][1] + if type(typ) != type: + raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ))) + + path = base_path + path + + path_str = " ".join(path) + + if typ == str: + config_hash[k] = config.return_value(path_str) + elif typ == list: + config_hash[k] = config.return_values(path_str) + elif typ == bool: + config_hash[k] = config.exists(path_str) + elif typ == dict: + try: + inner_hash = path_hash[k][2] + except IndexError: + raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k)) + config_hash[k] = {} + nodes = config.list_nodes(path_str) + for node in nodes: + config_hash[k][node] = retrieve_config(inner_hash, path + [node], config) + + return config_hash + + +def dict_merge(source, destination): + """ Merge two dictionaries. Only keys which are not present in destination + will be copied from source, anything else will be kept untouched. Function + will return a new dict which has the merged key/value pairs. """ + from copy import deepcopy + tmp = deepcopy(destination) + + for key, value in source.items(): + if key not in tmp: + tmp[key] = value + elif isinstance(source[key], dict): + tmp[key] = dict_merge(source[key], tmp[key]) + + return tmp + +def list_diff(first, second): + """ Diff two dictionaries and return only unique items """ + second = set(second) + return [item for item in first if item not in second] + +def T2665_default_dict_cleanup(dict): + """ Cleanup default keys for tag nodes https://phabricator.vyos.net/T2665. """ + # Cleanup + for vif in ['vif', 'vif_s']: + if vif in dict: + for key in ['ip', 'mtu', 'dhcpv6_options']: + if key in dict[vif]: + del dict[vif][key] + + # cleanup VIF-S defaults + if 'vif_c' in dict[vif]: + for key in ['ip', 'mtu', 'dhcpv6_options']: + if key in dict[vif]['vif_c']: + del dict[vif]['vif_c'][key] + # If there is no vif-c defined and we just cleaned the default + # keys - we can clean the entire vif-c dict as it's useless + if not dict[vif]['vif_c']: + del dict[vif]['vif_c'] + + # If there is no real vif/vif-s defined and we just cleaned the default + # keys - we can clean the entire vif dict as it's useless + if not dict[vif]: + del dict[vif] + + if 'dhcpv6_options' in dict and 'pd' in dict['dhcpv6_options']: + if 'length' in dict['dhcpv6_options']['pd']: + del dict['dhcpv6_options']['pd']['length'] + + # delete empty dicts + if 'dhcpv6_options' in dict: + if 'pd' in dict['dhcpv6_options']: + # test if 'pd' is an empty node so we can remove it + if not dict['dhcpv6_options']['pd']: + del dict['dhcpv6_options']['pd'] + + # test if 'dhcpv6_options' is an empty node so we can remove it + if not dict['dhcpv6_options']: + del dict['dhcpv6_options'] + + return dict + +def leaf_node_changed(conf, path): + """ + Check if a leaf node was altered. If it has been altered - values has been + changed, or it was added/removed, we will return the old value. If nothing + has been changed, None is returned + """ + from vyos.configdiff import get_config_diff + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + (new, old) = D.get_value_diff(path) + if new != old: + if isinstance(old, str): + return old + elif isinstance(old, list): + if isinstance(new, str): + new = [new] + elif isinstance(new, type(None)): + new = [] + return list_diff(old, new) + + return None + +def node_changed(conf, path): + """ + Check if a leaf node was altered. If it has been altered - values has been + changed, or it was added/removed, we will return the old value. If nothing + has been changed, None is returned + """ + from vyos.configdiff import get_config_diff, Diff + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(path, expand_nodes=Diff.DELETE)['delete'].keys() + return list(keys) + +def get_removed_vlans(conf, dict): + """ + Common function to parse a dictionary retrieved via get_config_dict() and + determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q. + """ + from vyos.configdiff import get_config_diff, Diff + + # Check vif, vif-s/vif-c VLAN interfaces for removal + D = get_config_diff(conf, key_mangling=('-', '_')) + D.set_level(conf.get_level()) + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(['vif'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_remove': [*keys]}) + + # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448 + keys = D.get_child_nodes_diff(['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_s_remove': [*keys]}) + + for vif in dict.get('vif_s', {}).keys(): + keys = D.get_child_nodes_diff(['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys() + if keys: + dict.update({'vif_s': { vif : {'vif_c_remove': [*keys]}}}) + + return dict + + +def dict_add_dhcpv6pd_defaults(defaults, config_dict): + # Implant default dictionary for DHCPv6-PD instances + if 'dhcpv6_options' in config_dict and 'pd' in config_dict['dhcpv6_options']: + for pd, pd_config in config_dict['dhcpv6_options']['pd'].items(): + config_dict['dhcpv6_options']['pd'][pd] = dict_merge( + defaults, pd_config) + + return config_dict + +def get_interface_dict(config, base, ifname=''): + """ + Common utility function to retrieve and mandgle the interfaces available + in CLI configuration. All interfaces have a common base ground where the + value retrival is identical - so it can and should be reused + + Will return a dictionary with the necessary interface configuration + """ + from vyos.util import vyos_dict_search + from vyos.validate import is_member + from vyos.xml import defaults + + if not ifname: + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + ifname = os.environ['VYOS_TAGNODE_VALUE'] + + # retrieve interface default values + default_values = defaults(base) + + # setup config level which is extracted in get_removed_vlans() + config.set_level(base + [ifname]) + dict = config.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + + # Check if interface has been removed + if dict == {}: + dict.update({'deleted' : ''}) + + # Add interface instance name into dictionary + dict.update({'ifname': ifname}) + + # We have gathered the dict representation of the CLI, but there are + # default options which we need to update into the dictionary + # retrived. + dict = dict_merge(default_values, dict) + + # Check if we are a member of a bridge device + bridge = is_member(config, ifname, 'bridge') + if bridge: + dict.update({'is_bridge_member' : bridge}) + + # Check if we are a member of a bond device + bond = is_member(config, ifname, 'bonding') + if bond: + dict.update({'is_bond_member' : bond}) + + mac = leaf_node_changed(config, ['mac']) + if mac: + dict.update({'mac_old' : mac}) + + eui64 = leaf_node_changed(config, ['ipv6', 'address', 'eui64']) + if eui64: + # XXX: T2636 workaround: convert string to a list with one element + if isinstance(eui64, str): + eui64 = [eui64] + tmp = vyos_dict_search('ipv6.address', dict) + if not tmp: + dict.update({'ipv6': {'address': {'eui64_old': eui64}}}) + else: + dict['ipv6']['address'].update({'eui64_old': eui64}) + + # remove wrongly inserted values + dict = T2665_default_dict_cleanup(dict) + + # Implant default dictionary for DHCPv6-PD instances + default_pd_values = defaults(base + ['dhcpv6-options', 'pd']) + dict = dict_add_dhcpv6pd_defaults(default_pd_values, dict) + + # Implant default dictionary in vif/vif-s VLAN interfaces. Values are + # identical for all types of VLAN interfaces as they all include the same + # XML definitions which hold the defaults. + default_vif_values = defaults(base + ['vif']) + for vif, vif_config in dict.get('vif', {}).items(): + dict['vif'][vif] = dict_add_dhcpv6pd_defaults( + default_pd_values, vif_config) + dict['vif'][vif] = T2665_default_dict_cleanup( + dict_merge(default_vif_values, vif_config)) + + for vif_s, vif_s_config in dict.get('vif_s', {}).items(): + dict['vif_s'][vif_s] = dict_add_dhcpv6pd_defaults( + default_pd_values, vif_s_config) + dict['vif_s'][vif_s] = T2665_default_dict_cleanup( + dict_merge(default_vif_values, vif_s_config)) + for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): + dict['vif_s'][vif_s]['vif_c'][vif_c] = dict_add_dhcpv6pd_defaults( + default_pd_values, vif_c_config) + dict['vif_s'][vif_s]['vif_c'][vif_c] = T2665_default_dict_cleanup( + dict_merge(default_vif_values, vif_c_config)) + + # Check vif, vif-s/vif-c VLAN interfaces for removal + dict = get_removed_vlans(config, dict) + + return dict + diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py new file mode 100644 index 000000000..b79893507 --- /dev/null +++ b/python/vyos/configdiff.py @@ -0,0 +1,249 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +from enum import IntFlag, auto + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.util import get_sub_dict, mangle_dict_keys +from vyos.xml import defaults + +class ConfigDiffError(Exception): + """ + Raised on config dict access errors, for example, calling get_value on + a non-leaf node. + """ + pass + +def enum_to_key(e): + return e.name.lower() + +class Diff(IntFlag): + MERGE = auto() + DELETE = auto() + ADD = auto() + STABLE = auto() + +requires_effective = [enum_to_key(Diff.DELETE)] +target_defaults = [enum_to_key(Diff.MERGE)] + +def _key_sets_from_dicts(session_dict, effective_dict): + session_keys = list(session_dict) + effective_keys = list(effective_dict) + + ret = {} + stable_keys = [k for k in session_keys if k in effective_keys] + + ret[enum_to_key(Diff.MERGE)] = session_keys + ret[enum_to_key(Diff.DELETE)] = [k for k in effective_keys if k not in stable_keys] + ret[enum_to_key(Diff.ADD)] = [k for k in session_keys if k not in stable_keys] + ret[enum_to_key(Diff.STABLE)] = stable_keys + + return ret + +def _dict_from_key_set(key_set, d): + # This will always be applied to a key_set obtained from a get_sub_dict, + # hence there is no possibility of KeyError, as get_sub_dict guarantees + # a return type of dict + ret = {k: d[k] for k in key_set} + + return ret + +def get_config_diff(config, key_mangling=None): + """ + Check type and return ConfigDiff instance. + """ + if not config or not isinstance(config, Config): + raise TypeError("argument must me a Config instance") + if key_mangling and not (isinstance(key_mangling, tuple) and \ + (len(key_mangling) == 2) and \ + isinstance(key_mangling[0], str) and \ + isinstance(key_mangling[1], str)): + raise ValueError("key_mangling must be a tuple of two strings") + + return ConfigDiff(config, key_mangling) + +class ConfigDiff(object): + """ + The class of config changes as represented by comparison between the + session config dict and the effective config dict. + """ + def __init__(self, config, key_mangling=None): + self._level = config.get_level() + self._session_config_dict = config.get_cached_dict() + self._effective_config_dict = config.get_cached_dict(effective=True) + self._key_mangling = key_mangling + + # mirrored from Config; allow path arguments relative to level + def _make_path(self, path): + if isinstance(path, str): + path = path.split() + elif isinstance(path, list): + pass + else: + raise TypeError("Path must be a whitespace-separated string or a list") + + ret = self._level + path + return ret + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config dict path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``get_value("name-server")`` is equivalent to calling + ``get_value("system name-server")`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + if isinstance(path, str): + if path: + self._level = path.split() + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def get_level(self): + """ + Gets the current edit level. + + Returns: + str: current edit level + """ + ret = self._level.copy() + return ret + + def _mangle_dict_keys(self, config_dict): + config_dict = mangle_dict_keys(config_dict, self._key_mangling[0], + self._key_mangling[1]) + return config_dict + + def get_child_nodes_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, under path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, + self._make_path(path), get_first_key=True) + effective_dict = get_sub_dict(self._effective_config_dict, + self._make_path(path), get_first_key=True) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + + return ret + + def get_node_diff(self, path=[], expand_nodes=Diff(0), no_defaults=False): + """ + Args: + path (str|list): config path + expand_nodes=Diff(0): bit mask of enum indicating for which nodes + to provide full dict; for example, Diff.MERGE + will expand dict['merge'] into dict under + value + no_detaults=False: if expand_nodes & Diff.MERGE, do not merge default + values to ret['merge'] + + Returns: dict of lists, representing differences between session + and effective config, at path + dict['merge'] = session config values + dict['delete'] = effective config values, not in session + dict['add'] = session config values, not in effective + dict['stable'] = config values in both session and effective + """ + session_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + effective_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + ret = _key_sets_from_dicts(session_dict, effective_dict) + + if not expand_nodes: + return ret + + for e in Diff: + if expand_nodes & e: + k = enum_to_key(e) + if k in requires_effective: + ret[k] = _dict_from_key_set(ret[k], effective_dict) + else: + ret[k] = _dict_from_key_set(ret[k], session_dict) + + if self._key_mangling: + ret[k] = self._mangle_dict_keys(ret[k]) + + if k in target_defaults and not no_defaults: + default_values = defaults(self._make_path(path)) + ret[k] = dict_merge(default_values, ret[k]) + + return ret + + def get_value_diff(self, path=[]): + """ + Args: + path (str|list): config path + + Returns: (new, old) tuple of values in session config/effective config + """ + # one should properly use is_leaf as check; for the moment we will + # deduce from type, which will not catch call on non-leaf node if None + new_value_dict = get_sub_dict(self._session_config_dict, self._make_path(path)) + old_value_dict = get_sub_dict(self._effective_config_dict, self._make_path(path)) + + new_value = None + old_value = None + if new_value_dict: + new_value = next(iter(new_value_dict.values())) + if old_value_dict: + old_value = next(iter(old_value_dict.values())) + + if new_value and isinstance(new_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + if old_value and isinstance(old_value, dict): + raise ConfigDiffError("get_value_changed called on non-leaf node") + + return new_value, old_value diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py new file mode 100644 index 000000000..0994fd974 --- /dev/null +++ b/python/vyos/configsession.py @@ -0,0 +1,191 @@ +# configsession -- the write API for the VyOS running config +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import re +import sys +import subprocess + +CLI_SHELL_API = '/bin/cli-shell-api' +SET = '/opt/vyatta/sbin/my_set' +DELETE = '/opt/vyatta/sbin/my_delete' +COMMENT = '/opt/vyatta/sbin/my_comment' +COMMIT = '/opt/vyatta/sbin/my_commit' +DISCARD = '/opt/vyatta/sbin/my_discard' +SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] +LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] +SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl'] +INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url'] +REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del'] +GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] +SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] + +# Default "commit via" string +APP = "vyos-http-api" + +# When started as a service rather than from a user shell, +# the process lacks the VyOS-specific environment that comes +# from bash configs, so we have to inject it +# XXX: maybe it's better to do via a systemd environment file +def inject_vyos_env(env): + env['VYATTA_CFG_GROUP_NAME'] = 'vyattacfg' + env['VYATTA_USER_LEVEL_DIR'] = '/opt/vyatta/etc/shell/level/admin' + env['VYATTA_PROCESS_CLIENT'] = 'gui2_rest' + env['VYOS_HEADLESS_CLIENT'] = 'vyos_http_api' + env['vyatta_bindir']= '/opt/vyatta/bin' + env['vyatta_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates' + env['vyatta_configdir'] = '/opt/vyatta/config' + env['vyatta_datadir'] = '/opt/vyatta/share' + env['vyatta_datarootdir'] = '/opt/vyatta/share' + env['vyatta_libdir'] = '/opt/vyatta/lib' + env['vyatta_libexecdir'] = '/opt/vyatta/libexec' + env['vyatta_op_templates'] = '/opt/vyatta/share/vyatta-op/templates' + env['vyatta_prefix'] = '/opt/vyatta' + env['vyatta_sbindir'] = '/opt/vyatta/sbin' + env['vyatta_sysconfdir'] = '/opt/vyatta/etc' + env['vyos_bin_dir'] = '/usr/bin' + env['vyos_cfg_templates'] = '/opt/vyatta/share/vyatta-cfg/templates' + env['vyos_completion_dir'] = '/usr/libexec/vyos/completion' + env['vyos_configdir'] = '/opt/vyatta/config' + env['vyos_conf_scripts_dir'] = '/usr/libexec/vyos/conf_mode' + env['vyos_datadir'] = '/opt/vyatta/share' + env['vyos_datarootdir']= '/opt/vyatta/share' + env['vyos_libdir'] = '/opt/vyatta/lib' + env['vyos_libexec_dir'] = '/usr/libexec/vyos' + env['vyos_op_scripts_dir'] = '/usr/libexec/vyos/op_mode' + env['vyos_op_templates'] = '/opt/vyatta/share/vyatta-op/templates' + env['vyos_prefix'] = '/opt/vyatta' + env['vyos_sbin_dir'] = '/usr/sbin' + env['vyos_validators_dir'] = '/usr/libexec/vyos/validators' + + return env + + +class ConfigSessionError(Exception): + pass + + +class ConfigSession(object): + """ + The write API of VyOS. + """ + def __init__(self, session_id, app=APP): + """ + Creates a new config session. + + Args: + session_id (str): Session identifier + app (str): Application name, purely informational + + Note: + The session identifier MUST be globally unique within the system. + The best practice is to only have one ConfigSession object per process + and used the PID for the session identifier. + """ + + env_str = subprocess.check_output([CLI_SHELL_API, 'getSessionEnv', str(session_id)]) + self.__session_id = session_id + + # Extract actual variables from the chunk of shell it outputs + # XXX: it's better to extend cli-shell-api to provide easily readable output + env_list = re.findall(r'([A-Z_]+)=([^;\s]+)', env_str.decode()) + + session_env = os.environ + session_env = inject_vyos_env(session_env) + for k, v in env_list: + session_env[k] = v + + self.__session_env = session_env + self.__session_env["COMMIT_VIA"] = app + + self.__run_command([CLI_SHELL_API, 'setupSession']) + + def __del__(self): + try: + output = subprocess.check_output([CLI_SHELL_API, 'teardownSession'], env=self.__session_env).decode().strip() + if output: + print("cli-shell-api teardownSession output for sesion {0}: {1}".format(self.__session_id, output), file=sys.stderr) + except Exception as e: + print("Could not tear down session {0}: {1}".format(self.__session_id, e), file=sys.stderr) + + def __run_command(self, cmd_list): + p = subprocess.Popen(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.__session_env) + result = p.wait() + output = p.stdout.read().decode() + p.communicate() + if result != 0: + raise ConfigSessionError(output) + return output + + def get_session_env(self): + return self.__session_env + + def set(self, path, value=None): + if not value: + value = [] + else: + value = [value] + self.__run_command([SET] + path + value) + + def delete(self, path, value=None): + if not value: + value = [] + else: + value = [value] + self.__run_command([DELETE] + path + value) + + def comment(self, path, value=None): + if not value: + value = [""] + else: + value = [value] + self.__run_command([COMMENT] + path + value) + + def commit(self): + out = self.__run_command([COMMIT]) + return out + + def discard(self): + self.__run_command([DISCARD]) + + def show_config(self, path, format='raw'): + config_data = self.__run_command(SHOW_CONFIG + path) + + if format == 'raw': + return config_data + + def load_config(self, file_path): + out = self.__run_command(LOAD_CONFIG + [file_path]) + return out + + def save_config(self, file_path): + out = self.__run_command(SAVE_CONFIG + [file_path]) + return out + + def install_image(self, url): + out = self.__run_command(INSTALL_IMAGE + [url]) + return out + + def remove_image(self, name): + out = self.__run_command(REMOVE_IMAGE + [name]) + return out + + def generate(self, path): + out = self.__run_command(GENERATE + path) + return out + + def show(self, path): + out = self.__run_command(SHOW + path) + return out + diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py new file mode 100644 index 000000000..50222e385 --- /dev/null +++ b/python/vyos/configsource.py @@ -0,0 +1,318 @@ + +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import subprocess + +from vyos.configtree import ConfigTree + +class VyOSError(Exception): + """ + Raised on config access errors. + """ + pass + +class ConfigSourceError(Exception): + ''' + Raised on error in ConfigSource subclass init. + ''' + pass + +class ConfigSource: + def __init__(self): + self._running_config: ConfigTree = None + self._session_config: ConfigTree = None + + def get_configtree_tuple(self): + return self._running_config, self._session_config + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + raise NotImplementedError(f"function not available for {type(self)}") + +class ConfigSourceSession(ConfigSource): + def __init__(self, session_env=None): + super().__init__() + self._cli_shell_api = "/bin/cli-shell-api" + self._level = [] + if session_env: + self.__session_env = session_env + else: + self.__session_env = None + + # Running config can be obtained either from op or conf mode, it always succeeds + # once the config system is initialized during boot; + # before initialization, set to empty string + if os.path.isfile('/tmp/vyos-config-status'): + try: + running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + running_config_text = '' + else: + running_config_text = '' + + # Session config ("active") only exists in conf mode. + # In op mode, we'll just use the same running config for both active and session configs. + if self.in_session(): + try: + session_config_text = self._run([self._cli_shell_api, '--show-working-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig']) + except VyOSError: + session_config_text = '' + else: + session_config_text = running_config_text + + if running_config_text: + self._running_config = ConfigTree(running_config_text) + else: + self._running_config = None + + if session_config_text: + self._session_config = ConfigTree(session_config_text) + else: + self._session_config = None + + def _make_command(self, op, path): + args = path.split() + cmd = [self._cli_shell_api, op] + args + return cmd + + def _run(self, cmd): + if self.__session_env: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, env=self.__session_env) + else: + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + out = p.stdout.read() + p.wait() + p.communicate() + if p.returncode != 0: + raise VyOSError() + else: + return out.decode('ascii') + + def set_level(self, path): + """ + Set the *edit level*, that is, a relative config tree path. + Once set, all operations will be relative to this path, + for example, after ``set_level("system")``, calling + ``exists("name-server")`` is equivalent to calling + ``exists("system name-server"`` without ``set_level``. + + Args: + path (str|list): relative config path + """ + # Make sure there's always a space between default path (level) + # and path supplied as method argument + # XXX: for small strings in-place concatenation is not a problem + if isinstance(path, str): + if path: + self._level = re.split(r'\s+', path) + else: + self._level = [] + elif isinstance(path, list): + self._level = path.copy() + else: + raise TypeError("Level path must be either a whitespace-separated string or a list") + + def session_changed(self): + """ + Returns: + True if the config session has uncommited changes, False otherwise. + """ + try: + self._run(self._make_command('sessionChanged', '')) + return True + except VyOSError: + return False + + def in_session(self): + """ + Returns: + True if called from a configuration session, False otherwise. + """ + try: + self._run(self._make_command('inSession', '')) + return True + except VyOSError: + return False + + def show_config(self, path=[], default=None, effective=False): + """ + Args: + path (str|list): Configuration tree path, or empty + default (str): Default value to return + + Returns: + str: working configuration + """ + + # show_config should be independent of CLI edit level. + # Set the CLI edit environment to the top level, and + # restore original on exit. + save_env = self.__session_env + + env_str = self._run(self._make_command('getEditResetEnv', '')) + env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) + root_env = os.environ + for k, v in env_list: + root_env[k] = v + + self.__session_env = root_env + + # FIXUP: by default, showConfig will give you a diff + # if there are uncommitted changes. + # The config parser obviously cannot work with diffs, + # so we need to supress diff production using appropriate + # options for getting either running (active) + # or proposed (working) config. + if effective: + path = ['--show-active-only'] + path + else: + path = ['--show-working-only'] + path + + if isinstance(path, list): + path = " ".join(path) + try: + out = self._run(self._make_command('showConfig', path)) + self.__session_env = save_env + return out + except VyOSError: + self.__session_env = save_env + return(default) + + def is_multi(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node can have multiple values, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isMulti', path)) + return True + except VyOSError: + return False + + def is_tag(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a tag node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isTag', path)) + return True + except VyOSError: + return False + + def is_leaf(self, path): + """ + Args: + path (str): Configuration tree path + + Returns: + True if a node is a leaf node, False otherwise. + + Note: + It also returns False if node doesn't exist. + """ + try: + path = " ".join(self._level) + " " + path + self._run(self._make_command('isLeaf', path)) + return True + except VyOSError: + return False + +class ConfigSourceString(ConfigSource): + def __init__(self, running_config_text=None, session_config_text=None): + super().__init__() + + try: + self._running_config = ConfigTree(running_config_text) if running_config_text else None + self._session_config = ConfigTree(session_config_text) if session_config_text else None + except ValueError: + raise ConfigSourceError(f"Init error in {type(self)}") diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py new file mode 100644 index 000000000..d8ffaca99 --- /dev/null +++ b/python/vyos/configtree.py @@ -0,0 +1,283 @@ +# configtree -- a standalone VyOS config file manipulation library (Python bindings) +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import re +import json + +from ctypes import cdll, c_char_p, c_void_p, c_int + + +def escape_backslash(string: str) -> str: + """Escape single backslashes in string that are not in escape sequence""" + p = re.compile(r'(?<!\\)[\\](?!b|f|n|r|t|\\[^bfnrt])') + result = p.sub(r'\\\\', string) + return result + +def extract_version(s): + """ Extract the version string from the config string """ + t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE) + return (s, ''.join(t[1:])) + +def check_path(path): + # Necessary type checking + if not isinstance(path, list): + raise TypeError("Expected a list, got a {}".format(type(path))) + else: + pass + + +class ConfigTreeError(Exception): + pass + + +class ConfigTree(object): + def __init__(self, config_string, libpath='/usr/lib/libvyosconfig.so.0'): + self.__config = None + self.__lib = cdll.LoadLibrary(libpath) + + # Import functions + self.__from_string = self.__lib.from_string + self.__from_string.argtypes = [c_char_p] + self.__from_string.restype = c_void_p + + self.__get_error = self.__lib.get_error + self.__get_error.argtypes = [] + self.__get_error.restype = c_char_p + + self.__to_string = self.__lib.to_string + self.__to_string.argtypes = [c_void_p] + self.__to_string.restype = c_char_p + + self.__to_commands = self.__lib.to_commands + self.__to_commands.argtypes = [c_void_p] + self.__to_commands.restype = c_char_p + + self.__to_json = self.__lib.to_json + self.__to_json.argtypes = [c_void_p] + self.__to_json.restype = c_char_p + + self.__to_json_ast = self.__lib.to_json_ast + self.__to_json_ast.argtypes = [c_void_p] + self.__to_json_ast.restype = c_char_p + + self.__set_add_value = self.__lib.set_add_value + self.__set_add_value.argtypes = [c_void_p, c_char_p, c_char_p] + self.__set_add_value.restype = c_int + + self.__delete_value = self.__lib.delete_value + self.__delete_value.argtypes = [c_void_p, c_char_p, c_char_p] + self.__delete_value.restype = c_int + + self.__delete = self.__lib.delete_node + self.__delete.argtypes = [c_void_p, c_char_p] + self.__delete.restype = c_int + + self.__rename = self.__lib.rename_node + self.__rename.argtypes = [c_void_p, c_char_p, c_char_p] + self.__rename.restype = c_int + + self.__copy = self.__lib.copy_node + self.__copy.argtypes = [c_void_p, c_char_p, c_char_p] + self.__copy.restype = c_int + + self.__set_replace_value = self.__lib.set_replace_value + self.__set_replace_value.argtypes = [c_void_p, c_char_p, c_char_p] + self.__set_replace_value.restype = c_int + + self.__set_valueless = self.__lib.set_valueless + self.__set_valueless.argtypes = [c_void_p, c_char_p] + self.__set_valueless.restype = c_int + + self.__exists = self.__lib.exists + self.__exists.argtypes = [c_void_p, c_char_p] + self.__exists.restype = c_int + + self.__list_nodes = self.__lib.list_nodes + self.__list_nodes.argtypes = [c_void_p, c_char_p] + self.__list_nodes.restype = c_char_p + + self.__return_value = self.__lib.return_value + self.__return_value.argtypes = [c_void_p, c_char_p] + self.__return_value.restype = c_char_p + + self.__return_values = self.__lib.return_values + self.__return_values.argtypes = [c_void_p, c_char_p] + self.__return_values.restype = c_char_p + + self.__is_tag = self.__lib.is_tag + self.__is_tag.argtypes = [c_void_p, c_char_p] + self.__is_tag.restype = c_int + + self.__set_tag = self.__lib.set_tag + self.__set_tag.argtypes = [c_void_p, c_char_p] + self.__set_tag.restype = c_int + + self.__destroy = self.__lib.destroy + self.__destroy.argtypes = [c_void_p] + + config_section, version_section = extract_version(config_string) + config_section = escape_backslash(config_section) + config = self.__from_string(config_section.encode()) + if config is None: + msg = self.__get_error().decode() + raise ValueError("Failed to parse config: {0}".format(msg)) + else: + self.__config = config + self.__version = version_section + + def __del__(self): + if self.__config is not None: + self.__destroy(self.__config) + + def __str__(self): + return self.to_string() + + def to_string(self): + config_string = self.__to_string(self.__config).decode() + config_string = "{0}\n{1}".format(config_string, self.__version) + return config_string + + def to_commands(self): + return self.__to_commands(self.__config).decode() + + def to_json(self): + return self.__to_json(self.__config).decode() + + def to_json_ast(self): + return self.__to_json_ast(self.__config).decode() + + def set(self, path, value=None, replace=True): + """Set new entry in VyOS configuration. + path: configuration path e.g. 'system dns forwarding listen-address' + value: value to be added to node, e.g. '172.18.254.201' + replace: True: current occurance will be replaced + False: new value will be appended to current occurances - use + this for adding values to a multi node + """ + + check_path(path) + path_str = " ".join(map(str, path)).encode() + + if value is None: + self.__set_valueless(self.__config, path_str) + else: + if replace: + self.__set_replace_value(self.__config, path_str, str(value).encode()) + else: + self.__set_add_value(self.__config, path_str, str(value).encode()) + + def delete(self, path): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + self.__delete(self.__config, path_str) + + def delete_value(self, path, value): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + self.__delete_value(self.__config, path_str, value.encode()) + + def rename(self, path, new_name): + check_path(path) + path_str = " ".join(map(str, path)).encode() + newname_str = new_name.encode() + + # Check if a node with intended new name already exists + new_path = path[:-1] + [new_name] + if self.exists(new_path): + raise ConfigTreeError() + res = self.__rename(self.__config, path_str, newname_str) + if (res != 0): + raise ConfigTreeError("Path [{}] doesn't exist".format(path)) + + def copy(self, old_path, new_path): + check_path(old_path) + check_path(new_path) + oldpath_str = " ".join(map(str, old_path)).encode() + newpath_str = " ".join(map(str, new_path)).encode() + + # Check if a node with intended new name already exists + if self.exists(new_path): + raise ConfigTreeError() + res = self.__copy(self.__config, oldpath_str, newpath_str) + if (res != 0): + raise ConfigTreeError("Path [{}] doesn't exist".format(old_path)) + + def exists(self, path): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + res = self.__exists(self.__config, path_str) + if (res == 0): + return False + else: + return True + + def list_nodes(self, path): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + res_json = self.__list_nodes(self.__config, path_str).decode() + res = json.loads(res_json) + + if res is None: + raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) + else: + return res + + def return_value(self, path): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + res_json = self.__return_value(self.__config, path_str).decode() + res = json.loads(res_json) + + if res is None: + raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) + else: + return res + + def return_values(self, path): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + res_json = self.__return_values(self.__config, path_str).decode() + res = json.loads(res_json) + + if res is None: + raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) + else: + return res + + def is_tag(self, path): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + res = self.__is_tag(self.__config, path_str) + if (res >= 1): + return True + else: + return False + + def set_tag(self, path): + check_path(path) + path_str = " ".join(map(str, path)).encode() + + res = self.__set_tag(self.__config, path_str) + if (res == 0): + return True + else: + raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) + diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py new file mode 100644 index 000000000..7e1930878 --- /dev/null +++ b/python/vyos/configverify.py @@ -0,0 +1,139 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +# The sole purpose of this module is to hold common functions used in +# all kinds of implementations to verify the CLI configuration. +# It is started by migrating the interfaces to the new get_config_dict() +# approach which will lead to a lot of code that can be reused. + +# NOTE: imports should be as local as possible to the function which +# makes use of it! + +from vyos import ConfigError + +def verify_vrf(config): + """ + Common helper function used by interface implementations to perform + recurring validation of VRF configuration. + """ + from netifaces import interfaces + if 'vrf' in config: + if config['vrf'] not in interfaces(): + raise ConfigError('VRF "{vrf}" does not exist'.format(**config)) + + if 'is_bridge_member' in config: + raise ConfigError( + 'Interface "{ifname}" cannot be both a member of VRF "{vrf}" ' + 'and bridge "{is_bridge_member}"!'.format(**config)) + + +def verify_address(config): + """ + Common helper function used by interface implementations to perform + recurring validation of IP address assignment when interface is part + of a bridge or bond. + """ + if {'is_bridge_member', 'address'} <= set(config): + raise ConfigError( + 'Cannot assign address to interface "{ifname}" as it is a ' + 'member of bridge "{is_bridge_member}"!'.format(**config)) + + +def verify_bridge_delete(config): + """ + Common helper function used by interface implementations to + perform recurring validation of IP address assignmenr + when interface also is part of a bridge. + """ + if 'is_bridge_member' in config: + raise ConfigError( + 'Interface "{ifname}" cannot be deleted as it is a ' + 'member of bridge "{is_bridge_member}"!'.format(**config)) + +def verify_interface_exists(config): + """ + Common helper function used by interface implementations to perform + recurring validation if an interface actually exists. + """ + from netifaces import interfaces + if not config['ifname'] in interfaces(): + raise ConfigError('Interface "{ifname}" does not exist!' + .format(**config)) + +def verify_source_interface(config): + """ + Common helper function used by interface implementations to + perform recurring validation of the existence of a source-interface + required by e.g. peth/MACvlan, MACsec ... + """ + from netifaces import interfaces + if 'source_interface' not in config: + raise ConfigError('Physical source-interface required for ' + 'interface "{ifname}"'.format(**config)) + if config['source_interface'] not in interfaces(): + raise ConfigError('Source interface {source_interface} does not ' + 'exist'.format(**config)) + +def verify_dhcpv6(config): + """ + Common helper function used by interface implementations to perform + recurring validation of DHCPv6 options which are mutually exclusive. + """ + if 'dhcpv6_options' in config: + from vyos.util import vyos_dict_search + + if {'parameters_only', 'temporary'} <= set(config['dhcpv6_options']): + raise ConfigError('DHCPv6 temporary and parameters-only options ' + 'are mutually exclusive!') + + # It is not allowed to have duplicate SLA-IDs as those identify an + # assigned IPv6 subnet from a delegated prefix + for pd in vyos_dict_search('dhcpv6_options.pd', config): + sla_ids = [] + for interface in vyos_dict_search(f'dhcpv6_options.pd.{pd}.interface', config): + sla_id = vyos_dict_search( + f'dhcpv6_options.pd.{pd}.interface.{interface}.sla_id', config) + sla_ids.append(sla_id) + + # Check for duplicates + duplicates = [x for n, x in enumerate(sla_ids) if x in sla_ids[:n]] + if duplicates: + raise ConfigError('Site-Level Aggregation Identifier (SLA-ID) ' + 'must be unique per prefix-delegation!') + +def verify_vlan_config(config): + """ + Common helper function used by interface implementations to perform + recurring validation of interface VLANs + """ + # 802.1q VLANs + for vlan in config.get('vif', {}): + vlan = config['vif'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) + + # 802.1ad (Q-in-Q) VLANs + for vlan in config.get('vif_s', {}): + vlan = config['vif_s'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) + + for vlan in config.get('vif_s', {}).get('vif_c', {}): + vlan = config['vif_c'][vlan] + verify_dhcpv6(vlan) + verify_address(vlan) + verify_vrf(vlan) diff --git a/python/vyos/debug.py b/python/vyos/debug.py new file mode 100644 index 000000000..6ce42b173 --- /dev/null +++ b/python/vyos/debug.py @@ -0,0 +1,205 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +from datetime import datetime + +def message(message, flag='', destination=sys.stdout): + """ + print a debug message line on stdout if debugging is enabled for the flag + also log it to a file if the flag 'log' is enabled + + message: the message to print + flag: which flag must be set for it to print + destination: which file like object to write to (default: sys.stdout) + + returns if any message was logged or not + """ + enable = enabled(flag) + if enable: + destination.write(_format(flag,message)) + + # the log flag is special as it logs all the commands + # executed to a log + logfile = _logfile('log', '/tmp/developer-log') + if not logfile: + return enable + + try: + # at boot the file is created as root:vyattacfg + # at runtime the file is created as user:vyattacfg + # but the helper scripts are not run as this so it + # need the default permission to be 666 (an not 660) + mask = os.umask(0o111) + + with open(logfile, 'a') as f: + f.write(_timed(_format('log', message))) + finally: + os.umask(mask) + + return enable + + +def enabled(flag): + """ + a flag can be set by touching the file in /tmp or /config + + The current flags are: + - developer: the code will drop into PBD on un-handled exception + - log: the code will log all command to a file + - ifconfig: when modifying an interface, + prints command with result and sysfs access on stdout for interface + - command: print command run with result + + Having the flag setup on the filesystem is required to have + debuging at boot time, however, setting the flag via environment + does not require a seek to the filesystem and is more efficient + it can be done on the shell on via .bashrc for the user + + The function returns an empty string if the flag was not set otherwise + the function returns either the file or environment name used to set it up + """ + + # this is to force all new flags to be registered here to be + # documented both here and a reminder to update readthedocs :-) + if flag not in ['developer', 'log', 'ifconfig', 'command']: + return '' + + return _fromenv(flag) or _fromfile(flag) + + +def _timed(message): + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + return f'{now} {message}' + + +def _remove_invisible(string): + for char in ('\0', '\a', '\b', '\f', '\v'): + string = string.replace(char, '') + return string + + +def _format(flag, message): + """ + format a log message + """ + message = _remove_invisible(message) + return f'DEBUG/{flag.upper():<7} {message}\n' + + +def _fromenv(flag): + """ + check if debugging is set for this flag via environment + + For a given debug flag named "test" + The presence of the environment VYOS_TEST_DEBUG (uppercase) enables it + + return empty string if not + return content of env value it is + """ + + flagname = f'VYOS_{flag.upper()}_DEBUG' + flagenv = os.environ.get(flagname, None) + + if flagenv is None: + return '' + return flagenv + + +def _fromfile(flag): + """ + Check if debug exist for a given debug flag name + + Check is a debug flag was set by the user. the flag can be set either: + - in /tmp for a non-persistent presence between reboot + - in /config for always on (an existence at boot time) + + For a given debug flag named "test" + The presence of the file vyos.test.debug (all lowercase) enables it + + The function returns an empty string if the flag was not set otherwise + the function returns the full flagname + """ + + for folder in ('/tmp', '/config'): + flagfile = f'{folder}/vyos.{flag}.debug' + if os.path.isfile(flagfile): + return flagfile + + return '' + + +def _contentenv(flag): + return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip() + + +def _contentfile(flag, default=''): + """ + Check if debug exist for a given debug flag name + + Check is a debug flag was set by the user. the flag can be set either: + - in /tmp for a non-persistent presence between reboot + - in /config for always on (an existence at boot time) + + For a given debug flag named "test" + The presence of the file vyos.test.debug (all lowercase) enables it + + The function returns an empty string if the flag was not set otherwise + the function returns the full flagname + """ + + for folder in ('/tmp', '/config'): + flagfile = f'{folder}/vyos.{flag}.debug' + if not os.path.isfile(flagfile): + continue + with open(flagfile) as f: + content = f.readline().strip() + return content or default + + return '' + + +def _logfile(flag, default): + """ + return the name of the file to use for logging when the flag 'log' is set + if it could not be established or the location is invalid it returns + an empty string + """ + + # For log we return the location of the log file + log_location = _contentenv(flag) or _contentfile(flag, default) + + # it was not set + if not log_location: + return '' + + # Make sure that the logs can only be in /tmp, /var/log, or /tmp + if not log_location.startswith('/tmp/') and \ + not log_location.startswith('/config/') and \ + not log_location.startswith('/var/log/'): + return default + # Do not allow to escape the folders + if '..' in log_location: + return default + + if not os.path.exists(log_location): + return log_location + + # this permission is unique the the config and var folder + stat = os.stat(log_location).st_mode + if stat != 0o100666: + return default + return log_location diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py new file mode 100644 index 000000000..9921e3b5f --- /dev/null +++ b/python/vyos/defaults.py @@ -0,0 +1,53 @@ +# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +directories = { + "data": "/usr/share/vyos/", + "conf_mode": "/usr/libexec/vyos/conf_mode", + "config": "/opt/vyatta/etc/config", + "current": "/opt/vyatta/etc/config-migrate/current", + "migrate": "/opt/vyatta/etc/config-migrate/migrate", + "log": "/var/log/vyatta", + "templates": "/usr/share/vyos/templates/", + "certbot": "/config/auth/letsencrypt" +} + +cfg_group = 'vyattacfg' + +cfg_vintage = 'vyos' + +commit_lock = '/opt/vyatta/config/.lock' + +version_file = '/usr/share/vyos/component-versions.json' + +https_data = { + 'listen_addresses' : { '*': ['_'] } +} + +api_data = { + 'listen_address' : '127.0.0.1', + 'port' : '8080', + 'strict' : 'false', + 'debug' : 'false', + 'api_keys' : [ {"id": "testapp", "key": "qwerty"} ] +} + +vyos_cert_data = { + "conf": "/etc/nginx/snippets/vyos-cert.conf", + "crt": "/etc/ssl/certs/vyos-selfsigned.crt", + "key": "/etc/ssl/private/vyos-selfsign", + "lifetime": "365", +} diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py new file mode 100644 index 000000000..b12cda40f --- /dev/null +++ b/python/vyos/dicts.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +from vyos import ConfigError + + +class FixedDict(dict): + """ + FixedDict: A dictionnary not allowing new keys to be created after initialisation. + + >>> f = FixedDict(**{'count':1}) + >>> f['count'] = 2 + >>> f['king'] = 3 + File "...", line ..., in __setitem__ + raise ConfigError(f'Option "{k}" has no defined default') + """ + + def __init__(self, **options): + self._allowed = options.keys() + super().__init__(**options) + + def __setitem__(self, k, v): + """ + __setitem__ is a builtin which is called by python when setting dict values: + >>> d = dict() + >>> d['key'] = 'value' + >>> d + {'key': 'value'} + + is syntaxic sugar for + + >>> d = dict() + >>> d.__setitem__('key','value') + >>> d + {'key': 'value'} + """ + if k not in self._allowed: + raise ConfigError(f'Option "{k}" has no defined default') + super().__setitem__(k, v) diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py new file mode 100644 index 000000000..29117a5d3 --- /dev/null +++ b/python/vyos/formatversions.py @@ -0,0 +1,109 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os +import re +import fileinput + +def read_vyatta_versions(config_file): + config_file_versions = {} + + with open(config_file, 'r') as config_file_handle: + for config_line in config_file_handle: + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line): + if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line): + raise ValueError("malformed configuration string: " + "{}".format(config_line)) + + for pair in re.findall(r'([\w,-]+)@(\d+)', config_line): + config_file_versions[pair[0]] = int(pair[1]) + + + return config_file_versions + +def read_vyos_versions(config_file): + config_file_versions = {} + + with open(config_file, 'r') as config_file_handle: + for config_line in config_file_handle: + if re.match(r'// vyos-config-version:.+', config_line): + if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line): + raise ValueError("malformed configuration string: " + "{}".format(config_line)) + + for pair in re.findall(r'([\w,-]+)@(\d+)', config_line): + config_file_versions[pair[0]] = int(pair[1]) + + return config_file_versions + +def remove_versions(config_file): + """ + Remove old version string. + """ + for line in fileinput.input(config_file, inplace=True): + if re.match(r'/\* Warning:.+ \*/$', line): + continue + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line): + continue + if re.match(r'/\* Release version:.+ \*/$', line): + continue + if re.match('// vyos-config-version:.+', line): + continue + if re.match('// Warning:.+', line): + continue + if re.match('// Release version:.+', line): + continue + sys.stdout.write(line) + +def format_versions_string(config_versions): + cfg_keys = list(config_versions.keys()) + cfg_keys.sort() + + component_version_strings = [] + + for key in cfg_keys: + cfg_vers = config_versions[key] + component_version_strings.append('{}@{}'.format(key, cfg_vers)) + + separator = ":" + component_version_string = separator.join(component_version_strings) + + return component_version_string + +def write_vyatta_versions_foot(config_file, component_version_string, + os_version_string): + if config_file: + with open(config_file, 'a') as config_file_handle: + config_file_handle.write('/* Warning: Do not remove the following line. */\n') + config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string)) + config_file_handle.write('/* Release version: {} */\n'.format(os_version_string)) + else: + sys.stdout.write('/* Warning: Do not remove the following line. */\n') + sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string)) + sys.stdout.write('/* Release version: {} */\n'.format(os_version_string)) + +def write_vyos_versions_foot(config_file, component_version_string, + os_version_string): + if config_file: + with open(config_file, 'a') as config_file_handle: + config_file_handle.write('// Warning: Do not remove the following line.\n') + config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string)) + config_file_handle.write('// Release version: {}\n'.format(os_version_string)) + else: + sys.stdout.write('// Warning: Do not remove the following line.\n') + sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string)) + sys.stdout.write('// Release version: {}\n'.format(os_version_string)) + diff --git a/python/vyos/frr.py b/python/vyos/frr.py new file mode 100644 index 000000000..3fc75bbdf --- /dev/null +++ b/python/vyos/frr.py @@ -0,0 +1,288 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +r""" +A Library for interracting with the FRR daemon suite. +It supports simple configuration manipulation and loading using the official tools +supplied with FRR (vtysh and frr-reload) + +All configuration management and manipulation is done using strings and regex. + + +Example Usage +##### + +# Reading configuration from frr: +``` +>>> original_config = get_configuration() +>>> repr(original_config) +'!\nfrr version 7.3.1\nfrr defaults traditional\nhostname debian\n...... +``` + + +# Modify a configuration section: +``` +>>> new_bgp_section = 'router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n' +>>> modified_config = replace_section(original_config, new_bgp_section, replace_re=r'router bgp \d+') +>>> repr(modified_config) +'............router bgp 65000\n neighbor 192.0.2.1 remote-as 65000\n...........' +``` + +Remove a configuration section: +``` +>>> modified_config = remove_section(original_config, r'router ospf') +``` + +Test the new configuration: +``` +>>> try: +>>> mark_configuration(modified configuration) +>>> except ConfigurationNotValid as e: +>>> print('resulting configuration is not valid') +>>> sys.exit(1) +``` + +Apply the new configuration: +``` +>>> try: +>>> replace_configuration(modified_config) +>>> except CommitError as e: +>>> print('Exception while commiting the supplied configuration') +>>> print(e) +>>> exit(1) +``` +""" + +import tempfile +import re +from vyos import util + +_frr_daemons = ['zebra', 'bgpd', 'fabricd', 'isisd', 'ospf6d', 'ospfd', 'pbrd', + 'pimd', 'ripd', 'ripngd', 'sharpd', 'staticd', 'vrrpd', 'ldpd'] + +path_vtysh = '/usr/bin/vtysh' +path_frr_reload = '/usr/lib/frr/frr-reload.py' + + +class FrrError(Exception): + pass + + +class ConfigurationNotValid(FrrError): + """ + The configuratioin supplied to vtysh is not valid + """ + pass + + +class CommitError(FrrError): + """ + Commiting the supplied configuration failed to commit by a unknown reason + see commit error and/or run mark_configuration on the specified configuration + to se error generated + + used by: reload_configuration() + """ + pass + + +class ConfigSectionNotFound(FrrError): + """ + Removal of configuration failed because it is not existing in the supplied configuration + """ + pass + + +def get_configuration(daemon=None, marked=False): + """ Get current running FRR configuration + daemon: Collect only configuration for the specified FRR daemon, + supplying daemon=None retrieves the complete configuration + marked: Mark the configuration with "end" tags + + return: string containing the running configuration from frr + + """ + if daemon and daemon not in _frr_daemons: + raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') + + cmd = f"{path_vtysh} -c 'show run'" + if daemon: + cmd += f' -d {daemon}' + + output, code = util.popen(cmd, stderr=util.STDOUT) + if code: + raise OSError(code, output) + + config = output.replace('\r', '') + # Remove first header lines from FRR config + config = config.split("\n", 3)[-1] + # Mark the configuration with end tags + if marked: + config = mark_configuration(config) + + return config + + +def mark_configuration(config): + """ Add end marks and Test the configuration for syntax faults + If the configuration is valid a marked version of the configuration is returned, + or else it failes with a ConfigurationNotValid Exception + + config: The configuration string to mark/test + return: The marked configuration from FRR + """ + output, code = util.popen(f"{path_vtysh} -m -f -", stderr=util.STDOUT, input=config) + + if code == 2: + raise ConfigurationNotValid(str(output)) + elif code: + raise OSError(code, output) + + config = output.replace('\r', '') + return config + + +def reload_configuration(config, daemon=None): + """ Execute frr-reload with the new configuration + This will try to reapply the supplied configuration inside FRR. + The configuration needs to be a complete configuration from the integrated config or + from a daemon. + + config: The configuration to apply + daemon: Apply the conigutaion to the specified FRR daemon, + supplying daemon=None applies to the integrated configuration + return: None + """ + if daemon and daemon not in _frr_daemons: + raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') + + f = tempfile.NamedTemporaryFile('w') + f.write(config) + f.flush() + + cmd = f'{path_frr_reload} --reload' + if daemon: + cmd += f' --daemon {daemon}' + cmd += f' {f.name}' + + output, code = util.popen(cmd, stderr=util.STDOUT) + f.close() + if code == 1: + raise CommitError(f'Configuration FRR failed while commiting code: {repr(output)}') + elif code: + raise OSError(code, output) + + return output + + +def execute(command): + """ Run commands inside vtysh + command: str containing commands to execute inside a vtysh session + """ + if not isinstance(command, str): + raise ValueError(f'command needs to be a string: {repr(command)}') + + cmd = f"{path_vtysh} -c '{command}'" + + output, code = util.popen(cmd, stderr=util.STDOUT) + if code: + raise OSError(code, output) + + config = output.replace('\r', '') + return config + + +def configure(lines, daemon=False): + """ run commands inside config mode vtysh + lines: list or str conaining commands to execute inside a configure session + only one command executed on each configure() + Executing commands inside a subcontext uses the list to describe the context + ex: ['router bgp 6500', 'neighbor 192.0.2.1 remote-as 65000'] + return: None + """ + if isinstance(lines, str): + lines = [lines] + elif not isinstance(lines, list): + raise ValueError('lines needs to be string or list of commands') + + if daemon and daemon not in _frr_daemons: + raise ValueError(f'The specified daemon type is not supported {repr(daemon)}') + + cmd = f'{path_vtysh}' + if daemon: + cmd += f' -d {daemon}' + + cmd += " -c 'configure terminal'" + for x in lines: + cmd += f" -c '{x}'" + + output, code = util.popen(cmd, stderr=util.STDOUT) + if code == 1: + raise ConfigurationNotValid(f'Configuration FRR failed: {repr(output)}') + elif code: + raise OSError(code, output) + + config = output.replace('\r', '') + return config + + +def _replace_section(config, replacement, replace_re, before_re): + r"""Replace a section of FRR config + config: full original configuration + replacement: replacement configuration section + replace_re: The regex to replace + example: ^router bgp \d+$.?*^!$ + this will replace everything between ^router bgp X$ and ^!$ + before_re: When replace_re is not existant, the config will be added before this tag + example: ^line vty$ + + return: modified configuration as a text file + """ + # Check if block is configured, remove the existing instance else add a new one + if re.findall(replace_re, config, flags=re.MULTILINE | re.DOTALL): + # Section is in the configration, replace it + return re.sub(replace_re, replacement, config, count=1, + flags=re.MULTILINE | re.DOTALL) + if before_re: + if not re.findall(before_re, config, flags=re.MULTILINE | re.DOTALL): + raise ConfigSectionNotFound(f"Config section {before_re} not found in config") + + # If no section is in the configuration, add it before the line vty line + return re.sub(before_re, rf'{replacement}\n\g<1>', config, count=1, + flags=re.MULTILINE | re.DOTALL) + + raise ConfigSectionNotFound(f"Config section {replacement} not found in config") + + +def replace_section(config, replacement, from_re, to_re=r'!', before_re=r'line vty'): + r"""Replace a section of FRR config + config: full original configuration + replacement: replacement configuration section + from_re: Regex for the start of section matching + example: 'router bgp \d+' + to_re: Regex for stop of section matching + default: '!' + example: '!' or 'end' + before_re: When from_re/to_re does not return a match, the config will + be added before this tag + default: ^line vty$ + + startline and endline tags will be automatically added to the resulting from_re/to_re and before_re regex'es + """ + return _replace_section(config, replacement, replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=rf'^({before_re})$') + + +def remove_section(config, from_re, to_re='!'): + return _replace_section(config, '', replace_re=rf'^{from_re}$.*?^{to_re}$', before_re=None) diff --git a/python/vyos/hostsd_client.py b/python/vyos/hostsd_client.py new file mode 100644 index 000000000..303b6ea47 --- /dev/null +++ b/python/vyos/hostsd_client.py @@ -0,0 +1,119 @@ +import json +import zmq + +SOCKET_PATH = "ipc:///run/vyos-hostsd/vyos-hostsd.sock" + +class VyOSHostsdError(Exception): + pass + +class Client(object): + def __init__(self): + try: + context = zmq.Context() + self.__socket = context.socket(zmq.REQ) + self.__socket.RCVTIMEO = 10000 #ms + self.__socket.setsockopt(zmq.LINGER, 0) + self.__socket.connect(SOCKET_PATH) + except zmq.error.Again: + raise VyOSHostsdError("Could not connect to vyos-hostsd") + + def _communicate(self, msg): + try: + request = json.dumps(msg).encode() + self.__socket.send(request) + + reply_msg = self.__socket.recv().decode() + reply = json.loads(reply_msg) + if 'error' in reply: + raise VyOSHostsdError(reply['error']) + else: + return reply["data"] + except zmq.error.Again: + raise VyOSHostsdError("Could not connect to vyos-hostsd") + + def add_name_servers(self, data): + msg = {'type': 'name_servers', 'op': 'add', 'data': data} + self._communicate(msg) + + def delete_name_servers(self, data): + msg = {'type': 'name_servers', 'op': 'delete', 'data': data} + self._communicate(msg) + + def get_name_servers(self, tag_regex): + msg = {'type': 'name_servers', 'op': 'get', 'tag_regex': tag_regex} + return self._communicate(msg) + + def add_name_server_tags_recursor(self, data): + msg = {'type': 'name_server_tags_recursor', 'op': 'add', 'data': data} + self._communicate(msg) + + def delete_name_server_tags_recursor(self, data): + msg = {'type': 'name_server_tags_recursor', 'op': 'delete', 'data': data} + self._communicate(msg) + + def get_name_server_tags_recursor(self): + msg = {'type': 'name_server_tags_recursor', 'op': 'get'} + return self._communicate(msg) + + def add_name_server_tags_system(self, data): + msg = {'type': 'name_server_tags_system', 'op': 'add', 'data': data} + self._communicate(msg) + + def delete_name_server_tags_system(self, data): + msg = {'type': 'name_server_tags_system', 'op': 'delete', 'data': data} + self._communicate(msg) + + def get_name_server_tags_system(self): + msg = {'type': 'name_server_tags_system', 'op': 'get'} + return self._communicate(msg) + + def add_forward_zones(self, data): + msg = {'type': 'forward_zones', 'op': 'add', 'data': data} + self._communicate(msg) + + def delete_forward_zones(self, data): + msg = {'type': 'forward_zones', 'op': 'delete', 'data': data} + self._communicate(msg) + + def get_forward_zones(self): + msg = {'type': 'forward_zones', 'op': 'get'} + return self._communicate(msg) + + def add_search_domains(self, data): + msg = {'type': 'search_domains', 'op': 'add', 'data': data} + self._communicate(msg) + + def delete_search_domains(self, data): + msg = {'type': 'search_domains', 'op': 'delete', 'data': data} + self._communicate(msg) + + def get_search_domains(self, tag_regex): + msg = {'type': 'search_domains', 'op': 'get', 'tag_regex': tag_regex} + return self._communicate(msg) + + def add_hosts(self, data): + msg = {'type': 'hosts', 'op': 'add', 'data': data} + self._communicate(msg) + + def delete_hosts(self, data): + msg = {'type': 'hosts', 'op': 'delete', 'data': data} + self._communicate(msg) + + def get_hosts(self, tag_regex): + msg = {'type': 'hosts', 'op': 'get', 'tag_regex': tag_regex} + return self._communicate(msg) + + def set_host_name(self, host_name, domain_name): + msg = { + 'type': 'host_name', + 'op': 'set', + 'data': { + 'host_name': host_name, + 'domain_name': domain_name, + } + } + self._communicate(msg) + + def apply(self): + msg = {'op': 'apply'} + return self._communicate(msg) diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py new file mode 100644 index 000000000..9cd8d44c1 --- /dev/null +++ b/python/vyos/ifconfig/__init__.py @@ -0,0 +1,44 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.ifconfig.section import Section +from vyos.ifconfig.control import Control +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.operational import Operational +from vyos.ifconfig.vrrp import VRRP + +from vyos.ifconfig.bond import BondIf +from vyos.ifconfig.bridge import BridgeIf +from vyos.ifconfig.dummy import DummyIf +from vyos.ifconfig.ethernet import EthernetIf +from vyos.ifconfig.geneve import GeneveIf +from vyos.ifconfig.loopback import LoopbackIf +from vyos.ifconfig.macvlan import MACVLANIf +from vyos.ifconfig.vxlan import VXLANIf +from vyos.ifconfig.wireguard import WireGuardIf +from vyos.ifconfig.vtun import VTunIf +from vyos.ifconfig.vti import VTIIf +from vyos.ifconfig.pppoe import PPPoEIf +from vyos.ifconfig.tunnel import GREIf +from vyos.ifconfig.tunnel import GRETapIf +from vyos.ifconfig.tunnel import IP6GREIf +from vyos.ifconfig.tunnel import IPIPIf +from vyos.ifconfig.tunnel import IPIP6If +from vyos.ifconfig.tunnel import IP6IP6If +from vyos.ifconfig.tunnel import SitIf +from vyos.ifconfig.tunnel import Sit6RDIf +from vyos.ifconfig.wireless import WiFiIf +from vyos.ifconfig.l2tpv3 import L2TPv3If +from vyos.ifconfig.macsec import MACsecIf diff --git a/python/vyos/ifconfig/afi.py b/python/vyos/ifconfig/afi.py new file mode 100644 index 000000000..fd263d220 --- /dev/null +++ b/python/vyos/ifconfig/afi.py @@ -0,0 +1,19 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +# https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml + +IP4 = 1 +IP6 = 2 diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py new file mode 100644 index 000000000..64407401b --- /dev/null +++ b/python/vyos/ifconfig/bond.py @@ -0,0 +1,383 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN + +from vyos.util import cmd +from vyos.util import vyos_dict_search +from vyos.validate import assert_list +from vyos.validate import assert_positive + +@Interface.register +@VLAN.enable +class BondIf(Interface): + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ + + default = { + 'type': 'bond', + } + definition = { + **Interface.definition, + ** { + 'section': 'bonding', + 'prefixes': ['bond', ], + 'broadcast': True, + 'bridgeable': True, + }, + } + + _sysfs_set = {**Interface._sysfs_set, **{ + 'bond_hash_policy': { + 'validate': lambda v: assert_list(v, ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']), + 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy', + }, + 'bond_miimon': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/miimon' + }, + 'bond_arp_interval': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/arp_interval' + }, + 'bond_arp_ip_target': { + # XXX: no validation of the IP + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + }, + 'bond_add_port': { + 'location': '/sys/class/net/{ifname}/bonding/slaves', + }, + 'bond_del_port': { + 'location': '/sys/class/net/{ifname}/bonding/slaves', + }, + 'bond_primary': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/bonding/primary', + }, + 'bond_mode': { + 'validate': lambda v: assert_list(v, ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']), + 'location': '/sys/class/net/{ifname}/bonding/mode', + }, + }} + + _sysfs_get = {**Interface._sysfs_get, **{ + 'bond_arp_ip_target': { + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + } + }} + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # when a bond member gets deleted, all members are placed in A/D state + # even when they are enabled inside CLI. This will make the config + # and system look async. + slave_list = [] + for s in self.get_slaves(): + slave = { + 'ifname': s, + 'state': Interface(s).get_admin_state() + } + slave_list.append(slave) + + # remove bond master which places members in disabled state + super().remove() + + # replicate previous interface state before bond destruction back to + # physical interface + for slave in slave_list: + i = Interface(slave['ifname']) + i.set_admin_state(slave['state']) + + def set_hash_policy(self, mode): + """ + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, + layer2+3, layer3+4, encap2+3, encap3+4. + + The default value is layer2 + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_hash_policy('layer2+3') + """ + self.set_interface('bond_hash_policy', mode) + + def set_arp_interval(self, interval): + """ + Specifies the ARP link monitoring frequency in milliseconds. + + The ARP monitor works by periodically checking the slave devices + to determine whether they have sent or received traffic recently + (the precise criteria depends upon the bonding mode, and the + state of the slave). Regular traffic is generated via ARP probes + issued for the addresses specified by the arp_ip_target option. + + If ARP monitoring is used in an etherchannel compatible mode + (modes 0 and 2), the switch should be configured in a mode that + evenly distributes packets across all links. If the switch is + configured to distribute the packets in an XOR fashion, all + replies from the ARP targets will be received on the same link + which could cause the other team members to fail. + + value of 0 disables ARP monitoring. The default value is 0. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_arp_interval('100') + """ + if int(interval) == 0: + """ + Specifies the MII link monitoring frequency in milliseconds. + This determines how often the link state of each slave is + inspected for link failures. A value of zero disables MII + link monitoring. A value of 100 is a good starting point. + """ + return self.set_interface('bond_miimon', interval) + else: + return self.set_interface('bond_arp_interval', interval) + + def get_arp_ip_target(self): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').get_arp_ip_target() + '192.0.2.1' + """ + # As this function might also be called from update() of a VLAN interface + # we must check if the bond_arp_ip_target retrieval worked or not - as this + # can not be set for a bond vif interface + try: + return self.get_interface('bond_arp_ip_target') + except FileNotFoundError: + return '' + + def set_arp_ip_target(self, target): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_arp_ip_target('192.0.2.1') + >>> BondIf('bond0').get_arp_ip_target() + '192.0.2.1' + """ + return self.set_interface('bond_arp_ip_target', target) + + def add_port(self, interface): + """ + Enslave physical interface to bond. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').add_port('eth0') + >>> BondIf('bond0').add_port('eth1') + """ + + # From drivers/net/bonding/bond_main.c: + # ... + # bond_set_slave_link_state(new_slave, + # BOND_LINK_UP, + # BOND_SLAVE_NOTIFY_NOW); + # ... + # + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the CLI will tell us! + + # Physical interface must be in admin down state before they can be + # enslaved. If this is not the case an error will be shown: + # bond0: eth0 is up - this may be due to an out of date ifenslave + slave = Interface(interface) + slave_state = slave.get_admin_state() + if slave_state == 'up': + slave.set_admin_state('down') + + ret = self.set_interface('bond_add_port', f'+{interface}') + # The kernel will ALWAYS place new bond members in "up" state regardless + # what the LI is configured for - thus we place the interface in its + # desired state + slave.set_admin_state(slave_state) + return ret + + def del_port(self, interface): + """ + Remove physical port from bond + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').del_port('eth1') + """ + return self.set_interface('bond_del_port', f'-{interface}') + + def get_slaves(self): + """ + Return a list with all configured slave interfaces on this bond. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').get_slaves() + ['eth1', 'eth2'] + """ + enslaved_ifs = [] + # retrieve real enslaved interfaces from OS kernel + sysfs_bond = '/sys/class/net/{}'.format(self.config['ifname']) + if os.path.isdir(sysfs_bond): + for directory in os.listdir(sysfs_bond): + if 'lower_' in directory: + enslaved_ifs.append(directory.replace('lower_', '')) + + return enslaved_ifs + + def set_primary(self, interface): + """ + A string (eth0, eth2, etc) specifying which slave is the primary + device. The specified device will always be the active slave while it + is available. Only when the primary is off-line will alternate devices + be used. This is useful when one slave is preferred over another, e.g., + when one slave has higher throughput than another. + + The primary option is only valid for active-backup, balance-tlb and + balance-alb mode. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_primary('eth2') + """ + return self.set_interface('bond_primary', interface) + + def set_mode(self, mode): + """ + Specifies one of the bonding policies. The default is balance-rr + (round robin). + + Possible values are: balance-rr, active-backup, balance-xor, + broadcast, 802.3ad, balance-tlb, balance-alb + + NOTE: the bonding mode can not be changed when the bond itself has + slaves + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_mode('802.3ad') + """ + return self.set_interface('bond_mode', mode) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # use ref-counting function to place an interface into admin down state. + # set_admin_state_up() must be called the same amount of times else the + # interface won't come up. This can/should be used to prevent link flapping + # when changing interface parameters require the interface to be down. + # We will disable it once before reconfiguration and enable it afterwards. + if 'shutdown_required' in config: + self.set_admin_state('down') + + # call base class first + super().update(config) + + # ARP monitor targets need to be synchronized between sysfs and CLI. + # Unfortunately an address can't be send twice to sysfs as this will + # result in the following exception: OSError: [Errno 22] Invalid argument. + # + # We remove ALL addresses prior to adding new ones, this will remove + # addresses manually added by the user too - but as we are limited to 16 adresses + # from the kernel side this looks valid to me. We won't run into an error + # when a user added manual adresses which would result in having more + # then 16 adresses in total. + arp_tgt_addr = list(map(str, self.get_arp_ip_target().split())) + for addr in arp_tgt_addr: + self.set_arp_ip_target('-' + addr) + + # Add configured ARP target addresses + value = vyos_dict_search('arp_monitor.target', config) + if isinstance(value, str): + value = [value] + if value: + for addr in value: + self.set_arp_ip_target('+' + addr) + + # Bonding transmit hash policy + value = config.get('hash_policy') + if value: self.set_hash_policy(value) + + # Some interface options can only be changed if the interface is + # administratively down + if self.get_admin_state() == 'down': + # Delete bond member port(s) + for interface in self.get_slaves(): + self.del_port(interface) + + # Bonding policy/mode + value = config.get('mode') + if value: self.set_mode(value) + + # Add (enslave) interfaces to bond + value = vyos_dict_search('member.interface', config) + if value: + for interface in value: + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + self.add_port(interface) + + # Primary device interface - must be set after 'mode' + value = config.get('primary') + if value: self.set_primary(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py new file mode 100644 index 000000000..4c76fe996 --- /dev/null +++ b/python/vyos/ifconfig/bridge.py @@ -0,0 +1,263 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.stp import STP +from vyos.validate import assert_boolean +from vyos.validate import assert_positive +from vyos.util import cmd +from vyos.util import vyos_dict_search + +@Interface.register +class BridgeIf(Interface): + """ + A bridge is a way to connect two Ethernet segments together in a protocol + independent way. Packets are forwarded based on Ethernet address, rather + than IP address (like a router). Since forwarding is done at Layer 2, all + protocols can go transparently through a bridge. + + The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. + """ + + default = { + 'type': 'bridge', + } + definition = { + **Interface.definition, + **{ + 'section': 'bridge', + 'prefixes': ['br', ], + 'broadcast': True, + }, + } + + _sysfs_set = {**Interface._sysfs_set, **{ + 'ageing_time': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/ageing_time', + }, + 'forward_delay': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/forward_delay', + }, + 'hello_time': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/hello_time', + }, + 'max_age': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/max_age', + }, + 'priority': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bridge/priority', + }, + 'stp': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/stp_state', + }, + 'multicast_querier': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', + }, + }} + + _command_set = {**Interface._command_set, **{ + 'add_port': { + 'shellcmd': 'ip link set dev {value} master {ifname}', + }, + 'del_port': { + 'shellcmd': 'ip link set dev {value} nomaster', + }, + }} + + + def set_ageing_time(self, time): + """ + Set bridge interface MAC address aging time in seconds. Internal kernel + representation is in centiseconds. Kernel default is 300 seconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').ageing_time(2) + """ + self.set_interface('ageing_time', time) + + def set_forward_delay(self, time): + """ + Set bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').forward_delay(15) + """ + self.set_interface('forward_delay', time) + + def set_hello_time(self, time): + """ + Set bridge hello time in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_hello_time(2) + """ + self.set_interface('hello_time', time) + + def set_max_age(self, time): + """ + Set bridge max message age in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_max_age(30) + """ + self.set_interface('max_age', time) + + def set_priority(self, priority): + """ + Set bridge max aging time in seconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_priority(8192) + """ + self.set_interface('priority', priority) + + def set_stp(self, state): + """ + Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_stp(1) + """ + self.set_interface('stp', state) + + def set_multicast_querier(self, enable): + """ + Sets whether the bridge actively runs a multicast querier or not. When a + bridge receives a 'multicast host membership' query from another network + host, that host is tracked based on the time that the query was received + plus the multicast query interval time. + + Use enable=1 to enable or enable=0 to disable + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_multicast_querier(1) + """ + self.set_interface('multicast_querier', enable) + + def add_port(self, interface): + """ + Add physical interface to bridge (member port) + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').add_port('eth0') + >>> BridgeIf('br0').add_port('eth1') + """ + return self.set_interface('add_port', interface) + + def del_port(self, interface): + """ + Remove member port from bridge instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + return self.set_interface('del_port', interface) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Set ageing time + value = config.get('aging') + self.set_ageing_time(value) + + # set bridge forward delay + value = config.get('forwarding_delay') + self.set_forward_delay(value) + + # set hello time + value = config.get('hello_time') + self.set_hello_time(value) + + # set max message age + value = config.get('max_age') + self.set_max_age(value) + + # set bridge priority + value = config.get('priority') + self.set_priority(value) + + # enable/disable spanning tree + value = '1' if 'stp' in config else '0' + self.set_stp(value) + + # enable or disable IGMP querier + tmp = vyos_dict_search('igmp.querier', config) + value = '1' if (tmp != None) else '0' + self.set_multicast_querier(value) + + # remove interface from bridge + tmp = vyos_dict_search('member.interface_remove', config) + if tmp: + for member in tmp: + self.del_port(member) + + STPBridgeIf = STP.enable(BridgeIf) + tmp = vyos_dict_search('member.interface', config) + if tmp: + for interface, interface_config in tmp.items(): + # if we've come here we already verified the interface + # does not have an addresses configured so just flush + # any remaining ones + Interface(interface).flush_addrs() + # enslave interface port to bridge + self.add_port(interface) + + tmp = STPBridgeIf(interface) + # set bridge port path cost + value = interface_config.get('cost') + tmp.set_path_cost(value) + + # set bridge port path priority + value = interface_config.get('priority') + tmp.set_path_priority(value) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py new file mode 100644 index 000000000..a6fc8ac6c --- /dev/null +++ b/python/vyos/ifconfig/control.py @@ -0,0 +1,185 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os +from inspect import signature +from inspect import _empty + +from vyos import debug +from vyos.util import popen +from vyos.util import cmd +from vyos.ifconfig.section import Section + + +class Control(Section): + _command_get = {} + _command_set = {} + _signature = {} + + def __init__(self, **kargs): + # some commands (such as operation comands - show interfaces, etc.) + # need to query the interface statistics. If the interface + # code is used and the debugging is enabled, the screen output + # will include both the command but also the debugging for that command + # to prevent this, debugging can be explicitely disabled + + # if debug is not explicitely disabled the the config, enable it + self.debug = '' + if kargs.get('debug', True) and debug.enabled('ifconfig'): + self.debug = 'ifconfig' + + def _debug_msg (self, message): + return debug.message(message, self.debug) + + def _popen(self, command): + return popen(command, self.debug) + + def _cmd(self, command): + return cmd(command, self.debug) + + def _get_command(self, config, name): + """ + Using the defined names, set data write to sysfs. + """ + cmd = self._command_get[name]['shellcmd'].format(**config) + return self._command_get[name].get('format', lambda _: _)(self._cmd(cmd)) + + def _values(self, name, validate, value): + """ + looks at the validation function "validate" + for the interface sysfs or command and + returns a dict with the right options to call it + """ + if name not in self._signature: + self._signature[name] = signature(validate) + + values = {} + + for k in self._signature[name].parameters: + default = self._signature[name].parameters[k].default + if default is not _empty: + continue + if k == 'self': + values[k] = self + elif k == 'ifname': + values[k] = self.ifname + else: + values[k] = value + + return values + + def _set_command(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + # the code can pass int as int + value = str(value) + + validate = self._command_set[name].get('validate', None) + if validate: + try: + validate(**self._values(name, validate, value)) + except Exception as e: + raise e.__class__(f'Could not set {name}. {e}') + + convert = self._command_set[name].get('convert', None) + if convert: + value = convert(value) + + possible = self._command_set[name].get('possible', None) + if possible and not possible(config['ifname'], value): + return False + + config = {**config, **{'value': value}} + + cmd = self._command_set[name]['shellcmd'].format(**config) + return self._command_set[name].get('format', lambda _: _)(self._cmd(cmd)) + + _sysfs_get = {} + _sysfs_set = {} + + def _read_sysfs(self, filename): + """ + Provide a single primitive w/ error checking for reading from sysfs. + """ + value = None + with open(filename, 'r') as f: + value = f.read().rstrip('\n') + + self._debug_msg("read '{}' < '{}'".format(value, filename)) + return value + + def _write_sysfs(self, filename, value): + """ + Provide a single primitive w/ error checking for writing to sysfs. + """ + self._debug_msg("write '{}' > '{}'".format(value, filename)) + if os.path.isfile(filename): + with open(filename, 'w') as f: + f.write(str(value)) + return True + return False + + def _get_sysfs(self, config, name): + """ + Using the defined names, get data write from sysfs. + """ + filename = self._sysfs_get[name]['location'].format(**config) + if not filename: + return None + return self._read_sysfs(filename) + + def _set_sysfs(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + # the code can pass int as int + value = str(value) + + validate = self._sysfs_set[name].get('validate', None) + if validate: + try: + validate(**self._values(name, validate, value)) + except Exception as e: + raise e.__class__(f'Could not set {name}. {e}') + + config = {**config, **{'value': value}} + + convert = self._sysfs_set[name].get('convert', None) + if convert: + value = convert(value) + + commited = self._write_sysfs( + self._sysfs_set[name]['location'].format(**config), value) + if not commited: + errmsg = self._sysfs_set.get('errormsg', '') + if errmsg: + raise TypeError(errmsg.format(**config)) + return commited + + def get_interface(self, name): + if name in self._sysfs_get: + return self._get_sysfs(self.config, name) + if name in self._command_get: + return self._get_command(self.config, name) + raise KeyError(f'{name} is not a attribute of the interface we can get') + + def set_interface(self, name, value): + if name in self._sysfs_set: + return self._set_sysfs(self.config, name, value) + if name in self._command_set: + return self._set_command(self.config, name, value) + raise KeyError(f'{name} is not a attribute of the interface we can set') diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py new file mode 100644 index 000000000..43614cd1c --- /dev/null +++ b/python/vyos/ifconfig/dummy.py @@ -0,0 +1,56 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class DummyIf(Interface): + """ + A dummy interface is entirely virtual like, for example, the loopback + interface. The purpose of a dummy interface is to provide a device to route + packets through without actually transmitting them. + """ + + default = { + 'type': 'dummy', + } + definition = { + **Interface.definition, + **{ + 'section': 'dummy', + 'prefixes': ['dum', ], + }, + } + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py new file mode 100644 index 000000000..17c1bd64d --- /dev/null +++ b/python/vyos/ifconfig/ethernet.py @@ -0,0 +1,309 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN +from vyos.validate import assert_list +from vyos.util import run +from vyos.util import vyos_dict_search + +@Interface.register +@VLAN.enable +class EthernetIf(Interface): + """ + Abstraction of a Linux Ethernet Interface + """ + + default = { + 'type': 'ethernet', + } + definition = { + **Interface.definition, + **{ + 'section': 'ethernet', + 'prefixes': ['lan', 'eth', 'eno', 'ens', 'enp', 'enx'], + 'bondable': True, + 'broadcast': True, + 'bridgeable': True, + 'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$', + } + } + + @staticmethod + def feature(ifname, option, value): + run(f'/sbin/ethtool -K {ifname} {option} {value}','ifconfig') + return False + + _command_set = {**Interface._command_set, **{ + 'gro': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gro', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}', + }, + 'gso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gso', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}', + }, + 'sg': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'sg', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}', + }, + 'tso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}', + }, + 'ufo': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'ufo', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}', + }, + }} + + def get_driver_name(self): + """ + Return the driver name used by NIC. Some NICs don't support all + features e.g. changing link-speed, duplex + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.get_driver_name() + 'vmxnet3' + """ + sysfs_file = '/sys/class/net/{}/device/driver/module'.format( + self.config['ifname']) + if os.path.exists(sysfs_file): + link = os.readlink(sysfs_file) + return os.path.basename(link) + else: + return None + + def set_flow_control(self, enable): + """ + Changes the pause parameters of the specified Ethernet device. + + @param enable: true -> enable pause frames, false -> disable pause frames + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_flow_control(True) + """ + ifname = self.config['ifname'] + + if enable not in ['on', 'off']: + raise ValueError("Value out of range") + + if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: + self._debug_msg('{} driver does not support changing flow control settings!' + .format(self.get_driver_name())) + return + + # Get current flow control settings: + cmd = f'/sbin/ethtool --show-pause {ifname}' + output, code = self._popen(cmd) + if code == 76: + # the interface does not support it + return '' + if code: + # never fail here as it prevent vyos to boot + print(f'unexpected return code {code} from {cmd}') + return '' + + # The above command returns - with tabs: + # + # Pause parameters for eth0: + # Autonegotiate: on + # RX: off + # TX: off + if re.search("Autonegotiate:\ton", output): + if enable == "on": + # flowcontrol is already enabled - no need to re-enable it again + # this will prevent the interface from flapping as applying the + # flow-control settings will take the interface down and bring + # it back up every time. + return '' + + # Assemble command executed on system. Unfortunately there is no way + # to change this setting via sysfs + cmd = f'/sbin/ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}' + output, code = self._popen(cmd) + if code: + print(f'could not set flowcontrol for {ifname}') + return output + + def set_speed_duplex(self, speed, duplex): + """ + Set link speed in Mbit/s and duplex. + + @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto + @duplex can be half, full, auto + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_speed_duplex('auto', 'auto') + """ + + if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']: + raise ValueError("Value out of range (speed)") + + if duplex not in ['auto', 'full', 'half']: + raise ValueError("Value out of range (duplex)") + + if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: + self._debug_msg('{} driver does not support changing speed/duplex settings!' + .format(self.get_driver_name())) + return + + # Get current speed and duplex settings: + cmd = '/sbin/ethtool {0}'.format(self.config['ifname']) + tmp = self._cmd(cmd) + + if re.search("\tAuto-negotiation: on", tmp): + if speed == 'auto' and duplex == 'auto': + # bail out early as nothing is to change + return + else: + # read in current speed and duplex settings + cur_speed = 0 + cur_duplex = '' + for line in tmp.splitlines(): + if line.lstrip().startswith("Speed:"): + non_decimal = re.compile(r'[^\d.]+') + cur_speed = non_decimal.sub('', line) + continue + + if line.lstrip().startswith("Duplex:"): + cur_duplex = line.split()[-1].lower() + break + + if (cur_speed == speed) and (cur_duplex == duplex): + # bail out early as nothing is to change + return + + cmd = '/sbin/ethtool -s {}'.format(self.config['ifname']) + if speed == 'auto' or duplex == 'auto': + cmd += ' autoneg on' + else: + cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex) + + return self._cmd(cmd) + + def set_gro(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_gro('on') + """ + return self.set_interface('gro', state) + + def set_gso(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_gso('on') + """ + return self.set_interface('gso', state) + + def set_sg(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_sg('on') + """ + return self.set_interface('sg', state) + + def set_tso(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_tso('on') + """ + return self.set_interface('tso', state) + + def set_ufo(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_udp_offload('on') + """ + return self.set_interface('ufo', state) + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # disable ethernet flow control (pause frames) + value = 'off' if 'disable_flow_control' in config.keys() else 'on' + self.set_flow_control(value) + + # GRO (generic receive offload) + tmp = vyos_dict_search('offload_options.generic_receive', config) + value = tmp if (tmp != None) else 'off' + self.set_gro(value) + + # GSO (generic segmentation offload) + tmp = vyos_dict_search('offload_options.generic_segmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_gso(value) + + # scatter-gather option + tmp = vyos_dict_search('offload_options.scatter_gather', config) + value = tmp if (tmp != None) else 'off' + self.set_sg(value) + + # TSO (TCP segmentation offloading) + tmp = vyos_dict_search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_tso(value) + + # UDP fragmentation offloading + tmp = vyos_dict_search('offload_options.udp_fragmentation', config) + value = tmp if (tmp != None) else 'off' + self.set_ufo(value) + + # Set physical interface speed and duplex + if {'speed', 'duplex'} <= set(config): + speed = config.get('speed') + duplex = config.get('duplex') + self.set_speed_duplex(speed, duplex) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py new file mode 100644 index 000000000..dd0658668 --- /dev/null +++ b/python/vyos/ifconfig/geneve.py @@ -0,0 +1,85 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from copy import deepcopy + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class GeneveIf(Interface): + """ + Geneve: Generic Network Virtualization Encapsulation + + For more information please refer to: + https://tools.ietf.org/html/draft-gross-geneve-00 + https://www.redhat.com/en/blog/what-geneve + https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve + https://lwn.net/Articles/644938/ + """ + + default = { + 'type': 'geneve', + 'vni': 0, + 'remote': '', + } + options = Interface.options + \ + ['vni', 'remote'] + definition = { + **Interface.definition, + **{ + 'section': 'geneve', + 'prefixes': ['gnv', ], + 'bridgeable': True, + } + } + + def _create(self): + cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}'.format(**self.config) + self._cmd(cmd) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @classmethod + def get_config(cls): + """ + GENEVE interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = GeneveIf().get_config() + """ + return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py new file mode 100644 index 000000000..bfab36335 --- /dev/null +++ b/python/vyos/ifconfig/input.py @@ -0,0 +1,31 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class InputIf(Interface): + default = { + 'type': '', + } + definition = { + **Interface.definition, + **{ + 'section': 'input', + 'prefixes': ['ifb', ], + }, + } diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py new file mode 100644 index 000000000..67ba973c4 --- /dev/null +++ b/python/vyos/ifconfig/interface.py @@ -0,0 +1,1067 @@ +# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import json +import jmespath + +from copy import deepcopy +from glob import glob + +from ipaddress import IPv4Network +from ipaddress import IPv6Address +from ipaddress import IPv6Network +from netifaces import ifaddresses +# this is not the same as socket.AF_INET/INET6 +from netifaces import AF_INET +from netifaces import AF_INET6 + +from vyos import ConfigError +from vyos.configdict import list_diff +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import mac2eui64 +from vyos.util import vyos_dict_search +from vyos.validate import is_ipv4 +from vyos.validate import is_ipv6 +from vyos.validate import is_intf_addr_assigned +from vyos.validate import assert_boolean +from vyos.validate import assert_list +from vyos.validate import assert_mac +from vyos.validate import assert_mtu +from vyos.validate import assert_positive +from vyos.validate import assert_range + +from vyos.ifconfig.control import Control +from vyos.ifconfig.vrrp import VRRP +from vyos.ifconfig.operational import Operational +from vyos.ifconfig import Section + +def get_ethertype(ethertype_val): + if ethertype_val == '0x88A8': + return '802.1ad' + elif ethertype_val == '0x8100': + return '802.1q' + else: + raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) + +class Interface(Control): + # This is the class which will be used to create + # self.operational, it allows subclasses, such as + # WireGuard to modify their display behaviour + OperationalClass = Operational + + options = ['debug', 'create'] + required = [] + default = { + 'type': '', + 'debug': True, + 'create': True, + } + definition = { + 'section': '', + 'prefixes': [], + 'vlan': False, + 'bondable': False, + 'broadcast': False, + 'bridgeable': False, + 'eternal': '', + } + + _command_get = { + 'admin_state': { + 'shellcmd': 'ip -json link show dev {ifname}', + 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', + }, + 'vlan_protocol': { + 'shellcmd': 'ip -json -details link show dev {ifname}', + 'format': lambda j: jmespath.search('[*].linkinfo.info_data.protocol | [0]', json.loads(j)), + }, + } + + _command_set = { + 'admin_state': { + 'validate': lambda v: assert_list(v, ['up', 'down']), + 'shellcmd': 'ip link set dev {ifname} {value}', + }, + 'mac': { + 'validate': assert_mac, + 'shellcmd': 'ip link set dev {ifname} address {value}', + }, + 'vrf': { + 'convert': lambda v: f'master {v}' if v else 'nomaster', + 'shellcmd': 'ip link set dev {ifname} {value}', + }, + } + + _sysfs_get = { + 'alias': { + 'location': '/sys/class/net/{ifname}/ifalias', + }, + 'mac': { + 'location': '/sys/class/net/{ifname}/address', + }, + 'mtu': { + 'location': '/sys/class/net/{ifname}/mtu', + }, + 'oper_state':{ + 'location': '/sys/class/net/{ifname}/operstate', + }, + } + + _sysfs_set = { + 'alias': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/ifalias', + }, + 'mtu': { + 'validate': assert_mtu, + 'location': '/sys/class/net/{ifname}/mtu', + }, + 'arp_cache_tmo': { + 'convert': lambda tmo: (int(tmo) * 1000), + 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms', + }, + 'arp_filter': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter', + }, + 'arp_accept': { + 'validate': lambda arp: assert_range(arp,0,2), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept', + }, + 'arp_announce': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce', + }, + 'arp_ignore': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', + }, + 'ipv6_accept_ra': { + 'validate': lambda ara: assert_range(ara,0,3), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', + }, + 'ipv6_autoconf': { + 'validate': lambda aco: assert_range(aco,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf', + }, + 'ipv6_forwarding': { + 'validate': lambda fwd: assert_range(fwd,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding', + }, + 'ipv6_dad_transmits': { + 'validate': assert_positive, + 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits', + }, + 'proxy_arp': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', + }, + 'proxy_arp_pvlan': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan', + }, + # link_detect vs link_filter name weirdness + 'link_detect': { + 'validate': lambda link: assert_range(link,0,3), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter', + }, + } + + @classmethod + def exists(cls, ifname): + return os.path.exists(f'/sys/class/net/{ifname}') + + def __init__(self, ifname, **kargs): + """ + This is the base interface class which supports basic IP/MAC address + operations as well as DHCP(v6). Other interface which represent e.g. + and ethernet bridge are implemented as derived classes adding all + additional functionality. + + For creation you will need to provide the interface type, otherwise + the existing interface is used + + DEBUG: + This class has embedded debugging (print) which can be enabled by + creating the following file: + vyos@vyos# touch /tmp/vyos.ifconfig.debug + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + """ + + self.config = deepcopy(self.default) + for k in self.options: + if k in kargs: + self.config[k] = kargs[k] + + # make sure the ifname is the first argument and not from the dict + self.config['ifname'] = ifname + self._admin_state_down_cnt = 0 + + # we must have updated config before initialising the Interface + super().__init__(**kargs) + self.ifname = ifname + + if not self.exists(ifname): + # Any instance of Interface, such as Interface('eth0') + # can be used safely to access the generic function in this class + # as 'type' is unset, the class can not be created + if not self.config['type']: + raise Exception(f'interface "{ifname}" not found') + + # Should an Instance of a child class (EthernetIf, DummyIf, ..) + # be required, then create should be set to False to not accidentally create it. + # In case a subclass does not define it, we use get to set the default to True + if self.config.get('create',True): + for k in self.required: + if k not in kargs: + name = self.default['type'] + raise ConfigError(f'missing required option {k} for {name} {ifname} creation') + + self._create() + # If we can not connect to the interface then let the caller know + # as the class could not be correctly initialised + else: + raise Exception('interface "{}" not found'.format(self.config['ifname'])) + + # temporary list of assigned IP addresses + self._addr = [] + + self.operational = self.OperationalClass(ifname) + self.vrrp = VRRP(ifname) + + def _create(self): + cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) + self._cmd(cmd) + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + + # remove all assigned IP addresses from interface - this is a bit redundant + # as the kernel will remove all addresses on interface deletion, but we + # can not delete ALL interfaces, see below + self.flush_addrs() + + # --------------------------------------------------------------------- + # Any class can define an eternal regex in its definition + # interface matching the regex will not be deleted + + eternal = self.definition['eternal'] + if not eternal: + self._delete() + elif not re.match(eternal, self.ifname): + self._delete() + + def _delete(self): + # NOTE (Improvement): + # after interface removal no other commands should be allowed + # to be called and instead should raise an Exception: + cmd = 'ip link del dev {ifname}'.format(**self.config) + return self._cmd(cmd) + + def get_mtu(self): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mtu() + '1500' + """ + return self.get_interface('mtu') + + def set_mtu(self, mtu): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_mtu(1400) + >>> Interface('eth0').get_mtu() + '1400' + """ + return self.set_interface('mtu', mtu) + + def get_mac(self): + """ + Get current interface MAC (Media Access Contrl) address used. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mac() + '00:50:ab:cd:ef:00' + """ + return self.get_interface('mac') + + def set_mac(self, mac): + """ + Set interface MAC (Media Access Contrl) address to given value. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01') + """ + + # If MAC is unchanged, bail out early + if mac == self.get_mac(): + return None + + # MAC address can only be changed if interface is in 'down' state + prev_state = self.get_admin_state() + if prev_state == 'up': + self.set_admin_state('down') + + self.set_interface('mac', mac) + + # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion + if prev_state == 'up': + self.set_admin_state('up') + + def set_vrf(self, vrf=''): + """ + Add/Remove interface from given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_vrf('foo') + >>> Interface('eth0').set_vrf() + """ + self.set_interface('vrf', vrf) + + def set_arp_cache_tmo(self, tmo): + """ + Set ARP cache timeout value in seconds. Internal Kernel representation + is in milliseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_arp_cache_tmo(40) + """ + return self.set_interface('arp_cache_tmo', tmo) + + def set_arp_filter(self, arp_filter): + """ + Filter ARP requests + + 1 - Allows you to have multiple network interfaces on the same + subnet, and have the ARPs for each interface be answered + based on whether or not the kernel would route a packet from + the ARP'd IP out that interface (therefore you must use source + based routing for this to work). In other words it allows control + of which cards (usually 1) will respond to an arp request. + + 0 - (default) The kernel can respond to arp requests with addresses + from other interfaces. This may seem wrong but it usually makes + sense, because it increases the chance of successful communication. + IP addresses are owned by the complete host on Linux, not by + particular interfaces. Only for more complex setups like load- + balancing, does this behaviour cause problems. + """ + return self.set_interface('arp_filter', arp_filter) + + def set_arp_accept(self, arp_accept): + """ + Define behavior for gratuitous ARP frames who's IP is not + already present in the ARP table: + 0 - don't create new entries in the ARP table + 1 - create new entries in the ARP table + + Both replies and requests type gratuitous arp will trigger the + ARP table to be updated, if this setting is on. + + If the ARP table already contains the IP address of the + gratuitous arp frame, the arp table will be updated regardless + if this setting is on or off. + """ + return self.set_interface('arp_accept', arp_accept) + + def set_arp_announce(self, arp_announce): + """ + Define different restriction levels for announcing the local + source IP address from IP packets in ARP requests sent on + interface: + 0 - (default) Use any local address, configured on any interface + 1 - Try to avoid local addresses that are not in the target's + subnet for this interface. This mode is useful when target + hosts reachable via this interface require the source IP + address in ARP requests to be part of their logical network + configured on the receiving interface. When we generate the + request we will check all our subnets that include the + target IP and will preserve the source address if it is from + such subnet. + + Increasing the restriction level gives more chance for + receiving answer from the resolved target while decreasing + the level announces more valid sender's information. + """ + return self.set_interface('arp_announce', arp_announce) + + def set_arp_ignore(self, arp_ignore): + """ + Define different modes for sending replies in response to received ARP + requests that resolve local target IP addresses: + + 0 - (default): reply for any local target IP address, configured + on any interface + 1 - reply only if the target IP address is local address + configured on the incoming interface + """ + return self.set_interface('arp_ignore', arp_ignore) + + def set_ipv6_accept_ra(self, accept_ra): + """ + Accept Router Advertisements; autoconfigure using them. + + It also determines whether or not to transmit Router Solicitations. + If and only if the functional setting is to accept Router + Advertisements, Router Solicitations will be transmitted. + + 0 - Do not accept Router Advertisements. + 1 - (default) Accept Router Advertisements if forwarding is disabled. + 2 - Overrule forwarding behaviour. Accept Router Advertisements even if + forwarding is enabled. + """ + return self.set_interface('ipv6_accept_ra', accept_ra) + + def set_ipv6_autoconf(self, autoconf): + """ + Autoconfigure addresses using Prefix Information in Router + Advertisements. + """ + return self.set_interface('ipv6_autoconf', autoconf) + + def add_ipv6_eui64_address(self, prefix): + """ + Extended Unique Identifier (EUI), as per RFC2373, allows a host to + assign itself a unique IPv6 address based on a given IPv6 prefix. + + Calculate the EUI64 from the interface's MAC, then assign it + with the given prefix to the interface. + """ + + eui64 = mac2eui64(self.get_mac(), prefix) + prefixlen = prefix.split('/')[1] + self.add_addr(f'{eui64}/{prefixlen}') + + def del_ipv6_eui64_address(self, prefix): + """ + Delete the address based on the interface's MAC-based EUI64 + combined with the prefix address. + """ + eui64 = mac2eui64(self.get_mac(), prefix) + prefixlen = prefix.split('/')[1] + self.del_addr(f'{eui64}/{prefixlen}') + + + def set_ipv6_forwarding(self, forwarding): + """ + Configure IPv6 interface-specific Host/Router behaviour. + + False: + + By default, Host behaviour is assumed. This means: + + 1. IsRouter flag is not set in Neighbour Advertisements. + 2. If accept_ra is TRUE (default), transmit Router + Solicitations. + 3. If accept_ra is TRUE (default), accept Router + Advertisements (and do autoconfiguration). + 4. If accept_redirects is TRUE (default), accept Redirects. + + True: + + If local forwarding is enabled, Router behaviour is assumed. + This means exactly the reverse from the above: + + 1. IsRouter flag is set in Neighbour Advertisements. + 2. Router Solicitations are not sent unless accept_ra is 2. + 3. Router Advertisements are ignored unless accept_ra is 2. + 4. Redirects are ignored. + """ + return self.set_interface('ipv6_forwarding', forwarding) + + def set_ipv6_dad_messages(self, dad): + """ + The amount of Duplicate Address Detection probes to send. + Default: 1 + """ + return self.set_interface('ipv6_dad_transmits', dad) + + def set_link_detect(self, link_filter): + """ + Configure kernel response in packets received on interfaces that are 'down' + + 0 - Allow packets to be received for the address on this interface + even if interface is disabled or no carrier. + + 1 - Ignore packets received if interface associated with the incoming + address is down. + + 2 - Ignore packets received if interface associated with the incoming + address is down or has no carrier. + + Default value is 0. Note that some distributions enable it in startup + scripts. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_link_detect(1) + """ + return self.set_interface('link_detect', link_filter) + + def get_alias(self): + """ + Get interface alias name used by e.g. SNMP + + Example: + >>> Interface('eth0').get_alias() + 'interface description as set by user' + """ + return self.get_interface('alias') + + def set_alias(self, ifalias=''): + """ + Set interface alias name used by e.g. SNMP + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_alias('VyOS upstream interface') + + to clear alias e.g. delete it use: + + >>> Interface('eth0').set_ifalias('') + """ + self.set_interface('alias', ifalias) + + def get_vlan_protocol(self): + """ + Retrieve VLAN protocol in use, this can be 802.1Q, 802.1ad or None + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0.10').get_vlan_protocol() + '802.1Q' + """ + return self.get_interface('vlan_protocol') + + def get_admin_state(self): + """ + Get interface administrative state. Function will return 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_admin_state() + 'up' + """ + return self.get_interface('admin_state') + + def set_admin_state(self, state): + """ + Set interface administrative state to be 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_admin_state('down') + >>> Interface('eth0').get_admin_state() + 'down' + """ + # A VLAN interface can only be placed in admin up state when + # the lower interface is up, too + if self.get_vlan_protocol(): + lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] + with open(lower_interface, 'r') as f: + flags = f.read() + # If parent is not up - bail out as we can not bring up the VLAN. + # Flags are defined in kernel source include/uapi/linux/if.h + if not int(flags, 16) & 1: + return None + + if state == 'up': + self._admin_state_down_cnt -= 1 + if self._admin_state_down_cnt < 1: + return self.set_interface('admin_state', state) + else: + self._admin_state_down_cnt += 1 + return self.set_interface('admin_state', state) + + def set_proxy_arp(self, enable): + """ + Set per interface proxy ARP configuration + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_proxy_arp(1) + """ + self.set_interface('proxy_arp', enable) + + def set_proxy_arp_pvlan(self, enable): + """ + Private VLAN proxy arp. + Basically allow proxy arp replies back to the same interface + (from which the ARP request/solicitation was received). + + This is done to support (ethernet) switch features, like RFC + 3069, where the individual ports are NOT allowed to + communicate with each other, but they are allowed to talk to + the upstream router. As described in RFC 3069, it is possible + to allow these hosts to communicate through the upstream + router by proxy_arp'ing. Don't need to be used together with + proxy_arp. + + This technology is known by different names: + In RFC 3069 it is called VLAN Aggregation. + Cisco and Allied Telesyn call it Private VLAN. + Hewlett-Packard call it Source-Port filtering or port-isolation. + Ericsson call it MAC-Forced Forwarding (RFC Draft). + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_proxy_arp_pvlan(1) + """ + self.set_interface('proxy_arp_pvlan', enable) + + def get_addr(self): + """ + Retrieve assigned IPv4 and IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_addrs() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + """ + + ipv4 = [] + ipv6 = [] + + if AF_INET in ifaddresses(self.config['ifname']).keys(): + for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + \ + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append(v4_addr['addr'] + prefix) + + if AF_INET6 in ifaddresses(self.config['ifname']).keys(): + for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]: + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( + int(v6_addr['netmask'].replace(':', ''), 16)).count('1') + prefix = '/' + str(bits) + + # we alsoneed to remove the interface suffix on link local + # addresses + v6_addr['addr'] = v6_addr['addr'].split('%')[0] + ipv6.append(v6_addr['addr'] + prefix) + + return ipv4 + ipv6 + + def add_addr(self, addr): + """ + Add IP(v6) address to interface. Address is only added if it is not + already assigned to that interface. Address format must be validated + and compressed/normalized before calling this function. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: add IPv4 address to interface + IPv6: add IPv6 address to interface + dhcp: start dhclient (IPv4) on interface + dhcpv6: start WIDE DHCPv6 (IPv6) on interface + + Returns False if address is already assigned and wasn't re-added. + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('192.0.2.1/24') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + """ + # XXX: normalize/compress with ipaddress if calling functions don't? + # is subnet mask always passed, and in the same way? + + # do not add same address twice + if addr in self._addr: + return False + + addr_is_v4 = is_ipv4(addr) + + # we can't have both DHCP and static IPv4 addresses assigned + for a in self._addr: + if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or + ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ): + raise ConfigError(( + "Can't configure both static IPv4 and DHCP address " + "on the same interface")) + + # add to interface + if addr == 'dhcp': + self.set_dhcp(True) + elif addr == 'dhcpv6': + self.set_dhcpv6(True) + elif not is_intf_addr_assigned(self.ifname, addr): + self._cmd(f'ip addr add "{addr}" ' + f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"') + else: + return False + + # add to cache + self._addr.append(addr) + + return True + + def del_addr(self, addr): + """ + Delete IP(v6) address from interface. Address is only deleted if it is + assigned to that interface. Address format must be exactly the same as + was used when adding the address. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: delete IPv4 address from interface + IPv6: delete IPv6 address from interface + dhcp: stop dhclient (IPv4) on interface + dhcpv6: stop dhclient (IPv6) on interface + + Returns False if address isn't already assigned and wasn't deleted. + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.add_addr('192.0.2.1/24') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + >>> j.del_addr('192.0.2.1/24') + >>> j.get_addr() + ['2001:db8::ffff/64'] + """ + + # remove from interface + if addr == 'dhcp': + self.set_dhcp(False) + elif addr == 'dhcpv6': + self.set_dhcpv6(False) + elif is_intf_addr_assigned(self.ifname, addr): + self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') + else: + return False + + # remove from cache + if addr in self._addr: + self._addr.remove(addr) + + return True + + def flush_addrs(self): + """ + Flush all addresses from an interface, including DHCP. + + Will raise an exception on error. + """ + # stop DHCP(v6) if running + self.set_dhcp(False) + self.set_dhcpv6(False) + + # flush all addresses + self._cmd(f'ip addr flush dev "{self.ifname}"') + + def add_to_bridge(self, br): + """ + Adds the interface to the bridge with the passed port config. + + Returns False if bridge doesn't exist. + """ + + # check if the bridge exists (on boot it doesn't) + if br not in Section.interfaces('bridge'): + return False + + self.flush_addrs() + # add interface to bridge - use Section.klass to get BridgeIf class + Section.klass(br)(br, create=False).add_port(self.ifname) + + # TODO: port config (STP) + + return True + + def set_dhcp(self, enable): + """ + Enable/Disable DHCP client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_base = r'/var/lib/dhcp/dhclient' + config_file = f'{config_base}_{ifname}.conf' + options_file = f'{config_base}_{ifname}.options' + pid_file = f'{config_base}_{ifname}.pid' + lease_file = f'{config_base}_{ifname}.leases' + + if enable and 'disable' not in self._config: + if vyos_dict_search('dhcp_options.host_name', self._config) == None: + # read configured system hostname. + # maybe change to vyos hostd client ??? + hostname = 'vyos' + with open('/etc/hostname', 'r') as f: + hostname = f.read().rstrip('\n') + tmp = {'dhcp_options' : { 'host_name' : hostname}} + self._config = dict_merge(tmp, self._config) + + render(options_file, 'dhcp-client/daemon-options.tmpl', + self._config, trim_blocks=True) + render(config_file, 'dhcp-client/ipv4.tmpl', + self._config, trim_blocks=True) + + # 'up' check is mandatory b/c even if the interface is A/D, as soon as + # the DHCP client is started the interface will be placed in u/u state. + # This is not what we intended to do when disabling an interface. + return self._cmd(f'systemctl restart dhclient@{ifname}.service') + else: + self._cmd(f'systemctl stop dhclient@{ifname}.service') + + # cleanup old config files + for file in [config_file, options_file, pid_file, lease_file]: + if os.path.isfile(file): + os.remove(file) + + + def set_dhcpv6(self, enable): + """ + Enable/Disable DHCPv6 client on a given interface. + """ + if enable not in [True, False]: + raise ValueError() + + ifname = self.ifname + config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' + + if enable and 'disable' not in self._config: + render(config_file, 'dhcp-client/ipv6.tmpl', + self._config, trim_blocks=True) + + # We must ignore any return codes. This is required to enable DHCPv6-PD + # for interfaces which are yet not up and running. + return self._popen(f'systemctl restart dhcp6c@{ifname}.service') + else: + self._popen(f'systemctl stop dhcp6c@{ifname}.service') + + if os.path.isfile(config_file): + os.remove(config_file) + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # Cache the configuration - it will be reused inside e.g. DHCP handler + # XXX: maybe pass the option via __init__ in the future and rename this + # method to apply()? + self._config = config + + # Update interface description + self.set_alias(config.get('description', '')) + + # Ignore link state changes + value = '2' if 'disable_link_detect' in config else '1' + self.set_link_detect(value) + + # Configure assigned interface IP addresses. No longer + # configured addresses will be removed first + new_addr = config.get('address', []) + + # XXX: T2636 workaround: convert string to a list with one element + if isinstance(new_addr, str): + new_addr = [new_addr] + + # always ensure DHCP client is stopped (when not configured explicitly) + if 'dhcp' not in new_addr: + self.del_addr('dhcp') + + # always ensure DHCPv6 client is stopped (when not configured as client + # for IPv6 address or prefix delegation + dhcpv6pd = vyos_dict_search('dhcpv6_options.pd', config) + if 'dhcpv6' not in new_addr or dhcpv6pd == None: + self.del_addr('dhcpv6') + + # determine IP addresses which are assigned to the interface and build a + # list of addresses which are no longer in the dict so they can be removed + cur_addr = self.get_addr() + for addr in list_diff(cur_addr, new_addr): + self.del_addr(addr) + + for addr in new_addr: + self.add_addr(addr) + + # start DHCPv6 client when only PD was configured + if dhcpv6pd != None: + self.set_dhcpv6(True) + + # There are some items in the configuration which can only be applied + # if this instance is not bound to a bridge. This should be checked + # by the caller but better save then sorry! + if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config): + # Bind interface to given VRF or unbind it if vrf node is not set. + # unbinding will call 'ip link set dev eth0 nomaster' which will + # also drop the interface out of a bridge or bond - thus this is + # checked before + self.set_vrf(config.get('vrf', '')) + + # Configure ARP cache timeout in milliseconds - has default value + tmp = vyos_dict_search('ip.arp_cache_timeout', config) + value = tmp if (tmp != None) else '30' + self.set_arp_cache_tmo(value) + + # Configure ARP filter configuration + tmp = vyos_dict_search('ip.disable_arp_filter', config) + value = '0' if (tmp != None) else '1' + self.set_arp_filter(value) + + # Configure ARP accept + tmp = vyos_dict_search('ip.enable_arp_accept', config) + value = '1' if (tmp != None) else '0' + self.set_arp_accept(value) + + # Configure ARP announce + tmp = vyos_dict_search('ip.enable_arp_announce', config) + value = '1' if (tmp != None) else '0' + self.set_arp_announce(value) + + # Configure ARP ignore + tmp = vyos_dict_search('ip.enable_arp_ignore', config) + value = '1' if (tmp != None) else '0' + self.set_arp_ignore(value) + + # Enable proxy-arp on this interface + tmp = vyos_dict_search('ip.enable_proxy_arp', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp(value) + + # Enable private VLAN proxy ARP on this interface + tmp = vyos_dict_search('ip.proxy_arp_pvlan', config) + value = '1' if (tmp != None) else '0' + self.set_proxy_arp_pvlan(value) + + # IPv6 forwarding + tmp = vyos_dict_search('ipv6.disable_forwarding', config) + value = '0' if (tmp != None) else '1' + self.set_ipv6_forwarding(value) + + # IPv6 router advertisements + tmp = vyos_dict_search('ipv6.address.autoconf', config) + value = '2' if (tmp != None) else '1' + if 'dhcpv6' in new_addr: + value = '2' + self.set_ipv6_accept_ra(value) + + # IPv6 address autoconfiguration + tmp = vyos_dict_search('ipv6.address.autoconf', config) + value = '1' if (tmp != None) else '0' + self.set_ipv6_autoconf(value) + + # IPv6 Duplicate Address Detection (DAD) tries + tmp = vyos_dict_search('ipv6.dup_addr_detect_transmits', config) + value = tmp if (tmp != None) else '1' + self.set_ipv6_dad_messages(value) + + # MTU - Maximum Transfer Unit + if 'mtu' in config: + self.set_mtu(config.get('mtu')) + + # Delete old IPv6 EUI64 addresses before changing MAC + tmp = vyos_dict_search('ipv6.address.eui64_old', config) + if tmp: + for addr in tmp: + self.del_ipv6_eui64_address(addr) + + # Change interface MAC address - re-set to real hardware address (hw-id) + # if custom mac is removed. Skip if bond member. + if 'is_bond_member' not in config: + mac = config.get('hw_id') + if 'mac' in config: + mac = config.get('mac') + if mac: + self.set_mac(mac) + + # Manage IPv6 link-local addresses + tmp = vyos_dict_search('ipv6.address.no_default_link_local', config) + # we must check explicitly for None type as if the key is set we will + # get an empty dict (<class 'dict'>) + if tmp is not None: + self.del_ipv6_eui64_address('fe80::/64') + else: + self.add_ipv6_eui64_address('fe80::/64') + + # Add IPv6 EUI-based addresses + tmp = vyos_dict_search('ipv6.address.eui64', config) + if tmp: + # XXX: T2636 workaround: convert string to a list with one element + if isinstance(tmp, str): + tmp = [tmp] + for addr in tmp: + self.add_ipv6_eui64_address(addr) + + # re-add ourselves to any bridge we might have fallen out of + if 'is_bridge_member' in config: + bridge = config.get('is_bridge_member') + self.add_to_bridge(bridge) + + # remove no longer required 802.1ad (Q-in-Q VLANs) + for vif_s_id in config.get('vif_s_remove', {}): + self.del_vlan(vif_s_id) + + # create/update 802.1ad (Q-in-Q VLANs) + ifname = config['ifname'] + for vif_s_id, vif_s in config.get('vif_s', {}).items(): + tmp=get_ethertype(vif_s.get('ethertype', '0x88A8')) + s_vlan = self.add_vlan(vif_s_id, ethertype=tmp) + vif_s['ifname'] = f'{ifname}.{vif_s_id}' + s_vlan.update(vif_s) + + # remove no longer required client VLAN (vif-c) + for vif_c_id in vif_s.get('vif_c_remove', {}): + s_vlan.del_vlan(vif_c_id) + + # create/update client VLAN (vif-c) interface + for vif_c_id, vif_c in vif_s.get('vif_c', {}).items(): + c_vlan = s_vlan.add_vlan(vif_c_id) + vif_c['ifname'] = f'{ifname}.{vif_s_id}.{vif_c_id}' + c_vlan.update(vif_c) + + # remove no longer required 802.1q VLAN interfaces + for vif_id in config.get('vif_remove', {}): + self.del_vlan(vif_id) + + # create/update 802.1q VLAN interfaces + for vif_id, vif in config.get('vif', {}).items(): + vlan = self.add_vlan(vif_id) + vif['ifname'] = f'{ifname}.{vif_id}' + vlan.update(vif) diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py new file mode 100644 index 000000000..34147eb38 --- /dev/null +++ b/python/vyos/ifconfig/l2tpv3.py @@ -0,0 +1,113 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class L2TPv3If(Interface): + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ + + default = { + 'type': 'l2tp', + } + definition = { + **Interface.definition, + **{ + 'section': 'l2tpeth', + 'prefixes': ['l2tpeth', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['tunnel_id', 'peer_tunnel_id', 'local_port', 'remote_port', + 'encapsulation', 'local_address', 'remote_address', 'session_id', + 'peer_session_id'] + + def _create(self): + # create tunnel interface + cmd = 'ip l2tp add tunnel tunnel_id {tunnel_id}' + cmd += ' peer_tunnel_id {peer_tunnel_id}' + cmd += ' udp_sport {local_port}' + cmd += ' udp_dport {remote_port}' + cmd += ' encap {encapsulation}' + cmd += ' local {local_address}' + cmd += ' remote {remote_address}' + self._cmd(cmd.format(**self.config)) + + # setup session + cmd = 'ip l2tp add session name {ifname}' + cmd += ' tunnel_id {tunnel_id}' + cmd += ' session_id {session_id}' + cmd += ' peer_session_id {peer_session_id}' + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses. + Example: + >>> from vyos.ifconfig import L2TPv3If + >>> i = L2TPv3If('l2tpeth0') + >>> i.remove() + """ + + if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + if self.config['tunnel_id'] and self.config['session_id']: + cmd = 'ip l2tp del session tunnel_id {tunnel_id}' + cmd += ' session_id {session_id}' + self._cmd(cmd.format(**self.config)) + + if self.config['tunnel_id']: + cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}' + self._cmd(cmd.format(**self.config)) + + @staticmethod + def get_config(): + """ + L2TPv3 interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = L2TPv3If().get_config() + """ + config = { + 'peer_tunnel_id': '', + 'local_port': 0, + 'remote_port': 0, + 'encapsulation': 'udp', + 'local_address': '', + 'remote_address': '', + 'session_id': '', + 'tunnel_id': '', + 'peer_session_id': '' + } + return config diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py new file mode 100644 index 000000000..2b4ebfdcc --- /dev/null +++ b/python/vyos/ifconfig/loopback.py @@ -0,0 +1,89 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class LoopbackIf(Interface): + """ + The loopback device is a special, virtual network interface that your router + uses to communicate with itself. + """ + _persistent_addresses = ['127.0.0.1/8', '::1/128'] + default = { + 'type': 'loopback', + } + definition = { + **Interface.definition, + **{ + 'section': 'loopback', + 'prefixes': ['lo', ], + 'bridgeable': True, + } + } + + name = 'loopback' + + def remove(self): + """ + Loopback interface can not be deleted from operating system. We can + only remove all assigned IP addresses. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = LoopbackIf('lo').remove() + """ + # remove all assigned IP addresses from interface + for addr in self.get_addr(): + if addr in self._persistent_addresses: + # Do not allow deletion of the default loopback addresses as + # this will cause weird system behavior like snmp/ssh no longer + # operating as expected, see https://phabricator.vyos.net/T2034. + continue + + self.del_addr(addr) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + addr = config.get('address', []) + # XXX workaround for T2636, convert IP address string to a list + # with one element + if isinstance(addr, str): + addr = [addr] + + # We must ensure that the loopback addresses are never deleted from the system + addr += self._persistent_addresses + + # Update IP address entry in our dictionary + config.update({'address' : addr}) + + # call base class + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py new file mode 100644 index 000000000..6f570d162 --- /dev/null +++ b/python/vyos/ifconfig/macsec.py @@ -0,0 +1,92 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.ifconfig.interface import Interface + +@Interface.register +class MACsecIf(Interface): + """ + MACsec is an IEEE standard (IEEE 802.1AE) for MAC security, introduced in + 2006. It defines a way to establish a protocol independent connection + between two hosts with data confidentiality, authenticity and/or integrity, + using GCM-AES-128. MACsec operates on the Ethernet layer and as such is a + layer 2 protocol, which means it's designed to secure traffic within a + layer 2 network, including DHCP or ARP requests. It does not compete with + other security solutions such as IPsec (layer 3) or TLS (layer 4), as all + those solutions are used for their own specific use cases. + """ + + default = { + 'type': 'macsec', + 'security_cipher': '', + 'source_interface': '' + } + definition = { + **Interface.definition, + **{ + 'section': 'macsec', + 'prefixes': ['macsec', ], + }, + } + options = Interface.options + \ + ['security_cipher', 'source_interface'] + + def _create(self): + """ + Create MACsec interface in OS kernel. Interface is administrative + down by default. + """ + # create tunnel interface + cmd = 'ip link add link {source_interface} {ifname} type {type}' + cmd += ' cipher {security_cipher}' + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @staticmethod + def get_config(): + """ + MACsec interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = MACsecIf().get_config() + """ + config = { + 'security_cipher': '', + 'source_interface': '', + } + return config + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py new file mode 100644 index 000000000..b068ce873 --- /dev/null +++ b/python/vyos/ifconfig/macvlan.py @@ -0,0 +1,89 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from copy import deepcopy + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN + + +@Interface.register +@VLAN.enable +class MACVLANIf(Interface): + """ + Abstraction of a Linux MACvlan interface + """ + + default = { + 'type': 'macvlan', + 'address': '', + 'source_interface': '', + 'mode': '', + } + definition = { + **Interface.definition, + **{ + 'section': 'pseudo-ethernet', + 'prefixes': ['peth', ], + }, + } + options = Interface.options + \ + ['source_interface', 'mode'] + + def _create(self): + # please do not change the order when assembling the command + cmd = 'ip link add {ifname}' + if self.config['source_interface']: + cmd += ' link {source_interface}' + cmd += ' type macvlan' + if self.config['mode']: + cmd += ' mode {mode}' + self._cmd(cmd.format(**self.config)) + + def set_mode(self, mode): + ifname = self.config['ifname'] + cmd = f'ip link set dev {ifname} type macvlan mode {mode}' + return self._cmd(cmd) + + @classmethod + def get_config(cls): + """ + MACVLAN interfaces require a configuration when they are added using + iproute2. This method will provide the configuration dictionary used + by this class. + + Example: + >> dict = MACVLANIf().get_config() + """ + return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/operational.py b/python/vyos/ifconfig/operational.py new file mode 100644 index 000000000..d585c1873 --- /dev/null +++ b/python/vyos/ifconfig/operational.py @@ -0,0 +1,179 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +from time import time +from datetime import datetime +from functools import reduce + +from tabulate import tabulate + +from vyos.ifconfig import Control + + +class Operational(Control): + """ + A class able to load Interface statistics + """ + + cache_magic = 'XYZZYX' + + _stat_names = { + 'rx': ['bytes', 'packets', 'errors', 'dropped', 'overrun', 'mcast'], + 'tx': ['bytes', 'packets', 'errors', 'dropped', 'carrier', 'collisions'], + } + + _stats_dir = { + 'rx': ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped', 'rx_over_errors', 'multicast'], + 'tx': ['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped', 'tx_carrier_errors', 'collisions'], + } + + # a list made of the content of _stats_dir['rx'] + _stats_dir['tx'] + _stats_all = reduce(lambda x, y: x+y, _stats_dir.values()) + + # this is not an interface but will be able to be controlled like one + _sysfs_get = { + 'oper_state':{ + 'location': '/sys/class/net/{ifname}/operstate', + }, + } + + + @classmethod + def cachefile (cls, ifname): + # the file where we are saving the counters + return f'/var/run/vyatta/{ifname}.stats' + + + def __init__(self, ifname): + """ + Operational provide access to the counters of an interface + It behave like an interface when it comes to access sysfs + + interface is an instance of the interface for which we want + to look at (a subclass of Interface, such as EthernetIf) + """ + + # add a self.config to minic Interface behaviour and make + # coding similar. Perhaps part of class Interface could be + # moved into a shared base class. + self.config = { + 'ifname': ifname, + 'create': False, + 'debug': False, + } + super().__init__(**self.config) + self.ifname = ifname + + # adds all the counters of an interface + for stat in self._stats_all: + self._sysfs_get[stat] = { + 'location': '/sys/class/net/{ifname}/statistics/'+stat, + } + + def get_state(self): + """ + Get interface operational state + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').operational.get_sate() + 'up' + """ + # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net + # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up" + return self.get_interface('oper_state') + + @classmethod + def strtime (cls, epoc): + """ + represent an epoc/unix date in the format used by operation commands + """ + return datetime.fromtimestamp(epoc).strftime("%a %b %d %R:%S %Z %Y") + + def save_counters(self, stats): + """ + record the provided stats to a file keeping vyatta compatibility + """ + + with open(self.cachefile(self.ifname), 'w') as f: + f.write(self.cache_magic) + f.write('\n') + f.write(str(int(time()))) + f.write('\n') + for k,v in stats.items(): + if v: + f.write(f'{k},{v}\n') + + def load_counters(self): + """ + load the stats from a file keeping vyatta compatibility + return a dict() with the value for each interface counter for the cache + """ + ifname = self.config['ifname'] + + stats = {} + no_stats = {} + for name in self._stats_all: + stats[name] = 0 + no_stats[name] = 0 + + try: + with open(self.cachefile(self.ifname),'r') as f: + magic = f.readline().strip() + if magic != self.cache_magic: + print(f'bad magic {ifname}') + return no_stats + stats['timestamp'] = f.readline().strip() + for line in f: + k, v = line.split(',') + stats[k] = int(v) + return stats + except IOError: + return no_stats + + def clear_counters(self, counters=None): + clear = self._stats_all if counters is None else [] + stats = self.load_counters() + for counter, value in stats.items(): + stats[counter] = 0 if counter in clear else value + self.save_counters(stats) + + def reset_counters(self): + os.remove(self.cachefile(self.ifname)) + + def get_stats(self): + """ return a dict() with the value for each interface counter """ + stats = {} + for counter in self._stats_all: + stats[counter] = int(self.get_interface(counter)) + return stats + + def formated_stats(self, indent=4): + tabs = [] + stats = self.get_stats() + for rtx in self._stats_dir: + tabs.append([f'{rtx.upper()}:', ] + [_ for _ in self._stat_names[rtx]]) + tabs.append(['', ] + [stats[_] for _ in self._stats_dir[rtx]]) + + s = tabulate( + tabs, + stralign="right", + numalign="right", + tablefmt="plain" + ) + + p = ' '*indent + return f'{p}' + s.replace('\n', f'\n{p}') diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py new file mode 100644 index 000000000..787245696 --- /dev/null +++ b/python/vyos/ifconfig/pppoe.py @@ -0,0 +1,41 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class PPPoEIf(Interface): + default = { + 'type': 'pppoe', + } + definition = { + **Interface.definition, + **{ + 'section': 'pppoe', + 'prefixes': ['pppoe', ], + }, + } + + # stub this interface is created in the configure script + + def _create(self): + # we can not create this interface as it is managed outside + pass + + def _delete(self): + # we can not create this interface as it is managed outside + pass diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py new file mode 100644 index 000000000..173a90bb4 --- /dev/null +++ b/python/vyos/ifconfig/section.py @@ -0,0 +1,189 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import re +import netifaces + + +class Section: + # the known interface prefixes + _prefixes = {} + _classes = [] + + # class need to define: definition['prefixes'] + # the interface prefixes declared by a class used to name interface with + # prefix[0-9]*(\.[0-9]+)?(\.[0-9]+)?, such as lo, eth0 or eth0.1.2 + + @classmethod + def register(cls, klass): + """ + A function to use as decorator the interfaces classes + It register the prefix for the interface (eth, dum, vxlan, ...) + with the class which can handle it (EthernetIf, DummyIf,VXLANIf, ...) + """ + if not klass.definition.get('prefixes',[]): + raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}') + + cls._classes.append(klass) + + for ifprefix in klass.definition['prefixes']: + if ifprefix in cls._prefixes: + raise RuntimeError(f'only one class can be registered for prefix "{ifprefix}" type') + cls._prefixes[ifprefix] = klass + + return klass + + @classmethod + def _basename (cls, name, vlan): + """ + remove the number at the end of interface name + name: name of the interface + vlan: if vlan is True, do not stop at the vlan number + """ + name = name.rstrip('0123456789') + name = name.rstrip('.') + if vlan: + name = name.rstrip('0123456789.') + return name + + @classmethod + def section(cls, name, vlan=True): + """ + return the name of a section an interface should be under + name: name of the interface (eth0, dum1, ...) + vlan: should we try try to remove the VLAN from the number + """ + name = cls._basename(name, vlan) + + if name in cls._prefixes: + return cls._prefixes[name].definition['section'] + return '' + + @classmethod + def sections(cls): + """ + return all the sections we found under 'set interfaces' + """ + return list(set([cls._prefixes[_].definition['section'] for _ in cls._prefixes])) + + @classmethod + def klass(cls, name, vlan=True): + name = cls._basename(name, vlan) + if name in cls._prefixes: + return cls._prefixes[name] + raise ValueError(f'No type found for interface name: {name}') + + @classmethod + def _intf_under_section (cls,section=''): + """ + return a generator with the name of the configured interface + which are under a section + """ + interfaces = netifaces.interfaces() + + for ifname in interfaces: + ifsection = cls.section(ifname) + if not ifsection: + continue + + if section and ifsection != section: + continue + + yield ifname + + @classmethod + def _sort_interfaces(cls, generator): + """ + return a list of the sorted interface by number, vlan, qinq + """ + def key(ifname): + value = 0 + parts = re.split(r'([^0-9]+)([0-9]+)[.]?([0-9]+)?[.]?([0-9]+)?', ifname) + length = len(parts) + name = parts[1] if length >= 3 else parts[0] + # the +1 makes sure eth0.0.0 after eth0.0 + number = int(parts[2]) + 1 if length >= 4 and parts[2] is not None else 0 + vlan = int(parts[3]) + 1 if length >= 5 and parts[3] is not None else 0 + qinq = int(parts[4]) + 1 if length >= 6 and parts[4] is not None else 0 + + # so that "lo" (or short names) are handled (as "loa") + for n in (name + 'aaa')[:3]: + value *= 100 + value += (ord(n) - ord('a')) + value += number + # vlan are 16 bits, so this can not overflow + value = (value << 16) + vlan + value = (value << 16) + qinq + return value + + l = list(generator) + l.sort(key=key) + return l + + @classmethod + def interfaces(cls, section=''): + """ + return a list of the name of the configured interface which are under a section + if no section is provided, then it returns all configured interfaces + """ + + return cls._sort_interfaces(cls._intf_under_section(section)) + + @classmethod + def _intf_with_feature(cls, feature=''): + """ + return a generator with the name of the configured interface which have + a particular feature set in their definition such as: + bondable, broadcast, bridgeable, ... + """ + for klass in cls._classes: + if klass.definition[feature]: + yield klass.definition['section'] + + @classmethod + def feature(cls, feature=''): + """ + return list with the name of the configured interface which have + a particular feature set in their definition such as: + bondable, broadcast, bridgeable, ... + """ + return list(cls._intf_with_feature(feature)) + + @classmethod + def reserved(cls): + """ + return list with the interface name prefixes + eth, lo, vxlan, dum, ... + """ + return list(cls._prefixes.keys()) + + @classmethod + def get_config_path(cls, name): + """ + get config path to interface with .vif or .vif-s.vif-c + example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2' + Returns False if interface name is invalid (not found in sections) + """ + sect = cls.section(name) + if sect: + splinterface = name.split('.') + intfpath = f'{sect} {splinterface[0]}' + if len(splinterface) == 2: + intfpath += f' vif {splinterface[1]}' + elif len(splinterface) == 3: + intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}' + return intfpath + else: + return False diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py new file mode 100644 index 000000000..5e83206c2 --- /dev/null +++ b/python/vyos/ifconfig/stp.py @@ -0,0 +1,70 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + +from vyos.validate import assert_positive + + +class STP: + """ + A spanning-tree capable interface. This applies only to bridge port member + interfaces! + """ + + @classmethod + def enable (cls, adaptee): + adaptee._sysfs_set = {**adaptee._sysfs_set, **cls._sysfs_set} + adaptee.set_path_cost = cls.set_path_cost + adaptee.set_path_priority = cls.set_path_priority + return adaptee + + _sysfs_set = { + 'path_cost': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/path_cost', + 'errormsg': '{ifname} is not a bridge port member' + }, + 'path_priority': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/priority', + 'errormsg': '{ifname} is not a bridge port member' + }, + } + + def set_path_cost(self, cost): + """ + Set interface path cost, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_path_cost(4) + """ + self.set_interface('path_cost', cost) + + def set_path_priority(self, priority): + """ + Set interface path priority, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_path_priority(4) + """ + self.set_interface('path_priority', priority) diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py new file mode 100644 index 000000000..85c22b5b4 --- /dev/null +++ b/python/vyos/ifconfig/tunnel.py @@ -0,0 +1,338 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +# https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/ +# https://community.hetzner.com/tutorials/linux-setup-gre-tunnel + + +from copy import deepcopy + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.afi import IP4, IP6 +from vyos.validate import assert_list + +def enable_to_on(value): + if value == 'enable': + return 'on' + if value == 'disable': + return 'off' + raise ValueError(f'expect enable or disable but got "{value}"') + + +@Interface.register +class _Tunnel(Interface): + """ + _Tunnel: private base class for tunnels + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/tunnel.c + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip6tunnel.c + """ + definition = { + **Interface.definition, + **{ + 'section': 'tunnel', + 'prefixes': ['tun',], + 'bridgeable': False, + }, + } + + # TODO: This is surely used for more than tunnels + # TODO: could be refactored elsewhere + _command_set = {**Interface._command_set, **{ + 'multicast': { + 'validate': lambda v: assert_list(v, ['enable', 'disable']), + 'convert': enable_to_on, + 'shellcmd': 'ip link set dev {ifname} multicast {value}', + }, + 'allmulticast': { + 'validate': lambda v: assert_list(v, ['enable', 'disable']), + 'convert': enable_to_on, + 'shellcmd': 'ip link set dev {ifname} allmulticast {value}', + }, + }} + + # use for "options" and "updates" + # If an key is only in the options list, it can only be set at creation time + # the create comand will only be make using the key in options + + # If an option is in the updates list, it can be updated + # upon, the creation, all key not yet applied will be updated + + # multicast/allmulticast can not be part of the create command + + # options matrix: + # with ip = 4, we have multicast + # wiht ip = 6, nothing + # with tunnel = 4, we have tos, ttl, key + # with tunnel = 6, we have encaplimit, hoplimit, tclass, flowlabel + + # TODO: For multicast, it is allowed on IP6IP6 and Sit6RD + # TODO: to match vyatta but it should be checked for correctness + + updates = [] + + create = '' + change = '' + delete = '' + + ip = [] # AFI of the families which can be used in the tunnel + tunnel = 0 # invalid - need to be set by subclasses + + def __init__(self, ifname, **config): + self.config = deepcopy(config) if config else {} + super().__init__(ifname, **config) + + def _create(self): + # add " option-name option-name-value ..." for all options set + options = " ".join(["{} {}".format(k, self.config[k]) + for k in self.options if k in self.config and self.config[k]]) + self._cmd('{} {}'.format(self.create.format(**self.config), options)) + self.set_admin_state('down') + + def _delete(self): + self.set_admin_state('down') + cmd = self.delete.format(**self.config) + return self._cmd(cmd) + + def set_interface(self, option, value): + try: + return Interface.set_interface(self, option, value) + except Exception: + pass + + if value == '': + # remove the value so that it is not used + self.config.pop(option, '') + + if self.change: + self._cmd('{} {} {}'.format( + self.change.format(**self.config), option, value)) + return True + + @classmethod + def get_config(cls): + return dict(zip(cls.options, ['']*len(cls.options))) + + +class GREIf(_Tunnel): + """ + GRE: Generic Routing Encapsulation + + For more information please refer to: + RFC1701, RFC1702, RFC2784 + https://tools.ietf.org/html/rfc2784 + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre.c + """ + + definition = { + **_Tunnel.definition, + **{ + 'bridgeable': True, + }, + } + + ip = [IP4, IP6] + tunnel = IP4 + + default = {'type': 'gre'} + required = ['local', ] # mGRE is a GRE without remote endpoint + + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +# GreTap also called GRE Bridge +class GRETapIf(_Tunnel): + """ + GRETapIF: GreIF using TAP instead of TUN + + https://en.wikipedia.org/wiki/TUN/TAP + """ + + # no multicast, ttl or tos for gretap + + definition = { + **_Tunnel.definition, + **{ + 'bridgeable': True, + }, + } + + ip = [IP4, ] + tunnel = IP4 + + default = {'type': 'gretap'} + required = ['local', ] + + options = ['local', 'remote', ] + updates = ['mtu', ] + + create = 'ip link add {ifname} type {type}' + change = '' + delete = 'ip link del {ifname}' + + +class IP6GREIf(_Tunnel): + """ + IP6Gre: IPv6 Support for Generic Routing Encapsulation (GRE) + + For more information please refer to: + https://tools.ietf.org/html/rfc7676 + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre6.c + """ + + ip = [IP4, IP6] + tunnel = IP6 + + default = {'type': 'ip6gre'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname} mode {type}' + delete = 'ip tunnel del {ifname}' + + # using "ip tunnel change" without using "mode" causes errors + # sudo ip tunnel add tun100 mode ip6gre local ::1 remote 1::1 + # sudo ip tunnel cha tun100 hoplimit 100 + # *** stack smashing detected ** *: < unknown > terminated + # sudo ip tunnel cha tun100 local: : 2 + # Error: an IP address is expected rather than "::2" + # works if mode is explicit + + +class IPIPIf(_Tunnel): + """ + IPIP: IP Encapsulation within IP + + For more information please refer to: + https://tools.ietf.org/html/rfc2003 + """ + + # IPIP does not allow to pass multicast, unlike GRE + # but the interface itself can be set with multicast + + ip = [IP4,] + tunnel = IP4 + + default = {'type': 'ipip'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +class IPIP6If(_Tunnel): + """ + IPIP6: IPv4 over IPv6 tunnel + + For more information please refer to: + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_ip6tnl.c + """ + + ip = [IP4,] + tunnel = IP6 + + default = {'type': 'ipip6'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'dev', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip -6 tunnel add {ifname} mode {type}' + change = 'ip -6 tunnel cha {ifname}' + delete = 'ip -6 tunnel del {ifname}' + + +class IP6IP6If(IPIP6If): + """ + IP6IP6: IPv6 over IPv6 tunnel + + For more information please refer to: + https://tools.ietf.org/html/rfc2473 + """ + + ip = [IP6,] + + default = {'type': 'ip6ip6'} + + +class SitIf(_Tunnel): + """ + Sit: Simple Internet Transition + + For more information please refer to: + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_iptnl.c + """ + + ip = [IP6, IP4] + tunnel = IP4 + + default = {'type': 'sit'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +class Sit6RDIf(SitIf): + """ + Sit6RDIf: Simple Internet Transition with 6RD + + https://en.wikipedia.org/wiki/IPv6_rapid_deployment + """ + + ip = [IP6,] + + required = ['remote', '6rd-prefix'] + + # TODO: check if key can really be used with 6RD + options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix'] + updates = ['remote', 'ttl', 'tos', + 'mtu', 'multicast', 'allmulticast'] + + def _create(self): + # do not call _Tunnel.create, building fully here + + create = 'ip tunnel add {ifname} mode {type} remote {remote}' + self._cmd(create.format(**self.config)) + self.set_interface('state','down') + + set6rd = 'ip tunnel 6rd dev {ifname} 6rd-prefix {6rd-prefix}' + if '6rd-relay-prefix' in self.config: + set6rd += ' 6rd-relay-prefix {6rd-relay-prefix}' + self._cmd(set6rd.format(**self.config)) diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py new file mode 100644 index 000000000..d68e8f6cd --- /dev/null +++ b/python/vyos/ifconfig/vlan.py @@ -0,0 +1,142 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os +import re + +from vyos.ifconfig.interface import Interface + + +# This is an internal implementation class +class VLAN: + """ + This class handels the creation and removal of a VLAN interface. It serves + as base class for BondIf and EthernetIf. + """ + + _novlan_remove = lambda : None + + @classmethod + def enable (cls,adaptee): + adaptee._novlan_remove = adaptee.remove + adaptee.remove = cls.remove + adaptee.add_vlan = cls.add_vlan + adaptee.del_vlan = cls.del_vlan + adaptee.definition['vlan'] = True + return adaptee + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + ifname = self.config['ifname'] + + # Do we have sub interfaces (VLANs)? We apply a regex matching + # subinterfaces (indicated by a .) of a parent interface. + # + # As interfaces need to be deleted "in order" starting from Q-in-Q + # we delete them first. + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') + if re.match(ifname + r'(?:\.\d+)(?:\.\d+)', f)] + + for vlan in vlan_ifs: + Interface(vlan).remove() + + # After deleting all Q-in-Q interfaces delete other VLAN interfaces + # which probably acted as parent to Q-in-Q or have been regular 802.1q + # interface. + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') + if re.match(ifname + r'(?:\.\d+)', f)] + + for vlan in vlan_ifs: + # self.__class__ is already VLAN.enabled + self.__class__(vlan)._novlan_remove() + + # All subinterfaces are now removed, continue on the physical interface + self._novlan_remove() + + def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''): + """ + A virtual LAN (VLAN) is any broadcast domain that is partitioned and + isolated in a computer network at the data link layer (OSI layer 2). + Use this function to create a new VLAN interface on a given physical + interface. + + This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto + parameter is used to indicate VLAN type. + + A new object of type VLANIf is returned once the interface has been + created. + + @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN + interface + @param ingress_qos: Defines a mapping of VLAN header prio field to the + Linux internal packet priority on incoming frames. + @param ingress_qos: Defines a mapping of Linux internal packet priority + to VLAN header prio field but for outgoing frames. + + Example: + >>> from vyos.ifconfig import MACVLANIf + >>> i = MACVLANIf('eth0') + >>> i.add_vlan(10) + """ + vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) + if os.path.exists(f'/sys/class/net/{vlan_ifname}'): + return self.__class__(vlan_ifname) + + if ethertype: + self._ethertype = ethertype + ethertype = 'proto {}'.format(ethertype) + + # Optional ingress QOS mapping + opt_i = '' + if ingress_qos: + opt_i = 'ingress-qos-map ' + ingress_qos + # Optional egress QOS mapping + opt_e = '' + if egress_qos: + opt_e = 'egress-qos-map ' + egress_qos + + # create interface in the system + cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ + .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) + self._cmd(cmd) + + # return new object mapping to the newly created interface + # we can now work on this object for e.g. IP address setting + # or interface description and so on + return self.__class__(vlan_ifname) + + def del_vlan(self, vlan_id): + """ + Remove VLAN interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import MACVLANIf + >>> i = MACVLANIf('eth0.10') + >>> i.del_vlan() + """ + ifname = self.config['ifname'] + self.__class__(f'{ifname}.{vlan_id}')._novlan_remove() diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py new file mode 100644 index 000000000..01a7cc7ab --- /dev/null +++ b/python/vyos/ifconfig/vrrp.py @@ -0,0 +1,151 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import json +import signal +from time import time +from time import sleep + +from tabulate import tabulate + +from vyos import airbag +from vyos import util + + +class VRRPError(Exception): + pass + +class VRRPNoData(VRRPError): + pass + +class VRRP(object): + _vrrp_prefix = '00:00:5E:00:01:' + location = { + 'pid': '/run/keepalived.pid', + 'fifo': '/run/keepalived_notify_fifo', + 'state': '/tmp/keepalived.data', + 'stats': '/tmp/keepalived.stats', + 'json': '/tmp/keepalived.json', + 'daemon': '/etc/default/keepalived', + 'config': '/etc/keepalived/keepalived.conf', + 'vyos': '/run/keepalived_config.dict', + } + + _signal = { + 'state': signal.SIGUSR1, + 'stats': signal.SIGUSR2, + 'json': signal.SIGRTMIN + 2, + } + + _name = { + 'state': 'information', + 'stats': 'statistics', + 'json': 'data', + } + + state = { + 0: 'INIT', + 1: 'BACKUP', + 2: 'MASTER', + 3: 'FAULT', + # UNKNOWN + } + + def __init__(self,ifname): + self.ifname = ifname + + def enabled(self): + return self.ifname in self.active_interfaces() + + @classmethod + def active_interfaces(cls): + if not os.path.exists(cls.location['pid']): + return [] + data = cls.collect('json') + return [group['data']['ifp_ifname'] for group in json.loads(data)] + + @classmethod + def decode_state(cls, code): + return cls.state.get(code,'UNKNOWN') + + # used in conf mode + @classmethod + def is_running(cls): + if not os.path.exists(cls.location['pid']): + return False + return util.process_running(cls.location['pid']) + + @classmethod + def collect(cls, what): + fname = cls.location[what] + try: + # send signal to generate the configuration file + pid = util.read_file(cls.location['pid']) + os.kill(int(pid), cls._signal[what]) + + # should look for file size change? + sleep(0.2) + return util.read_file(fname) + except FileNotFoundError: + raise VRRPNoData("VRRP data is not available (process not running or no active groups)") + except Exception: + name = cls._name[what] + raise VRRPError(f'VRRP {name} is not available') + finally: + if os.path.exists(fname): + os.remove(fname) + + @classmethod + def disabled(cls): + if not os.path.exists(cls.location['vyos']): + return [] + + disabled = [] + config = json.loads(util.read_file(cls.location['vyos'])) + + # add disabled groups to the list + for group in config['vrrp_groups']: + if group['disable']: + disabled.append( + [group['name'], group['interface'], group['vrid'], 'DISABLED', '']) + + # return list with disabled instances + return disabled + + @classmethod + def format(cls, data): + headers = ["Name", "Interface", "VRID", "State", "Priority", "Last Transition"] + groups = [] + + data = json.loads(data) + for group in data: + data = group['data'] + + name = data['iname'] + intf = data['ifp_ifname'] + vrid = data['vrid'] + state = cls.decode_state(data["state"]) + priority = data['effective_priority'] + + since = int(time() - float(data['last_transition'])) + last = util.seconds_to_human(since) + + groups.append([name, intf, vrid, state, priority, last]) + + # add to the active list disabled instances + groups.extend(cls.disabled()) + return(tabulate(groups, headers)) + diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py new file mode 100644 index 000000000..56ebe01d1 --- /dev/null +++ b/python/vyos/ifconfig/vti.py @@ -0,0 +1,31 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class VTIIf(Interface): + default = { + 'type': 'vti', + } + definition = { + **Interface.definition, + **{ + 'section': 'vti', + 'prefixes': ['vti', ], + }, + } diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py new file mode 100644 index 000000000..60c178b9a --- /dev/null +++ b/python/vyos/ifconfig/vtun.py @@ -0,0 +1,44 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class VTunIf(Interface): + default = { + 'type': 'vtun', + } + definition = { + **Interface.definition, + **{ + 'section': 'openvpn', + 'prefixes': ['vtun', ], + 'bridgeable': True, + }, + } + + # stub this interface is created in the configure script + + def _create(self): + # we can not create this interface as it is managed outside + # it requires configuring OpenVPN + pass + + def _delete(self): + # we can not create this interface as it is managed outside + # it requires configuring OpenVPN + pass diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py new file mode 100644 index 000000000..18a500336 --- /dev/null +++ b/python/vyos/ifconfig/vxlan.py @@ -0,0 +1,130 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from copy import deepcopy + +from vyos import ConfigError +from vyos.ifconfig.interface import Interface + + +@Interface.register +class VXLANIf(Interface): + """ + The VXLAN protocol is a tunnelling protocol designed to solve the + problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the + size of the identifier is expanded to 24 bits (16777216). + + VXLAN is described by IETF RFC 7348, and has been implemented by a + number of vendors. The protocol runs over UDP using a single + destination port. This document describes the Linux kernel tunnel + device, there is also a separate implementation of VXLAN for + Openvswitch. + + Unlike most tunnels, a VXLAN is a 1 to N network, not just point to + point. A VXLAN device can learn the IP address of the other endpoint + either dynamically in a manner similar to a learning bridge, or make + use of statically-configured forwarding entries. + + For more information please refer to: + https://www.kernel.org/doc/Documentation/networking/vxlan.txt + """ + + default = { + 'type': 'vxlan', + 'group': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port + 'remote': '', + 'source_address': '', + 'source_interface': '', + 'vni': 0 + } + definition = { + **Interface.definition, + **{ + 'section': 'vxlan', + 'prefixes': ['vxlan', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['group', 'remote', 'source_interface', 'port', 'vni', 'source_address'] + + mapping = { + 'ifname': 'add', + 'vni': 'id', + 'port': 'dstport', + 'source_address': 'local', + 'source_interface': 'dev', + } + + def _create(self): + cmdline = ['ifname', 'type', 'vni', 'port'] + + if self.config['source_address']: + cmdline.append('source_address') + + if self.config['remote']: + cmdline.append('remote') + + if self.config['group'] or self.config['source_interface']: + if self.config['group'] and self.config['source_interface']: + cmdline.append('group') + cmdline.append('source_interface') + else: + ifname = self.config['ifname'] + raise ConfigError( + f'VXLAN "{ifname}" is missing mandatory underlay multicast' + 'group or source interface for a multicast network.') + + cmd = 'ip link' + for key in cmdline: + value = self.config.get(key, '') + if not value: + continue + cmd += ' {} {}'.format(self.mapping.get(key, key), value) + + self._cmd(cmd) + + @classmethod + def get_config(cls): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = VXLANIf().get_config() + """ + return deepcopy(cls.default) + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py new file mode 100644 index 000000000..fad4ef282 --- /dev/null +++ b/python/vyos/ifconfig/wireguard.py @@ -0,0 +1,247 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os +import time +from datetime import timedelta + +from hurry.filesize import size +from hurry.filesize import alternative + +from vyos.config import Config +from vyos.ifconfig import Interface +from vyos.ifconfig import Operational +from vyos.validate import is_ipv6 + +class WireGuardOperational(Operational): + def _dump(self): + """Dump wireguard data in a python friendly way.""" + last_device = None + output = {} + + # Dump wireguard connection data + _f = self._cmd('wg show all dump') + for line in _f.split('\n'): + if not line: + # Skip empty lines and last line + continue + items = line.split('\t') + + if last_device != items[0]: + # We are currently entering a new node + device, private_key, public_key, listen_port, fw_mark = items + last_device = device + + output[device] = { + 'private_key': None if private_key == '(none)' else private_key, + 'public_key': None if public_key == '(none)' else public_key, + 'listen_port': int(listen_port), + 'fw_mark': None if fw_mark == 'off' else int(fw_mark), + 'peers': {}, + } + else: + # We are entering a peer + device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items + if allowed_ips == '(none)': + allowed_ips = [] + else: + allowed_ips = allowed_ips.split('\t') + output[device]['peers'][public_key] = { + 'preshared_key': None if preshared_key == '(none)' else preshared_key, + 'endpoint': None if endpoint == '(none)' else endpoint, + 'allowed_ips': allowed_ips, + 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake), + 'transfer_rx': int(transfer_rx), + 'transfer_tx': int(transfer_tx), + 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive), + } + return output + + def show_interface(self): + wgdump = self._dump().get(self.config['ifname'], None) + + c = Config() + + c.set_level(["interfaces", "wireguard", self.config['ifname']]) + description = c.return_effective_value(["description"]) + ips = c.return_effective_values(["address"]) + + answer = "interface: {}\n".format(self.config['ifname']) + if (description): + answer += " description: {}\n".format(description) + if (ips): + answer += " address: {}\n".format(", ".join(ips)) + + answer += " public key: {}\n".format(wgdump['public_key']) + answer += " private key: (hidden)\n" + answer += " listening port: {}\n".format(wgdump['listen_port']) + answer += "\n" + + for peer in c.list_effective_nodes(["peer"]): + if wgdump['peers']: + pubkey = c.return_effective_value(["peer", peer, "pubkey"]) + if pubkey in wgdump['peers']: + wgpeer = wgdump['peers'][pubkey] + + answer += " peer: {}\n".format(peer) + answer += " public key: {}\n".format(pubkey) + + """ figure out if the tunnel is recently active or not """ + status = "inactive" + if (wgpeer['latest_handshake'] is None): + """ no handshake ever """ + status = "inactive" + else: + if int(wgpeer['latest_handshake']) > 0: + delta = timedelta(seconds=int( + time.time() - wgpeer['latest_handshake'])) + answer += " latest handshake: {}\n".format(delta) + if (time.time() - int(wgpeer['latest_handshake']) < (60*5)): + """ Five minutes and the tunnel is still active """ + status = "active" + else: + """ it's been longer than 5 minutes """ + status = "inactive" + elif int(wgpeer['latest_handshake']) == 0: + """ no handshake ever """ + status = "inactive" + answer += " status: {}\n".format(status) + + if wgpeer['endpoint'] is not None: + answer += " endpoint: {}\n".format(wgpeer['endpoint']) + + if wgpeer['allowed_ips'] is not None: + answer += " allowed ips: {}\n".format( + ",".join(wgpeer['allowed_ips']).replace(",", ", ")) + + if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: + rx_size = size( + wgpeer['transfer_rx'], system=alternative) + tx_size = size( + wgpeer['transfer_tx'], system=alternative) + answer += " transfer: {} received, {} sent\n".format( + rx_size, tx_size) + + if wgpeer['persistent_keepalive'] is not None: + answer += " persistent keepalive: every {} seconds\n".format( + wgpeer['persistent_keepalive']) + answer += '\n' + return answer + super().formated_stats() + + +@Interface.register +class WireGuardIf(Interface): + OperationalClass = WireGuardOperational + + default = { + 'type': 'wireguard', + 'port': 0, + 'private_key': None, + 'pubkey': None, + 'psk': '', + 'allowed_ips': [], + 'fwmark': 0x00, + 'endpoint': None, + 'keepalive': 0 + } + definition = { + **Interface.definition, + **{ + 'section': 'wireguard', + 'prefixes': ['wg', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['port', 'private_key', 'pubkey', 'psk', + 'allowed_ips', 'fwmark', 'endpoint', 'keepalive'] + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # remove no longer associated peers first + if 'peer_remove' in config: + for tmp in config['peer_remove']: + peer = config['peer_remove'][tmp] + peer['ifname'] = config['ifname'] + + cmd = 'wg set {ifname} peer {pubkey} remove' + self._cmd(cmd.format(**peer)) + + # Wireguard base command is identical for every peer + base_cmd = 'wg set {ifname} private-key {private_key}' + if 'port' in config: + base_cmd += ' listen-port {port}' + if 'fwmark' in config: + base_cmd += ' fwmark {fwmark}' + + base_cmd = base_cmd.format(**config) + + for tmp in config['peer']: + peer = config['peer'][tmp] + + # start of with a fresh 'wg' command + cmd = base_cmd + ' peer {pubkey}' + + # If no PSK is given remove it by using /dev/null - passing keys via + # the shell (usually bash) is considered insecure, thus we use a file + no_psk_file = '/dev/null' + psk_file = no_psk_file + if 'preshared_key' in peer: + psk_file = '/tmp/tmp.wireguard.psk' + with open(psk_file, 'w') as f: + f.write(peer['preshared_key']) + cmd += f' preshared-key {psk_file}' + + # Persistent keepalive is optional + if 'persistent_keepalive'in peer: + cmd += ' persistent-keepalive {persistent_keepalive}' + + # Multiple allowed-ip ranges can be defined - ensure we are always + # dealing with a list + if isinstance(peer['allowed_ips'], str): + peer['allowed_ips'] = [peer['allowed_ips']] + cmd += ' allowed-ips ' + ','.join(peer['allowed_ips']) + + # Endpoint configuration is optional + if {'address', 'port'} <= set(peer): + if is_ipv6(config['address']): + cmd += ' endpoint [{address}]:{port}' + else: + cmd += ' endpoint {address}:{port}' + + self._cmd(cmd.format(**peer)) + + # PSK key file is not required to be stored persistently as its backed by CLI + if psk_file != no_psk_file and os.path.exists(psk_file): + os.remove(psk_file) + + # call base class + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) + diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py new file mode 100644 index 000000000..a50346ffa --- /dev/null +++ b/python/vyos/ifconfig/wireless.py @@ -0,0 +1,102 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN + + +@Interface.register +@VLAN.enable +class WiFiIf(Interface): + """ + Handle WIFI/WLAN interfaces. + """ + + default = { + 'type': 'wifi', + 'phy': 'phy0' + } + definition = { + **Interface.definition, + **{ + 'section': 'wireless', + 'prefixes': ['wlan', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['phy', 'op_mode'] + + def _create(self): + # all interfaces will be added in monitor mode + cmd = 'iw phy {phy} interface add {ifname} type monitor' \ + .format(**self.config) + self._cmd(cmd) + + # wireless interface is administratively down by default + self.set_admin_state('down') + + def _delete(self): + cmd = 'iw dev {ifname} del' \ + .format(**self.config) + self._cmd(cmd) + + @staticmethod + def get_config(): + """ + WiFi interfaces require a configuration when they are added using + iw (type/phy). This static method will provide the configuration + ictionary used by this class. + + Example: + >> conf = WiFiIf().get_config() + """ + config = { + 'phy': 'phy0' + } + return config + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) + + +@Interface.register +class WiFiModemIf(WiFiIf): + definition = { + **WiFiIf.definition, + **{ + 'section': 'wirelessmodem', + 'prefixes': ['wlm', ], + } + } diff --git a/python/vyos/iflag.py b/python/vyos/iflag.py new file mode 100644 index 000000000..7ff8e5623 --- /dev/null +++ b/python/vyos/iflag.py @@ -0,0 +1,38 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from enum import Enum, unique, IntEnum + + +class IFlag(IntEnum): + """ net/if.h interface flags """ + + IFF_UP = 0x1 #: Interface up/down status + IFF_BROADCAST = 0x2 #: Broadcast address valid + IFF_DEBUG = 0x4, #: Debugging + IFF_LOOPBACK = 0x8 #: Is loopback network + IFF_POINTOPOINT = 0x10 #: Is point-to-point link + IFF_NOTRAILERS = 0x20 #: Avoid use of trailers + IFF_RUNNING = 0x40 #: Resources allocated + IFF_NOARP = 0x80 #: No address resolution protocol + IFF_PROMISC = 0x100 #: Promiscuous mode + IFF_ALLMULTI = 0x200 #: Receive all multicast + IFF_MASTER = 0x400 #: Load balancer master + IFF_SLAVE = 0x800 #: Load balancer slave + IFF_MULTICAST = 0x1000 #: Supports multicast + IFF_PORTSEL = 0x2000 #: Media type adjustable + IFF_AUTOMEDIA = 0x4000 #: Automatic media type enabled + IFF_DYNAMIC = 0x8000 #: Is a dial-up device with dynamic address + diff --git a/python/vyos/initialsetup.py b/python/vyos/initialsetup.py new file mode 100644 index 000000000..574e7892d --- /dev/null +++ b/python/vyos/initialsetup.py @@ -0,0 +1,72 @@ +# initialsetup -- functions for setting common values in config file, +# for use in installation and first boot scripts +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import vyos.configtree +import vyos.authutils + +def set_interface_address(config, intf, addr, intf_type="ethernet"): + config.set(["interfaces", intf_type, intf, "address"], value=addr) + config.set_tag(["interfaces", intf_type]) + +def set_host_name(config, hostname): + config.set(["system", "host-name"], value=hostname) + +def set_name_servers(config, servers): + for s in servers: + config.set(["system", "name-server"], replace=False, value=s) + +def set_default_gateway(config, gateway): + config.set(["protocols", "static", "route", "0.0.0.0/0", "next-hop", gateway]) + config.set_tag(["protocols", "static", "route"]) + config.set_tag(["protocols", "static", "route", "0.0.0.0/0", "next-hop"]) + +def set_user_password(config, user, password): + # Make a password hash + hash = vyos.authutils.make_password_hash(password) + + config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value=hash) + config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="") + +def disable_user_password(config, user): + config.set(["system", "login", "user", user, "authentication", "encrypted-password"], value="!") + config.set(["system", "login", "user", user, "authentication", "plaintext-password"], value="") + +def set_user_level(config, user, level): + config.set(["system", "login", "user", user, "level"], value=level) + +def set_user_ssh_key(config, user, key_string): + key = vyos.authutils.split_ssh_public_key(key_string, defaultname=user) + + config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "key"], value=key["data"]) + config.set(["system", "login", "user", user, "authentication", "public-keys", key["name"], "type"], value=key["type"]) + config.set_tag(["system", "login", "user", user, "authentication", "public-keys"]) + +def create_user(config, user, password=None, key=None, level="admin"): + config.set(["system", "login", "user", user]) + config.set_tag(["system", "login", "user", user]) + + if not key and not password: + raise ValueError("Must set at least password or SSH public key") + + if password: + set_user_password(config, user, password) + else: + disable_user_password(config, user) + + if key: + set_user_ssh_key(config, user, key) + + set_user_level(config, user, level) diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py new file mode 100644 index 000000000..cfa75aac6 --- /dev/null +++ b/python/vyos/ioctl.py @@ -0,0 +1,36 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os +import socket +import fcntl +import struct + +SIOCGIFFLAGS = 0x8913 + +def get_terminal_size(): + """ pull the terminal size """ + """ rows,cols = vyos.ioctl.get_terminal_size() """ + columns, rows = os.get_terminal_size(0) + return (rows,columns) + +def get_interface_flags(intf): + """ Pull the SIOCGIFFLAGS """ + nullif = '\0'*256 + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + raw = fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, intf + nullif) + flags, = struct.unpack('H', raw[16:18]) + return flags diff --git a/python/vyos/limericks.py b/python/vyos/limericks.py new file mode 100644 index 000000000..e03ccd32b --- /dev/null +++ b/python/vyos/limericks.py @@ -0,0 +1,72 @@ +# Copyright 2015, 2018 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import random + +limericks = [ + +""" +A programmer who's name was Searle +Once wrote a long program in Perl. +Despite very few quirks +No one got how it works, +Not even the interpreter. +""", + +""" +There was a young lady of Maine +Who set up IPsec VPN. +Problems didn't arise +'til other vendors' device +had to add she to that VPN. +""", + +""" +One day a programmer from York +started his own Vyatta fork. +Though he was a huge geek, +it still took him a week +to get the damn build scripts to work. +""", + +""" +A network admin from Hong Kong +knew MPPE cipher's not strong. +But he was behind NAT, +so he put up we that, +sad network admin from Hong Kong. +""", + +""" +A network admin named Drake +greeted friends with a three-way handshake +and refused to proceed +if they didn't complete it, +that standards-compliant guy Drake. +""", + +""" +A network admin from Nantucket +used hierarchy token buckets. +Bandwidth limits he set +slowed down his net, +users drove him away from Nantucket. +""" + +] + + +def get_random(): + return limericks[random.randint(0, len(limericks) - 1)] diff --git a/python/vyos/logger.py b/python/vyos/logger.py new file mode 100644 index 000000000..f7cc964d5 --- /dev/null +++ b/python/vyos/logger.py @@ -0,0 +1,143 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# A wrapper class around logging to make it easier to use + +# for a syslog logger: +# from vyos.logger import syslog +# syslog.critical('message') + +# for a stderr logger: +# from vyos.logger import stderr +# stderr.critical('message') + +# for a custom logger (syslog and file): +# from vyos.logger import getLogger +# combined = getLogger(__name__, syslog=True, stream=sys.stdout, filename='/tmp/test') +# combined.critical('message') + +import sys +import logging +import logging.handlers as handlers + +TIMED = '%(asctime)s: %(message)s' +SHORT = '%(filename)s: %(message)s' +CLEAR = '%(levelname) %(asctime)s %(filename)s: %(message)s' + +_levels = { + 'CRITICAL': logging.CRITICAL, + 'ERROR': logging.CRITICAL, + 'WARNING': logging.WARNING, + 'INFO': logging.INFO, + 'DEBUG': logging.DEBUG, + 'NOTSET': logging.NOTSET, +} + +# prevent recreation of already created logger +_created = {} + +def getLogger(name=None, **kwargs): + if name in _created: + if len(kwargs) == 0: + return _created[name] + raise ValueError('a logger with the name "{name} already exists') + + logger = logging.getLogger(name) + logger.setLevel(_levels[kwargs.get('level', 'DEBUG')]) + + if 'address' in kwargs or kwargs.get('syslog', False): + logger.addHandler(_syslog(**kwargs)) + if 'stream' in kwargs: + logger.addHandler(_stream(**kwargs)) + if 'filename' in kwargs: + logger.addHandler(_file(**kwargs)) + + _created[name] = logger + return logger + + +def _syslog(**kwargs): + formating = kwargs.get('format', SHORT) + handler = handlers.SysLogHandler( + address=kwargs.get('address', '/dev/log'), + facility=kwargs.get('facility', 'syslog'), + ) + handler.setFormatter(logging.Formatter(formating)) + return handler + + +def _stream(**kwargs): + formating = kwargs.get('format', CLEAR) + handler = logging.StreamHandler( + stream=kwargs.get('stream', sys.stderr), + ) + handler.setFormatter(logging.Formatter(formating)) + return handler + + +def _file(**kwargs): + formating = kwargs.get('format', CLEAR) + handler = handlers.RotatingFileHandler( + filename=kwargs.get('filename', 1048576), + maxBytes=kwargs.get('maxBytes', 1048576), + backupCount=kwargs.get('backupCount', 3), + ) + handler.setFormatter(logging.Formatter(formating)) + return handler + + +# exported pre-built logger, please keep in mind that the names +# must be unique otherwise the logger are shared + +# a logger for stderr +stderr = getLogger( + 'VyOS Syslog', + format=SHORT, + stream=sys.stderr, + address='/dev/log' +) + +# a logger to syslog +syslog = getLogger( + 'VyOS StdErr', + format='%(message)s', + address='/dev/log' +) + + +# testing +if __name__ == '__main__': + # from vyos.logger import getLogger + formating = '%(asctime)s (%(filename)s) %(levelname)s: %(message)s' + + # syslog logger + # syslog=True if no 'address' field is provided + syslog = getLogger(__name__ + '.1', syslog=True, format=formating) + syslog.info('syslog test') + + # steam logger + stream = getLogger(__name__ + '.2', stream=sys.stdout, level='ERROR') + stream.info('steam test') + + # file logger + filelog = getLogger(__name__ + '.3', filename='/tmp/test') + filelog.info('file test') + + # create a combined logger + getLogger('VyOS', syslog=True, stream=sys.stdout, filename='/tmp/test') + + # recover the created logger from name + combined = getLogger('VyOS') + combined.info('combined test') diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py new file mode 100644 index 000000000..9a5fdef2f --- /dev/null +++ b/python/vyos/migrator.py @@ -0,0 +1,220 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os +import subprocess +import vyos.version +import vyos.defaults +import vyos.systemversions as systemversions +import vyos.formatversions as formatversions + +class MigratorError(Exception): + pass + +class Migrator(object): + def __init__(self, config_file, force=False, set_vintage='vyos'): + self._config_file = config_file + self._force = force + self._set_vintage = set_vintage + self._config_file_vintage = None + self._log_file = None + self._changed = False + + def read_config_file_versions(self): + """ + Get component versions from config file footer and set vintage; + return empty dictionary if config string is missing. + """ + cfg_file = self._config_file + component_versions = {} + + cfg_versions = formatversions.read_vyatta_versions(cfg_file) + + if cfg_versions: + self._config_file_vintage = 'vyatta' + component_versions = cfg_versions + + cfg_versions = formatversions.read_vyos_versions(cfg_file) + + if cfg_versions: + self._config_file_vintage = 'vyos' + component_versions = cfg_versions + + return component_versions + + def update_vintage(self): + old_vintage = self._config_file_vintage + + if self._set_vintage: + self._config_file_vintage = self._set_vintage + + if self._config_file_vintage not in ['vyatta', 'vyos']: + raise MigratorError("Unknown vintage.") + + if self._config_file_vintage == old_vintage: + return False + else: + return True + + def open_log_file(self): + """ + Open log file for migration, catching any error. + Note that, on boot, migration takes place before the canonical log + directory is created, hence write to the config file directory. + """ + self._log_file = os.path.join(vyos.defaults.directories['config'], + 'vyos-migrate.log') + # on creation, allow write permission for cfg_group; + # restore original umask on exit + mask = os.umask(0o113) + try: + log = open('{0}'.format(self._log_file), 'w') + log.write("List of executed migration scripts:\n") + except Exception as e: + os.umask(mask) + print("Logging error: {0}".format(e)) + return None + + os.umask(mask) + return log + + def run_migration_scripts(self, config_file_versions, system_versions): + """ + Run migration scripts iteratively, until config file version equals + system component version. + """ + log = self.open_log_file() + + cfg_versions = config_file_versions + sys_versions = system_versions + + sys_keys = list(sys_versions.keys()) + sys_keys.sort() + + rev_versions = {} + + for key in sys_keys: + sys_ver = sys_versions[key] + if key in cfg_versions: + cfg_ver = cfg_versions[key] + else: + cfg_ver = 0 + + migrate_script_dir = os.path.join( + vyos.defaults.directories['migrate'], key) + + while cfg_ver < sys_ver: + next_ver = cfg_ver + 1 + + migrate_script = os.path.join(migrate_script_dir, + '{}-to-{}'.format(cfg_ver, next_ver)) + + try: + subprocess.check_call([migrate_script, + self._config_file]) + except FileNotFoundError: + pass + except Exception as err: + print("\nMigration script error: {0}: {1}." + "".format(migrate_script, err)) + sys.exit(1) + + if log: + try: + log.write('{0}\n'.format(migrate_script)) + except Exception as e: + print("Error writing log: {0}".format(e)) + + cfg_ver = next_ver + + rev_versions[key] = cfg_ver + + if log: + log.close() + + return rev_versions + + def write_config_file_versions(self, cfg_versions): + """ + Write new versions string. + """ + versions_string = formatversions.format_versions_string(cfg_versions) + + os_version_string = vyos.version.get_version() + + if self._config_file_vintage == 'vyatta': + formatversions.write_vyatta_versions_foot(self._config_file, + versions_string, + os_version_string) + + if self._config_file_vintage == 'vyos': + formatversions.write_vyos_versions_foot(self._config_file, + versions_string, + os_version_string) + + def run(self): + """ + Gather component versions from config file and system. + Run migration scripts. + Update vintage ('vyatta' or 'vyos'), if needed. + If changed, remove old versions string from config file, and + write new versions string. + """ + cfg_file = self._config_file + + cfg_versions = self.read_config_file_versions() + if self._force: + # This will force calling all migration scripts: + cfg_versions = {} + + sys_versions = systemversions.get_system_versions() + + rev_versions = self.run_migration_scripts(cfg_versions, sys_versions) + + if rev_versions != cfg_versions: + self._changed = True + + if self.update_vintage(): + self._changed = True + + if not self._changed: + return + + formatversions.remove_versions(cfg_file) + + self.write_config_file_versions(rev_versions) + + def config_changed(self): + return self._changed + +class VirtualMigrator(Migrator): + def run(self): + cfg_file = self._config_file + + cfg_versions = self.read_config_file_versions() + if not cfg_versions: + return + + if self.update_vintage(): + self._changed = True + + if not self._changed: + return + + formatversions.remove_versions(cfg_file) + + self.write_config_file_versions(cfg_versions) + diff --git a/python/vyos/remote.py b/python/vyos/remote.py new file mode 100644 index 000000000..3f46d979b --- /dev/null +++ b/python/vyos/remote.py @@ -0,0 +1,143 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os +import re +import fileinput + +from vyos.util import cmd +from vyos.util import DEVNULL + + +def check_and_add_host_key(host_name): + """ + Filter host keys and prompt for adding key to known_hosts file, if + needed. + """ + known_hosts = '{}/.ssh/known_hosts'.format(os.getenv('HOME')) + if not os.path.exists(known_hosts): + mode = 0o600 + os.mknod(known_hosts, 0o600) + + keyscan_cmd = 'ssh-keyscan -t rsa {}'.format(host_name) + + try: + host_key = cmd(keyscan_cmd, stderr=DEVNULL) + except OSError: + sys.exit("Can not get RSA host key") + + # libssh2 (jessie; stretch) does not recognize ec host keys, and curl + # will fail with error 51 if present in known_hosts file; limit to rsa. + usable_keys = False + offending_keys = [] + for line in fileinput.input(known_hosts, inplace=True): + if host_name in line and 'ssh-rsa' in line: + if line.split()[-1] != host_key.split()[-1]: + offending_keys.append(line) + continue + else: + usable_keys = True + if host_name in line and not 'ssh-rsa' in line: + continue + + sys.stdout.write(line) + + if usable_keys: + return + + if offending_keys: + print("Host key has changed!") + print("If you trust the host key fingerprint below, continue.") + + fingerprint_cmd = 'ssh-keygen -lf /dev/stdin' + try: + fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, input=host_key) + except OSError: + sys.exit("Can not get RSA host key fingerprint.") + + print("RSA host key fingerprint is {}".format(fingerprint.split()[1])) + response = input("Do you trust this host? [y]/n ") + + if not response or response == 'y': + with open(known_hosts, 'a+') as f: + print("Adding {} to the list of known" + " hosts.".format(host_name)) + f.write(host_key) + else: + sys.exit("Host not trusted") + +def get_remote_config(remote_file): + """ Invoke curl to download remote (config) file. + + Args: + remote file URI: + scp://<user>[:<passwd>]@<host>/<file> + sftp://<user>[:<passwd>]@<host>/<file> + http://<host>/<file> + https://<host>/<file> + ftp://<user>[:<passwd>]@<host>/<file> + tftp://<host>/<file> + """ + request = dict.fromkeys(['protocol', 'user', 'host', 'file']) + protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + or_protocols = '|'.join(protocols) + + request_match = re.match(r'(' + or_protocols + r')://(.*?)(/.*)', + remote_file) + if request_match: + (request['protocol'], request['host'], + request['file']) = request_match.groups() + else: + print("Malformed URI") + sys.exit(1) + + user_match = re.search(r'(.*)@(.*)', request['host']) + if user_match: + request['user'] = user_match.groups()[0] + request['host'] = user_match.groups()[1] + + remote_file = '{0}://{1}{2}'.format(request['protocol'], request['host'], request['file']) + + if request['protocol'] in ('scp', 'sftp'): + check_and_add_host_key(request['host']) + + redirect_opt = '' + + if request['protocol'] in ('http', 'https'): + redirect_opt = '-L' + # Try header first, and look for 'OK' or 'Moved' codes: + curl_cmd = 'curl {0} -q -I {1}'.format(redirect_opt, remote_file) + try: + curl_output = cmd(curl_cmd) + except OSError: + sys.exit(1) + + return_vals = re.findall(r'^HTTP\/\d+\.?\d\s+(\d+)\s+(.*)$', + curl_output, re.MULTILINE) + for val in return_vals: + if int(val[0]) not in [200, 301, 302]: + print('HTTP error: {0} {1}'.format(*val)) + sys.exit(1) + + if request['user']: + curl_cmd = 'curl -# -u {0} {1}'.format(request['user'], remote_file) + else: + curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file) + + try: + return cmd(curl_cmd, stderr=None) + except OSError: + return None diff --git a/python/vyos/snmpv3_hashgen.py b/python/vyos/snmpv3_hashgen.py new file mode 100644 index 000000000..324c3274d --- /dev/null +++ b/python/vyos/snmpv3_hashgen.py @@ -0,0 +1,50 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +# Documentation / Inspiration +# - https://tools.ietf.org/html/rfc3414#appendix-A.3 +# - https://github.com/TheMysteriousX/SNMPv3-Hash-Generator + +key_length = 1048576 + +def random(l): + # os.urandom(8) returns 8 bytes of random data + import os + from binascii import hexlify + return hexlify(os.urandom(l)).decode('utf-8') + +def expand(s, l): + """ repead input string (s) as long as we reach the desired length in bytes """ + from itertools import repeat + reps = l // len(s) + 1 # approximation; worst case: overrun = l + len(s) + return ''.join(list(repeat(s, reps)))[:l].encode('utf-8') + +def plaintext_to_md5(passphrase, engine): + """ Convert input plaintext passphrase to MD5 hashed version usable by net-snmp """ + from hashlib import md5 + tmp = expand(passphrase, key_length) + hash = md5(tmp).digest() + engine = bytearray.fromhex(engine) + out = b''.join([hash, engine, hash]) + return md5(out).digest().hex() + +def plaintext_to_sha1(passphrase, engine): + """ Convert input plaintext passphrase to SHA1hashed version usable by net-snmp """ + from hashlib import sha1 + tmp = expand(passphrase, key_length) + hash = sha1(tmp).digest() + engine = bytearray.fromhex(engine) + out = b''.join([hash, engine, hash]) + return sha1(out).digest().hex() diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py new file mode 100644 index 000000000..5c4deca29 --- /dev/null +++ b/python/vyos/systemversions.py @@ -0,0 +1,63 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import sys +import json + +import vyos.defaults + +def get_system_versions(): + """ + Get component versions from running system: read vyatta directory + structure for versions, then read vyos JSON file. It is a critical + error if either migration directory or JSON file is unreadable. + """ + system_versions = {} + + try: + version_info = os.listdir(vyos.defaults.directories['current']) + except OSError as err: + print("OS error: {}".format(err)) + sys.exit(1) + + for info in version_info: + if re.match(r'[\w,-]+@\d+', info): + pair = info.split('@') + system_versions[pair[0]] = int(pair[1]) + + version_dict = {} + path = vyos.defaults.version_file + + if os.path.isfile(path): + with open(path, 'r') as f: + try: + version_dict = json.load(f) + except ValueError as err: + print(f"\nValue error in {path}: {err}") + sys.exit(1) + + for k, v in version_dict.items(): + if not isinstance(v, int): + print(f"\nType error in {path}; expecting Dict[str, int]") + sys.exit(1) + existing = system_versions.get(k) + if existing is None: + system_versions[k] = v + elif v > existing: + system_versions[k] = v + + return system_versions diff --git a/python/vyos/template.py b/python/vyos/template.py new file mode 100644 index 000000000..c88ab04a0 --- /dev/null +++ b/python/vyos/template.py @@ -0,0 +1,143 @@ +# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import functools +import os + +from ipaddress import ip_network +from jinja2 import Environment +from jinja2 import FileSystemLoader +from vyos.defaults import directories +from vyos.util import chmod, chown, makedir + + +# Holds template filters registered via register_filter() +_FILTERS = {} + + +# reuse Environments with identical trim_blocks setting to improve performance +@functools.lru_cache(maxsize=2) +def _get_environment(trim_blocks): + env = Environment( + # Don't check if template files were modified upon re-rendering + auto_reload=False, + # Cache up to this number of templates for quick re-rendering + cache_size=100, + loader=FileSystemLoader(directories["templates"]), + trim_blocks=trim_blocks, + ) + env.filters.update(_FILTERS) + return env + + +def register_filter(name, func=None): + """Register a function to be available as filter in templates under given name. + + It can also be used as a decorator, see below in this module for examples. + + :raise RuntimeError: + when trying to register a filter after a template has been rendered already + :raise ValueError: when trying to register a name which was taken already + """ + if func is None: + return functools.partial(register_filter, name) + if _get_environment.cache_info().currsize: + raise RuntimeError( + "Filters can only be registered before rendering the first template" + ) + if name in _FILTERS: + raise ValueError(f"A filter with name {name!r} was registered already") + _FILTERS[name] = func + return func + + +def render_to_string(template, content, trim_blocks=False, formater=None): + """Render a template from the template directory, raise on any errors. + + :param template: the path to the template relative to the template folder + :param content: the dictionary of variables to put into rendering context + :param trim_blocks: controls the trim_blocks jinja2 feature + :param formater: + if given, it has to be a callable the rendered string is passed through + + The parsed template files are cached, so rendering the same file multiple times + does not cause as too much overhead. + If used everywhere, it could be changed to load the template from Python + environment variables from an importable Python module generated when the Debian + package is build (recovering the load time and overhead caused by having the + file out of the code). + """ + template = _get_environment(bool(trim_blocks)).get_template(template) + rendered = template.render(content) + if formater is not None: + rendered = formater(rendered) + return rendered + + +def render( + destination, + template, + content, + trim_blocks=False, + formater=None, + permission=None, + user=None, + group=None, +): + """Render a template from the template directory to a file, raise on any errors. + + :param destination: path to the file to save the rendered template in + :param permission: permission bitmask to set for the output file + :param user: user to own the output file + :param group: group to own the output file + + All other parameters are as for :func:`render_to_string`. + """ + # Create the directory if it does not exist + folder = os.path.dirname(destination) + makedir(folder, user, group) + + # As we are opening the file with 'w', we are performing the rendering before + # calling open() to not accidentally erase the file if rendering fails + rendered = render_to_string(template, content, trim_blocks, formater) + + # Write to file + with open(destination, "w") as file: + chmod(file.fileno(), permission) + chown(file.fileno(), user, group) + file.write(rendered) + + +################################## +# Custom template filters follow # +################################## + + +@register_filter("address_from_cidr") +def vyos_address_from_cidr(text): + """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address". + Example: + 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8:: + """ + return ip_network(text).network_address + + +@register_filter("netmask_from_cidr") +def vyos_netmask_from_cidr(text): + """ Take an IPv4/IPv6 CIDR prefix and convert the prefix length to a "subnet mask". + Example: + 192.0.2.0/24 -> 255.255.255.0, 2001:db8::/48 -> ffff:ffff:ffff:: + """ + return ip_network(text).netmask diff --git a/python/vyos/util.py b/python/vyos/util.py new file mode 100644 index 000000000..84aa16791 --- /dev/null +++ b/python/vyos/util.py @@ -0,0 +1,688 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import sys + +# +# NOTE: Do not import full classes here, move your import to the function +# where it is used so it is as local as possible to the execution +# + + +def _need_sudo(command): + return os.path.basename(command.split()[0]) in ('systemctl', ) + + +def _add_sudo(command): + if _need_sudo(command): + return 'sudo ' + command + return command + + +from subprocess import Popen +from subprocess import PIPE +from subprocess import STDOUT +from subprocess import DEVNULL + + +def popen(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True): + """ + popen is a wrapper helper aound subprocess.Popen + with it default setting it will return a tuple (out, err) + out: the output of the program run + err: the error code returned by the program + + it can be affected by the following flags: + shell: do not try to auto-detect if a shell is required + for example if a pipe (|) or redirection (>, >>) is used + input: data to sent to the child process via STDIN + the data should be bytes but string will be converted + timeout: time after which the command will be considered to have failed + env: mapping that defines the environment variables for the new process + stdout: define how the output of the program should be handled + - PIPE (default), sends stdout to the output + - DEVNULL, discard the output + stderr: define how the output of the program should be handled + - None (default), send/merge the data to/with stderr + - PIPE, popen will append it to output + - STDOUT, send the data to be merged with stdout + - DEVNULL, discard the output + decode: specify the expected text encoding (utf-8, ascii, ...) + the default is explicitely utf-8 which is python's own default + + usage: + get both stdout and stderr: popen('command', stdout=PIPE, stderr=STDOUT) + discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE) + """ + + # airbag must be left as an import in the function as otherwise we have a + # a circual import dependency + from vyos import debug + from vyos import airbag + + # log if the flag is set, otherwise log if command is set + if not debug.enabled(flag): + flag = 'command' + + if autosudo: + command = _add_sudo(command) + + cmd_msg = f"cmd '{command}'" + debug.message(cmd_msg, flag) + + use_shell = shell + stdin = None + if shell is None: + use_shell = False + if ' ' in command: + use_shell = True + if env: + use_shell = True + + if input: + stdin = PIPE + input = input.encode() if type(input) is str else input + + p = Popen( + command, + stdin=stdin, stdout=stdout, stderr=stderr, + env=env, shell=use_shell, + ) + + pipe = p.communicate(input, timeout) + + pipe_out = b'' + if stdout == PIPE: + pipe_out = pipe[0] + + pipe_err = b'' + if stderr == PIPE: + pipe_err = pipe[1] + + str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip() + str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip() + + out_msg = f"returned (out):\n{str_out}" + if str_out: + debug.message(out_msg, flag) + + if str_err: + err_msg = f"returned (err):\n{str_err}" + # this message will also be send to syslog via airbag + debug.message(err_msg, flag, destination=sys.stderr) + + # should something go wrong, report this too via airbag + airbag.noteworthy(cmd_msg) + airbag.noteworthy(out_msg) + airbag.noteworthy(err_msg) + + return str_out, p.returncode + + +def run(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=DEVNULL, stderr=PIPE, decode='utf-8', autosudo=True): + """ + A wrapper around popen, which discard the stdout and + will return the error code of a command + """ + _, code = popen( + command, flag, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + decode=decode, + ) + return code + + +def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True, + raising=None, message='', expect=[0]): + """ + A wrapper around popen, which returns the stdout and + will raise the error code of a command + + raising: specify which call should be used when raising + the class should only require a string as parameter + (default is OSError) with the error code + expect: a list of error codes to consider as normal + """ + decoded, code = popen( + command, flag, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + decode=decode, + ) + if code not in expect: + feedback = message + '\n' if message else '' + feedback += f'failed to run command: {command}\n' + feedback += f'returned: {decoded}\n' + feedback += f'exit code: {code}' + if raising is None: + # error code can be recovered with .errno + raise OSError(code, feedback) + else: + raise raising(feedback) + return decoded + + +def call(command, flag='', shell=None, input=None, timeout=None, env=None, + stdout=PIPE, stderr=PIPE, decode='utf-8', autosudo=True): + """ + A wrapper around popen, which print the stdout and + will return the error code of a command + """ + out, code = popen( + command, flag, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + decode=decode, + ) + if out: + print(out) + return code + + +def read_file(fname, defaultonfailure=None): + """ + read the content of a file, stripping any end characters (space, newlines) + should defaultonfailure be not None, it is returned on failure to read + """ + try: + """ Read a file to string """ + with open(fname, 'r') as f: + data = f.read().strip() + return data + except Exception as e: + if defaultonfailure is not None: + return defaultonfailure + raise e + + +def read_json(fname, defaultonfailure=None): + """ + read and json decode the content of a file + should defaultonfailure be not None, it is returned on failure to read + """ + import json + try: + with open(fname, 'r') as f: + data = json.load(f) + return data + except Exception as e: + if defaultonfailure is not None: + return defaultonfailure + raise e + + +def chown(path, user, group): + """ change file/directory owner """ + from pwd import getpwnam + from grp import getgrnam + + if user is None or group is None: + return False + + # path may also be an open file descriptor + if not isinstance(path, int) and not os.path.exists(path): + return False + + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(path, uid, gid) + return True + + +def chmod(path, bitmask): + # path may also be an open file descriptor + if not isinstance(path, int) and not os.path.exists(path): + return + if bitmask is None: + return + os.chmod(path, bitmask) + + +def chmod_600(path): + """ make file only read/writable by owner """ + from stat import S_IRUSR, S_IWUSR + + bitmask = S_IRUSR | S_IWUSR + chmod(path, bitmask) + + +def chmod_750(path): + """ make file/directory only executable to user and group """ + from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP + + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP + chmod(path, bitmask) + + +def chmod_755(path): + """ make file executable by all """ + from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH + + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \ + S_IROTH | S_IXOTH + chmod(path, bitmask) + + +def makedir(path, user=None, group=None): + if os.path.exists(path): + return + os.mkdir(path) + chown(path, user, group) + + +def colon_separated_to_dict(data_string, uniquekeys=False): + """ Converts a string containing newline-separated entries + of colon-separated key-value pairs into a dict. + + Such files are common in Linux /proc filesystem + + Args: + data_string (str): data string + uniquekeys (bool): whether to insist that keys are unique or not + + Returns: dict + + Raises: + ValueError: if uniquekeys=True and the data string has + duplicate keys. + + Note: + If uniquekeys=True, then dict entries are always strings, + otherwise they are always lists of strings. + """ + import re + key_value_re = re.compile('([^:]+)\s*\:\s*(.*)') + + data_raw = re.split('\n', data_string) + + data = {} + + for l in data_raw: + l = l.strip() + if l: + match = re.match(key_value_re, l) + if match: + key = match.groups()[0].strip() + value = match.groups()[1].strip() + if key in data.keys(): + if uniquekeys: + raise ValueError("Data string has duplicate keys: {0}".format(key)) + else: + data[key].append(value) + else: + if uniquekeys: + data[key] = value + else: + data[key] = [value] + else: + pass + + return data + +def mangle_dict_keys(data, regex, replacement): + """ Mangles dict keys according to a regex and replacement character. + Some libraries like Jinja2 do not like certain characters in dict keys. + This function can be used for replacing all offending characters + with something acceptable. + + Args: + data (dict): Original dict to mangle + + Returns: dict + """ + new_dict = {} + for key in data.keys(): + new_key = re.sub(regex, replacement, key) + + value = data[key] + if isinstance(value, dict): + new_dict[new_key] = mangle_dict_keys(value, regex, replacement) + else: + new_dict[new_key] = value + + return new_dict + +def _get_sub_dict(d, lpath): + k = lpath[0] + if k not in d.keys(): + return {} + c = {k: d[k]} + lpath = lpath[1:] + if not lpath: + return c + elif not isinstance(c[k], dict): + return {} + return _get_sub_dict(c[k], lpath) + +def get_sub_dict(source, lpath, get_first_key=False): + """ Returns the sub-dict of a nested dict, defined by path of keys. + + Args: + source (dict): Source dict to extract from + lpath (list[str]): sequence of keys + + Returns: source, if lpath is empty, else + {key : source[..]..[key]} for key the last element of lpath, if exists + {} otherwise + """ + if not isinstance(source, dict): + raise TypeError("source must be of type dict") + if not isinstance(lpath, list): + raise TypeError("path must be of type list") + if not lpath: + return source + + ret = _get_sub_dict(source, lpath) + + if get_first_key and lpath and ret: + tmp = next(iter(ret.values())) + if not isinstance(tmp, dict): + raise TypeError("Data under node is not of type dict") + ret = tmp + + return ret + +def process_running(pid_file): + """ Checks if a process with PID in pid_file is running """ + from psutil import pid_exists + if not os.path.isfile(pid_file): + return False + with open(pid_file, 'r') as f: + pid = f.read().strip() + return pid_exists(int(pid)) + + +def process_named_running(name): + """ Checks if process with given name is running and returns its PID. + If Process is not running, return None + """ + from psutil import process_iter + for p in process_iter(): + if name in p.name(): + return p.pid + return None + + +def seconds_to_human(s, separator=""): + """ Converts number of seconds passed to a human-readable + interval such as 1w4d18h35m59s + """ + s = int(s) + + week = 60 * 60 * 24 * 7 + day = 60 * 60 * 24 + hour = 60 * 60 + + remainder = 0 + result = "" + + weeks = s // week + if weeks > 0: + result = "{0}w".format(weeks) + s = s % week + + days = s // day + if days > 0: + result = "{0}{1}{2}d".format(result, separator, days) + s = s % day + + hours = s // hour + if hours > 0: + result = "{0}{1}{2}h".format(result, separator, hours) + s = s % hour + + minutes = s // 60 + if minutes > 0: + result = "{0}{1}{2}m".format(result, separator, minutes) + s = s % 60 + + seconds = s + if seconds > 0: + result = "{0}{1}{2}s".format(result, separator, seconds) + + return result + + +def get_cfg_group_id(): + from grp import getgrnam + from vyos.defaults import cfg_group + + group_data = getgrnam(cfg_group) + return group_data.gr_gid + + +def file_is_persistent(path): + import re + location = r'^(/config|/opt/vyatta/etc/config)' + absolute = os.path.abspath(os.path.dirname(path)) + return re.match(location,absolute) + + +def commit_in_progress(): + """ Not to be used in normal op mode scripts! """ + + # The CStore backend locks the config by opening a file + # The file is not removed after commit, so just checking + # if it exists is insufficient, we need to know if it's open by anyone + + # There are two ways to check if any other process keeps a file open. + # The first one is to try opening it and see if the OS objects. + # That's faster but prone to race conditions and can be intrusive. + # The other one is to actually check if any process keeps it open. + # It's non-intrusive but needs root permissions, else you can't check + # processes of other users. + # + # Since this will be used in scripts that modify the config outside of the CLI + # framework, those knowingly have root permissions. + # For everything else, we add a safeguard. + from psutil import process_iter, NoSuchProcess + from vyos.defaults import commit_lock + + idu = cmd('/usr/bin/id -u') + if idu != '0': + raise OSError("This functions needs root permissions to return correct results") + + for proc in process_iter(): + try: + files = proc.open_files() + if files: + for f in files: + if f.path == commit_lock: + return True + except NoSuchProcess as err: + # Process died before we could examine it + pass + # Default case + return False + + +def wait_for_commit_lock(): + """ Not to be used in normal op mode scripts! """ + from time import sleep + # Very synchronous approach to multiprocessing + while commit_in_progress(): + sleep(1) + + +def ask_yes_no(question, default=False) -> bool: + """Ask a yes/no question via input() and return their answer.""" + from sys import stdout + default_msg = "[Y/n]" if default else "[y/N]" + while True: + stdout.write("%s %s " % (question, default_msg)) + c = input().lower() + if c == '': + return default + elif c in ("y", "ye", "yes"): + return True + elif c in ("n", "no"): + return False + else: + stdout.write("Please respond with yes/y or no/n\n") + + +def is_admin() -> bool: + """Look if current user is in sudo group""" + from getpass import getuser + from grp import getgrnam + current_user = getuser() + (_, _, _, admin_group_members) = getgrnam('sudo') + return current_user in admin_group_members + + +def mac2eui64(mac, prefix=None): + """ + Convert a MAC address to a EUI64 address or, with prefix provided, a full + IPv6 address. + Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3 + """ + import re + from ipaddress import ip_network + # http://tools.ietf.org/html/rfc4291#section-2.5.1 + eui64 = re.sub(r'[.:-]', '', mac).lower() + eui64 = eui64[0:6] + 'fffe' + eui64[6:] + eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:] + + if prefix is None: + return ':'.join(re.findall(r'.{4}', eui64)) + else: + try: + net = ip_network(prefix, strict=False) + euil = int('0x{0}'.format(eui64), 16) + return str(net[euil]) + except: # pylint: disable=bare-except + return + +def get_half_cpus(): + """ return 1/2 of the numbers of available CPUs """ + cpu = os.cpu_count() + if cpu > 1: + cpu /= 2 + return int(cpu) + +def ifname_from_config(conf): + """ + Gets interface name with VLANs from current config level. + Level must be at the interface whose name we want. + + Example: + >>> from vyos.util import ifname_from_config + >>> from vyos.config import Config + >>> conf = Config() + >>> conf.set_level('interfaces ethernet eth0 vif-s 1 vif-c 2') + >>> ifname_from_config(conf) + 'eth0.1.2' + """ + level = conf.get_level() + + # vlans + if level[-2] == 'vif' or level[-2] == 'vif-s': + return level[-3] + '.' + level[-1] + if level[-2] == 'vif-c': + return level[-5] + '.' + level[-3] + '.' + level[-1] + + # no vlans + return level[-1] + +def get_bridge_member_config(conf, br, intf): + """ + Gets bridge port (member) configuration + + Arguments: + conf: Config + br: bridge name + intf: interface name + + Returns: + dict with the configuration + False if bridge or bridge port doesn't exist + """ + old_level = conf.get_level() + conf.set_level([]) + + bridge = f'interfaces bridge {br}' + member = f'{bridge} member interface {intf}' + if not ( conf.exists(bridge) and conf.exists(member) ): + return False + + # default bridge port configuration + # cost and priority initialized with linux defaults + # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} + # after adding interface to bridge after reboot + memberconf = { + 'cost': 100, + 'priority': 32, + 'arp_cache_tmo': 30, + 'disable_link_detect': 1, + } + + if conf.exists(f'{member} cost'): + memberconf['cost'] = int(conf.return_value(f'{member} cost')) + + if conf.exists(f'{member} priority'): + memberconf['priority'] = int(conf.return_value(f'{member} priority')) + + if conf.exists(f'{bridge} ip arp-cache-timeout'): + memberconf['arp_cache_tmo'] = int(conf.return_value(f'{bridge} ip arp-cache-timeout')) + + if conf.exists(f'{bridge} disable-link-detect'): + memberconf['disable_link_detect'] = 2 + + conf.set_level(old_level) + return memberconf + +def check_kmod(k_mod): + """ Common utility function to load required kernel modules on demand """ + if isinstance(k_mod, str): + k_mod = k_mod.split() + for module in k_mod: + if not os.path.exists(f'/sys/module/{module}'): + if call(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') + +def find_device_file(device): + """ Recurively search /dev for the given device file and return its full path. + If no device file was found 'None' is returned """ + from fnmatch import fnmatch + + for root, dirs, files in os.walk('/dev'): + for basename in files: + if fnmatch(basename, device): + return os.path.join(root, basename) + + return None + +def vyos_dict_search(path, dict): + """ Traverse Python dictionary (dict) delimited by dot (.). + Return value of key if found, None otherwise. + + This is faster implementation then jmespath.search('foo.bar', dict)""" + parts = path.split('.') + inside = parts[:-1] + if not inside: + return dict[path] + c = dict + for p in parts[:-1]: + c = c.get(p, {}) + return c.get(parts[-1], None) diff --git a/python/vyos/validate.py b/python/vyos/validate.py new file mode 100644 index 000000000..ceeb6888a --- /dev/null +++ b/python/vyos/validate.py @@ -0,0 +1,331 @@ +# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import json +import socket +import netifaces +import ipaddress + +from vyos.util import cmd +from vyos import xml + +# Important note when you are adding new validation functions: +# +# The Control class will analyse the signature of the function in this file +# and will build the parameters to be passed to it. +# +# The parameter names "ifname" and "self" will get the Interface name and class +# parameters with default will be left unset +# all other paramters will receive the value to check + + +def is_ip(addr): + """ + Check addr if it is an IPv4 or IPv6 address + """ + return is_ipv4(addr) or is_ipv6(addr) + +def is_ipv4(addr): + """ + Check addr if it is an IPv4 address/network. Returns True/False + """ + + # With the below statement we can check for IPv4 networks and host + # addresses at the same time + try: + if ipaddress.ip_address(addr.split(r'/')[0]).version == 4: + return True + except: + pass + + return False + +def is_ipv6(addr): + """ + Check addr if it is an IPv6 address/network. Returns True/False + """ + + # With the below statement we can check for IPv4 networks and host + # addresses at the same time + try: + if ipaddress.ip_network(addr.split(r'/')[0]).version == 6: + return True + except: + pass + + return False + +def is_ipv6_link_local(addr): + """ + Check addr if it is an IPv6 link-local address/network. Returns True/False + """ + + addr = addr.split('%')[0] + if is_ipv6(addr): + if ipaddress.IPv6Address(addr).is_link_local: + return True + + return False + +def _are_same_ip(one, two): + # compare the binary representation of the IP + f_one = socket.AF_INET if is_ipv4(one) else socket.AF_INET6 + s_two = socket.AF_INET if is_ipv4(two) else socket.AF_INET6 + return socket.inet_pton(f_one, one) == socket.inet_pton(f_one, two) + +def is_intf_addr_assigned(intf, addr): + if '/' in addr: + ip,mask = addr.split('/') + return _is_intf_addr_assigned(intf, ip, mask) + return _is_intf_addr_assigned(intf, addr) + +def _is_intf_addr_assigned(intf, address, netmask=''): + """ + Verify if the given IPv4/IPv6 address is assigned to specific interface. + It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR + address 192.0.2.1/24. + """ + + # check if the requested address type is configured at all + # { + # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], + # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}], + # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}] + # } + try: + ifaces = netifaces.ifaddresses(intf) + except ValueError as e: + print(e) + return False + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET if is_ipv4(address) else netifaces.AF_INET6 + + # Check every IP address on this interface for a match + for ip in ifaces.get(addr_type,[]): + # ip can have the interface name in the 'addr' field, we need to remove it + # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'} + ip_addr = ip['addr'].split('%')[0] + + if not _are_same_ip(address, ip_addr): + continue + + # we do not have a netmask to compare against, they are the same + if netmask == '': + return True + + prefixlen = '' + if is_ipv4(ip_addr): + prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')]) + else: + prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split(':') if _]) + + if str(prefixlen) == netmask: + return True + + return False + +def is_addr_assigned(addr): + """ + Verify if the given IPv4/IPv6 address is assigned to any interface + """ + + for intf in netifaces.interfaces(): + tmp = is_intf_addr_assigned(intf, addr) + if tmp == True: + return True + + return False + +def is_loopback_addr(addr): + """ + Check if supplied IPv4/IPv6 address is a loopback address + """ + return ipaddress.ip_address(addr).is_loopback + +def is_subnet_connected(subnet, primary=False): + """ + Verify is the given IPv4/IPv6 subnet is connected to any interface on this + system. + + primary check if the subnet is reachable via the primary IP address of this + interface, or in other words has a broadcast address configured. ISC DHCP + for instance will complain if it should listen on non broadcast interfaces. + + Return True/False + """ + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET + if is_ipv6(subnet): + addr_type = netifaces.AF_INET6 + + for interface in netifaces.interfaces(): + # check if the requested address type is configured at all + if addr_type not in netifaces.ifaddresses(interface).keys(): + continue + + # An interface can have multiple addresses, but some software components + # only support the primary address :( + if primary: + ip = netifaces.ifaddresses(interface)[addr_type][0]['addr'] + if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet): + return True + else: + # Check every assigned IP address if it is connected to the subnet + # in question + for ip in netifaces.ifaddresses(interface)[addr_type]: + # remove interface extension (e.g. %eth0) that gets thrown on the end of _some_ addrs + addr = ip['addr'].split('%')[0] + if ipaddress.ip_address(addr) in ipaddress.ip_network(subnet): + return True + + return False + + +def assert_boolean(b): + if int(b) not in (0, 1): + raise ValueError(f'Value {b} out of range') + + +def assert_range(value, lower=0, count=3): + if int(value) not in range(lower,lower+count): + raise ValueError("Value out of range") + + +def assert_list(s, l): + if s not in l: + o = ' or '.join([f'"{n}"' for n in l]) + raise ValueError(f'state must be {o}, got {s}') + + +def assert_number(n): + if not str(n).isnumeric(): + raise ValueError(f'{n} must be a number') + + +def assert_positive(n, smaller=0): + assert_number(n) + if int(n) < smaller: + raise ValueError(f'{n} is smaller than {smaller}') + + +def assert_mtu(mtu, ifname): + assert_number(mtu) + + out = cmd(f'ip -j -d link show dev {ifname}') + # [{"ifindex":2,"ifname":"eth0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","linkmode":"DEFAULT","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:d9:5b:04","broadcast":"ff:ff:ff:ff:ff:ff","promiscuity":0,"min_mtu":46,"max_mtu":16110,"inet6_addr_gen_mode":"none","num_tx_queues":1,"num_rx_queues":1,"gso_max_size":65536,"gso_max_segs":65535}] + parsed = json.loads(out)[0] + min_mtu = int(parsed.get('min_mtu', '0')) + # cur_mtu = parsed.get('mtu',0), + max_mtu = int(parsed.get('max_mtu', '0')) + cur_mtu = int(mtu) + + if (min_mtu and cur_mtu < min_mtu) or cur_mtu < 68: + raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} < {min_mtu}') + if (max_mtu and cur_mtu > max_mtu) or cur_mtu > 65536: + raise ValueError(f'MTU is too small for interface "{ifname}": {mtu} > {max_mtu}') + + +def assert_mac(m): + split = m.split(':') + size = len(split) + + # a mac address consits out of 6 octets + if size != 6: + raise ValueError(f'wrong number of MAC octets ({size}): {m}') + + octets = [] + try: + for octet in split: + octets.append(int(octet, 16)) + except ValueError: + raise ValueError(f'invalid hex number "{octet}" in : {m}') + + # validate against the first mac address byte if it's a multicast + # address + if octets[0] & 1: + raise ValueError(f'{m} is a multicast MAC address') + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(octets) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + if octets[:5] == (0, 0, 94, 0, 1): + raise ValueError(f'{m} is a VRRP MAC address') + + +def is_member(conf, interface, intftype=None): + """ + Checks if passed interface is member of other interface of specified type. + intftype is optional, if not passed it will search all known types + (currently bridge and bonding) + + Returns: + None -> Interface is not a member + interface name -> Interface is a member of this interface + False -> interface type cannot have members + """ + ret_val = None + if intftype not in ['bonding', 'bridge', None]: + raise ValueError(( + f'unknown interface type "{intftype}" or it cannot ' + f'have member interfaces')) + + intftype = ['bonding', 'bridge'] if intftype == None else [intftype] + + # set config level to root + old_level = conf.get_level() + conf.set_level([]) + + for it in intftype: + base = ['interfaces', it] + for intf in conf.list_nodes(base): + memberintf = base + [intf, 'member', 'interface'] + if xml.is_tag(memberintf): + if interface in conf.list_nodes(memberintf): + ret_val = intf + break + elif xml.is_leaf(memberintf): + if ( conf.exists(memberintf) and + interface in conf.return_values(memberintf) ): + ret_val = intf + break + + old_level = conf.set_level(old_level) + return ret_val + +def has_address_configured(conf, intf): + """ + Checks if interface has an address configured. + Checks the following config nodes: + 'address', 'ipv6 address eui64', 'ipv6 address autoconf' + + Returns True if interface has address configured, False if it doesn't. + """ + from vyos.ifconfig import Section + ret = False + + old_level = conf.get_level() + conf.set_level([]) + + intfpath = 'interfaces ' + Section.get_config_path(intf) + if ( conf.exists(f'{intfpath} address') or + conf.exists(f'{intfpath} ipv6 address autoconf') or + conf.exists(f'{intfpath} ipv6 address eui64') ): + ret = True + + conf.set_level(old_level) + return ret diff --git a/python/vyos/version.py b/python/vyos/version.py new file mode 100644 index 000000000..871bb0f1b --- /dev/null +++ b/python/vyos/version.py @@ -0,0 +1,107 @@ +# Copyright 2017-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +""" +VyOS version data access library. + +VyOS stores its version data, which include the version number and some +additional information in a JSON file. This module provides a convenient +interface to reading it. + +Example of the version data dict:: + { + 'built_by': 'autobuild@vyos.net', + 'build_id': '021ac2ee-cd07-448b-9991-9c68d878cddd', + 'version': '1.2.0-rolling+201806200337', + 'built_on': 'Wed 20 Jun 2018 03:37 UTC' + } +""" + +import os +import json +import vyos.defaults + +from vyos.util import read_file +from vyos.util import read_json +from vyos.util import popen +from vyos.util import run +from vyos.util import DEVNULL + + +version_file = os.path.join(vyos.defaults.directories['data'], 'version.json') + + +def get_version_data(fname=version_file): + """ + Get complete version data + + Args: + file (str): path to the version file + + Returns: + dict: version data, if it can not be found and empty dict + + The optional ``file`` argument comes in handy in upgrade scripts + that need to retrieve information from images other than the running image. + It should not be used on a running system since the location of that file + is an implementation detail and may change in the future, while the interface + of this module will stay the same. + """ + return read_json(fname, {}) + + +def get_version(fname=version_file): + """ + Get the version number, or an empty string if it could not be determined + """ + return get_version_data(fname=fname).get('version', '') + + +def get_full_version_data(fname=version_file): + version_data = get_version_data(fname) + + # Get system architecture (well, kernel architecture rather) + version_data['system_arch'], _ = popen('uname -m', stderr=DEVNULL) + + hypervisor,code = popen('hvinfo', stderr=DEVNULL) + if code == 1: + # hvinfo returns 1 if it cannot detect any hypervisor + version_data['system_type'] = 'bare metal' + else: + version_data['system_type'] = f"{hypervisor} guest" + + # Get boot type, it can be livecd, installed image, or, possible, a system installed + # via legacy "install system" mechanism + # In installed images, the squashfs image file is named after its image version, + # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot + # from an installed image + boot_via = "installed image" + if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0: + boot_via = "livecd" + elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0: + boot_via = "legacy non-image installation" + version_data['boot_via'] = boot_via + + # Get hardware details from DMI + dmi = '/sys/class/dmi/id' + version_data['hardware_vendor'] = read_file(dmi + '/sys_vendor', 'Unknown') + version_data['hardware_model'] = read_file(dmi +'/product_name','Unknown') + + # These two assume script is run as root, normal users can't access those files + subsystem = '/sys/class/dmi/id/subsystem/id' + version_data['hardware_serial'] = read_file(subsystem + '/product_serial','Unknown') + version_data['hardware_uuid'] = read_file(subsystem + '/product_uuid', 'Unknown') + + return version_data diff --git a/python/vyos/xml/.gitignore b/python/vyos/xml/.gitignore new file mode 100644 index 000000000..e934adfd1 --- /dev/null +++ b/python/vyos/xml/.gitignore @@ -0,0 +1 @@ +cache/ diff --git a/python/vyos/xml/__init__.py b/python/vyos/xml/__init__.py new file mode 100644 index 000000000..0f914fed2 --- /dev/null +++ b/python/vyos/xml/__init__.py @@ -0,0 +1,59 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from vyos.xml import definition +from vyos.xml import load +from vyos.xml import kw + + +def load_configuration(cache=[]): + if cache: + return cache[0] + + xml = definition.XML() + + try: + from vyos.xml.cache import configuration + xml.update(configuration.definition) + cache.append(xml) + except Exception: + xml = definition.XML() + print('no xml configuration cache') + xml.update(load.xml(load.configuration_definition)) + + return xml + + +# def is_multi(lpath): +# return load_configuration().is_multi(lpath) + + +def is_tag(lpath): + return load_configuration().is_tag(lpath) + + +def is_leaf(lpath, flat=True): + return load_configuration().is_leaf(lpath, flat) + + +def defaults(lpath, flat=False): + return load_configuration().defaults(lpath, flat) + + +if __name__ == '__main__': + print(defaults(['service'], flat=True)) + print(defaults(['service'], flat=False)) + + print(is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + print(is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop'])) diff --git a/python/vyos/xml/cache/__init__.py b/python/vyos/xml/cache/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python/vyos/xml/cache/__init__.py diff --git a/python/vyos/xml/definition.py b/python/vyos/xml/definition.py new file mode 100644 index 000000000..098e64f7e --- /dev/null +++ b/python/vyos/xml/definition.py @@ -0,0 +1,330 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from vyos.xml import kw + +# As we index by key, the name is first and then the data: +# {'dummy': { +# '[node]': '[tagNode]', +# 'address': { ... } +# } } + +# so when we encounter a tagNode, we are really encountering +# the tagNode data. + + +class XML(dict): + def __init__(self): + self[kw.tree] = {} + self[kw.priorities] = {} + self[kw.owners] = {} + self[kw.default] = {} + self[kw.tags] = [] + + dict.__init__(self) + + self.tree = self[kw.tree] + # the options which matched the last incomplete world we had + # or the last word in a list + self.options = [] + # store all the part of the command we processed + self.inside = [] + # should we check the data pass with the constraints + self.check = False + # are we still typing a word + self.filling = False + # do what have the tagNode value ? + self.filled = False + # last word seen + self.word = '' + # do we have all the data we want ? + self.final = False + # do we have too much data ? + self.extra = False + # what kind of node are we in plain vs data not + self.plain = True + + def reset(self): + self.tree = self[kw.tree] + self.options = [] + self.inside = [] + self.check = False + self.filling = False + self.filled = False + self.word = '' + self.final = False + self.extra = False + self.plain = True + + # from functools import lru_cache + # @lru_cache(maxsize=100) + # XXX: need to use cachetool instead - for later + + def traverse(self, cmd): + self.reset() + + # using split() intead of split(' ') eats the final ' ' + words = cmd.split(' ') + passed = [] + word = '' + data_node = False + space = False + + while words: + word = words.pop(0) + space = word == '' + perfect = False + if word in self.tree: + passed = [] + perfect = True + self.tree = self.tree[word] + data_node = self.tree[kw.node] + self.inside.append(word) + word = '' + continue + if word and data_node: + passed.append(word) + + is_valueless = self.tree.get(kw.valueless, False) + is_leafNode = data_node == kw.leafNode + is_dataNode = data_node in (kw.leafNode, kw.tagNode) + named_options = [_ for _ in self.tree if not kw.found(_)] + + if is_leafNode: + self.final = is_valueless or len(passed) > 0 + self.extra = is_valueless and len(passed) > 0 + self.check = len(passed) >= 1 + else: + self.final = False + self.extra = False + self.check = len(passed) == 1 and not space + + if self.final: + self.word = ' '.join(passed) + else: + self.word = word + + if self.final: + self.filling = True + else: + self.filling = not perfect and bool(cmd and word != '') + + self.filled = self.final or (is_dataNode and len(passed) > 0 and word == '') + + if is_dataNode and len(passed) == 0: + self.options = [] + elif word: + if data_node != kw.plainNode or len(passed) == 1: + self.options = [_ for _ in self.tree if _.startswith(word)] + self.options.sort() + else: + self.options = [] + else: + self.options = named_options + self.options.sort() + + self.plain = not is_dataNode + + # self.debug() + + return self.word + + def speculate(self): + if len(self.options) == 1: + self.tree = self.tree[self.options[0]] + self.word = '' + if self.tree.get(kw.node,'') not in (kw.tagNode, kw.leafNode): + self.options = [_ for _ in self.tree if not kw.found(_)] + self.options.sort() + + def checks(self, cmd): + # as we move thought the named node twice + # the first time we get the data with the node + # and the second with the pass parameters + xml = self[kw.tree] + + words = cmd.split(' ') + send = True + last = [] + while words: + word = words.pop(0) + if word in xml: + xml = xml[word] + send = True + last = [] + continue + if xml[kw.node] in (kw.tagNode, kw.leafNode): + if kw.constraint in xml: + if send: + yield (word, xml[kw.constraint]) + send = False + else: + last.append((word, None)) + if len(last) >= 2: + yield last[0] + + def summary(self): + yield ('enter', '[ summary ]', str(self.inside)) + + if kw.help not in self.tree: + yield ('skip', '[ summary ]', str(self.inside)) + return + + if self.filled: + return + + yield('', '', '\nHelp:') + + if kw.help in self.tree: + summary = self.tree[kw.help].get(kw.summary) + values = self.tree[kw.help].get(kw.valuehelp, []) + if summary: + yield(summary, '', '') + for value in values: + yield(value[kw.format], value[kw.description], '') + + def constraint(self): + yield ('enter', '[ constraint ]', str(self.inside)) + + if kw.help in self.tree: + yield ('skip', '[ constraint ]', str(self.inside)) + return + if kw.error not in self.tree: + yield ('skip', '[ constraint ]', str(self.inside)) + return + if not self.word or self.filling: + yield ('skip', '[ constraint ]', str(self.inside)) + return + + yield('', '', '\nData Constraint:') + + yield('', 'constraint', str(self.tree[kw.error])) + + def listing(self): + yield ('enter', '[ listing ]', str(self.inside)) + + # only show the details when we passed the tagNode data + if not self.plain and not self.filled: + yield ('skip', '[ listing ]', str(self.inside)) + return + + yield('', '', '\nPossible completions:') + + options = list(self.tree.keys()) + options.sort() + for option in options: + if kw.found(option): + continue + if not option.startswith(self.word): + continue + inner = self.tree[option] + prefix = '+> ' if inner.get(kw.node, '') != kw.leafNode else ' ' + if kw.help in inner: + yield (prefix + option, inner[kw.help].get(kw.summary), '') + else: + yield (prefix + option, '(no help available)', '') + + def debug(self): + print('------') + print("word '%s'" % self.word) + print("filling " + str(self.filling)) + print("filled " + str(self.filled)) + print("final " + str(self.final)) + print("extra " + str(self.extra)) + print("plain " + str(self.plain)) + print("options " + str(self.options)) + + # from functools import lru_cache + # @lru_cache(maxsize=100) + # XXX: need to use cachetool instead - for later + + def defaults(self, lpath, flat): + d = self[kw.default] + for k in lpath: + d = d.get(k, {}) + + if not flat: + r = {} + for k in d: + under = k.replace('-','_') + if isinstance(d[k],dict): + r[under] = self.defaults(lpath + [k], flat) + continue + r[under] = d[k] + return r + + def _flatten(inside, index, d): + r = {} + local = inside[index:] + prefix = '_'.join(_.replace('-','_') for _ in local) + '_' if local else '' + for k in d: + under = prefix + k.replace('-','_') + level = inside + [k] + if isinstance(d[k],dict): + r.update(_flatten(level, index, d[k])) + continue + if self.is_multi(level, with_tag=False): + r[under] = [_.strip() for _ in d[k].split(',')] + continue + r[under] = d[k] + return r + + return _flatten(lpath, len(lpath), d) + + # from functools import lru_cache + # @lru_cache(maxsize=100) + # XXX: need to use cachetool instead - for later + + def _tree(self, lpath, with_tag=True): + """ + returns the part of the tree searched or None if it does not exists + if with_tag is set, this is a configuration path (with tagNode names) + and tag name will be removed from the path when traversing the tree + """ + tree = self[kw.tree] + spath = lpath.copy() + while spath: + p = spath.pop(0) + if p not in tree: + return None + tree = tree[p] + if with_tag and spath and tree[kw.node] == kw.tagNode: + spath.pop(0) + return tree + + def _get(self, lpath, tag, with_tag=True): + tree = self._tree(lpath, with_tag) + if tree is None: + return None + return tree.get(tag, None) + + def is_multi(self, lpath, with_tag=True): + tree = self._get(lpath, kw.multi, with_tag) + if tree is None: + return None + return tree is True + + def is_tag(self, lpath, with_tag=True): + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.tagNode + + def is_leaf(self, lpath, with_tag=True): + tree = self._get(lpath, kw.node, with_tag) + if tree is None: + return None + return tree == kw.leafNode + + def exists(self, lpath, with_tag=True): + return self._get(lpath, kw.node, with_tag) is not None diff --git a/python/vyos/xml/generate.py b/python/vyos/xml/generate.py new file mode 100755 index 000000000..dfbbadd74 --- /dev/null +++ b/python/vyos/xml/generate.py @@ -0,0 +1,70 @@ + +#!/usr/bin/env python3 + +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +import sys +import pprint +import argparse + +from vyos.xml import kw +from vyos.xml import load + + +# import json +# def save_json(fname, loaded): +# with open(fname, 'w') as w: +# print(f'saving {fname}') +# w.write(json.dumps(loaded)) + + +def save_dict(fname, loaded): + with open(fname, 'w') as w: + print(f'saving {fname}') + w.write(f'# generated by {__file__}\n\n') + w.write('definition = ') + w.write(str(loaded)) + + +def main(): + parser = argparse.ArgumentParser(description='generate python file from xml defintions') + parser.add_argument('--conf-folder', type=str, default=load.configuration_definition, help='XML interface definition folder') + parser.add_argument('--conf-cache', type=str, default=load.configuration_cache, help='python file with the conf mode dict') + + # parser.add_argument('--op-folder', type=str, default=load.operational_definition, help='XML interface definition folder') + # parser.add_argument('--op-cache', type=str, default=load.operational_cache, help='python file with the conf mode dict') + + parser.add_argument('--dry', action='store_true', help='dry run, print to screen') + + args = parser.parse_args() + + if os.path.exists(load.configuration_cache): + os.remove(load.configuration_cache) + # if os.path.exists(load.operational_cache): + # os.remove(load.operational_cache) + + conf = load.xml(args.conf_folder) + # op = load.xml(args.op_folder) + + if args.dry: + pprint.pprint(conf) + return + + save_dict(args.conf_cache, conf) + # save_dict(args.op_cache, op) + + +if __name__ == '__main__': + main() diff --git a/python/vyos/xml/kw.py b/python/vyos/xml/kw.py new file mode 100644 index 000000000..64521c51a --- /dev/null +++ b/python/vyos/xml/kw.py @@ -0,0 +1,83 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# all named used as key (keywords) in this module are defined here. +# using variable name will allow the linter to warn on typos +# it separates our dict syntax from the xmldict one, making it easy to change + +# we are redefining a python keyword "list" for ease + + +def found(word): + """ + is the word following the format for a keyword + """ + return word and word[0] == '[' and word[-1] == ']' + + +# root + +version = '[version]' +tree = '[tree]' +priorities = '[priorities]' +owners = '[owners]' +tags = '[tags]' +default = '[default]' + +# nodes + +node = '[node]' + +plainNode = '[plainNode]' +leafNode = '[leafNode]' +tagNode = '[tagNode]' + +owner = '[owner]' + +valueless = '[valueless]' +multi = '[multi]' +hidden = '[hidden]' + +# properties + +priority = '[priority]' + +completion = '[completion]' +list = '[list]' +script = '[script]' +path = '[path]' + +# help + +help = '[help]' + +summary = '[summary]' + +valuehelp = '[valuehelp]' +format = 'format' +description = 'description' + +# constraint + +constraint = '[constraint]' +name = '[name]' + +regex = '[regex]' +validator = '[validator]' +argument = '[argument]' + +error = '[error]' + +# created + +node = '[node]' diff --git a/python/vyos/xml/load.py b/python/vyos/xml/load.py new file mode 100644 index 000000000..1f463a5b7 --- /dev/null +++ b/python/vyos/xml/load.py @@ -0,0 +1,290 @@ +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or modify it under the terms of +# the GNU Lesser General Public License as published by the Free Software Foundation; +# either version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along with this library; +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import glob + +from os.path import join +from os.path import abspath +from os.path import dirname + +import xmltodict + +from vyos import debug +from vyos.xml import kw +from vyos.xml import definition + + +# where the files are located + +_here = dirname(__file__) + +configuration_definition = abspath(join(_here, '..', '..' ,'..', 'interface-definitions')) +configuration_cache = abspath(join(_here, 'cache', 'configuration.py')) + +operational_definition = abspath(join(_here, '..', '..' ,'..', 'op-mode-definitions')) +operational_cache = abspath(join(_here, 'cache', 'operational.py')) + + +# This code is only ran during the creation of the debian package +# therefore we accept that failure can be fatal and not handled +# gracefully. + + +def _fatal(debug_info=''): + """ + raise a RuntimeError or if in developer mode stop the code + """ + if not debug.enabled('developer'): + raise RuntimeError(str(debug_info)) + + if debug_info: + print(debug_info) + breakpoint() + + +def _safe_update(dict1, dict2): + """ + return a dict made of two, raise if any root key would be overwritten + """ + if set(dict1).intersection(dict2): + raise RuntimeError('overlapping configuration') + return {**dict1, **dict2} + + +def _merge(dict1, dict2): + """ + merge dict2 in to dict1 and return it + """ + for k in list(dict2): + if k not in dict1: + dict1[k] = dict2[k] + continue + if isinstance(dict1[k], dict) and isinstance(dict2[k], dict): + dict1[k] = _merge(dict1[k], dict2[k]) + elif isinstance(dict1[k], dict) and isinstance(dict2[k], dict): + dict1[k].extend(dict2[k]) + elif dict1[k] == dict2[k]: + # A definition shared between multiple files + if k in (kw.valueless, kw.multi, kw.hidden, kw.node, kw.summary, kw.owner, kw.priority): + continue + _fatal() + raise RuntimeError('parsing issue - undefined leaf?') + else: + raise RuntimeError('parsing issue - we messed up?') + return dict1 + + +def _include(fname, folder=''): + """ + return the content of a file, including any file referenced with a #include + """ + if not folder: + folder = dirname(fname) + content = '' + with open(fname, 'r') as r: + for line in r.readlines(): + if '#include' in line: + content += _include(join(folder,line.strip()[10:-1]), folder) + continue + content += line + return content + + +def _format_nodes(inside, conf, xml): + r = {} + while conf: + nodetype = '' + nodename = '' + if 'node' in conf.keys(): + nodetype = 'node' + nodename = kw.plainNode + elif 'leafNode' in conf.keys(): + nodetype = 'leafNode' + nodename = kw.leafNode + elif 'tagNode' in conf.keys(): + nodetype = 'tagNode' + nodename = kw.tagNode + elif 'syntaxVersion' in conf.keys(): + r[kw.version] = conf.pop('syntaxVersion')['@version'] + continue + else: + _fatal(conf.keys()) + + nodes = conf.pop(nodetype) + if isinstance(nodes, list): + for node in nodes: + name = node.pop('@name') + into = inside + [name] + r[name] = _format_node(into, node, xml) + r[name][kw.node] = nodename + xml[kw.tags].append(' '.join(into)) + else: + node = nodes + name = node.pop('@name') + into = inside + [name] + r[name] = _format_node(inside + [name], node, xml) + r[name][kw.node] = nodename + xml[kw.tags].append(' '.join(into)) + return r + + +def _set_validator(r, validator): + v = {} + while validator: + if '@name' in validator: + v[kw.name] = validator.pop('@name') + elif '@argument' in validator: + v[kw.argument] = validator.pop('@argument') + else: + _fatal(validator) + r[kw.constraint][kw.validator].append(v) + + +def _format_node(inside, conf, xml): + r = { + kw.valueless: False, + kw.multi: False, + kw.hidden: False, + } + + if '@owner' in conf: + owner = conf.pop('@owner', '') + r[kw.owner] = owner + xml[kw.owners][' '.join(inside)] = owner + + while conf: + keys = conf.keys() + if 'children' in keys: + children = conf.pop('children') + + if isinstance(conf, list): + for child in children: + r = _safe_update(r, _format_nodes(inside, child, xml)) + else: + child = children + r = _safe_update(r, _format_nodes(inside, child, xml)) + + elif 'properties' in keys: + properties = conf.pop('properties') + + while properties: + if 'help' in properties: + helpname = properties.pop('help') + r[kw.help] = {} + r[kw.help][kw.summary] = helpname + + elif 'valueHelp' in properties: + valuehelps = properties.pop('valueHelp') + if kw.valuehelp in r[kw.help]: + _fatal(valuehelps) + r[kw.help][kw.valuehelp] = [] + if isinstance(valuehelps, list): + for valuehelp in valuehelps: + r[kw.help][kw.valuehelp].append(dict(valuehelp)) + else: + valuehelp = valuehelps + r[kw.help][kw.valuehelp].append(dict(valuehelp)) + + elif 'constraint' in properties: + constraint = properties.pop('constraint') + r[kw.constraint] = {} + while constraint: + if 'regex' in constraint: + regexes = constraint.pop('regex') + if kw.regex in kw.constraint: + _fatal(regexes) + r[kw.constraint][kw.regex] = [] + if isinstance(regexes, list): + r[kw.constraint][kw.regex] = [] + for regex in regexes: + r[kw.constraint][kw.regex].append(regex) + else: + regex = regexes + r[kw.constraint][kw.regex].append(regex) + elif 'validator' in constraint: + validators = constraint.pop('validator') + if kw.validator in r[kw.constraint]: + _fatal(validators) + r[kw.constraint][kw.validator] = [] + if isinstance(validators, list): + for validator in validators: + _set_validator(r, validator) + else: + validator = validators + _set_validator(r, validator) + else: + _fatal(constraint) + + elif 'constraintErrorMessage' in properties: + r[kw.error] = properties.pop('constraintErrorMessage') + + elif 'valueless' in properties: + properties.pop('valueless') + r[kw.valueless] = True + + elif 'multi' in properties: + properties.pop('multi') + r[kw.multi] = True + + elif 'hidden' in properties: + properties.pop('hidden') + r[kw.hidden] = True + + elif 'completionHelp' in properties: + completionHelp = properties.pop('completionHelp') + r[kw.completion] = {} + while completionHelp: + if 'list' in completionHelp: + r[kw.completion][kw.list] = completionHelp.pop('list') + elif 'script' in completionHelp: + r[kw.completion][kw.script] = completionHelp.pop('script') + elif 'path' in completionHelp: + r[kw.completion][kw.path] = completionHelp.pop('path') + else: + _fatal(completionHelp.keys()) + + elif 'priority' in properties: + priority = int(properties.pop('priority')) + r[kw.priority] = priority + xml[kw.priorities].setdefault(priority, []).append(' '.join(inside)) + + else: + _fatal(properties.keys()) + + elif 'defaultValue' in keys: + default = conf.pop('defaultValue') + x = xml[kw.default] + for k in inside[:-1]: + x = x.setdefault(k,{}) + x[inside[-1]] = '' if default is None else default + + else: + _fatal(conf) + + return r + + +def xml(folder): + """ + read all the xml in the folder + """ + xml = definition.XML() + for fname in glob.glob(f'{folder}/*.xml.in'): + parsed = xmltodict.parse(_include(fname)) + formated = _format_nodes([], parsed['interfaceDefinition'], xml) + _merge(xml[kw.tree], formated) + # fix the configuration root node for completion + # as we moved all the name "up" the chain to use them as index. + xml[kw.tree][kw.node] = kw.plainNode + # XXX: do the others + return xml diff --git a/python/vyos/xml/test_xml.py b/python/vyos/xml/test_xml.py new file mode 100644 index 000000000..ff55151d2 --- /dev/null +++ b/python/vyos/xml/test_xml.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import unittest +from unittest import TestCase, mock + +from vyos.xml import load_configuration + +import sys + + +class TestSearch(TestCase): + def setUp(self): + self.xml = load_configuration() + + def test_(self): + last = self.xml.traverse("") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, []) + self.assertEqual(self.xml.options, ['firewall', 'high-availability', 'interfaces', 'nat', 'protocols', 'service', 'system', 'vpn', 'vrf']) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_i(self): + last = self.xml.traverse("i") + self.assertEqual(last, 'i') + self.assertEqual(self.xml.inside, []) + self.assertEqual(self.xml.options, ['interfaces']) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces(self): + last = self.xml.traverse("interfaces") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces']) + self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem']) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, '') + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces_space(self): + last = self.xml.traverse("interfaces ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces']) + self.assertEqual(self.xml.options, ['bonding', 'bridge', 'dummy', 'ethernet', 'geneve', 'l2tpv3', 'loopback', 'macsec', 'openvpn', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan', 'wireguard', 'wireless', 'wirelessmodem']) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces_w(self): + last = self.xml.traverse("interfaces w") + self.assertEqual(last, 'w') + self.assertEqual(self.xml.inside, ['interfaces']) + self.assertEqual(self.xml.options, ['wireguard', 'wireless', 'wirelessmodem']) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, True) + + def test_interfaces_ethernet(self): + last = self.xml.traverse("interfaces ethernet") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, '') + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_space(self): + last = self.xml.traverse("interfaces ethernet ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, '') + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_e(self): + last = self.xml.traverse("interfaces ethernet e") + self.assertEqual(last, 'e') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_la(self): + last = self.xml.traverse("interfaces ethernet la") + self.assertEqual(last, 'la') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0(self): + last = self.xml.traverse("interfaces ethernet lan0") + self.assertEqual(last, 'lan0') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_space(self): + last = self.xml.traverse("interfaces ethernet lan0 ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(len(self.xml.options), 19) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_ad(self): + last = self.xml.traverse("interfaces ethernet lan0 ad") + self.assertEqual(last, 'ad') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet']) + self.assertEqual(self.xml.options, ['address']) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address(self): + last = self.xml.traverse("interfaces ethernet lan0 address") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space(self): + last = self.xml.traverse("interfaces ethernet lan0 address ") + self.assertEqual(last, '') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, False) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, False) + self.assertEqual(self.xml.final, False) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, False) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_11(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1") + self.assertEqual(last, '1.1') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32") + self.assertEqual(last, '1.1.1.1/32') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32_space(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 ") + self.assertEqual(last, '1.1.1.1/32') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32_space_text(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text") + self.assertEqual(last, '1.1.1.1/32 text') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + def test_interfaces_ethernet_lan0_address_space_1111_32_space_text_space(self): + last = self.xml.traverse("interfaces ethernet lan0 address 1.1.1.1/32 text ") + self.assertEqual(last, '1.1.1.1/32 text') + self.assertEqual(self.xml.inside, ['interfaces', 'ethernet', 'address']) + self.assertEqual(self.xml.options, []) + self.assertEqual(self.xml.filling, True) + self.assertEqual(self.xml.word, last) + self.assertEqual(self.xml.check, True) + self.assertEqual(self.xml.final, True) + self.assertEqual(self.xml.extra, False) + self.assertEqual(self.xml.filled, True) + self.assertEqual(self.xml.plain, False) + + # Need to add a check for a valuless leafNode
\ No newline at end of file diff --git a/schema/interface_definition.rnc b/schema/interface_definition.rnc new file mode 100644 index 000000000..6647f5e11 --- /dev/null +++ b/schema/interface_definition.rnc @@ -0,0 +1,175 @@ +# interface_definition.rnc: VyConf reference tree XML grammar +# +# Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + +# The language of this file is compact form RELAX-NG +# http://relaxng.org/compact-tutorial-20030326.htm +# (unless converted to XML, then just RELAX-NG :) + +# Interface definition starts with interfaceDefinition tag that may contain node tags +start = element interfaceDefinition +{ + syntaxVersion*, + node* +} + +# interfaceDefinition may contain syntax version attribute lists. +syntaxVersion = element syntaxVersion +{ + (componentAttr & versionAttr) +} + +# node tag may contain node, leafNode, or tagNode tags +# Those are intermediate configuration nodes that may only contain +# other nodes and must not have values +node = element node +{ + (ownerAttr? & nodeNameAttr), + (properties? & children? ) +} + +# Tag nodes are containers for nodes without predefined names, like network interfaces +# or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker") +# Tag nodes may contain node and leafNode elements, and also nameConstraint tags +# They must not contain other tag nodes +tagNode = element tagNode +{ + (ownerAttr? & nodeNameAttr), + (properties? & children ) +} + +# Leaf nodes are terminal configuration nodes that can't have children, +# but can have values. +# Leaf node may contain one or more valueConstraint tags +# If multiple valueConstraint tags are used, they work a logical OR +# Leaf nodes can have "multi" attribute that indicated that it can have more than one value +# It can also have a default value +leafNode = element leafNode +{ + (ownerAttr? & nodeNameAttr), + (defaultValue? & properties) +} + +# Default value for leaf node, if applicable +# It is used to generate default node state representation +defaultValue = element defaultValue { text } + +# Normal and tag nodes may have children +children = element children +{ + (node | tagNode | leafNode)+ +} + +# Nodes may have properties +# For simplicity, any property is allowed in any node, +# but whether they are used or not is implementation-defined +# +# Leaf nodes may differ in number of values that can be +# associated with them. +# By default, a leaf node can have only one value. +# "multi" tag means a node can have one or more values, +# "valueless" means it can have no values at all. +# "hidden" means node visibility can be toggled, eg 'dangerous' commands, +# "secret" allows a node to hide its value from unprivileged users. +# +# "priority" is used to influence node processing order for nodes +# with exact same dependencies and in compatibility modes. +properties = element properties +{ + help? & + constraint? & + valueHelp* & + (element constraintErrorMessage { text })? & + completionHelp* & + + # These are meaningful only for leaf nodes + (element valueless { empty })? & + (element multi { empty })? & + (element hidden { empty })? & + (element secret { empty })? & + (element priority { text })? & + + # These are meaningful only for tag nodes + (element keepChildOrder { empty })? +} + +componentAttr = attribute component +{ + text +} + +versionAttr = attribute version +{ + text +} + +# All nodes must have "name" attribute +nodeNameAttr = attribute name +{ + text +} + +# Ordinary nodes and tag nodes can have "owner" attribute. +# Owner is the component that is notified when node changes. +ownerAttr = attribute owner +{ + text +} + +# Tag and leaf nodes may have constraints on their names and values +# (respectively). +# When multiple constraints are listed, they work as logical OR +constraint = element constraint +{ + ( (element regex { text }) | + validator )+ +} + +# A constraint may also use an external validator rather than regex +validator = element validator +{ + ( (attribute name { text }) & + (attribute argument { text })? ), + empty +} + +# help tags contains brief description of the purpose of the node +help = element help +{ + text +} + +# valueHelp tags contain information about acceptable value format +valueHelp = element valueHelp +{ + element format { text } & + element description { text } +} + +# completionHelp tags contain information about allowed values of a node that is used for generating +# tab completion in the CLI frontend and drop-down lists in GUI frontends +# It is only meaninful for leaf nodes +# Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>), +# as a configuration path (e.g. <path>interfaces ethernet</path>), +# or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script> +completionHelp = element completionHelp +{ + (element list { text })* & + (element path { text })* & + (element script { text })* +} diff --git a/schema/interface_definition.rng b/schema/interface_definition.rng new file mode 100644 index 000000000..22e886006 --- /dev/null +++ b/schema/interface_definition.rng @@ -0,0 +1,307 @@ +<?xml version="1.0" encoding="UTF-8"?> +<grammar xmlns="http://relaxng.org/ns/structure/1.0"> + <!-- + interface_definition.rnc: VyConf reference tree XML grammar + + Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + --> + <!-- + The language of this file is compact form RELAX-NG + http://relaxng.org/compact-tutorial-20030326.htm + (unless converted to XML, then just RELAX-NG :) + --> + <!-- Interface definition starts with interfaceDefinition tag that may contain node tags --> + <start> + <element name="interfaceDefinition"> + <zeroOrMore> + <ref name="syntaxVersion"/> + </zeroOrMore> + <zeroOrMore> + <ref name="node"/> + </zeroOrMore> + </element> + </start> + <!-- interfaceDefinition may contain syntax version attribute lists. --> + <define name="syntaxVersion"> + <element name="syntaxVersion"> + <interleave> + <ref name="componentAttr"/> + <ref name="versionAttr"/> + </interleave> + </element> + </define> + <!-- + node tag may contain node, leafNode, or tagNode tags + Those are intermediate configuration nodes that may only contain + other nodes and must not have values + --> + <define name="node"> + <element name="node"> + <interleave> + <optional> + <ref name="ownerAttr"/> + </optional> + <ref name="nodeNameAttr"/> + </interleave> + <interleave> + <optional> + <ref name="properties"/> + </optional> + <optional> + <ref name="children"/> + </optional> + </interleave> + </element> + </define> + <!-- + Tag nodes are containers for nodes without predefined names, like network interfaces + or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker") + Tag nodes may contain node and leafNode elements, and also nameConstraint tags + They must not contain other tag nodes + --> + <define name="tagNode"> + <element name="tagNode"> + <interleave> + <optional> + <ref name="ownerAttr"/> + </optional> + <ref name="nodeNameAttr"/> + </interleave> + <interleave> + <optional> + <ref name="properties"/> + </optional> + <ref name="children"/> + </interleave> + </element> + </define> + <!-- + Leaf nodes are terminal configuration nodes that can't have children, + but can have values. + Leaf node may contain one or more valueConstraint tags + If multiple valueConstraint tags are used, they work a logical OR + Leaf nodes can have "multi" attribute that indicated that it can have more than one value + It can also have a default value + --> + <define name="leafNode"> + <element name="leafNode"> + <interleave> + <optional> + <ref name="ownerAttr"/> + </optional> + <ref name="nodeNameAttr"/> + </interleave> + <interleave> + <optional> + <ref name="defaultValue"/> + </optional> + <ref name="properties"/> + </interleave> + </element> + </define> + <!-- + Default value for leaf node, if applicable + It is used to generate default node state representation + --> + <define name="defaultValue"> + <element name="defaultValue"> + <text/> + </element> + </define> + <!-- Normal and tag nodes may have children --> + <define name="children"> + <element name="children"> + <oneOrMore> + <choice> + <ref name="node"/> + <ref name="tagNode"/> + <ref name="leafNode"/> + </choice> + </oneOrMore> + </element> + </define> + <!-- + Nodes may have properties + For simplicity, any property is allowed in any node, + but whether they are used or not is implementation-defined + + Leaf nodes may differ in number of values that can be + associated with them. + By default, a leaf node can have only one value. + "multi" tag means a node can have one or more values, + "valueless" means it can have no values at all. + "hidden" means node visibility can be toggled, eg 'dangerous' commands, + "secret" allows a node to hide its value from unprivileged users. + + "priority" is used to influence node processing order for nodes + with exact same dependencies and in compatibility modes. + --> + <define name="properties"> + <element name="properties"> + <interleave> + <optional> + <ref name="help"/> + </optional> + <optional> + <ref name="constraint"/> + </optional> + <zeroOrMore> + <ref name="valueHelp"/> + </zeroOrMore> + <optional> + <element name="constraintErrorMessage"> + <text/> + </element> + </optional> + <zeroOrMore> + <ref name="completionHelp"/> + </zeroOrMore> + <optional> + <!-- These are meaningful only for leaf nodes --> + <group> + <element name="valueless"> + <empty/> + </element> + </group> + </optional> + <optional> + <element name="multi"> + <empty/> + </element> + </optional> + <optional> + <element name="hidden"> + <empty/> + </element> + </optional> + <optional> + <element name="secret"> + <empty/> + </element> + </optional> + <optional> + <element name="priority"> + <text/> + </element> + </optional> + <optional> + <!-- These are meaningful only for tag nodes --> + <group> + <element name="keepChildOrder"> + <empty/> + </element> + </group> + </optional> + </interleave> + </element> + </define> + <define name="componentAttr"> + <attribute name="component"/> + </define> + <define name="versionAttr"> + <attribute name="version"/> + </define> + <!-- All nodes must have "name" attribute --> + <define name="nodeNameAttr"> + <attribute name="name"/> + </define> + <!-- + Ordinary nodes and tag nodes can have "owner" attribute. + Owner is the component that is notified when node changes. + --> + <define name="ownerAttr"> + <attribute name="owner"/> + </define> + <!-- + Tag and leaf nodes may have constraints on their names and values + (respectively). + When multiple constraints are listed, they work as logical OR + --> + <define name="constraint"> + <element name="constraint"> + <oneOrMore> + <choice> + <element name="regex"> + <text/> + </element> + <ref name="validator"/> + </choice> + </oneOrMore> + </element> + </define> + <!-- A constraint may also use an external validator rather than regex --> + <define name="validator"> + <element name="validator"> + <interleave> + <attribute name="name"/> + <optional> + <attribute name="argument"/> + </optional> + </interleave> + <empty/> + </element> + </define> + <!-- help tags contains brief description of the purpose of the node --> + <define name="help"> + <element name="help"> + <text/> + </element> + </define> + <!-- valueHelp tags contain information about acceptable value format --> + <define name="valueHelp"> + <element name="valueHelp"> + <interleave> + <element name="format"> + <text/> + </element> + <element name="description"> + <text/> + </element> + </interleave> + </element> + </define> + <!-- + completionHelp tags contain information about allowed values of a node that is used for generating + tab completion in the CLI frontend and drop-down lists in GUI frontends + It is only meaninful for leaf nodes + Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>), + as a configuration path (e.g. <path>interfaces ethernet</path>), + or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script> + --> + <define name="completionHelp"> + <element name="completionHelp"> + <interleave> + <zeroOrMore> + <element name="list"> + <text/> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="path"> + <text/> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="script"> + <text/> + </element> + </zeroOrMore> + </interleave> + </element> + </define> +</grammar> diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc new file mode 100644 index 000000000..cbe51e6dc --- /dev/null +++ b/schema/op-mode-definition.rnc @@ -0,0 +1,107 @@ +# interface_definition.rnc: VyConf reference tree XML grammar +# +# Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + +# The language of this file is compact form RELAX-NG +# http://relaxng.org/compact-tutorial-20030326.htm +# (unless converted to XML, then just RELAX-NG :) + +# Interface definition starts with interfaceDefinition tag that may contain node tags +start = element interfaceDefinition +{ + (node | tagNode)* +} + +# node tag may contain node, leafNode, or tagNode tags +# Those are intermediate configuration nodes that may only contain +# other nodes and must not have values +node = element node +{ + nodeNameAttr, + (properties? & children? & command?) +} + +# Tag nodes are containers for nodes without predefined names, like network interfaces +# or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker") +# Tag nodes may contain node and leafNode elements, and also nameConstraint tags +# They must not contain other tag nodes +tagNode = element tagNode +{ + nodeNameAttr, + (properties? & children? & command?) +} + +# Leaf nodes are terminal configuration nodes that can't have children, +# but can have values. + +leafNode = element leafNode +{ + nodeNameAttr, + (command & properties) +} + +# Normal and tag nodes may have children +children = element children +{ + (node | tagNode | leafNode)+ +} + +# Nodes may have properties +# For simplicity, any property is allowed in any node, +# but whether they are used or not is implementation-defined + + +properties = element properties +{ + help? & + completionHelp* +} + +# All nodes must have "name" attribute +nodeNameAttr = attribute name +{ + text +} + + + + + +# help tags contains brief description of the purpose of the node +help = element help +{ + text +} + +command = element command +{ + text +} + +# completionHelp tags contain information about allowed values of a node that is used for generating +# tab completion in the CLI frontend and drop-down lists in GUI frontends +# It is only meaninful for leaf nodes +# Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>), +# as a configuration path (e.g. <path>interfaces ethernet</path>), +# or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script> +completionHelp = element completionHelp +{ + (element list { text })* & + (element path { text })* & + (element script { text })* +} diff --git a/schema/op-mode-definition.rng b/schema/op-mode-definition.rng new file mode 100644 index 000000000..900f41e27 --- /dev/null +++ b/schema/op-mode-definition.rng @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="UTF-8"?> +<grammar xmlns="http://relaxng.org/ns/structure/1.0"> + <!-- + interface_definition.rnc: VyConf reference tree XML grammar + + Copyright (C) 2014. 2017 VyOS maintainers and contributors <maintainers@vyos.net> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + USA + --> + <!-- + The language of this file is compact form RELAX-NG + http://relaxng.org/compact-tutorial-20030326.htm + (unless converted to XML, then just RELAX-NG :) + --> + <!-- Interface definition starts with interfaceDefinition tag that may contain node tags --> + <start> + <element name="interfaceDefinition"> + <zeroOrMore> + <choice> + <ref name="node"/> + <ref name="tagNode"/> + </choice> + </zeroOrMore> + </element> + </start> + <!-- + node tag may contain node, leafNode, or tagNode tags + Those are intermediate configuration nodes that may only contain + other nodes and must not have values + --> + <define name="node"> + <element name="node"> + <ref name="nodeNameAttr"/> + <interleave> + <optional> + <ref name="properties"/> + </optional> + <optional> + <ref name="children"/> + </optional> + <optional> + <ref name="command"/> + </optional> + </interleave> + </element> + </define> + <!-- + Tag nodes are containers for nodes without predefined names, like network interfaces + or user names (e.g. "interfaces ethernet eth0" or "user jrandomhacker") + Tag nodes may contain node and leafNode elements, and also nameConstraint tags + They must not contain other tag nodes + --> + <define name="tagNode"> + <element name="tagNode"> + <ref name="nodeNameAttr"/> + <interleave> + <optional> + <ref name="properties"/> + </optional> + <optional> + <ref name="children"/> + </optional> + <optional> + <ref name="command"/> + </optional> + </interleave> + </element> + </define> + <!-- + Leaf nodes are terminal configuration nodes that can't have children, + but can have values. + --> + <define name="leafNode"> + <element name="leafNode"> + <ref name="nodeNameAttr"/> + <interleave> + <ref name="command"/> + <ref name="properties"/> + </interleave> + </element> + </define> + <!-- Normal and tag nodes may have children --> + <define name="children"> + <element name="children"> + <oneOrMore> + <choice> + <ref name="node"/> + <ref name="tagNode"/> + <ref name="leafNode"/> + </choice> + </oneOrMore> + </element> + </define> + <!-- + Nodes may have properties + For simplicity, any property is allowed in any node, + but whether they are used or not is implementation-defined + --> + <define name="properties"> + <element name="properties"> + <interleave> + <optional> + <ref name="help"/> + </optional> + <zeroOrMore> + <ref name="completionHelp"/> + </zeroOrMore> + </interleave> + </element> + </define> + <!-- All nodes must have "name" attribute --> + <define name="nodeNameAttr"> + <attribute name="name"/> + </define> + <!-- help tags contains brief description of the purpose of the node --> + <define name="help"> + <element name="help"> + <text/> + </element> + </define> + <define name="command"> + <element name="command"> + <text/> + </element> + </define> + <!-- + completionHelp tags contain information about allowed values of a node that is used for generating + tab completion in the CLI frontend and drop-down lists in GUI frontends + It is only meaninful for leaf nodes + Allowed values can be given as a fixed list of values (e.g. <list>foo bar baz</list>), + as a configuration path (e.g. <path>interfaces ethernet</path>), + or as a path to a script file that generates the list (e.g. <script>/usr/lib/foo/list-things</script> + --> + <define name="completionHelp"> + <element name="completionHelp"> + <interleave> + <zeroOrMore> + <element name="list"> + <text/> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="path"> + <text/> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="script"> + <text/> + </element> + </zeroOrMore> + </interleave> + </element> + </define> +</grammar> diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates new file mode 100755 index 000000000..c60b32a1e --- /dev/null +++ b/scripts/build-command-op-templates @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +# +# build-command-template: converts new style command definitions in XML +# to the old style (bunch of dirs and node.def's) command templates +# +# Copyright (C) 2017 VyOS maintainers <maintainers@vyos.net> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + +import sys +import os +import argparse +import copy +import functools + +from lxml import etree as ET + +# Defaults + +validator_dir = "/opt/vyatta/libexec/validators" +default_constraint_err_msg = "Invalid value" + + +## Get arguments + +parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates') +parser.add_argument('--debug', help='Enable debug information output', action='store_true') +parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file") +parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file") +parser.add_argument('OUTPUT_DIR', type=str, help="Output directory") + +args = parser.parse_args() + +input_file = args.INPUT_FILE +schema_file = args.SCHEMA_FILE +output_dir = args.OUTPUT_DIR +debug = args.debug + +## Load and validate the inputs + +try: + xml = ET.parse(input_file) +except Exception as e: + print("Failed to load interface definition file {0}".format(input_file)) + print(e) + sys.exit(1) + +try: + relaxng_xml = ET.parse(schema_file) + validator = ET.RelaxNG(relaxng_xml) + + if not validator.validate(xml): + print(validator.error_log) + print("Interface definition file {0} does not match the schema!".format(input_file)) + sys.exit(1) +except Exception as e: + print("Failed to load the XML schema {0}".format(schema_file)) + print(e) + sys.exit(1) + +if not os.access(output_dir, os.W_OK): + print("The output directory {0} is not writeable".format(output_dir)) + sys.exit(1) + +## If we got this far, everything must be ok and we can convert the file + +def make_path(l): + path = functools.reduce(os.path.join, l) + if debug: + print(path) + return path + +def get_properties(p): + props = {} + + if p is None: + return props + + # Get the help string + try: + props["help"] = p.find("help").text + except: + props["help"] = "No help available" + + + # Get the completion help strings + try: + che = p.findall("completionHelp") + ch = "" + for c in che: + scripts = c.findall("script") + paths = c.findall("path") + lists = c.findall("list") + + # Current backend doesn't support multiple allowed: tags + # so we get to emulate it + comp_exprs = [] + for i in lists: + comp_exprs.append("echo \"{0}\"".format(i.text)) + for i in paths: + comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\" && echo".format(i.text)) + for i in scripts: + comp_exprs.append("{0}".format(i.text)) + comp_help = " && ".join(comp_exprs) + props["comp_help"] = comp_help + except: + props["comp_help"] = [] + + return props + + +def make_node_def(props, command): + # XXX: replace with a template processor if it grows + # out of control + + node_def = "" + + if "help" in props: + node_def += "help: {0}\n".format(props["help"]) + + + if "comp_help" in props: + node_def += "allowed: {0}\n".format(props["comp_help"]) + + + if command is not None: + node_def += "run: {0}\n".format(command.text) + + + if debug: + print("The contents of the node.def file:\n", node_def) + + return node_def + +def process_node(n, tmpl_dir): + # Avoid mangling the path from the outer call + my_tmpl_dir = copy.copy(tmpl_dir) + + props_elem = n.find("properties") + children = n.find("children") + command = n.find("command") + + name = n.get("name") + + node_type = n.tag + + my_tmpl_dir.append(name) + + if debug: + print("Name of the node: {};\n Created directory: ".format(name), end="") + os.makedirs(make_path(my_tmpl_dir), exist_ok=True) + + props = get_properties(props_elem) + + if node_type == "node": + if debug: + print("Processing node {}".format(name)) + + nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def") + if not os.path.exists(nodedef_path): + with open(nodedef_path, "w") as f: + f.write(make_node_def(props, command)) + else: + # Something has already generated this file + pass + + if children is not None: + inner_nodes = children.iterfind("*") + for inner_n in inner_nodes: + process_node(inner_n, my_tmpl_dir) + if node_type == "tagNode": + if debug: + print("Processing tag node {}".format(name)) + + os.makedirs(make_path(my_tmpl_dir), exist_ok=True) + + nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def") + if not os.path.exists(nodedef_path): + with open(nodedef_path, "w") as f: + f.write('help: {0}\n'.format(props['help'])) + else: + # Something has already generated this file + pass + + # Create the inner node.tag part + my_tmpl_dir.append("node.tag") + os.makedirs(make_path(my_tmpl_dir), exist_ok=True) + if debug: + print("Created path for the tagNode: {}".format(make_path(my_tmpl_dir)), end="") + + # Not sure if we want partially defined tag nodes, write the file unconditionally + with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f: + f.write(make_node_def(props, command)) + + if children is not None: + inner_nodes = children.iterfind("*") + for inner_n in inner_nodes: + process_node(inner_n, my_tmpl_dir) + else: + # This is a leaf node + if debug: + print("Processing leaf node {}".format(name)) + + with open(os.path.join(make_path(my_tmpl_dir), "node.def"), "w") as f: + f.write(make_node_def(props, command)) + + +root = xml.getroot() + +nodes = root.iterfind("*") +for n in nodes: + process_node(n, [output_dir]) diff --git a/scripts/build-command-templates b/scripts/build-command-templates new file mode 100755 index 000000000..457adbec2 --- /dev/null +++ b/scripts/build-command-templates @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# +# build-command-template: converts new style command definitions in XML +# to the old style (bunch of dirs and node.def's) command templates +# +# Copyright (C) 2017 VyOS maintainers <maintainers@vyos.net> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + +import sys +import os +import argparse +import copy +import functools + +from lxml import etree as ET + +# Defaults + +#validator_dir = "/usr/libexec/vyos/validators" +validator_dir = "${vyos_validators_dir}" +default_constraint_err_msg = "Invalid value" + + +## Get arguments + +parser = argparse.ArgumentParser(description='Converts new-style XML interface definitions to old-style command templates') +parser.add_argument('--debug', help='Enable debug information output', action='store_true') +parser.add_argument('INPUT_FILE', type=str, help="XML interface definition file") +parser.add_argument('SCHEMA_FILE', type=str, help="RelaxNG schema file") +parser.add_argument('OUTPUT_DIR', type=str, help="Output directory") + +args = parser.parse_args() + +input_file = args.INPUT_FILE +schema_file = args.SCHEMA_FILE +output_dir = args.OUTPUT_DIR +debug = args.debug + +#debug = True + +## Load and validate the inputs + +try: + xml = ET.parse(input_file) +except Exception as e: + print("Failed to load interface definition file {0}".format(input_file)) + print(e) + sys.exit(1) + +try: + relaxng_xml = ET.parse(schema_file) + validator = ET.RelaxNG(relaxng_xml) + + if not validator.validate(xml): + print(validator.error_log) + print("Interface definition file {0} does not match the schema!".format(input_file)) + sys.exit(1) +except Exception as e: + print("Failed to load the XML schema {0}".format(schema_file)) + print(e) + sys.exit(1) + +if not os.access(output_dir, os.W_OK): + print("The output directory {0} is not writeable".format(output_dir)) + sys.exit(1) + +## If we got this far, everything must be ok and we can convert the file + +def make_path(l): + path = functools.reduce(os.path.join, l) + if debug: + print(path) + return path + +def get_properties(p): + props = {} + + if p is None: + return props + + # Get the help string + try: + props["help"] = p.find("help").text + except: + pass + + # Get value help strings + try: + vhe = p.findall("valueHelp") + vh = [] + for v in vhe: + vh.append( (v.find("format").text, v.find("description").text) ) + props["val_help"] = vh + except: + props["val_help"] = [] + + # Get the constraint statements + error_msg = default_constraint_err_msg + # Get the error message if it's there + try: + error_msg = p.find("constraintErrorMessage").text + except: + pass + + vce = p.find("constraint") + vc = [] + if vce is not None: + # The old backend doesn't support multiple validators in OR mode + # so we emulate it + + regexes = [] + regex_elements = vce.findall("regex") + if regex_elements is not None: + regexes = list(map(lambda e: e.text.strip().replace('\\','\\\\'), regex_elements)) + if "" in regexes: + print("Warning: empty regex, node will be accepting any value") + + validator_elements = vce.findall("validator") + validators = [] + if validator_elements is not None: + for v in validator_elements: + v_name = os.path.join(validator_dir, v.get("name")) + + # XXX: lxml returns None for empty arguments + v_argument = None + try: + v_argument = v.get("argument") + except: + pass + if v_argument is None: + v_argument = "" + + validators.append("{0} {1}".format(v_name, v_argument)) + + + regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes)) + validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators)) + validator_script = '${vyos_libexec_dir}/validate-value' + validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg) + + props["constraint"] = validator_string + + # Get the completion help strings + try: + che = p.findall("completionHelp") + ch = "" + for c in che: + scripts = c.findall("script") + paths = c.findall("path") + lists = c.findall("list") + + # Current backend doesn't support multiple allowed: tags + # so we get to emulate it + comp_exprs = [] + for i in lists: + comp_exprs.append("echo \"{0}\"".format(i.text)) + for i in paths: + comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text)) + for i in scripts: + comp_exprs.append("sh -c \"{0}\"".format(i.text)) + comp_help = " && ".join(comp_exprs) + props["comp_help"] = comp_help + except: + props["comp_help"] = [] + + # Get priority + try: + props["priority"] = p.find("priority").text + except: + pass + + # Get "multi" + if p.find("multi") is not None: + props["multi"] = True + + # Get "valueless" + if p.find("valueless") is not None: + props["valueless"] = True + + return props + +def make_node_def(props): + # XXX: replace with a template processor if it grows + # out of control + + node_def = "" + + if "tag" in props: + node_def += "tag:\n" + + if "multi" in props: + node_def += "multi:\n" + + if "type" in props: + # Will always be txt in practice if it's set + node_def += "type: {0}\n".format(props["type"]) + + if "priority" in props: + node_def += "priority: {0}\n".format(props["priority"]) + + if "help" in props: + node_def += "help: {0}\n".format(props["help"]) + + if "val_help" in props: + for v in props["val_help"]: + node_def += "val_help: {0}; {1}\n".format(v[0], v[1]) + + if "comp_help" in props: + node_def += "allowed: {0}\n".format(props["comp_help"]) + + if "constraint" in props: + node_def += "syntax:expression: {0}\n".format(props["constraint"]) + + if "owner" in props: + if "tag" in props: + node_def += "end: sudo sh -c \"VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"]) + else: + node_def += "end: sudo sh -c \"{0}\"\n".format(props["owner"]) + + if debug: + print("The contents of the node.def file:\n", node_def) + + return node_def + +def process_node(n, tmpl_dir): + # Avoid mangling the path from the outer call + my_tmpl_dir = copy.copy(tmpl_dir) + + props_elem = n.find("properties") + children = n.find("children") + + name = n.get("name") + owner = n.get("owner") + node_type = n.tag + + my_tmpl_dir.append(name) + + if debug: + print("Name of the node: {0}. Created directory: {1}\n".format(name, "/".join(my_tmpl_dir)), end="") + os.makedirs(make_path(my_tmpl_dir), exist_ok=True) + + props = get_properties(props_elem) + if owner: + props["owner"] = owner + # Type should not be set for non-tag, non-leaf nodes + # For non-valueless leaf nodes, set the type to txt: to make them have some type, + # actual value validation is handled by constraints translated to syntax:expression: + if node_type != "node": + if "valueless" not in props.keys(): + props["type"] = "txt" + if node_type == "tagNode": + props["tag"] = "True" + + if node_type != "leafNode": + if "multi" in props: + raise ValueError("<multi/> tag is only allowed in <leafNode>") + if "valueless" in props: + raise ValueError("<valueless/> is only allowed in <leafNode>") + + nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def") + if not os.path.exists(nodedef_path): + with open(nodedef_path, "w") as f: + f.write(make_node_def(props)) + else: + # Something has already generated that file + pass + + + if node_type == "node": + inner_nodes = children.iterfind("*") + for inner_n in inner_nodes: + process_node(inner_n, my_tmpl_dir) + if node_type == "tagNode": + my_tmpl_dir.append("node.tag") + if debug: + print("Created path for the tagNode:", end="") + os.makedirs(make_path(my_tmpl_dir), exist_ok=True) + inner_nodes = children.iterfind("*") + for inner_n in inner_nodes: + process_node(inner_n, my_tmpl_dir) + else: + # This is a leaf node + pass + + +root = xml.getroot() + +nodes = root.iterfind("*") +for n in nodes: + if n.tag == "syntaxVersion": + continue + process_node(n, [output_dir]) diff --git a/scripts/build-component-versions b/scripts/build-component-versions new file mode 100755 index 000000000..5362dbdd4 --- /dev/null +++ b/scripts/build-component-versions @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import sys +import os +import argparse +import json + +from lxml import etree as ET + +parser = argparse.ArgumentParser() +parser.add_argument('INPUT_DIR', type=str, + help="Directory containing XML interface definition files") +parser.add_argument('OUTPUT_DIR', type=str, + help="Output directory for JSON file") + +args = parser.parse_args() + +input_dir = args.INPUT_DIR +output_dir = args.OUTPUT_DIR + +version_dict = {} + +for filename in os.listdir(input_dir): + filepath = os.path.join(input_dir, filename) + print(filepath) + try: + xml = ET.parse(filepath) + except Exception as e: + print("Failed to load interface definition file {0}".format(filename)) + print(e) + sys.exit(1) + + root = xml.getroot() + version_data = root.iterfind("syntaxVersion") + for ver in version_data: + component = ver.get("component") + version = int(ver.get("version")) + + v = version_dict.get(component) + if v is None: + version_dict[component] = version + elif version > v: + version_dict[component] = version + +out_file = os.path.join(output_dir, 'component-versions.json') +with open(out_file, 'w') as f: + json.dump(version_dict, f, indent=4, sort_keys=True) diff --git a/scripts/import-conf-mode-commands b/scripts/import-conf-mode-commands new file mode 100755 index 000000000..996b31c9c --- /dev/null +++ b/scripts/import-conf-mode-commands @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +# +# build-command-template: converts old style commands definitions to XML +# +# Copyright (C) 2019 VyOS maintainers <maintainers@vyos.net> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 +# USA + + +import os +import re +import sys + +from lxml import etree + + +# Node types +NODE = 0 +LEAF_NODE = 1 +TAG_NODE = 2 + +def parse_command_data(t): + regs = { + 'help': r'\bhelp:(.*)(?:\n|$)', + 'priority': r'\bpriority:(.*)(?:\n|$)', + 'type': r'\btype:(.*)(?:\n|$)', + 'syntax_expression_var': r'\bsyntax:expression: \$VAR\(\@\) in (.*)' + } + + data = {'multi': False, 'help': ""} + + for r in regs: + try: + data[r] = re.search(regs[r], t).group(1).strip() + except: + data[r] = None + + # val_help is special: there can be multiple instances + val_help_strings = re.findall(r'\bval_help:(.*)(?:\n|$)', t) + val_help = [] + for v in val_help_strings: + try: + fmt, msg = re.match(r'\s*(.*)\s*;\s*(.*)\s*(?:\n|$)', v).groups() + except: + fmt = "<text>" + msg = v + val_help.append((fmt, msg)) + data['val_help'] = val_help + + # multi is on/off + if re.match(r'\bmulti:', t): + data['multi'] = True + + return(data) + +def walk(tree, base_path, name): + path = os.path.join(base_path, name) + + contents = os.listdir(path) + + # Determine node type and create XML element for the node + # Tag node dirs will always have 'node.tag' subdir and 'node.def' file + # Leaf node dirs have nothing but a 'node.def' file + # Everything that doesn't match either of these patterns is a normal node + if 'node.tag' in contents: + print("Creating a tag node from {0}".format(path)) + elem = etree.Element('tagNode') + node_type = TAG_NODE + elif contents == ['node.def']: + print("Creating a leaf node from {0}".format(path)) + elem = etree.Element('leafNode') + node_type = LEAF_NODE + else: + print("Creating a node from {0}".format(path)) + elem = etree.Element('node') + node_type = NODE + + # Read and parse the command definition data (the 'node.def' file) + with open(os.path.join(path, 'node.def'), 'r') as f: + node_def = f.read() + data = parse_command_data(node_def) + + # Import the data into the properties element + props_elem = etree.Element('properties') + + if data['priority']: + # Priority values sometimes come with comments that explain the value choice + try: + prio, prio_comment = re.match(r'\s*(\d+)\s*#(.*)', data['priority']).groups() + except: + prio = data['priority'].strip() + prio_comment = None + prio_elem = etree.Element('priority') + prio_elem.text = prio + props_elem.append(prio_elem) + if prio_comment: + prio_comment_elem = etree.Comment(prio_comment) + props_elem.append(prio_comment_elem) + + if data['multi']: + multi_elem = etree.Element('multi') + props_elem.append(multi_elem) + + if data['help']: + help_elem = etree.Element('help') + help_elem.text = data['help'] + props_elem.append(help_elem) + + # For leaf nodes, absense of a type: tag means they take no values + # For any other nodes, it doesn't mean anything + if not data['type'] and (node_type == LEAF_NODE): + valueless = etree.Element('valueless') + props_elem.append(valueless) + + # There can be only one constraint element in the definition + # Create it now, we'll modify it in the next two cases, then append + constraint_elem = etree.Element('constraint') + has_constraint = False + + # Add regexp field for multiple options + if data['syntax_expression_var']: + regex = etree.Element('regex') + constraint_error=etree.Element('constraintErrorMessage') + values = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(1) + message = re.search(r'(.+) ; (.+)', data['syntax_expression_var']).group(2) + values = re.findall(r'\"(.+?)\"', values) + regex.text = '|'.join(values) + constraint_error.text = re.sub('\".*?VAR.*?\"', '', message) + constraint_error.text = re.sub(r'[\"|\\]', '', message) + constraint_elem.append(regex) + props_elem.append(constraint_elem) + props_elem.append(constraint_error) + + if data['val_help']: + for vh in data['val_help']: + vh_elem = etree.Element('valueHelp') + + vh_fmt_elem = etree.Element('format') + # Many commands use special "u32:<start>-<end>" format for ranges + if re.match(r'u32:', vh[0]): + vh_fmt = re.match(r'u32:(.*)', vh[0]).group(1).strip() + + # If valid range of values is specified in val_help, we can automatically + # create a constraint for it + # Extracting it from syntax:expression: would be much more complicated + vh_validator = etree.Element('validator') + vh_validator.set("name", "numeric") + vh_validator.set("argument", "--range {0}".format(vh_fmt)) + constraint_elem.append(vh_validator) + has_constraint = True + else: + vh_fmt = vh[0] + vh_fmt_elem.text = vh_fmt + + vh_help_elem = etree.Element('description') + vh_help_elem.text = vh[1] + + vh_elem.append(vh_fmt_elem) + vh_elem.append(vh_help_elem) + props_elem.append(vh_elem) + + # Translate the "type:" to the new validator system + if data['type']: + t = data['type'] + if t == 'txt': + # Can't infer anything from the generic "txt" type + pass + else: + validator = etree.Element('validator') + if t == 'u32': + validator.set('name', 'numeric') + validator.set('argument', '--non-negative') + elif t == 'ipv4': + validator.set('name', 'ipv4-address') + elif t == 'ipv4net': + validator.set('name', 'ipv4-prefix') + elif t == 'ipv6': + validator.set('name', 'ipv6-address') + elif t == 'ipv6net': + validator.set('name', 'ipv6-prefix') + elif t == 'macaddr': + validator.set('name', 'mac-address') + else: + print("Warning: unsupported type \'{0}\'".format(t)) + validator = None + + if (validator is not None) and (not has_constraint): + # If has_constraint is true, it means a more specific validator + # was already extracted from another option + constraint_elem.append(validator) + has_constraint = True + + if has_constraint: + props_elem.append(constraint_elem) + + elem.append(props_elem) + + elem.set("name", name) + + if node_type != LEAF_NODE: + children = etree.Element('children') + + # Create the next level dir path, + # accounting for the "virtual" node.tag subdir for tag nodes + next_level = path + if node_type == TAG_NODE: + next_level = os.path.join(path, 'node.tag') + + # Walk the subdirs of the next level + for d in os.listdir(next_level): + dp = os.path.join(next_level, d) + if os.path.isdir(dp): + walk(children, next_level, d) + + elem.append(children) + + tree.append(elem) + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Usage: {0} <base path>".format(sys.argv[0])) + sys.exit(1) + else: + base_path = sys.argv[1] + + root = etree.Element('interfaceDefinition') + contents = os.listdir(base_path) + elem = etree.Element('node') + elem.set('name', os.path.basename(base_path)) + children = etree.Element('children') + + for c in contents: + path = os.path.join(base_path, c) + if os.path.isdir(path): + walk(children, base_path, c) + + elem.append(children) + root.append(elem) + + xml_data = etree.tostring(root, pretty_print=True).decode() + with open('output.xml', 'w') as f: + f.write(xml_data) diff --git a/bin/vyos-smoketest b/smoketest/bin/vyos-smoketest index cb039db42..cb039db42 100755 --- a/bin/vyos-smoketest +++ b/smoketest/bin/vyos-smoketest diff --git a/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 14ec7e137..14ec7e137 100644 --- a/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py diff --git a/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index e3d3b25ee..e3d3b25ee 100755 --- a/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py diff --git a/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index bc0bb69c6..bc0bb69c6 100755 --- a/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py diff --git a/scripts/cli/test_interfaces_dummy.py b/smoketest/scripts/cli/test_interfaces_dummy.py index 01942fc89..01942fc89 100755 --- a/scripts/cli/test_interfaces_dummy.py +++ b/smoketest/scripts/cli/test_interfaces_dummy.py diff --git a/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 761ec7506..761ec7506 100755 --- a/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py diff --git a/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py index f84a55f86..f84a55f86 100755 --- a/scripts/cli/test_interfaces_geneve.py +++ b/smoketest/scripts/cli/test_interfaces_geneve.py diff --git a/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py index d8655d157..d8655d157 100755 --- a/scripts/cli/test_interfaces_l2tpv3.py +++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py diff --git a/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py index ba428b5d3..ba428b5d3 100755 --- a/scripts/cli/test_interfaces_loopback.py +++ b/smoketest/scripts/cli/test_interfaces_loopback.py diff --git a/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index 0f1b6486d..0f1b6486d 100755 --- a/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py diff --git a/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 822f05de6..822f05de6 100755 --- a/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py diff --git a/scripts/cli/test_interfaces_pseudo_ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py index bc2e6e7eb..bc2e6e7eb 100755 --- a/scripts/cli/test_interfaces_pseudo_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py diff --git a/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index 7611ffe26..7611ffe26 100755 --- a/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py diff --git a/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 2628e0285..2628e0285 100755 --- a/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py diff --git a/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index 0c32a4696..0c32a4696 100755 --- a/scripts/cli/test_interfaces_wireguard.py +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py diff --git a/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index fae233244..fae233244 100755 --- a/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py diff --git a/scripts/cli/test_interfaces_wirelessmodem.py b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py index 40cd03b93..40cd03b93 100755 --- a/scripts/cli/test_interfaces_wirelessmodem.py +++ b/smoketest/scripts/cli/test_interfaces_wirelessmodem.py diff --git a/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 416810e40..416810e40 100755 --- a/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py diff --git a/scripts/cli/test_service_bcast-relay.py b/smoketest/scripts/cli/test_service_bcast-relay.py index fe4531c3b..fe4531c3b 100755 --- a/scripts/cli/test_service_bcast-relay.py +++ b/smoketest/scripts/cli/test_service_bcast-relay.py diff --git a/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index be52360ed..be52360ed 100755 --- a/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py diff --git a/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py index 18900b6d2..18900b6d2 100755 --- a/scripts/cli/test_service_mdns-repeater.py +++ b/smoketest/scripts/cli/test_service_mdns-repeater.py diff --git a/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 901ca792d..901ca792d 100755 --- a/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py diff --git a/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index ec2110c8a..ec2110c8a 100755 --- a/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py diff --git a/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index fb5f5393f..fb5f5393f 100755 --- a/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py diff --git a/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index 3ee498f3d..3ee498f3d 100755 --- a/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py diff --git a/scripts/cli/test_system_lcd.py b/smoketest/scripts/cli/test_system_lcd.py index 931a91c53..931a91c53 100755 --- a/scripts/cli/test_system_lcd.py +++ b/smoketest/scripts/cli/test_system_lcd.py diff --git a/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index 3c4b1fa28..3c4b1fa28 100755 --- a/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py diff --git a/scripts/cli/test_system_nameserver.py b/smoketest/scripts/cli/test_system_nameserver.py index 9040be072..9040be072 100755 --- a/scripts/cli/test_system_nameserver.py +++ b/smoketest/scripts/cli/test_system_nameserver.py diff --git a/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py index 856a28916..856a28916 100755 --- a/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_system_ntp.py diff --git a/scripts/cli/test_vpn_anyconnect.py b/smoketest/scripts/cli/test_vpn_anyconnect.py index dd8ab1609..dd8ab1609 100755 --- a/scripts/cli/test_vpn_anyconnect.py +++ b/smoketest/scripts/cli/test_vpn_anyconnect.py diff --git a/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index efa095b30..efa095b30 100755 --- a/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py diff --git a/scripts/system/test_module_load.py b/smoketest/scripts/system/test_module_load.py index 59c3e0b6a..59c3e0b6a 100755 --- a/scripts/system/test_module_load.py +++ b/smoketest/scripts/system/test_module_load.py diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..1258da817 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,21 @@ +sonar.projectKey=vyos:vyos-1x +sonar.projectName=vyos-1x +sonar.projectVersion=1.2.0 +sonar.organization=vyos + +sonar.sources=src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators +sonar.language=py +sonar.sourceEncoding=UTF-8 + +sonar.links.homepage=https://github.com/vyos/vyos-1x +sonar.links.ci=https://ci.vyos.net/job/vyos-1x/ +sonar.links.scm=https://github.com/vyos/vyos-1x +sonar.links.issue=https://phabricator.vyos.net/ + +sonar.host.url=https://sonarcloud.io + +sonar.python.pylint=/usr/local/bin/pylint +sonar.python.pylint_config=.pylintrc +sonar.python.pylint.reportPath=pylint-report.txt +sonar.python.xunit.reportPath=nosetests.xml +sonar.python.coverage.reportPath=coverage.xml diff --git a/sphinx/Makefile b/sphinx/Makefile new file mode 100644 index 000000000..1e344639e --- /dev/null +++ b/sphinx/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/VyOS.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/VyOS.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/VyOS" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/VyOS" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/sphinx/source/conf.py b/sphinx/source/conf.py new file mode 100644 index 000000000..3447259b3 --- /dev/null +++ b/sphinx/source/conf.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# VyOS documentation build configuration file, created by +# sphinx-quickstart on Wed Jun 20 01:14:27 2018. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'VyOS' +copyright = '2018, VyOS maintainers and contributors' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.2.0' +# The full version, including alpha/beta/rc tags. +release = '1.2.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'VyOSdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'VyOS.tex', 'VyOS Documentation', + 'VyOS maintainers and contributors', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'vyos', 'VyOS Documentation', + ['VyOS maintainers and contributors'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'VyOS', 'VyOS Documentation', + 'VyOS maintainers and contributors', 'VyOS', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/sphinx/source/index.rst b/sphinx/source/index.rst new file mode 100644 index 000000000..c31cac4e5 --- /dev/null +++ b/sphinx/source/index.rst @@ -0,0 +1,22 @@ +.. VyOS documentation master file, created by + sphinx-quickstart on Wed Jun 20 01:14:27 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to VyOS's documentation! +================================ + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/src/completion/list_disks.py b/src/completion/list_disks.py new file mode 100755 index 000000000..ff1135e23 --- /dev/null +++ b/src/completion/list_disks.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Completion script used by show disks to collect physical disk + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("-e", "--exclude", type=str, help="Exclude specified device from the result list") +args = parser.parse_args() + +disks = set() +with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + +if args.exclude: + disks.remove(args.exclude) + +for disk in disks: + print(disk) diff --git a/src/completion/list_dumpable_interfaces.py b/src/completion/list_dumpable_interfaces.py new file mode 100755 index 000000000..101c92fbe --- /dev/null +++ b/src/completion/list_dumpable_interfaces.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 + +# Extract the list of interfaces available for traffic dumps from tcpdump -D + +import re + +from vyos.util import cmd + +if __name__ == '__main__': + out = cmd('/usr/sbin/tcpdump -D').split('\n') + intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out)) + print(intfs) diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py new file mode 100755 index 000000000..e27281433 --- /dev/null +++ b/src/completion/list_interfaces.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 + +import sys +import argparse +from vyos.ifconfig import Section + + +def matching(feature): + for section in Section.feature(feature): + for intf in Section.interfaces(section): + yield intf + + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-t", "--type", type=str, help="List interfaces of specific type") +group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces") +group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces") +group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces") + +args = parser.parse_args() + +if args.type: + try: + interfaces = Section.interfaces(args.type) + print(" ".join(interfaces)) + except ValueError as e: + print(e, file=sys.stderr) + print("") + +elif args.broadcast: + print(" ".join(matching("broadcast"))) + +elif args.bridgeable: + print(" ".join(matching("bridgeable"))) + +elif args.bondable: + # we need to filter out VLAN interfaces identified by a dot (.) in their name + print(" ".join([intf for intf in matching("bondable") if '.' not in intf])) + +else: + print(" ".join(Section.interfaces())) diff --git a/src/completion/list_ipoe.py b/src/completion/list_ipoe.py new file mode 100755 index 000000000..c386b46a2 --- /dev/null +++ b/src/completion/list_ipoe.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 + +import argparse +from vyos.util import popen + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--selector', help='Selector: username|ifname|sid', required=True) + args = parser.parse_args() + + output, err = popen("accel-cmd -p 2002 show sessions {0}".format(args.selector)) + if not err: + res = output.split("\r\n") + # Delete header from list + del res[:2] + print(' '.join(res)) diff --git a/src/completion/list_local.py b/src/completion/list_local.py new file mode 100755 index 000000000..40cc95f1e --- /dev/null +++ b/src/completion/list_local.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import json +import argparse + +from vyos.util import cmd + +# [{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00","addr_info":[{"family":"inet","local":"127.0.0.1","prefixlen":8,"scope":"host","label":"lo","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"::1","prefixlen":128,"scope":"host","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":2,"ifname":"eth0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:fa:12:53","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet","local":"10.0.2.15","prefixlen":24,"broadcast":"10.0.2.255","scope":"global","label":"eth0","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"fe80::a00:27ff:fefa:1253","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":3,"ifname":"eth1","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:0d:25:dc","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe0d:25dc","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":4,"ifname":"eth2","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:68:d0:b1","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe68:d0b1","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":5,"ifname":"eth3","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:f0:17:c5","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fef0:17c5","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]}] + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + + out = cmd('ip -j address show') + data = json.loads(out) + + + interfaces = [] + for interface in data: + if not 'addr_info' in interface: + continue + interfaces.extend(interface['addr_info']) + + print(' '.join([interface['local'] for interface in interfaces if 'local' in interface])) diff --git a/src/completion/list_ntp_servers.sh b/src/completion/list_ntp_servers.sh new file mode 100755 index 000000000..d0977fbd6 --- /dev/null +++ b/src/completion/list_ntp_servers.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +# Completion script used to select specific NTP server +/bin/cli-shell-api -- listEffectiveNodes system ntp server | sed "s/'//g" diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py new file mode 100755 index 000000000..177ac90c9 --- /dev/null +++ b/src/completion/list_openvpn_clients.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import argparse + +from vyos.ifconfig import Section + +def get_client_from_interface(interface): + clients = [] + with open('/opt/vyatta/etc/openvpn/status/' + interface + '.status', 'r') as f: + dump = False + for line in f: + if line.startswith("Common Name,"): + dump = True + continue + if line.startswith("ROUTING TABLE"): + dump = False + continue + if dump: + # client entry in this file looks like + # client1,172.18.202.10:47495,2957,2851,Sat Aug 17 00:07:11 2019 + # we are only interested in the client name 'client1' + clients.append(line.split(',')[0]) + + return clients + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--interface", type=str, help="List connected clients per interface") + parser.add_argument("-a", "--all", action='store_true', help="List all connected OpenVPN clients") + args = parser.parse_args() + + clients = [] + + if args.interface: + clients = get_client_from_interface(args.interface) + elif args.all: + for interface in Section.interfaces("openvpn"): + clients += get_client_from_interface(interface) + + print(" ".join(clients)) + diff --git a/src/completion/list_raidset.sh b/src/completion/list_raidset.sh new file mode 100755 index 000000000..9ff3523aa --- /dev/null +++ b/src/completion/list_raidset.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo -n `cat /proc/partitions | grep md | awk '{ print $4 }'` diff --git a/src/completion/list_wireless_phys.sh b/src/completion/list_wireless_phys.sh new file mode 100755 index 000000000..70b8d1ff9 --- /dev/null +++ b/src/completion/list_wireless_phys.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +if [ -d /sys/class/ieee80211 ]; then + ls -x /sys/class/ieee80211 +fi diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py new file mode 100755 index 000000000..aac07bd80 --- /dev/null +++ b/src/conf_mode/arp.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re +import syslog as sl + +from vyos.config import Config +from vyos.util import call +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +arp_cmd = '/usr/sbin/arp' + +def get_config(): + c = Config() + if not c.exists('protocols static arp'): + return None + + c.set_level('protocols static') + config_data = {} + + for ip_addr in c.list_nodes('arp'): + config_data.update( + { + ip_addr : c.return_value('arp ' + ip_addr + ' hwaddr') + } + ) + + return config_data + +def generate(c): + c_eff = Config() + c_eff.set_level('protocols static') + c_eff_cnf = {} + for ip_addr in c_eff.list_effective_nodes('arp'): + c_eff_cnf.update( + { + ip_addr : c_eff.return_effective_value('arp ' + ip_addr + ' hwaddr') + } + ) + + config_data = { + 'remove' : [], + 'update' : {} + } + ### removal + if c == None: + for ip_addr in c_eff_cnf: + config_data['remove'].append(ip_addr) + else: + for ip_addr in c_eff_cnf: + if not ip_addr in c or c[ip_addr] == None: + config_data['remove'].append(ip_addr) + + ### add/update + if c != None: + for ip_addr in c: + if not ip_addr in c_eff_cnf: + config_data['update'][ip_addr] = c[ip_addr] + if ip_addr in c_eff_cnf: + if c[ip_addr] != c_eff_cnf[ip_addr] and c[ip_addr] != None: + config_data['update'][ip_addr] = c[ip_addr] + + return config_data + +def apply(c): + for ip_addr in c['remove']: + sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr) + call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1') + + for ip_addr in c['update']: + sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr]) + updated = c['update'][ip_addr] + call(f'{arp_cmd} -s {ip_addr} {updated}') + + +if __name__ == '__main__': + try: + c = get_config() + ## syntax verification is done via cli + config = generate(c) + apply(config) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py new file mode 100755 index 000000000..a3e141a00 --- /dev/null +++ b/src/conf_mode/bcast_relay.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from glob import glob +from netifaces import interfaces +from sys import exit + +from vyos.config import Config +from vyos.util import call +from vyos.template import render +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file_base = r'/etc/default/udp-broadcast-relay' + +def get_config(): + conf = Config() + base = ['service', 'broadcast-relay'] + + relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return relay + +def verify(relay): + if not relay or 'disabled' in relay: + return None + + for instance, config in relay.get('id', {}).items(): + # we don't have to check this instance when it's disabled + if 'disabled' in config: + continue + + # we certainly require a UDP port to listen to + if 'port' not in config: + raise ConfigError(f'Port number mandatory for udp broadcast relay "{instance}"') + + # if only oone interface is given it's a string -> move to list + if isinstance(config.get('interface', []), str): + config['interface'] = [ config['interface'] ] + # Relaying data without two interface is kinda senseless ... + if len(config.get('interface', [])) < 2: + raise ConfigError('At least two interfaces are required for udp broadcast relay "{instance}"') + + for interface in config.get('interface', []): + if interface not in interfaces(): + raise ConfigError('Interface "{interface}" does not exist!') + + return None + +def generate(relay): + if not relay or 'disabled' in relay: + return None + + for config in glob(config_file_base + '*'): + os.remove(config) + + for instance, config in relay.get('id').items(): + # we don't have to check this instance when it's disabled + if 'disabled' in config: + continue + + config['instance'] = instance + render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl', config) + + return None + +def apply(relay): + # first stop all running services + call('systemctl stop udp-broadcast-relay@*.service') + + if not relay or 'disable' in relay: + return None + + # start only required service instances + for instance, config in relay.get('id').items(): + # we don't have to check this instance when it's disabled + if 'disabled' in config: + continue + + call(f'systemctl start udp-broadcast-relay@{instance}.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py new file mode 100755 index 000000000..f093a005e --- /dev/null +++ b/src/conf_mode/dhcp_relay.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +config_file = r'/run/dhcp-relay/dhcp.conf' + +default_config_data = { + 'interface': [], + 'server': [], + 'options': [], + 'hop_count': '10', + 'relay_agent_packets': 'forward' +} + +def get_config(): + relay = default_config_data + conf = Config() + if not conf.exists(['service', 'dhcp-relay']): + return None + else: + conf.set_level(['service', 'dhcp-relay']) + + # Network interfaces to listen on + if conf.exists(['interface']): + relay['interface'] = conf.return_values(['interface']) + + # Servers equal to the address of the DHCP server(s) + if conf.exists(['server']): + relay['server'] = conf.return_values(['server']) + + conf.set_level(['service', 'dhcp-relay', 'relay-options']) + + if conf.exists(['hop-count']): + count = '-c ' + conf.return_value(['hop-count']) + relay['options'].append(count) + + # Specify the maximum packet size to send to a DHCPv4/BOOTP server. + # This might be done to allow sufficient space for addition of relay agent + # options while still fitting into the Ethernet MTU size. + # + # Available in DHCPv4 mode only: + if conf.exists(['max-size']): + size = '-A ' + conf.return_value(['max-size']) + relay['options'].append(size) + + # Control the handling of incoming DHCPv4 packets which already contain + # relay agent options. If such a packet does not have giaddr set in its + # header, the DHCP standard requires that the packet be discarded. However, + # if giaddr is set, the relay agent may handle the situation in four ways: + # It may append its own set of relay options to the packet, leaving the + # supplied option field intact; it may replace the existing agent option + # field; it may forward the packet unchanged; or, it may discard it. + # + # Available in DHCPv4 mode only: + if conf.exists(['relay-agents-packets']): + pkt = '-a -m ' + conf.return_value(['relay-agents-packets']) + relay['options'].append(pkt) + + return relay + +def verify(relay): + # bail out early - looks like removal from running config + if relay is None: + return None + + if 'lo' in relay['interface']: + raise ConfigError('DHCP relay does not support the loopback interface.') + + if len(relay['server']) == 0: + raise ConfigError('No DHCP relay server(s) configured.\n' \ + 'At least one DHCP relay server required.') + + return None + +def generate(relay): + # bail out early - looks like removal from running config + if not relay: + return None + + render(config_file, 'dhcp-relay/config.tmpl', relay) + return None + +def apply(relay): + if relay: + call('systemctl restart isc-dhcp-relay.service') + else: + # DHCP relay support is removed in the commit + call('systemctl stop isc-dhcp-relay.service') + if os.path.exists(config_file): + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py new file mode 100755 index 000000000..0eaa14c5b --- /dev/null +++ b/src/conf_mode/dhcp_server.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from ipaddress import ip_address, ip_network +from socket import inet_ntoa +from struct import pack +from sys import exit + +from vyos.config import Config +from vyos.validate import is_subnet_connected +from vyos import ConfigError +from vyos.template import render +from vyos.util import call, chown + +from vyos import airbag +airbag.enable() + +config_file = r'/run/dhcp-server/dhcpd.conf' + +default_config_data = { + 'disabled': False, + 'ddns_enable': False, + 'global_parameters': [], + 'hostfile_update': False, + 'host_decl_name': False, + 'static_route': False, + 'wpad': False, + 'shared_network': [], +} + +def dhcp_slice_range(exclude_list, range_list): + """ + This function is intended to slice a DHCP range. What does it mean? + + Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100' + but want to exclude address '192.0.2.74' and '192.0.2.75'. We will + pass an input 'range_list' in the format: + [{'start' : '192.0.2.1', 'stop' : '192.0.2.100' }] + and we will receive an output list of: + [{'start' : '192.0.2.1' , 'stop' : '192.0.2.73' }, + {'start' : '192.0.2.76', 'stop' : '192.0.2.100' }] + The resulting list can then be used in turn to build the proper dhcpd + configuration file. + """ + output = [] + # exclude list must be sorted for this to work + exclude_list = sorted(exclude_list) + for ra in range_list: + range_start = ra['start'] + range_stop = ra['stop'] + range_last_exclude = '' + + for e in exclude_list: + if (ip_address(e) >= ip_address(range_start)) and \ + (ip_address(e) <= ip_address(range_stop)): + range_last_exclude = e + + for e in exclude_list: + if (ip_address(e) >= ip_address(range_start)) and \ + (ip_address(e) <= ip_address(range_stop)): + + # Build new IP address range ending one IP address before exclude address + r = { + 'start' : range_start, + 'stop' : str(ip_address(e) -1) + } + # On the next run our IP address range will start one address after the exclude address + range_start = str(ip_address(e) + 1) + + # on subsequent exclude addresses we can not + # append them to our output + if not (ip_address(r['start']) > ip_address(r['stop'])): + # Everything is fine, add range to result + output.append(r) + + # Take care of last IP address range spanning from the last exclude + # address (+1) to the end of the initial configured range + if ip_address(e) == ip_address(range_last_exclude): + r = { + 'start': str(ip_address(e) + 1), + 'stop': str(range_stop) + } + if not (ip_address(r['start']) > ip_address(r['stop'])): + output.append(r) + else: + # if we have no exclude in the whole range - we just take the range + # as it is + if not range_last_exclude: + if ra not in output: + output.append(ra) + + return output + +def dhcp_static_route(static_subnet, static_router): + # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server + # Option format is: + # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> + # where bytes with the value 0 are omitted. + net = ip_network(static_subnet) + # add netmask + string = str(net.prefixlen) + ',' + # add network bytes + if net.prefixlen: + width = net.prefixlen // 8 + if net.prefixlen % 8: + width += 1 + string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ',' + + # add router bytes + string += ','.join(static_router.split('.')) + + return string + +def get_config(): + dhcp = default_config_data + conf = Config() + if not conf.exists('service dhcp-server'): + return None + else: + conf.set_level('service dhcp-server') + + # check for global disable of DHCP service + if conf.exists('disable'): + dhcp['disabled'] = True + + # check for global dynamic DNS upste + if conf.exists('dynamic-dns-update'): + dhcp['ddns_enable'] = True + + # HACKS AND TRICKS + # + # check for global 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters from any user + if conf.exists('global-parameters'): + dhcp['global_parameters'] = conf.return_values('global-parameters') + + # check for global DHCP server updating /etc/host per lease + if conf.exists('hostfile-update'): + dhcp['hostfile_update'] = True + + # If enabled every host declaration within that scope, the name provided + # for the host declaration will be supplied to the client as its hostname. + if conf.exists('host-decl-name'): + dhcp['host_decl_name'] = True + + # check for multiple, shared networks served with DHCP addresses + if conf.exists('shared-network-name'): + for network in conf.list_nodes('shared-network-name'): + conf.set_level('service dhcp-server shared-network-name {0}'.format(network)) + config = { + 'name': network, + 'authoritative': False, + 'description': '', + 'disabled': False, + 'network_parameters': [], + 'subnet': [] + } + # check if DHCP server should be authoritative on this network + if conf.exists('authoritative'): + config['authoritative'] = True + + # A description for this given network + if conf.exists('description'): + config['description'] = conf.return_value('description') + + # If disabled, the shared-network configuration becomes inactive in + # the running DHCP server instance + if conf.exists('disable'): + config['disabled'] = True + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('shared-network-parameters'): + config['network_parameters'] = conf.return_values('shared-network-parameters') + + # check for multiple subnet configurations in a shared network + # config segment + if conf.exists('subnet'): + for net in conf.list_nodes('subnet'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + subnet = { + 'network': net, + 'address': str(ip_network(net).network_address), + 'netmask': str(ip_network(net).netmask), + 'bootfile_name': '', + 'bootfile_server': '', + 'client_prefix_length': '', + 'default_router': '', + 'rfc3442_default_router': '', + 'dns_server': [], + 'domain_name': '', + 'domain_search': [], + 'exclude': [], + 'failover_local_addr': '', + 'failover_name': '', + 'failover_peer_addr': '', + 'failover_status': '', + 'ip_forwarding': False, + 'lease': '86400', + 'ntp_server': [], + 'pop_server': [], + 'server_identifier': '', + 'smtp_server': [], + 'range': [], + 'static_mapping': [], + 'static_subnet': '', + 'static_router': '', + 'static_route': '', + 'subnet_parameters': [], + 'tftp_server': '', + 'time_offset': '', + 'time_server': [], + 'wins_server': [], + 'wpad_url': '' + } + + # Used to identify a bootstrap file + if conf.exists('bootfile-name'): + subnet['bootfile_name'] = conf.return_value('bootfile-name') + + # Specify host address of the server from which the initial boot file + # (specified above) is to be loaded. Should be a numeric IP address or + # domain name. + if conf.exists('bootfile-server'): + subnet['bootfile_server'] = conf.return_value('bootfile-server') + + # The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet + # mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet + # mask from the subnet declaration for the network on which an address is being assigned. + if conf.exists('client-prefix-length'): + # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python + host_bits = 32 - int(conf.return_value('client-prefix-length')) + subnet['client_prefix_length'] = inet_ntoa(pack('!I', (1 << 32) - (1 << host_bits))) + + # Default router IP address on the client's subnet + if conf.exists('default-router'): + subnet['default_router'] = conf.return_value('default-router') + subnet['rfc3442_default_router'] = dhcp_static_route("0.0.0.0/0", subnet['default_router']) + + # Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to + # the client. Servers should be listed in order of preference. + if conf.exists('dns-server'): + subnet['dns_server'] = conf.return_values('dns-server') + + # Option specifies the domain name that client should use when resolving hostnames + # via the Domain Name System. + if conf.exists('domain-name'): + subnet['domain_name'] = conf.return_value('domain-name') + + # The domain-search option specifies a 'search list' of Domain Names to be used + # by the client to locate not-fully-qualified domain names. + if conf.exists('domain-search'): + for domain in conf.return_values('domain-search'): + subnet['domain_search'].append('"' + domain + '"') + + # IP address (local) for failover peer to connect + if conf.exists('failover local-address'): + subnet['failover_local_addr'] = conf.return_value('failover local-address') + + # DHCP failover peer name + if conf.exists('failover name'): + subnet['failover_name'] = conf.return_value('failover name') + + # IP address (remote) of failover peer + if conf.exists('failover peer-address'): + subnet['failover_peer_addr'] = conf.return_value('failover peer-address') + + # DHCP failover peer status (primary|secondary) + if conf.exists('failover status'): + subnet['failover_status'] = conf.return_value('failover status') + + # Option specifies whether the client should configure its IP layer for packet + # forwarding + if conf.exists('ip-forwarding'): + subnet['ip_forwarding'] = True + + # Time should be the length in seconds that will be assigned to a lease if the + # client requesting the lease does not ask for a specific expiration time + if conf.exists('lease'): + subnet['lease'] = conf.return_value('lease') + + # Specifies a list of IP addresses indicating NTP (RFC 5905) servers available + # to the client. + if conf.exists('ntp-server'): + subnet['ntp_server'] = conf.return_values('ntp-server') + + # POP3 server option specifies a list of POP3 servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('pop-server'): + subnet['pop_server'] = conf.return_values('pop-server') + + # DHCP servers include this option in the DHCPOFFER in order to allow the client + # to distinguish between lease offers. DHCP clients use the contents of the + # 'server identifier' field as the destination address for any DHCP messages + # unicast to the DHCP server + if conf.exists('server-identifier'): + subnet['server_identifier'] = conf.return_value('server-identifier') + + # SMTP server option specifies a list of SMTP servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('smtp-server'): + subnet['smtp_server'] = conf.return_values('smtp-server') + + # For any subnet on which addresses will be assigned dynamically, there must be at + # least one range statement. The range statement gives the lowest and highest IP + # addresses in a range. All IP addresses in the range should be in the subnet in + # which the range statement is declared. + if conf.exists('range'): + for range in conf.list_nodes('range'): + range = { + 'start': conf.return_value('range {0} start'.format(range)), + 'stop': conf.return_value('range {0} stop'.format(range)) + } + subnet['range'].append(range) + + # IP address that needs to be excluded from DHCP lease range + if conf.exists('exclude'): + subnet['exclude'] = conf.return_values('exclude') + subnet['range'] = dhcp_slice_range(subnet['exclude'], subnet['range']) + + # Static DHCP leases + if conf.exists('static-mapping'): + addresses_for_exclude = [] + for mapping in conf.list_nodes('static-mapping'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + mapping = { + 'name': mapping, + 'disabled': False, + 'ip_address': '', + 'mac_address': '', + 'static_parameters': [] + } + + # This static lease is disabled + if conf.exists('disable'): + mapping['disabled'] = True + + # IP address used for this DHCP client + if conf.exists('ip-address'): + mapping['ip_address'] = conf.return_value('ip-address') + addresses_for_exclude.append(mapping['ip_address']) + + # MAC address of requesting DHCP client + if conf.exists('mac-address'): + mapping['mac_address'] = conf.return_value('mac-address') + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('static-mapping-parameters'): + mapping['static_parameters'] = conf.return_values('static-mapping-parameters') + + # append static-mapping configuration to subnet list + subnet['static_mapping'].append(mapping) + + # Now we have all static DHCP leases - we also need to slice them + # out of our DHCP ranges to avoid ISC DHCPd warnings as: + # dhcpd: Dynamic and static leases present for 192.0.2.51. + # dhcpd: Remove host declaration DMZ_PC1 or remove 192.0.2.51 + # dhcpd: from the dynamic address pool for DMZ + subnet['range'] = dhcp_slice_range(addresses_for_exclude, subnet['range']) + + # Reset config level to matching hirachy + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + + # This option specifies a list of static routes that the client should install in its routing + # cache. If multiple routes to the same destination are specified, they are listed in descending + # order of priority. + if conf.exists('static-route destination-subnet'): + subnet['static_subnet'] = conf.return_value('static-route destination-subnet') + # Required for global config section + dhcp['static_route'] = True + + if conf.exists('static-route router'): + subnet['static_router'] = conf.return_value('static-route router') + + if subnet['static_router'] and subnet['static_subnet']: + subnet['static_route'] = dhcp_static_route(subnet['static_subnet'], subnet['static_router']) + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('subnet-parameters'): + subnet['subnet_parameters'] = conf.return_values('subnet-parameters') + + # This option is used to identify a TFTP server and, if supported by the client, should have + # the same effect as the server-name declaration. BOOTP clients are unlikely to support this + # option. Some DHCP clients will support it, and others actually require it. + if conf.exists('tftp-server-name'): + subnet['tftp_server'] = conf.return_value('tftp-server-name') + + # The time-offset option specifies the offset of the client’s subnet in seconds from + # Coordinated Universal Time (UTC). + if conf.exists('time-offset'): + subnet['time_offset'] = conf.return_value('time-offset') + + # The time-server option specifies a list of RFC 868 time servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('time-server'): + subnet['time_server'] = conf.return_values('time-server') + + # The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers + # listed in order of preference. NetBIOS Name Service is currently more commonly referred to + # as WINS. WINS servers can be specified using the netbios-name-servers option. + if conf.exists('wins-server'): + subnet['wins_server'] = conf.return_values('wins-server') + + # URL for Web Proxy Autodiscovery Protocol + if conf.exists('wpad-url'): + subnet['wpad_url'] = conf.return_value('wpad-url') + # Required for global config section + dhcp['wpad'] = True + + # append subnet configuration to shared network subnet list + config['subnet'].append(subnet) + + # append shared network configuration to config dictionary + dhcp['shared_network'].append(config) + + return dhcp + +def verify(dhcp): + if not dhcp or dhcp['disabled']: + return None + + # If DHCP is enabled we need one share-network + if len(dhcp['shared_network']) == 0: + raise ConfigError('No DHCP shared networks configured.\n' \ + 'At least one DHCP shared network must be configured.') + + # Inspect shared-network/subnet + failover_names = [] + listen_ok = False + subnets = [] + + # A shared-network requires a subnet definition + for network in dhcp['shared_network']: + if len(network['subnet']) == 0: + raise ConfigError('No DHCP lease subnets configured for {0}. At least one\n' \ + 'lease subnet must be configured for each shared network.'.format(network['name'])) + + for subnet in network['subnet']: + # Subnet static route declaration requires destination and router + if subnet['static_subnet'] or subnet['static_router']: + if not (subnet['static_subnet'] and subnet['static_router']): + raise ConfigError('Please specify missing DHCP static-route parameter(s):\n' \ + 'destination-subnet | router') + + # Failover requires all 4 parameters set + if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']: + if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']): + raise ConfigError('Please specify missing DHCP failover parameter(s):\n' \ + 'local-address | peer-address | name | status') + + # Failover names must be uniquie + if subnet['failover_name'] in failover_names: + raise ConfigError('Failover names must be unique:\n' \ + '{0} has already been configured!'.format(subnet['failover_name'])) + else: + failover_names.append(subnet['failover_name']) + + # Failover requires start/stop ranges for pool + if (len(subnet['range']) == 0): + raise ConfigError('At least one start-stop range must be configured for {0}\n' \ + 'to set up DHCP failover!'.format(subnet['network'])) + + # Check if DHCP address range is inside configured subnet declaration + range_start = [] + range_stop = [] + for range in subnet['range']: + start = range['start'] + stop = range['stop'] + # DHCP stop IP required after start IP + if start and not stop: + raise ConfigError('DHCP range stop address for start {0} is not defined!'.format(start)) + + # Start address must be inside network + if not ip_address(start) in ip_network(subnet['network']): + raise ConfigError('DHCP range start address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) + + # Stop address must be inside network + if not ip_address(stop) in ip_network(subnet['network']): + raise ConfigError('DHCP range stop address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) + + # Stop address must be greater or equal to start address + if not ip_address(stop) >= ip_address(start): + raise ConfigError('DHCP range stop address {0} must be greater or equal\n' \ + 'to the range start address {1}!'.format(stop, start)) + + # Range start address must be unique + if start in range_start: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool start address {0} defined multipe times!'.format(start)) + else: + range_start.append(start) + + # Range stop address must be unique + if stop in range_stop: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool stop address {0} defined multipe times!'.format(stop)) + else: + range_stop.append(stop) + + # Exclude addresses must be in bound + for exclude in subnet['exclude']: + if not ip_address(exclude) in ip_network(subnet['network']): + raise ConfigError('Exclude IP address {0} is outside of the DHCP lease network {1}\n' \ + 'under shared network {2}!'.format(exclude, subnet['network'], network['name'])) + + # At least one DHCP address range or static-mapping required + active_mapping = False + if (len(subnet['range']) == 0): + for mapping in subnet['static_mapping']: + # we need at least one active mapping + if (not active_mapping) and (not mapping['disabled']): + active_mapping = True + else: + active_mapping = True + + if not active_mapping: + raise ConfigError('No DHCP address range or active static-mapping set\n' \ + 'for subnet {0}!'.format(subnet['network'])) + + # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set) + for mapping in subnet['static_mapping']: + + if mapping['ip_address']: + # Static IP address must be in bound + if not ip_address(mapping['ip_address']) in ip_network(subnet['network']): + raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside DHCP lease subnet {3}!' \ + .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) + + # Static mapping requires MAC address + if not mapping['mac_address']: + raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \ + '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) + + # There must be one subnet connected to a listen interface. + # This only counts if the network itself is not disabled! + if not network['disabled']: + if is_subnet_connected(subnet['network'], primary=True): + listen_ok = True + + # Subnets must be non overlapping + if subnet['network'] in subnets: + raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) + else: + subnets.append(subnet['network']) + + # Check for overlapping subnets + net = ip_network(subnet['network']) + for n in subnets: + net2 = ip_network(n) + if (net != net2): + if net.overlaps(net2): + raise ConfigError('DHCP conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) + + if not listen_ok: + raise ConfigError('DHCP server configuration error!\n' \ + 'None of configured DHCP subnets does not have appropriate\n' \ + 'primary IP address on any broadcast interface.') + + return None + +def generate(dhcp): + if not dhcp or dhcp['disabled']: + return None + + # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters + # we can pass to ISC DHCPd + render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, + formater=lambda _: _.replace(""", '"')) + return None + +def apply(dhcp): + if not dhcp or dhcp['disabled']: + # DHCP server is removed in the commit + call('systemctl stop isc-dhcp-server.service') + if os.path.exists(config_file): + os.unlink(config_file) + return None + + call('systemctl restart isc-dhcp-server.service') + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py new file mode 100755 index 000000000..6ef290bf0 --- /dev/null +++ b/src/conf_mode/dhcpv6_relay.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/run/dhcp-relay/dhcpv6.conf' + +default_config_data = { + 'listen_addr': [], + 'upstream_addr': [], + 'options': [], +} + +def get_config(): + relay = deepcopy(default_config_data) + conf = Config() + if not conf.exists('service dhcpv6-relay'): + return None + else: + conf.set_level('service dhcpv6-relay') + + # Network interfaces/address to listen on for DHCPv6 query(s) + if conf.exists('listen-interface'): + interfaces = conf.list_nodes('listen-interface') + for intf in interfaces: + if conf.exists('listen-interface {0} address'.format(intf)): + addr = conf.return_value('listen-interface {0} address'.format(intf)) + listen = addr + '%' + intf + relay['listen_addr'].append(listen) + + # Upstream interface/address for remote DHCPv6 server + if conf.exists('upstream-interface'): + interfaces = conf.list_nodes('upstream-interface') + for intf in interfaces: + addresses = conf.return_values('upstream-interface {0} address'.format(intf)) + for addr in addresses: + server = addr + '%' + intf + relay['upstream_addr'].append(server) + + # Maximum hop count. When forwarding packets, dhcrelay discards packets + # which have reached a hop count of COUNT. Default is 10. Maximum is 255. + if conf.exists('max-hop-count'): + count = '-c ' + conf.return_value('max-hop-count') + relay['options'].append(count) + + if conf.exists('use-interface-id-option'): + relay['options'].append('-I') + + return relay + +def verify(relay): + # bail out early - looks like removal from running config + if relay is None: + return None + + if len(relay['listen_addr']) == 0 or len(relay['upstream_addr']) == 0: + raise ConfigError('Must set at least one listen and upstream interface addresses.') + + return None + +def generate(relay): + # bail out early - looks like removal from running config + if relay is None: + return None + + render(config_file, 'dhcpv6-relay/config.tmpl', relay) + return None + +def apply(relay): + if relay is not None: + call('systemctl restart isc-dhcp-relay6.service') + else: + # DHCPv6 relay support is removed in the commit + call('systemctl stop isc-dhcp-relay6.service') + if os.path.exists(config_file): + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py new file mode 100755 index 000000000..53c8358a5 --- /dev/null +++ b/src/conf_mode/dhcpv6_server.py @@ -0,0 +1,386 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import ipaddress + +from sys import exit +from copy import deepcopy + +from vyos.config import Config +from vyos.template import render +from vyos.util import call +from vyos.validate import is_subnet_connected, is_ipv6 +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +config_file = r'/run/dhcp-server/dhcpdv6.conf' + +default_config_data = { + 'preference': '', + 'disabled': False, + 'shared_network': [] +} + +def get_config(): + dhcpv6 = deepcopy(default_config_data) + conf = Config() + base = ['service', 'dhcpv6-server'] + if not conf.exists(base): + return None + else: + conf.set_level(base) + + # Check for global disable of DHCPv6 service + if conf.exists(['disable']): + dhcpv6['disabled'] = True + return dhcpv6 + + # Preference of this DHCPv6 server compared with others + if conf.exists(['preference']): + dhcpv6['preference'] = conf.return_value(['preference']) + + # check for multiple, shared networks served with DHCPv6 addresses + if conf.exists(['shared-network-name']): + for network in conf.list_nodes(['shared-network-name']): + conf.set_level(base + ['shared-network-name', network]) + config = { + 'name': network, + 'disabled': False, + 'subnet': [] + } + + # If disabled, the shared-network configuration becomes inactive + if conf.exists(['disable']): + config['disabled'] = True + + # check for multiple subnet configurations in a shared network + if conf.exists(['subnet']): + for net in conf.list_nodes(['subnet']): + conf.set_level(base + ['shared-network-name', network, 'subnet', net]) + subnet = { + 'network': net, + 'range6_prefix': [], + 'range6': [], + 'default_router': '', + 'dns_server': [], + 'domain_name': '', + 'domain_search': [], + 'lease_def': '', + 'lease_min': '', + 'lease_max': '', + 'nis_domain': '', + 'nis_server': [], + 'nisp_domain': '', + 'nisp_server': [], + 'prefix_delegation': [], + 'sip_address': [], + 'sip_hostname': [], + 'sntp_server': [], + 'static_mapping': [] + } + + # For any subnet on which addresses will be assigned dynamically, there must be at + # least one address range statement. The range statement gives the lowest and highest + # IP addresses in a range. All IP addresses in the range should be in the subnet in + # which the range statement is declared. + if conf.exists(['address-range', 'prefix']): + for prefix in conf.list_nodes(['address-range', 'prefix']): + range = { + 'prefix': prefix, + 'temporary': False + } + + # Address range will be used for temporary addresses + if conf.exists(['address-range' 'prefix', prefix, 'temporary']): + range['temporary'] = True + + # Append to subnet temporary range6 list + subnet['range6_prefix'].append(range) + + if conf.exists(['address-range', 'start']): + for range in conf.list_nodes(['address-range', 'start']): + range = { + 'start': range, + 'stop': conf.return_value(['address-range', 'start', range, 'stop']) + } + + # Append to subnet range6 list + subnet['range6'].append(range) + + # The domain-search option specifies a 'search list' of Domain Names to be used + # by the client to locate not-fully-qualified domain names. + if conf.exists(['domain-search']): + subnet['domain_search'] = conf.return_values(['domain-search']) + + # IPv6 address valid lifetime + # (at the end the address is no longer usable by the client) + # (set to 30 days, the usual IPv6 default) + if conf.exists(['lease-time', 'default']): + subnet['lease_def'] = conf.return_value(['lease-time', 'default']) + + # Time should be the maximum length in seconds that will be assigned to a lease. + # The only exception to this is that Dynamic BOOTP lease lengths, which are not + # specified by the client, are not limited by this maximum. + if conf.exists(['lease-time', 'maximum']): + subnet['lease_max'] = conf.return_value(['lease-time', 'maximum']) + + # Time should be the minimum length in seconds that will be assigned to a lease + if conf.exists(['lease-time', 'minimum']): + subnet['lease_min'] = conf.return_value(['lease-time', 'minimum']) + + # Specifies a list of Domain Name System name servers available to the client. + # Servers should be listed in order of preference. + if conf.exists(['name-server']): + subnet['dns_server'] = conf.return_values(['name-server']) + + # Ancient NIS (Network Information Service) domain name + if conf.exists(['nis-domain']): + subnet['nis_domain'] = conf.return_value(['nis-domain']) + + # Ancient NIS (Network Information Service) servers + if conf.exists(['nis-server']): + subnet['nis_server'] = conf.return_values(['nis-server']) + + # Ancient NIS+ (Network Information Service) domain name + if conf.exists(['nisplus-domain']): + subnet['nisp_domain'] = conf.return_value(['nisplus-domain']) + + # Ancient NIS+ (Network Information Service) servers + if conf.exists(['nisplus-server']): + subnet['nisp_server'] = conf.return_values(['nisplus-server']) + + # Local SIP server that is to be used for all outbound SIP requests - IPv6 address + if conf.exists(['sip-server']): + for value in conf.return_values(['sip-server']): + if is_ipv6(value): + subnet['sip_address'].append(value) + else: + subnet['sip_hostname'].append(value) + + # List of local SNTP servers available for the client to synchronize their clocks + if conf.exists(['sntp-server']): + subnet['sntp_server'] = conf.return_values(['sntp-server']) + + # Prefix Delegation (RFC 3633) + if conf.exists(['prefix-delegation', 'start']): + for address in conf.list_nodes(['prefix-delegation', 'start']): + conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'prefix-delegation', 'start', address]) + prefix = { + 'start' : address, + 'stop' : '', + 'length' : '' + } + + if conf.exists(['prefix-length']): + prefix['length'] = conf.return_value(['prefix-length']) + + if conf.exists(['stop']): + prefix['stop'] = conf.return_value(['stop']) + + subnet['prefix_delegation'].append(prefix) + + # + # Static DHCP v6 leases + # + conf.set_level(base + ['shared-network-name', network, 'subnet', net]) + if conf.exists(['static-mapping']): + for mapping in conf.list_nodes(['static-mapping']): + conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping]) + mapping = { + 'name': mapping, + 'disabled': False, + 'ipv6_address': '', + 'client_identifier': '', + } + + # This static lease is disabled + if conf.exists(['disable']): + mapping['disabled'] = True + + # IPv6 address used for this DHCP client + if conf.exists(['ipv6-address']): + mapping['ipv6_address'] = conf.return_value(['ipv6-address']) + + # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers + if conf.exists(['identifier']): + mapping['client_identifier'] = conf.return_value(['identifier']) + + # append static mapping configuration tu subnet list + subnet['static_mapping'].append(mapping) + + # append subnet configuration to shared network subnet list + config['subnet'].append(subnet) + + # append shared network configuration to config dictionary + dhcpv6['shared_network'].append(config) + + # If all shared-networks are disabled, there's nothing to do. + if all(net['disabled'] for net in dhcpv6['shared_network']): + return None + + return dhcpv6 + +def verify(dhcpv6): + if not dhcpv6 or dhcpv6['disabled']: + return None + + # If DHCP is enabled we need one share-network + if len(dhcpv6['shared_network']) == 0: + raise ConfigError('No DHCPv6 shared networks configured.\n' \ + 'At least one DHCPv6 shared network must be configured.') + + # Inspect shared-network/subnet + subnets = [] + listen_ok = False + + for network in dhcpv6['shared_network']: + # A shared-network requires a subnet definition + if len(network['subnet']) == 0: + raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \ + 'lease subnet must be configured for each shared network.'.format(network['name'])) + + range6_start = [] + range6_stop = [] + for subnet in network['subnet']: + # Ususal range declaration with a start and stop address + for range6 in subnet['range6']: + # shorten names + start = range6['start'] + stop = range6['stop'] + + # DHCPv6 stop address is required + if start and not stop: + raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start)) + + # Start address must be inside network + if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) + + # Stop address must be inside network + if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) + + # Stop address must be greater or equal to start address + if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start): + raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \ + 'to the range start address {1}!'.format(stop, start)) + + # DHCPv6 range start address must be unique - two ranges can't + # start with the same address - makes no sense + if start in range6_start: + raise ConfigError('Conflicting DHCPv6 lease range:\n' \ + 'Pool start address {0} defined multipe times!'.format(start)) + else: + range6_start.append(start) + + # DHCPv6 range stop address must be unique - two ranges can't + # end with the same address - makes no sense + if stop in range6_stop: + raise ConfigError('Conflicting DHCPv6 lease range:\n' \ + 'Pool stop address {0} defined multipe times!'.format(stop)) + else: + range6_stop.append(stop) + + # Prefix delegation sanity checks + for prefix in subnet['prefix_delegation']: + if not prefix['stop']: + raise ConfigError('Stop address of delegated IPv6 prefix range must be configured') + + if not prefix['length']: + raise ConfigError('Length of delegated IPv6 prefix must be configured') + + # We also have prefixes that require checking + for prefix in subnet['range6_prefix']: + # If configured prefix does not match our subnet, we have to check that it's inside + if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']): + # Configured prefixes must be inside our network + if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name'])) + + # Static mappings don't require anything (but check if IP is in subnet if it's set) + for mapping in subnet['static_mapping']: + if mapping['ipv6_address']: + # Static address must be in subnet + if not ipaddress.ip_address(mapping['ipv6_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCPv6 static mapping IPv6 address {0} for static mapping {1}\n' \ + 'in shared network {2} is outside subnet {3}!' \ + .format(mapping['ipv6_address'], mapping['name'], network['name'], subnet['network'])) + + # Subnets must be unique + if subnet['network'] in subnets: + raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network'])) + else: + subnets.append(subnet['network']) + + # DHCPv6 requires at least one configured address range or one static mapping + # (FIXME: is not actually checked right now?) + + # There must be one subnet connected to a listen interface if network is not disabled. + if not network['disabled']: + if is_subnet_connected(subnet['network']): + listen_ok = True + + # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping + # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32" + net = ipaddress.ip_network(subnet['network']) + for n in subnets: + net2 = ipaddress.ip_network(n) + if (net != net2): + if net.overlaps(net2): + raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) + + if not listen_ok: + raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \ + 'this machine. At least one subnet6 must be connected such that\n' \ + 'DHCPv6 listens on an interface!') + + + return None + +def generate(dhcpv6): + if not dhcpv6 or dhcpv6['disabled']: + return None + + render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6) + return None + +def apply(dhcpv6): + if not dhcpv6 or dhcpv6['disabled']: + # DHCP server is removed in the commit + call('systemctl stop isc-dhcp-server6.service') + if os.path.exists(config_file): + os.unlink(config_file) + + else: + call('systemctl restart isc-dhcp-server6.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py new file mode 100755 index 000000000..51631dc16 --- /dev/null +++ b/src/conf_mode/dns_forwarding.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy + +from vyos.config import Config +from vyos.hostsd_client import Client as hostsd_client +from vyos import ConfigError +from vyos.util import call, chown +from vyos.template import render + +from vyos import airbag +airbag.enable() + +pdns_rec_user = pdns_rec_group = 'pdns' +pdns_rec_run_dir = '/run/powerdns' +pdns_rec_lua_conf_file = f'{pdns_rec_run_dir}/recursor.conf.lua' +pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.lua' +pdns_rec_hostsd_zones_file = f'{pdns_rec_run_dir}/recursor.forward-zones.conf' +pdns_rec_config_file = f'{pdns_rec_run_dir}/recursor.conf' + +default_config_data = { + 'allow_from': [], + 'cache_size': 10000, + 'export_hosts_file': 'yes', + 'listen_address': [], + 'name_servers': [], + 'negative_ttl': 3600, + 'system': False, + 'domains': {}, + 'dnssec': 'process-no-validate', + 'dhcp_interfaces': [] +} + +hostsd_tag = 'static' + +def get_config(conf): + dns = deepcopy(default_config_data) + base = ['service', 'dns', 'forwarding'] + + if not conf.exists(base): + return None + + conf.set_level(base) + + if conf.exists(['allow-from']): + dns['allow_from'] = conf.return_values(['allow-from']) + + if conf.exists(['cache-size']): + cache_size = conf.return_value(['cache-size']) + dns['cache_size'] = cache_size + + if conf.exists('negative-ttl'): + negative_ttl = conf.return_value(['negative-ttl']) + dns['negative_ttl'] = negative_ttl + + if conf.exists(['domain']): + for domain in conf.list_nodes(['domain']): + conf.set_level(base + ['domain', domain]) + entry = { + 'nslist': bracketize_ipv6_addrs(conf.return_values(['server'])), + 'addNTA': conf.exists(['addnta']), + 'recursion-desired': conf.exists(['recursion-desired']) + } + dns['domains'][domain] = entry + + conf.set_level(base) + + if conf.exists(['ignore-hosts-file']): + dns['export_hosts_file'] = "no" + + if conf.exists(['name-server']): + dns['name_servers'] = bracketize_ipv6_addrs( + conf.return_values(['name-server'])) + + if conf.exists(['system']): + dns['system'] = True + + if conf.exists(['listen-address']): + dns['listen_address'] = conf.return_values(['listen-address']) + + if conf.exists(['dnssec']): + dns['dnssec'] = conf.return_value(['dnssec']) + + if conf.exists(['dhcp']): + dns['dhcp_interfaces'] = conf.return_values(['dhcp']) + + return dns + +def bracketize_ipv6_addrs(addrs): + """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched.""" + return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs] + +def verify(conf, dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + if not dns['listen_address']: + raise ConfigError( + "Error: DNS forwarding requires a listen-address") + + if not dns['allow_from']: + raise ConfigError( + "Error: DNS forwarding requires an allow-from network") + + if dns['domains']: + for domain in dns['domains']: + if not dns['domains'][domain]['nslist']: + raise ConfigError(( + f'Error: No server configured for domain {domain}')) + + no_system_nameservers = False + if dns['system'] and not ( + conf.exists(['system', 'name-server']) or + conf.exists(['system', 'name-servers-dhcp']) ): + no_system_nameservers = True + print(("DNS forwarding warning: No 'system name-server' or " + "'system name-servers-dhcp' set\n")) + + if (no_system_nameservers or not dns['system']) and not ( + dns['name_servers'] or dns['dhcp_interfaces']): + print(("DNS forwarding warning: No 'dhcp', 'name-server' or 'system' " + "nameservers set. Forwarding will operate as a recursor.\n")) + + return None + +def generate(dns): + # bail out early - looks like removal from running config + if dns is None: + return None + + render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl', + dns, user=pdns_rec_user, group=pdns_rec_group) + + render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl', + dns, user=pdns_rec_user, group=pdns_rec_group) + + # if vyos-hostsd didn't create its files yet, create them (empty) + for f in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]: + with open(f, 'a'): + pass + chown(f, user=pdns_rec_user, group=pdns_rec_group) + + return None + +def apply(dns): + if dns is None: + # DNS forwarding is removed in the commit + call("systemctl stop pdns-recursor.service") + if os.path.isfile(pdns_rec_config_file): + os.unlink(pdns_rec_config_file) + else: + ### first apply vyos-hostsd config + hc = hostsd_client() + + # add static nameservers to hostsd so they can be joined with other + # sources + hc.delete_name_servers([hostsd_tag]) + if dns['name_servers']: + hc.add_name_servers({hostsd_tag: dns['name_servers']}) + + # delete all nameserver tags + hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor()) + + ## add nameserver tags - the order determines the nameserver order! + # our own tag (static) + hc.add_name_server_tags_recursor([hostsd_tag]) + + if dns['system']: + hc.add_name_server_tags_recursor(['system']) + else: + hc.delete_name_server_tags_recursor(['system']) + + # add dhcp nameserver tags for configured interfaces + for intf in dns['dhcp_interfaces']: + hc.add_name_server_tags_recursor(['dhcp-' + intf, 'dhcpv6-' + intf ]) + + # hostsd will generate the forward-zones file + # the list and keys() are required as get returns a dict, not list + hc.delete_forward_zones(list(hc.get_forward_zones().keys())) + if dns['domains']: + hc.add_forward_zones(dns['domains']) + + # call hostsd to generate forward-zones and its lua-config-file + hc.apply() + + ### finally (re)start pdns-recursor + call("systemctl restart pdns-recursor.service") + +if __name__ == '__main__': + try: + conf = Config() + c = get_config(conf) + verify(conf, c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py new file mode 100755 index 000000000..5b1883c03 --- /dev/null +++ b/src/conf_mode/dynamic_dns.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/run/ddclient/ddclient.conf' + +# Mapping of service name to service protocol +default_service_protocol = { + 'afraid': 'freedns', + 'changeip': 'changeip', + 'cloudflare': 'cloudflare', + 'dnspark': 'dnspark', + 'dslreports': 'dslreports1', + 'dyndns': 'dyndns2', + 'easydns': 'easydns', + 'namecheap': 'namecheap', + 'noip': 'noip', + 'sitelutions': 'sitelutions', + 'zoneedit': 'zoneedit1' +} + +default_config_data = { + 'interfaces': [], + 'deleted': False +} + +def get_config(): + dyndns = deepcopy(default_config_data) + conf = Config() + base_level = ['service', 'dns', 'dynamic'] + + if not conf.exists(base_level): + dyndns['deleted'] = True + return dyndns + + for interface in conf.list_nodes(base_level + ['interface']): + node = { + 'interface': interface, + 'rfc2136': [], + 'service': [], + 'web_skip': '', + 'web_url': '' + } + + # set config level to e.g. "service dns dynamic interface eth0" + conf.set_level(base_level + ['interface', interface]) + # Handle RFC2136 - Dynamic Updates in the Domain Name System + for rfc2136 in conf.list_nodes(['rfc2136']): + rfc = { + 'name': rfc2136, + 'keyfile': '', + 'record': [], + 'server': '', + 'ttl': '600', + 'zone': '' + } + + # set config level + conf.set_level(base_level + ['interface', interface, 'rfc2136', rfc2136]) + + if conf.exists(['key']): + rfc['keyfile'] = conf.return_value(['key']) + + if conf.exists(['record']): + rfc['record'] = conf.return_values(['record']) + + if conf.exists(['server']): + rfc['server'] = conf.return_value(['server']) + + if conf.exists(['ttl']): + rfc['ttl'] = conf.return_value(['ttl']) + + if conf.exists(['zone']): + rfc['zone'] = conf.return_value(['zone']) + + node['rfc2136'].append(rfc) + + # set config level to e.g. "service dns dynamic interface eth0" + conf.set_level(base_level + ['interface', interface]) + # Handle DynDNS service providers + for service in conf.list_nodes(['service']): + srv = { + 'provider': service, + 'host': [], + 'login': '', + 'password': '', + 'protocol': '', + 'server': '', + 'custom' : False, + 'zone' : '' + } + + # set config level + conf.set_level(base_level + ['interface', interface, 'service', service]) + + # preload protocol from default service mapping + if service in default_service_protocol.keys(): + srv['protocol'] = default_service_protocol[service] + else: + srv['custom'] = True + + if conf.exists(['login']): + srv['login'] = conf.return_value(['login']) + + if conf.exists(['host-name']): + srv['host'] = conf.return_values(['host-name']) + + if conf.exists(['protocol']): + srv['protocol'] = conf.return_value(['protocol']) + + if conf.exists(['password']): + srv['password'] = conf.return_value(['password']) + + if conf.exists(['server']): + srv['server'] = conf.return_value(['server']) + + if conf.exists(['zone']): + srv['zone'] = conf.return_value(['zone']) + elif srv['provider'] == 'cloudflare': + # default populate zone entry with bar.tld if + # host-name is foo.bar.tld + srv['zone'] = srv['host'][0].split('.',1)[1] + + node['service'].append(srv) + + # Set config back to appropriate level for these options + conf.set_level(base_level + ['interface', interface]) + + # Additional settings in CLI + if conf.exists(['use-web', 'skip']): + node['web_skip'] = conf.return_value(['use-web', 'skip']) + + if conf.exists(['use-web', 'url']): + node['web_url'] = conf.return_value(['use-web', 'url']) + + # set config level back to top level + conf.set_level(base_level) + + dyndns['interfaces'].append(node) + + return dyndns + +def verify(dyndns): + # bail out early - looks like removal from running config + if dyndns['deleted']: + return None + + # A 'node' corresponds to an interface + for node in dyndns['interfaces']: + + # RFC2136 - configuration validation + for rfc2136 in node['rfc2136']: + if not rfc2136['record']: + raise ConfigError('Set key for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['zone']: + raise ConfigError('Set zone for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['keyfile']: + raise ConfigError('Set keyfile for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + else: + if not os.path.isfile(rfc2136['keyfile']): + raise ConfigError('Keyfile for service "{0}" to send DDNS updates for interface "{1}" does not exist'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['server']: + raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + # DynDNS service provider - configuration validation + for service in node['service']: + if not service['host']: + raise ConfigError('Set host-name for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['login']: + raise ConfigError('Set login for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['password']: + raise ConfigError('Set password for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if service['custom'] is True: + if not service['protocol']: + raise ConfigError('Set protocol for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['server']: + raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if service['zone']: + if service['provider'] != 'cloudflare': + raise ConfigError('Zone option not allowed for "{0}", it can only be used for CloudFlare'.format(service['provider'])) + + return None + +def generate(dyndns): + # bail out early - looks like removal from running config + if dyndns['deleted']: + return None + + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) + + # Config file must be accessible only by its owner + os.chmod(config_file, S_IRUSR | S_IWUSR) + + return None + +def apply(dyndns): + if dyndns['deleted']: + call('systemctl stop ddclient.service') + if os.path.exists(config_file): + os.unlink(config_file) + + else: + call('systemctl restart ddclient.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py new file mode 100755 index 000000000..71b2a98b3 --- /dev/null +++ b/src/conf_mode/firewall_options.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys +import os +import copy + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call + +from vyos import airbag +airbag.enable() + +default_config_data = { + 'intf_opts': [], + 'new_chain4': False, + 'new_chain6': False +} + +def get_config(): + opts = copy.deepcopy(default_config_data) + conf = Config() + if not conf.exists('firewall options'): + # bail out early + return opts + else: + conf.set_level('firewall options') + + # Parse configuration of each individual instance + if conf.exists('interface'): + for intf in conf.list_nodes('interface'): + conf.set_level('firewall options interface {0}'.format(intf)) + config = { + 'intf': intf, + 'disabled': False, + 'mss4': '', + 'mss6': '' + } + + # Check if individual option is disabled + if conf.exists('disable'): + config['disabled'] = True + + # + # Get MSS value IPv4 + # + if conf.exists('adjust-mss'): + config['mss4'] = conf.return_value('adjust-mss') + + # We need a marker that a new iptables chain needs to be generated + if not opts['new_chain4']: + opts['new_chain4'] = True + + # + # Get MSS value IPv6 + # + if conf.exists('adjust-mss6'): + config['mss6'] = conf.return_value('adjust-mss6') + + # We need a marker that a new ip6tables chain needs to be generated + if not opts['new_chain6']: + opts['new_chain6'] = True + + # Append interface options to global list + opts['intf_opts'].append(config) + + return opts + +def verify(tcp): + # syntax verification is done via cli + return None + +def apply(tcp): + target = 'VYOS_FW_OPTIONS' + + # always cleanup iptables + call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('iptables --table mangle --flush {} >&/dev/null'.format(target)) + call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target)) + + # always cleanup ip6tables + call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --flush {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target)) + + # Setup new iptables rules + if tcp['new_chain4']: + call('iptables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + + for opts in tcp['intf_opts']: + intf = opts['intf'] + mss = opts['mss4'] + + # Check if this rule iis disabled + if opts['disabled']: + continue + + # adjust TCP MSS per interface + if mss: + call('iptables --table mangle --append {} --out-interface {} --protocol tcp ' + '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) + + # Setup new ip6tables rules + if tcp['new_chain6']: + call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + + for opts in tcp['intf_opts']: + intf = opts['intf'] + mss = opts['mss6'] + + # Check if this rule iis disabled + if opts['disabled']: + continue + + # adjust TCP MSS per interface + if mss: + call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' + '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) + + return None + +if __name__ == '__main__': + + try: + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py new file mode 100755 index 000000000..b7e73eaeb --- /dev/null +++ b/src/conf_mode/flow_accounting_conf.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +from sys import exit +import ipaddress + +from ipaddress import ip_address +from jinja2 import FileSystemLoader, Environment + +from vyos.ifconfig import Section +from vyos.ifconfig import Interface +from vyos.config import Config +from vyos import ConfigError +from vyos.util import cmd +from vyos.template import render + +from vyos import airbag +airbag.enable() + +# default values +default_sflow_server_port = 6343 +default_netflow_server_port = 2055 +default_plugin_pipe_size = 10 +default_captured_packet_size = 128 +default_netflow_version = '9' +default_sflow_agentip = 'auto' +uacctd_conf_path = '/etc/pmacct/uacctd.conf' +iptables_nflog_table = 'raw' +iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK' + +# helper functions +# check if node exists and return True if this is true +def _node_exists(path): + vyos_config = Config() + if vyos_config.exists(path): + return True + +# get sFlow agent-ip if agent-address is "auto" (default behaviour) +def _sflow_default_agentip(config): + # check if any of BGP, OSPF, OSPFv3 protocols are configured and use router-id from there + if config.exists('protocols bgp'): + bgp_router_id = config.return_value("protocols bgp {} parameters router-id".format(config.list_nodes('protocols bgp')[0])) + if bgp_router_id: + return bgp_router_id + if config.return_value('protocols ospf parameters router-id'): + return config.return_value('protocols ospf parameters router-id') + if config.return_value('protocols ospfv3 parameters router-id'): + return config.return_value('protocols ospfv3 parameters router-id') + + # if router-id was not found, use first available ip of any interface + for iface in Section.interfaces(): + for address in Interface(iface).get_addr(): + # return an IP, if this is not loopback + regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') + if regex_filter.search(address): + return regex_filter.search(address).group('ipaddr') + + # return nothing by default + return None + +# get iptables rule dict for chain in table +def _iptables_get_nflog(): + # define list with rules + rules = [] + + # prepare regex for parsing rules + rule_pattern = "^-A (?P<rule_definition>{0} -i (?P<interface>[\w\.\*\-]+).*--comment FLOW_ACCOUNTING_RULE.* -j NFLOG.*$)".format(iptables_nflog_chain) + rule_re = re.compile(rule_pattern) + + for iptables_variant in ['iptables', 'ip6tables']: + # run iptables, save output and split it by lines + iptables_command = f'{iptables_variant} -t {iptables_nflog_table} -S {iptables_nflog_chain}' + tmp = cmd(iptables_command, message='Failed to get flows list') + + # parse each line and add information to list + for current_rule in tmp.splitlines(): + current_rule_parsed = rule_re.search(current_rule) + if current_rule_parsed: + rules.append({ 'interface': current_rule_parsed.groupdict()["interface"], 'iptables_variant': iptables_variant, 'table': iptables_nflog_table, 'rule_definition': current_rule_parsed.groupdict()["rule_definition"] }) + + # return list with rules + return rules + +# modify iptables rules +def _iptables_config(configured_ifaces): + # define list of iptables commands to modify settings + iptable_commands = [] + + # prepare extended list with configured interfaces + configured_ifaces_extended = [] + for iface in configured_ifaces: + configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'iptables' }) + configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'ip6tables' }) + + # get currently configured interfaces with iptables rules + active_nflog_rules = _iptables_get_nflog() + + # compare current active list with configured one and delete excessive interfaces, add missed + active_nflog_ifaces = [] + for rule in active_nflog_rules: + iptables = rule['iptables_variant'] + interface = rule['interface'] + if interface not in configured_ifaces: + table = rule['table'] + rule = rule['rule_definition'] + iptable_commands.append(f'{iptables} -t {table} -D {rule}') + else: + active_nflog_ifaces.append({ + 'iface': interface, + 'iptables_variant': iptables, + }) + + # do not create new rules for already configured interfaces + for iface in active_nflog_ifaces: + if iface in active_nflog_ifaces: + configured_ifaces_extended.remove(iface) + + # create missed rules + for iface_extended in configured_ifaces_extended: + iface = iface_extended['iface'] + iptables = iface_extended['iptables_variant'] + rule_definition = f'{iptables_nflog_chain} -i {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100' + iptable_commands.append(f'{iptables} -t {iptables_nflog_table} -I {rule_definition}') + + # change iptables + for command in iptable_commands: + cmd(command, raising=ConfigError) + + +def get_config(): + vc = Config() + vc.set_level('') + # Convert the VyOS config to an abstract internal representation + flow_config = { + 'flow-accounting-configured': vc.exists('system flow-accounting'), + 'buffer-size': vc.return_value('system flow-accounting buffer-size'), + 'disable-imt': _node_exists('system flow-accounting disable-imt'), + 'syslog-facility': vc.return_value('system flow-accounting syslog-facility'), + 'interfaces': None, + 'sflow': { + 'configured': vc.exists('system flow-accounting sflow'), + 'agent-address': vc.return_value('system flow-accounting sflow agent-address'), + 'sampling-rate': vc.return_value('system flow-accounting sflow sampling-rate'), + 'servers': None + }, + 'netflow': { + 'configured': vc.exists('system flow-accounting netflow'), + 'engine-id': vc.return_value('system flow-accounting netflow engine-id'), + 'max-flows': vc.return_value('system flow-accounting netflow max-flows'), + 'sampling-rate': vc.return_value('system flow-accounting netflow sampling-rate'), + 'source-ip': vc.return_value('system flow-accounting netflow source-ip'), + 'version': vc.return_value('system flow-accounting netflow version'), + 'timeout': { + 'expint': vc.return_value('system flow-accounting netflow timeout expiry-interval'), + 'general': vc.return_value('system flow-accounting netflow timeout flow-generic'), + 'icmp': vc.return_value('system flow-accounting netflow timeout icmp'), + 'maxlife': vc.return_value('system flow-accounting netflow timeout max-active-life'), + 'tcp.fin': vc.return_value('system flow-accounting netflow timeout tcp-fin'), + 'tcp': vc.return_value('system flow-accounting netflow timeout tcp-generic'), + 'tcp.rst': vc.return_value('system flow-accounting netflow timeout tcp-rst'), + 'udp': vc.return_value('system flow-accounting netflow timeout udp') + }, + 'servers': None + } + } + + # get interfaces list + if vc.exists('system flow-accounting interface'): + flow_config['interfaces'] = vc.return_values('system flow-accounting interface') + + # get sFlow collectors list + if vc.exists('system flow-accounting sflow server'): + flow_config['sflow']['servers'] = [] + sflow_collectors = vc.list_nodes('system flow-accounting sflow server') + for collector in sflow_collectors: + port = default_sflow_server_port + if vc.return_value("system flow-accounting sflow server {} port".format(collector)): + port = vc.return_value("system flow-accounting sflow server {} port".format(collector)) + flow_config['sflow']['servers'].append({ 'address': collector, 'port': port }) + + # get NetFlow collectors list + if vc.exists('system flow-accounting netflow server'): + flow_config['netflow']['servers'] = [] + netflow_collectors = vc.list_nodes('system flow-accounting netflow server') + for collector in netflow_collectors: + port = default_netflow_server_port + if vc.return_value("system flow-accounting netflow server {} port".format(collector)): + port = vc.return_value("system flow-accounting netflow server {} port".format(collector)) + flow_config['netflow']['servers'].append({ 'address': collector, 'port': port }) + + # get sflow agent-id + if flow_config['sflow']['agent-address'] == None or flow_config['sflow']['agent-address'] == 'auto': + flow_config['sflow']['agent-address'] = _sflow_default_agentip(vc) + + # get NetFlow version + if not flow_config['netflow']['version']: + flow_config['netflow']['version'] = default_netflow_version + + # convert NetFlow engine-id format, if this is necessary + if flow_config['netflow']['engine-id'] and flow_config['netflow']['version'] == '5': + regex_filter = re.compile('^\d+$') + if regex_filter.search(flow_config['netflow']['engine-id']): + flow_config['netflow']['engine-id'] = "{}:0".format(flow_config['netflow']['engine-id']) + + # return dict with flow-accounting configuration + return flow_config + +def verify(config): + # Verify that configuration is valid + # skip all checks if flow-accounting was removed + if not config['flow-accounting-configured']: + return True + + # check if at least one collector is enabled + if not (config['sflow']['configured'] or config['netflow']['configured'] or not config['disable-imt']): + raise ConfigError("You need to configure at least one sFlow or NetFlow protocol, or not set \"disable-imt\" for flow-accounting") + + # Check if at least one interface is configured + if not config['interfaces']: + raise ConfigError("You need to configure at least one interface for flow-accounting") + + # check that all configured interfaces exists in the system + for iface in config['interfaces']: + if not iface in Section.interfaces(): + # chnged from error to warning to allow adding dynamic interfaces and interface templates + # raise ConfigError("The {} interface is not presented in the system".format(iface)) + print("Warning: the {} interface is not presented in the system".format(iface)) + + # check sFlow configuration + if config['sflow']['configured']: + # check if at least one sFlow collector is configured if sFlow configuration is presented + if not config['sflow']['servers']: + raise ConfigError("You need to configure at least one sFlow server") + + # check that all sFlow collectors use the same IP protocol version + sflow_collector_ipver = None + for sflow_collector in config['sflow']['servers']: + if sflow_collector_ipver: + if sflow_collector_ipver != ip_address(sflow_collector['address']).version: + raise ConfigError("All sFlow servers must use the same IP protocol") + else: + sflow_collector_ipver = ip_address(sflow_collector['address']).version + + + # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa + for sflow_collector in config['sflow']['servers']: + if ip_address(sflow_collector['address']).version != ip_address(config['sflow']['agent-address']).version: + raise ConfigError("Different IP address versions cannot be mixed in \"sflow agent-address\" and \"sflow server\". You need to set manually the same IP version for \"agent-address\" as for all sFlow servers") + + # check if configured sFlow agent-id exist in the system + agent_id_presented = None + for iface in Section.interfaces(): + for address in Interface(iface).get_addr(): + # check an IP, if this is not loopback + regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') + if regex_filter.search(address): + if regex_filter.search(address).group('ipaddr') == config['sflow']['agent-address']: + agent_id_presented = True + break + if not agent_id_presented: + raise ConfigError("Your \"sflow agent-address\" does not exist in the system") + + # check NetFlow configuration + if config['netflow']['configured']: + # check if at least one NetFlow collector is configured if NetFlow configuration is presented + if not config['netflow']['servers']: + raise ConfigError("You need to configure at least one NetFlow server") + + # check if configured netflow source-ip exist in the system + if config['netflow']['source-ip']: + source_ip_presented = None + for iface in Section.interfaces(): + for address in Interface(iface).get_addr(): + # check an IP + regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') + if regex_filter.search(address): + if regex_filter.search(address).group('ipaddr') == config['netflow']['source-ip']: + source_ip_presented = True + break + if not source_ip_presented: + raise ConfigError("Your \"netflow source-ip\" does not exist in the system") + + # check if engine-id compatible with selected protocol version + if config['netflow']['engine-id']: + v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$' + v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$' + if config['netflow']['version'] == '5': + regex_filter = re.compile(v5_filter) + if not regex_filter.search(config['netflow']['engine-id']): + raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version'])) + else: + regex_filter = re.compile(v9v10_filter) + if not regex_filter.search(config['netflow']['engine-id']): + raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version'])) + + # return True if all checks were passed + return True + +def generate(config): + # skip all checks if flow-accounting was removed + if not config['flow-accounting-configured']: + return True + + # Calculate all necessary values + if config['buffer-size']: + # circular queue size + config['plugin_pipe_size'] = int(config['buffer-size']) * 1024**2 + else: + config['plugin_pipe_size'] = default_plugin_pipe_size * 1024**2 + # transfer buffer size + # recommended value from pmacct developers 1/1000 of pipe size + config['plugin_buffer_size'] = int(config['plugin_pipe_size'] / 1000) + + # Prepare a timeouts string + timeout_string = '' + for timeout_type, timeout_value in config['netflow']['timeout'].items(): + if timeout_value: + if timeout_string == '': + timeout_string = "{}{}={}".format(timeout_string, timeout_type, timeout_value) + else: + timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value) + config['netflow']['timeout_string'] = timeout_string + + render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', { + 'templatecfg': config, + 'snaplen': default_captured_packet_size, + }) + + +def apply(config): + # define variables + command = None + # Check if flow-accounting was removed and define command + if not config['flow-accounting-configured']: + command = 'systemctl stop uacctd.service' + else: + command = 'systemctl restart uacctd.service' + + # run command to start or stop flow-accounting + cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting') + + # configure iptables rules for defined interfaces + if config['interfaces']: + _iptables_config(config['interfaces']) + else: + _iptables_config([]) + +if __name__ == '__main__': + try: + config = get_config() + verify(config) + generate(config) + apply(config) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py new file mode 100755 index 000000000..9d66bd434 --- /dev/null +++ b/src/conf_mode/host_name.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +conf-mode script for 'system host-name' and 'system domain-name'. +""" + +import re +import sys +import copy + +import vyos.util +import vyos.hostsd_client + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import cmd, call, process_named_running + +from vyos import airbag +airbag.enable() + +default_config_data = { + 'hostname': 'vyos', + 'domain_name': '', + 'domain_search': [], + 'nameserver': [], + 'nameservers_dhcp_interfaces': [], + 'static_host_mapping': {} +} + +hostsd_tag = 'system' + +def get_config(): + conf = Config() + + hosts = copy.deepcopy(default_config_data) + + hosts['hostname'] = conf.return_value("system host-name") + + # This may happen if the config is not loaded yet, + # e.g. if run by cloud-init + if not hosts['hostname']: + hosts['hostname'] = default_config_data['hostname'] + + if conf.exists("system domain-name"): + hosts['domain_name'] = conf.return_value("system domain-name") + hosts['domain_search'].append(hosts['domain_name']) + + for search in conf.return_values("system domain-search domain"): + hosts['domain_search'].append(search) + + hosts['nameserver'] = conf.return_values("system name-server") + + hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp") + + # system static-host-mapping + for hn in conf.list_nodes('system static-host-mapping host-name'): + hosts['static_host_mapping'][hn] = {} + hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet') + hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias') + + return hosts + + +def verify(hosts): + if hosts is None: + return None + + # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)" + hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$") + if not hostname_regex.match(hosts['hostname']): + raise ConfigError('Invalid host name ' + hosts["hostname"]) + + # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length" + length = len(hosts['hostname']) + if length < 1 or length > 63: + raise ConfigError( + 'Invalid host-name length, must be less than 63 characters') + + all_static_host_mapping_addresses = [] + # static mappings alias hostname + for host, hostprops in hosts['static_host_mapping'].items(): + if not hostprops['address']: + raise ConfigError(f'IP address required for static-host-mapping "{host}"') + all_static_host_mapping_addresses.append(hostprops['address']) + for a in hostprops['aliases']: + if not hostname_regex.match(a) and len(a) != 0: + raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"') + + # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't + # exist or doesn't have address dhcp(v6) + + return None + + +def generate(config): + pass + +def apply(config): + if config is None: + return None + + ## Send the updated data to vyos-hostsd + try: + hc = vyos.hostsd_client.Client() + + hc.set_host_name(config['hostname'], config['domain_name']) + + hc.delete_search_domains([hostsd_tag]) + if config['domain_search']: + hc.add_search_domains({hostsd_tag: config['domain_search']}) + + hc.delete_name_servers([hostsd_tag]) + if config['nameserver']: + hc.add_name_servers({hostsd_tag: config['nameserver']}) + + # add our own tag's (system) nameservers and search to resolv.conf + hc.delete_name_server_tags_system(hc.get_name_server_tags_system()) + hc.add_name_server_tags_system([hostsd_tag]) + + # this will add the dhcp client nameservers to resolv.conf + for intf in config['nameservers_dhcp_interfaces']: + hc.add_name_server_tags_system([f'dhcp-{intf}', f'dhcpv6-{intf}']) + + hc.delete_hosts([hostsd_tag]) + if config['static_host_mapping']: + hc.add_hosts({hostsd_tag: config['static_host_mapping']}) + + hc.apply() + except vyos.hostsd_client.VyOSHostsdError as e: + raise ConfigError(str(e)) + + ## Actually update the hostname -- vyos-hostsd doesn't do that + + # No domain name -- the Debian way. + hostname_new = config['hostname'] + + # rsyslog runs into a race condition at boot time with systemd + # restart rsyslog only if the hostname changed. + hostname_old = cmd('hostnamectl --static') + call(f'hostnamectl set-hostname --static {hostname_new}') + + # Restart services that use the hostname + if hostname_new != hostname_old: + call("systemctl restart rsyslog.service") + + # If SNMP is running, restart it too + if process_named_running('snmpd'): + call('systemctl restart snmpd.service') + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py new file mode 100755 index 000000000..b8a084a40 --- /dev/null +++ b/src/conf_mode/http-api.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import json +from copy import deepcopy + +import vyos.defaults +from vyos.config import Config +from vyos import ConfigError +from vyos.util import cmd +from vyos.util import call + +from vyos import airbag +airbag.enable() + +config_file = '/etc/vyos/http-api.conf' + +vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] + +# XXX: this model will need to be extended for tag nodes +dependencies = [ + 'https.py', +] + +def get_config(): + http_api = deepcopy(vyos.defaults.api_data) + x = http_api.get('api_keys') + if x is None: + default_key = None + else: + default_key = x[0] + keys_added = False + + conf = Config() + if not conf.exists('service https api'): + return None + else: + conf.set_level('service https api') + + if conf.exists('strict'): + http_api['strict'] = 'true' + + if conf.exists('debug'): + http_api['debug'] = 'true' + + if conf.exists('port'): + port = conf.return_value('port') + http_api['port'] = port + + if conf.exists('keys'): + for name in conf.list_nodes('keys id'): + if conf.exists('keys id {0} key'.format(name)): + key = conf.return_value('keys id {0} key'.format(name)) + new_key = { 'id': name, 'key': key } + http_api['api_keys'].append(new_key) + keys_added = True + + if keys_added and default_key: + if default_key in http_api['api_keys']: + http_api['api_keys'].remove(default_key) + + return http_api + +def verify(http_api): + return None + +def generate(http_api): + if http_api is None: + return None + + if not os.path.exists('/etc/vyos'): + os.mkdir('/etc/vyos') + + with open(config_file, 'w') as f: + json.dump(http_api, f, indent=2) + + return None + +def apply(http_api): + if http_api is not None: + call('systemctl restart vyos-http-api.service') + else: + call('systemctl stop vyos-http-api.service') + + for dep in dependencies: + cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py new file mode 100755 index 000000000..a13f131ab --- /dev/null +++ b/src/conf_mode/https.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys + +from copy import deepcopy + +import vyos.defaults +import vyos.certbot_util + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = '/etc/nginx/sites-available/default' +certbot_dir = vyos.defaults.directories['certbot'] + +# https config needs to coordinate several subsystems: api, certbot, +# self-signed certificate, as well as the virtual hosts defined within the +# https config definition itself. Consequently, one needs a general dict, +# encompassing the https and other configs, and a list of such virtual hosts +# (server blocks in nginx terminology) to pass to the jinja2 template. +default_server_block = { + 'id' : '', + 'address' : '*', + 'port' : '443', + 'name' : ['_'], + 'api' : {}, + 'vyos_cert' : {}, + 'certbot' : False +} + +def get_config(): + conf = Config() + if not conf.exists('service https'): + return None + + server_block_list = [] + https_dict = conf.get_config_dict('service https', get_first_key=True) + + # organize by vhosts + + vhost_dict = https_dict.get('virtual-host', {}) + + if not vhost_dict: + # no specified virtual hosts (server blocks); use default + server_block_list.append(default_server_block) + else: + for vhost in list(vhost_dict): + server_block = deepcopy(default_server_block) + server_block['id'] = vhost + data = vhost_dict.get(vhost, {}) + server_block['address'] = data.get('listen-address', '*') + server_block['port'] = data.get('listen-port', '443') + name = data.get('server-name', ['_']) + # XXX: T2636 workaround: convert string to a list with one element + if not isinstance(name, list): + name = [name] + server_block['name'] = name + server_block_list.append(server_block) + + # get certificate data + + cert_dict = https_dict.get('certificates', {}) + + # self-signed certificate + + vyos_cert_data = {} + if 'system-generated-certificate' in list(cert_dict): + vyos_cert_data = vyos.defaults.vyos_cert_data + if vyos_cert_data: + for block in server_block_list: + block['vyos_cert'] = vyos_cert_data + + # letsencrypt certificate using certbot + + certbot = False + cert_domains = cert_dict.get('certbot', {}).get('domain-name', []) + if cert_domains: + # XXX: T2636 workaround: convert string to a list with one element + if not isinstance(cert_domains, list): + cert_domains = [cert_domains] + certbot = True + for domain in cert_domains: + sub_list = vyos.certbot_util.choose_server_block(server_block_list, + domain) + if sub_list: + for sb in sub_list: + sb['certbot'] = True + sb['certbot_dir'] = certbot_dir + # certbot organizes certificates by first domain + sb['certbot_domain_dir'] = cert_domains[0] + + # get api data + + api_set = False + api_data = {} + if 'api' in list(https_dict): + api_set = True + api_data = vyos.defaults.api_data + api_settings = https_dict.get('api', {}) + if api_settings: + port = api_settings.get('port', '') + if port: + api_data['port'] = port + vhosts = https_dict.get('api-restrict', {}).get('virtual-host', []) + # XXX: T2636 workaround: convert string to a list with one element + if not isinstance(vhosts, list): + vhosts = [vhosts] + if vhosts: + api_data['vhost'] = vhosts[:] + + if api_data: + vhost_list = api_data.get('vhost', []) + if not vhost_list: + for block in server_block_list: + block['api'] = api_data + else: + for block in server_block_list: + if block['id'] in vhost_list: + block['api'] = api_data + + # return dict for use in template + + https = {'server_block_list' : server_block_list, + 'api_set': api_set, + 'certbot': certbot} + + return https + +def verify(https): + if https is None: + return None + + if https['certbot']: + for sb in https['server_block_list']: + if sb['certbot']: + return None + raise ConfigError("At least one 'virtual-host <id> server-name' " + "matching the 'certbot domain-name' is required.") + return None + +def generate(https): + if https is None: + return None + + if 'server_block_list' not in https or not https['server_block_list']: + https['server_block_list'] = [default_server_block] + + render(config_file, 'https/nginx.default.tmpl', https, trim_blocks=True) + + return None + +def apply(https): + if https is not None: + call('systemctl restart nginx.service') + else: + call('systemctl stop nginx.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py new file mode 100755 index 000000000..49aea9b7f --- /dev/null +++ b/src/conf_mode/igmp_proxy.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy + +from netifaces import interfaces +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/etc/igmpproxy.conf' + +default_config_data = { + 'disable': False, + 'disable_quickleave': False, + 'interfaces': [], +} + +def get_config(): + igmp_proxy = deepcopy(default_config_data) + conf = Config() + base = ['protocols', 'igmp-proxy'] + if not conf.exists(base): + return None + else: + conf.set_level(base) + + # Network interfaces to listen on + if conf.exists(['disable']): + igmp_proxy['disable'] = True + + # Option to disable "quickleave" + if conf.exists(['disable-quickleave']): + igmp_proxy['disable_quickleave'] = True + + for intf in conf.list_nodes(['interface']): + conf.set_level(base + ['interface', intf]) + interface = { + 'name': intf, + 'alt_subnet': [], + 'role': 'downstream', + 'threshold': '1', + 'whitelist': [] + } + + if conf.exists(['alt-subnet']): + interface['alt_subnet'] = conf.return_values(['alt-subnet']) + + if conf.exists(['role']): + interface['role'] = conf.return_value(['role']) + + if conf.exists(['threshold']): + interface['threshold'] = conf.return_value(['threshold']) + + if conf.exists(['whitelist']): + interface['whitelist'] = conf.return_values(['whitelist']) + + # Append interface configuration to global configuration list + igmp_proxy['interfaces'].append(interface) + + return igmp_proxy + +def verify(igmp_proxy): + # bail out early - looks like removal from running config + if igmp_proxy is None: + return None + + # bail out early - service is disabled + if igmp_proxy['disable']: + return None + + # at least two interfaces are required, one upstream and one downstream + if len(igmp_proxy['interfaces']) < 2: + raise ConfigError('Must define an upstream and at least 1 downstream interface!') + + upstream = 0 + for interface in igmp_proxy['interfaces']: + if interface['name'] not in interfaces(): + raise ConfigError('Interface "{}" does not exist'.format(interface['name'])) + if "upstream" == interface['role']: + upstream += 1 + + if upstream == 0: + raise ConfigError('At least 1 upstream interface is required!') + elif upstream > 1: + raise ConfigError('Only 1 upstream interface allowed!') + + return None + +def generate(igmp_proxy): + # bail out early - looks like removal from running config + if igmp_proxy is None: + return None + + # bail out early - service is disabled, but inform user + if igmp_proxy['disable']: + print('Warning: IGMP Proxy will be deactivated because it is disabled') + return None + + render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy) + return None + +def apply(igmp_proxy): + if igmp_proxy is None or igmp_proxy['disable']: + # IGMP Proxy support is removed in the commit + call('systemctl stop igmpproxy.service') + if os.path.exists(config_file): + os.unlink(config_file) + else: + call('systemctl restart igmpproxy.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py new file mode 100755 index 000000000..742f09a54 --- /dev/null +++ b/src/conf_mode/intel_qat.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import popen, run + +from vyos import airbag +airbag.enable() + +# Define for recovering +gl_ipsec_conf = None + +def get_config(): + c = Config() + config_data = { + 'qat_conf' : None, + 'ipsec_conf' : None, + 'openvpn_conf' : None, + } + + if c.exists('system acceleration qat'): + config_data['qat_conf'] = True + + if c.exists('vpn ipsec '): + gl_ipsec_conf = True + config_data['ipsec_conf'] = True + + if c.exists('interfaces openvpn'): + config_data['openvpn_conf'] = True + + return config_data + +# Control configured VPN service which can use QAT +def vpn_control(action): + # XXX: Should these commands report failure + if action == 'restore' and gl_ipsec_conf: + return run('ipsec start') + return run(f'ipsec {action}') + +def verify(c): + # Check if QAT service installed + if not os.path.exists('/etc/init.d/qat_service'): + raise ConfigError("Warning: QAT init file not found") + + if c['qat_conf'] == None: + return + + # Check if QAT device exist + output, err = popen('lspci -nn', decode='utf-8') + if not err: + data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output) + #If QAT devices found + if not data: + print("\t No QAT acceleration device found") + sys.exit(1) + +def apply(c): + if c['ipsec_conf']: + # Shutdown VPN service which can use QAT + vpn_control('stop') + + # Disable QAT service + if c['qat_conf'] == None: + run('/etc/init.d/qat_service stop') + if c['ipsec_conf']: + vpn_control('start') + return + + # Run qat init.d script + run('/etc/init.d/qat_service start') + if c['ipsec_conf']: + # Recovery VPN service + vpn_control('start') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + vpn_control('restore') + sys.exit(1) diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py new file mode 100755 index 000000000..3b238f1ea --- /dev/null +++ b/src/conf_mode/interfaces-bonding.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import BondIf +from vyos.validate import is_member +from vyos.validate import has_address_configured +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_bond_mode(mode): + if mode == 'round-robin': + return 'balance-rr' + elif mode == 'active-backup': + return 'active-backup' + elif mode == 'xor-hash': + return 'balance-xor' + elif mode == 'broadcast': + return 'broadcast' + elif mode == '802.3ad': + return '802.3ad' + elif mode == 'transmit-load-balance': + return 'balance-tlb' + elif mode == 'adaptive-load-balance': + return 'balance-alb' + else: + raise ConfigError(f'invalid bond mode "{mode}"') + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'bonding'] + bond = get_interface_dict(conf, base) + + # To make our own life easier transfor the list of member interfaces + # into a dictionary - we will use this to add additional information + # later on for wach member + if 'member' in bond and 'interface' in bond['member']: + # first convert it to a list if only one member is given + if isinstance(bond['member']['interface'], str): + bond['member']['interface'] = [bond['member']['interface']] + + tmp={} + for interface in bond['member']['interface']: + tmp.update({interface: {}}) + + bond['member']['interface'] = tmp + + if 'mode' in bond: + bond['mode'] = get_bond_mode(bond['mode']) + + tmp = leaf_node_changed(conf, ['mode']) + if tmp: + bond.update({'shutdown_required': ''}) + + # determine which members have been removed + tmp = leaf_node_changed(conf, ['member', 'interface']) + if tmp: + bond.update({'shutdown_required': ''}) + if 'member' in bond: + bond['member'].update({'interface_remove': tmp }) + else: + bond.update({'member': {'interface_remove': tmp }}) + + if 'member' in bond and 'interface' in bond['member']: + for interface, interface_config in bond['member']['interface'].items(): + # Check if we are a member of another bond device + tmp = is_member(conf, interface, 'bridge') + if tmp: + interface_config.update({'is_bridge_member' : tmp}) + + # Check if we are a member of a bond device + tmp = is_member(conf, interface, 'bonding') + if tmp and tmp != bond['ifname']: + interface_config.update({'is_bond_member' : tmp}) + + # bond members must not have an assigned address + tmp = has_address_configured(conf, interface) + if tmp: + interface_config.update({'has_address' : ''}) + + return bond + + +def verify(bond): + if 'deleted' in bond: + verify_bridge_delete(bond) + return None + + if 'arp_monitor' in bond: + if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16: + raise ConfigError('The maximum number of arp-monitor targets is 16') + + if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0: + if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: + raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ + 'transmit-load-balance or adaptive-load-balance') + + if 'primary' in bond: + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('Option primary - mode dependency failed, not' + 'supported in mode {mode}!'.format(**bond)) + + verify_address(bond) + verify_dhcpv6(bond) + verify_vrf(bond) + + # use common function to verify VLAN configuration + verify_vlan_config(bond) + + bond_name = bond['ifname'] + if 'member' in bond: + member = bond.get('member') + for interface, interface_config in member.get('interface', {}).items(): + error_msg = f'Can not add interface "{interface}" to bond "{bond_name}", ' + + if interface == 'lo': + raise ConfigError('Loopback interface "lo" can not be added to a bond') + + if interface not in interfaces(): + raise ConfigError(error_msg + 'it does not exist!') + + if 'is_bridge_member' in interface_config: + tmp = interface_config['is_bridge_member'] + raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') + + if 'is_bond_member' in interface_config: + tmp = interface_config['is_bond_member'] + raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') + + if 'has_address' in interface_config: + raise ConfigError(error_msg + 'it has an address assigned!') + + + if 'primary' in bond: + if bond['primary'] not in bond['member']['interface']: + raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface') + + if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: + raise ConfigError('primary interface only works for mode active-backup, ' \ + 'transmit-load-balance or adaptive-load-balance') + + return None + +def generate(bond): + return None + +def apply(bond): + b = BondIf(bond['ifname']) + + if 'deleted' in bond: + # delete interface + b.remove() + else: + b.update(bond) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py new file mode 100755 index 000000000..ee8e85e73 --- /dev/null +++ b/src/conf_mode/interfaces-bridge.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_vrf +from vyos.ifconfig import BridgeIf +from vyos.validate import is_member, has_address_configured +from vyos.xml import defaults + +from vyos.util import cmd +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'bridge'] + bridge = get_interface_dict(conf, base) + + # determine which members have been removed + tmp = node_changed(conf, ['member', 'interface']) + if tmp: + if 'member' in bridge: + bridge['member'].update({'interface_remove': tmp }) + else: + bridge.update({'member': {'interface_remove': tmp }}) + + if 'member' in bridge and 'interface' in bridge['member']: + # XXX TT2665 we need a copy of the dict keys for iteration, else we will get: + # RuntimeError: dictionary changed size during iteration + for interface in list(bridge['member']['interface']): + for key in ['cost', 'priority']: + if interface == key: + del bridge['member']['interface'][key] + continue + + # the default dictionary is not properly paged into the dict (see T2665) + # thus we will ammend it ourself + default_member_values = defaults(base + ['member', 'interface']) + for interface, interface_config in bridge['member']['interface'].items(): + interface_config.update(default_member_values) + + # Check if we are a member of another bridge device + tmp = is_member(conf, interface, 'bridge') + if tmp and tmp != bridge['ifname']: + interface_config.update({'is_bridge_member' : tmp}) + + # Check if we are a member of a bond device + tmp = is_member(conf, interface, 'bonding') + if tmp: + interface_config.update({'is_bond_member' : tmp}) + + # Bridge members must not have an assigned address + tmp = has_address_configured(conf, interface) + if tmp: + interface_config.update({'has_address' : ''}) + + return bridge + +def verify(bridge): + if 'deleted' in bridge: + return None + + verify_dhcpv6(bridge) + verify_vrf(bridge) + + if 'member' in bridge: + member = bridge.get('member') + bridge_name = bridge['ifname'] + for interface, interface_config in member.get('interface', {}).items(): + error_msg = f'Can not add interface "{interface}" to bridge "{bridge_name}", ' + + if interface == 'lo': + raise ConfigError('Loopback interface "lo" can not be added to a bridge') + + if interface not in interfaces(): + raise ConfigError(error_msg + 'it does not exist!') + + if 'is_bridge_member' in interface_config: + tmp = interface_config['is_bridge_member'] + raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') + + if 'is_bond_member' in interface_config: + tmp = interface_config['is_bond_member'] + raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') + + if 'has_address' in interface_config: + raise ConfigError(error_msg + 'it has an address assigned!') + + return None + +def generate(bridge): + return None + +def apply(bridge): + br = BridgeIf(bridge['ifname']) + if 'deleted' in bridge: + # delete interface + br.remove() + else: + br.update(bridge) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py new file mode 100755 index 000000000..8df86c8ea --- /dev/null +++ b/src/conf_mode/interfaces-dummy.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.ifconfig import DummyIf +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'dummy'] + dummy = get_interface_dict(conf, base) + return dummy + +def verify(dummy): + if 'deleted' in dummy.keys(): + verify_bridge_delete(dummy) + return None + + verify_vrf(dummy) + verify_address(dummy) + + return None + +def generate(dummy): + return None + +def apply(dummy): + d = DummyIf(dummy['ifname']) + + # Remove dummy interface + if 'deleted' in dummy.keys(): + d.remove() + else: + d.update(dummy) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py new file mode 100755 index 000000000..10758e35a --- /dev/null +++ b/src/conf_mode/interfaces-ethernet.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_interface_exists +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_address +from vyos.configverify import verify_vrf +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import EthernetIf +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'ethernet'] + ethernet = get_interface_dict(conf, base) + return ethernet + +def verify(ethernet): + if 'deleted' in ethernet: + return None + + verify_interface_exists(ethernet) + + if ethernet.get('speed', None) == 'auto': + if ethernet.get('duplex', None) != 'auto': + raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') + + if ethernet.get('duplex', None) == 'auto': + if ethernet.get('speed', None) != 'auto': + raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too') + + verify_dhcpv6(ethernet) + verify_address(ethernet) + verify_vrf(ethernet) + + if {'is_bond_member', 'mac'} <= set(ethernet): + print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" ' + f'is a member of bond "{is_bond_member}"'.format(**ethernet)) + + # use common function to verify VLAN configuration + verify_vlan_config(ethernet) + return None + +def generate(ethernet): + return None + +def apply(ethernet): + e = EthernetIf(ethernet['ifname']) + if 'deleted' in ethernet: + # delete interface + e.remove() + else: + e.update(ethernet) + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py new file mode 100755 index 000000000..1104bd3c0 --- /dev/null +++ b/src/conf_mode/interfaces-geneve.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.ifconfig import GeneveIf +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'geneve'] + geneve = get_interface_dict(conf, base) + return geneve + +def verify(geneve): + if 'deleted' in geneve: + verify_bridge_delete(geneve) + return None + + verify_address(geneve) + + if 'remote' not in geneve: + raise ConfigError('Remote side must be configured') + + if 'vni' not in geneve: + raise ConfigError('VNI must be configured') + + return None + + +def generate(geneve): + return None + + +def apply(geneve): + # Check if GENEVE interface already exists + if geneve['ifname'] in interfaces(): + g = GeneveIf(geneve['ifname']) + # GENEVE is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + g.remove() + + if 'deleted' not in geneve: + # GENEVE interface needs to be created on-block + # instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(GeneveIf.get_config()) + + # Assign GENEVE instance configuration parameters to config dict + conf['vni'] = geneve['vni'] + conf['remote'] = geneve['remote'] + + # Finally create the new interface + g = GeneveIf(geneve['ifname'], **conf) + g.update(geneve) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py new file mode 100755 index 000000000..0978df5b6 --- /dev/null +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.ifconfig import L2TPv3If +from vyos.util import check_kmod +from vyos.validate import is_addr_assigned +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] + + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'l2tpv3'] + l2tpv3 = get_interface_dict(conf, base) + + # L2TPv3 is "special" the default MTU is 1488 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + l2tpv3['mtu'] = '1488' + + # To delete an l2tpv3 interface we need the current tunnel and session-id + if 'deleted' in l2tpv3: + tmp = leaf_node_changed(conf, ['tunnel-id']) + l2tpv3.update({'tunnel_id': tmp}) + + tmp = leaf_node_changed(conf, ['session-id']) + l2tpv3.update({'session_id': tmp}) + + return l2tpv3 + +def verify(l2tpv3): + if 'deleted' in l2tpv3: + verify_bridge_delete(l2tpv3) + return None + + interface = l2tpv3['ifname'] + + for key in ['local_ip', 'remote_ip', 'tunnel_id', 'peer_tunnel_id', + 'session_id', 'peer_session_id']: + if key not in l2tpv3: + tmp = key.replace('_', '-') + raise ConfigError(f'L2TPv3 {tmp} must be configured!') + + if not is_addr_assigned(l2tpv3['local_ip']): + raise ConfigError('L2TPv3 local-ip address ' + '"{local_ip}" is not configured!'.format(**l2tpv3)) + + verify_address(l2tpv3) + return None + +def generate(l2tpv3): + return None + +def apply(l2tpv3): + # L2TPv3 interface needs to be created/deleted on-block, instead of + # passing a ton of arguments, I just use a dict that is managed by + # vyos.ifconfig + conf = deepcopy(L2TPv3If.get_config()) + + # Check if L2TPv3 interface already exists + if l2tpv3['ifname'] in interfaces(): + # L2TPv3 is picky when changing tunnels/sessions, thus we can simply + # always delete it first. + conf['session_id'] = l2tpv3['session_id'] + conf['tunnel_id'] = l2tpv3['tunnel_id'] + l = L2TPv3If(l2tpv3['ifname'], **conf) + l.remove() + + if 'deleted' not in l2tpv3: + conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id'] + conf['local_port'] = l2tpv3['source_port'] + conf['remote_port'] = l2tpv3['destination_port'] + conf['encapsulation'] = l2tpv3['encapsulation'] + conf['local_address'] = l2tpv3['local_ip'] + conf['remote_address'] = l2tpv3['remote_ip'] + conf['session_id'] = l2tpv3['session_id'] + conf['tunnel_id'] = l2tpv3['tunnel_id'] + conf['peer_session_id'] = l2tpv3['peer_session_id'] + + # Finally create the new interface + l = L2TPv3If(l2tpv3['ifname'], **conf) + l.update(l2tpv3) + + return None + +if __name__ == '__main__': + try: + check_kmod(k_mod) + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py new file mode 100755 index 000000000..0398cd591 --- /dev/null +++ b/src/conf_mode/interfaces-loopback.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.ifconfig import LoopbackIf +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'loopback'] + loopback = get_interface_dict(conf, base) + return loopback + +def verify(loopback): + return None + +def generate(loopback): + return None + +def apply(loopback): + l = LoopbackIf(loopback['ifname']) + if 'deleted' in loopback.keys(): + l.remove() + else: + l.update(loopback) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py new file mode 100755 index 000000000..ca15212d4 --- /dev/null +++ b/src/conf_mode/interfaces-macsec.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from copy import deepcopy +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.ifconfig import MACsecIf +from vyos.template import render +from vyos.util import call +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +# XXX: wpa_supplicant works on the source interface +wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'macsec'] + macsec = get_interface_dict(conf, base) + + # Check if interface has been removed + if 'deleted' in macsec: + source_interface = conf.return_effective_value( + base + ['source-interface']) + macsec.update({'source_interface': source_interface}) + + return macsec + + +def verify(macsec): + if 'deleted' in macsec: + verify_bridge_delete(macsec) + return None + + verify_source_interface(macsec) + verify_vrf(macsec) + verify_address(macsec) + + if not (('security' in macsec) and + ('cipher' in macsec['security'])): + raise ConfigError( + 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec)) + + if (('security' in macsec) and + ('encrypt' in macsec['security'])): + tmp = macsec.get('security') + + if not (('mka' in tmp) and + ('cak' in tmp['mka']) and + ('ckn' in tmp['mka'])): + raise ConfigError('Missing mandatory MACsec security ' + 'keys as encryption is enabled!') + + return None + + +def generate(macsec): + render(wpa_suppl_conf.format(**macsec), + 'macsec/wpa_supplicant.conf.tmpl', macsec) + return None + + +def apply(macsec): + # Remove macsec interface + if 'deleted' in macsec.keys(): + call('systemctl stop wpa_supplicant-macsec@{source_interface}' + .format(**macsec)) + + MACsecIf(macsec['ifname']).remove() + + # delete configuration on interface removal + if os.path.isfile(wpa_suppl_conf.format(**macsec)): + os.unlink(wpa_suppl_conf.format(**macsec)) + + else: + # MACsec interfaces require a configuration when they are added using + # iproute2. This static method will provide the configuration + # dictionary used by this class. + + # XXX: subject of removal after completing T2653 + conf = deepcopy(MACsecIf.get_config()) + conf['source_interface'] = macsec['source_interface'] + conf['security_cipher'] = macsec['security']['cipher'] + + # It is safe to "re-create" the interface always, there is a sanity + # check that the interface will only be create if its non existent + i = MACsecIf(macsec['ifname'], **conf) + i.update(macsec) + + call('systemctl restart wpa_supplicant-macsec@{source_interface}' + .format(**macsec)) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py new file mode 100755 index 000000000..1420b4116 --- /dev/null +++ b/src/conf_mode/interfaces-openvpn.py @@ -0,0 +1,1116 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from copy import deepcopy +from sys import exit,stderr +from ipaddress import ip_address,ip_network,IPv4Address,IPv4Network,IPv6Address,IPv6Network,summarize_address_range +from netifaces import interfaces +from time import sleep +from shutil import rmtree + +from vyos.config import Config +from vyos.configdict import list_diff +from vyos.ifconfig import VTunIf +from vyos.template import render +from vyos.util import call, chown, chmod_600, chmod_755 +from vyos.validate import is_addr_assigned, is_member, is_ipv4 +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +user = 'openvpn' +group = 'openvpn' + +default_config_data = { + 'address': [], + 'auth_user': '', + 'auth_pass': '', + 'auth_user_pass_file': '', + 'auth': False, + 'compress_lzo': False, + 'deleted': False, + 'description': '', + 'disable': False, + 'disable_ncp': False, + 'encryption': '', + 'hash': '', + 'intf': '', + 'ipv6_accept_ra': 1, + 'ipv6_autoconf': 0, + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], + 'ipv6_forwarding': 1, + 'ipv6_dup_addr_detect': 1, + 'ipv6_local_address': [], + 'ipv6_remote_address': [], + 'is_bridge_member': False, + 'ping_restart': '60', + 'ping_interval': '10', + 'local_address': [], + 'local_address_subnet': '', + 'local_host': '', + 'local_port': '', + 'mode': '', + 'ncp_ciphers': '', + 'options': [], + 'persistent_tunnel': False, + 'protocol': 'udp', + 'protocol_real': '', + 'redirect_gateway': '', + 'remote_address': [], + 'remote_host': [], + 'remote_port': '', + 'client': [], + 'server_domain': '', + 'server_max_conn': '', + 'server_dns_nameserver': [], + 'server_pool': True, + 'server_pool_start': '', + 'server_pool_stop': '', + 'server_pool_netmask': '', + 'server_push_route': [], + 'server_reject_unconfigured': False, + 'server_subnet': [], + 'server_topology': '', + 'server_ipv6_dns_nameserver': [], + 'server_ipv6_local': '', + 'server_ipv6_prefixlen': '', + 'server_ipv6_remote': '', + 'server_ipv6_pool': True, + 'server_ipv6_pool_base': '', + 'server_ipv6_pool_prefixlen': '', + 'server_ipv6_push_route': [], + 'server_ipv6_subnet': [], + 'shared_secret_file': '', + 'tls': False, + 'tls_auth': '', + 'tls_ca_cert': '', + 'tls_cert': '', + 'tls_crl': '', + 'tls_dh': '', + 'tls_key': '', + 'tls_crypt': '', + 'tls_role': '', + 'tls_version_min': '', + 'type': 'tun', + 'uid': user, + 'gid': group, + 'vrf': '' +} + + +def get_config_name(intf): + cfg_file = f'/run/openvpn/{intf}.conf' + return cfg_file + + +def checkCertHeader(header, filename): + """ + Verify if filename contains specified header. + Returns True if match is found, False if no match or file is not found + """ + if not os.path.isfile(filename): + return False + + with open(filename, 'r') as f: + for line in f: + if re.match(header, line): + return True + + return False + +def getDefaultServer(network, topology, devtype): + """ + Gets the default server parameters for a IPv4 "server" directive. + Logic from openvpn's src/openvpn/helper.c. + Returns a dict with addresses or False if the input parameters were incorrect. + """ + if not (devtype == 'tun' or devtype == 'tap'): + return False + + if not network.version == 4: + return False + elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30): + return False + + server = { + 'local': '', + 'remote_netmask': '', + 'client_remote_netmask': '', + 'pool_start': '', + 'pool_stop': '', + 'pool_netmask': '' + } + + if devtype == 'tun': + if topology == 'net30' or topology == 'point-to-point': + server['local'] = network[1] + server['remote_netmask'] = network[2] + server['client_remote_netmask'] = server['local'] + + # pool start is 4th host IP in subnet (.4 in a /24) + server['pool_start'] = network[4] + + if network.prefixlen == 29: + server['pool_stop'] = network.broadcast_address + else: + # pool end is -4 from the broadcast address (.251 in a /24) + server['pool_stop'] = network[-5] + + elif topology == 'subnet': + server['local'] = network[1] + server['remote_netmask'] = str(network.netmask) + server['client_remote_netmask'] = server['remote_netmask'] + server['pool_start'] = network[2] + server['pool_stop'] = network[-3] + server['pool_netmask'] = server['remote_netmask'] + + elif devtype == 'tap': + server['local'] = network[1] + server['remote_netmask'] = str(network.netmask) + server['client_remote_netmask'] = server['remote_netmask'] + server['pool_start'] = network[2] + server['pool_stop'] = network[-2] + server['pool_netmask'] = server['remote_netmask'] + + return server + +def get_config(): + openvpn = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw" + + # check if interface is member of a bridge + openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge') + + # Check if interface instance has been removed + if not conf.exists('interfaces openvpn ' + openvpn['intf']): + openvpn['deleted'] = True + return openvpn + + # bridged server should not have a pool by default (but can be specified manually) + if openvpn['is_bridge_member']: + openvpn['server_pool'] = False + openvpn['server_ipv6_pool'] = False + + # set configuration level + conf.set_level('interfaces openvpn ' + openvpn['intf']) + + # retrieve authentication options - username + if conf.exists('authentication username'): + openvpn['auth_user'] = conf.return_value('authentication username') + openvpn['auth'] = True + + # retrieve authentication options - username + if conf.exists('authentication password'): + openvpn['auth_pass'] = conf.return_value('authentication password') + openvpn['auth'] = True + + # retrieve interface description + if conf.exists('description'): + openvpn['description'] = conf.return_value('description') + + # interface device-type + if conf.exists('device-type'): + openvpn['type'] = conf.return_value('device-type') + + # disable interface + if conf.exists('disable'): + openvpn['disable'] = True + + # data encryption algorithm cipher + if conf.exists('encryption cipher'): + openvpn['encryption'] = conf.return_value('encryption cipher') + + # disable ncp-ciphers support + if conf.exists('encryption disable-ncp'): + openvpn['disable_ncp'] = True + + # data encryption algorithm ncp-list + if conf.exists('encryption ncp-ciphers'): + _ncp_ciphers = [] + for enc in conf.return_values('encryption ncp-ciphers'): + if enc == 'des': + _ncp_ciphers.append('des-cbc') + _ncp_ciphers.append('DES-CBC') + elif enc == '3des': + _ncp_ciphers.append('des-ede3-cbc') + _ncp_ciphers.append('DES-EDE3-CBC') + elif enc == 'aes128': + _ncp_ciphers.append('aes-128-cbc') + _ncp_ciphers.append('AES-128-CBC') + elif enc == 'aes128gcm': + _ncp_ciphers.append('aes-128-gcm') + _ncp_ciphers.append('AES-128-GCM') + elif enc == 'aes192': + _ncp_ciphers.append('aes-192-cbc') + _ncp_ciphers.append('AES-192-CBC') + elif enc == 'aes192gcm': + _ncp_ciphers.append('aes-192-gcm') + _ncp_ciphers.append('AES-192-GCM') + elif enc == 'aes256': + _ncp_ciphers.append('aes-256-cbc') + _ncp_ciphers.append('AES-256-CBC') + elif enc == 'aes256gcm': + _ncp_ciphers.append('aes-256-gcm') + _ncp_ciphers.append('AES-256-GCM') + openvpn['ncp_ciphers'] = ':'.join(_ncp_ciphers) + + # hash algorithm + if conf.exists('hash'): + openvpn['hash'] = conf.return_value('hash') + + # Maximum number of keepalive packet failures + if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'): + fail_count = conf.return_value('keep-alive failure-count') + interval = conf.return_value('keep-alive interval') + openvpn['ping_interval' ] = interval + openvpn['ping_restart' ] = int(interval) * int(fail_count) + + # Local IP address of tunnel - even as it is a tag node - we can only work + # on the first address + if conf.exists('local-address'): + for tmp in conf.list_nodes('local-address'): + tmp_ip = ip_address(tmp) + if tmp_ip.version == 4: + openvpn['local_address'].append(tmp) + if conf.exists('local-address {} subnet-mask'.format(tmp)): + openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(tmp)) + elif tmp_ip.version == 6: + # input IPv6 address could be expanded so get the compressed version + openvpn['ipv6_local_address'].append(str(tmp_ip)) + + # Local IP address to accept connections + if conf.exists('local-host'): + openvpn['local_host'] = conf.return_value('local-host') + + # Local port number to accept connections + if conf.exists('local-port'): + openvpn['local_port'] = conf.return_value('local-port') + + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) + if conf.exists('ipv6 address autoconf'): + openvpn['ipv6_autoconf'] = 1 + + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) + if conf.exists('ipv6 address eui64'): + openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') + + # Determine currently effective EUI64 addresses - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('ipv6 address eui64') + openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix']) + + # Remove the default link-local address if set. + if conf.exists('ipv6 address no-default-link-local'): + openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64') + else: + # add the link-local by default to make IPv6 work + openvpn['ipv6_eui64_prefix'].append('fe80::/64') + + # Disable IPv6 forwarding on this interface + if conf.exists('ipv6 disable-forwarding'): + openvpn['ipv6_forwarding'] = 0 + + # IPv6 Duplicate Address Detection (DAD) tries + if conf.exists('ipv6 dup-addr-detect-transmits'): + openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + + # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, + # accept_ra must be 2 + if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']: + openvpn['ipv6_accept_ra'] = 2 + + # OpenVPN operation mode + if conf.exists('mode'): + openvpn['mode'] = conf.return_value('mode') + + # Additional OpenVPN options + if conf.exists('openvpn-option'): + openvpn['options'] = conf.return_values('openvpn-option') + + # Do not close and reopen interface + if conf.exists('persistent-tunnel'): + openvpn['persistent_tunnel'] = True + + # Communication protocol + if conf.exists('protocol'): + openvpn['protocol'] = conf.return_value('protocol') + + # IP address of remote end of tunnel + if conf.exists('remote-address'): + for tmp in conf.return_values('remote-address'): + tmp_ip = ip_address(tmp) + if tmp_ip.version == 4: + openvpn['remote_address'].append(tmp) + elif tmp_ip.version == 6: + openvpn['ipv6_remote_address'].append(str(tmp_ip)) + + # Remote host to connect to (dynamic if not set) + if conf.exists('remote-host'): + openvpn['remote_host'] = conf.return_values('remote-host') + + # Remote port number to connect to + if conf.exists('remote-port'): + openvpn['remote_port'] = conf.return_value('remote-port') + + # OpenVPN tunnel to be used as the default route + # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/ + # redirect-gateway flags + if conf.exists('replace-default-route'): + openvpn['redirect_gateway'] = 'def1' + + if conf.exists('replace-default-route local'): + openvpn['redirect_gateway'] = 'local def1' + + # Topology for clients + if conf.exists('server topology'): + openvpn['server_topology'] = conf.return_value('server topology') + + # Server-mode subnet (from which client IPs are allocated) + server_network_v4 = None + server_network_v6 = None + if conf.exists('server subnet'): + for tmp in conf.return_values('server subnet'): + tmp_ip = ip_network(tmp) + if tmp_ip.version == 4: + server_network_v4 = tmp_ip + # convert the network to format: "192.0.2.0 255.255.255.0" for later use in template + openvpn['server_subnet'].append(tmp_ip.with_netmask.replace(r'/', ' ')) + elif tmp_ip.version == 6: + server_network_v6 = tmp_ip + openvpn['server_ipv6_subnet'].append(str(tmp_ip)) + + # Client-specific settings + for client in conf.list_nodes('server client'): + # set configuration level + conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client) + data = { + 'name': client, + 'disable': False, + 'ip': [], + 'ipv6_ip': [], + 'ipv6_remote': '', + 'ipv6_push_route': [], + 'ipv6_subnet': [], + 'push_route': [], + 'subnet': [], + 'remote_netmask': '' + } + + # Option to disable client connection + if conf.exists('disable'): + data['disable'] = True + + # IP address of the client + for tmp in conf.return_values('ip'): + tmp_ip = ip_address(tmp) + if tmp_ip.version == 4: + data['ip'].append(tmp) + elif tmp_ip.version == 6: + data['ipv6_ip'].append(str(tmp_ip)) + + # Route to be pushed to the client + for tmp in conf.return_values('push-route'): + tmp_ip = ip_network(tmp) + if tmp_ip.version == 4: + data['push_route'].append(tmp_ip.with_netmask.replace(r'/', ' ')) + elif tmp_ip.version == 6: + data['ipv6_push_route'].append(str(tmp_ip)) + + # Subnet belonging to the client + for tmp in conf.return_values('subnet'): + tmp_ip = ip_network(tmp) + if tmp_ip.version == 4: + data['subnet'].append(tmp_ip.with_netmask.replace(r'/', ' ')) + elif tmp_ip.version == 6: + data['ipv6_subnet'].append(str(tmp_ip)) + + # Append to global client list + openvpn['client'].append(data) + + # re-set configuration level + conf.set_level('interfaces openvpn ' + openvpn['intf']) + + # Server client IP pool + if conf.exists('server client-ip-pool'): + conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ip-pool') + + # enable or disable server_pool where necessary + # default is enabled, or disabled in bridge mode + openvpn['server_pool'] = not conf.exists('disable') + + if conf.exists('start'): + openvpn['server_pool_start'] = conf.return_value('start') + + if conf.exists('stop'): + openvpn['server_pool_stop'] = conf.return_value('stop') + + if conf.exists('netmask'): + openvpn['server_pool_netmask'] = conf.return_value('netmask') + + conf.set_level('interfaces openvpn ' + openvpn['intf']) + + # Server client IPv6 pool + if conf.exists('server client-ipv6-pool'): + conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ipv6-pool') + openvpn['server_ipv6_pool'] = not conf.exists('disable') + if conf.exists('base'): + tmp = conf.return_value('base').split('/') + openvpn['server_ipv6_pool_base'] = str(IPv6Address(tmp[0])) + if 1 < len(tmp): + openvpn['server_ipv6_pool_prefixlen'] = tmp[1] + + conf.set_level('interfaces openvpn ' + openvpn['intf']) + + # DNS suffix to be pushed to all clients + if conf.exists('server domain-name'): + openvpn['server_domain'] = conf.return_value('server domain-name') + + # Number of maximum client connections + if conf.exists('server max-connections'): + openvpn['server_max_conn'] = conf.return_value('server max-connections') + + # Domain Name Server (DNS) + if conf.exists('server name-server'): + for tmp in conf.return_values('server name-server'): + tmp_ip = ip_address(tmp) + if tmp_ip.version == 4: + openvpn['server_dns_nameserver'].append(tmp) + elif tmp_ip.version == 6: + openvpn['server_ipv6_dns_nameserver'].append(str(tmp_ip)) + + # Route to be pushed to all clients + if conf.exists('server push-route'): + for tmp in conf.return_values('server push-route'): + tmp_ip = ip_network(tmp) + if tmp_ip.version == 4: + openvpn['server_push_route'].append(tmp_ip.with_netmask.replace(r'/', ' ')) + elif tmp_ip.version == 6: + openvpn['server_ipv6_push_route'].append(str(tmp_ip)) + + # Reject connections from clients that are not explicitly configured + if conf.exists('server reject-unconfigured-clients'): + openvpn['server_reject_unconfigured'] = True + + # File containing TLS auth static key + if conf.exists('tls auth-file'): + openvpn['tls_auth'] = conf.return_value('tls auth-file') + openvpn['tls'] = True + + # File containing certificate for Certificate Authority (CA) + if conf.exists('tls ca-cert-file'): + openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file') + openvpn['tls'] = True + + # File containing certificate for this host + if conf.exists('tls cert-file'): + openvpn['tls_cert'] = conf.return_value('tls cert-file') + openvpn['tls'] = True + + # File containing certificate revocation list (CRL) for this host + if conf.exists('tls crl-file'): + openvpn['tls_crl'] = conf.return_value('tls crl-file') + openvpn['tls'] = True + + # File containing Diffie Hellman parameters (server only) + if conf.exists('tls dh-file'): + openvpn['tls_dh'] = conf.return_value('tls dh-file') + openvpn['tls'] = True + + # File containing this host's private key + if conf.exists('tls key-file'): + openvpn['tls_key'] = conf.return_value('tls key-file') + openvpn['tls'] = True + + # File containing key to encrypt control channel packets + if conf.exists('tls crypt-file'): + openvpn['tls_crypt'] = conf.return_value('tls crypt-file') + openvpn['tls'] = True + + # Role in TLS negotiation + if conf.exists('tls role'): + openvpn['tls_role'] = conf.return_value('tls role') + openvpn['tls'] = True + + # Minimum required TLS version + if conf.exists('tls tls-version-min'): + openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') + openvpn['tls'] = True + + if conf.exists('shared-secret-key-file'): + openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') + + if conf.exists('use-lzo-compression'): + openvpn['compress_lzo'] = True + + # Special case when using EC certificates: + # if key-file is EC and dh-file is unset, set tls_dh to 'none' + if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): + openvpn['tls_dh'] = 'none' + + # set default server topology to net30 + if openvpn['mode'] == 'server' and not openvpn['server_topology']: + openvpn['server_topology'] = 'net30' + + # Convert protocol to real protocol used by openvpn. + # To make openvpn listen on both IPv4 and IPv6 we must use *6 protocols + # (https://community.openvpn.net/openvpn/ticket/360), unless the local-host + # or each of the remote-host in client mode is IPv4 + # in which case it must use the standard protocols. + if openvpn['protocol'] == 'tcp-active': + openvpn['protocol_real'] = 'tcp6-client' + elif openvpn['protocol'] == 'tcp-passive': + openvpn['protocol_real'] = 'tcp6-server' + else: + openvpn['protocol_real'] = 'udp6' + + if ( is_ipv4(openvpn['local_host']) or + # in client mode test all the remotes instead + (openvpn['mode'] == 'client' and all([is_ipv4(h) for h in openvpn['remote_host']])) ): + # takes out the '6' + openvpn['protocol_real'] = openvpn['protocol_real'][:3] + openvpn['protocol_real'][4:] + + # Set defaults where necessary. + # If any of the input parameters are wrong, + # this will return False and no defaults will be set. + if server_network_v4 and openvpn['server_topology'] and openvpn['type']: + default_server = None + default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type']) + if default_server: + # server-bridge doesn't require a pool so don't set defaults for it + if openvpn['server_pool'] and not openvpn['is_bridge_member']: + if not openvpn['server_pool_start']: + openvpn['server_pool_start'] = default_server['pool_start'] + + if not openvpn['server_pool_stop']: + openvpn['server_pool_stop'] = default_server['pool_stop'] + + if not openvpn['server_pool_netmask']: + openvpn['server_pool_netmask'] = default_server['pool_netmask'] + + for client in openvpn['client']: + client['remote_netmask'] = default_server['client_remote_netmask'] + + if server_network_v6: + if not openvpn['server_ipv6_local']: + openvpn['server_ipv6_local'] = server_network_v6[1] + if not openvpn['server_ipv6_prefixlen']: + openvpn['server_ipv6_prefixlen'] = server_network_v6.prefixlen + if not openvpn['server_ipv6_remote']: + openvpn['server_ipv6_remote'] = server_network_v6[2] + + if openvpn['server_ipv6_pool'] and server_network_v6.prefixlen < 112: + if not openvpn['server_ipv6_pool_base']: + openvpn['server_ipv6_pool_base'] = server_network_v6[0x1000] + if not openvpn['server_ipv6_pool_prefixlen']: + openvpn['server_ipv6_pool_prefixlen'] = openvpn['server_ipv6_prefixlen'] + + for client in openvpn['client']: + client['ipv6_remote'] = openvpn['server_ipv6_local'] + + if openvpn['redirect_gateway']: + openvpn['redirect_gateway'] += ' ipv6' + + # retrieve VRF instance + if conf.exists('vrf'): + openvpn['vrf'] = conf.return_value('vrf') + + return openvpn + +def verify(openvpn): + if openvpn['deleted']: + if openvpn['is_bridge_member']: + raise ConfigError(( + f'Cannot delete interface "{openvpn["intf"]}" as it is a ' + f'member of bridge "{openvpn["is_bridge_menber"]}"!')) + return None + + + if not openvpn['mode']: + raise ConfigError('Must specify OpenVPN operation mode') + + # Check if we have disabled ncp and at the same time specified ncp-ciphers + if openvpn['disable_ncp'] and openvpn['ncp_ciphers']: + raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"') + # + # OpenVPN client mode - VERIFY + # + if openvpn['mode'] == 'client': + if openvpn['local_port']: + raise ConfigError('Cannot specify "local-port" in client mode') + + if openvpn['local_host']: + raise ConfigError('Cannot specify "local-host" in client mode') + + if openvpn['protocol'] == 'tcp-passive': + raise ConfigError('Protocol "tcp-passive" is not valid in client mode') + + if not openvpn['remote_host']: + raise ConfigError('Must specify "remote-host" in client mode') + + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': + raise ConfigError('Cannot specify "tls dh-file" in client mode') + + # + # OpenVPN site-to-site - VERIFY + # + if openvpn['mode'] == 'site-to-site': + if openvpn['ncp_ciphers']: + raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') + + if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']: + if not (openvpn['local_address'] or openvpn['ipv6_local_address']): + raise ConfigError('Must specify "local-address" or add interface to bridge') + + if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1: + raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"') + + if len(openvpn['remote_address']) > 1 or len(openvpn['ipv6_remote_address']) > 1: + raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "remote-address"') + + for host in openvpn['remote_host']: + if host in openvpn['remote_address'] or host in openvpn['ipv6_remote_address']: + raise ConfigError('"remote-address" cannot be the same as "remote-host"') + + if openvpn['local_address'] and not (openvpn['remote_address'] or openvpn['local_address_subnet']): + raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address" or IPv4 "local-address subnet"') + + if openvpn['remote_address'] and not openvpn['local_address']: + raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"') + + if openvpn['ipv6_local_address'] and not openvpn['ipv6_remote_address']: + raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"') + + if openvpn['ipv6_remote_address'] and not openvpn['ipv6_local_address']: + raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"') + + if openvpn['type'] == 'tun': + if not (openvpn['remote_address'] or openvpn['ipv6_remote_address']): + raise ConfigError('Must specify "remote-address"') + + if ( (openvpn['local_address'] and openvpn['local_address'] == openvpn['remote_address']) or + (openvpn['ipv6_local_address'] and openvpn['ipv6_local_address'] == openvpn['ipv6_remote_address']) ): + raise ConfigError('"local-address" and "remote-address" cannot be the same') + + if openvpn['local_host'] in openvpn['local_address'] or openvpn['local_host'] in openvpn['ipv6_local_address']: + raise ConfigError('"local-address" cannot be the same as "local-host"') + + else: + # checks for client-server or site-to-site bridged + if openvpn['local_address'] or openvpn['ipv6_local_address'] or openvpn['remote_address'] or openvpn['ipv6_remote_address']: + raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server or bridge mode') + + # + # OpenVPN server mode - VERIFY + # + if openvpn['mode'] == 'server': + if openvpn['protocol'] == 'tcp-active': + raise ConfigError('Protocol "tcp-active" is not valid in server mode') + + if openvpn['remote_port']: + raise ConfigError('Cannot specify "remote-port" in server mode') + + if openvpn['remote_host']: + raise ConfigError('Cannot specify "remote-host" in server mode') + + if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1: + raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"') + + if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): + raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode') + + if len(openvpn['server_subnet']) > 1 or len(openvpn['server_ipv6_subnet']) > 1: + raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 server subnet') + + for client in openvpn['client']: + if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1: + raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP') + + if openvpn['server_subnet']: + subnet = IPv4Network(openvpn['server_subnet'][0].replace(' ', '/')) + + if openvpn['type'] == 'tun' and subnet.prefixlen > 29: + raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported') + elif openvpn['type'] == 'tap' and subnet.prefixlen > 30: + raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported') + + for client in openvpn['client']: + if client['ip'] and not IPv4Address(client['ip'][0]) in subnet: + raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}') + + else: + if not openvpn['is_bridge_member']: + raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode') + + if openvpn['server_pool']: + if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']): + raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode') + else: + v4PoolStart = IPv4Address(openvpn['server_pool_start']) + v4PoolStop = IPv4Address(openvpn['server_pool_stop']) + if v4PoolStart > v4PoolStop: + raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}') + + v4PoolSize = int(v4PoolStop) - int(v4PoolStart) + if v4PoolSize >= 65536: + raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.') + + v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop)) + for client in openvpn['client']: + if client['ip']: + for v4PoolNet in v4PoolNets: + if IPv4Address(client['ip'][0]) in v4PoolNet: + print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.', + file=stderr) + + if openvpn['server_ipv6_subnet']: + if not openvpn['server_subnet']: + raise ConfigError('IPv6 server requires an IPv4 server subnet') + + if openvpn['server_ipv6_pool']: + if not openvpn['server_pool']: + raise ConfigError('IPv6 server pool requires an IPv4 server pool') + + if int(openvpn['server_ipv6_pool_prefixlen']) >= 112: + raise ConfigError('IPv6 server pool must be larger than /112') + + v6PoolStart = IPv6Address(openvpn['server_ipv6_pool_base']) + v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple + v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536 + if v6PoolSize < v4PoolSize: + raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})') + + v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop)) + for client in openvpn['client']: + if client['ipv6_ip']: + for v6PoolNet in v6PoolNets: + if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet: + print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.', + file=stderr) + + else: + if openvpn['server_ipv6_push_route']: + raise ConfigError('IPv6 push-route requires an IPv6 server subnet') + + for client in openvpn ['client']: + if client['ipv6_ip']: + raise ConfigError(f'Server client "{client["name"]}" IPv6 IP requires an IPv6 server subnet') + if client['ipv6_push_route']: + raise ConfigError(f'Server client "{client["name"]} IPv6 push-route requires an IPv6 server subnet"') + if client['ipv6_subnet']: + raise ConfigError(f'Server client "{client["name"]} IPv6 subnet requires an IPv6 server subnet"') + + else: + # checks for both client and site-to-site go here + if openvpn['server_reject_unconfigured']: + raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode') + + if openvpn['server_topology']: + raise ConfigError('The "topology" option is only valid in server mode') + + if (not openvpn['remote_host']) and openvpn['redirect_gateway']: + raise ConfigError('Cannot set "replace-default-route" without "remote-host"') + + # + # OpenVPN common verification section + # not depending on any operation mode + # + + # verify specified IP address is present on any interface on this system + if openvpn['local_host']: + if not is_addr_assigned(openvpn['local_host']): + raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host'])) + + # TCP active + if openvpn['protocol'] == 'tcp-active': + if openvpn['local_port']: + raise ConfigError('Cannot specify "local-port" with "tcp-active"') + + if not openvpn['remote_host']: + raise ConfigError('Must specify "remote-host" with "tcp-active"') + + # shared secret and TLS + if not (openvpn['shared_secret_file'] or openvpn['tls']): + raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"') + + if openvpn['shared_secret_file'] and openvpn['tls']: + raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"') + + if openvpn['mode'] in ['client', 'server']: + if not openvpn['tls']: + raise ConfigError('Must specify "tls" in client-server mode') + + # + # TLS/encryption + # + if openvpn['shared_secret_file']: + if openvpn['encryption'] in ['aes128gcm', 'aes192gcm', 'aes256gcm']: + raise ConfigError('GCM encryption with shared-secret-key-file is not supported') + + if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']): + raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file'])) + + if openvpn['tls']: + if not openvpn['tls_ca_cert']: + raise ConfigError('Must specify "tls ca-cert-file"') + + if not (openvpn['mode'] == 'client' and openvpn['auth']): + if not openvpn['tls_cert']: + raise ConfigError('Must specify "tls cert-file"') + + if not openvpn['tls_key']: + raise ConfigError('Must specify "tls key-file"') + + if openvpn['tls_auth'] and openvpn['tls_crypt']: + raise ConfigError('TLS auth and crypt are mutually exclusive') + + if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']): + raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert'])) + + if openvpn['tls_auth']: + if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_auth']): + raise ConfigError('Specified auth-file "{}" is invalid'.format(openvpn['tls_auth'])) + + if openvpn['tls_cert']: + if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']): + raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert'])) + + if openvpn['tls_key']: + if not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', openvpn['tls_key']): + raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key'])) + + if openvpn['tls_crypt']: + if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']): + raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt'])) + + if openvpn['tls_crl']: + if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']): + raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl'])) + + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': + if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']): + raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh'])) + + if openvpn['tls_role']: + if openvpn['mode'] in ['client', 'server']: + if not openvpn['tls_auth']: + raise ConfigError('Cannot specify "tls role" in client-server mode') + + if openvpn['tls_role'] == 'active': + if openvpn['protocol'] == 'tcp-passive': + raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"') + + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': + raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"') + + elif openvpn['tls_role'] == 'passive': + if openvpn['protocol'] == 'tcp-active': + raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"') + + if not openvpn['tls_dh']: + raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"') + + if openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': + print('Warning: using dh-file and EC keys simultaneously will lead to DH ciphers being used instead of ECDH') + else: + print('Diffie-Hellman prime file is unspecified, assuming ECDH') + + # + # Auth user/pass + # + if openvpn['auth']: + if not openvpn['auth_user']: + raise ConfigError('Username for authentication is missing') + + if not openvpn['auth_pass']: + raise ConfigError('Password for authentication is missing') + + if openvpn['vrf']: + if openvpn['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{openvpn["vrf"]}" does not exist') + + if openvpn['is_bridge_member']: + raise ConfigError(( + f'Interface "{openvpn["intf"]}" cannot be member of VRF ' + f'"{openvpn["vrf"]}" and bridge "{openvpn["is_bridge_member"]}" ' + f'at the same time!')) + + return None + +def generate(openvpn): + interface = openvpn['intf'] + directory = os.path.dirname(get_config_name(interface)) + + # we can't know in advance which clients have been removed, + # thus all client configs will be removed and re-added on demand + ccd_dir = os.path.join(directory, 'ccd', interface) + if os.path.isdir(ccd_dir): + rmtree(ccd_dir, ignore_errors=True) + + if openvpn['deleted'] or openvpn['disable']: + return None + + # create config directory on demand + directories = [] + directories.append(f'{directory}/status') + directories.append(f'{directory}/ccd/{interface}') + for onedir in directories: + if not os.path.exists(onedir): + os.makedirs(onedir, 0o755) + chown(onedir, user, group) + + # Fix file permissons for keys + fix_permissions = [] + fix_permissions.append(openvpn['shared_secret_file']) + fix_permissions.append(openvpn['tls_key']) + + # Generate User/Password authentication file + if openvpn['auth']: + with open(openvpn['auth_user_pass_file'], 'w') as f: + f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) + # also change permission on auth file + fix_permissions.append(openvpn['auth_user_pass_file']) + + else: + # delete old auth file if present + if os.path.isfile(openvpn['auth_user_pass_file']): + os.remove(openvpn['auth_user_pass_file']) + + # Generate client specific configuration + for client in openvpn['client']: + client_file = os.path.join(ccd_dir, client['name']) + render(client_file, 'openvpn/client.conf.tmpl', client) + chown(client_file, user, group) + + # we need to support quoting of raw parameters from OpenVPN CLI + # see https://phabricator.vyos.net/T1632 + render(get_config_name(interface), 'openvpn/server.conf.tmpl', openvpn, + formater=lambda _: _.replace(""", '"')) + chown(get_config_name(interface), user, group) + + # Fixup file permissions + for file in fix_permissions: + chmod_600(file) + + return None + +def apply(openvpn): + interface = openvpn['intf'] + call(f'systemctl stop openvpn@{interface}.service') + + # Do some cleanup when OpenVPN is disabled/deleted + if openvpn['deleted'] or openvpn['disable']: + # cleanup old configuration files + cleanup = [] + cleanup.append(get_config_name(interface)) + cleanup.append(openvpn['auth_user_pass_file']) + + for file in cleanup: + if os.path.isfile(file): + os.unlink(file) + + return None + + # On configuration change we need to wait for the 'old' interface to + # vanish from the Kernel, if it is not gone, OpenVPN will report: + # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16) + while interface in interfaces(): + sleep(0.250) # 250ms + + # No matching OpenVPN process running - maybe it got killed or none + # existed - nevertheless, spawn new OpenVPN process + call(f'systemctl start openvpn@{interface}.service') + + # better late then sorry ... but we can only set interface alias after + # OpenVPN has been launched and created the interface + cnt = 0 + while interface not in interfaces(): + # If VPN tunnel can't be established because the peer/server isn't + # (temporarily) available, the vtun interface never becomes registered + # with the kernel, and the commit would hang if there is no bail out + # condition + cnt += 1 + if cnt == 50: + break + + # sleep 250ms + sleep(0.250) + + try: + # we need to catch the exception if the interface is not up due to + # reason stated above + o = VTunIf(interface) + # update interface description used e.g. within SNMP + o.set_alias(openvpn['description']) + # IPv6 accept RA + o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra']) + # IPv6 address autoconfiguration + o.set_ipv6_autoconf(openvpn['ipv6_autoconf']) + # IPv6 forwarding + o.set_ipv6_forwarding(openvpn['ipv6_forwarding']) + # IPv6 Duplicate Address Detection (DAD) tries + o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect']) + + # IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC) + # If MAC has changed, old EUI64 addresses won't get deleted, + # but this isn't easy to solve, so leave them. + # This is even more difficult as openvpn uses a random MAC for the + # initial interface creation, unless set by 'lladdr'. + # NOTE: right now the interface is always deleted. For future + # compatibility when tap's are not deleted, leave the del_ in + if openvpn['mode'] == 'tap': + for addr in openvpn['ipv6_eui64_prefix_remove']: + o.del_ipv6_eui64_address(addr) + for addr in openvpn['ipv6_eui64_prefix']: + o.add_ipv6_eui64_address(addr) + + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not openvpn['is_bridge_member']: + o.set_vrf(openvpn['vrf']) + + except: + pass + + # TAP interface needs to be brought up explicitly + if openvpn['type'] == 'tap': + if not openvpn['disable']: + VTunIf(interface).set_admin_state('up') + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py new file mode 100755 index 000000000..901ea769c --- /dev/null +++ b/src/conf_mode/interfaces-pppoe.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vrf +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'pppoe'] + pppoe = get_interface_dict(conf, base) + + # PPPoE is "special" the default MTU is 1492 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + pppoe['mtu'] = '1492' + + return pppoe + +def verify(pppoe): + if 'deleted' in pppoe: + # bail out early + return None + + verify_source_interface(pppoe) + verify_vrf(pppoe) + + if {'connect_on_demand', 'vrf'} <= set(pppoe): + raise ConfigError('On-demand dialing and VRF can not be used at the same time') + + return None + +def generate(pppoe): + # set up configuration file path variables where our templates will be + # rendered into + ifname = pppoe['ifname'] + config_pppoe = f'/etc/ppp/peers/{ifname}' + script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{ifname}' + script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{ifname}' + script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{ifname}' + script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{ifname}' + config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{ifname}.conf' + + config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, + script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c] + + if 'deleted' in pppoe: + # stop DHCPv6-PD client + call(f'systemctl stop dhcp6c@{ifname}.service') + # Hang-up PPPoE connection + call(f'systemctl stop ppp@{ifname}.service') + + # Delete PPP configuration files + for file in config_files: + if os.path.exists(file): + os.unlink(file) + + return None + + # Create PPP configuration files + render(config_pppoe, 'pppoe/peer.tmpl', + pppoe, trim_blocks=True, permission=0o755) + # Create script for ip-pre-up.d + render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', + pppoe, trim_blocks=True, permission=0o755) + # Create script for ip-up.d + render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', + pppoe, trim_blocks=True, permission=0o755) + # Create script for ip-down.d + render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', + pppoe, trim_blocks=True, permission=0o755) + # Create script for ipv6-up.d + render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', + pppoe, trim_blocks=True, permission=0o755) + + if 'dhcpv6_options' in pppoe and 'pd' in pppoe['dhcpv6_options']: + # ipv6.tmpl relies on ifname - this should be made consitent in the + # future better then double key-ing the same value + render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True) + + return None + +def apply(pppoe): + if 'deleted' in pppoe: + # bail out early + return None + + if 'disable' not in pppoe: + # Dial PPPoE connection + call('systemctl restart ppp@{ifname}.service'.format(**pppoe)) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py new file mode 100755 index 000000000..fe2d7b1be --- /dev/null +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from copy import deepcopy +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.ifconfig import MACVLANIf +from vyos.validate import is_member +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at + least the interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'pseudo-ethernet'] + peth = get_interface_dict(conf, base) + + mode = leaf_node_changed(conf, ['mode']) + if mode: + peth.update({'mode_old' : mode}) + + # Check if source-interface is member of a bridge device + if 'source_interface' in peth: + bridge = is_member(conf, peth['source_interface'], 'bridge') + if bridge: + peth.update({'source_interface_is_bridge_member' : bridge}) + + # Check if we are a member of a bond device + bond = is_member(conf, peth['source_interface'], 'bonding') + if bond: + peth.update({'source_interface_is_bond_member' : bond}) + + return peth + +def verify(peth): + if 'deleted' in peth: + verify_bridge_delete(peth) + return None + + verify_source_interface(peth) + verify_vrf(peth) + verify_address(peth) + + if 'source_interface_is_bridge_member' in peth: + raise ConfigError( + 'Source interface "{source_interface}" can not be used as it is already a ' + 'member of bridge "{source_interface_is_bridge_member}"!'.format(**peth)) + + if 'source_interface_is_bond_member' in peth: + raise ConfigError( + 'Source interface "{source_interface}" can not be used as it is already a ' + 'member of bond "{source_interface_is_bond_member}"!'.format(**peth)) + + # use common function to verify VLAN configuration + verify_vlan_config(peth) + return None + +def generate(peth): + return None + +def apply(peth): + if 'deleted' in peth: + # delete interface + MACVLANIf(peth['ifname']).remove() + return None + + # Check if MACVLAN interface already exists. Parameters like the underlaying + # source-interface device or mode can not be changed on the fly and the + # interface needs to be recreated from the bottom. + if 'mode_old' in peth: + MACVLANIf(peth['ifname']).remove() + + # MACVLAN interface needs to be created on-block instead of passing a ton + # of arguments, I just use a dict that is managed by vyos.ifconfig + conf = deepcopy(MACVLANIf.get_config()) + + # Assign MACVLAN instance configuration parameters to config dict + conf['source_interface'] = peth['source_interface'] + conf['mode'] = peth['mode'] + + # It is safe to "re-create" the interface always, there is a sanity check + # that the interface will only be create if its non existent + p = MACVLANIf(peth['ifname'], **conf) + p.update(peth) + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py new file mode 100755 index 000000000..ea15a7fb7 --- /dev/null +++ b/src/conf_mode/interfaces-tunnel.py @@ -0,0 +1,718 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import netifaces + +from sys import exit +from copy import deepcopy +from netifaces import interfaces + +from vyos.config import Config +from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf +from vyos.ifconfig.afi import IP4, IP6 +from vyos.configdict import list_diff +from vyos.validate import is_ipv4, is_ipv6, is_member +from vyos import ConfigError +from vyos.dicts import FixedDict + +from vyos import airbag +airbag.enable() + + +class ConfigurationState(object): + """ + The current API require a dict to be generated by get_config() + which is then consumed by verify(), generate() and apply() + + ConfiguartionState is an helper class wrapping Config and providing + an common API to this dictionary structure + + Its to_api() function return a dictionary containing three fields, + each a dict, called options, changes, actions. + + options: + + contains the configuration options for the dict and its value + {'options': {'commment': 'test'}} will be set if + 'set interface dummy dum1 description test' was used and + the key 'commment' is used to index the description info. + + changes: + + per key, let us know how the data was modified using one of the action + a special key called 'section' is used to indicate what happened to the + section. for example: + + 'set interface dummy dum1 description test' when no interface was setup + will result in the following changes + {'changes': {'section': 'create', 'comment': 'create'}} + + on an existing interface, depending if there was a description + 'set interface dummy dum1 description test' will result in one of + {'changes': {'comment': 'create'}} (not present before) + {'changes': {'comment': 'static'}} (unchanged) + {'changes': {'comment': 'modify'}} (changed from half) + + and 'delete interface dummy dummy1 description' will result in: + {'changes': {'comment': 'delete'}} + + actions: + + for each action list the configuration key which were changes + in our example if we added the 'description' and added an IP we would have + {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} + + the actions are: + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + which for each field represent how it was modified since the last commit + """ + + def __init__(self, configuration, section, default): + """ + initialise the class for a given configuration path: + + >>> conf = ConfigurationState(conf, 'interfaces ethernet eth1') + all further references to get_value(s) and get_effective(s) + will be for this part of the configuration (eth1) + """ + self._conf = configuration + + self.default = deepcopy(default) + self.options = FixedDict(**default) + self.actions = { + 'create': [], # the key did not exist and was added + 'static': [], # the key exists and its value was not modfied + 'modify': [], # the key exists and its value was modified + 'absent': [], # the key is not present + 'delete': [], # the key was present and was deleted + } + self.changes = {} + if not self._conf.exists(section): + self.changes['section'] = 'delete' + elif self._conf.exists_effective(section): + self.changes['section'] = 'modify' + else: + self.changes['section'] = 'create' + + self.set_level(section) + + def set_level(self, lpath): + self.section = lpath + self._conf.set_level(lpath) + + def _act(self, section): + """ + Returns for a given configuration field determine what happened to it + + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + """ + if self._conf.exists(section): + if self._conf.exists_effective(section): + if self._conf.return_value(section) != self._conf.return_effective_value(section): + return 'modify' + return 'static' + return 'create' + else: + if self._conf.exists_effective(section): + return 'delete' + return 'absent' + + def _action(self, name, key): + action = self._act(key) + self.changes[name] = action + self.actions[action].append(name) + return action + + def _get(self, name, key, default, getter): + value = getter(key) + if not value: + if default: + self.options[name] = default + return + self.options[name] = self.default[name] + return + self.options[name] = value + + def get_value(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_value + (the data in the configuration to apply) + """ + if self._action(name, key) in ('delete', 'absent'): + return + return self._get(name, key, default, self._conf.return_value) + + def get_values(self, name, key, default=None): + """ + >>> conf.get_values('addresses', 'address') + will place a list of the new IP present in 'interface dummy dum1 address' + into the dictionnary entry "-add" (here 'addresses-add') using + Config.return_values and will add the the one which were removed in into + the entry "-del" (here addresses-del') + """ + add_name = f'{name}-add' + + if self._action(add_name, key) in ('delete', 'absent'): + return + + self._get(add_name, key, default, self._conf.return_values) + + # get the effective values to determine which data is no longer valid + self.options['addresses-del'] = list_diff( + self._conf.return_effective_values('address'), + self.options['addresses-add'] + ) + + def get_effective(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_effective_value + (the data in the configuration to apply) + """ + self._action(name, key) + return self._get(name, key, default, self._conf.return_effective_value) + + def get_effectives(self, name, key, default=None): + """ + >>> conf.get_effectives('addresses-add', 'address') + will place a list made of the IP present in 'interface ethernet eth1 address' + into the dictionnary entry 'addresses-add' using Config.return_effectives_value + (the data in the un-modified configuration) + """ + self._action(name, key) + return self._get(name, key, default, self._conf.return_effectives_value) + + def load(self, mapping): + """ + load will take a dictionary defining how we wish the configuration + to be parsed and apply this definition to set the data. + + >>> mapping = { + 'addresses-add' : ('address', True, None), + 'comment' : ('description', False, 'auto'), + } + >>> conf.load(mapping) + + mapping is a dictionary where each key represents the name we wish + to have (such as 'addresses-add'), with a list a content representing + how the data should be parsed: + - the configuration section name + such as 'address' under 'interface ethernet eth1' + - boolean indicating if this data can have multiple values + for 'address', True, as multiple IPs can be set + for 'description', False, as it is a single string + - default represent the default value if absent from the configuration + 'None' indicate that no default should be set if the configuration + does not have the configuration section + + """ + for local_name, (config_name, multiple, default) in mapping.items(): + if multiple: + self.get_values(local_name, config_name, default) + else: + self.get_value(local_name, config_name, default) + + def remove_default(self,*options): + """ + remove all the values which were not changed from the default + """ + for option in options: + if not self._conf.exists(option): + del self.options[option] + continue + + if self._conf.return_value(option) == self.default[option]: + del self.options[option] + continue + + if self._conf.return_values(option) == self.default[option]: + del self.options[option] + continue + + def as_dict(self, lpath): + l = self._conf.get_level() + self._conf.set_level([]) + d = self._conf.get_config_dict(lpath) + # XXX: that not what I would have expected from get_config_dict + if lpath: + d = d[lpath[-1]] + # XXX: it should have provided me the content and not the key + self._conf.set_level(l) + return d + + def to_api(self): + """ + provide a dictionary with the generated data for the configuration + options: the configuration value for the key + changes: per key how they changed from the previous configuration + actions: per changes all the options which were changed + """ + # as we have to use a dict() for the API for verify and apply the options + return { + 'options': self.options, + 'changes': self.changes, + 'actions': self.actions, + } + + +default_config_data = { + # interface definition + 'vrf': '', + 'addresses-add': [], + 'addresses-del': [], + 'state': 'up', + 'dhcp-interface': '', + 'link_detect': 1, + 'ip': False, + 'ipv6': False, + 'nhrp': [], + 'arp_filter': 1, + 'arp_accept': 0, + 'arp_announce': 0, + 'arp_ignore': 0, + 'ipv6_accept_ra': 1, + 'ipv6_autoconf': 0, + 'ipv6_forwarding': 1, + 'ipv6_dad_transmits': 1, + # internal + 'interfaces': [], + 'tunnel': {}, + 'bridge': '', + # the following names are exactly matching the name + # for the ip command and must not be changed + 'ifname': '', + 'type': '', + 'alias': '', + 'mtu': '1476', + 'local': '', + 'remote': '', + 'dev': '', + 'multicast': 'disable', + 'allmulticast': 'disable', + 'ttl': '255', + 'tos': 'inherit', + 'key': '', + 'encaplimit': '4', + 'flowlabel': 'inherit', + 'hoplimit': '64', + 'tclass': 'inherit', + '6rd-prefix': '', + '6rd-relay-prefix': '', +} + + +# dict name -> config name, multiple values, default +mapping = { + 'type': ('encapsulation', False, None), + 'alias': ('description', False, None), + 'mtu': ('mtu', False, None), + 'local': ('local-ip', False, None), + 'remote': ('remote-ip', False, None), + 'multicast': ('multicast', False, None), + 'dev': ('source-interface', False, None), + 'ttl': ('parameters ip ttl', False, None), + 'tos': ('parameters ip tos', False, None), + 'key': ('parameters ip key', False, None), + 'encaplimit': ('parameters ipv6 encaplimit', False, None), + 'flowlabel': ('parameters ipv6 flowlabel', False, None), + 'hoplimit': ('parameters ipv6 hoplimit', False, None), + 'tclass': ('parameters ipv6 tclass', False, None), + '6rd-prefix': ('6rd-prefix', False, None), + '6rd-relay-prefix': ('6rd-relay-prefix', False, None), + 'dhcp-interface': ('dhcp-interface', False, None), + 'state': ('disable', False, 'down'), + 'link_detect': ('disable-link-detect', False, 2), + 'vrf': ('vrf', False, None), + 'addresses': ('address', True, None), + 'arp_filter': ('ip disable-arp-filter', False, 0), + 'arp_accept': ('ip enable-arp-accept', False, 1), + 'arp_announce': ('ip enable-arp-announce', False, 1), + 'arp_ignore': ('ip enable-arp-ignore', False, 1), + 'ipv6_autoconf': ('ipv6 address autoconf', False, 1), + 'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0), + 'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None) +} + + +def get_class (options): + dispatch = { + 'gre': GREIf, + 'gre-bridge': GRETapIf, + 'ipip': IPIPIf, + 'ipip6': IPIP6If, + 'ip6ip6': IP6IP6If, + 'ip6gre': IP6GREIf, + 'sit': SitIf, + } + + kls = dispatch[options['type']] + if options['type'] == 'gre' and not options['remote'] \ + and not options['key'] and not options['multicast']: + # will use GreTapIf on GreIf deletion but it does not matter + return GRETapIf + elif options['type'] == 'sit' and options['6rd-prefix']: + # will use SitIf on Sit6RDIf deletion but it does not matter + return Sit6RDIf + return kls + +def get_interface_ip (ifname): + if not ifname: + return '' + try: + addrs = Interface(ifname).get_addr() + if addrs: + return addrs[0].split('/')[0] + except Exception: + return '' + +def get_afi (ip): + return IP6 if is_ipv6(ip) else IP4 + +def ip_proto (afi): + return 6 if afi == IP6 else 4 + + +def get_config(): + ifname = os.environ.get('VYOS_TAGNODE_VALUE','') + if not ifname: + raise ConfigError('Interface not specified') + + config = Config() + conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data) + options = conf.options + changes = conf.changes + options['ifname'] = ifname + + if changes['section'] == 'delete': + conf.get_effective('type', mapping['type'][0]) + config.set_level(['protocols', 'nhrp', 'tunnel']) + options['nhrp'] = config.list_nodes('') + return conf.to_api() + + # load all the configuration option according to the mapping + conf.load(mapping) + + # remove default value if not set and not required + afi_local = get_afi(options['local']) + if afi_local == IP6: + conf.remove_default('ttl', 'tos', 'key') + if afi_local == IP4: + conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass') + + # if the local-ip is not set, pick one from the interface ! + # hopefully there is only one, otherwise it will not be very deterministic + # at time of writing the code currently returns ipv4 before ipv6 in the list + + # XXX: There is no way to trigger an update of the interface source IP if + # XXX: the underlying interface IP address does change, I believe this + # XXX: limit/issue is present in vyatta too + + if not options['local'] and options['dhcp-interface']: + # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if + # XXX: the interface was not DHCP. As there is no easy way to find if an + # XXX: interface is using DHCP, and using this feature to get 127.0.0.1 + # XXX: makes little sense, I feel the change in behaviour is acceptable + picked = get_interface_ip(options['dhcp-interface']) + if picked == '': + picked = '127.0.0.1' + print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead') + options['local'] = picked + options['dhcp-interface'] = '' + + # to make IPv6 SLAAC and DHCPv6 work with forwarding=1, + # accept_ra must be 2 + if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']: + options['ipv6_accept_ra'] = 2 + + # allmulticast fate is linked to multicast + options['allmulticast'] = options['multicast'] + + # check that per encapsulation all local-remote pairs are unique + ct = conf.as_dict(['interfaces', 'tunnel']) + options['tunnel'] = {} + + # check for bridges + options['bridge'] = is_member(config, ifname, 'bridge') + options['interfaces'] = interfaces() + + for name in ct: + tunnel = ct[name] + encap = tunnel.get('encapsulation', '') + local = tunnel.get('local-ip', '') + if not local: + local = get_interface_ip(tunnel.get('dhcp-interface', '')) + remote = tunnel.get('remote-ip', '<unset>') + pair = f'{local}-{remote}' + options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1 + + return conf.to_api() + + +def verify(conf): + options = conf['options'] + changes = conf['changes'] + actions = conf['actions'] + + ifname = options['ifname'] + iftype = options['type'] + + if changes['section'] == 'delete': + if ifname in options['nhrp']: + raise ConfigError(( + f'Cannot delete interface tunnel {iftype} {ifname}, ' + 'it is used by NHRP')) + + if options['bridge']: + raise ConfigError(( + f'Cannot delete interface "{options["ifname"]}" as it is a ' + f'member of bridge "{options["bridge"]}"!')) + + # done, bail out early + return None + + # tunnel encapsulation checks + + if not iftype: + raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}') + + if changes['type'] in ('modify', 'delete'): + # TODO: we could now deal with encapsulation modification by deleting / recreating + raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}') + + if iftype != 'sit' and options['6rd-prefix']: + # XXX: should be able to remove this and let the definition catch it + print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}') + + # what are the tunnel options we can set / modified / deleted + + kls = get_class(options) + valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state'] + valid += ['arp_filter', 'arp_accept', 'arp_announce', 'arp_ignore'] + valid += ['ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'] + + if changes['section'] == 'create': + valid.extend(['type',]) + valid.extend([o for o in kls.options if o not in kls.updates]) + + for create in actions['create']: + if create not in valid: + raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation') + + for modify in actions['modify']: + if modify not in valid: + raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation') + + for delete in actions['delete']: + if delete in kls.required: + raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}') + + # tunnel information + + tun_local = options['local'] + afi_local = get_afi(tun_local) + tun_remote = options['remote'] or tun_local + afi_remote = get_afi(tun_remote) + tun_ismgre = iftype == 'gre' and not options['remote'] + tun_is6rd = iftype == 'sit' and options['6rd-prefix'] + tun_dev = options['dev'] + + # incompatible options + + if not tun_local and not options['dhcp-interface'] and not tun_is6rd: + raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}') + + if tun_local and options['dhcp-interface']: + raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}') + + if tun_dev and iftype in ('gre-bridge', 'sit'): + raise ConfigError(f'source interface can not be used with {iftype} {ifname}') + + # tunnel endpoint + + if afi_local != afi_remote: + raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}') + + if afi_local != kls.tunnel: + version = 4 if tun_local == IP4 else 6 + raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}') + + ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)]) + ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)]) + + if tun_ismgre and afi_local == IP6: + raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}') + + # check address family use + # checks are not enforced (but ip command failing) for backward compatibility + + if ipv4_count and not IP4 in kls.ip: + print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}') + + if ipv6_count and not IP6 in kls.ip: + print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}') + + # vrf check + if options['vrf']: + if options['vrf'] not in options['interfaces']: + raise ConfigError(f'VRF "{options["vrf"]}" does not exist') + + if options['bridge']: + raise ConfigError(( + f'Interface "{options["ifname"]}" cannot be member of VRF ' + f'"{options["vrf"]}" and bridge {options["bridge"]} ' + f'at the same time!')) + + # bridge and address check + if ( options['bridge'] + and ( options['addresses-add'] + or options['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{options["name"]}" ' + f'as it is a member of bridge "{options["bridge"]}"!')) + + # source-interface check + + if tun_dev and tun_dev not in options['interfaces']: + raise ConfigError(f'device "{tun_dev}" does not exist') + + # tunnel encapsulation check + + convert = { + (6, 4, 'gre'): 'ip6gre', + (6, 6, 'gre'): 'ip6gre', + (4, 6, 'ipip'): 'ipip6', + (6, 6, 'ipip'): 'ip6ip6', + } + + iprotos = [] + if ipv4_count: + iprotos.append(4) + if ipv6_count: + iprotos.append(6) + + for iproto in iprotos: + replace = convert.get((kls.tunnel, iproto, iftype), '') + if replace: + raise ConfigError( + f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' + + f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.' + ) + + # tunnel options + + incompatible = [] + if afi_local == IP6: + incompatible.extend(['ttl', 'tos', 'key',]) + if afi_local == IP4: + incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass']) + + for option in incompatible: + if option in options: + # TODO: raise converted to print as not enforced by vyatta + # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}') + print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}') + + # duplicate tunnel pairs + + pair = '{}-{}'.format(options['local'], options['remote']) + if options['tunnel'].get(iftype, {}).get(pair, 0) > 1: + raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}') + + return None + + +def generate(gre): + return None + +def apply(conf): + options = conf['options'] + changes = conf['changes'] + actions = conf['actions'] + kls = get_class(options) + + # extract ifname as otherwise it is duplicated on the interface creation + ifname = options.pop('ifname') + + # only the valid keys for creation of a Interface + config = dict((k, options[k]) for k in kls.options if options[k]) + + # setup or create the tunnel interface if it does not exist + tunnel = kls(ifname, **config) + + if changes['section'] == 'delete': + tunnel.remove() + # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup + # which identified tunnels type which were not used anymore to remove them + # (ie: gre0, gretap0, etc.) The perl code did however nothing + # This feature is also not implemented yet + return + + # A GRE interface without remote will be mGRE + # if the interface does not suppor the option, it skips the change + for option in tunnel.updates: + if changes['section'] in 'create' and option in tunnel.options: + # it was setup at creation + continue + if not options[option]: + # remote can be set to '' and it would generate an invalide command + continue + tunnel.set_interface(option, options[option]) + + # set other interface properties + for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast', + 'arp_accept', 'arp_filter', 'arp_announce', 'arp_ignore', + 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): + if not options[option]: + # should never happen but better safe + continue + tunnel.set_interface(option, options[option]) + + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not options['bridge']: + tunnel.set_vrf(options['vrf']) + + # Configure interface address(es) + for addr in options['addresses-del']: + tunnel.del_addr(addr) + for addr in options['addresses-add']: + tunnel.add_addr(addr) + + # now bring it up (or not) + tunnel.set_admin_state(options['state']) + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py new file mode 100755 index 000000000..47c0bdcb8 --- /dev/null +++ b/src/conf_mode/interfaces-vxlan.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_source_interface +from vyos.ifconfig import VXLANIf, Interface +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'vxlan'] + vxlan = get_interface_dict(conf, base) + + # VXLAN is "special" the default MTU is 1492 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + vxlan['mtu'] = '1450' + + return vxlan + +def verify(vxlan): + if 'deleted' in vxlan: + verify_bridge_delete(vxlan) + return None + + if int(vxlan['mtu']) < 1500: + print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') + + if 'group' in vxlan: + if 'source_interface' not in vxlan: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + + verify_source_interface(vxlan) + + if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan): + raise ConfigError('Group, remote or source-address must be configured') + + if 'vni' not in vxlan: + raise ConfigError('Must configure VNI for VXLAN') + + if 'source_interface' in vxlan: + # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU + # if our configured MTU is at least 50 bytes less + underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu()) + if underlay_mtu < (int(vxlan['mtu']) + 50): + raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ + f'MTU is to small ({underlay_mtu} bytes)') + + verify_address(vxlan) + return None + + +def generate(vxlan): + return None + + +def apply(vxlan): + # Check if the VXLAN interface already exists + if vxlan['ifname'] in interfaces(): + v = VXLANIf(vxlan['ifname']) + # VXLAN is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + v.remove() + + if 'deleted' not in vxlan: + # VXLAN interface needs to be created on-block + # instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(VXLANIf.get_config()) + + # Assign VXLAN instance configuration parameters to config dict + for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']: + if tmp in vxlan: + conf[tmp] = vxlan[tmp] + + # Finally create the new interface + v = VXLANIf(vxlan['ifname'], **conf) + v.update(vxlan) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py new file mode 100755 index 000000000..8b64cde4d --- /dev/null +++ b/src/conf_mode/interfaces-wireguard.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import get_interface_dict +from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed +from vyos.configverify import verify_vrf +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.ifconfig import WireGuardIf +from vyos.util import check_kmod +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'wireguard'] + wireguard = get_interface_dict(conf, base) + + # Wireguard is "special" the default MTU is 1420 - update accordingly + # as the config_level is already st in get_interface_dict() - we can use [] + tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) + if 'mtu' not in tmp: + wireguard['mtu'] = '1420' + + # Mangle private key - it has a default so its always valid + wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard) + + # Determine which Wireguard peer has been removed. + # Peers can only be removed with their public key! + tmp = node_changed(conf, ['peer']) + if tmp: + dict = {} + for peer in tmp: + peer_config = leaf_node_changed(conf, ['peer', peer, 'pubkey']) + dict = dict_merge({'peer_remove' : {peer : {'pubkey' : peer_config}}}, dict) + wireguard.update(dict) + + return wireguard + +def verify(wireguard): + if 'deleted' in wireguard: + verify_bridge_delete(wireguard) + return None + + verify_address(wireguard) + verify_vrf(wireguard) + + if not os.path.exists(wireguard['private_key']): + raise ConfigError('Wireguard private-key not found! Execute: ' \ + '"run generate wireguard [default-keypair|named-keypairs]"') + + if 'address' not in wireguard: + raise ConfigError('IP address required!') + + if 'peer' not in wireguard: + raise ConfigError('At least one Wireguard peer is required!') + + # run checks on individual configured WireGuard peer + for tmp in wireguard['peer']: + peer = wireguard['peer'][tmp] + + if 'allowed_ips' not in peer: + raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') + + if 'pubkey' not in peer: + raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!') + + if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer): + raise ConfigError('Both Wireguard port and address must be defined ' + f'for peer "{tmp}" if either one of them is set!') + +def apply(wireguard): + if 'deleted' in wireguard: + WireGuardIf(wireguard['ifname']).remove() + return None + + w = WireGuardIf(wireguard['ifname']) + w.update(wireguard) + return None + +if __name__ == '__main__': + try: + check_kmod('wireguard') + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py new file mode 100755 index 000000000..b6f247952 --- /dev/null +++ b/src/conf_mode/interfaces-wireless.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from re import findall +from copy import deepcopy +from netaddr import EUI, mac_unix_expanded + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import dict_merge +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_source_interface +from vyos.configverify import verify_vlan_config +from vyos.configverify import verify_vrf +from vyos.ifconfig import WiFiIf +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +# XXX: wpa_supplicant works on the source interface +wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' +hostapd_conf = '/run/hostapd/{ifname}.conf' + +def find_other_stations(conf, base, ifname): + """ + Only one wireless interface per phy can be in station mode - + find all interfaces attached to a phy which run in station mode + """ + old_level = conf.get_level() + conf.set_level(base) + dict = {} + for phy in os.listdir('/sys/class/ieee80211'): + list = [] + for interface in conf.list_nodes([]): + if interface == ifname: + continue + # the following node is mandatory + if conf.exists([interface, 'physical-device', phy]): + tmp = conf.return_value([interface, 'type']) + if tmp == 'station': + list.append(interface) + if list: + dict.update({phy: list}) + conf.set_level(old_level) + return dict + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'wireless'] + wifi = get_interface_dict(conf, base) + + if 'security' in wifi and 'wpa' in wifi['security']: + wpa_cipher = wifi['security']['wpa'].get('cipher') + wpa_mode = wifi['security']['wpa'].get('mode') + if not wpa_cipher: + tmp = None + if wpa_mode == 'wpa': + tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}} + elif wpa_mode == 'wpa2': + tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}} + elif wpa_mode == 'both': + tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}} + + if tmp: wifi = dict_merge(tmp, wifi) + + # retrieve configured regulatory domain + conf.set_level(['system']) + if conf.exists(['wifi-regulatory-domain']): + wifi['country_code'] = conf.return_value(['wifi-regulatory-domain']) + + # Only one wireless interface per phy can be in station mode + tmp = find_other_stations(conf, base, wifi['ifname']) + if tmp: wifi['station_interfaces'] = tmp + + return wifi + +def verify(wifi): + if 'deleted' in wifi: + verify_bridge_delete(wifi) + return None + + if 'physical_device' not in wifi: + raise ConfigError('You must specify a physical-device "phy"') + + if 'type' not in wifi: + raise ConfigError('You must specify a WiFi mode') + + if 'ssid' not in wifi and wifi['type'] != 'monitor': + raise ConfigError('SSID must be configured') + + if wifi['type'] == 'access-point': + if 'country_code' not in wifi: + raise ConfigError('Wireless regulatory domain is mandatory,\n' \ + 'use "set system wifi-regulatory-domain" for configuration.') + + if 'channel' not in wifi: + raise ConfigError('Wireless channel must be configured!') + + if 'security' in wifi: + if {'wep', 'wpa'} <= set(wifi.get('security', {})): + raise ConfigError('Must either use WEP or WPA security!') + + if 'wep' in wifi['security']: + if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4: + raise ConfigError('No more then 4 WEP keys configurable') + elif 'key' not in wifi['security']['wep']: + raise ConfigError('Security WEP configured - missing WEP keys!') + + elif 'wpa' in wifi['security']: + wpa = wifi['security']['wpa'] + if not any(i in ['passphrase', 'radius'] for i in wpa): + raise ConfigError('Misssing WPA key or RADIUS server') + + if 'radius' in wpa: + if 'server' in wpa['radius']: + for server in wpa['radius']['server']: + if 'key' not in wpa['radius']['server'][server]: + raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}') + + if 'capabilities' in wifi: + capabilities = wifi['capabilities'] + if 'vht' in capabilities: + if 'ht' not in capabilities: + raise ConfigError('Specify HT flags if you want to use VHT!') + + if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})): + if capabilities['vht']['antenna_count'] == '1': + raise ConfigError('Cannot use beam forming with just one antenna!') + + if capabilities['vht']['beamform'] == 'single-user-beamformer': + if int(capabilities['vht']['antenna_count']) < 3: + # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 + raise ConfigError('Single-user beam former requires at least 3 antennas!') + + if 'station_interfaces' in wifi and wifi['type'] == 'station': + phy = wifi['physical_device'] + if phy in wifi['station_interfaces']: + if len(wifi['station_interfaces'][phy]) > 0: + raise ConfigError('Only one station per wireless physical interface possible!') + + verify_address(wifi) + verify_vrf(wifi) + + # use common function to verify VLAN configuration + verify_vlan_config(wifi) + + return None + +def generate(wifi): + interface = wifi['ifname'] + + # always stop hostapd service first before reconfiguring it + call(f'systemctl stop hostapd@{interface}.service') + # always stop wpa_supplicant service first before reconfiguring it + call(f'systemctl stop wpa_supplicant@{interface}.service') + + # Delete config files if interface is removed + if 'deleted' in wifi: + if os.path.isfile(hostapd_conf.format(**wifi)): + os.unlink(hostapd_conf.format(**wifi)) + + if os.path.isfile(wpa_suppl_conf.format(**wifi)): + os.unlink(wpa_suppl_conf.format(**wifi)) + + return None + + if 'mac' not in wifi: + # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd + # generate locally administered MAC address from used phy interface + with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), 'r') as f: + # some PHYs tend to have multiple interfaces and thus supply multiple MAC + # addresses - we only need the first one for our calculation + tmp = f.readline().rstrip() + tmp = EUI(tmp).value + # mask last nibble from the MAC address + tmp &= 0xfffffffffff0 + # set locally administered bit in MAC address + tmp |= 0x020000000000 + # we now need to add an offset to our MAC address indicating this + # subinterfaces index + tmp += int(findall(r'\d+', interface)[0]) + + # convert integer to "real" MAC address representation + mac = EUI(hex(tmp).split('x')[-1]) + # change dialect to use : as delimiter instead of - + mac.dialect = mac_unix_expanded + wifi['mac'] = str(mac) + + # render appropriate new config files depending on access-point or station mode + if wifi['type'] == 'access-point': + render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True) + + elif wifi['type'] == 'station': + render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True) + + return None + +def apply(wifi): + interface = wifi['ifname'] + if 'deleted' in wifi: + WiFiIf(interface).remove() + else: + # WiFi interface needs to be created on-block (e.g. mode or physical + # interface) instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(WiFiIf.get_config()) + + # Assign WiFi instance configuration parameters to config dict + conf['phy'] = wifi['physical_device'] + + # Finally create the new interface + w = WiFiIf(interface, **conf) + w.update(wifi) + + # Enable/Disable interface - interface is always placed in + # administrative down state in WiFiIf class + if 'disable' not in wifi: + # Physical interface is now configured. Proceed by starting hostapd or + # wpa_supplicant daemon. When type is monitor we can just skip this. + if wifi['type'] == 'access-point': + call(f'systemctl start hostapd@{interface}.service') + + elif wifi['type'] == 'station': + call(f'systemctl start wpa_supplicant@{interface}.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py new file mode 100755 index 000000000..6d168d918 --- /dev/null +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_vrf +from vyos.template import render +from vyos.util import call +from vyos.util import check_kmod +from vyos.util import find_device_file +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +k_mod = ['option', 'usb_wwan', 'usbserial'] + +def get_config(): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + conf = Config() + base = ['interfaces', 'wirelessmodem'] + wwan = get_interface_dict(conf, base) + return wwan + +def verify(wwan): + if 'deleted' in wwan: + return None + + if not 'apn' in wwan: + raise ConfigError('No APN configured for "{ifname}"'.format(**wwan)) + + if not 'device' in wwan: + raise ConfigError('Physical "device" must be configured') + + # we can not use isfile() here as Linux device files are no regular files + # thus the check will return False + if not os.path.exists(find_device_file(wwan['device'])): + raise ConfigError('Device "{device}" does not exist'.format(**wwan)) + + verify_vrf(wwan) + + return None + +def generate(wwan): + # set up configuration file path variables where our templates will be + # rendered into + ifname = wwan['ifname'] + config_wwan = f'/etc/ppp/peers/{ifname}' + config_wwan_chat = f'/etc/ppp/peers/chat.{ifname}' + script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{ifname}' + script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{ifname}' + script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{ifname}' + + config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up, + script_wwan_ip_up, script_wwan_ip_down] + + # Always hang-up WWAN connection prior generating new configuration file + call(f'systemctl stop ppp@{ifname}.service') + + if 'deleted' in wwan: + # Delete PPP configuration files + for file in config_files: + if os.path.exists(file): + os.unlink(file) + + else: + wwan['device'] = find_device_file(wwan['device']) + + # Create PPP configuration files + render(config_wwan, 'wwan/peer.tmpl', wwan) + # Create PPP chat script + render(config_wwan_chat, 'wwan/chat.tmpl', wwan) + + # generated script file must be executable + + # Create script for ip-pre-up.d + render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', + wwan, permission=0o755) + # Create script for ip-up.d + render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', + wwan, permission=0o755) + # Create script for ip-down.d + render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', + wwan, permission=0o755) + + return None + +def apply(wwan): + if 'deleted' in wwan: + # bail out early + return None + + if not 'disable' in wwan: + # "dial" WWAN connection + call('systemctl start ppp@{ifname}.service'.format(**wwan)) + + return None + +if __name__ == '__main__': + try: + check_kmod(k_mod) + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py new file mode 100755 index 000000000..015d1a480 --- /dev/null +++ b/src/conf_mode/ipsec-settings.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import os + +from time import sleep +from sys import exit + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +ra_conn_name = "remote-access" +charon_conf_file = "/etc/strongswan.d/charon.conf" +ipsec_secrets_file = "/etc/ipsec.secrets" +ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/" +ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name +ipsec_conf_file = "/etc/ipsec.conf" +ca_cert_path = "/etc/ipsec.d/cacerts" +server_cert_path = "/etc/ipsec.d/certs" +server_key_path = "/etc/ipsec.d/private" +delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" +delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" +charon_pidfile = "/var/run/charon.pid" + +def get_config(): + config = Config() + data = {"install_routes": "yes"} + + if config.exists("vpn ipsec options disable-route-autoinstall"): + data["install_routes"] = "no" + + if config.exists("vpn ipsec ipsec-interfaces interface"): + data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface") + + # Init config variables + data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin + data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end + data["ipsec_ra_conn_file"] = ipsec_ra_conn_file + data["ra_conn_name"] = ra_conn_name + # Get l2tp ipsec settings + data["ipsec_l2tp"] = False + conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful + if config.exists(conf_ipsec_command): + data["ipsec_l2tp"] = True + + # Authentication params + if config.exists(conf_ipsec_command + "authentication mode"): + data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode") + if config.exists(conf_ipsec_command + "authentication pre-shared-secret"): + data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret") + + # mode x509 + if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"): + data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file") + if config.exists(conf_ipsec_command + "authentication x509 crl-file"): + data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file") + if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"): + data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file") + data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0) + if config.exists(conf_ipsec_command + "authentication x509 server-key-file"): + data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file") + data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0) + if config.exists(conf_ipsec_command + "authentication x509 server-key-password"): + data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password") + + # Common l2tp ipsec params + if config.exists(conf_ipsec_command + "ike-lifetime"): + data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime") + else: + data["ipsec_l2tp_ike_lifetime"] = "3600" + + if config.exists(conf_ipsec_command + "lifetime"): + data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime") + else: + data["ipsec_l2tp_lifetime"] = "3600" + + if config.exists("vpn l2tp remote-access outside-address"): + data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address') + + return data + +def write_ipsec_secrets(c): + if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": + secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end) + elif c.get("ipsec_l2tp_auth_mode") == "x509": + secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end) + + old_umask = os.umask(0o077) + with open(ipsec_secrets_file, 'a+') as f: + f.write(secret_txt) + os.umask(old_umask) + +def write_ipsec_conf(c): + ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end) + + old_umask = os.umask(0o077) + with open(ipsec_conf_file, 'a+') as f: + f.write(ipsec_confg_txt) + os.umask(old_umask) + +### Remove config from file by delimiter +def remove_confs(delim_begin, delim_end, conf_file): + call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) + + +### Checking certificate storage and notice if certificate not in /config directory +def check_cert_file_store(cert_name, file_path, dts_path): + if not re.search('^\/config\/.+', file_path): + print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.") + #Checking file existence + if not os.path.isfile(file_path): + raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"") + else: + ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ + # todo make check + ret = call('cp -f '+file_path+' '+dts_path) + if ret: + raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) + +def verify(data): + # l2tp ipsec check + if data["ipsec_l2tp"]: + # Checking dependecies for "authentication mode pre-shared-secret" + if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": + if not data.get("ipsec_l2tp_secret"): + raise ConfigError("pre-shared-secret required") + if not data.get("outside_addr"): + raise ConfigError("outside-address not defined") + + # Checking dependecies for "authentication mode x509" + if data.get("ipsec_l2tp_auth_mode") == "x509": + if not data.get("ipsec_l2tp_x509_server_key_file"): + raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.") + else: + check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path) + + if not data.get("ipsec_l2tp_x509_server_cert_file"): + raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.") + else: + check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path) + + if not data.get("ipsec_l2tp_x509_ca_cert_file"): + raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509") + else: + check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path) + + if not data.get('ipsec_interfaces'): + raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") + +def generate(data): + render(charon_conf_file, 'ipsec/charon.tmpl', data, trim_blocks=True) + + if data["ipsec_l2tp"]: + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) + # old_umask = os.umask(0o077) + # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data, trim_blocks=True) + # os.umask(old_umask) + ## Use this method while IPSec CLI handler won't be overwritten to python + write_ipsec_secrets(data) + + old_umask = os.umask(0o077) + + # Create tunnels directory if does not exist + if not os.path.exists(ipsec_ra_conn_dir): + os.makedirs(ipsec_ra_conn_dir) + + render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data, trim_blocks=True) + os.umask(old_umask) + + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) + # old_umask = os.umask(0o077) + # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data, trim_blocks=True) + # os.umask(old_umask) + ## Use this method while IPSec CLI handler won't be overwritten to python + write_ipsec_conf(data) + + else: + if os.path.exists(ipsec_ra_conn_file): + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) + +def restart_ipsec(): + call('ipsec restart >&/dev/null') + # counter for apply swanctl config + counter = 10 + while counter <= 10: + if os.path.exists(charon_pidfile): + call('swanctl -q >&/dev/null') + break + counter -=1 + sleep(1) + if counter == 0: + raise ConfigError('VPN configuration error: IPSec is not running.') + +def apply(data): + # Restart IPSec daemon + restart_ipsec() + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py new file mode 100755 index 000000000..755c89966 --- /dev/null +++ b/src/conf_mode/le_cert.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os + +import vyos.defaults +from vyos.config import Config +from vyos import ConfigError +from vyos.util import cmd +from vyos.util import call + +from vyos import airbag +airbag.enable() + +vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] +vyos_certbot_dir = vyos.defaults.directories['certbot'] + +dependencies = [ + 'https.py', +] + +def request_certbot(cert): + email = cert.get('email') + if email is not None: + email_flag = '-m {0}'.format(email) + else: + email_flag = '' + + domains = cert.get('domains') + if domains is not None: + domain_flag = '-d ' + ' -d '.join(domains) + else: + domain_flag = '' + + certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}' + + cmd(certbot_cmd, + raising=ConfigError, + message="The certbot request failed for the specified domains.") + +def get_config(): + conf = Config() + if not conf.exists('service https certificates certbot'): + return None + else: + conf.set_level('service https certificates certbot') + + cert = {} + + if conf.exists('domain-name'): + cert['domains'] = conf.return_values('domain-name') + + if conf.exists('email'): + cert['email'] = conf.return_value('email') + + return cert + +def verify(cert): + if cert is None: + return None + + if 'domains' not in cert: + raise ConfigError("At least one domain name is required to" + " request a letsencrypt certificate.") + + if 'email' not in cert: + raise ConfigError("An email address is required to request" + " a letsencrypt certificate.") + +def generate(cert): + if cert is None: + return None + + # certbot will attempt to reload nginx, even with 'certonly'; + # start nginx if not active + ret = call('systemctl is-active --quiet nginx.service') + if ret: + call('systemctl start nginx.service') + + request_certbot(cert) + +def apply(cert): + if cert is not None: + call('systemctl restart certbot.timer') + else: + call('systemctl stop certbot.timer') + return None + + for dep in dependencies: + cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) + diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py new file mode 100755 index 000000000..1b539887a --- /dev/null +++ b/src/conf_mode/lldp.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from copy import deepcopy +from sys import exit + +from vyos.config import Config +from vyos.validate import is_addr_assigned,is_loopback_addr +from vyos.version import get_version_data +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = "/etc/default/lldpd" +vyos_config_file = "/etc/lldpd.d/01-vyos.conf" +base = ['service', 'lldp'] + +default_config_data = { + "options": '', + "interface_list": '', + "location": '' +} + +def get_options(config): + options = {} + config.set_level(base) + + options['listen_vlan'] = config.exists('listen-vlan') + options['mgmt_addr'] = [] + for addr in config.return_values('management-address'): + if is_addr_assigned(addr) and not is_loopback_addr(addr): + options['mgmt_addr'].append(addr) + else: + message = 'WARNING: LLDP management address {0} invalid - '.format(addr) + if is_loopback_addr(addr): + message += '(loopback address).' + else: + message += 'address not found.' + print(message) + + snmp = config.exists('snmp enable') + options["snmp"] = snmp + if snmp: + config.set_level('') + options["sys_snmp"] = config.exists('service snmp') + config.set_level(base) + + config.set_level(base + ['legacy-protocols']) + options['cdp'] = config.exists('cdp') + options['edp'] = config.exists('edp') + options['fdp'] = config.exists('fdp') + options['sonmp'] = config.exists('sonmp') + + # start with an unknown version information + version_data = get_version_data() + options['description'] = version_data['version'] + options['listen_on'] = [] + + return options + +def get_interface_list(config): + config.set_level(base) + intfs_names = config.list_nodes(['interface']) + if len(intfs_names) < 0: + return 0 + + interface_list = [] + for name in intfs_names: + config.set_level(base + ['interface', name]) + disable = config.exists(['disable']) + intf = { + 'name': name, + 'disable': disable + } + interface_list.append(intf) + return interface_list + + +def get_location_intf(config, name): + path = base + ['interface', name] + config.set_level(path) + + config.set_level(path + ['location']) + elin = '' + coordinate_based = {} + + if config.exists('elin'): + elin = config.return_value('elin') + + if config.exists('coordinate-based'): + config.set_level(path + ['location', 'coordinate-based']) + + coordinate_based['latitude'] = config.return_value(['latitude']) + coordinate_based['longitude'] = config.return_value(['longitude']) + + coordinate_based['altitude'] = '0' + if config.exists(['altitude']): + coordinate_based['altitude'] = config.return_value(['altitude']) + + coordinate_based['datum'] = 'WGS84' + if config.exists(['datum']): + coordinate_based['datum'] = config.return_value(['datum']) + + intf = { + 'name': name, + 'elin': elin, + 'coordinate_based': coordinate_based + + } + return intf + + +def get_location(config): + config.set_level(base) + intfs_names = config.list_nodes(['interface']) + if len(intfs_names) < 0: + return 0 + + if config.exists('disable'): + return 0 + + intfs_location = [] + for name in intfs_names: + intf = get_location_intf(config, name) + intfs_location.append(intf) + + return intfs_location + + +def get_config(): + lldp = deepcopy(default_config_data) + conf = Config() + if not conf.exists(base): + return None + else: + lldp['options'] = get_options(conf) + lldp['interface_list'] = get_interface_list(conf) + lldp['location'] = get_location(conf) + + return lldp + + +def verify(lldp): + # bail out early - looks like removal from running config + if lldp is None: + return + + # check location + for location in lldp['location']: + # check coordinate-based + if len(location['coordinate_based']) > 0: + # check longitude and latitude + if not location['coordinate_based']['longitude']: + raise ConfigError('Must define longitude for interface {0}'.format(location['name'])) + + if not location['coordinate_based']['latitude']: + raise ConfigError('Must define latitude for interface {0}'.format(location['name'])) + + if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'latitude should be a number followed by S or N'.format(location['name'])) + + if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'longitude should be a number followed by E or W'.format(location['name'])) + + # check altitude and datum if exist + if location['coordinate_based']['altitude']: + if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'altitude should be a positive or negative number'.format(location['name'])) + + if location['coordinate_based']['datum']: + if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']): + raise ConfigError("Invalid location for interface {0}:\n' \ + 'datum should be WGS84, NAD83, or MLLW".format(location['name'])) + + # check elin + elif location['elin']: + if not re.match(r'^[0-9]{10,25}$', location['elin']): + raise ConfigError('Invalid location for interface {0}:\n' \ + 'ELIN number must be between 10-25 numbers'.format(location['name'])) + + # check options + if lldp['options']['snmp']: + if not lldp['options']['sys_snmp']: + raise ConfigError('SNMP must be configured to enable LLDP SNMP') + + +def generate(lldp): + # bail out early - looks like removal from running config + if lldp is None: + return + + # generate listen on interfaces + for intf in lldp['interface_list']: + tmp = '' + # add exclamation mark if interface is disabled + if intf['disable']: + tmp = '!' + + tmp += intf['name'] + lldp['options']['listen_on'].append(tmp) + + # generate /etc/default/lldpd + render(config_file, 'lldp/lldpd.tmpl', lldp) + # generate /etc/lldpd.d/01-vyos.conf + render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp) + + +def apply(lldp): + if lldp: + # start/restart lldp service + call('systemctl restart lldpd.service') + else: + # LLDP service has been terminated + call('systemctl stop lldpd.service') + os.unlink(config_file) + os.unlink(vyos_config_file) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) + diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py new file mode 100755 index 000000000..dd34dfd66 --- /dev/null +++ b/src/conf_mode/nat.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import jmespath +import json +import os + +from copy import deepcopy +from sys import exit +from netifaces import interfaces + +from vyos.config import Config +from vyos.template import render +from vyos.util import call +from vyos.util import cmd +from vyos.util import check_kmod +from vyos.validate import is_addr_assigned +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] + +default_config_data = { + 'deleted': False, + 'destination': [], + 'helper_functions': None, + 'pre_ct_helper': '', + 'pre_ct_conntrack': '', + 'out_ct_helper': '', + 'out_ct_conntrack': '', + 'source': [] +} + +iptables_nat_config = '/tmp/vyos-nat-rules.nft' + +def get_handler(json, chain, target): + """ Get nftable rule handler number of given chain/target combination. + Handler is required when adding NAT/Conntrack helper targets """ + for x in json: + if x['chain'] != chain: + continue + if x['target'] != target: + continue + return x['handle'] + + return None + + +def verify_rule(rule, err_msg): + """ Common verify steps used for both source and destination NAT """ + if rule['translation_port'] or rule['dest_port'] or rule['source_port']: + if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + proto = rule['protocol'] + raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")') + + if '/' in rule['translation_address']: + raise ConfigError(f'{err_msg}\n' \ + 'Cannot use ports with an IPv4net type translation address as it\n' \ + 'statically maps a whole network of addresses onto another\n' \ + 'network of addresses') + + +def parse_configuration(conf, source_dest): + """ Common wrapper to read in both NAT source and destination CLI """ + ruleset = [] + base_level = ['nat', source_dest] + conf.set_level(base_level) + for number in conf.list_nodes(['rule']): + rule = { + 'description': '', + 'dest_address': '', + 'dest_port': '', + 'disabled': False, + 'exclude': False, + 'interface_in': '', + 'interface_out': '', + 'log': False, + 'protocol': 'all', + 'number': number, + 'source_address': '', + 'source_prefix': '', + 'source_port': '', + 'translation_address': '', + 'translation_prefix': '', + 'translation_port': '' + } + conf.set_level(base_level + ['rule', number]) + + if conf.exists(['description']): + rule['description'] = conf.return_value(['description']) + + if conf.exists(['destination', 'address']): + tmp = conf.return_value(['destination', 'address']) + if tmp.startswith('!'): + tmp = tmp.replace('!', '!=') + rule['dest_address'] = tmp + + if conf.exists(['destination', 'port']): + tmp = conf.return_value(['destination', 'port']) + if tmp.startswith('!'): + tmp = tmp.replace('!', '!=') + rule['dest_port'] = tmp + + if conf.exists(['disable']): + rule['disabled'] = True + + if conf.exists(['exclude']): + rule['exclude'] = True + + if conf.exists(['inbound-interface']): + rule['interface_in'] = conf.return_value(['inbound-interface']) + + if conf.exists(['outbound-interface']): + rule['interface_out'] = conf.return_value(['outbound-interface']) + + if conf.exists(['log']): + rule['log'] = True + + if conf.exists(['protocol']): + rule['protocol'] = conf.return_value(['protocol']) + + if conf.exists(['source', 'address']): + tmp = conf.return_value(['source', 'address']) + if tmp.startswith('!'): + tmp = tmp.replace('!', '!=') + rule['source_address'] = tmp + + if conf.exists(['source', 'prefix']): + rule['source_prefix'] = conf.return_value(['source', 'prefix']) + + if conf.exists(['source', 'port']): + tmp = conf.return_value(['source', 'port']) + if tmp.startswith('!'): + tmp = tmp.replace('!', '!=') + rule['source_port'] = tmp + + if conf.exists(['translation', 'address']): + rule['translation_address'] = conf.return_value(['translation', 'address']) + + if conf.exists(['translation', 'prefix']): + rule['translation_prefix'] = conf.return_value(['translation', 'prefix']) + + if conf.exists(['translation', 'port']): + rule['translation_port'] = conf.return_value(['translation', 'port']) + + ruleset.append(rule) + + return ruleset + +def get_config(): + nat = deepcopy(default_config_data) + conf = Config() + + # read in current nftable (once) for further processing + tmp = cmd('nft -j list table raw') + nftable_json = json.loads(tmp) + + # condense the full JSON table into a list with only relevand informations + pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}' + condensed_json = jmespath.search(pattern, nftable_json) + + if not conf.exists(['nat']): + nat['helper_functions'] = 'remove' + + # Retrieve current table handler positions + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER') + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') + + nat['deleted'] = True + + return nat + + # check if NAT connection tracking helpers need to be set up - this has to + # be done only once + if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'): + nat['helper_functions'] = 'add' + + # Retrieve current table handler positions + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE') + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK') + + # set config level for parsing in NAT configuration + conf.set_level(['nat']) + + # use a common wrapper function to read in the source / destination + # tree from the config - thus we do not need to replicate almost the + # same code :-) + for tgt in ['source', 'destination', 'nptv6']: + nat[tgt] = parse_configuration(conf, tgt) + + return nat + +def verify(nat): + if nat['deleted']: + # no need to verify the CLI as NAT is going to be deactivated + return None + + if nat['helper_functions']: + if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']): + raise Exception('could not determine nftable ruleset handlers') + + for rule in nat['source']: + interface = rule['interface_out'] + err_msg = f'Source NAT configuration error in rule "{rule["number"]}":' + + if interface and interface not in 'any' and interface not in interfaces(): + print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system') + + if not rule['interface_out']: + raise ConfigError(f'{err_msg} outbound-interface not specified') + + if rule['translation_address']: + addr = rule['translation_address'] + if addr != 'masquerade' and not is_addr_assigned(addr): + print(f'Warning: IP address {addr} does not exist on the system!') + + # common rule verification + verify_rule(rule, err_msg) + + for rule in nat['destination']: + interface = rule['interface_in'] + err_msg = f'Destination NAT configuration error in rule "{rule["number"]}":' + + if interface and interface not in 'any' and interface not in interfaces(): + print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system') + + if not rule['interface_in']: + raise ConfigError(f'{err_msg} inbound-interface not specified') + + # common rule verification + verify_rule(rule, err_msg) + + return None + +def generate(nat): + render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755) + return None + +def apply(nat): + cmd(f'{iptables_nat_config}') + if os.path.isfile(iptables_nat_config): + os.unlink(iptables_nat_config) + + return None + +if __name__ == '__main__': + try: + check_kmod(k_mod) + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py new file mode 100755 index 000000000..bba8f87a4 --- /dev/null +++ b/src/conf_mode/ntp.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.config import Config +from vyos.configverify import verify_vrf +from vyos import ConfigError +from vyos.util import call +from vyos.template import render +from vyos import airbag +airbag.enable() + +config_file = r'/etc/ntp.conf' +systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf' + +def get_config(): + conf = Config() + base = ['system', 'ntp'] + + ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return ntp + +def verify(ntp): + # bail out early - looks like removal from running config + if not ntp: + return None + + if len(ntp.get('allow_clients', {})) and not (len(ntp.get('server', {})) > 0): + raise ConfigError('NTP server not configured') + + verify_vrf(ntp) + return None + +def generate(ntp): + # bail out early - looks like removal from running config + if not ntp: + return None + + render(config_file, 'ntp/ntp.conf.tmpl', ntp, trim_blocks=True) + render(systemd_override, 'ntp/override.conf.tmpl', ntp, trim_blocks=True) + + return None + +def apply(ntp): + if not ntp: + # NTP support is removed in the commit + call('systemctl stop ntp.service') + if os.path.exists(config_file): + os.unlink(config_file) + if os.path.isfile(systemd_override): + os.unlink(systemd_override) + + # Reload systemd manager configuration + call('systemctl daemon-reload') + if ntp: + call('systemctl restart ntp.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py new file mode 100755 index 000000000..c8e791c78 --- /dev/null +++ b/src/conf_mode/protocols_bfd.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy + +from vyos.config import Config +from vyos.validate import is_ipv6_link_local, is_ipv6 +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/bfd.frr' + +default_config_data = { + 'new_peers': [], + 'old_peers' : [] +} + +# get configuration for BFD peer from proposed or effective configuration +def get_bfd_peer_config(peer, conf_mode="proposed"): + conf = Config() + conf.set_level('protocols bfd peer {0}'.format(peer)) + + bfd_peer = { + 'remote': peer, + 'shutdown': False, + 'src_if': '', + 'src_addr': '', + 'multiplier': '3', + 'rx_interval': '300', + 'tx_interval': '300', + 'multihop': False, + 'echo_interval': '', + 'echo_mode': False, + } + + # Check if individual peer is disabled + if conf_mode == "effective" and conf.exists_effective('shutdown'): + bfd_peer['shutdown'] = True + if conf_mode == "proposed" and conf.exists('shutdown'): + bfd_peer['shutdown'] = True + + # Check if peer has a local source interface configured + if conf_mode == "effective" and conf.exists_effective('source interface'): + bfd_peer['src_if'] = conf.return_effective_value('source interface') + if conf_mode == "proposed" and conf.exists('source interface'): + bfd_peer['src_if'] = conf.return_value('source interface') + + # Check if peer has a local source address configured - this is mandatory for IPv6 + if conf_mode == "effective" and conf.exists_effective('source address'): + bfd_peer['src_addr'] = conf.return_effective_value('source address') + if conf_mode == "proposed" and conf.exists('source address'): + bfd_peer['src_addr'] = conf.return_value('source address') + + # Tell BFD daemon that we should expect packets with TTL less than 254 + # (because it will take more than one hop) and to listen on the multihop + # port (4784) + if conf_mode == "effective" and conf.exists_effective('multihop'): + bfd_peer['multihop'] = True + if conf_mode == "proposed" and conf.exists('multihop'): + bfd_peer['multihop'] = True + + # Configures the minimum interval that this system is capable of receiving + # control packets. The default value is 300 milliseconds. + if conf_mode == "effective" and conf.exists_effective('interval receive'): + bfd_peer['rx_interval'] = conf.return_effective_value('interval receive') + if conf_mode == "proposed" and conf.exists('interval receive'): + bfd_peer['rx_interval'] = conf.return_value('interval receive') + + # The minimum transmission interval (less jitter) that this system wants + # to use to send BFD control packets. + if conf_mode == "effective" and conf.exists_effective('interval transmit'): + bfd_peer['tx_interval'] = conf.return_effective_value('interval transmit') + if conf_mode == "proposed" and conf.exists('interval transmit'): + bfd_peer['tx_interval'] = conf.return_value('interval transmit') + + # Configures the detection multiplier to determine packet loss. The remote + # transmission interval will be multiplied by this value to determine the + # connection loss detection timer. The default value is 3. + if conf_mode == "effective" and conf.exists_effective('interval multiplier'): + bfd_peer['multiplier'] = conf.return_effective_value('interval multiplier') + if conf_mode == "proposed" and conf.exists('interval multiplier'): + bfd_peer['multiplier'] = conf.return_value('interval multiplier') + + # Configures the minimal echo receive transmission interval that this system is capable of handling + if conf_mode == "effective" and conf.exists_effective('interval echo-interval'): + bfd_peer['echo_interval'] = conf.return_effective_value('interval echo-interval') + if conf_mode == "proposed" and conf.exists('interval echo-interval'): + bfd_peer['echo_interval'] = conf.return_value('interval echo-interval') + + # Enables or disables the echo transmission mode + if conf_mode == "effective" and conf.exists_effective('echo-mode'): + bfd_peer['echo_mode'] = True + if conf_mode == "proposed" and conf.exists('echo-mode'): + bfd_peer['echo_mode'] = True + + return bfd_peer + +def get_config(): + bfd = deepcopy(default_config_data) + conf = Config() + if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')): + return None + else: + conf.set_level('protocols bfd') + + # as we have to use vtysh to talk to FRR we also need to know + # which peers are gone due to a config removal - thus we read in + # all peers (active or to delete) + for peer in conf.list_effective_nodes('peer'): + bfd['old_peers'].append(get_bfd_peer_config(peer, "effective")) + + for peer in conf.list_nodes('peer'): + bfd['new_peers'].append(get_bfd_peer_config(peer)) + + # find deleted peers + set_new_peers = set(conf.list_nodes('peer')) + set_old_peers = set(conf.list_effective_nodes('peer')) + bfd['deleted_peers'] = set_old_peers - set_new_peers + + return bfd + +def verify(bfd): + if bfd is None: + return None + + # some variables to use later + conf = Config() + + for peer in bfd['new_peers']: + # IPv6 link local peers require an explicit local address/interface + if is_ipv6_link_local(peer['remote']): + if not (peer['src_if'] and peer['src_addr']): + raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting') + + # IPv6 peers require an explicit local address + if is_ipv6(peer['remote']): + if not peer['src_addr']: + raise ConfigError('BFD IPv6 peers require explicit local address setting') + + # multihop require source address + if peer['multihop'] and not peer['src_addr']: + raise ConfigError('Multihop require source address') + + # multihop and echo-mode cannot be used together + if peer['multihop'] and peer['echo_mode']: + raise ConfigError('Multihop and echo-mode cannot be used together') + + # multihop doesn't accept interface names + if peer['multihop'] and peer['src_if']: + raise ConfigError('Multihop and source interface cannot be used together') + + # echo interval can be configured only with enabled echo-mode + if peer['echo_interval'] != '' and not peer['echo_mode']: + raise ConfigError('echo-interval can be configured only with enabled echo-mode') + + # check if we deleted peers are not used in configuration + if conf.exists('protocols bgp'): + bgp_as = conf.list_nodes('protocols bgp')[0] + + # check BGP neighbors + for peer in bfd['deleted_peers']: + if conf.exists('protocols bgp {0} neighbor {1} bfd'.format(bgp_as, peer)): + raise ConfigError('Cannot delete BFD peer {0}: it is used in BGP configuration'.format(peer)) + if conf.exists('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer)): + peer_group = conf.return_value('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer)) + if conf.exists('protocols bgp {0} peer-group {1} bfd'.format(bgp_as, peer_group)): + raise ConfigError('Cannot delete BFD peer {0}: it belongs to BGP peer-group {1} with enabled BFD'.format(peer, peer_group)) + + return None + +def generate(bfd): + if bfd is None: + return None + + render(config_file, 'frr/bfd.frr.tmpl', bfd) + return None + +def apply(bfd): + if bfd is None: + return None + + call("vtysh -d bfdd -f " + config_file) + if os.path.exists(config_file): + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py new file mode 100755 index 000000000..3aa76d866 --- /dev/null +++ b/src/conf_mode/protocols_bgp.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import jmespath + +from copy import deepcopy +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos import ConfigError, airbag +airbag.enable() + +config_file = r'/tmp/bgp.frr' + +default_config_data = { + 'as_number': '' +} + +def get_config(): + bgp = deepcopy(default_config_data) + conf = Config() + + # this lives in the "nbgp" tree until we switch over + base = ['protocols', 'nbgp'] + if not conf.exists(base): + return None + + bgp = deepcopy(default_config_data) + # Get full BGP configuration as dictionary - output the configuration for development + # + # vyos@vyos# commit + # [ protocols nbgp 65000 ] + # {'nbgp': {'65000': {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {}, + # '2.2.2.0/24': {}}}, + # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}}, + # 'neighbor': {'192.0.2.1': {'password': 'foo', + # 'remote-as': '100'}}}}} + # + tmp = conf.get_config_dict(base) + + # extract base key from dict as this is our AS number + bgp['as_number'] = jmespath.search('nbgp | keys(@) [0]', tmp) + + # adjust level of dictionary returned by get_config_dict() + # by using jmesgpath and update dictionary + bgp.update(jmespath.search('nbgp.* | [0]', tmp)) + + from pprint import pprint + pprint(bgp) + # resulting in e.g. + # vyos@vyos# commit + # [ protocols nbgp 65000 ] + # {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {}, + # '2.2.2.0/24': {}}}, + # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}}, + # 'as_number': '65000', + # 'neighbor': {'192.0.2.1': {'password': 'foo', 'remote-as': '100'}}, + # 'timers': {'holdtime': '5'}} + + return bgp + +def verify(bgp): + # bail out early - looks like removal from running config + if not bgp: + return None + + return None + +def generate(bgp): + # bail out early - looks like removal from running config + if not bgp: + return None + + render(config_file, 'frr/bgp.frr.tmpl', bgp) + return None + +def apply(bgp): + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py new file mode 100755 index 000000000..ca148fd6a --- /dev/null +++ b/src/conf_mode/protocols_igmp.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from ipaddress import IPv4Address +from sys import exit + +from vyos import ConfigError +from vyos.config import Config +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/igmp.frr' + +def get_config(): + conf = Config() + igmp_conf = { + 'igmp_conf' : False, + 'old_ifaces' : {}, + 'ifaces' : {} + } + if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')): + return None + + if conf.exists('protocols igmp'): + igmp_conf['igmp_conf'] = True + + conf.set_level('protocols igmp') + + # # Get interfaces + for iface in conf.list_effective_nodes('interface'): + igmp_conf['old_ifaces'].update({ + iface : { + 'version' : conf.return_effective_value('interface {0} version'.format(iface)), + 'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)), + 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)), + 'gr_join' : {} + } + }) + for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)): + igmp_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join)) + + for iface in conf.list_nodes('interface'): + igmp_conf['ifaces'].update({ + iface : { + 'version' : conf.return_value('interface {0} version'.format(iface)), + 'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)), + 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)), + 'gr_join' : {} + } + }) + for gr_join in conf.list_nodes('interface {0} join'.format(iface)): + igmp_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join)) + + return igmp_conf + +def verify(igmp): + if igmp is None: + return None + + if igmp['igmp_conf']: + # Check interfaces + if not igmp['ifaces']: + raise ConfigError(f"IGMP require defined interfaces!") + # Check, is this multicast group + for intfc in igmp['ifaces']: + for gr_addr in igmp['ifaces'][intfc]['gr_join']: + if IPv4Address(gr_addr) < IPv4Address('224.0.0.0'): + raise ConfigError(gr_addr + " not a multicast group") + +def generate(igmp): + if igmp is None: + return None + + render(config_file, 'frr/igmp.frr.tmpl', igmp) + return None + +def apply(igmp): + if igmp is None: + return None + + if os.path.exists(config_file): + call(f'vtysh -d pimd -f {config_file}') + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py new file mode 100755 index 000000000..bcb16fa04 --- /dev/null +++ b/src/conf_mode/protocols_mpls.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/ldpd.frr' + +def sysctl(name, value): + call('sysctl -wq {}={}'.format(name, value)) + +def get_config(): + conf = Config() + mpls_conf = { + 'router_id' : None, + 'mpls_ldp' : False, + 'old_ldp' : { + 'interfaces' : [], + 'neighbors' : {}, + 'd_transp_ipv4' : None, + 'd_transp_ipv6' : None, + 'hello_holdtime' : None, + 'hello_interval' : None + }, + 'ldp' : { + 'interfaces' : [], + 'neighbors' : {}, + 'd_transp_ipv4' : None, + 'd_transp_ipv6' : None, + 'hello_holdtime' : None, + 'hello_interval' : None + } + } + if not (conf.exists('protocols mpls') or conf.exists_effective('protocols mpls')): + return None + + if conf.exists('protocols mpls ldp'): + mpls_conf['mpls_ldp'] = True + + conf.set_level('protocols mpls ldp') + + # Get router-id + if conf.exists_effective('router-id'): + mpls_conf['old_router_id'] = conf.return_effective_value('router-id') + if conf.exists('router-id'): + mpls_conf['router_id'] = conf.return_value('router-id') + + # Get hello holdtime + if conf.exists_effective('discovery hello-holdtime'): + mpls_conf['old_ldp']['hello_holdtime'] = conf.return_effective_value('discovery hello-holdtime') + + if conf.exists('discovery hello-holdtime'): + mpls_conf['ldp']['hello_holdtime'] = conf.return_value('discovery hello-holdtime') + + # Get hello interval + if conf.exists_effective('discovery hello-interval'): + mpls_conf['old_ldp']['hello_interval'] = conf.return_effective_value('discovery hello-interval') + + if conf.exists('discovery hello-interval'): + mpls_conf['ldp']['hello_interval'] = conf.return_value('discovery hello-interval') + + # Get discovery transport-ipv4-address + if conf.exists_effective('discovery transport-ipv4-address'): + mpls_conf['old_ldp']['d_transp_ipv4'] = conf.return_effective_value('discovery transport-ipv4-address') + + if conf.exists('discovery transport-ipv4-address'): + mpls_conf['ldp']['d_transp_ipv4'] = conf.return_value('discovery transport-ipv4-address') + + # Get discovery transport-ipv6-address + if conf.exists_effective('discovery transport-ipv6-address'): + mpls_conf['old_ldp']['d_transp_ipv6'] = conf.return_effective_value('discovery transport-ipv6-address') + + if conf.exists('discovery transport-ipv6-address'): + mpls_conf['ldp']['d_transp_ipv6'] = conf.return_value('discovery transport-ipv6-address') + + # Get interfaces + if conf.exists_effective('interface'): + mpls_conf['old_ldp']['interfaces'] = conf.return_effective_values('interface') + + if conf.exists('interface'): + mpls_conf['ldp']['interfaces'] = conf.return_values('interface') + + # Get neighbors + for neighbor in conf.list_effective_nodes('neighbor'): + mpls_conf['old_ldp']['neighbors'].update({ + neighbor : { + 'password' : conf.return_effective_value('neighbor {0} password'.format(neighbor)) + } + }) + + for neighbor in conf.list_nodes('neighbor'): + mpls_conf['ldp']['neighbors'].update({ + neighbor : { + 'password' : conf.return_value('neighbor {0} password'.format(neighbor)) + } + }) + + return mpls_conf + +def operate_mpls_on_intfc(interfaces, action): + rp_filter = 0 + if action == 1: + rp_filter = 2 + for iface in interfaces: + sysctl('net.mpls.conf.{0}.input'.format(iface), action) + # Operate rp filter + sysctl('net.ipv4.conf.{0}.rp_filter'.format(iface), rp_filter) + +def verify(mpls): + if mpls is None: + return None + + if mpls['mpls_ldp']: + # Requre router-id + if not mpls['router_id']: + raise ConfigError(f"MPLS ldp router-id is mandatory!") + + # Requre discovery transport-address + if not mpls['ldp']['d_transp_ipv4'] and not mpls['ldp']['d_transp_ipv6']: + raise ConfigError(f"MPLS ldp discovery transport address is mandatory!") + + # Requre interface + if not mpls['ldp']['interfaces']: + raise ConfigError(f"MPLS ldp interface is mandatory!") + +def generate(mpls): + if mpls is None: + return None + + render(config_file, 'frr/ldpd.frr.tmpl', mpls) + return None + +def apply(mpls): + if mpls is None: + return None + + # Set number of entries in the platform label table + if mpls['mpls_ldp']: + sysctl('net.mpls.platform_labels', '1048575') + else: + sysctl('net.mpls.platform_labels', '0') + + # Do not copy IP TTL to MPLS header + sysctl('net.mpls.ip_ttl_propagate', '0') + + # Allow mpls on interfaces + operate_mpls_on_intfc(mpls['ldp']['interfaces'], 1) + + # Disable mpls on deleted interfaces + diactive_ifaces = set(mpls['old_ldp']['interfaces']).difference(mpls['ldp']['interfaces']) + operate_mpls_on_intfc(diactive_ifaces, 0) + + if os.path.exists(config_file): + call(f'vtysh -d ldpd -f {config_file}') + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py new file mode 100755 index 000000000..8aa324bac --- /dev/null +++ b/src/conf_mode/protocols_pim.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from ipaddress import IPv4Address +from sys import exit + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/pimd.frr' + +def get_config(): + conf = Config() + pim_conf = { + 'pim_conf' : False, + 'old_pim' : { + 'ifaces' : {}, + 'rp' : {} + }, + 'pim' : { + 'ifaces' : {}, + 'rp' : {} + } + } + if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')): + return None + + if conf.exists('protocols pim'): + pim_conf['pim_conf'] = True + + conf.set_level('protocols pim') + + # Get interfaces + for iface in conf.list_effective_nodes('interface'): + pim_conf['old_pim']['ifaces'].update({ + iface : { + 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)), + 'dr_prio' : conf.return_effective_value('interface {0} dr-priority'.format(iface)) + } + }) + + for iface in conf.list_nodes('interface'): + pim_conf['pim']['ifaces'].update({ + iface : { + 'hello' : conf.return_value('interface {0} hello'.format(iface)), + 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)), + } + }) + + conf.set_level('protocols pim rp') + + # Get RPs addresses + for rp_addr in conf.list_effective_nodes('address'): + pim_conf['old_pim']['rp'][rp_addr] = conf.return_effective_values('address {0} group'.format(rp_addr)) + + for rp_addr in conf.list_nodes('address'): + pim_conf['pim']['rp'][rp_addr] = conf.return_values('address {0} group'.format(rp_addr)) + + # Get RP keep-alive-timer + if conf.exists_effective('rp keep-alive-timer'): + pim_conf['old_pim']['rp_keep_alive'] = conf.return_effective_value('rp keep-alive-timer') + if conf.exists('rp keep-alive-timer'): + pim_conf['pim']['rp_keep_alive'] = conf.return_value('rp keep-alive-timer') + + return pim_conf + +def verify(pim): + if pim is None: + return None + + if pim['pim_conf']: + # Check interfaces + if not pim['pim']['ifaces']: + raise ConfigError(f"PIM require defined interfaces!") + + if not pim['pim']['rp']: + raise ConfigError(f"RP address required") + + # Check unique multicast groups + uniq_groups = [] + for rp_addr in pim['pim']['rp']: + if not pim['pim']['rp'][rp_addr]: + raise ConfigError(f"Group should be specified for RP " + rp_addr) + for group in pim['pim']['rp'][rp_addr]: + if (group in uniq_groups): + raise ConfigError(f"Group range " + group + " specified cannot exact match another") + + # Check, is this multicast group + gr_addr = group.split('/') + if IPv4Address(gr_addr[0]) < IPv4Address('224.0.0.0'): + raise ConfigError(group + " not a multicast group") + + uniq_groups.extend(pim['pim']['rp'][rp_addr]) + +def generate(pim): + if pim is None: + return None + + render(config_file, 'frr/pimd.frr.tmpl', pim) + return None + +def apply(pim): + if pim is None: + return None + + if os.path.exists(config_file): + call("vtysh -d pimd -f " + config_file) + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py new file mode 100755 index 000000000..4f8816d61 --- /dev/null +++ b/src/conf_mode/protocols_rip.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos import ConfigError +from vyos.config import Config +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/ripd.frr' + +def get_config(): + conf = Config() + base = ['protocols', 'rip'] + rip_conf = { + 'rip_conf' : False, + 'default_distance' : [], + 'default_originate' : False, + 'old_rip' : { + 'default_metric' : [], + 'distribute' : {}, + 'neighbors' : {}, + 'networks' : {}, + 'net_distance' : {}, + 'passive_iface' : {}, + 'redist' : {}, + 'route' : {}, + 'ifaces' : {}, + 'timer_garbage' : 120, + 'timer_timeout' : 180, + 'timer_update' : 30 + }, + 'rip' : { + 'default_metric' : None, + 'distribute' : {}, + 'neighbors' : {}, + 'networks' : {}, + 'net_distance' : {}, + 'passive_iface' : {}, + 'redist' : {}, + 'route' : {}, + 'ifaces' : {}, + 'timer_garbage' : 120, + 'timer_timeout' : 180, + 'timer_update' : 30 + } + } + + if not (conf.exists(base) or conf.exists_effective(base)): + return None + + if conf.exists(base): + rip_conf['rip_conf'] = True + + conf.set_level(base) + + # Get default distance + if conf.exists_effective('default-distance'): + rip_conf['old_default_distance'] = conf.return_effective_value('default-distance') + + if conf.exists('default-distance'): + rip_conf['default_distance'] = conf.return_value('default-distance') + + # Get default information originate (originate default route) + if conf.exists_effective('default-information originate'): + rip_conf['old_default_originate'] = True + + if conf.exists('default-information originate'): + rip_conf['default_originate'] = True + + # Get default-metric + if conf.exists_effective('default-metric'): + rip_conf['old_rip']['default_metric'] = conf.return_effective_value('default-metric') + + if conf.exists('default-metric'): + rip_conf['rip']['default_metric'] = conf.return_value('default-metric') + + # Get distribute list interface old_rip + for dist_iface in conf.list_effective_nodes('distribute-list interface'): + # Set level 'distribute-list interface ethX' + conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface) + rip_conf['rip']['distribute'].update({ + dist_iface : { + 'iface_access_list_in': conf.return_effective_value('access-list in'.format(dist_iface)), + 'iface_access_list_out': conf.return_effective_value('access-list out'.format(dist_iface)), + 'iface_prefix_list_in': conf.return_effective_value('prefix-list in'.format(dist_iface)), + 'iface_prefix_list_out': conf.return_effective_value('prefix-list out'.format(dist_iface)) + } + }) + + # Access-list in old_rip + if conf.exists_effective('access-list in'.format(dist_iface)): + rip_conf['old_rip']['iface_access_list_in'] = conf.return_effective_value('access-list in'.format(dist_iface)) + # Access-list out old_rip + if conf.exists_effective('access-list out'.format(dist_iface)): + rip_conf['old_rip']['iface_access_list_out'] = conf.return_effective_value('access-list out'.format(dist_iface)) + # Prefix-list in old_rip + if conf.exists_effective('prefix-list in'.format(dist_iface)): + rip_conf['old_rip']['iface_prefix_list_in'] = conf.return_effective_value('prefix-list in'.format(dist_iface)) + # Prefix-list out old_rip + if conf.exists_effective('prefix-list out'.format(dist_iface)): + rip_conf['old_rip']['iface_prefix_list_out'] = conf.return_effective_value('prefix-list out'.format(dist_iface)) + + conf.set_level(base) + + # Get distribute list interface + for dist_iface in conf.list_nodes('distribute-list interface'): + # Set level 'distribute-list interface ethX' + conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface) + rip_conf['rip']['distribute'].update({ + dist_iface : { + 'iface_access_list_in': conf.return_value('access-list in'.format(dist_iface)), + 'iface_access_list_out': conf.return_value('access-list out'.format(dist_iface)), + 'iface_prefix_list_in': conf.return_value('prefix-list in'.format(dist_iface)), + 'iface_prefix_list_out': conf.return_value('prefix-list out'.format(dist_iface)) + } + }) + + # Access-list in + if conf.exists('access-list in'.format(dist_iface)): + rip_conf['rip']['iface_access_list_in'] = conf.return_value('access-list in'.format(dist_iface)) + # Access-list out + if conf.exists('access-list out'.format(dist_iface)): + rip_conf['rip']['iface_access_list_out'] = conf.return_value('access-list out'.format(dist_iface)) + # Prefix-list in + if conf.exists('prefix-list in'.format(dist_iface)): + rip_conf['rip']['iface_prefix_list_in'] = conf.return_value('prefix-list in'.format(dist_iface)) + # Prefix-list out + if conf.exists('prefix-list out'.format(dist_iface)): + rip_conf['rip']['iface_prefix_list_out'] = conf.return_value('prefix-list out'.format(dist_iface)) + + conf.set_level((str(base)) + ' distribute-list') + + # Get distribute list, access-list in + if conf.exists_effective('access-list in'): + rip_conf['old_rip']['dist_acl_in'] = conf.return_effective_value('access-list in') + + if conf.exists('access-list in'): + rip_conf['rip']['dist_acl_in'] = conf.return_value('access-list in') + + # Get distribute list, access-list out + if conf.exists_effective('access-list out'): + rip_conf['old_rip']['dist_acl_out'] = conf.return_effective_value('access-list out') + + if conf.exists('access-list out'): + rip_conf['rip']['dist_acl_out'] = conf.return_value('access-list out') + + # Get ditstribute list, prefix-list in + if conf.exists_effective('prefix-list in'): + rip_conf['old_rip']['dist_prfx_in'] = conf.return_effective_value('prefix-list in') + + if conf.exists('prefix-list in'): + rip_conf['rip']['dist_prfx_in'] = conf.return_value('prefix-list in') + + # Get distribute list, prefix-list out + if conf.exists_effective('prefix-list out'): + rip_conf['old_rip']['dist_prfx_out'] = conf.return_effective_value('prefix-list out') + + if conf.exists('prefix-list out'): + rip_conf['rip']['dist_prfx_out'] = conf.return_value('prefix-list out') + + conf.set_level(base) + + # Get network Interfaces + if conf.exists_effective('interface'): + rip_conf['old_rip']['ifaces'] = conf.return_effective_values('interface') + + if conf.exists('interface'): + rip_conf['rip']['ifaces'] = conf.return_values('interface') + + # Get neighbors + if conf.exists_effective('neighbor'): + rip_conf['old_rip']['neighbors'] = conf.return_effective_values('neighbor') + + if conf.exists('neighbor'): + rip_conf['rip']['neighbors'] = conf.return_values('neighbor') + + # Get networks + if conf.exists_effective('network'): + rip_conf['old_rip']['networks'] = conf.return_effective_values('network') + + if conf.exists('network'): + rip_conf['rip']['networks'] = conf.return_values('network') + + # Get network-distance old_rip + for net_dist in conf.list_effective_nodes('network-distance'): + rip_conf['old_rip']['net_distance'].update({ + net_dist : { + 'access_list' : conf.return_effective_value('network-distance {0} access-list'.format(net_dist)), + 'distance' : conf.return_effective_value('network-distance {0} distance'.format(net_dist)), + } + }) + + # Get network-distance + for net_dist in conf.list_nodes('network-distance'): + rip_conf['rip']['net_distance'].update({ + net_dist : { + 'access_list' : conf.return_value('network-distance {0} access-list'.format(net_dist)), + 'distance' : conf.return_value('network-distance {0} distance'.format(net_dist)), + } + }) + + # Get passive-interface + if conf.exists_effective('passive-interface'): + rip_conf['old_rip']['passive_iface'] = conf.return_effective_values('passive-interface') + + if conf.exists('passive-interface'): + rip_conf['rip']['passive_iface'] = conf.return_values('passive-interface') + + # Get redistribute for old_rip + for protocol in conf.list_effective_nodes('redistribute'): + rip_conf['old_rip']['redist'].update({ + protocol : { + 'metric' : conf.return_effective_value('redistribute {0} metric'.format(protocol)), + 'route_map' : conf.return_effective_value('redistribute {0} route-map'.format(protocol)), + } + }) + + # Get redistribute + for protocol in conf.list_nodes('redistribute'): + rip_conf['rip']['redist'].update({ + protocol : { + 'metric' : conf.return_value('redistribute {0} metric'.format(protocol)), + 'route_map' : conf.return_value('redistribute {0} route-map'.format(protocol)), + } + }) + + conf.set_level(base) + + # Get route + if conf.exists_effective('route'): + rip_conf['old_rip']['route'] = conf.return_effective_values('route') + + if conf.exists('route'): + rip_conf['rip']['route'] = conf.return_values('route') + + # Get timers garbage + if conf.exists_effective('timers garbage-collection'): + rip_conf['old_rip']['timer_garbage'] = conf.return_effective_value('timers garbage-collection') + + if conf.exists('timers garbage-collection'): + rip_conf['rip']['timer_garbage'] = conf.return_value('timers garbage-collection') + + # Get timers timeout + if conf.exists_effective('timers timeout'): + rip_conf['old_rip']['timer_timeout'] = conf.return_effective_value('timers timeout') + + if conf.exists('timers timeout'): + rip_conf['rip']['timer_timeout'] = conf.return_value('timers timeout') + + # Get timers update + if conf.exists_effective('timers update'): + rip_conf['old_rip']['timer_update'] = conf.return_effective_value('timers update') + + if conf.exists('timers update'): + rip_conf['rip']['timer_update'] = conf.return_value('timers update') + + return rip_conf + +def verify(rip): + if rip is None: + return None + + # Check for network. If network-distance acl is set and distance not set + for net in rip['rip']['net_distance']: + if not rip['rip']['net_distance'][net]['distance']: + raise ConfigError(f"Must specify distance for network {net}") + +def generate(rip): + if rip is None: + return None + + render(config_file, 'frr/rip.frr.tmpl', rip) + return None + +def apply(rip): + if rip is None: + return None + + if os.path.exists(config_file): + call(f'vtysh -d ripd -f {config_file}') + os.remove(config_file) + else: + print("File {0} not found".format(config_file)) + + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) + diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py new file mode 100755 index 000000000..232d1e181 --- /dev/null +++ b/src/conf_mode/protocols_static_multicast.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from ipaddress import IPv4Address +from sys import exit + +from vyos import ConfigError +from vyos.config import Config +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/tmp/static_mcast.frr' + +# Get configuration for static multicast route +def get_config(): + conf = Config() + mroute = { + 'old_mroute' : {}, + 'mroute' : {} + } + + base_path = "protocols static multicast" + + if not (conf.exists(base_path) or conf.exists_effective(base_path)): + return None + + conf.set_level(base_path) + + # Get multicast effective routes + for route in conf.list_effective_nodes('route'): + mroute['old_mroute'][route] = {} + for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)): + mroute['old_mroute'][route].update({ + next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) + }) + + # Get multicast effective interface-routes + for route in conf.list_effective_nodes('interface-route'): + if not route in mroute['old_mroute']: + mroute['old_mroute'][route] = {} + for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)): + mroute['old_mroute'][route].update({ + next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) + }) + + # Get multicast routes + for route in conf.list_nodes('route'): + mroute['mroute'][route] = {} + for next_hop in conf.list_nodes('route {0} next-hop'.format(route)): + mroute['mroute'][route].update({ + next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) + }) + + # Get multicast interface-routes + for route in conf.list_nodes('interface-route'): + if not route in mroute['mroute']: + mroute['mroute'][route] = {} + for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)): + mroute['mroute'][route].update({ + next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) + }) + + return mroute + +def verify(mroute): + if mroute is None: + return None + + for route in mroute['mroute']: + route = route.split('/') + if IPv4Address(route[0]) < IPv4Address('224.0.0.0'): + raise ConfigError(route + " not a multicast network") + +def generate(mroute): + if mroute is None: + return None + + render(config_file, 'frr/static_mcast.frr.tmpl', mroute) + return None + +def apply(mroute): + if mroute is None: + return None + + if os.path.exists(config_file): + call(f'vtysh -d staticd -f {config_file}') + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py new file mode 100755 index 000000000..3343d1247 --- /dev/null +++ b/src/conf_mode/salt-minion.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from copy import deepcopy +from socket import gethostname +from sys import exit +from urllib3 import PoolManager + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, chown +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +config_file = r'/etc/salt/minion' +master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub' + +default_config_data = { + 'hash': 'sha256', + 'log_level': 'warning', + 'master' : 'salt', + 'user': 'minion', + 'group': 'vyattacfg', + 'salt_id': gethostname(), + 'mine_interval': '60', + 'verify_master_pubkey_sign': 'false', + 'master_key': '' +} + +def get_config(): + salt = deepcopy(default_config_data) + conf = Config() + base = ['service', 'salt-minion'] + + if not conf.exists(base): + return None + else: + conf.set_level(base) + + if conf.exists(['hash']): + salt['hash'] = conf.return_value(['hash']) + + if conf.exists(['master']): + salt['master'] = conf.return_values(['master']) + + if conf.exists(['id']): + salt['salt_id'] = conf.return_value(['id']) + + if conf.exists(['user']): + salt['user'] = conf.return_value(['user']) + + if conf.exists(['interval']): + salt['interval'] = conf.return_value(['interval']) + + if conf.exists(['master-key']): + salt['master_key'] = conf.return_value(['master-key']) + salt['verify_master_pubkey_sign'] = 'true' + + return salt + +def verify(salt): + return None + +def generate(salt): + if not salt: + return None + + render(config_file, 'salt-minion/minion.tmpl', salt, + user=salt['user'], group=salt['group']) + + if not os.path.exists(master_keyfile): + if salt['master_key']: + req = PoolManager().request('GET', salt['master_key'], preload_content=False) + + with open(master_keyfile, 'wb') as f: + while True: + data = req.read(1024) + if not data: + break + f.write(data) + + req.release_conn() + chown(master_keyfile, salt['user'], salt['group']) + + return None + +def apply(salt): + if not salt: + # Salt removed from running config + call('systemctl stop salt-minion.service') + if os.path.exists(config_file): + os.unlink(config_file) + else: + call('systemctl restart salt-minion.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py new file mode 100755 index 000000000..613ec6879 --- /dev/null +++ b/src/conf_mode/service_console-server.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError + +config_file = r'/run/conserver/conserver.cf' + +def get_config(): + conf = Config() + base = ['service', 'console-server'] + + # Retrieve CLI representation as dictionary + proxy = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) + # The retrieved dictionary will look something like this: + # + # {'device': {'usb0b2.4p1.0': {'speed': '9600'}, + # 'usb0b2.4p1.1': {'data_bits': '8', + # 'parity': 'none', + # 'speed': '115200', + # 'stop_bits': '2'}}} + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base + ['device']) + if 'device' in proxy: + for device in proxy['device']: + tmp = dict_merge(default_values, proxy['device'][device]) + proxy['device'][device] = tmp + + return proxy + +def verify(proxy): + if not proxy: + return None + + if 'device' in proxy: + for device in proxy['device']: + if 'speed' not in proxy['device'][device]: + raise ConfigError(f'Serial port speed must be defined for "{device}"!') + + if 'ssh' in proxy['device'][device]: + if 'port' not in proxy['device'][device]['ssh']: + raise ConfigError(f'SSH port must be defined for "{device}"!') + + return None + +def generate(proxy): + if not proxy: + return None + + render(config_file, 'conserver/conserver.conf.tmpl', proxy) + return None + +def apply(proxy): + call('systemctl stop dropbear@*.service conserver-server.service') + + if not proxy: + if os.path.isfile(config_file): + os.unlink(config_file) + return None + + call('systemctl restart conserver-server.service') + + if 'device' in proxy: + for device in proxy['device']: + if 'ssh' in proxy['device'][device]: + port = proxy['device'][device]['ssh']['port'] + call(f'systemctl restart dropbear@{device}.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py new file mode 100755 index 000000000..d46f9578e --- /dev/null +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render +from vyos import airbag +airbag.enable() + +config_file = r'/etc/fastnetmon.conf' +networks_list = r'/etc/networks_list' + +def get_config(): + conf = Config() + base = ['service', 'ids', 'ddos-protection'] + fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return fastnetmon + +def verify(fastnetmon): + if not fastnetmon: + return None + + if not "mode" in fastnetmon: + raise ConfigError('ddos-protection mode is mandatory!') + + if not "network" in fastnetmon: + raise ConfigError('Required define network!') + + if not "listen_interface" in fastnetmon: + raise ConfigError('Define listen-interface is mandatory!') + + if "alert_script" in fastnetmon: + if os.path.isfile(fastnetmon["alert_script"]): + # Check script permissions + if not os.access(fastnetmon["alert_script"], os.X_OK): + raise ConfigError('Script {0} does not have permissions for execution'.format(fastnetmon["alert_script"])) + else: + raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"])) + +def generate(fastnetmon): + if not fastnetmon: + if os.path.isfile(config_file): + os.unlink(config_file) + if os.path.isfile(networks_list): + os.unlink(networks_list) + + return + + render(config_file, 'ids/fastnetmon.tmpl', fastnetmon, trim_blocks=True) + render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon, trim_blocks=True) + + return None + +def apply(fastnetmon): + if not fastnetmon: + # Stop fastnetmon service if removed + call('systemctl stop fastnetmon.service') + else: + call('systemctl restart fastnetmon.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py new file mode 100755 index 000000000..553cc2e97 --- /dev/null +++ b/src/conf_mode/service_ipoe-server.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, get_half_cpus +from vyos.validate import is_ipv4 +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +ipoe_conf = '/run/accel-pppd/ipoe.conf' +ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' + +default_config_data = { + 'auth_mode': 'local', + 'auth_interfaces': [], + 'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template + 'interfaces': [], + 'dnsv4': [], + 'dnsv6': [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + 'radius_server': [], + 'radius_acct_tmo': '3', + 'radius_max_try': '3', + 'radius_timeout': '3', + 'radius_nas_id': '', + 'radius_nas_ip': '', + 'radius_source_address': '', + 'radius_shaper_attr': '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author': '', + 'thread_cnt': get_half_cpus() +} + +def get_config(): + conf = Config() + base_path = ['service', 'ipoe-server'] + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + ipoe = deepcopy(default_config_data) + + for interface in conf.list_nodes(['interface']): + tmp = { + 'mode': 'L2', + 'name': interface, + 'shared': '1', + # may need a config option, can be dhcpv4 or up for unclassified pkts + 'sess_start': 'dhcpv4', + 'range': None, + 'ifcfg': '1', + 'vlan_mon': [] + } + + conf.set_level(base_path + ['interface', interface]) + + if conf.exists(['network-mode']): + tmp['mode'] = conf.return_value(['network-mode']) + + if conf.exists(['network']): + mode = conf.return_value(['network']) + if mode == 'vlan': + tmp['shared'] = '0' + + if conf.exists(['vlan-id']): + tmp['vlan_mon'] += conf.return_values(['vlan-id']) + + if conf.exists(['vlan-range']): + tmp['vlan_mon'] += conf.return_values(['vlan-range']) + + if conf.exists(['client-subnet']): + tmp['range'] = conf.return_value(['client-subnet']) + + ipoe['interfaces'].append(tmp) + + conf.set_level(base_path) + + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + ipoe['dnsv4'].append(name_server) + else: + ipoe['dnsv6'].append(name_server) + + if conf.exists(['authentication', 'mode']): + ipoe['auth_mode'] = conf.return_value(['authentication', 'mode']) + + if conf.exists(['authentication', 'interface']): + for interface in conf.list_nodes(['authentication', 'interface']): + tmp = { + 'name': interface, + 'mac': [] + } + for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']): + client = { + 'address': mac, + 'rate_download': '', + 'rate_upload': '', + 'vlan_id': '' + } + conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac]) + + if conf.exists(['rate-limit', 'download']): + client['rate_download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + client['rate_upload'] = conf.return_value(['rate-limit', 'upload']) + + if conf.exists(['vlan-id']): + client['vlan'] = conf.return_value(['vlan-id']) + + tmp['mac'].append(client) + + ipoe['auth_interfaces'].append(tmp) + + conf.set_level(base_path) + + # + # authentication mode radius servers and settings + if conf.exists(['authentication', 'mode', 'radius']): + for server in conf.list_nodes(['authentication', 'radius', 'server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812', + 'acct_port' : '1813' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail_time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + ipoe['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['acct-timeout']): + ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + ipoe['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + ipoe['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + ipoe['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + ipoe['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dynamic-author']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'server']): + dae['server'] = conf.return_value(['dynamic-author', 'server']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + ipoe['radius_dynamic_author'] = dae + + + conf.set_level(base_path) + if conf.exists(['client-ipv6-pool', 'prefix']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) + + ipoe['client_ipv6_pool'].append(tmp) + + + if conf.exists(['client-ipv6-pool', 'delegate']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) + + ipoe['client_ipv6_delegate_prefix'].append(tmp) + + return ipoe + + +def verify(ipoe): + if not ipoe: + return None + + if not ipoe['interfaces']: + raise ConfigError('No IPoE interface configured') + + for interface in ipoe['interfaces']: + if not interface['range']: + raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"') + + if len(ipoe['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(ipoe['dnsv6']) > 3: + raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + + if ipoe['auth_mode'] == 'radius': + if len(ipoe['radius_server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + for radius in ipoe['radius_server']: + if not radius['key']: + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + + if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']: + raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') + + return None + + +def generate(ipoe): + if not ipoe: + return None + + render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True) + + if ipoe['auth_mode'] == 'local': + render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe) + os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + + else: + if os.path.exists(ipoe_chap_secrets): + os.unlink(ipoe_chap_secrets) + + return None + + +def apply(ipoe): + if ipoe == None: + call('systemctl stop accel-ppp@ipoe.service') + for file in [ipoe_conf, ipoe_chap_secrets]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@ipoe.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py new file mode 100755 index 000000000..1a6b2c328 --- /dev/null +++ b/src/conf_mode/service_mdns-repeater.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from netifaces import ifaddresses, interfaces, AF_INET + +from vyos.config import Config +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = r'/etc/default/mdns-repeater' + +def get_config(): + conf = Config() + base = ['service', 'mdns', 'repeater'] + mdns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return mdns + +def verify(mdns): + if not mdns: + return None + + if 'disable' in mdns: + return None + + # We need at least two interfaces to repeat mDNS advertisments + if 'interface' not in mdns or len(mdns['interface']) < 2: + raise ConfigError('mDNS repeater requires at least 2 configured interfaces!') + + # For mdns-repeater to work it is essential that the interfaces has + # an IPv4 address assigned + for interface in mdns['interface']: + if interface not in interfaces(): + raise ConfigError(f'Interface "{interface}" does not exist!') + + if AF_INET not in ifaddresses(interface): + raise ConfigError('mDNS repeater requires an IPv4 address to be ' + f'configured on interface "{interface}"') + + return None + +def generate(mdns): + if not mdns: + return None + + if 'disable' in mdns: + print('Warning: mDNS repeater will be deactivated because it is disabled') + return None + + render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns) + return None + +def apply(mdns): + if not mdns or 'disable' in mdns: + call('systemctl stop mdns-repeater.service') + if os.path.exists(config_file): + os.unlink(config_file) + else: + call('systemctl restart mdns-repeater.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py new file mode 100755 index 000000000..39d34a7e2 --- /dev/null +++ b/src/conf_mode/service_pppoe-server.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, get_half_cpus +from vyos.validate import is_ipv4 +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +pppoe_conf = r'/run/accel-pppd/pppoe.conf' +pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets' + +default_config_data = { + 'auth_mode': 'local', + 'auth_proto': ['auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap'], + 'chap_secrets_file': pppoe_chap_secrets, # used in Jinja2 template + 'client_ip_pool': '', + 'client_ip_subnets': [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + 'concentrator': 'vyos-ac', + 'interfaces': [], + 'local_users' : [], + + 'svc_name': [], + 'dnsv4': [], + 'dnsv6': [], + 'wins': [], + 'mtu': '1492', + + 'limits_burst': '', + 'limits_connections': '', + 'limits_timeout': '', + + 'pado_delay': '', + 'ppp_ccp': False, + 'ppp_gw': '', + 'ppp_ipv4': '', + 'ppp_ipv6': '', + 'ppp_ipv6_accept_peer_intf_id': False, + 'ppp_ipv6_intf_id': '', + 'ppp_ipv6_peer_intf_id': '', + 'ppp_echo_failure': '3', + 'ppp_echo_interval': '30', + 'ppp_echo_timeout': '0', + 'ppp_min_mtu': '', + 'ppp_mppe': 'prefer', + 'ppp_mru': '', + + 'radius_server': [], + 'radius_acct_tmo': '3', + 'radius_max_try': '3', + 'radius_timeout': '3', + 'radius_nas_id': '', + 'radius_nas_ip': '', + 'radius_source_address': '', + 'radius_shaper_attr': '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author': '', + 'sesscrtl': 'replace', + 'snmp': False, + 'thread_cnt': get_half_cpus() +} + +def get_config(): + conf = Config() + base_path = ['service', 'pppoe-server'] + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + pppoe = deepcopy(default_config_data) + + # general options + if conf.exists(['access-concentrator']): + pppoe['concentrator'] = conf.return_value(['access-concentrator']) + + if conf.exists(['service-name']): + pppoe['svc_name'] = conf.return_values(['service-name']) + + if conf.exists(['interface']): + for interface in conf.list_nodes(['interface']): + conf.set_level(base_path + ['interface', interface]) + tmp = { + 'name': interface, + 'vlans': [] + } + + if conf.exists(['vlan-id']): + tmp['vlans'] += conf.return_values(['vlan-id']) + + if conf.exists(['vlan-range']): + tmp['vlans'] += conf.return_values(['vlan-range']) + + pppoe['interfaces'].append(tmp) + + conf.set_level(base_path) + + if conf.exists(['local-ip']): + pppoe['ppp_gw'] = conf.return_value(['local-ip']) + + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + pppoe['dnsv4'].append(name_server) + else: + pppoe['dnsv6'].append(name_server) + + if conf.exists(['wins-server']): + pppoe['wins'] = conf.return_values(['wins-server']) + + + if conf.exists(['client-ip-pool']): + if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): + start = conf.return_value(['client-ip-pool', 'start']) + stop = conf.return_value(['client-ip-pool', 'stop']) + pppoe['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + + if conf.exists(['client-ip-pool', 'subnet']): + pppoe['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) + + + if conf.exists(['client-ipv6-pool', 'prefix']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) + + pppoe['client_ipv6_pool'].append(tmp) + + + if conf.exists(['client-ipv6-pool', 'delegate']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) + + pppoe['client_ipv6_delegate_prefix'].append(tmp) + + + if conf.exists(['limits']): + if conf.exists(['limits', 'burst']): + pppoe['limits_burst'] = conf.return_value(['limits', 'burst']) + + if conf.exists(['limits', 'connection-limit']): + pppoe['limits_connections'] = conf.return_value(['limits', 'connection-limit']) + + if conf.exists(['limits', 'timeout']): + pppoe['limits_timeout'] = conf.return_value(['limits', 'timeout']) + + + if conf.exists(['snmp']): + pppoe['snmp'] = True + + if conf.exists(['snmp', 'master-agent']): + pppoe['snmp'] = 'enable-ma' + + # authentication mode local + if conf.exists(['authentication', 'mode']): + pppoe['auth_mode'] = conf.return_value(['authentication', 'mode']) + + if conf.exists(['authentication', 'local-users']): + for username in conf.list_nodes(['authentication', 'local-users', 'username']): + user = { + 'name' : username, + 'password' : '', + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) + + if conf.exists(['password']): + user['password'] = conf.return_value(['password']) + + if conf.exists(['disable']): + user['state'] = 'disable' + + if conf.exists(['static-ip']): + user['ip'] = conf.return_value(['static-ip']) + + if conf.exists(['rate-limit', 'download']): + user['download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + user['upload'] = conf.return_value(['rate-limit', 'upload']) + + pppoe['local_users'].append(user) + + conf.set_level(base_path) + + if conf.exists(['authentication', 'protocols']): + auth_mods = { + 'mschap-v2': 'auth_mschap_v2', + 'mschap': 'auth_mschap_v1', + 'chap': 'auth_chap_md5', + 'pap': 'auth_pap' + } + + pppoe['auth_proto'] = [] + for proto in conf.return_values(['authentication', 'protocols']): + pppoe['auth_proto'].append(auth_mods[proto]) + + # + # authentication mode radius servers and settings + if conf.exists(['authentication', 'mode', 'radius']): + + for server in conf.list_nodes(['authentication', 'radius', 'server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812', + 'acct_port' : '1813' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail_time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + pppoe['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + pppoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + pppoe['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + pppoe['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + pppoe['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + pppoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + pppoe['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dynamic-author']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'server']): + dae['server'] = conf.return_value(['dynamic-author', 'server']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + pppoe['radius_dynamic_author'] = dae + + # RADIUS based rate-limiter + if conf.exists(['rate-limit', 'enable']): + pppoe['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + pppoe['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + pppoe['radius_shaper_vendor'] = conf.return_value(c_vendor) + + # re-set config level + conf.set_level(base_path) + + if conf.exists(['mtu']): + pppoe['mtu'] = conf.return_value(['mtu']) + + if conf.exists(['session-control']): + pppoe['sesscrtl'] = conf.return_value(['session-control']) + + # ppp_options + if conf.exists(['ppp-options']): + conf.set_level(base_path + ['ppp-options']) + + if conf.exists(['ccp']): + pppoe['ppp_ccp'] = True + + if conf.exists(['ipv4']): + pppoe['ppp_ipv4'] = conf.return_value(['ipv4']) + + if conf.exists(['ipv6']): + pppoe['ppp_ipv6'] = conf.return_value(['ipv6']) + + if conf.exists(['ipv6-accept-peer-intf-id']): + pppoe['ppp_ipv6_peer_intf_id'] = True + + if conf.exists(['ipv6-intf-id']): + pppoe['ppp_ipv6_intf_id'] = conf.return_value(['ipv6-intf-id']) + + if conf.exists(['ipv6-peer-intf-id']): + pppoe['ppp_ipv6_peer_intf_id'] = conf.return_value(['ipv6-peer-intf-id']) + + if conf.exists(['lcp-echo-failure']): + pppoe['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure']) + + if conf.exists(['lcp-echo-failure']): + pppoe['ppp_echo_interval'] = conf.return_value(['lcp-echo-failure']) + + if conf.exists(['lcp-echo-timeout']): + pppoe['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout']) + + if conf.exists(['min-mtu']): + pppoe['ppp_min_mtu'] = conf.return_value(['min-mtu']) + + if conf.exists(['mppe']): + pppoe['ppp_mppe'] = conf.return_value(['mppe']) + + if conf.exists(['mru']): + pppoe['ppp_mru'] = conf.return_value(['mru']) + + if conf.exists(['pado-delay']): + pppoe['pado_delay'] = '0' + a = {} + for id in conf.list_nodes(['pado-delay']): + if not conf.return_value(['pado-delay', id, 'sessions']): + a[id] = 0 + else: + a[id] = conf.return_value(['pado-delay', id, 'sessions']) + + for k in sorted(a.keys()): + if k != sorted(a.keys())[-1]: + pppoe['pado_delay'] += ",{0}:{1}".format(k, a[k]) + else: + pppoe['pado_delay'] += ",{0}:{1}".format('-1', a[k]) + + return pppoe + + +def verify(pppoe): + if not pppoe: + return None + + # vertify auth settings + if pppoe['auth_mode'] == 'local': + if not pppoe['local_users']: + raise ConfigError('PPPoE local auth mode requires local users to be configured!') + + for user in pppoe['local_users']: + username = user['name'] + if not user['password']: + raise ConfigError(f'Password required for local user "{username}"') + + # if up/download is set, check that both have a value + if user['upload'] and not user['download']: + raise ConfigError(f'Download speed value required for local user "{username}"') + + if user['download'] and not user['upload']: + raise ConfigError(f'Upload speed value required for local user "{username}"') + + elif pppoe['auth_mode'] == 'radius': + if len(pppoe['radius_server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + for radius in pppoe['radius_server']: + if not radius['key']: + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + + if len(pppoe['wins']) > 2: + raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + + if len(pppoe['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(pppoe['dnsv6']) > 3: + raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + + if not pppoe['interfaces']: + raise ConfigError('At least one listen interface must be defined!') + + # local ippool and gateway settings config checks + if pppoe['client_ip_subnets'] or pppoe['client_ip_pool']: + if not pppoe['ppp_gw']: + raise ConfigError('PPPoE server requires local IP to be configured') + + if pppoe['ppp_gw'] and not pppoe['client_ip_subnets'] and not pppoe['client_ip_pool']: + print("Warning: No PPPoE client pool defined") + + return None + + +def generate(pppoe): + if not pppoe: + return None + + render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True) + + if pppoe['local_users']: + render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pppoe, trim_blocks=True) + os.chmod(pppoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + else: + if os.path.exists(pppoe_chap_secrets): + os.unlink(pppoe_chap_secrets) + + return None + + +def apply(pppoe): + if not pppoe: + call('systemctl stop accel-ppp@pppoe.service') + for file in [pppoe_conf, pppoe_chap_secrets]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@pppoe.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py new file mode 100755 index 000000000..4e1c432ab --- /dev/null +++ b/src/conf_mode/service_router-advert.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = r'/run/radvd/radvd.conf' + +def get_config(): + conf = Config() + base = ['service', 'router-advert'] + rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_interface_values = defaults(base + ['interface']) + # we deal with prefix defaults later on + if 'prefix' in default_interface_values: + del default_interface_values['prefix'] + + default_prefix_values = defaults(base + ['interface', 'prefix']) + + if 'interface' in rtradv: + for interface in rtradv['interface']: + rtradv['interface'][interface] = dict_merge( + default_interface_values, rtradv['interface'][interface]) + + if 'prefix' in rtradv['interface'][interface]: + for prefix in rtradv['interface'][interface]['prefix']: + rtradv['interface'][interface]['prefix'][prefix] = dict_merge( + default_prefix_values, rtradv['interface'][interface]['prefix'][prefix]) + + if 'name_server' in rtradv['interface'][interface]: + # always use a list when dealing with nameservers - eases the template generation + if isinstance(rtradv['interface'][interface]['name_server'], str): + rtradv['interface'][interface]['name_server'] = [ + rtradv['interface'][interface]['name_server']] + + return rtradv + +def verify(rtradv): + if not rtradv: + return None + + if 'interface' not in rtradv: + return None + + for interface in rtradv['interface']: + interface = rtradv['interface'][interface] + if 'prefix' in interface: + for prefix in interface['prefix']: + prefix = interface['prefix'][prefix] + valid_lifetime = prefix['valid_lifetime'] + if valid_lifetime == 'infinity': + valid_lifetime = 4294967295 + + preferred_lifetime = prefix['preferred_lifetime'] + if preferred_lifetime == 'infinity': + preferred_lifetime = 4294967295 + + if not (int(valid_lifetime) > int(preferred_lifetime)): + raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') + + return None + +def generate(rtradv): + if not rtradv: + return None + + render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True, permission=0o644) + return None + +def apply(rtradv): + if not rtradv: + # bail out early - looks like removal from running config + call('systemctl stop radvd.service') + if os.path.exists(config_file): + os.unlink(config_file) + + return None + + call('systemctl restart radvd.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py new file mode 100755 index 000000000..e9806ef47 --- /dev/null +++ b/src/conf_mode/snmp.py @@ -0,0 +1,581 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configverify import verify_vrf +from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random +from vyos.template import render +from vyos.util import call +from vyos.validate import is_ipv4, is_addr_assigned +from vyos.version import get_version_data +from vyos import ConfigError, airbag +airbag.enable() + +config_file_client = r'/etc/snmp/snmp.conf' +config_file_daemon = r'/etc/snmp/snmpd.conf' +config_file_access = r'/usr/share/snmp/snmpd.conf' +config_file_user = r'/var/lib/snmp/snmpd.conf' +default_script_dir = r'/config/user-data/' +systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf' + +# SNMP OIDs used to mark auth/priv type +OIDs = { + 'md5' : '.1.3.6.1.6.3.10.1.1.2', + 'sha' : '.1.3.6.1.6.3.10.1.1.3', + 'aes' : '.1.3.6.1.6.3.10.1.2.4', + 'des' : '.1.3.6.1.6.3.10.1.2.2', + 'none': '.1.3.6.1.6.3.10.1.2.1' +} + +default_config_data = { + 'listen_on': [], + 'listen_address': [], + 'ipv6_enabled': 'True', + 'communities': [], + 'smux_peers': [], + 'location' : '', + 'description' : '', + 'contact' : '', + 'trap_source': '', + 'trap_targets': [], + 'vyos_user': '', + 'vyos_user_pass': '', + 'version': '', + 'v3_enabled': 'False', + 'v3_engineid': '', + 'v3_groups': [], + 'v3_traps': [], + 'v3_users': [], + 'v3_views': [], + 'script_ext': [] +} + +def rmfile(file): + if os.path.isfile(file): + os.unlink(file) + +def get_config(): + snmp = default_config_data + conf = Config() + if not conf.exists('service snmp'): + return None + else: + if conf.exists('system ipv6 disable'): + snmp['ipv6_enabled'] = False + + conf.set_level('service snmp') + + version_data = get_version_data() + snmp['version'] = version_data['version'] + + # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx' + snmp['vyos_user'] = 'vyos' + random(8) + snmp['vyos_user_pass'] = random(16) + + if conf.exists('community'): + for name in conf.list_nodes('community'): + community = { + 'name': name, + 'authorization': 'ro', + 'network_v4': [], + 'network_v6': [], + 'has_source' : False + } + + if conf.exists('community {0} authorization'.format(name)): + community['authorization'] = conf.return_value('community {0} authorization'.format(name)) + + # Subnet of SNMP client(s) allowed to contact system + if conf.exists('community {0} network'.format(name)): + for addr in conf.return_values('community {0} network'.format(name)): + if is_ipv4(addr): + community['network_v4'].append(addr) + else: + community['network_v6'].append(addr) + + # IP address of SNMP client allowed to contact system + if conf.exists('community {0} client'.format(name)): + for addr in conf.return_values('community {0} client'.format(name)): + if is_ipv4(addr): + community['network_v4'].append(addr) + else: + community['network_v6'].append(addr) + + if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0): + community['has_source'] = True + + snmp['communities'].append(community) + + if conf.exists('contact'): + snmp['contact'] = conf.return_value('contact') + + if conf.exists('description'): + snmp['description'] = conf.return_value('description') + + if conf.exists('listen-address'): + for addr in conf.list_nodes('listen-address'): + port = '161' + if conf.exists('listen-address {0} port'.format(addr)): + port = conf.return_value('listen-address {0} port'.format(addr)) + + snmp['listen_address'].append((addr, port)) + + # Always listen on localhost if an explicit address has been configured + # This is a safety measure to not end up with invalid listen addresses + # that are not configured on this system. See https://phabricator.vyos.net/T850 + if not '127.0.0.1' in conf.list_nodes('listen-address'): + snmp['listen_address'].append(('127.0.0.1', '161')) + + if not '::1' in conf.list_nodes('listen-address'): + snmp['listen_address'].append(('::1', '161')) + + if conf.exists('location'): + snmp['location'] = conf.return_value('location') + + if conf.exists('smux-peer'): + snmp['smux_peers'] = conf.return_values('smux-peer') + + if conf.exists('trap-source'): + snmp['trap_source'] = conf.return_value('trap-source') + + if conf.exists('trap-target'): + for target in conf.list_nodes('trap-target'): + trap_tgt = { + 'target': target, + 'community': '', + 'port': '' + } + + if conf.exists('trap-target {0} community'.format(target)): + trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target)) + + if conf.exists('trap-target {0} port'.format(target)): + trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target)) + + snmp['trap_targets'].append(trap_tgt) + + if conf.exists('script-extensions'): + for extname in conf.list_nodes('script-extensions extension-name'): + conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname)) + # if script has not absolute path, use pre configured path + if "/" not in conf_script: + conf_script = default_script_dir + conf_script + + extension = { + 'name': extname, + 'script' : conf_script + } + + snmp['script_ext'].append(extension) + + if conf.exists('vrf'): + # Append key to dict but don't place it in the default dictionary. + # This is required to make the override.conf.tmpl work until we + # migrate to get_config_dict(). + snmp['vrf'] = conf.return_value('vrf') + + + ######################################################################### + # ____ _ _ __ __ ____ _____ # + # / ___|| \ | | \/ | _ \ __ _|___ / # + # \___ \| \| | |\/| | |_) | \ \ / / |_ \ # + # ___) | |\ | | | | __/ \ V / ___) | # + # |____/|_| \_|_| |_|_| \_/ |____/ # + # # + # now take care about the fancy SNMP v3 stuff, or bail out eraly # + ######################################################################### + if not conf.exists('v3'): + return snmp + else: + snmp['v3_enabled'] = True + + # 'set service snmp v3 engineid' + if conf.exists('v3 engineid'): + snmp['v3_engineid'] = conf.return_value('v3 engineid') + + # 'set service snmp v3 group' + if conf.exists('v3 group'): + for group in conf.list_nodes('v3 group'): + v3_group = { + 'name': group, + 'mode': 'ro', + 'seclevel': 'auth', + 'view': '' + } + + if conf.exists('v3 group {0} mode'.format(group)): + v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group)) + + if conf.exists('v3 group {0} seclevel'.format(group)): + v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group)) + + if conf.exists('v3 group {0} view'.format(group)): + v3_group['view'] = conf.return_value('v3 group {0} view'.format(group)) + + snmp['v3_groups'].append(v3_group) + + # 'set service snmp v3 trap-target' + if conf.exists('v3 trap-target'): + for trap in conf.list_nodes('v3 trap-target'): + trap_cfg = { + 'ipAddr': trap, + 'secName': '', + 'authProtocol': 'md5', + 'authPassword': '', + 'authMasterKey': '', + 'privProtocol': 'des', + 'privPassword': '', + 'privMasterKey': '', + 'ipProto': 'udp', + 'ipPort': '162', + 'type': '', + 'secLevel': 'noAuthNoPriv' + } + + if conf.exists('v3 trap-target {0} user'.format(trap)): + # Set the securityName used for authenticated SNMPv3 messages. + trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap)) + + if conf.exists('v3 trap-target {0} auth type'.format(trap)): + # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages + # cmdline option '-a' + trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap)) + + if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)): + # Set the authentication pass phrase used for authenticated SNMPv3 messages. + # cmdline option '-A' + trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap)) + + if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)): + # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys. + # cmdline option '-3m' + trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy type'.format(trap)): + # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages. + # cmdline option '-x' + trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)): + # Set the privacy pass phrase used for encrypted SNMPv3 messages. + # cmdline option '-X' + trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap)) + + if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)): + # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys. + # cmdline option '-3M' + trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap)) + + if conf.exists('v3 trap-target {0} protocol'.format(trap)): + trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap)) + + if conf.exists('v3 trap-target {0} port'.format(trap)): + trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap)) + + if conf.exists('v3 trap-target {0} type'.format(trap)): + trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap)) + + # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv). + # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv. + if trap_cfg['authPassword'] or trap_cfg['authMasterKey']: + if trap_cfg['privProtocol'] or trap_cfg['privPassword']: + trap_cfg['secLevel'] = 'authPriv' + else: + trap_cfg['secLevel'] = 'authNoPriv' + + snmp['v3_traps'].append(trap_cfg) + + # 'set service snmp v3 user' + if conf.exists('v3 user'): + for user in conf.list_nodes('v3 user'): + user_cfg = { + 'name': user, + 'authMasterKey': '', + 'authPassword': '', + 'authProtocol': 'md5', + 'authOID': 'none', + 'group': '', + 'mode': 'ro', + 'privMasterKey': '', + 'privPassword': '', + 'privOID': '', + 'privProtocol': 'des' + } + + # v3 user {0} auth + if conf.exists('v3 user {0} auth encrypted-password'.format(user)): + user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user)) + + if conf.exists('v3 user {0} auth plaintext-password'.format(user)): + user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user)) + + # load default value + type = user_cfg['authProtocol'] + if conf.exists('v3 user {0} auth type'.format(user)): + type = conf.return_value('v3 user {0} auth type'.format(user)) + + # (re-)update with either default value or value from CLI + user_cfg['authProtocol'] = type + user_cfg['authOID'] = OIDs[type] + + # v3 user {0} group + if conf.exists('v3 user {0} group'.format(user)): + user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user)) + + # v3 user {0} mode + if conf.exists('v3 user {0} mode'.format(user)): + user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user)) + + # v3 user {0} privacy + if conf.exists('v3 user {0} privacy encrypted-password'.format(user)): + user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user)) + + if conf.exists('v3 user {0} privacy plaintext-password'.format(user)): + user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user)) + + # load default value + type = user_cfg['privProtocol'] + if conf.exists('v3 user {0} privacy type'.format(user)): + type = conf.return_value('v3 user {0} privacy type'.format(user)) + + # (re-)update with either default value or value from CLI + user_cfg['privProtocol'] = type + user_cfg['privOID'] = OIDs[type] + + snmp['v3_users'].append(user_cfg) + + # 'set service snmp v3 view' + if conf.exists('v3 view'): + for view in conf.list_nodes('v3 view'): + view_cfg = { + 'name': view, + 'oids': [] + } + + if conf.exists('v3 view {0} oid'.format(view)): + for oid in conf.list_nodes('v3 view {0} oid'.format(view)): + oid_cfg = { + 'oid': oid + } + view_cfg['oids'].append(oid_cfg) + snmp['v3_views'].append(view_cfg) + + return snmp + +def verify(snmp): + if snmp is None: + # we can not delete SNMP when LLDP is configured with SNMP + conf = Config() + if conf.exists('service lldp snmp enable'): + raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!') + + return None + + ### check if the configured script actually exist + if snmp['script_ext']: + for ext in snmp['script_ext']: + if not os.path.isfile(ext['script']): + print ("WARNING: script: {} doesn't exist".format(ext['script'])) + else: + chmod_755(ext['script']) + + for listen in snmp['listen_address']: + addr = listen[0] + port = listen[1] + + if is_ipv4(addr): + # example: udp:127.0.0.1:161 + listen = 'udp:' + addr + ':' + port + elif snmp['ipv6_enabled']: + # example: udp6:[::1]:161 + listen = 'udp6:' + '[' + addr + ']' + ':' + port + + # We only wan't to configure addresses that exist on the system. + # Hint the user if they don't exist + if is_addr_assigned(addr): + snmp['listen_on'].append(listen) + else: + print('WARNING: SNMP listen address {0} not configured!'.format(addr)) + + verify_vrf(snmp) + + # bail out early if SNMP v3 is not configured + if not snmp['v3_enabled']: + return None + + if 'v3_groups' in snmp.keys(): + for group in snmp['v3_groups']: + # + # A view must exist prior to mapping it into a group + # + if 'view' in group.keys(): + error = True + if 'v3_views' in snmp.keys(): + for view in snmp['v3_views']: + if view['name'] == group['view']: + error = False + if error: + raise ConfigError('You must create view "{0}" first'.format(group['view'])) + else: + raise ConfigError('"view" must be specified') + + if not 'mode' in group.keys(): + raise ConfigError('"mode" must be specified') + + if not 'seclevel' in group.keys(): + raise ConfigError('"seclevel" must be specified') + + if 'v3_traps' in snmp.keys(): + for trap in snmp['v3_traps']: + if trap['authPassword'] and trap['authMasterKey']: + raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth') + + if trap['authPassword'] == '' and trap['authMasterKey'] == '': + raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth') + + if trap['privPassword'] and trap['privMasterKey']: + raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy') + + if trap['privPassword'] == '' and trap['privMasterKey'] == '': + raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy') + + if not 'type' in trap.keys(): + raise ConfigError('v3 trap: "type" must be specified') + + if not 'authPassword' and 'authMasterKey' in trap.keys(): + raise ConfigError('v3 trap: "auth" must be specified') + + if not 'authProtocol' in trap.keys(): + raise ConfigError('v3 trap: "protocol" must be specified') + + if not 'privPassword' and 'privMasterKey' in trap.keys(): + raise ConfigError('v3 trap: "user" must be specified') + + if 'v3_users' in snmp.keys(): + for user in snmp['v3_users']: + # + # Group must exist prior to mapping it into a group + # seclevel will be extracted from group + # + if user['group']: + error = True + if 'v3_groups' in snmp.keys(): + for group in snmp['v3_groups']: + if group['name'] == user['group']: + seclevel = group['seclevel'] + error = False + + if error: + raise ConfigError('You must create group "{0}" first'.format(user['group'])) + + # Depending on the configured security level the user has to provide additional info + if (not user['authPassword'] and not user['authMasterKey']): + raise ConfigError('Must specify encrypted-password or plaintext-key for user auth') + + if user['privPassword'] == '' and user['privMasterKey'] == '': + raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy') + + if user['mode'] == '': + raise ConfigError('Must specify user mode ro/rw') + + if 'v3_views' in snmp.keys(): + for view in snmp['v3_views']: + if not view['oids']: + raise ConfigError('Must configure an oid') + + return None + +def generate(snmp): + # + # As we are manipulating the snmpd user database we have to stop it first! + # This is even save if service is going to be removed + call('systemctl stop snmpd.service') + config_files = [config_file_client, config_file_daemon, config_file_access, + config_file_user, systemd_override] + for file in config_files: + rmfile(file) + + if not snmp: + return None + + if 'v3_users' in snmp.keys(): + # net-snmp is now regenerating the configuration file in the background + # thus we need to re-open and re-read the file as the content changed. + # After that we can no read the encrypted password from the config and + # replace the CLI plaintext password with its encrypted version. + os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos" + + for user in snmp['v3_users']: + if user['authProtocol'] == 'sha': + hash = plaintext_to_sha1 + else: + hash = plaintext_to_md5 + + if user['authPassword']: + user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid']) + user['authPassword'] = '' + + call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user)) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user)) + + if user['privPassword']: + user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid']) + user['privPassword'] = '' + + call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user)) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user)) + + # Write client config file + render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp) + # Write server config file + render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp) + # Write access rights config file + render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp) + # Write access rights config file + render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp) + # Write daemon configuration file + render(systemd_override, 'snmp/override.conf.tmpl', snmp) + + return None + +def apply(snmp): + # Always reload systemd manager configuration + call('systemctl daemon-reload') + + if not snmp: + return None + + # start SNMP daemon + call('systemctl restart snmpd.service') + + # Enable AgentX in FRR + call('vtysh -c "configure terminal" -c "agentx" >/dev/null') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py new file mode 100755 index 000000000..7b262565a --- /dev/null +++ b/src/conf_mode/ssh.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from netifaces import interfaces +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos import ConfigError +from vyos.util import call +from vyos.template import render +from vyos.xml import defaults +from vyos import airbag +airbag.enable() + +config_file = r'/run/ssh/sshd_config' +systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' + +def get_config(): + conf = Config() + base = ['service', 'ssh'] + if not conf.exists(base): + return None + + ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + ssh = dict_merge(default_values, ssh) + # pass config file path - used in override template + ssh['config_file'] = config_file + + return ssh + +def verify(ssh): + if not ssh: + return None + + if 'vrf' in ssh.keys() and ssh['vrf'] not in interfaces(): + raise ConfigError('VRF "{vrf}" does not exist'.format(**ssh)) + + return None + +def generate(ssh): + if not ssh: + if os.path.isfile(config_file): + os.unlink(config_file) + if os.path.isfile(systemd_override): + os.unlink(systemd_override) + + return None + + render(config_file, 'ssh/sshd_config.tmpl', ssh, trim_blocks=True) + render(systemd_override, 'ssh/override.conf.tmpl', ssh, trim_blocks=True) + + return None + +def apply(ssh): + if not ssh: + # SSH access is removed in the commit + call('systemctl stop ssh.service') + + # Reload systemd manager configuration + call('systemctl daemon-reload') + + if ssh: + call('systemctl restart ssh.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py new file mode 100755 index 000000000..85f1e3771 --- /dev/null +++ b/src/conf_mode/system-ip.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call + +from vyos import airbag +airbag.enable() + +default_config_data = { + 'arp_table': 8192, + 'ipv4_forward': '1', + 'mp_unreach_nexthop': '0', + 'mp_layer4_hashing': '0' +} + +def sysctl(name, value): + call('sysctl -wq {}={}'.format(name, value)) + +def get_config(): + ip_opt = deepcopy(default_config_data) + conf = Config() + conf.set_level('system ip') + if conf.exists(''): + if conf.exists('arp table-size'): + ip_opt['arp_table'] = int(conf.return_value('arp table-size')) + + if conf.exists('disable-forwarding'): + ip_opt['ipv4_forward'] = '0' + + if conf.exists('multipath ignore-unreachable-nexthops'): + ip_opt['mp_unreach_nexthop'] = '1' + + if conf.exists('multipath layer4-hashing'): + ip_opt['mp_layer4_hashing'] = '1' + + return ip_opt + +def verify(ip_opt): + pass + +def generate(ip_opt): + pass + +def apply(ip_opt): + # apply ARP threshold values + sysctl('net.ipv4.neigh.default.gc_thresh3', ip_opt['arp_table']) + sysctl('net.ipv4.neigh.default.gc_thresh2', ip_opt['arp_table'] // 2) + sysctl('net.ipv4.neigh.default.gc_thresh1', ip_opt['arp_table'] // 8) + + # enable/disable IPv4 forwarding + with open('/proc/sys/net/ipv4/conf/all/forwarding', 'w') as f: + f.write(ip_opt['ipv4_forward']) + + # configure multipath + sysctl('net.ipv4.fib_multipath_use_neigh', ip_opt['mp_unreach_nexthop']) + sysctl('net.ipv4.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing']) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py new file mode 100755 index 000000000..3417c609d --- /dev/null +++ b/src/conf_mode/system-ipv6.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from sys import exit +from copy import deepcopy +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call + +from vyos import airbag +airbag.enable() + +ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf' + +default_config_data = { + 'reboot_message': False, + 'ipv6_forward': '1', + 'disable_addr_assignment': False, + 'mp_layer4_hashing': '0', + 'neighbor_cache': 8192, + 'strict_dad': '1' + +} + +def sysctl(name, value): + call('sysctl -wq {}={}'.format(name, value)) + +def get_config(): + ip_opt = deepcopy(default_config_data) + conf = Config() + conf.set_level('system ipv6') + if conf.exists(''): + ip_opt['disable_addr_assignment'] = conf.exists('disable') + if conf.exists_effective('disable') != conf.exists('disable'): + ip_opt['reboot_message'] = True + + if conf.exists('disable-forwarding'): + ip_opt['ipv6_forward'] = '0' + + if conf.exists('multipath layer4-hashing'): + ip_opt['mp_layer4_hashing'] = '1' + + if conf.exists('neighbor table-size'): + ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size')) + + if conf.exists('strict-dad'): + ip_opt['strict_dad'] = 2 + + return ip_opt + +def verify(ip_opt): + pass + +def generate(ip_opt): + pass + +def apply(ip_opt): + # disable IPv6 address assignment + if ip_opt['disable_addr_assignment']: + with open(ipv6_disable_file, 'w') as f: + f.write('options ipv6 disable_ipv6=1') + else: + if os.path.exists(ipv6_disable_file): + os.unlink(ipv6_disable_file) + + if ip_opt['reboot_message']: + print('Changing IPv6 disable parameter will only take affect\n' \ + 'when the system is rebooted.') + + # configure multipath + sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing']) + + # apply neighbor table threshold values + sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache']) + sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2) + sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8) + + # enable/disable IPv6 forwarding + with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f: + f.write(ip_opt['ipv6_forward']) + + # configure IPv6 strict-dad + for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'): + for name in files: + if name == "accept_dad": + with open(os.path.join(root, name), 'w') as f: + f.write(str(ip_opt['strict_dad'])) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py new file mode 100755 index 000000000..5c0adc921 --- /dev/null +++ b/src/conf_mode/system-login-banner.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import exit +from vyos.config import Config +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +motd=""" +The programs included with the Debian GNU/Linux system are free software; +the exact distribution terms for each program are described in the +individual files in /usr/share/doc/*/copyright. + +Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent +permitted by applicable law. + +""" + +PRELOGIN_FILE = r'/etc/issue' +PRELOGIN_NET_FILE = r'/etc/issue.net' +POSTLOGIN_FILE = r'/etc/motd' + +default_config_data = { + 'issue': 'Welcome to VyOS - \n \l\n', + 'issue_net': 'Welcome to VyOS\n', + 'motd': motd +} + +def get_config(): + banner = default_config_data + conf = Config() + base_level = ['system', 'login', 'banner'] + + if not conf.exists(base_level): + return banner + else: + conf.set_level(base_level) + + # Post-Login banner + if conf.exists(['post-login']): + tmp = conf.return_value(['post-login']) + # post-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + # always add newline character + tmp += '\n' + else: + tmp = '' + + banner['motd'] = tmp + + # Pre-Login banner + if conf.exists(['pre-login']): + tmp = conf.return_value(['pre-login']) + # pre-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + # always add newline character + tmp += '\n' + else: + tmp = '' + + banner['issue'] = banner['issue_net'] = tmp + + return banner + +def verify(banner): + pass + +def generate(banner): + pass + +def apply(banner): + with open(PRELOGIN_FILE, 'w') as f: + f.write(banner['issue']) + + with open(PRELOGIN_NET_FILE, 'w') as f: + f.write(banner['issue_net']) + + with open(POSTLOGIN_FILE, 'w') as f: + f.write(banner['motd']) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py new file mode 100755 index 000000000..b1dd583b5 --- /dev/null +++ b/src/conf_mode/system-login.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from crypt import crypt, METHOD_SHA512 +from netifaces import interfaces +from psutil import users +from pwd import getpwall, getpwnam +from spwd import getspnam +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import cmd, call, DEVNULL, chmod_600, chmod_755 +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +radius_config_file = "/etc/pam_radius_auth.conf" + +default_config_data = { + 'deleted': False, + 'add_users': [], + 'del_users': [], + 'radius_server': [], + 'radius_source_address': '', + 'radius_vrf': '' +} + + +def get_local_users(): + """Return list of dynamically allocated users (see Debian Policy Manual)""" + local_users = [] + for p in getpwall(): + username = p[0] + uid = getpwnam(username).pw_uid + if uid in range(1000, 29999): + if username not in ['radius_user', 'radius_priv_user']: + local_users.append(username) + + return local_users + + +def get_config(): + login = default_config_data + conf = Config() + base_level = ['system', 'login'] + + # We do not need to check if the nodes exist or not and bail out early + # ... this would interrupt the following logic on determine which users + # should be deleted and which users should stay. + # + # All fine so far! + + # Read in all local users and store to list + for username in conf.list_nodes(base_level + ['user']): + user = { + 'name': username, + 'password_plaintext': '', + 'password_encrypted': '!', + 'public_keys': [], + 'full_name': '', + 'home_dir': '/home/' + username, + } + conf.set_level(base_level + ['user', username]) + + # Plaintext password + if conf.exists(['authentication', 'plaintext-password']): + user['password_plaintext'] = conf.return_value( + ['authentication', 'plaintext-password']) + + # Encrypted password + if conf.exists(['authentication', 'encrypted-password']): + user['password_encrypted'] = conf.return_value( + ['authentication', 'encrypted-password']) + + # User real name + if conf.exists(['full-name']): + user['full_name'] = conf.return_value(['full-name']) + + # User home-directory + if conf.exists(['home-directory']): + user['home_dir'] = conf.return_value(['home-directory']) + + # Read in public keys + for id in conf.list_nodes(['authentication', 'public-keys']): + key = { + 'name': id, + 'key': '', + 'options': '', + 'type': '' + } + conf.set_level(base_level + ['user', username, 'authentication', + 'public-keys', id]) + + # Public Key portion + if conf.exists(['key']): + key['key'] = conf.return_value(['key']) + + # Options for individual public key + if conf.exists(['options']): + key['options'] = conf.return_value(['options']) + + # Type of public key + if conf.exists(['type']): + key['type'] = conf.return_value(['type']) + + # Append individual public key to list of user keys + user['public_keys'].append(key) + + login['add_users'].append(user) + + # + # RADIUS configuration + # + conf.set_level(base_level + ['radius']) + + if conf.exists(['source-address']): + login['radius_source_address'] = conf.return_value(['source-address']) + + # retrieve VRF instance + if conf.exists(['vrf']): + login['radius_vrf'] = conf.return_value(['vrf']) + + # Read in all RADIUS servers and store to list + for server in conf.list_nodes(['server']): + server_cfg = { + 'address': server, + 'disabled': False, + 'key': '', + 'port': '1812', + 'timeout': '2', + 'priority': 255 + } + conf.set_level(base_level + ['radius', 'server', server]) + + # Check if RADIUS server was temporary disabled + if conf.exists(['disable']): + server_cfg['disabled'] = True + + # RADIUS shared secret + if conf.exists(['key']): + server_cfg['key'] = conf.return_value(['key']) + + # RADIUS authentication port + if conf.exists(['port']): + server_cfg['port'] = conf.return_value(['port']) + + # RADIUS session timeout + if conf.exists(['timeout']): + server_cfg['timeout'] = conf.return_value(['timeout']) + + # Check if RADIUS server has priority + if conf.exists(['priority']): + server_cfg['priority'] = int(conf.return_value(['priority'])) + + # Append individual RADIUS server configuration to global server list + login['radius_server'].append(server_cfg) + + # users no longer existing in the running configuration need to be deleted + local_users = get_local_users() + cli_users = [tmp['name'] for tmp in login['add_users']] + # create a list of all users, cli and users + all_users = list(set(local_users+cli_users)) + + # Remove any normal users that dos not exist in the current configuration. + # This can happen if user is added but configuration was not saved and + # system is rebooted. + login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users] + + return login + + +def verify(login): + cur_user = os.environ['SUDO_USER'] + if cur_user in login['del_users']: + raise ConfigError( + 'Attempting to delete current user: {}'.format(cur_user)) + + for user in login['add_users']: + for key in user['public_keys']: + if not key['type']: + raise ConfigError( + 'SSH public key type missing for "{name}"!'.format(**key)) + + if not key['key']: + raise ConfigError( + 'SSH public key for id "{name}" missing!'.format(**key)) + + # At lease one RADIUS server must not be disabled + if len(login['radius_server']) > 0: + fail = True + for server in login['radius_server']: + if not server['disabled']: + fail = False + if fail: + raise ConfigError('At least one RADIUS server must be active.') + + vrf_name = login['radius_vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + return None + + +def generate(login): + # calculate users encrypted password + for user in login['add_users']: + if user['password_plaintext']: + user['password_encrypted'] = crypt( + user['password_plaintext'], METHOD_SHA512) + user['password_plaintext'] = '' + + # remove old plaintext password and set new encrypted password + env = os.environ.copy() + env['vyos_libexec_dir'] = '/usr/libexec/vyos' + + call("/opt/vyatta/sbin/my_set system login user '{name}' " + "authentication plaintext-password '{password_plaintext}'" + .format(**user), env=env) + + call("/opt/vyatta/sbin/my_set system login user '{name}' " + "authentication encrypted-password '{password_encrypted}'" + .format(**user), env=env) + + else: + try: + if getspnam(user['name']).sp_pwdp == user['password_encrypted']: + # If the current encrypted bassword matches the encrypted password + # from the config - do not update it. This will remove the encrypted + # value from the system logs. + # + # The encrypted password will be set only once during the first boot + # after an image upgrade. + user['password_encrypted'] = '' + except: + pass + + if len(login['radius_server']) > 0: + render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', + login, trim_blocks=True) + + uid = getpwnam('root').pw_uid + gid = getpwnam('root').pw_gid + os.chown(radius_config_file, uid, gid) + chmod_600(radius_config_file) + else: + if os.path.isfile(radius_config_file): + os.unlink(radius_config_file) + + return None + + +def apply(login): + for user in login['add_users']: + # make new user using vyatta shell and make home directory (-m), + # default group of 100 (users) + command = "useradd -m -N" + # check if user already exists: + if user['name'] in get_local_users(): + # update existing account + command = "usermod" + + # all accounts use /bin/vbash + command += " -s /bin/vbash" + # we need to use '' quotes when passing formatted data to the shell + # else it will not work as some data parts are lost in translation + if user['password_encrypted']: + command += " -p '{}'".format(user['password_encrypted']) + + if user['full_name']: + command += " -c '{}'".format(user['full_name']) + + if user['home_dir']: + command += " -d '{}'".format(user['home_dir']) + + command += " -G frrvty,vyattacfg,sudo,adm,dip,disk" + command += " {}".format(user['name']) + + try: + cmd(command) + + uid = getpwnam(user['name']).pw_uid + gid = getpwnam(user['name']).pw_gid + + # we should not rely on the value stored in user['home_dir'], as a + # crazy user will choose username root or any other system user + # which will fail. Should we deny using root at all? + home_dir = getpwnam(user['name']).pw_dir + + # install ssh keys + ssh_key_dir = home_dir + '/.ssh' + if not os.path.isdir(ssh_key_dir): + os.mkdir(ssh_key_dir) + os.chown(ssh_key_dir, uid, gid) + chmod_755(ssh_key_dir) + + ssh_key_file = ssh_key_dir + '/authorized_keys' + with open(ssh_key_file, 'w') as f: + f.write("# Automatically generated by VyOS\n") + f.write("# Do not edit, all changes will be lost\n") + + for id in user['public_keys']: + line = '' + if id['options']: + line = '{} '.format(id['options']) + + line += '{} {} {}\n'.format(id['type'], + id['key'], id['name']) + f.write(line) + + os.chown(ssh_key_file, uid, gid) + chmod_600(ssh_key_file) + + except Exception as e: + print(e) + raise ConfigError('Adding user "{name}" raised exception' + .format(**user)) + + for user in login['del_users']: + try: + # Logout user if he is logged in + if user in list(set([tmp[0] for tmp in users()])): + print('{} is logged in, forcing logout'.format(user)) + call('pkill -HUP -u {}'.format(user)) + + # Remove user account but leave home directory to be safe + call(f'userdel -r {user}', stderr=DEVNULL) + + except Exception as e: + raise ConfigError(f'Deleting user "{user}" raised exception: {e}') + + # + # RADIUS configuration + # + if len(login['radius_server']) > 0: + try: + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' + # Enable RADIUS in PAM + cmd("pam-auth-update --package --enable radius", env=env) + + # Make NSS system aware of RADIUS, too + command = "sed -i -e \'/\smapname/b\' \ + -e \'/^passwd:/s/\s\s*/&mapuid /\' \ + -e \'/^passwd:.*#/s/#.*/mapname &/\' \ + -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \ + -e \'/^group:.*#/s/#.*/ mapname &/\' \ + -e \'/^group:[^#]*$/s/: */&mapname /\' \ + /etc/nsswitch.conf" + + cmd(command) + + except Exception as e: + raise ConfigError('RADIUS configuration failed: {}'.format(e)) + + else: + try: + env = os.environ.copy() + env['DEBIAN_FRONTEND'] = 'noninteractive' + + # Disable RADIUS in PAM + cmd("pam-auth-update --package --remove radius", env=env) + + command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ + -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ + -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \ + -e \'s/[ \t]*$//\' \ + /etc/nsswitch.conf" + + cmd(command) + + except Exception as e: + raise ConfigError( + 'Removing RADIUS configuration failed.\n{}'.format(e)) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py new file mode 100755 index 000000000..0aacd19d8 --- /dev/null +++ b/src/conf_mode/system-options.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from netifaces import interfaces +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call +from vyos.validate import is_addr_assigned +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +curlrc_config = r'/etc/curlrc' +ssh_config = r'/etc/ssh/ssh_config' +systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' + +def get_config(): + conf = Config() + base = ['system', 'options'] + options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + return options + +def verify(options): + if 'http_client' in options: + config = options['http_client'] + if 'source_interface' in config: + if not config['source_interface'] in interfaces(): + raise ConfigError(f'Source interface {source_interface} does not ' + f'exist'.format(**config)) + + if {'source_address', 'source_interface'} <= set(config): + raise ConfigError('Can not define both HTTP source-interface and source-address') + + if 'source_address' in config: + if not is_addr_assigned(config['source_address']): + raise ConfigError('No interface with give address specified!') + + if 'ssh_client' in options: + config = options['ssh_client'] + if 'source_address' in config: + if not is_addr_assigned(config['source_address']): + raise ConfigError('No interface with give address specified!') + + return None + +def generate(options): + render(curlrc_config, 'system/curlrc.tmpl', options, trim_blocks=True) + render(ssh_config, 'system/ssh_config.tmpl', options, trim_blocks=True) + return None + +def apply(options): + # Beep action + if 'beep_if_fully_booted' in options.keys(): + call('systemctl enable vyos-beep.service') + else: + call('systemctl disable vyos-beep.service') + + # Ctrl-Alt-Delete action + if os.path.exists(systemd_action_file): + os.unlink(systemd_action_file) + + if 'ctrl_alt_del_action' in options: + if options['ctrl_alt_del_action'] == 'reboot': + os.symlink('/lib/systemd/system/reboot.target', systemd_action_file) + elif options['ctrl_alt_del_action'] == 'poweroff': + os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) + + if 'http_client' not in options: + if os.path.exists(curlrc_config): + os.unlink(curlrc_config) + + if 'ssh_client' not in options: + if os.path.exists(ssh_config): + os.unlink(ssh_config) + + # Reboot system on kernel panic + with open('/proc/sys/kernel/panic', 'w') as f: + if 'reboot_on_panic' in options.keys(): + f.write('60') + else: + f.write('0') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) + diff --git a/src/conf_mode/system-proxy.py b/src/conf_mode/system-proxy.py new file mode 100755 index 000000000..02536c2ab --- /dev/null +++ b/src/conf_mode/system-proxy.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re + +from vyos import ConfigError +from vyos.config import Config + +from vyos import airbag +airbag.enable() + +proxy_def = r'/etc/profile.d/vyos-system-proxy.sh' + + +def get_config(): + c = Config() + if not c.exists('system proxy'): + return None + + c.set_level('system proxy') + + cnf = { + 'url': None, + 'port': None, + 'usr': None, + 'passwd': None + } + + if c.exists('url'): + cnf['url'] = c.return_value('url') + if c.exists('port'): + cnf['port'] = c.return_value('port') + if c.exists('username'): + cnf['usr'] = c.return_value('username') + if c.exists('password'): + cnf['passwd'] = c.return_value('password') + + return cnf + + +def verify(c): + if not c: + return None + if not c['url'] or not c['port']: + raise ConfigError("proxy url and port requires a value") + elif c['usr'] and not c['passwd']: + raise ConfigError("proxy password requires a value") + elif not c['usr'] and c['passwd']: + raise ConfigError("proxy username requires a value") + + +def generate(c): + if not c: + return None + if not c['usr']: + return str("export http_proxy={url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy" + .format(url=c['url'], port=c['port'])) + else: + return str("export http_proxy=http://{usr}:{passwd}@{url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy" + .format(url=re.sub('http://', '', c['url']), port=c['port'], usr=c['usr'], passwd=c['passwd'])) + + +def apply(ln): + if not ln and os.path.exists(proxy_def): + os.remove(proxy_def) + else: + open(proxy_def, 'w').write( + "# generated by system-proxy.py\n{}\n".format(ln)) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + ln = generate(c) + apply(ln) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py new file mode 100755 index 000000000..cfc1ca55f --- /dev/null +++ b/src/conf_mode/system-syslog.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from sys import exit + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import run +from vyos.template import render + +from vyos import airbag +airbag.enable() + +def get_config(): + c = Config() + if not c.exists('system syslog'): + return None + c.set_level('system syslog') + + config_data = { + 'files': {}, + 'console': {}, + 'hosts': {}, + 'user': {} + } + + # + # /etc/rsyslog.d/vyos-rsyslog.conf + # 'set system syslog global' + # + config_data['files'].update( + { + 'global': { + 'log-file': '/var/log/messages', + 'max-size': 262144, + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', + 'selectors': '*.notice;local7.debug', + 'max-files': '5', + 'preserver_fqdn': False + } + } + ) + + if c.exists('global marker'): + config_data['files']['global']['marker'] = True + if c.exists('global marker interval'): + config_data['files']['global'][ + 'marker-interval'] = c.return_value('global marker interval') + if c.exists('global facility'): + config_data['files']['global'][ + 'selectors'] = generate_selectors(c, 'global facility') + if c.exists('global archive size'): + config_data['files']['global']['max-size'] = int( + c.return_value('global archive size')) * 1024 + if c.exists('global archive file'): + config_data['files']['global'][ + 'max-files'] = c.return_value('global archive file') + if c.exists('global preserve-fqdn'): + config_data['files']['global']['preserver_fqdn'] = True + + # + # set system syslog file + # + + if c.exists('file'): + filenames = c.list_nodes('file') + for filename in filenames: + config_data['files'].update( + { + filename: { + 'log-file': '/var/log/user/' + filename, + 'max-files': '5', + 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename, + 'selectors': '*.err', + 'max-size': 262144 + } + } + ) + + if c.exists('file ' + filename + ' facility'): + config_data['files'][filename]['selectors'] = generate_selectors( + c, 'file ' + filename + ' facility') + if c.exists('file ' + filename + ' archive size'): + config_data['files'][filename]['max-size'] = int( + c.return_value('file ' + filename + ' archive size')) * 1024 + if c.exists('file ' + filename + ' archive files'): + config_data['files'][filename]['max-files'] = c.return_value( + 'file ' + filename + ' archive files') + + # set system syslog console + if c.exists('console'): + config_data['console'] = { + '/dev/console': { + 'selectors': '*.err' + } + } + + for f in c.list_nodes('console facility'): + if c.exists('console facility ' + f + ' level'): + config_data['console'] = { + '/dev/console': { + 'selectors': generate_selectors(c, 'console facility') + } + } + + # set system syslog host + if c.exists('host'): + rhosts = c.list_nodes('host') + proto = 'udp' + for rhost in rhosts: + for fac in c.list_nodes('host ' + rhost + ' facility'): + if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): + proto = c.return_value( + 'host ' + rhost + ' facility ' + fac + ' protocol') + else: + proto = 'udp' + + config_data['hosts'].update( + { + rhost: { + 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'), + 'proto': proto + } + } + ) + if c.exists('host ' + rhost + ' port'): + config_data['hosts'][rhost][ + 'port'] = c.return_value(['host', rhost, 'port']) + + # set system syslog user + if c.exists('user'): + usrs = c.list_nodes('user') + for usr in usrs: + config_data['user'].update( + { + usr: { + 'selectors': generate_selectors(c, 'user ' + usr + ' facility') + } + } + ) + + return config_data + + +def generate_selectors(c, config_node): +# protocols and security are being mapped here +# for backward compatibility with old configs +# security and protocol mappings can be removed later + nodes = c.list_nodes(config_node) + selectors = "" + for node in nodes: + lvl = c.return_value(config_node + ' ' + node + ' level') + if lvl == None: + lvl = "err" + if lvl == 'all': + lvl = '*' + if node == 'all' and node != nodes[-1]: + selectors += "*." + lvl + ";" + elif node == 'all': + selectors += "*." + lvl + elif node != nodes[-1]: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + ";" + else: + if node == 'protocols': + node = 'local7' + if node == 'security': + node = 'auth' + selectors += node + "." + lvl + return selectors + + +def generate(c): + if c == None: + return None + + conf = '/etc/rsyslog.d/vyos-rsyslog.conf' + render(conf, 'syslog/rsyslog.conf.tmpl', c, trim_blocks=True) + + # eventually write for each file its own logrotate file, since size is + # defined it shouldn't matter + conf = '/etc/logrotate.d/vyos-rsyslog' + render(conf, 'syslog/logrotate.tmpl', c, trim_blocks=True) + + +def verify(c): + if c == None: + return None + + # may be obsolete + # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) + # it interferes with the global logging, to make sure we are using a single base, template is enforced here + # + if not os.path.islink('/etc/rsyslog.conf'): + os.remove('/etc/rsyslog.conf') + os.symlink( + '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') + + # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there + # is a chance that someone still needs it, so I don't automatically remove + # them + # + + if c == None: + return None + + fac = [ + '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security', + 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7'] + lvl = ['emerg', 'alert', 'crit', 'err', + 'warning', 'notice', 'info', 'debug', '*'] + + for conf in c: + if c[conf]: + for item in c[conf]: + for s in c[conf][item]['selectors'].split(";"): + f = re.sub("\..*$", "", s) + if f not in fac: + raise ConfigError( + 'Invalid facility ' + s + ' set in ' + conf + ' ' + item) + l = re.sub("^.+\.", "", s) + if l not in lvl: + raise ConfigError( + 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item) + + +def apply(c): + if not c: + return run('systemctl stop syslog.service') + return run('systemctl restart syslog.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py new file mode 100755 index 000000000..0f4513122 --- /dev/null +++ b/src/conf_mode/system-timezone.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os + +from copy import deepcopy +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call + +from vyos import airbag +airbag.enable() + +default_config_data = { + 'name': 'UTC' +} + +def get_config(): + tz = deepcopy(default_config_data) + conf = Config() + if conf.exists('system time-zone'): + tz['name'] = conf.return_value('system time-zone') + + return tz + +def verify(tz): + pass + +def generate(tz): + pass + +def apply(tz): + call('/usr/bin/timedatectl set-timezone {}'.format(tz['name'])) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py new file mode 100755 index 000000000..30ea89098 --- /dev/null +++ b/src/conf_mode/system-wifi-regdom.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from copy import deepcopy +from sys import exit + +from vyos.config import Config +from vyos import ConfigError +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_80211_file='/etc/modprobe.d/cfg80211.conf' +config_crda_file='/etc/default/crda' + +default_config_data = { + 'regdom' : '', + 'deleted' : False +} + +def get_config(): + regdom = deepcopy(default_config_data) + conf = Config() + base = ['system', 'wifi-regulatory-domain'] + + # Check if interface has been removed + if not conf.exists(base): + regdom['deleted'] = True + return regdom + else: + regdom['regdom'] = conf.return_value(base) + + return regdom + +def verify(regdom): + if regdom['deleted']: + return None + + if not regdom['regdom']: + raise ConfigError("Wireless regulatory domain is mandatory.") + + return None + +def generate(regdom): + print("Changing the wireless regulatory domain requires a system reboot.") + + if regdom['deleted']: + if os.path.isfile(config_80211_file): + os.unlink(config_80211_file) + + if os.path.isfile(config_crda_file): + os.unlink(config_crda_file) + + return None + + render(config_80211_file, 'wifi/cfg80211.conf.tmpl', regdom) + render(config_crda_file, 'wifi/crda.tmpl', regdom) + return None + +def apply(regdom): + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py new file mode 100755 index 000000000..6f83335f3 --- /dev/null +++ b/src/conf_mode/system_console.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from fileinput import input as replace_in_file +from vyos.config import Config +from vyos.util import call +from vyos.template import render +from vyos import ConfigError, airbag +airbag.enable() + +by_bus_dir = '/dev/serial/by-bus' + +def get_config(): + conf = Config() + base = ['system', 'console'] + + # retrieve configuration at once + console = conf.get_config_dict(base, get_first_key=True) + + # bail out early if no serial console is configured + if 'device' not in console.keys(): + return console + + # convert CLI values to system values + for device in console['device'].keys(): + # no speed setting has been configured - use default value + if not 'speed' in console['device'][device].keys(): + tmp = { 'speed': '' } + if device.startswith('hvc'): + tmp['speed'] = 38400 + else: + tmp['speed'] = 115200 + + console['device'][device].update(tmp) + + if device.startswith('usb'): + # It is much easiert to work with the native ttyUSBn name when using + # getty, but that name may change across reboots - depending on the + # amount of connected devices. We will resolve the fixed device name + # to its dynamic device file - and create a new dict entry for it. + by_bus_device = f'{by_bus_dir}/{device}' + if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): + tmp = os.path.basename(os.readlink(by_bus_device)) + # updating the dict must come as last step in the loop! + console['device'][tmp] = console['device'].pop(device) + + return console + +def verify(console): + return None + +def generate(console): + base_dir = '/etc/systemd/system' + # Remove all serial-getty configuration files in advance + for root, dirs, files in os.walk(base_dir): + for basename in files: + if 'serial-getty' in basename: + call(f'systemctl stop {basename}') + os.unlink(os.path.join(root, basename)) + + if not console: + return None + + for device in console['device'].keys(): + config_file = base_dir + f'/serial-getty@{device}.service' + getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service' + + render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device]) + os.symlink(config_file, getty_wants_symlink) + + # GRUB + # For existing serial line change speed (if necessary) + # Only applys to ttyS0 + if 'ttyS0' not in console['device'].keys(): + return None + + speed = console['device']['ttyS0']['speed'] + grub_config = '/boot/grub/grub.cfg' + if not os.path.isfile(grub_config): + return None + + # stdin/stdout are redirected in replace_in_file(), thus print() is fine + p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$') + for line in replace_in_file(grub_config, inplace=True): + if line.startswith('serial --unit'): + line = f'serial --unit=0 --speed={speed}\n' + elif p.match(line): + line = '{},{}{}\n'.format(p.search(line)[1], speed, p.search(line)[2]) + + print(line, end='') + + return None + +def apply(console): + # reset screen blanking + call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1') + + # Reload systemd manager configuration + call('systemctl daemon-reload') + + if not console: + return None + + if 'powersave' in console.keys(): + # Configure screen blank powersaving on VGA console + call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1') + + # Start getty process on configured serial interfaces + for device in console['device'].keys(): + # Only start console if it exists on the running system. If a user + # detaches a USB serial console and reboots - it should not fail! + if os.path.exists(f'/dev/{device}'): + call(f'systemctl start serial-getty@{device}.service') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py new file mode 100755 index 000000000..31a09252d --- /dev/null +++ b/src/conf_mode/system_lcd.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.util import call +from vyos.util import find_device_file +from vyos.template import render +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +lcdd_conf = '/run/LCDd/LCDd.conf' +lcdproc_conf = '/run/lcdproc/lcdproc.conf' + +def get_config(): + conf = Config() + base = ['system', 'lcd'] + lcd = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) + # Return (possibly empty) dictionary + return lcd + +def verify(lcd): + if not lcd: + return None + + if 'model' in lcd and lcd['model'] in ['sdec']: + # This is a fixed LCD display, no device needed - bail out early + return None + + if not {'device', 'model'} <= set(lcd): + raise ConfigError('Both device and driver must be set!') + + return None + +def generate(lcd): + if not lcd: + return None + + if 'device' in lcd: + lcd['device'] = find_device_file(lcd['device']) + + # Render config file for daemon LCDd + render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd, trim_blocks=True) + # Render config file for client lcdproc + render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd, trim_blocks=True) + + return None + +def apply(lcd): + if not lcd: + call('systemctl stop lcdproc.service LCDd.service') + + for file in [lcdd_conf, lcdproc_conf]: + if os.path.exists(file): + os.remove(file) + else: + # Restart server + call('systemctl restart LCDd.service lcdproc.service') + + return None + +if __name__ == '__main__': + try: + config_dict = get_config() + verify(config_dict) + generate(config_dict) + apply(config_dict) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/task_scheduler.py b/src/conf_mode/task_scheduler.py new file mode 100755 index 000000000..51d8684cb --- /dev/null +++ b/src/conf_mode/task_scheduler.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2017 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import re +import sys + +from vyos.config import Config +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +crontab_file = "/etc/cron.d/vyos-crontab" + + +def format_task(minute="*", hour="*", day="*", dayofweek="*", month="*", user="root", rawspec=None, command=""): + fmt_full = "{minute} {hour} {day} {month} {dayofweek} {user} {command}\n" + fmt_raw = "{spec} {user} {command}\n" + + if rawspec is None: + s = fmt_full.format(minute=minute, hour=hour, day=day, + dayofweek=dayofweek, month=month, command=command, user=user) + else: + s = fmt_raw.format(spec=rawspec, user=user, command=command) + + return s + +def split_interval(s): + result = re.search(r"(\d+)([mdh]?)", s) + value = int(result.group(1)) + suffix = result.group(2) + return( (value, suffix) ) + +def make_command(executable, arguments): + if arguments: + return("sg vyattacfg \"{0} {1}\"".format(executable, arguments)) + else: + return("sg vyattacfg \"{0}\"".format(executable)) + +def get_config(): + conf = Config() + conf.set_level("system task-scheduler task") + task_names = conf.list_nodes("") + tasks = [] + + for name in task_names: + interval = conf.return_value("{0} interval".format(name)) + spec = conf.return_value("{0} crontab-spec".format(name)) + executable = conf.return_value("{0} executable path".format(name)) + args = conf.return_value("{0} executable arguments".format(name)) + task = { + "name": name, + "interval": interval, + "spec": spec, + "executable": executable, + "args": args + } + tasks.append(task) + + return tasks + +def verify(tasks): + for task in tasks: + if not task["interval"] and not task["spec"]: + raise ConfigError("Invalid task {0}: must define either interval or crontab-spec".format(task["name"])) + + if task["interval"]: + if task["spec"]: + raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"])) + + if not re.match(r"^\d+[mdh]?$", task["interval"]): + raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"]))) + else: + # Check if values are within allowed range + value, suffix = split_interval(task["interval"]) + + if not suffix or suffix == "m": + if value > 60: + raise ConfigError("Invalid task {0}: interval in minutes must not exceed 60".format(task["name"])) + elif suffix == "h": + if value > 24: + raise ConfigError("Invalid task {0}: interval in hours must not exceed 24".format(task["name"])) + elif suffix == "d": + if value > 31: + raise ConfigError("Invalid task {0}: interval in days must not exceed 31".format(task["name"])) + + if not task["executable"]: + raise ConfigError("Invalid task {0}: executable is not defined".format(task["name"])) + else: + # Check if executable exists and is executable + if not (os.path.isfile(task["executable"]) and os.access(task["executable"], os.X_OK)): + raise ConfigError("Invalid task {0}: file {1} does not exist or is not executable".format(task["name"], task["executable"])) + +def generate(tasks): + crontab_header = "### Generated by vyos-update-crontab.py ###\n" + if len(tasks) == 0: + if os.path.exists(crontab_file): + os.remove(crontab_file) + else: + pass + else: + crontab_lines = [] + for task in tasks: + command = make_command(task["executable"], task["args"]) + if task["spec"]: + line = format_task(command=command, rawspec=task["spec"]) + else: + value, suffix = split_interval(task["interval"]) + if not suffix or suffix == "m": + line = format_task(command=command, minute="*/{0}".format(value)) + elif suffix == "h": + line = format_task(command=command, minute="0", hour="*/{0}".format(value)) + elif suffix == "d": + line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value)) + crontab_lines.append(line) + + with open(crontab_file, 'w') as f: + f.write(crontab_header) + f.writelines(crontab_lines) + +def apply(config): + # No daemon restarts etc. needed for cron + pass + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py new file mode 100755 index 000000000..d31851bef --- /dev/null +++ b/src/conf_mode/tftp_server.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import stat +import pwd + +from copy import deepcopy +from glob import glob +from sys import exit + +from vyos.config import Config +from vyos.validate import is_ipv4, is_addr_assigned +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/etc/default/tftpd' + +default_config_data = { + 'directory': '', + 'allow_upload': False, + 'port': '69', + 'listen': [] +} + +def get_config(): + tftpd = deepcopy(default_config_data) + conf = Config() + base = ['service', 'tftp-server'] + if not conf.exists(base): + return None + else: + conf.set_level(base) + + if conf.exists(['directory']): + tftpd['directory'] = conf.return_value(['directory']) + + if conf.exists(['allow-upload']): + tftpd['allow_upload'] = True + + if conf.exists(['port']): + tftpd['port'] = conf.return_value(['port']) + + if conf.exists(['listen-address']): + tftpd['listen'] = conf.return_values(['listen-address']) + + return tftpd + +def verify(tftpd): + # bail out early - looks like removal from running config + if tftpd is None: + return None + + # Configuring allowed clients without a server makes no sense + if not tftpd['directory']: + raise ConfigError('TFTP root directory must be configured!') + + if not tftpd['listen']: + raise ConfigError('TFTP server listen address must be configured!') + + for addr in tftpd['listen']: + if not is_addr_assigned(addr): + print('WARNING: TFTP server listen address {0} not assigned to any interface!'.format(addr)) + + return None + +def generate(tftpd): + # cleanup any available configuration file + # files will be recreated on demand + for i in glob(config_file + '*'): + os.unlink(i) + + # bail out early - looks like removal from running config + if tftpd is None: + return None + + idx = 0 + for listen in tftpd['listen']: + config = deepcopy(tftpd) + if is_ipv4(listen): + config['listen'] = [listen + ":" + tftpd['port'] + " -4"] + else: + config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"] + + file = config_file + str(idx) + render(file, 'tftp-server/default.tmpl', config) + + idx = idx + 1 + + return None + +def apply(tftpd): + # stop all services first - then we will decide + call('systemctl stop tftpd@{0..20}.service') + + # bail out early - e.g. service deletion + if tftpd is None: + return None + + tftp_root = tftpd['directory'] + if not os.path.exists(tftp_root): + os.makedirs(tftp_root) + os.chmod(tftp_root, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH) + + # get UNIX uid for user 'tftp' + tftp_uid = pwd.getpwnam('tftp').pw_uid + tftp_gid = pwd.getpwnam('tftp').pw_gid + + # get UNIX uid for tftproot directory + dir_uid = os.stat(tftp_root).st_uid + dir_gid = os.stat(tftp_root).st_gid + + # adjust uid/gid of tftproot directory if files don't belong to user tftp + if (tftp_uid != dir_uid) or (tftp_gid != dir_gid): + os.chown(tftp_root, tftp_uid, tftp_gid) + + idx = 0 + for listen in tftpd['listen']: + call('systemctl restart tftpd@{0}.service'.format(idx)) + idx = idx + 1 + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_anyconnect.py b/src/conf_mode/vpn_anyconnect.py new file mode 100755 index 000000000..158e1a117 --- /dev/null +++ b/src/conf_mode/vpn_anyconnect.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.xml import defaults +from vyos.template import render +from vyos.util import call +from vyos import ConfigError +from crypt import crypt, mksalt, METHOD_SHA512 + +from vyos import airbag +airbag.enable() + +cfg_dir = '/run/ocserv' +ocserv_conf = cfg_dir + '/ocserv.conf' +ocserv_passwd = cfg_dir + '/ocpasswd' +radius_cfg = cfg_dir + '/radiusclient.conf' +radius_servers = cfg_dir + '/radius_servers' + + +# Generate hash from user cleartext password +def get_hash(password): + return crypt(password, mksalt(METHOD_SHA512)) + + +def get_config(): + conf = Config() + base = ['vpn', 'anyconnect'] + if not conf.exists(base): + return None + + ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + default_values = defaults(base) + ocserv = dict_merge(default_values, ocserv) + return ocserv + + +def verify(ocserv): + if ocserv is None: + return None + + # Check authentication + if "authentication" in ocserv: + if "mode" in ocserv["authentication"]: + if "local" in ocserv["authentication"]["mode"]: + if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: + raise ConfigError('Anyconect mode local required at leat one user') + else: + for user in ocserv["authentication"]["local_users"]["username"]: + if not "password" in ocserv["authentication"]["local_users"]["username"][user]: + raise ConfigError(f'password required for user {user}') + else: + raise ConfigError('anyconnect authentication mode required') + else: + raise ConfigError('anyconnect authentication credentials required') + + # Check ssl + if "ssl" in ocserv: + req_cert = ['ca_cert_file', 'cert_file', 'key_file'] + for cert in req_cert: + if not cert in ocserv["ssl"]: + raise ConfigError('anyconnect ssl {0} required'.format(cert.replace('_', '-'))) + else: + raise ConfigError('anyconnect ssl required') + + # Check network settings + if "network_settings" in ocserv: + if "push_route" in ocserv["network_settings"]: + # Replace default route + if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]: + ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") + ocserv["network_settings"]["push_route"].append("default") + else: + ocserv["network_settings"]["push_route"] = "default" + else: + raise ConfigError('anyconnect network settings required') + + +def generate(ocserv): + if not ocserv: + return None + + if "radius" in ocserv["authentication"]["mode"]: + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + # Render radius servers + render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + else: + if "local_users" in ocserv["authentication"]: + for user in ocserv["authentication"]["local_users"]["username"]: + ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) + # Render local users + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True) + + # Render config + render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True) + + + +def apply(ocserv): + if not ocserv: + call('systemctl stop ocserv.service') + for file in [ocserv_conf, ocserv_passwd]: + if os.path.exists(file): + os.unlink(file) + else: + call('systemctl restart ocserv.service') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py new file mode 100755 index 000000000..26ad1af84 --- /dev/null +++ b/src/conf_mode/vpn_l2tp.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit +from time import sleep + +from ipaddress import ip_network + +from vyos.config import Config +from vyos.util import call, get_half_cpus +from vyos.validate import is_ipv4 +from vyos import ConfigError +from vyos.template import render + +from vyos import airbag +airbag.enable() + +l2tp_conf = '/run/accel-pppd/l2tp.conf' +l2tp_chap_secrets = '/run/accel-pppd/l2tp.chap-secrets' + +default_config_data = { + 'auth_mode': 'local', + 'auth_ppp_mppe': 'prefer', + 'auth_proto': ['auth_mschap_v2'], + 'chap_secrets_file': l2tp_chap_secrets, # used in Jinja2 template + 'client_ip_pool': None, + 'client_ip_subnets': [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + 'dnsv4': [], + 'dnsv6': [], + 'gateway_address': '10.255.255.0', + 'local_users' : [], + 'mtu': '1436', + 'outside_addr': '', + 'ppp_mppe': 'prefer', + 'ppp_echo_failure' : '3', + 'ppp_echo_interval' : '30', + 'ppp_echo_timeout': '0', + 'radius_server': [], + 'radius_acct_tmo': '3', + 'radius_max_try': '3', + 'radius_timeout': '3', + 'radius_nas_id': '', + 'radius_nas_ip': '', + 'radius_source_address': '', + 'radius_shaper_attr': '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author': '', + 'wins': [], + 'ip6_column': [], + 'thread_cnt': get_half_cpus() +} + +def get_config(): + conf = Config() + base_path = ['vpn', 'l2tp', 'remote-access'] + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + l2tp = deepcopy(default_config_data) + + ### general options ### + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + l2tp['dnsv4'].append(name_server) + else: + l2tp['dnsv6'].append(name_server) + + if conf.exists(['wins-server']): + l2tp['wins'] = conf.return_values(['wins-server']) + + if conf.exists('outside-address'): + l2tp['outside_addr'] = conf.return_value('outside-address') + + if conf.exists(['authentication', 'mode']): + l2tp['auth_mode'] = conf.return_value(['authentication', 'mode']) + + if conf.exists(['authentication', 'protocols']): + auth_mods = { + 'pap': 'auth_pap', + 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', + 'mschap-v2': 'auth_mschap_v2' + } + + for proto in conf.return_values(['authentication', 'protocols']): + l2tp['auth_proto'].append(auth_mods[proto]) + + if conf.exists(['authentication', 'mppe']): + l2tp['auth_ppp_mppe'] = conf.return_value(['authentication', 'mppe']) + + # + # local auth + if conf.exists(['authentication', 'local-users']): + for username in conf.list_nodes(['authentication', 'local-users', 'username']): + user = { + 'name' : username, + 'password' : '', + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + + conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) + + if conf.exists(['password']): + user['password'] = conf.return_value(['password']) + + if conf.exists(['disable']): + user['state'] = 'disable' + + if conf.exists(['static-ip']): + user['ip'] = conf.return_value(['static-ip']) + + if conf.exists(['rate-limit', 'download']): + user['download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + user['upload'] = conf.return_value(['rate-limit', 'upload']) + + l2tp['local_users'].append(user) + + # + # RADIUS auth and settings + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['server']): + for server in conf.list_nodes(['server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812', + 'acct_port' : '1813' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail_time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + l2tp['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + l2tp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + l2tp['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + l2tp['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + l2tp['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + l2tp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + l2tp['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dynamic-author']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'server']): + dae['server'] = conf.return_value(['dynamic-author', 'server']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + l2tp['radius_dynamic_author'] = dae + + if conf.exists(['rate-limit', 'enable']): + l2tp['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + l2tp['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + l2tp['radius_shaper_vendor'] = conf.return_value(c_vendor) + + conf.set_level(base_path) + if conf.exists(['client-ip-pool']): + if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): + start = conf.return_value(['client-ip-pool', 'start']) + stop = conf.return_value(['client-ip-pool', 'stop']) + l2tp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + + if conf.exists(['client-ip-pool', 'subnet']): + l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) + + if conf.exists(['client-ipv6-pool', 'prefix']): + l2tp['ip6_column'].append('ip6') + for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) + + l2tp['client_ipv6_pool'].append(tmp) + + if conf.exists(['client-ipv6-pool', 'delegate']): + l2tp['ip6_column'].append('ip6-db') + for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) + + l2tp['client_ipv6_delegate_prefix'].append(tmp) + + if conf.exists(['mtu']): + l2tp['mtu'] = conf.return_value(['mtu']) + + # gateway address + if conf.exists(['gateway-address']): + l2tp['gateway_address'] = conf.return_value(['gateway-address']) + else: + # calculate gw-ip-address + if conf.exists(['client-ip-pool', 'start']): + # use start ip as gw-ip-address + l2tp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) + + elif conf.exists(['client-ip-pool', 'subnet']): + # use first ip address from first defined pool + subnet = conf.return_values(['client-ip-pool', 'subnet'])[0] + subnet = ip_network(subnet) + l2tp['gateway_address'] = str(list(subnet.hosts())[0]) + + # LNS secret + if conf.exists(['lns', 'shared-secret']): + l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) + + if conf.exists(['ccp-disable']): + l2tp['ccp_disable'] = True + + # PPP options + if conf.exists(['idle']): + l2tp['ppp_echo_timeout'] = conf.return_value(['idle']) + + if conf.exists(['ppp-options', 'lcp-echo-failure']): + l2tp['ppp_echo_failure'] = conf.return_value(['ppp-options', 'lcp-echo-failure']) + + if conf.exists(['ppp-options', 'lcp-echo-interval']): + l2tp['ppp_echo_interval'] = conf.return_value(['ppp-options', 'lcp-echo-interval']) + + return l2tp + + +def verify(l2tp): + if not l2tp: + return None + + if l2tp['auth_mode'] == 'local': + if not l2tp['local_users']: + raise ConfigError('L2TP local auth mode requires local users to be configured!') + + for user in l2tp['local_users']: + if not user['password']: + raise ConfigError(f"Password required for user {user['name']}") + + elif l2tp['auth_mode'] == 'radius': + if len(l2tp['radius_server']) == 0: + raise ConfigError("RADIUS authentication requires at least one server") + + for radius in l2tp['radius_server']: + if not radius['key']: + raise ConfigError(f"Missing RADIUS secret for server { radius['key'] }") + + # check for the existence of a client ip pool + if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): + raise ConfigError( + "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") + + # check ipv6 + if l2tp['client_ipv6_delegate_prefix'] and not l2tp['client_ipv6_pool']: + raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix') + + for prefix in l2tp['client_ipv6_delegate_prefix']: + if not prefix['mask']: + raise ConfigError('Delegation-prefix required for individual delegated networks') + + if len(l2tp['wins']) > 2: + raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + + if len(l2tp['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(l2tp['dnsv6']) > 3: + raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + + return None + + +def generate(l2tp): + if not l2tp: + return None + + render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True) + + if l2tp['auth_mode'] == 'local': + render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp) + os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + + else: + if os.path.exists(l2tp_chap_secrets): + os.unlink(l2tp_chap_secrets) + + return None + + +def apply(l2tp): + if not l2tp: + call('systemctl stop accel-ppp@l2tp.service') + for file in [l2tp_chap_secrets, l2tp_conf]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@l2tp.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py new file mode 100755 index 000000000..32cbadd74 --- /dev/null +++ b/src/conf_mode/vpn_pptp.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, get_half_cpus +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +pptp_conf = '/run/accel-pppd/pptp.conf' +pptp_chap_secrets = '/run/accel-pppd/pptp.chap-secrets' + +default_pptp = { + 'auth_mode' : 'local', + 'local_users' : [], + 'radius_server' : [], + 'radius_acct_tmo' : '30', + 'radius_max_try' : '3', + 'radius_timeout' : '30', + 'radius_nas_id' : '', + 'radius_nas_ip' : '', + 'radius_source_address' : '', + 'radius_shaper_attr' : '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author' : '', + 'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template + 'outside_addr': '', + 'dnsv4': [], + 'wins': [], + 'client_ip_pool': '', + 'mtu': '1436', + 'auth_proto' : ['auth_mschap_v2'], + 'ppp_mppe' : 'prefer', + 'thread_cnt': get_half_cpus() +} + +def get_config(): + conf = Config() + base_path = ['vpn', 'pptp', 'remote-access'] + if not conf.exists(base_path): + return None + + pptp = deepcopy(default_pptp) + conf.set_level(base_path) + + if conf.exists(['name-server']): + pptp['dnsv4'] = conf.return_values(['name-server']) + + if conf.exists(['wins-server']): + pptp['wins'] = conf.return_values(['wins-server']) + + if conf.exists(['outside-address']): + pptp['outside_addr'] = conf.return_value(['outside-address']) + + if conf.exists(['authentication', 'mode']): + pptp['auth_mode'] = conf.return_value(['authentication', 'mode']) + + # + # local auth + if conf.exists(['authentication', 'local-users']): + for username in conf.list_nodes(['authentication', 'local-users', 'username']): + user = { + 'name': username, + 'password' : '', + 'state' : 'enabled', + 'ip' : '*', + } + + conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) + + if conf.exists(['password']): + user['password'] = conf.return_value(['password']) + + if conf.exists(['disable']): + user['state'] = 'disable' + + if conf.exists(['static-ip']): + user['ip'] = conf.return_value(['static-ip']) + + if not conf.exists(['disable']): + pptp['local_users'].append(user) + + # + # RADIUS auth and settings + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['server']): + for server in conf.list_nodes(['server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812', + 'acct_port' : '1813' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail_time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + pptp['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + pptp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + pptp['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + pptp['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + pptp['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + pptp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + pptp['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dae-server']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'ip-address']): + dae['server'] = conf.return_value(['dynamic-author', 'ip-address']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + pptp['radius_dynamic_author'] = dae + + if conf.exists(['rate-limit', 'enable']): + pptp['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + pptp['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + pptp['radius_shaper_vendor'] = conf.return_value(c_vendor) + + conf.set_level(base_path) + if conf.exists(['client-ip-pool']): + if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): + start = conf.return_value(['client-ip-pool', 'start']) + stop = conf.return_value(['client-ip-pool', 'stop']) + pptp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + + if conf.exists(['mtu']): + pptp['mtu'] = conf.return_value(['mtu']) + + # gateway address + if conf.exists(['gateway-address']): + pptp['gw_ip'] = conf.return_value(['gateway-address']) + else: + # calculate gw-ip-address + if conf.exists(['client-ip-pool', 'start']): + # use start ip as gw-ip-address + pptp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) + + if conf.exists(['authentication', 'require']): + # clear default list content, now populate with actual CLI values + pptp['auth_proto'] = [] + auth_mods = { + 'pap': 'auth_pap', + 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', + 'mschap-v2': 'auth_mschap_v2' + } + + for proto in conf.return_values(['authentication', 'require']): + pptp['auth_proto'].append(auth_mods[proto]) + + if conf.exists(['authentication', 'mppe']): + pptp['ppp_mppe'] = conf.return_value(['authentication', 'mppe']) + + return pptp + + +def verify(pptp): + if not pptp: + return None + + if pptp['auth_mode'] == 'local': + if not pptp['local_users']: + raise ConfigError('PPTP local auth mode requires local users to be configured!') + + for user in pptp['local_users']: + username = user['name'] + if not user['password']: + raise ConfigError(f'Password required for local user "{username}"') + + elif pptp['auth_mode'] == 'radius': + if len(pptp['radius_server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + for radius in pptp['radius_server']: + if not radius['key']: + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + + if len(pptp['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(pptp['wins']) > 2: + raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + + +def generate(pptp): + if not pptp: + return None + + render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True) + + if pptp['local_users']: + render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp, trim_blocks=True) + os.chmod(pptp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + else: + if os.path.exists(pptp_chap_secrets): + os.unlink(pptp_chap_secrets) + + +def apply(pptp): + if not pptp: + call('systemctl stop accel-ppp@pptp.service') + for file in [pptp_conf, pptp_chap_secrets]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@pptp.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py new file mode 100755 index 000000000..ddb499bf4 --- /dev/null +++ b/src/conf_mode/vpn_sstp.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from time import sleep +from sys import exit +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, run, get_half_cpus +from vyos.validate import is_ipv4 +from vyos import ConfigError + +from vyos import airbag +airbag.enable() + +sstp_conf = '/run/accel-pppd/sstp.conf' +sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets' + +default_config_data = { + 'local_users' : [], + 'auth_mode' : 'local', + 'auth_proto' : ['auth_mschap_v2'], + 'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template + 'client_ip_pool' : [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + 'client_gateway': '', + 'dnsv4' : [], + 'dnsv6' : [], + 'radius_server' : [], + 'radius_acct_tmo' : '3', + 'radius_max_try' : '3', + 'radius_timeout' : '3', + 'radius_nas_id' : '', + 'radius_nas_ip' : '', + 'radius_source_address' : '', + 'radius_shaper_attr' : '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author' : '', + 'ssl_ca' : '', + 'ssl_cert' : '', + 'ssl_key' : '', + 'mtu' : '', + 'ppp_mppe' : 'prefer', + 'ppp_echo_failure' : '', + 'ppp_echo_interval' : '', + 'ppp_echo_timeout' : '', + 'thread_cnt' : get_half_cpus() +} + +def get_config(): + sstp = deepcopy(default_config_data) + base_path = ['vpn', 'sstp'] + conf = Config() + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + + if conf.exists(['authentication', 'mode']): + sstp['auth_mode'] = conf.return_value(['authentication', 'mode']) + + # + # local auth + if conf.exists(['authentication', 'local-users']): + for username in conf.list_nodes(['authentication', 'local-users', 'username']): + user = { + 'name' : username, + 'password' : '', + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + + conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) + + if conf.exists(['password']): + user['password'] = conf.return_value(['password']) + + if conf.exists(['disable']): + user['state'] = 'disable' + + if conf.exists(['static-ip']): + user['ip'] = conf.return_value(['static-ip']) + + if conf.exists(['rate-limit', 'download']): + user['download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + user['upload'] = conf.return_value(['rate-limit', 'upload']) + + sstp['local_users'].append(user) + + # + # RADIUS auth and settings + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['server']): + for server in conf.list_nodes(['server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812', + 'acct_port' : '1813' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail_time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['acct-port']): + radius['acct_port'] = conf.return_value(['acct-port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + sstp['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + sstp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + sstp['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + sstp['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + sstp['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + sstp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + sstp['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dynamic-author']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'server']): + dae['server'] = conf.return_value(['dynamic-author', 'server']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + sstp['radius_dynamic_author'] = dae + + if conf.exists(['rate-limit', 'enable']): + sstp['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + sstp['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + sstp['radius_shaper_vendor'] = conf.return_value(c_vendor) + + # + # authentication protocols + conf.set_level(base_path + ['authentication']) + if conf.exists(['protocols']): + # clear default list content, now populate with actual CLI values + sstp['auth_proto'] = [] + auth_mods = { + 'pap': 'auth_pap', + 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', + 'mschap-v2': 'auth_mschap_v2' + } + + for proto in conf.return_values(['protocols']): + sstp['auth_proto'].append(auth_mods[proto]) + + # + # read in SSL certs + conf.set_level(base_path + ['ssl']) + if conf.exists(['ca-cert-file']): + sstp['ssl_ca'] = conf.return_value(['ca-cert-file']) + + if conf.exists(['cert-file']): + sstp['ssl_cert'] = conf.return_value(['cert-file']) + + if conf.exists(['key-file']): + sstp['ssl_key'] = conf.return_value(['key-file']) + + + # + # read in client IPv4 pool + conf.set_level(base_path + ['network-settings', 'client-ip-settings']) + if conf.exists(['subnet']): + sstp['client_ip_pool'] = conf.return_values(['subnet']) + + if conf.exists(['gateway-address']): + sstp['client_gateway'] = conf.return_value(['gateway-address']) + + # + # read in client IPv6 pool + conf.set_level(base_path + ['network-settings', 'client-ipv6-pool']) + if conf.exists(['prefix']): + for prefix in conf.list_nodes(['prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['prefix', prefix, 'mask']) + + sstp['client_ipv6_pool'].append(tmp) + + if conf.exists(['delegate']): + for prefix in conf.list_nodes(['delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['delegate', prefix, 'delegation-prefix']): + tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix']) + + sstp['client_ipv6_delegate_prefix'].append(tmp) + + # + # read in network settings + conf.set_level(base_path + ['network-settings']) + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + sstp['dnsv4'].append(name_server) + else: + sstp['dnsv6'].append(name_server) + + if conf.exists(['mtu']): + sstp['mtu'] = conf.return_value(['mtu']) + + # + # read in PPP stuff + conf.set_level(base_path + ['ppp-settings']) + if conf.exists('mppe'): + sstp['ppp_mppe'] = conf.return_value(['ppp-settings', 'mppe']) + + if conf.exists(['lcp-echo-failure']): + sstp['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure']) + + if conf.exists(['lcp-echo-interval']): + sstp['ppp_echo_interval'] = conf.return_value(['lcp-echo-interval']) + + if conf.exists(['lcp-echo-timeout']): + sstp['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout']) + + return sstp + + +def verify(sstp): + if sstp is None: + return None + + # vertify auth settings + if sstp['auth_mode'] == 'local': + if not sstp['local_users']: + raise ConfigError('SSTP local auth mode requires local users to be configured!') + + for user in sstp['local_users']: + username = user['name'] + if not user['password']: + raise ConfigError(f'Password required for local user "{username}"') + + # if up/download is set, check that both have a value + if user['upload'] and not user['download']: + raise ConfigError(f'Download speed value required for local user "{username}"') + + if user['download'] and not user['upload']: + raise ConfigError(f'Upload speed value required for local user "{username}"') + + if not sstp['client_ip_pool']: + raise ConfigError('Client IP subnet required') + + if not sstp['client_gateway']: + raise ConfigError('Client gateway IP address required') + + if len(sstp['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + # check ipv6 + if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']: + raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix') + + for prefix in sstp['client_ipv6_delegate_prefix']: + if not prefix['mask']: + raise ConfigError('Delegation-prefix required for individual delegated networks') + + if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']: + raise ConfigError('One or more SSL certificates missing') + + if not os.path.exists(sstp['ssl_ca']): + file = sstp['ssl_ca'] + raise ConfigError(f'SSL CA certificate file "{file}" does not exist') + + if not os.path.exists(sstp['ssl_cert']): + file = sstp['ssl_cert'] + raise ConfigError(f'SSL public key file "{file}" does not exist') + + if not os.path.exists(sstp['ssl_key']): + file = sstp['ssl_key'] + raise ConfigError(f'SSL private key file "{file}" does not exist') + + if sstp['auth_mode'] == 'radius': + if len(sstp['radius_server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + for radius in sstp['radius_server']: + if not radius['key']: + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + +def generate(sstp): + if not sstp: + return None + + # accel-cmd reload doesn't work so any change results in a restart of the daemon + render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True) + + if sstp['local_users']: + render(sstp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', sstp, trim_blocks=True) + os.chmod(sstp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + else: + if os.path.exists(sstp_chap_secrets): + os.unlink(sstp_chap_secrets) + + return sstp + +def apply(sstp): + if not sstp: + call('systemctl stop accel-ppp@sstp.service') + for file in [sstp_chap_secrets, sstp_conf]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@sstp.service') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py new file mode 100755 index 000000000..56ca813ff --- /dev/null +++ b/src/conf_mode/vrf.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from copy import deepcopy +from json import loads + +from vyos.config import Config +from vyos.configdict import list_diff +from vyos.ifconfig import Interface +from vyos.util import read_file, cmd +from vyos import ConfigError +from vyos.template import render + +from vyos import airbag +airbag.enable() + +config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' + +default_config_data = { + 'bind_to_all': '0', + 'deleted': False, + 'vrf_add': [], + 'vrf_existing': [], + 'vrf_remove': [] +} + +def _cmd(command): + cmd(command, raising=ConfigError, message='Error changing VRF') + +def list_rules(): + command = 'ip -j -4 rule show' + answer = loads(cmd(command)) + return [_ for _ in answer if _] + +def vrf_interfaces(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['interfaces']) + section = c.get_config_dict([], get_first_key=True) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'vrf' in interface: + v = interface.get('vrf', '') + if v == match: + matched.append(name) + + c.set_level(old_level) + return matched + +def vrf_routing(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['protocols', 'vrf']) + if match in c.list_nodes([]): + matched.append(match) + + c.set_level(old_level) + return matched + + +def get_config(): + conf = Config() + vrf_config = deepcopy(default_config_data) + + cfg_base = ['vrf'] + if not conf.exists(cfg_base): + # get all currently effetive VRFs and mark them for deletion + vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) + else: + # set configuration level base + conf.set_level(cfg_base) + + # Should services be allowed to bind to all VRFs? + if conf.exists(['bind-to-all']): + vrf_config['bind_to_all'] = '1' + + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(['name']) + act_vrf = conf.list_nodes(['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(['name']): + vrf_inst = { + 'description' : '', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False + } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True + + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) + + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) + + # set configuration level base + conf.set_level(cfg_base) + + # check VRFs which need to be removed as they are not allowed to have + # interfaces attached + tmp = [] + for name in vrf_config['vrf_remove']: + vrf_inst = { + 'interfaces': [], + 'name': name, + 'routes': [] + } + + # find member interfaces of this particulat VRF + vrf_inst['interfaces'] = vrf_interfaces(conf, name) + + # find routing protocols used by this VRF + vrf_inst['routes'] = vrf_routing(conf, name) + + # append individual VRF configuration to temporary configuration list + tmp.append(vrf_inst) + + # replace values in vrf_remove with list of dictionaries + # as we need it in verify() - we can't delete a VRF with members attached + vrf_config['vrf_remove'] = tmp + return vrf_config + +def verify(vrf_config): + # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_remove']: + if len(vrf['interfaces']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!") + + if len(vrf['routes']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!") + + table_ids = [] + for vrf in vrf_config['vrf_add']: + # table id is mandatory + if not vrf['table']: + raise ConfigError(f"VRF {vrf['name']} table id is mandatory!") + + # routing table id can't be changed - OS restriction + if vrf['table_mod']: + raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!") + + # VRf routing table ID must be unique on the system + if vrf['table'] in table_ids: + raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!") + + table_ids.append(vrf['table']) + + return None + +def generate(vrf_config): + render(config_file, 'vrf/vrf.conf.tmpl', vrf_config) + return None + +def apply(vrf_config): + # Documentation + # + # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt + # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) + # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling + # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf + # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf + + # set the default VRF global behaviour + bind_all = vrf_config['bind_to_all'] + if read_file('/proc/sys/net/ipv4/tcp_l3mdev_accept') != bind_all: + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + + for vrf in vrf_config['vrf_remove']: + name = vrf['name'] + if os.path.isdir(f'/sys/class/net/{name}'): + _cmd(f'ip -4 route del vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route del vrf {name} unreachable default metric 4278198272') + _cmd(f'ip link delete dev {name}') + + for vrf in vrf_config['vrf_add']: + name = vrf['name'] + table = vrf['table'] + + if not os.path.isdir(f'/sys/class/net/{name}'): + # For each VRF apart from your default context create a VRF + # interface with a separate routing table + _cmd(f'ip link add {name} type vrf table {table}') + # Start VRf + _cmd(f'ip link set dev {name} up') + # The kernel Documentation/networking/vrf.txt also recommends + # adding unreachable routes to the VRF routing tables so that routes + # afterwards are taken. + _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') + + # set VRF description for e.g. SNMP monitoring + Interface(name).set_alias(vrf['description']) + + # Linux routing uses rules to find tables - routing targets are then + # looked up in those tables. If the lookup got a matching route, the + # process ends. + # + # TL;DR; first table with a matching entry wins! + # + # You can see your routing table lookup rules using "ip rule", sadly the + # local lookup is hit before any VRF lookup. Pinging an addresses from the + # VRF will usually find a hit in the local table, and never reach the VRF + # routing table - this is usually not what you want. Thus we will + # re-arrange the tables and move the local lookup furhter down once VRFs + # are enabled. + + # get current preference on local table + local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] + + # change preference when VRFs are enabled and local lookup table is default + if not local_pref and vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 32765 table local') + _cmd(f'ip {af} rule del pref 0') + + # return to default lookup preference when no VRF is configured + if not vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 0 table local') + _cmd(f'ip {af} rule del pref 32765') + + # clean out l3mdev-table rule if present + if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: + _cmd(f'ip {af} rule del pref 1000') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py new file mode 100755 index 000000000..292eb0c78 --- /dev/null +++ b/src/conf_mode/vrrp.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address +from json import dumps +from pathlib import Path + +import vyos.config + +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +from vyos.ifconfig.vrrp import VRRP + +from vyos import airbag +airbag.enable() + +def get_config(): + vrrp_groups = [] + sync_groups = [] + + config = vyos.config.Config() + + # Get the VRRP groups + for group_name in config.list_nodes("high-availability vrrp group"): + config.set_level("high-availability vrrp group {0}".format(group_name)) + + # Retrieve the values + group = {"preempt": True, "use_vmac": False, "disable": False} + + if config.exists("disable"): + group["disable"] = True + + group["name"] = group_name + group["vrid"] = config.return_value("vrid") + group["interface"] = config.return_value("interface") + group["description"] = config.return_value("description") + group["advertise_interval"] = config.return_value("advertise-interval") + group["priority"] = config.return_value("priority") + group["hello_source"] = config.return_value("hello-source-address") + group["peer_address"] = config.return_value("peer-address") + group["sync_group"] = config.return_value("sync-group") + group["preempt_delay"] = config.return_value("preempt-delay") + group["virtual_addresses"] = config.return_values("virtual-address") + + group["auth_password"] = config.return_value("authentication password") + group["auth_type"] = config.return_value("authentication type") + + group["health_check_script"] = config.return_value("health-check script") + group["health_check_interval"] = config.return_value("health-check interval") + group["health_check_count"] = config.return_value("health-check failure-count") + + group["master_script"] = config.return_value("transition-script master") + group["backup_script"] = config.return_value("transition-script backup") + group["fault_script"] = config.return_value("transition-script fault") + group["stop_script"] = config.return_value("transition-script stop") + + if config.exists("no-preempt"): + group["preempt"] = False + if config.exists("rfc3768-compatibility"): + group["use_vmac"] = True + + # Substitute defaults where applicable + if not group["advertise_interval"]: + group["advertise_interval"] = 1 + if not group["priority"]: + group["priority"] = 100 + if not group["preempt_delay"]: + group["preempt_delay"] = 0 + if not group["health_check_interval"]: + group["health_check_interval"] = 60 + if not group["health_check_count"]: + group["health_check_count"] = 3 + + # FIXUP: translate our option for auth type to keepalived's syntax + # for simplicity + if group["auth_type"]: + if group["auth_type"] == "plaintext-password": + group["auth_type"] = "PASS" + else: + group["auth_type"] = "AH" + + vrrp_groups.append(group) + + config.set_level("") + + # Get the sync group used for conntrack-sync + conntrack_sync_group = None + if config.exists("service conntrack-sync failover-mechanism vrrp"): + conntrack_sync_group = config.return_value("service conntrack-sync failover-mechanism vrrp sync-group") + + # Get the sync groups + for sync_group_name in config.list_nodes("high-availability vrrp sync-group"): + config.set_level("high-availability vrrp sync-group {0}".format(sync_group_name)) + + sync_group = {"conntrack_sync": False} + sync_group["name"] = sync_group_name + sync_group["members"] = config.return_values("member") + if conntrack_sync_group: + if conntrack_sync_group == sync_group_name: + sync_group["conntrack_sync"] = True + + # add transition script configuration + sync_group["master_script"] = config.return_value("transition-script master") + sync_group["backup_script"] = config.return_value("transition-script backup") + sync_group["fault_script"] = config.return_value("transition-script fault") + sync_group["stop_script"] = config.return_value("transition-script stop") + + sync_groups.append(sync_group) + + # create a file with dict with proposed configuration + with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file: + dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) + + return (vrrp_groups, sync_groups) + + +def verify(data): + vrrp_groups, sync_groups = data + + for group in vrrp_groups: + # Check required fields + if not group["vrid"]: + raise ConfigError("vrid is required but not set in VRRP group {0}".format(group["name"])) + if not group["interface"]: + raise ConfigError("interface is required but not set in VRRP group {0}".format(group["name"])) + if not group["virtual_addresses"]: + raise ConfigError("virtual-address is required but not set in VRRP group {0}".format(group["name"])) + + if group["auth_password"] and (not group["auth_type"]): + raise ConfigError("authentication type is required but not set in VRRP group {0}".format(group["name"])) + + # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction + + # XXX: filter on map object is destructive, so we force it to list. + # Additionally, filter objects always evaluate to True, empty or not, + # so we force them to lists as well. + vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"])) + vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs)) + vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs)) + + if vaddrs4 and vaddrs6: + raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"])) + + if vaddrs4: + if group["hello_source"]: + hsa = ip_address(group["hello_source"]) + if isinstance(hsa, IPv6Address): + raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"])) + if group["peer_address"]: + pa = ip_address(group["peer_address"]) + if isinstance(pa, IPv6Address): + raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"])) + + if vaddrs6: + if group["hello_source"]: + hsa = ip_address(group["hello_source"]) + if isinstance(hsa, IPv4Address): + raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"])) + if group["peer_address"]: + pa = ip_address(group["peer_address"]) + if isinstance(pa, IPv4Address): + raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"])) + + # Disallow same VRID on multiple interfaces + _groups = sorted(vrrp_groups, key=(lambda x: x["interface"])) + count = len(_groups) - 1 + index = 0 + while (index < count): + if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]): + raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format( + _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"])) + else: + index += 1 + + # Check sync groups + vrrp_group_names = list(map(lambda x: x["name"], vrrp_groups)) + + for sync_group in sync_groups: + for m in sync_group["members"]: + if not (m in vrrp_group_names): + raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m)) + + +def generate(data): + vrrp_groups, sync_groups = data + + # Remove disabled groups from the sync group member lists + for sync_group in sync_groups: + for member in sync_group["members"]: + g = list(filter(lambda x: x["name"] == member, vrrp_groups))[0] + if g["disable"]: + print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"])) + # Filter out disabled groups + vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) + + render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', + {"groups": vrrp_groups, "sync_groups": sync_groups}) + render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {}) + return None + + +def apply(data): + vrrp_groups, sync_groups = data + if vrrp_groups: + # safely rename a temporary file with configuration dict + try: + dict_file = Path("{}.temp".format(VRRP.location['vyos'])) + dict_file.rename(Path(VRRP.location['vyos'])) + except Exception as err: + print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err)) + + if not VRRP.is_running(): + print("Starting the VRRP process") + ret = call("systemctl restart keepalived.service") + else: + print("Reloading the VRRP process") + ret = call("systemctl reload keepalived.service") + + if ret != 0: + raise ConfigError("keepalived failed to start") + else: + # VRRP is removed in the commit + print("Stopping the VRRP process") + call("systemctl stop keepalived.service") + os.unlink(VRRP.location['daemon']) + + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print("VRRP error: {0}".format(str(e))) + exit(1) diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py new file mode 100755 index 000000000..fb4644d5a --- /dev/null +++ b/src/conf_mode/vyos_cert.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import tempfile +import pathlib +import ssl + +import vyos.defaults +from vyos.config import Config +from vyos import ConfigError +from vyos.util import cmd + +from vyos import airbag +airbag.enable() + +vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] + +# XXX: this model will need to be extended for tag nodes +dependencies = [ + 'https.py', +] + +def status_self_signed(cert_data): +# check existence and expiration date + path = pathlib.Path(cert_data['conf']) + if not path.is_file(): + return False + path = pathlib.Path(cert_data['crt']) + if not path.is_file(): + return False + path = pathlib.Path(cert_data['key']) + if not path.is_file(): + return False + + # check if certificate is 1/2 past lifetime, with openssl -checkend + end_days = int(cert_data['lifetime']) + end_seconds = int(0.5*60*60*24*end_days) + checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data) + try: + cmd(checkend_cmd, message='Called process error') + return True + except OSError as err: + if err.errno == 1: + return False + print(err) + # XXX: This seems wrong to continue on failure + # implicitely returning None + +def generate_self_signed(cert_data): + san_config = None + + if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0): + san_config = tempfile.NamedTemporaryFile() + with open(san_config.name, 'w') as fd: + fd.write('[req]\n') + fd.write('distinguished_name=req\n') + fd.write('[san]\n') + fd.write('subjectAltName=DNS:vyos\n') + + openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} ' + '-newkey rsa:4096 -keyout {key} -out {crt} ' + '-subj "/O=Sentrium/OU=VyOS/CN=vyos" ' + '-extensions san -config {san_conf}' + ''.format(san_conf=san_config.name, + **cert_data)) + + else: + openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} ' + '-newkey rsa:4096 -keyout {key} -out {crt} ' + '-subj "/O=Sentrium/OU=VyOS/CN=vyos" ' + '-addext "subjectAltName=DNS:vyos"' + ''.format(**cert_data)) + + try: + cmd(openssl_req_cmd, message='Called process error') + except OSError as err: + print(err) + # XXX: seems wrong to ignore the failure + + os.chmod('{key}'.format(**cert_data), 0o400) + + with open('{conf}'.format(**cert_data), 'w') as f: + f.write('ssl_certificate {crt};\n'.format(**cert_data)) + f.write('ssl_certificate_key {key};\n'.format(**cert_data)) + + if san_config: + san_config.close() + +def get_config(): + vyos_cert = vyos.defaults.vyos_cert_data + + conf = Config() + if not conf.exists('service https certificates system-generated-certificate'): + return None + else: + conf.set_level('service https certificates system-generated-certificate') + + if conf.exists('lifetime'): + lifetime = conf.return_value('lifetime') + vyos_cert['lifetime'] = lifetime + + return vyos_cert + +def verify(vyos_cert): + return None + +def generate(vyos_cert): + if vyos_cert is None: + return None + + if not status_self_signed(vyos_cert): + generate_self_signed(vyos_cert) + +def apply(vyos_cert): + for dep in dependencies: + command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) + cmd(command, raising=ConfigError) + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/01-vyos-logging b/src/etc/dhcp/dhclient-enter-hooks.d/01-vyos-logging new file mode 100644 index 000000000..121fb21be --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/01-vyos-logging @@ -0,0 +1,20 @@ +# enable logging +LOG_ENABLE="yes" +LOG_STDERR="no" +LOG_TAG="dhclient-script-vyos" + +function logmsg () { + # log message to journal + case $1 in + error) LOG_PRIO="daemon.err" ;; + info) LOG_PRIO="daemon.info" ;; + esac + + if [ "${LOG_ENABLE}" == "yes" ] ; then + if [ "${LOG_STDERR}" == "yes" ] ; then + /usr/bin/logger -e --id=$$ -s -p ${LOG_PRIO} -t ${LOG_TAG} "${@:2}" + else + /usr/bin/logger -e --id=$$ -p ${LOG_PRIO} -t ${LOG_TAG} "${@:2}" + fi + fi +} diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient new file mode 100644 index 000000000..d5d90632c --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/02-vyos-stopdhclient @@ -0,0 +1,27 @@ +# skip all of this if dhclient-script running by stop command defined below +if [ -z ${CONTROLLED_STOP} ] ; then + # stop dhclient for this interface, if it is not current one + # get PID for current dhclient + current_dhclient=`ps --no-headers --format ppid --pid $$ | awk '{ print $1 }'` + + # get PID for master process (current can be a fork) + master_dhclient=`ps --no-headers --format ppid --pid $current_dhclient | awk '{ print $1 }'` + + # get IP version for current dhclient + ipversion_arg=`ps --no-headers --format args --pid $current_dhclient | awk '{ print $2 }'` + + # get list of all dhclient running for current interface + dhclients_pids=(`ps --no-headers --format pid,args -C dhclient | awk -v IFACE="/sbin/dhclient $ipversion_arg .*$interface$" '$0 ~ IFACE { print $1 }'`) + + logmsg info "Current dhclient PID: $current_dhclient, Parent PID: $master_dhclient, IP version: $ipversion_arg, All dhclients for interface $interface: ${dhclients_pids[@]}" + # stop all dhclients for current interface, except current one + for dhclient in ${dhclients_pids[@]}; do + if ([ $dhclient -ne $current_dhclient ] && [ $dhclient -ne $master_dhclient ]); then + logmsg info "Stopping dhclient with PID: ${dhclient}" + # get path to PID-file of dhclient process + local dhclient_pidfile=`ps --no-headers --format args --pid $dhclient | awk 'match($0, ".*-pf (/.*pid) .*", PF) { print PF[1] }'` + # stop dhclient with native command - this will run dhclient-script with correct reason unlike simple kill + dhclient -e CONTROLLED_STOP=yes -x -pf $dhclient_pidfile + fi + done +fi diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper new file mode 100644 index 000000000..d1161e704 --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -0,0 +1,87 @@ +# redefine ip command to use FRR when it is available + +# get status of FRR +function frr_alive () { + /usr/lib/frr/watchfrr.sh all_status + if [ "$?" -eq "0" ] ; then + logmsg info "FRR status: running" + return 0 + else + logmsg info "FRR status: not running" + return 1 + fi +} + +# convert ip route command to vtysh +function iptovtysh () { + # prepare variables for vtysh command + local VTYSH_DISTANCE="210" + local VTYSH_TAG="210" + local VTYSH_NETADDR="" + local VTYSH_GATEWAY="" + local VTYSH_DEV="" + # convert default route to 0.0.0.0/0 + if [ "$4" == "default" ] ; then + VTYSH_NETADDR="0.0.0.0/0" + else + VTYSH_NETADDR=$4 + fi + # add /32 to ip addresses without netmasks + if [[ ! $VTYSH_NETADDR =~ ^.*/[[:digit:]]+$ ]] ; then + VTYSH_NETADDR="$VTYSH_NETADDR/32" + fi + # get gateway address + if [ "$5" == "via" ] ; then + VTYSH_GATEWAY=$6 + fi + # get device name + if [ "$5" == "dev" ]; then + VTYSH_DEV=$6 + elif [ "$7" == "dev" ]; then + VTYSH_DEV=$8 + fi + + # Add route to VRF routing table + local VTYSH_VRF_NAME=$(basename /sys/class/net/$VTYSH_DEV/upper_* | sed -e 's/upper_//') + if [ -n $VTYSH_VRF_NAME ]; then + VTYSH_VRF="vrf $VTYSH_VRF_NAME" + fi + VTYSH_CMD="ip route $VTYSH_NETADDR $VTYSH_GATEWAY $VTYSH_DEV tag $VTYSH_TAG $VTYSH_DISTANCE $VTYSH_VRF" + + # delete route if the command is "del" + if [ "$3" == "del" ] ; then + VTYSH_CMD="no $VTYSH_CMD" + fi + logmsg info "Converted vtysh command: \"$VTYSH_CMD\"" +} + +# delete the same route from kernel before adding new one +function delroute () { + logmsg info "Checking if the route presented in kernel: $@" + if /usr/sbin/ip route show $@ | grep -qx "$1 " ; then + logmsg info "Deleting IP route: \"/usr/sbin/ip route del $@\"" + /usr/sbin/ip route del $@ + fi +} + +# replace ip command with this wrapper +function ip () { + # pass comand to system `ip` if this is not related to routes change + if [ "$2" != "route" ] ; then + logmsg info "Passing command to /usr/sbin/ip: \"$@\"" + /usr/sbin/ip $@ + else + # if we want to work with routes, try to use FRR first + if frr_alive ; then + delroute ${@:4} + iptovtysh $@ + logmsg info "Sending command to vtysh" + vtysh -c "conf t" -c "$VTYSH_CMD" + else + # add ip route to kernel + logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\"" + /usr/sbin/ip $@ + fi + fi +} + diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf new file mode 100644 index 000000000..24090e2a8 --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf @@ -0,0 +1,44 @@ +# modified make_resolv_conf () for VyOS +make_resolv_conf() { + hostsd_client="/usr/bin/vyos-hostsd-client" + hostsd_changes= + + if [ -n "$new_domain_name" ]; then + logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcp-$interface" + logmsg info "Adding domain name \"$new_domain_name\" as search domain with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --add-search-domains "$new_domain_name" --tag "dhcp-$interface" + hostsd_changes=y + fi + + if [ -n "$new_dhcp6_domain_search" ]; then + logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" + logmsg info "Adding search domain \"$new_dhcp6_domain_search\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --add-search-domains "$new_dhcp6_domain_search" --tag "dhcpv6-$interface" + hostsd_changes=y + fi + + if [ -n "$new_domain_name_servers" ]; then + logmsg info "Deleting nameservers with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcp-$interface" + logmsg info "Adding nameservers \"$new_domain_name_servers\" with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --add-name-servers $new_domain_name_servers --tag "dhcp-$interface" + hostsd_changes=y + fi + + if [ -n "$new_dhcp6_name_servers" ]; then + logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" + logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface" + hostsd_changes=y + fi + + if [ $hostsd_changes ]; then + logmsg info "Applying changes via vyos-hostsd-client" + $hostsd_client --apply + else + logmsg info "No changes to apply via vyos-hostsd-client" + fi +} diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/05-vyos-mtureplace b/src/etc/dhcp/dhclient-enter-hooks.d/05-vyos-mtureplace new file mode 100644 index 000000000..4a08765ba --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/05-vyos-mtureplace @@ -0,0 +1,38 @@ +# replace MTU with value from configuration + +# get MTU value via Python +# as configuration is not available to cli-shell-api at the first boot, we must use vyos.config, which contain workaround for this, instead clean shell +function get_mtu { +python3 - <<PYEND +from vyos.config import Config +import os +import re + +# check if interface is not VLAN and fix name if necessary +interface_name = os.getenv('interface', '') +regex_filter = re.compile('^(?P<interface>\w+)\.(?P<vid>\d+)$') +if regex_filter.search(interface_name): + iface = regex_filter.search(interface_name).group('interface') + vid = regex_filter.search(interface_name).group('vid') + interface_name = "{} vif {}".format(iface, vid) + +# initialize config +config = Config() +if config.exists('interfaces'): + iface_types = config.list_nodes('interfaces') + for iface_type in iface_types: + # check if configuration contain MTU value for interface and return (print) it + if config.exists("interfaces {} {} mtu".format(iface_type, interface_name)): + print(format(config.return_value("interfaces {} {} mtu".format(iface_type, interface_name)))) +PYEND +} + +# check if DHCP server return MTU value +if [ -n "$new_interface_mtu" ]; then + # try to get MTU from config and replace original one + configured_mtu="$(get_mtu)" + if [[ -n $configured_mtu ]] ; then + logmsg info "Replacing MTU value for $interface with preconfigured one: $configured_mtu" + new_interface_mtu="$configured_mtu" + fi +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup new file mode 100644 index 000000000..b768e1ae5 --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -0,0 +1,104 @@ +## +## VyOS cleanup +## +# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state +hostsd_client="/usr/bin/vyos-hostsd-client" +hostsd_changes= + +if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then + # delete search domains and nameservers via vyos-hostsd + logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcp-$interface" + logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcp-${interface}" + hostsd_changes=y + + # try to delete default ip route + for router in $old_routers; do + # check if we are bound to a VRF + local vrf_name=$(basename /sys/class/net/${interface}/upper_* | sed -e 's/upper_//') + if [ -n $vrf_name ]; then + vrf="vrf $vrf_name" + fi + + logmsg info "Deleting default route: via $router dev ${interface} ${vrf}" + ip -4 route del default via $router dev ${interface} ${vrf} + done + + # delete rfc3442 routes + if [ -n "$old_rfc3442_classless_static_routes" ]; then + set -- $old_rfc3442_classless_static_routes + while [ $# -gt 0 ]; do + net_length=$1 + via_arg='' + case $net_length in + 32|31|30|29|28|27|26|25) + if [ $# -lt 9 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.${5}" + gateway="${6}.${7}.${8}.${9}" + shift 9 + ;; + 24|23|22|21|20|19|18|17) + if [ $# -lt 8 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.0" + gateway="${5}.${6}.${7}.${8}" + shift 8 + ;; + 16|15|14|13|12|11|10|9) + if [ $# -lt 7 ]; then + return 1 + fi + net_address="${2}.${3}.0.0" + gateway="${4}.${5}.${6}.${7}" + shift 7 + ;; + 8|7|6|5|4|3|2|1) + if [ $# -lt 6 ]; then + return 1 + fi + net_address="${2}.0.0.0" + gateway="${3}.${4}.${5}.${6}" + shift 6 + ;; + 0) # default route + if [ $# -lt 5 ]; then + return 1 + fi + net_address="0.0.0.0" + gateway="${2}.${3}.${4}.${5}" + shift 5 + ;; + *) # error + return 1 + ;; + esac + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + fi + # delete route (ip detects host routes automatically) + ip -4 route del "${net_address}/${net_length}" \ + ${via_arg} dev "${interface}" >/dev/null 2>&1 + done + fi +fi + +if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then + # delete search domains and nameservers via vyos-hostsd + logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --delete-search-domains --tag "dhcpv6-$interface" + logmsg info "Deleting nameservers with tag \"dhcpv6-${interface}\" via vyos-hostsd-client" + $hostsd_client --delete-name-servers --tag "dhcpv6-${interface}" + hostsd_changes=y +fi + +if [ $hostsd_changes ]; then + logmsg info "Applying changes via vyos-hostsd-client" + $hostsd_client --apply +else + logmsg info "No changes to apply via vyos-hostsd-client" +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 new file mode 100644 index 000000000..9202fe72d --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 @@ -0,0 +1,148 @@ +# support for RFC3442 routes in DHCP RENEW + +function convert_to_cidr () { + cidr="" + set -- $1 + while [ $# -gt 0 ]; do + net_length=$1 + + case $net_length in + 32|31|30|29|28|27|26|25) + if [ $# -lt 9 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.${5}" + gateway="${6}.${7}.${8}.${9}" + shift 9 + ;; + 24|23|22|21|20|19|18|17) + if [ $# -lt 8 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.0" + gateway="${5}.${6}.${7}.${8}" + shift 8 + ;; + 16|15|14|13|12|11|10|9) + if [ $# -lt 7 ]; then + return 1 + fi + net_address="${2}.${3}.0.0" + gateway="${4}.${5}.${6}.${7}" + shift 7 + ;; + 8|7|6|5|4|3|2|1) + if [ $# -lt 6 ]; then + return 1 + fi + net_address="${2}.0.0.0" + gateway="${3}.${4}.${5}.${6}" + shift 6 + ;; + 0) # default route + if [ $# -lt 5 ]; then + return 1 + fi + net_address="0.0.0.0" + gateway="${2}.${3}.${4}.${5}" + shift 5 + ;; + *) # error + return 1 + ;; + esac + + cidr+="${net_address}/${net_length}:${gateway} " + done +} + +# main script starts here + +RUN="yes" + +if [ "$RUN" = "yes" ]; then + convert_to_cidr "$old_rfc3442_classless_static_routes" + old_cidr=$cidr + convert_to_cidr "$new_rfc3442_classless_static_routes" + new_cidr=$cidr + + if [ "$reason" = "RENEW" ]; then + if [ "$new_rfc3442_classless_static_routes" != "$old_rfc3442_classless_static_routes" ]; then + logmsg info "RFC3442 route change detected, old_routes: $old_rfc3442_classless_static_routes" + logmsg info "RFC3442 route change detected, new_routes: $new_rfc3442_classless_static_routes" + if [ -z "$new_rfc3442_classless_static_routes" ]; then + # delete all routes from the old_rfc3442_classless_static_routes + for route in $old_cidr; do + network=$(printf "${route}" | awk -F ":" '{print $1}') + gateway=$(printf "${route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + done + elif [ -z "$old_rfc3442_classless_static_routes" ]; then + # add all routes from the new_rfc3442_classless_static_routes + for route in $new_cidr; do + network=$(printf "${route}" | awk -F ":" '{print $1}') + gateway=$(printf "${route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + done + else + # update routes + # delete old + for old_route in $old_cidr; do + match="false" + for new_route in $new_cidr; do + if [[ "$old_route" == "$new_route" ]]; then + match="true" + break + fi + done + if [[ "$match" == "false" ]]; then + # delete old_route + network=$(printf "${old_route}" | awk -F ":" '{print $1}') + gateway=$(printf "${old_route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + fi + done + # add new + for new_route in $new_cidr; do + match="false" + for old_route in $old_cidr; do + if [[ "$new_route" == "$old_route" ]]; then + match="true" + break + fi + done + if [[ "$match" == "false" ]]; then + # add new_route + network=$(printf "${new_route}" | awk -F ":" '{print $1}') + gateway=$(printf "${new_route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + fi + done + fi + fi + fi +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook new file mode 100644 index 000000000..eeb8b0782 --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook @@ -0,0 +1,44 @@ +#!/bin/sh + +# Author: Stig Thormodsrud <stig@vyatta.com> +# Date: 2007 +# Description: dhcp client hook + +# **** License **** +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# This code was originally developed by Vyatta, Inc. +# Portions created by Vyatta are Copyright (C) 2006, 2007, 2008 Vyatta, Inc. +# All Rights Reserved. +# **** End License **** + +# To enable this script set the following variable to "yes" +RUN="yes" + +proto="" +if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then + proto="v6" +fi + +if [ "$RUN" = "yes" ]; then + LOG=/var/lib/dhcp/dhclient_"$interface"."$proto"lease + echo `date` > $LOG + + for i in reason interface new_expiry new_dhcp_lease_time medium \ + alias_ip_address new_ip_address new_broadcast_address \ + new_subnet_mask new_domain_name new_network_number \ + new_domain_name_servers new_routers new_static_routes \ + new_dhcp_server_identifier new_dhcp_message_type \ + old_ip_address old_subnet_mask old_domain_name \ + old_domain_name_servers old_routers \ + old_static_routes; do + echo $i=\'${!i}\' >> $LOG + done +fi diff --git a/src/etc/ppp/ip-pre-up b/src/etc/ppp/ip-pre-up new file mode 100755 index 000000000..05840650b --- /dev/null +++ b/src/etc/ppp/ip-pre-up @@ -0,0 +1,51 @@ +#!/bin/sh +# +# This script is run by the pppd when the link is created. +# It uses run-parts to run scripts in /etc/ppp/ip-pre-up.d, to +# change name, setup firewall,etc you should create script(s) there. +# +# Be aware that other packages may include /etc/ppp/ip-pre-up.d scripts (named +# after that package), so choose local script names with that in mind. +# +# This script is called with the following arguments: +# Arg Name Example +# $1 Interface name ppp0 +# $2 The tty ttyS1 +# $3 The link speed 38400 +# $4 Local IP number 12.34.56.78 +# $5 Peer IP number 12.34.56.99 +# $6 Optional ``ipparam'' value foo + +# The environment is cleared before executing this script +# so the path must be reset +PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin +export PATH + +# These variables are for the use of the scripts run by run-parts +PPP_IFACE="$1" +PPP_TTY="$2" +PPP_SPEED="$3" +PPP_LOCAL="$4" +PPP_REMOTE="$5" +PPP_IPPARAM="$6" +export PPP_IFACE PPP_TTY PPP_SPEED PPP_LOCAL PPP_REMOTE PPP_IPPARAM + +# as an additional convenience, $PPP_TTYNAME is set to the tty name, +# stripped of /dev/ (if present) for easier matching. +PPP_TTYNAME=`/usr/bin/basename "$2"` +export PPP_TTYNAME + +# If /var/log/ppp-ipupdown.log exists use it for logging. +if [ -e /var/log/ppp-ipupdown.log ]; then + exec > /var/log/ppp-ipupdown.log 2>&1 + echo $0 $* + echo +fi + +# This script can be used to override the .d files supplied by other packages. +if [ -x /etc/ppp/ip-pre-up.local ]; then + exec /etc/ppp/ip-pre-up.local "$*" +fi + +run-parts /etc/ppp/ip-pre-up.d \ + --arg="$1" --arg="$2" --arg="$3" --arg="$4" --arg="$5" --arg="$6" diff --git a/src/etc/rsyslog.d/01-auth.conf b/src/etc/rsyslog.d/01-auth.conf new file mode 100644 index 000000000..cc64099d6 --- /dev/null +++ b/src/etc/rsyslog.d/01-auth.conf @@ -0,0 +1,14 @@ +# The lines below cause all listed daemons/processes to be logged into +# /var/log/auth.log, then drops the message so it does not also go to the +# regular syslog so that messages are not duplicated + +$outchannel auth_log,/var/log/auth.log +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then :omfile:$auth_log + +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then stop diff --git a/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf new file mode 100644 index 000000000..07a0d1584 --- /dev/null +++ b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf @@ -0,0 +1,14 @@ +### Added by vyos-1x ### +# +# addr_gen_mode - INTEGER +# Defines how link-local and autoconf addresses are generated. +# +# 0: generate address based on EUI64 (default) +# 1: do no generate a link-local address, use EUI64 for addresses generated +# from autoconf +# 2: generate stable privacy addresses, using the secret from +# stable_secret (RFC7217) +# 3: generate stable privacy addresses, using a random secret if unset +# +net.ipv6.conf.all.addr_gen_mode = 1 +net.ipv6.conf.default.addr_gen_mode = 1 diff --git a/src/etc/systemd/system/LCDd.service.d/override.conf b/src/etc/systemd/system/LCDd.service.d/override.conf new file mode 100644 index 000000000..5f3f0dc95 --- /dev/null +++ b/src/etc/systemd/system/LCDd.service.d/override.conf @@ -0,0 +1,8 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/sbin/LCDd -c /run/LCDd/LCDd.conf + diff --git a/src/etc/systemd/system/conserver-server.service.d/override.conf b/src/etc/systemd/system/conserver-server.service.d/override.conf new file mode 100644 index 000000000..3c753f572 --- /dev/null +++ b/src/etc/systemd/system/conserver-server.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +After= +After=vyos-router.service +ConditionPathExists=/run/conserver/conserver.cf + +[Service] +Type=simple +ExecStart= +ExecStart=/usr/sbin/conserver -M localhost -C /run/conserver/conserver.cf +Restart=on-failure diff --git a/src/etc/systemd/system/hostapd@.service.d/override.conf b/src/etc/systemd/system/hostapd@.service.d/override.conf new file mode 100644 index 000000000..bb8e81d7a --- /dev/null +++ b/src/etc/systemd/system/hostapd@.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory=/run/hostapd +EnvironmentFile= +ExecStart= +ExecStart=/usr/sbin/hostapd -B -P /run/hostapd/%i.pid /run/hostapd/%i.conf +PIDFile=/run/hostapd/%i.pid diff --git a/src/etc/systemd/system/keepalived.service.d/override.conf b/src/etc/systemd/system/keepalived.service.d/override.conf new file mode 100644 index 000000000..9fcabf652 --- /dev/null +++ b/src/etc/systemd/system/keepalived.service.d/override.conf @@ -0,0 +1,2 @@ +[Service] +KillMode=process diff --git a/src/etc/systemd/system/ocserv.service.d/override.conf b/src/etc/systemd/system/ocserv.service.d/override.conf new file mode 100644 index 000000000..89dbb153f --- /dev/null +++ b/src/etc/systemd/system/ocserv.service.d/override.conf @@ -0,0 +1,14 @@ +[Unit] +RequiresMountsFor=/run +ConditionPathExists=/run/ocserv/ocserv.conf +After= +After=vyos-router.service +After=dbus.service + +[Service] +WorkingDirectory=/run/ocserv +PIDFile= +PIDFile=/run/ocserv/ocserv.pid +ExecStart= +ExecStart=/usr/sbin/ocserv --foreground --pid-file /run/ocserv/ocserv.pid --config /run/ocserv/ocserv.conf + diff --git a/src/etc/systemd/system/openvpn@.service.d/override.conf b/src/etc/systemd/system/openvpn@.service.d/override.conf new file mode 100644 index 000000000..7946484a3 --- /dev/null +++ b/src/etc/systemd/system/openvpn@.service.d/override.conf @@ -0,0 +1,9 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/openvpn +ExecStart= +ExecStart=/usr/sbin/openvpn --daemon openvpn-%i --config %i.conf --status %i.status 30 --writepid %i.pid diff --git a/src/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf new file mode 100644 index 000000000..158bac02b --- /dev/null +++ b/src/etc/systemd/system/pdns-recursor.service.d/override.conf @@ -0,0 +1,8 @@ +[Service] +WorkingDirectory= +WorkingDirectory=/run/powerdns +RuntimeDirectory= +RuntimeDirectory=powerdns +RuntimeDirectoryPreserve=yes +ExecStart= +ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns --socket-dir=/run/powerdns diff --git a/src/etc/systemd/system/radvd.service.d/override.conf b/src/etc/systemd/system/radvd.service.d/override.conf new file mode 100644 index 000000000..c2f640cf5 --- /dev/null +++ b/src/etc/systemd/system/radvd.service.d/override.conf @@ -0,0 +1,17 @@ +[Unit] +ConditionPathExists=/run/radvd/radvd.conf +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/radvd +ExecStartPre= +ExecStartPre=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf +ExecStart= +ExecStart=/usr/sbin/radvd --logmethod stderr_clean --config /run/radvd/radvd.conf --pidfile /run/radvd/radvd.pid +ExecReload= +ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/radvd/radvd.conf +ExecReload=/bin/kill -HUP $MAINPID +PIDFile= +PIDFile=/run/radvd/radvd.pid diff --git a/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf new file mode 100644 index 000000000..a895e675f --- /dev/null +++ b/src/etc/systemd/system/wpa_supplicant@.service.d/override.conf @@ -0,0 +1,10 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/wpa_supplicant +PIDFile=/run/wpa_supplicant/%I.pid +ExecStart= +ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dnl80211,wext -i%I diff --git a/src/etc/udev/rules.d/90-vyos-serial.rules b/src/etc/udev/rules.d/90-vyos-serial.rules new file mode 100644 index 000000000..3f10f4924 --- /dev/null +++ b/src/etc/udev/rules.d/90-vyos-serial.rules @@ -0,0 +1,28 @@ +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="serial_end" +SUBSYSTEM!="tty", GOTO="serial_end" + +SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}" +SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci" +SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb" + +# /dev/serial/by-path/, /dev/serial/by-id/ for USB devices +KERNEL!="ttyUSB[0-9]*|ttyACM[0-9]*", GOTO="serial_end" + +SUBSYSTEMS=="usb-serial", ENV{.ID_PORT}="$attr{port_number}" + +IMPORT{builtin}="path_id", IMPORT{builtin}="usb_id" + +# Change the name of the usb id to a "more" human redable format. +# +# - $env{ID_PATH} usually is a name like: "pci-0000:00:10.0-usb-0:2.3.3.4:1.0-port0" so we strip the "pci-*" +# portion and only use the usb part +# - Transform the USB "speach" to the tree like structure so we start with "usb0" as root-complex 0. +# (tr -d -) does the replacement +# - Replace the first group after ":" to represent the bus relation (sed -e 0,/:/s//b/) indicated by "b" +# - Replace the next group after ":" to represent the port relation (sed -e 0,/:/s//p/) indicated by "p" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", PROGRAM="/bin/sh -c 'D=$env{ID_PATH}; echo ${D:17} | tr -d - | sed -e 0,/:/s//b/ | sed -e 0,/:/s//p/'", SYMLINK+="serial/by-bus/$result" + +LABEL="serial_end" diff --git a/src/etc/udev/rules.d/99-vyos-wwan.rules b/src/etc/udev/rules.d/99-vyos-wwan.rules new file mode 100644 index 000000000..67f30a3dd --- /dev/null +++ b/src/etc/udev/rules.d/99-vyos-wwan.rules @@ -0,0 +1,11 @@ +ACTION!="add|change", GOTO="mbim_to_qmi_rules_end" + +SUBSYSTEM!="usb", GOTO="mbim_to_qmi_rules_end" + +# ignore any device with only one configuration +ATTR{bNumConfigurations}=="1", GOTO="mbim_to_qmi_rules_end" + +# force Sierra Wireless MC7710 to configuration #1 +ATTR{idVendor}=="1199",ATTR{idProduct}=="68a2",ATTR{bConfigurationValue}="1" + +LABEL="mbim_to_qmi_rules_end" diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py new file mode 100755 index 000000000..dc751c45c --- /dev/null +++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import syslog as sl + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import run + + +def get_config(): + c = Config() + interfaces = dict() + for intf in c.list_effective_nodes('interfaces ethernet'): + # skip interfaces that are disabled or is configured for dhcp + check_disable = "interfaces ethernet {} disable".format(intf) + check_dhcp = "interfaces ethernet {} address dhcp".format(intf) + if c.exists_effective(check_disable): + continue + + # get addresses configured on the interface + intf_addresses = c.return_effective_values( + "interfaces ethernet {} address".format(intf) + ) + interfaces[intf] = [addr.strip("'") for addr in intf_addresses] + return interfaces + + +def apply(config): + for intf, addresses in config.items(): + # bring the interface up + cmd = ["ip", "link", "set", "dev", intf, "up"] + sl.syslog(sl.LOG_NOTICE, " ".join(cmd)) + run(cmd) + + # add configured addresses to interface + for addr in addresses: + if addr == "dhcp": + cmd = ["dhclient", intf] + else: + cmd = ["ip", "address", "add", addr, "dev", intf] + sl.syslog(sl.LOG_NOTICE, " ".join(cmd)) + run(cmd) + + +if __name__ == '__main__': + try: + config = get_config() + apply(config) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py new file mode 100755 index 000000000..cc7166c22 --- /dev/null +++ b/src/helpers/run-config-migration.py @@ -0,0 +1,86 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import argparse +import datetime + +from vyos.util import cmd +from vyos.migrator import Migrator, VirtualMigrator + +def main(): + argparser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter) + argparser.add_argument('config_file', type=str, + help="configuration file to migrate") + argparser.add_argument('--force', action='store_true', + help="Force calling of all migration scripts.") + argparser.add_argument('--set-vintage', type=str, + choices=['vyatta', 'vyos'], + help="Set the format for the config version footer in config" + " file:\n" + "set to 'vyatta':\n" + "(for '/* === vyatta-config-version ... */' format)\n" + "or 'vyos':\n" + "(for '// vyos-config-version ...' format).") + argparser.add_argument('--virtual', action='store_true', + help="Update the format of the trailing comments in" + " config file,\nfrom 'vyatta' to 'vyos'; no migration" + " scripts are run.") + args = argparser.parse_args() + + config_file_name = args.config_file + force_on = args.force + vintage = args.set_vintage + virtual = args.virtual + + if not os.access(config_file_name, os.R_OK): + print("Read error: {}.".format(config_file_name)) + sys.exit(1) + + if not os.access(config_file_name, os.W_OK): + print("Write error: {}.".format(config_file_name)) + sys.exit(1) + + separator = "." + backup_file_name = separator.join([config_file_name, + '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()), + 'pre-migration']) + + cmd(f'cp -p {config_file_name} {backup_file_name}') + + if not virtual: + virtual_migration = VirtualMigrator(config_file_name) + virtual_migration.run() + + migration = Migrator(config_file_name, force=force_on) + migration.run() + + if not migration.config_changed(): + os.remove(backup_file_name) + else: + virtual_migration = VirtualMigrator(config_file_name, + set_vintage=vintage) + + virtual_migration.run() + + if not virtual_migration.config_changed(): + os.remove(backup_file_name) + +if __name__ == '__main__': + main() diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py new file mode 100755 index 000000000..c33e41d79 --- /dev/null +++ b/src/helpers/system-versions-foot.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import sys +import vyos.formatversions as formatversions +import vyos.systemversions as systemversions +import vyos.defaults +import vyos.version + +sys_versions = systemversions.get_system_versions() + +component_string = formatversions.format_versions_string(sys_versions) + +os_version_string = vyos.version.get_version() + +sys.stdout.write("\n\n") +if vyos.defaults.cfg_vintage == 'vyos': + formatversions.write_vyos_versions_foot(None, component_string, + os_version_string) +elif vyos.defaults.cfg_vintage == 'vyatta': + formatversions.write_vyatta_versions_foot(None, component_string, + os_version_string) +else: + formatversions.write_vyatta_versions_foot(None, component_string, + os_version_string) diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py new file mode 100755 index 000000000..c5bf22f10 --- /dev/null +++ b/src/helpers/vyos-boot-config-loader.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import sys +import pwd +import grp +import traceback +from datetime import datetime + +from vyos.defaults import directories +from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configtree import ConfigTree +from vyos.util import cmd + +STATUS_FILE = '/tmp/vyos-config-status' +TRACE_FILE = '/tmp/boot-config-trace' + +CFG_GROUP = 'vyattacfg' + +trace_config = False + +if 'log' in directories: + LOG_DIR = directories['log'] +else: + LOG_DIR = '/var/log/vyatta' + +LOG_FILE = LOG_DIR + '/vyos-boot-config-loader.log' + +try: + with open('/proc/cmdline', 'r') as f: + cmdline = f.read() + if 'vyos-debug' in cmdline: + os.environ['VYOS_DEBUG'] = 'yes' + if 'vyos-config-debug' in cmdline: + os.environ['VYOS_DEBUG'] = 'yes' + trace_config = True +except Exception as e: + print('{0}'.format(e)) + +def write_config_status(status): + try: + with open(STATUS_FILE, 'w') as f: + f.write('{0}\n'.format(status)) + except Exception as e: + print('{0}'.format(e)) + +def trace_to_file(trace_file_name): + try: + with open(trace_file_name, 'w') as trace_file: + traceback.print_exc(file=trace_file) + except Exception as e: + print('{0}'.format(e)) + +def failsafe(config_file_name): + fail_msg = """ + !!!!! + There were errors loading the configuration + Please examine the errors in + {0} + and correct + !!!!! + """.format(TRACE_FILE) + + print(fail_msg, file=sys.stderr) + + users = [x[0] for x in pwd.getpwall()] + if 'vyos' in users: + return + + try: + with open(config_file_name, 'r') as f: + config_file = f.read() + except Exception as e: + print("Catastrophic: no default config file " + "'{0}'".format(config_file_name)) + sys.exit(1) + + config = ConfigTree(config_file) + if not config.exists(['system', 'login', 'user', 'vyos', + 'authentication', 'encrypted-password']): + print("No password entry in default config file;") + print("unable to recover password for user 'vyos'.") + sys.exit(1) + else: + passwd = config.return_value(['system', 'login', 'user', 'vyos', + 'authentication', + 'encrypted-password']) + + cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos") + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Must specify boot config file.") + sys.exit(1) + else: + file_name = sys.argv[1] + + # Set user and group options, so that others will be able to commit + # Currently, the only caller does 'sg CFG_GROUP', but that may change + cfg_group = grp.getgrnam(CFG_GROUP) + os.setgid(cfg_group.gr_gid) + + # Need to set file permissions to 775 so that every vyattacfg group + # member has write access to the running config + os.umask(0o002) + + session = ConfigSession(os.getpid(), 'vyos-boot-config-loader') + env = session.get_session_env() + + default_file_name = env['vyatta_sysconfdir'] + '/config.boot.default' + + try: + with open(file_name, 'r') as f: + config_file = f.read() + except Exception: + write_config_status(1) + if trace_config: + failsafe(default_file_name) + trace_to_file(TRACE_FILE) + sys.exit(1) + + try: + time_begin_load = datetime.now() + load_out = session.load_config(file_name) + time_end_load = datetime.now() + time_begin_commit = datetime.now() + commit_out = session.commit() + time_end_commit = datetime.now() + write_config_status(0) + except ConfigSessionError: + # If here, there is no use doing session.discard, as we have no + # recoverable config environment, and will only throw an error + write_config_status(1) + if trace_config: + failsafe(default_file_name) + trace_to_file(TRACE_FILE) + sys.exit(1) + + time_elapsed_load = time_end_load - time_begin_load + time_elapsed_commit = time_end_commit - time_begin_commit + + try: + if not os.path.exists(LOG_DIR): + os.mkdir(LOG_DIR) + with open(LOG_FILE, 'a') as f: + f.write('\n\n') + f.write('{0} Begin config load\n' + ''.format(time_begin_load)) + f.write(load_out) + f.write('{0} End config load\n' + ''.format(time_end_load)) + f.write('Elapsed time for config load: {0}\n' + ''.format(time_elapsed_load)) + f.write('{0} Begin config commit\n' + ''.format(time_begin_commit)) + f.write(commit_out) + f.write('{0} End config commit\n' + ''.format(time_end_commit)) + f.write('Elapsed time for config commit: {0}\n' + ''.format(time_elapsed_commit)) + except Exception as e: + print('{0}'.format(e)) diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py new file mode 100755 index 000000000..097d28d85 --- /dev/null +++ b/src/helpers/vyos-bridge-sync.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# Script is used to synchronize configured bridge interfaces. +# one can add a non existing interface to a bridge group (e.g. VLAN) +# but the vlan interface itself does yet not exist. It should be added +# to the bridge automatically once it's available + +import argparse +from sys import exit +from time import sleep + +from vyos.config import Config +from vyos.util import cmd, run + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True) + args, unknownargs = parser.parse_known_args() + + conf = Config() + if not conf.list_nodes('interfaces bridge'): + # no bridge interfaces exist .. bail out early + exit(0) + else: + for bridge in conf.list_nodes('interfaces bridge'): + for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): + if args.interface == member_if: + command = 'brctl addif "{}" "{}"'.format(bridge, args.interface) + # let interfaces etc. settle - especially required for OpenVPN bridged interfaces + sleep(4) + # XXX: This is ignoring any issue, should be cmd but kept as it + # XXX: during the migration to not cause any regression + run(command) + + exit(0) diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py new file mode 100755 index 000000000..c2da1bb11 --- /dev/null +++ b/src/helpers/vyos-load-config.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +"""Load config file from within config session. +Config file specified by URI or path (without scheme prefix). +Example: load https://somewhere.net/some.config + or + load /tmp/some.config +""" + +import sys +import tempfile +import vyos.defaults +import vyos.remote +from vyos.configsource import ConfigSourceSession, VyOSError +from vyos.migrator import Migrator, VirtualMigrator, MigratorError + +class LoadConfig(ConfigSourceSession): + """A subclass for calling 'loadFile'. + This does not belong in configsource.py, and only has a single caller. + """ + def load_config(self, path): + return self._run(['/bin/cli-shell-api','loadFile',path]) + + +file_name = sys.argv[1] if len(sys.argv) > 1 else 'config.boot' +configdir = vyos.defaults.directories['config'] +protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + + +if any(x in file_name for x in protocols): + config_file = vyos.remote.get_remote_config(file_name) + if not config_file: + sys.exit("No config file by that name.") +else: + canonical_path = '{0}/{1}'.format(configdir, file_name) + try: + with open(canonical_path, 'r') as f: + config_file = f.read() + except OSError as err1: + try: + with open(file_name, 'r') as f: + config_file = f.read() + except OSError as err2: + sys.exit('{0}\n{1}'.format(err1, err2)) + +config = LoadConfig() + +print("Loading configuration from '{}'".format(file_name)) + +with tempfile.NamedTemporaryFile() as fp: + with open(fp.name, 'w') as fd: + fd.write(config_file) + + virtual_migration = VirtualMigrator(fp.name) + try: + virtual_migration.run() + except MigratorError as err: + sys.exit('{}'.format(err)) + + migration = Migrator(fp.name) + try: + migration.run() + except MigratorError as err: + sys.exit('{}'.format(err)) + + try: + config.load_config(fp.name) + except VyOSError as err: + sys.exit('{}'.format(err)) + +if config.session_changed(): + print("Load complete. Use 'commit' to make changes effective.") +else: + print("No configuration changes to commit.") diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py new file mode 100755 index 000000000..14df2734b --- /dev/null +++ b/src/helpers/vyos-merge-config.py @@ -0,0 +1,111 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import sys +import os +import tempfile +import vyos.defaults +import vyos.remote +from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.migrator import Migrator, VirtualMigrator +from vyos.util import cmd, DEVNULL + + +if (len(sys.argv) < 2): + print("Need config file name to merge.") + print("Usage: merge <config file> [config path]") + sys.exit(0) + +file_name = sys.argv[1] + +configdir = vyos.defaults.directories['config'] + +protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + +if any(x in file_name for x in protocols): + config_file = vyos.remote.get_remote_config(file_name) + if not config_file: + sys.exit("No config file by that name.") +else: + canonical_path = "{0}/{1}".format(configdir, file_name) + first_err = None + try: + with open(canonical_path, 'r') as f: + config_file = f.read() + except Exception as err: + first_err = err + try: + with open(file_name, 'r') as f: + config_file = f.read() + except Exception as err: + print(first_err) + print(err) + sys.exit(1) + +with tempfile.NamedTemporaryFile() as file_to_migrate: + with open(file_to_migrate.name, 'w') as fd: + fd.write(config_file) + + virtual_migration = VirtualMigrator(file_to_migrate.name) + virtual_migration.run() + + migration = Migrator(file_to_migrate.name) + migration.run() + + if virtual_migration.config_changed() or migration.config_changed(): + with open(file_to_migrate.name, 'r') as fd: + config_file = fd.read() + +merge_config_tree = ConfigTree(config_file) + +effective_config = Config() +effective_config_tree = effective_config._running_config + +effective_cmds = effective_config_tree.to_commands() +merge_cmds = merge_config_tree.to_commands() + +effective_cmd_list = effective_cmds.splitlines() +merge_cmd_list = merge_cmds.splitlines() + +effective_cmd_set = set(effective_cmd_list) +add_cmds = [ cmd for cmd in merge_cmd_list if cmd not in effective_cmd_set ] + +path = None +if (len(sys.argv) > 2): + path = sys.argv[2:] + if (not effective_config_tree.exists(path) and not + merge_config_tree.exists(path)): + print("path {} does not exist in either effective or merge" + " config; will use root.".format(path)) + path = None + else: + path = " ".join(path) + +if path: + add_cmds = [ cmd for cmd in add_cmds if path in cmd ] + +for add in add_cmds: + try: + cmd(f'/opt/vyatta/sbin/my_{add}', shell=True, stderr=DEVNULL) + except OSError as err: + print(err) + +if effective_config.session_changed(): + print("Merge complete. Use 'commit' to make changes effective.") +else: + print("No configuration changes to commit.") diff --git a/src/helpers/vyos-sudo.py b/src/helpers/vyos-sudo.py new file mode 100755 index 000000000..3e4c196d9 --- /dev/null +++ b/src/helpers/vyos-sudo.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys + +from vyos.util import is_admin + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print('Missing command argument') + sys.exit(1) + + if not is_admin(): + print('This account is not authorized to run this command') + sys.exit(1) + + os.execvp('sudo', ['sudo'] + sys.argv[1:]) diff --git a/src/migration-scripts/config-management/0-to-1 b/src/migration-scripts/config-management/0-to-1 new file mode 100755 index 000000000..344359110 --- /dev/null +++ b/src/migration-scripts/config-management/0-to-1 @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +# Add commit-revisions option if it doesn't exist + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if config.exists(['system', 'config-management', 'commit-revisions']): + # Nothing to do + sys.exit(0) +else: + config.set(['system', 'config-management', 'commit-revisions'], value='200') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/dhcp-relay/1-to-2 b/src/migration-scripts/dhcp-relay/1-to-2 new file mode 100755 index 000000000..b72da1028 --- /dev/null +++ b/src/migration-scripts/dhcp-relay/1-to-2 @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +# Delete "set service dhcp-relay relay-options port" option +# Delete "set service dhcpv6-relay listen-port" option + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not (config.exists(['service', 'dhcp-relay', 'relay-options', 'port']) or config.exists(['service', 'dhcpv6-relay', 'listen-port'])): + # Nothing to do + sys.exit(0) +else: + # Delete abandoned node + config.delete(['service', 'dhcp-relay', 'relay-options', 'port']) + # Delete abandoned node + config.delete(['service', 'dhcpv6-relay', 'listen-port']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/dhcp-server/4-to-5 b/src/migration-scripts/dhcp-server/4-to-5 new file mode 100755 index 000000000..313b5279a --- /dev/null +++ b/src/migration-scripts/dhcp-server/4-to-5 @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +# Removes boolean operator from: +# - "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable (true|false)" +# - "set service dhcp-server shared-network-name <xyz> authoritative (true|false)" +# - "set service dhcp-server disabled (true|false)" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['service', 'dhcp-server']): + # Nothing to do + sys.exit(0) +else: + base = ['service', 'dhcp-server'] + # Make node "set service dhcp-server dynamic-dns-update enable (true|false)" valueless + if config.exists(base + ['dynamic-dns-update']): + bool_val = config.return_value(base + ['dynamic-dns-update', 'enable']) + + # Delete the node with the old syntax + config.delete(base + ['dynamic-dns-update']) + if str(bool_val) == 'true': + # Enable dynamic-dns-update with new syntax + config.set(base + ['dynamic-dns-update'], value=None) + + # Make node "set service dhcp-server disabled (true|false)" valueless + if config.exists(base + ['disabled']): + bool_val = config.return_value(base + ['disabled']) + + # Delete the node with the old syntax + config.delete(base + ['disabled']) + if str(bool_val) == 'true': + # Now disable DHCP server with the new syntax + config.set(base + ['disable'], value=None) + + # Make node "set service dhcp-server hostfile-update (enable|disable) valueless + if config.exists(base + ['hostfile-update']): + bool_val = config.return_value(base + ['hostfile-update']) + + # Delete the node with the old syntax incl. all subnodes + config.delete(base + ['hostfile-update']) + if str(bool_val) == 'enable': + # Enable hostfile update with new syntax + config.set(base + ['hostfile-update'], value=None) + + # Run this for every instance if 'shared-network-name' + for network in config.list_nodes(base + ['shared-network-name']): + base_network = base + ['shared-network-name', network] + # format as tag node to avoid loading problems + config.set_tag(base + ['shared-network-name']) + + # Run this for every specified 'subnet' + for subnet in config.list_nodes(base_network + ['subnet']): + base_subnet = base_network + ['subnet', subnet] + # format as tag node to avoid loading problems + config.set_tag(base_network + ['subnet']) + + # Make node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 ip-forwarding enable" valueless + if config.exists(base_subnet + ['ip-forwarding', 'enable']): + bool_val = config.return_value(base_subnet + ['ip-forwarding', 'enable']) + # Delete the node with the old syntax + config.delete(base_subnet + ['ip-forwarding']) + if str(bool_val) == 'true': + # Recreate node with new syntax + config.set(base_subnet + ['ip-forwarding'], value=None) + + # Rename node "set service dhcp-server shared-network-name <xyz> subnet 172.31.0.0/24 start <172.16.0.4> stop <172.16.0.9> + if config.exists(base_subnet + ['start']): + # This is the new "range" id for DHCP lease ranges + r_id = 0 + for range in config.list_nodes(base_subnet + ['start']): + range_start = range + range_stop = config.return_value(base_subnet + ['start', range_start, 'stop']) + + # Delete the node with the old syntax + config.delete(base_subnet + ['start', range_start]) + + # Create the node for the new syntax + # Note: range is a tag node, counter is its child, not a value + config.set(base_subnet + ['range', r_id]) + config.set(base_subnet + ['range', r_id, 'start'], value=range_start) + config.set(base_subnet + ['range', r_id, 'stop'], value=range_stop) + + # format as tag node to avoid loading problems + config.set_tag(base_subnet + ['range']) + + # increment range id for possible next range definition + r_id += 1 + + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'subnet', subnet, 'start']) + + + # Make node "set service dhcp-server shared-network-name <xyz> authoritative" valueless + if config.exists(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']): + authoritative = config.return_value(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + # Recreate node with new syntax - if required + if authoritative == "enable": + config.set(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/dhcpv6-server/0-to-1 b/src/migration-scripts/dhcpv6-server/0-to-1 new file mode 100755 index 000000000..6f1150da1 --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/0-to-1 @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# combine both sip-server-address and sip-server-name nodes to common sip-server + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'dhcpv6-server', 'shared-network-name'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + # we need to run this for every configured network + for network in config.list_nodes(base): + for subnet in config.list_nodes(base + [network, 'subnet']): + sip_server = [] + + # Do we have 'sip-server-address' configured? + if config.exists(base + [network, 'subnet', subnet, 'sip-server-address']): + sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-address']) + config.delete(base + [network, 'subnet', subnet, 'sip-server-address']) + + # Do we have 'sip-server-name' configured? + if config.exists(base + [network, 'subnet', subnet, 'sip-server-name']): + sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-name']) + config.delete(base + [network, 'subnet', subnet, 'sip-server-name']) + + # Write new CLI value for sip-server + for server in sip_server: + config.set(base + [network, 'subnet', subnet, 'sip-server'], value=server, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/dns-forwarding/0-to-1 b/src/migration-scripts/dns-forwarding/0-to-1 new file mode 100755 index 000000000..6e8720eef --- /dev/null +++ b/src/migration-scripts/dns-forwarding/0-to-1 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# This migration script will check if there is a allow-from directive configured +# for the dns forwarding service - if not, the node will be created with the old +# default values of 0.0.0.0/0 and ::/0 + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if not config.exists(base + ['allow-from']): + config.set(base + ['allow-from'], value='0.0.0.0/0', replace=False) + config.set(base + ['allow-from'], value='::/0', replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 new file mode 100755 index 000000000..8c4f4b5c7 --- /dev/null +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# This migration script will remove the deprecated 'listen-on' statement +# from the dns forwarding service and will add the corresponding +# listen-address nodes instead. This is required as PowerDNS can only listen +# on interface addresses and not on interface names. + +from ipaddress import ip_interface +from sys import argv, exit +from vyos.ifconfig import Interface +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['listen-on']): + listen_intf = config.return_values(base + ['listen-on']) + # Delete node with abandoned command + config.delete(base + ['listen-on']) + + # retrieve interface addresses for every configured listen-on interface + listen_addr = [] + for intf in listen_intf: + # we need to evaluate the interface section before manipulating the 'intf' variable + section = Interface.section(intf) + if not section: + raise ValueError(f'Invalid interface name {intf}') + + # we need to treat vif and vif-s interfaces differently, + # both "real interfaces" use dots for vlan identifiers - those + # need to be exchanged with vif and vif-s identifiers + if intf.count('.') == 1: + # this is a regular VLAN interface + intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] + elif intf.count('.') == 2: + # this is a QinQ VLAN interface + intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] + + # retrieve corresponding interface addresses in CIDR format + # those need to be converted in pure IP addresses without network information + path = ['interfaces', section, intf, 'address'] + for addr in config.return_values(path): + listen_addr.append( ip_interface(addr).ip ) + + for addr in listen_addr: + config.set(base + ['listen-address'], value=addr, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) + +exit(0) diff --git a/src/migration-scripts/dns-forwarding/2-to-3 b/src/migration-scripts/dns-forwarding/2-to-3 new file mode 100755 index 000000000..01e445b22 --- /dev/null +++ b/src/migration-scripts/dns-forwarding/2-to-3 @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# Sets the new options "addnta" and "recursion-desired" for all +# 'dns forwarding domain' as this is usually desired + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding'] +if not config.exists(base): + # Nothing to do + sys.exit(0) + +if config.exists(base + ['domain']): + for domain in config.list_nodes(base + ['domain']): + domain_base = base + ['domain', domain] + config.set(domain_base + ['addnta']) + config.set(domain_base + ['recursion-desired']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/https/0-to-1 b/src/migration-scripts/https/0-to-1 new file mode 100755 index 000000000..23809f5ad --- /dev/null +++ b/src/migration-scripts/https/0-to-1 @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# * Move server block directives under 'virtual-host' tag node, instead of +# relying on 'listen-address' tag node + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +old_base = ['service', 'https', 'listen-address'] +if not config.exists(old_base): + # Nothing to do + sys.exit(0) +else: + new_base = ['service', 'https', 'virtual-host'] + config.set(new_base) + config.set_tag(new_base) + + index = 0 + for addr in config.list_nodes(old_base): + tag_name = f'vhost{index}' + config.set(new_base + [tag_name]) + config.set(new_base + [tag_name, 'listen-address'], value=addr) + + if config.exists(old_base + [addr, 'listen-port']): + port = config.return_value(old_base + [addr, 'listen-port']) + config.set(new_base + [tag_name, 'listen-port'], value=port) + + if config.exists(old_base + [addr, 'server-name']): + names = config.return_values(old_base + [addr, 'server-name']) + for name in names: + config.set(new_base + [tag_name, 'server-name'], value=name, + replace=False) + + index += 1 + + config.delete(old_base) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/https/1-to-2 b/src/migration-scripts/https/1-to-2 new file mode 100755 index 000000000..b1cf37ea6 --- /dev/null +++ b/src/migration-scripts/https/1-to-2 @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# * Move 'api virtual-host' list to 'api-restrict virtual-host' so it +# is owned by https.py instead of http-api.py + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +old_base = ['service', 'https', 'api', 'virtual-host'] +if not config.exists(old_base): + # Nothing to do + sys.exit(0) +else: + new_base = ['service', 'https', 'api-restrict', 'virtual-host'] + config.set(new_base) + + names = config.return_values(old_base) + for name in names: + config.set(new_base, value=name, replace=False) + + config.delete(old_base) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 new file mode 100755 index 000000000..ee4d6b82c --- /dev/null +++ b/src/migration-scripts/interfaces/0-to-1 @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +# Change syntax of bridge interface +# - move interface based bridge-group to actual bridge (de-nest) +# - make stp and igmp-snooping nodes valueless +# https://phabricator.vyos.net/T1556 + +import sys +from vyos.configtree import ConfigTree + +def migrate_bridge(config, tree, intf): + # check if bridge-group exists + tree_bridge = tree + ['bridge-group'] + if config.exists(tree_bridge): + bridge = config.return_value(tree_bridge + ['bridge']) + # create new bridge member interface + config.set(base + [bridge, 'member', 'interface', intf]) + # format as tag node to avoid loading problems + config.set_tag(base + [bridge, 'member', 'interface']) + + # cost: migrate if configured + tree_cost = tree + ['bridge-group', 'cost'] + if config.exists(tree_cost): + cost = config.return_value(tree_cost) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost) + + # priority: migrate if configured + tree_priority = tree + ['bridge-group', 'priority'] + if config.exists(tree_priority): + priority = config.return_value(tree_priority) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority) + + # Delete the old bridge-group assigned to an interface + config.delete(tree_bridge) + + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + + file_name = sys.argv[1] + + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'bridge'] + + if not config.exists(base): + # Nothing to do + sys.exit(0) + else: + # + # make stp and igmp-snooping nodes valueless + # + for br in config.list_nodes(base): + # STP: check if enabled + if config.exists(base + [br, 'stp']): + stp_val = config.return_value(base + [br, 'stp']) + # STP: delete node with old syntax + config.delete(base + [br, 'stp']) + # STP: set new node - if enabled + if stp_val == "true": + config.set(base + [br, 'stp'], value=None) + + # igmp-snooping: check if enabled + if config.exists(base + [br, 'igmp-snooping', 'querier']): + igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: delete node with old syntax + config.delete(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: set new node - if enabled + if igmp_val == "enable": + config.set(base + [br, 'igmp', 'querier'], value=None) + + # + # move interface based bridge-group to actual bridge (de-nest) + # + bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless'] + for type in bridge_types: + if not config.exists(['interfaces', type]): + continue + + for interface in config.list_nodes(['interfaces', type]): + # check if bridge-group exists + bridge_group = ['interfaces', type, interface] + if config.exists(bridge_group + ['bridge-group']): + migrate_bridge(config, bridge_group, interface) + + # We also need to migrate VLAN interfaces + vlan_base = ['interfaces', type, interface, 'vif'] + if config.exists(vlan_base): + for vlan in config.list_nodes(vlan_base): + intf = "{}.{}".format(interface, vlan) + migrate_bridge(config, vlan_base + [vlan], intf) + + # And then we have service VLANs (vif-s) interfaces + vlan_base = ['interfaces', type, interface, 'vif-s'] + if config.exists(vlan_base): + for vif_s in config.list_nodes(vlan_base): + intf = "{}.{}".format(interface, vif_s) + migrate_bridge(config, vlan_base + [vif_s], intf) + + # Every service VLAN can have multiple customer VLANs (vif-c) + vlan_c = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] + if config.exists(vlan_c): + for vif_c in config.list_nodes(vlan_c): + intf = "{}.{}.{}".format(interface, vif_s, vif_c) + migrate_bridge(config, vlan_c + [vif_c], intf) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2 new file mode 100755 index 000000000..050137318 --- /dev/null +++ b/src/migration-scripts/interfaces/1-to-2 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 + +# Change syntax of bond interface +# - move interface based bond-group to actual bond (de-nest) +# https://phabricator.vyos.net/T1614 + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'bonding'] + +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # + # move interface based bond-group to actual bond (de-nest) + # + for intf in config.list_nodes(['interfaces', 'ethernet']): + # check if bond-group exists + if config.exists(['interfaces', 'ethernet', intf, 'bond-group']): + # get configured bond interface + bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group']) + # delete old interface asigned (nested) bond group + config.delete(['interfaces', 'ethernet', intf, 'bond-group']) + # create new bond member interface + config.set(base + [bond, 'member', 'interface'], value=intf, replace=False) + + # + # some combinations were allowed in the past from a CLI perspective + # but the kernel overwrote them - remove from CLI to not confuse the users. + # In addition new consitency checks are in place so users can't repeat the + # mistake. One of those nice issues is https://phabricator.vyos.net/T532 + for bond in config.list_nodes(base): + if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']): + mode = config.return_value(base + [bond, 'mode']) + if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']: + intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval'])) + if intvl > 0: + # this is not allowed and the linux kernel replies with: + # option arp_interval: mode dependency failed, not supported in mode 802.3ad(4) + # option arp_interval: mode dependency failed, not supported in mode balance-alb(6) + # option arp_interval: mode dependency failed, not supported in mode balance-tlb(5) + # + # so we simply disable arp_interval by setting it to 0 and miimon will take care about the link + config.set(base + [bond, 'arp-monitor', 'interval'], value='0') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/10-to-11 b/src/migration-scripts/interfaces/10-to-11 new file mode 100755 index 000000000..6b8e49ed9 --- /dev/null +++ b/src/migration-scripts/interfaces/10-to-11 @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# rename WWAN (wirelessmodem) serial interface from non persistent ttyUSB2 to +# a bus like name, e.g. "usb0b1.3p1.3" + +import os + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'wirelessmodem'] + if not config.exists(base): + # Nothing to do + exit(0) + + for wwan in config.list_nodes(base): + if config.exists(base + [wwan, 'device']): + device = config.return_value(base + [wwan, 'device']) + + for root, dirs, files in os.walk('/dev/serial/by-bus'): + for file in files: + device_file = os.path.realpath(os.path.join(root, file)) + if os.path.basename(device_file) == device: + config.set(base + [wwan, 'device'], value=file, replace=True) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/interfaces/11-to-12 b/src/migration-scripts/interfaces/11-to-12 new file mode 100755 index 000000000..0dad24642 --- /dev/null +++ b/src/migration-scripts/interfaces/11-to-12 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - rename 'dhcpv6-options prefix-delegation' from single node to a new tag node +# 'dhcpv6-options pd 0' +# - delete 'sla-len' from CLI - value is calculated on demand + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + # cache current config tree + base_path = ['interfaces', type, interface, 'dhcpv6-options'] + old_base = base_path + ['prefix-delegation'] + new_base = base_path + ['pd'] + if config.exists(old_base): + config.set(new_base) + config.set_tag(new_base) + config.copy(old_base, new_base + ['0']) + config.delete(old_base) + + for pd in config.list_nodes(new_base): + for tmp in config.list_nodes(new_base + [pd, 'interface']): + sla_config = new_base + [pd, 'interface', tmp, 'sla-len'] + if config.exists(sla_config): + config.delete(sla_config) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/interfaces/2-to-3 b/src/migration-scripts/interfaces/2-to-3 new file mode 100755 index 000000000..a63a54cdf --- /dev/null +++ b/src/migration-scripts/interfaces/2-to-3 @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# Change syntax of openvpn encryption settings +# - move cipher from encryption to encryption cipher +# https://phabricator.vyos.net/T1704 + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'openvpn'] + +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # + # move cipher from "encryption" to "encryption cipher" + # + for intf in config.list_nodes(['interfaces', 'openvpn']): + # Check if encryption is set + if config.exists(['interfaces', 'openvpn', intf, 'encryption']): + # Get cipher used + cipher = config.return_value(['interfaces', 'openvpn', intf, 'encryption']) + # Delete old syntax + config.delete(['interfaces', 'openvpn', intf, 'encryption']) + # Add new syntax to config + config.set(['interfaces', 'openvpn', intf, 'encryption', 'cipher'], value=cipher) + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/3-to-4 b/src/migration-scripts/interfaces/3-to-4 new file mode 100755 index 000000000..e3bd25a68 --- /dev/null +++ b/src/migration-scripts/interfaces/3-to-4 @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +# Change syntax of wireless interfaces +# Migrate boolean nodes to valueless + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'wireless'] + +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + for wifi in config.list_nodes(base): + # as converting a node to bool is always the same, we can script it + to_bool_nodes = ['capabilities ht 40MHz-incapable', + 'capabilities ht auto-powersave', + 'capabilities ht delayed-block-ack', + 'capabilities ht dsss-cck-40', + 'capabilities ht greenfield', + 'capabilities ht ldpc', + 'capabilities ht lsig-protection', + 'capabilities ht stbc tx', + 'capabilities require-ht', + 'capabilities require-vht', + 'capabilities vht antenna-pattern-fixed', + 'capabilities vht ldpc', + 'capabilities vht stbc tx', + 'capabilities vht tx-powersave', + 'capabilities vht vht-cf', + 'expunge-failing-stations', + 'isolate-stations'] + + for node in to_bool_nodes: + if config.exists(base + [wifi, node]): + tmp = config.return_value(base + [wifi, node]) + # delete old node + config.delete(base + [wifi, node]) + # set new node if it was enabled + if tmp == 'true': + # OLD CLI used camel casing in 40MHz-incapable which is + # not supported in the new backend. Convert all to lower-case + config.set(base + [wifi, node.lower()]) + + # Remove debug node + if config.exists(base + [wifi, 'debug']): + config.delete(base + [wifi, 'debug']) + + # RADIUS servers + if config.exists(base + [wifi, 'security', 'wpa', 'radius-server']): + for server in config.list_nodes(base + [wifi, 'security', 'wpa', 'radius-server']): + base_server = base + [wifi, 'security', 'wpa', 'radius-server', server] + + # Migrate RADIUS shared secret + if config.exists(base_server + ['secret']): + key = config.return_value(base_server + ['secret']) + # write new configuration node + config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'key'], value=key) + # format as tag node + config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server']) + + # Migrate RADIUS port + if config.exists(base_server + ['port']): + port = config.return_value(base_server + ['port']) + # write new configuration node + config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'port'], value=port) + # format as tag node + config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server']) + + # Migrate RADIUS accounting + if config.exists(base_server + ['accounting']): + port = config.return_value(base_server + ['accounting']) + # write new configuration node + config.set(base + [wifi, 'security', 'wpa', 'radius', 'server', server, 'accounting']) + # format as tag node + config.set_tag(base + [wifi, 'security', 'wpa', 'radius', 'server']) + + # delete old radius-server nodes + config.delete(base + [wifi, 'security', 'wpa', 'radius-server']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/4-to-5 b/src/migration-scripts/interfaces/4-to-5 new file mode 100755 index 000000000..2a42c60ff --- /dev/null +++ b/src/migration-scripts/interfaces/4-to-5 @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +# De-nest PPPoE interfaces +# Migrate boolean nodes to valueless + +import sys +from vyos.configtree import ConfigTree + +def migrate_dialer(config, tree, intf): + for pppoe in config.list_nodes(tree): + # assemble string, 0 -> pppoe0 + new_base = ['interfaces', 'pppoe'] + pppoe_base = new_base + ['pppoe' + pppoe] + config.set(new_base) + # format as tag node to avoid loading problems + config.set_tag(new_base) + + # Copy the entire old node to the new one before migrating individual + # parts + config.copy(tree + [pppoe], pppoe_base) + + # Instead of letting the user choose between auto and none + # where auto is default, it makes more sesne to just offer + # an option to disable the default behavior (declutter CLI) + if config.exists(pppoe_base + ['name-server']): + tmp = config.return_value(pppoe_base + ['name-server']) + if tmp == "none": + config.set(pppoe_base + ['no-peer-dns']) + config.delete(pppoe_base + ['name-server']) + + # Migrate user-id and password nodes under an 'authentication' + # node + if config.exists(pppoe_base + ['user-id']): + user = config.return_value(pppoe_base + ['user-id']) + config.set(pppoe_base + ['authentication', 'user'], value=user) + config.delete(pppoe_base + ['user-id']) + + if config.exists(pppoe_base + ['password']): + pwd = config.return_value(pppoe_base + ['password']) + config.set(pppoe_base + ['authentication', 'password'], value=pwd) + config.delete(pppoe_base + ['password']) + + # remove enable-ipv6 node and rather place it under ipv6 node + if config.exists(pppoe_base + ['enable-ipv6']): + config.set(pppoe_base + ['ipv6', 'enable']) + config.delete(pppoe_base + ['enable-ipv6']) + + # Source interface migration + config.set(pppoe_base + ['source-interface'], value=intf) + + # Remove IPv6 router-advert nodes as this makes no sense on a + # client diale rinterface to send RAs back into the network + # https://phabricator.vyos.net/T2055 + ipv6_ra = pppoe_base + ['ipv6', 'router-advert'] + if config.exists(ipv6_ra): + config.delete(ipv6_ra) + + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = sys.argv[1] + + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + pppoe_links = ['bonding', 'ethernet'] + + for link_type in pppoe_links: + if not config.exists(['interfaces', link_type]): + continue + + for interface in config.list_nodes(['interfaces', link_type]): + # check if PPPoE exists + base_if = ['interfaces', link_type, interface] + pppoe_if = base_if + ['pppoe'] + if config.exists(pppoe_if): + for dialer in config.list_nodes(pppoe_if): + migrate_dialer(config, pppoe_if, interface) + + # Delete old PPPoE interface + config.delete(pppoe_if) + + # bail out early if there are no VLAN interfaces to migrate + if not config.exists(base_if + ['vif']): + continue + + # Migrate PPPoE interfaces attached to a VLAN + for vlan in config.list_nodes(base_if + ['vif']): + vlan_if = base_if + ['vif', vlan] + pppoe_if = vlan_if + ['pppoe'] + if config.exists(pppoe_if): + for dialer in config.list_nodes(pppoe_if): + intf = "{}.{}".format(interface, vlan) + migrate_dialer(config, pppoe_if, intf) + + # Delete old PPPoE interface + config.delete(pppoe_if) + + # Add interface description that this is required for PPPoE + if not config.exists(vlan_if + ['description']): + config.set(vlan_if + ['description'], value='PPPoE link interface') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/5-to-6 b/src/migration-scripts/interfaces/5-to-6 new file mode 100755 index 000000000..1291751d8 --- /dev/null +++ b/src/migration-scripts/interfaces/5-to-6 @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Migrate IPv6 router advertisments from a nested interface configuration to +# a denested "service router-advert" + +import sys +from vyos.configtree import ConfigTree + +def copy_rtradv(c, old_base, interface): + base = ['service', 'router-advert', 'interface'] + + if c.exists(old_base): + if not c.exists(base): + c.set(base) + c.set_tag(base) + + # take the old node as a whole and copy it to new new path, + # additional migrations will be done afterwards + new_base = base + [interface] + c.copy(old_base, new_base) + c.delete(old_base) + + # cur-hop-limit has been renamed to hop-limit + if c.exists(new_base + ['cur-hop-limit']): + c.rename(new_base + ['cur-hop-limit'], 'hop-limit') + + bool_cleanup = ['managed-flag', 'other-config-flag'] + for bool in bool_cleanup: + if c.exists(new_base + [bool]): + tmp = c.return_value(new_base + [bool]) + c.delete(new_base + [bool]) + if tmp == 'true': + c.set(new_base + [bool]) + + # max/min interval moved to subnode + intervals = ['max-interval', 'min-interval'] + for interval in intervals: + if c.exists(new_base + [interval]): + tmp = c.return_value(new_base + [interval]) + c.delete(new_base + [interval]) + min_max = interval.split('-')[0] + c.set(new_base + ['interval', min_max], value=tmp) + + # cleanup boolean nodes in individual prefix + prefix_base = new_base + ['prefix'] + if c.exists(prefix_base): + for prefix in config.list_nodes(prefix_base): + if c.exists(prefix_base + [prefix, 'autonomous-flag']): + tmp = c.return_value(prefix_base + [prefix, 'autonomous-flag']) + c.delete(prefix_base + [prefix, 'autonomous-flag']) + if tmp == 'false': + c.set(prefix_base + [prefix, 'no-autonomous-flag']) + + if c.exists(prefix_base + [prefix, 'on-link-flag']): + tmp = c.return_value(prefix_base + [prefix, 'on-link-flag']) + c.delete(prefix_base + [prefix, 'on-link-flag']) + if tmp == 'true': + c.set(prefix_base + [prefix, 'on-link-flag']) + + # router advertisement can be individually disabled per interface + # the node has been renamed from send-advert {true | false} to no-send-advert + if c.exists(new_base + ['send-advert']): + tmp = c.return_value(new_base + ['send-advert']) + c.delete(new_base + ['send-advert']) + if tmp == 'false': + c.set(new_base + ['no-send-advert']) + + # link-mtu advertisement was formerly disabled by setting its value to 0 + # ... this makes less sense - if it should not be send, just do not + # configure it + if c.exists(new_base + ['link-mtu']): + tmp = c.return_value(new_base + ['link-mtu']) + if tmp == '0': + c.delete(new_base + ['link-mtu']) + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = sys.argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + # list all individual interface types like dummy, ethernet and so on + for if_type in config.list_nodes(['interfaces']): + base_if_type = ['interfaces', if_type] + + # for every individual interface we need to check if there is an + # ipv6 ra configured ... and also for every VIF (VLAN) interface + for intf in config.list_nodes(base_if_type): + old_base = base_if_type + [intf, 'ipv6', 'router-advert'] + copy_rtradv(config, old_base, intf) + + vif_base = base_if_type + [intf, 'vif'] + if config.exists(vif_base): + for vif in config.list_nodes(vif_base): + old_base = vif_base + [vif, 'ipv6', 'router-advert'] + vlan_name = f'{intf}.{vif}' + copy_rtradv(config, old_base, vlan_name) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/6-to-7 b/src/migration-scripts/interfaces/6-to-7 new file mode 100755 index 000000000..220c7e601 --- /dev/null +++ b/src/migration-scripts/interfaces/6-to-7 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Remove network provider name from CLI and rather use provider APN from CLI + +import sys +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = sys.argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'wirelessmodem'] + + if not config.exists(base): + # Nothing to do + sys.exit(0) + + # list all individual wwan/wireless modem interfaces + for i in config.list_nodes(base): + iface = base + [i] + + # only three carries have been supported in the past, thus + # this will be fairly simple \o/ - and only one (AT&T) did + # configure an APN + if config.exists(iface + ['network']): + network = config.return_value(iface + ['network']) + if network == "att": + apn = 'isp.cingular' + config.set(iface + ['apn'], value=apn) + + config.delete(iface + ['network']) + + # synchronize DNS configuration with PPPoE interfaces to have a + # uniform CLI experience + if config.exists(iface + ['no-dns']): + config.rename(iface + ['no-dns'], 'no-peer-dns') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/7-to-8 b/src/migration-scripts/interfaces/7-to-8 new file mode 100755 index 000000000..a4051301f --- /dev/null +++ b/src/migration-scripts/interfaces/7-to-8 @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Split WireGuard endpoint into address / port nodes to make use of common +# validators + +import os + +from sys import exit, argv +from vyos.configtree import ConfigTree +from vyos.util import chown, chmod_750 + +def migrate_default_keys(): + kdir = r'/config/auth/wireguard' + if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): + location = f'{kdir}/default' + if not os.path.exists(location): + os.makedirs(location) + + chown(location, 'root', 'vyattacfg') + chmod_750(location) + os.rename(f'{kdir}/private.key', f'{location}/private.key') + os.rename(f'{kdir}/public.key', f'{location}/public.key') + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'wireguard'] + + migrate_default_keys() + + if not config.exists(base): + # Nothing to do + exit(0) + + # list all individual wireguard interface isntance + for i in config.list_nodes(base): + iface = base + [i] + for peer in config.list_nodes(iface + ['peer']): + base_peer = iface + ['peer', peer] + if config.exists(base_peer + ['endpoint']): + endpoint = config.return_value(base_peer + ['endpoint']) + address = endpoint.split(':')[0] + port = endpoint.split(':')[1] + # delete old node + config.delete(base_peer + ['endpoint']) + # setup new nodes + config.set(base_peer + ['address'], value=address) + config.set(base_peer + ['port'], value=port) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/interfaces/8-to-9 b/src/migration-scripts/interfaces/8-to-9 new file mode 100755 index 000000000..2d1efd418 --- /dev/null +++ b/src/migration-scripts/interfaces/8-to-9 @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Rename link nodes to source-interface for the following interface types: +# - vxlan +# - pseudo-ethernet + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + for if_type in ['vxlan', 'pseudo-ethernet']: + base = ['interfaces', if_type] + if not config.exists(base): + # Nothing to do + continue + + # list all individual interface isntance + for i in config.list_nodes(base): + iface = base + [i] + if config.exists(iface + ['link']): + config.rename(iface + ['link'], 'source-interface') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/interfaces/9-to-10 b/src/migration-scripts/interfaces/9-to-10 new file mode 100755 index 000000000..4aa2c42b5 --- /dev/null +++ b/src/migration-scripts/interfaces/9-to-10 @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - rename CLI node 'dhcpv6-options delgate' to 'dhcpv6-options prefix-delegation +# interface' +# - rename CLI node 'interface-id' for prefix-delegation to 'address' as it +# represents the local interface IPv6 address assigned by DHCPv6-PD + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + for intf_type in config.list_nodes(['interfaces']): + for intf in config.list_nodes(['interfaces', intf_type]): + # cache current config tree + base_path = ['interfaces', intf_type, intf, 'dhcpv6-options', + 'delegate'] + + if config.exists(base_path): + # cache new config tree + new_path = ['interfaces', intf_type, intf, 'dhcpv6-options', + 'prefix-delegation'] + if not config.exists(new_path): + config.set(new_path) + + # copy to new node + config.copy(base_path, new_path + ['interface']) + + # rename interface-id to address + for interface in config.list_nodes(new_path + ['interface']): + config.rename(new_path + ['interface', interface, 'interface-id'], 'address') + + # delete old noe + config.delete(base_path) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1 new file mode 100755 index 000000000..f328ebced --- /dev/null +++ b/src/migration-scripts/ipoe-server/0-to-1 @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - remove primary/secondary identifier from nameserver +# - Unifi RADIUS configuration by placing it all under "authentication radius" node + +import os +import sys + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'ipoe-server'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # Migrate IPv4 DNS servers + dns_base = base + ['dns-server'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv6 DNS servers + dns_base = base + ['dnsv6-server'] + if config.exists(dns_base): + for server in ['server-1', 'server-2', 'server-3']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate radius-settings node to RADIUS and use this as base for the + # later migration of the RADIUS servers - this will save a lot of code + radius_settings = base + ['authentication', 'radius-settings'] + if config.exists(radius_settings): + config.rename(radius_settings, 'radius') + + # Migrate RADIUS dynamic author / change of authorisation server + dae_old = base + ['authentication', 'radius', 'dae-server'] + if config.exists(dae_old): + config.rename(dae_old, 'dynamic-author') + dae_new = base + ['authentication', 'radius', 'dynamic-author'] + + if config.exists(dae_new + ['ip-address']): + config.rename(dae_new + ['ip-address'], 'server') + + if config.exists(dae_new + ['secret']): + config.rename(dae_new + ['secret'], 'key') + + # Migrate RADIUS server + radius_server = base + ['authentication', 'radius-server'] + if config.exists(radius_server): + new_base = base + ['authentication', 'radius', 'server'] + config.set(new_base) + config.set_tag(new_base) + for server in config.list_nodes(radius_server): + old_base = radius_server + [server] + config.copy(old_base, new_base + [server]) + + # migrate key + if config.exists(new_base + [server, 'secret']): + config.rename(new_base + [server, 'secret'], 'key') + + # remove old req-limit node + if config.exists(new_base + [server, 'req-limit']): + config.delete(new_base + [server, 'req-limit']) + + config.delete(radius_server) + + # Migrate IPv6 prefixes + ipv6_base = base + ['client-ipv6-pool'] + if config.exists(ipv6_base + ['prefix']): + prefix_old = config.return_values(ipv6_base + ['prefix']) + # delete old prefix CLI nodes + config.delete(ipv6_base + ['prefix']) + # create ned prefix tag node + config.set(ipv6_base + ['prefix']) + config.set_tag(ipv6_base + ['prefix']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) + + if config.exists(ipv6_base + ['delegate-prefix']): + prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) + # delete old delegate prefix CLI nodes + config.delete(ipv6_base + ['delegate-prefix']) + # create ned delegation tag node + config.set(ipv6_base + ['delegate']) + config.set_tag(ipv6_base + ['delegate']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/ipsec/4-to-5 b/src/migration-scripts/ipsec/4-to-5 new file mode 100755 index 000000000..b64aa8462 --- /dev/null +++ b/src/migration-scripts/ipsec/4-to-5 @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# log-modes have changed, keyword all to any + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +ctree = ConfigTree(config_file) + +if not ctree.exists(['vpn', 'ipsec', 'logging','log-modes']): + # Nothing to do + sys.exit(0) +else: + lmodes = ctree.return_values(['vpn', 'ipsec', 'logging','log-modes']) + for mode in lmodes: + if mode == 'all': + ctree.set(['vpn', 'ipsec', 'logging','log-modes'], value='any', replace=True) + + try: + open(file_name,'w').write(ctree.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/l2tp/0-to-1 b/src/migration-scripts/l2tp/0-to-1 new file mode 100755 index 000000000..686ebc655 --- /dev/null +++ b/src/migration-scripts/l2tp/0-to-1 @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +# Unclutter L2TP VPN configuiration - move radius-server top level tag +# nodes to a regular node which now also configures the radius source address +# used when querying a radius server + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_base = ['vpn', 'l2tp', 'remote-access', 'authentication'] +if not config.exists(cfg_base): + # Nothing to do + sys.exit(0) +else: + # Migrate "vpn l2tp authentication radius-source-address" to new + # "vpn l2tp authentication radius source-address" + if config.exists(cfg_base + ['radius-source-address']): + address = config.return_value(cfg_base + ['radius-source-address']) + # delete old configuration node + config.delete(cfg_base + ['radius-source-address']) + # write new configuration node + config.set(cfg_base + ['radius', 'source-address'], value=address) + + # Migrate "vpn l2tp authentication radius-server" tag node to new + # "vpn l2tp authentication radius server" tag node + if config.exists(cfg_base + ['radius-server']): + for server in config.list_nodes(cfg_base + ['radius-server']): + base_server = cfg_base + ['radius-server', server] + key = config.return_value(base_server + ['key']) + + # delete old configuration node + config.delete(base_server) + # write new configuration node + config.set(cfg_base + ['radius', 'server', server, 'key'], value=key) + + # format as tag node + config.set_tag(cfg_base + ['radius', 'server']) + + # delete top level tag node + if config.exists(cfg_base + ['radius-server']): + config.delete(cfg_base + ['radius-server']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/l2tp/1-to-2 b/src/migration-scripts/l2tp/1-to-2 new file mode 100755 index 000000000..c46eba8f8 --- /dev/null +++ b/src/migration-scripts/l2tp/1-to-2 @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +# Delete depricated outside-nexthop address + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_base = ['vpn', 'l2tp', 'remote-access'] +if not config.exists(cfg_base): + # Nothing to do + sys.exit(0) +else: + if config.exists(cfg_base + ['outside-nexthop']): + config.delete(cfg_base + ['outside-nexthop']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/l2tp/2-to-3 b/src/migration-scripts/l2tp/2-to-3 new file mode 100755 index 000000000..3472ee3ed --- /dev/null +++ b/src/migration-scripts/l2tp/2-to-3 @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - remove primary/secondary identifier from nameserver +# - TODO: remove radius server req-limit + +import os +import sys + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'l2tp', 'remote-access'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # Migrate IPv4 DNS servers + dns_base = base + ['dns-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv6 DNS servers + dns_base = base + ['dnsv6-servers'] + if config.exists(dns_base): + for server in config.return_values(dns_base): + config.set(base + ['name-server'], value=server, replace=False) + + config.delete(dns_base) + + # Migrate IPv4 WINS servers + wins_base = base + ['wins-servers'] + if config.exists(wins_base): + for server in ['server-1', 'server-2']: + if config.exists(wins_base + [server]): + wins = config.return_value(wins_base + [server]) + config.set(base + ['wins-server'], value=wins, replace=False) + + config.delete(wins_base) + + + # Remove RADIUS server req-limit node + radius_base = base + ['authentication', 'radius'] + if config.exists(radius_base): + for server in config.list_nodes(radius_base + ['server']): + if config.exists(radius_base + ['server', server, 'req-limit']): + config.delete(radius_base + ['server', server, 'req-limit']) + + # Migrate IPv6 prefixes + ipv6_base = base + ['client-ipv6-pool'] + if config.exists(ipv6_base + ['prefix']): + prefix_old = config.return_values(ipv6_base + ['prefix']) + # delete old prefix CLI nodes + config.delete(ipv6_base + ['prefix']) + # create ned prefix tag node + config.set(ipv6_base + ['prefix']) + config.set_tag(ipv6_base + ['prefix']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) + + if config.exists(ipv6_base + ['delegate-prefix']): + prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) + # delete old delegate prefix CLI nodes + config.delete(ipv6_base + ['delegate-prefix']) + # create ned delegation tag node + config.set(ipv6_base + ['delegate']) + config.set_tag(ipv6_base + ['delegate']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['delegate', prefix, 'delegate-prefix'], value=mask) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/lldp/0-to-1 b/src/migration-scripts/lldp/0-to-1 new file mode 100755 index 000000000..5f66570e7 --- /dev/null +++ b/src/migration-scripts/lldp/0-to-1 @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +# Delete "set service lldp interface <interface> location civic-based" option +# as it was broken most of the time anyways + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'lldp', 'interface'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # Delete nodes with abandoned CLI syntax + for interface in config.list_nodes(base): + if config.exists(base + [interface, 'location', 'civic-based']): + config.delete(base + [interface, 'location', 'civic-based']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/nat/4-to-5 b/src/migration-scripts/nat/4-to-5 new file mode 100755 index 000000000..dda191719 --- /dev/null +++ b/src/migration-scripts/nat/4-to-5 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Drop the enable/disable from the nat "log" node. If log node is specified +# it is "enabled" + +from sys import argv,exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['nat']): + # Nothing to do + exit(0) +else: + for direction in ['source', 'destination']: + if not config.exists(['nat', direction]): + continue + + for rule in config.list_nodes(['nat', direction, 'rule']): + base = ['nat', direction, 'rule', rule] + + # Check if the log node exists and if log is enabled, + # migrate it to the new valueless 'log' node + if config.exists(base + ['log']): + tmp = config.return_value(base + ['log']) + config.delete(base + ['log']) + if tmp == 'enable': + config.set(base + ['log']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/ntp/0-to-1 b/src/migration-scripts/ntp/0-to-1 new file mode 100755 index 000000000..9c66f3109 --- /dev/null +++ b/src/migration-scripts/ntp/0-to-1 @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# Delete "set system ntp server <n> dynamic" option + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['system', 'ntp']): + # Nothing to do + sys.exit(0) +else: + # Delete abandoned leaf node if found inside tag node for + # "set system ntp server <n> dynamic" + base = ['system', 'ntp', 'server'] + for server in config.list_nodes(base): + if config.exists(base + [server, 'dynamic']): + config.delete(base + [server, 'dynamic']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/pppoe-server/0-to-1 b/src/migration-scripts/pppoe-server/0-to-1 new file mode 100755 index 000000000..bb24211b6 --- /dev/null +++ b/src/migration-scripts/pppoe-server/0-to-1 @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +# Convert "service pppoe-server authentication radius-server node key" +# to: +# "service pppoe-server authentication radius-server node secret" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +ctree = ConfigTree(config_file) + + +if not ctree.exists(['service', 'pppoe-server', 'authentication','radius-server']): + # Nothing to do + sys.exit(0) +else: + nodes = ctree.list_nodes(['service', 'pppoe-server', 'authentication','radius-server']) + for node in nodes: + if ctree.exists(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key']): + val = ctree.return_value(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key']) + ctree.set(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'secret'], value=val, replace=False) + ctree.delete(['service', 'pppoe-server', 'authentication', 'radius-server', node, 'key']) + try: + open(file_name,'w').write(ctree.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/pppoe-server/1-to-2 b/src/migration-scripts/pppoe-server/1-to-2 new file mode 100755 index 000000000..fa83896d3 --- /dev/null +++ b/src/migration-scripts/pppoe-server/1-to-2 @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +# Convert "service pppoe-server interface ethX" +# to: +# "service pppoe-server interface ethX {}" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +ctree = ConfigTree(config_file) +cbase = ['service', 'pppoe-server','interface'] + +if not ctree.exists(cbase): + sys.exit(0) +else: + nics = ctree.return_values(cbase) + # convert leafNode to a tagNode + ctree.set(cbase) + ctree.set_tag(cbase) + for nic in nics: + ctree.set(cbase + [nic]) + + try: + open(file_name,'w').write(ctree.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) + diff --git a/src/migration-scripts/pppoe-server/2-to-3 b/src/migration-scripts/pppoe-server/2-to-3 new file mode 100755 index 000000000..5f9730a41 --- /dev/null +++ b/src/migration-scripts/pppoe-server/2-to-3 @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - remove primary/secondary identifier from nameserver + +import os + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'pppoe-server'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # Migrate IPv4 DNS servers + dns_base = base + ['dns-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv6 DNS servers + dns_base = base + ['dnsv6-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2', 'server-3']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv4 WINS servers + wins_base = base + ['wins-servers'] + if config.exists(wins_base): + for server in ['server-1', 'server-2']: + if config.exists(wins_base + [server]): + wins = config.return_value(wins_base + [server]) + config.set(base + ['wins-server'], value=wins, replace=False) + + config.delete(wins_base) + + # Migrate radius-settings node to RADIUS and use this as base for the + # later migration of the RADIUS servers - this will save a lot of code + radius_settings = base + ['authentication', 'radius-settings'] + if config.exists(radius_settings): + config.rename(radius_settings, 'radius') + + # Migrate RADIUS dynamic author / change of authorisation server + dae_old = base + ['authentication', 'radius', 'dae-server'] + if config.exists(dae_old): + config.rename(dae_old, 'dynamic-author') + dae_new = base + ['authentication', 'radius', 'dynamic-author'] + + if config.exists(dae_new + ['ip-address']): + config.rename(dae_new + ['ip-address'], 'server') + + if config.exists(dae_new + ['secret']): + config.rename(dae_new + ['secret'], 'key') + + # Migrate RADIUS server + radius_server = base + ['authentication', 'radius-server'] + if config.exists(radius_server): + new_base = base + ['authentication', 'radius', 'server'] + config.set(new_base) + config.set_tag(new_base) + for server in config.list_nodes(radius_server): + old_base = radius_server + [server] + config.copy(old_base, new_base + [server]) + + # migrate key + if config.exists(new_base + [server, 'secret']): + config.rename(new_base + [server, 'secret'], 'key') + + # remove old req-limit node + if config.exists(new_base + [server, 'req-limit']): + config.delete(new_base + [server, 'req-limit']) + + config.delete(radius_server) + + # Migrate IPv6 prefixes + ipv6_base = base + ['client-ipv6-pool'] + if config.exists(ipv6_base + ['prefix']): + prefix_old = config.return_values(ipv6_base + ['prefix']) + # delete old prefix CLI nodes + config.delete(ipv6_base + ['prefix']) + # create ned prefix tag node + config.set(ipv6_base + ['prefix']) + config.set_tag(ipv6_base + ['prefix']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) + + if config.exists(ipv6_base + ['delegate-prefix']): + prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) + # delete old delegate prefix CLI nodes + config.delete(ipv6_base + ['delegate-prefix']) + # create ned delegation tag node + config.set(ipv6_base + ['delegate']) + config.set_tag(ipv6_base + ['delegate']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/pppoe-server/3-to-4 b/src/migration-scripts/pppoe-server/3-to-4 new file mode 100755 index 000000000..ed5a01625 --- /dev/null +++ b/src/migration-scripts/pppoe-server/3-to-4 @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# change mppe node to a leaf node with value prefer + +import os + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'pppoe-server'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + mppe_base = base + ['ppp-options', 'mppe'] + if config.exists(mppe_base): + # drop node first ... + config.delete(mppe_base) + # ... and set new default + config.set(mppe_base, value='prefer') + + print(config.to_string()) + exit(1) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/pptp/0-to-1 b/src/migration-scripts/pptp/0-to-1 new file mode 100755 index 000000000..d0c7a83b5 --- /dev/null +++ b/src/migration-scripts/pptp/0-to-1 @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 + +# Unclutter PPTP VPN configuiration - move radius-server top level tag +# nodes to a regular node which now also configures the radius source address +# used when querying a radius server + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_base = ['vpn', 'pptp', 'remote-access', 'authentication'] +if not config.exists(cfg_base): + # Nothing to do + sys.exit(0) +else: + # Migrate "vpn pptp authentication radius-source-address" to new + # "vpn pptp authentication radius source-address" + if config.exists(cfg_base + ['radius-source-address']): + address = config.return_value(cfg_base + ['radius-source-address']) + # delete old configuration node + config.delete(cfg_base + ['radius-source-address']) + # write new configuration node + config.set(cfg_base + ['radius', 'source-address'], value=address) + + # Migrate "vpn pptp authentication radius-server" tag node to new + # "vpn pptp authentication radius server" tag node + for server in config.list_nodes(cfg_base + ['radius-server']): + base_server = cfg_base + ['radius-server', server] + key = config.return_value(base_server + ['key']) + + # delete old configuration node + config.delete(base_server) + # write new configuration node + config.set(cfg_base + ['radius', 'server', server, 'key'], value=key) + + # format as tag node + config.set_tag(cfg_base + ['radius', 'server']) + + # delete top level tag node + if config.exists(cfg_base + ['radius-server']): + config.delete(cfg_base + ['radius-server']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/pptp/1-to-2 b/src/migration-scripts/pptp/1-to-2 new file mode 100755 index 000000000..a13cc3a4f --- /dev/null +++ b/src/migration-scripts/pptp/1-to-2 @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - migrate dns-servers node to common name-servers +# - remove radios req-limit node + +from sys import argv, exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'pptp', 'remote-access'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + # Migrate IPv4 DNS servers + dns_base = base + ['dns-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv4 WINS servers + wins_base = base + ['wins-servers'] + if config.exists(wins_base): + for server in ['server-1', 'server-2']: + if config.exists(wins_base + [server]): + wins = config.return_value(wins_base + [server]) + config.set(base + ['wins-server'], value=wins, replace=False) + + config.delete(wins_base) + + # Remove RADIUS server req-limit node + radius_base = base + ['authentication', 'radius'] + if config.exists(radius_base): + for server in config.list_nodes(radius_base + ['server']): + if config.exists(radius_base + ['server', server, 'req-limit']): + config.delete(radius_base + ['server', server, 'req-limit']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/quagga/2-to-3 b/src/migration-scripts/quagga/2-to-3 new file mode 100755 index 000000000..4c1cd86a3 --- /dev/null +++ b/src/migration-scripts/quagga/2-to-3 @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +def migrate_neighbor(config, neighbor_path, neighbor): + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + # Move the valueless options: as-override, next-hop-self, route-reflector-client, route-server-client, + # remove-private-as + for valueless_option in ['as-override', 'nexthop-self', 'route-reflector-client', 'route-server-client', + 'remove-private-as']: + if config.exists(neighbor_path + [neighbor, valueless_option]): + config.set(neighbor_path + [neighbor] + af_path + [valueless_option]) + config.delete(neighbor_path + [neighbor, valueless_option]) + + # Move filter options: distribute-list, filter-list, prefix-list, and route-map + # They share the same syntax inside so we can group them + for filter_type in ['distribute-list', 'filter-list', 'prefix-list', 'route-map']: + if config.exists(neighbor_path + [neighbor, filter_type]): + for filter_dir in ['import', 'export']: + if config.exists(neighbor_path + [neighbor, filter_type, filter_dir]): + filter_name = config.return_value(neighbor_path + [neighbor, filter_type, filter_dir]) + config.set(neighbor_path + [neighbor] + af_path + [filter_type, filter_dir], value=filter_name) + config.delete(neighbor_path + [neighbor, filter_type]) + + # Move simple leaf node options: maximum-prefix, unsuppress-map, weight + for leaf_option in ['maximum-prefix', 'unsuppress-map', 'weight']: + if config.exists(neighbor_path + [neighbor, leaf_option]): + if config.exists(neighbor_path + [neighbor, leaf_option]): + leaf_opt_value = config.return_value(neighbor_path + [neighbor, leaf_option]) + config.set(neighbor_path + [neighbor] + af_path + [leaf_option], value=leaf_opt_value) + config.delete(neighbor_path + [neighbor, leaf_option]) + + # The rest is special cases, for better or worse + + # Move allowas-in + if config.exists(neighbor_path + [neighbor, 'allowas-in']): + if config.exists(neighbor_path + [neighbor, 'allowas-in', 'number']): + allowas_in = config.return_value(neighbor_path + [neighbor, 'allowas-in', 'number']) + config.set(neighbor_path + [neighbor] + af_path + ['allowas-in', 'number'], value=allowas_in) + config.delete(neighbor_path + [neighbor, 'allowas-in']) + + # Move attribute-unchanged options + if config.exists(neighbor_path + [neighbor, 'attribute-unchanged']): + for attr in ['as-path', 'med', 'next-hop']: + if config.exists(neighbor_path + [neighbor, 'attribute-unchanged', attr]): + config.set(neighbor_path + [neighbor] + af_path + ['attribute-unchanged', attr]) + config.delete(neighbor_path + [neighbor, 'attribute-unchanged', attr]) + config.delete(neighbor_path + [neighbor, 'attribute-unchanged']) + + # Move capability options + if config.exists(neighbor_path + [neighbor, 'capability']): + # "capability dynamic" is a peer-global option, we only migrate ORF + if config.exists(neighbor_path + [neighbor, 'capability', 'orf']): + if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list']): + for orf in ['send', 'receive']: + if config.exists(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf]): + config.set(neighbor_path + [neighbor] + af_path + ['capability', 'orf', 'prefix-list', orf]) + config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list', orf]) + config.delete(neighbor_path + [neighbor, 'capability', 'orf', 'prefix-list']) + config.delete(neighbor_path + [neighbor, 'capability', 'orf']) + + # Move default-originate + if config.exists(neighbor_path + [neighbor, 'default-originate']): + if config.exists(neighbor_path + [neighbor, 'default-originate', 'route-map']): + route_map = config.return_value(neighbor_path + [neighbor, 'default-originate', 'route-map']) + config.set(neighbor_path + [neighbor] + af_path + ['default-originate', 'route-map'], value=route_map) + else: + # Empty default-originate node is meaningful so we re-create it + config.set(neighbor_path + [neighbor] + af_path + ['default-originate']) + config.delete(neighbor_path + [neighbor, 'default-originate']) + + # Move soft-reconfiguration + if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration']): + if config.exists(neighbor_path + [neighbor, 'soft-reconfiguration', 'inbound']): + config.set(neighbor_path + [neighbor] + af_path + ['soft-reconfiguration', 'inbound']) + # Empty soft-reconfiguration is meaningless, so we just remove it + config.delete(neighbor_path + [neighbor, 'soft-reconfiguration']) + + # Move disable-send-community + if config.exists(neighbor_path + [neighbor, 'disable-send-community']): + for comm_type in ['standard', 'extended']: + if config.exists(neighbor_path + [neighbor, 'disable-send-community', comm_type]): + config.set(neighbor_path + [neighbor] + af_path + ['disable-send-community', comm_type]) + config.delete(neighbor_path + [neighbor, 'disable-send-community', comm_type]) + config.delete(neighbor_path + [neighbor, 'disable-send-community']) + + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Just to avoid writing it so many times + af_path = ['address-family', 'ipv4-unicast'] + + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + bgp_path = ['protocols', 'bgp', asn] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + ## Move global IPv4-specific BGP options to "address-family ipv4-unicast" + + # Move networks + network_path = ['protocols', 'bgp', asn, 'network'] + if config.exists(network_path): + config.set(bgp_path + af_path + ['network']) + config.set_tag(bgp_path + af_path + ['network']) + + networks = config.list_nodes(network_path) + for network in networks: + config.set(bgp_path + af_path + ['network', network]) + if config.exists(network_path + [network, 'route-map']): + route_map = config.return_value(network_path + [network, 'route-map']) + config.set(bgp_path + af_path + ['network', network, 'route-map'], value=route_map) + config.delete(network_path) + + # Move aggregate-address statements + aggregate_path = ['protocols', 'bgp', asn, 'aggregate-address'] + if config.exists(aggregate_path): + config.set(bgp_path + af_path + ['aggregate-address']) + config.set_tag(bgp_path + af_path + ['aggregate-address']) + + aggregates = config.list_nodes(aggregate_path) + for aggregate in aggregates: + config.set(bgp_path + af_path + ['aggregate-address', aggregate]) + if config.exists(aggregate_path + [aggregate, 'as-set']): + config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'as-set']) + if config.exists(aggregate_path + [aggregate, 'summary-only']): + config.set(bgp_path + af_path + ['aggregate-address', aggregate, 'summary-only']) + config.delete(aggregate_path) + + ## Migrate neighbor options + neighbor_path = ['protocols', 'bgp', asn, 'neighbor'] + if config.exists(neighbor_path): + neighbors = config.list_nodes(neighbor_path) + for neighbor in neighbors: + migrate_neighbor(config, neighbor_path, neighbor) + + peer_group_path = ['protocols', 'bgp', asn, 'peer-group'] + if config.exists(peer_group_path): + peer_groups = config.list_nodes(peer_group_path) + for peer_group in peer_groups: + migrate_neighbor(config, peer_group_path, peer_group) + + ## Migrate redistribute statements + redistribute_path = ['protocols', 'bgp', asn, 'redistribute'] + if config.exists(redistribute_path): + config.set(bgp_path + af_path + ['redistribute']) + + redistributes = config.list_nodes(redistribute_path) + for redistribute in redistributes: + config.set(bgp_path + af_path + ['redistribute', redistribute]) + if config.exists(redistribute_path + [redistribute, 'metric']): + redist_metric = config.return_value(redistribute_path + [redistribute, 'metric']) + config.set(bgp_path + af_path + ['redistribute', redistribute, 'metric'], value=redist_metric) + if config.exists(redistribute_path + [redistribute, 'route-map']): + redist_route_map = config.return_value(redistribute_path + [redistribute, 'route-map']) + config.set(bgp_path + af_path + ['redistribute', redistribute, 'route-map'], value=redist_route_map) + + config.delete(redistribute_path) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/quagga/3-to-4 b/src/migration-scripts/quagga/3-to-4 new file mode 100755 index 000000000..be3528391 --- /dev/null +++ b/src/migration-scripts/quagga/3-to-4 @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +# Between 1.2.3 and 1.2.4, FRR added per-neighbor enforce-first-as option. +# Unfortunately they also removed the global enforce-first-as option, +# which broke all old configs that used to have it. +# +# To emulate the effect of the original option, we insert it in every neighbor +# if the config used to have the original global option + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + # Check if BGP enforce-first-as option is set + enforce_first_as_path = ['protocols', 'bgp', asn, 'parameters', 'enforce-first-as'] + if config.exists(enforce_first_as_path): + # Delete the obsolete option + config.delete(enforce_first_as_path) + + # Now insert it in every peer + peers = config.list_nodes(['protocols', 'bgp', asn, 'neighbor']) + for p in peers: + config.set(['protocols', 'bgp', asn, 'neighbor', p, 'enforce-first-as']) + else: + # Do nothing + sys.exit(0) + + # Save a new configuration file + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) + diff --git a/src/migration-scripts/quagga/4-to-5 b/src/migration-scripts/quagga/4-to-5 new file mode 100755 index 000000000..f8c87ce8c --- /dev/null +++ b/src/migration-scripts/quagga/4-to-5 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + # Check if BGP scan-time parameter exist + scan_time_param = ['protocols', 'bgp', asn, 'parameters', 'scan-time'] + if config.exists(scan_time_param): + # Delete BGP scan-time parameter + config.delete(scan_time_param) + else: + # Do nothing + sys.exit(0) + + # Save a new configuration file + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/quagga/5-to-6 b/src/migration-scripts/quagga/5-to-6 new file mode 100755 index 000000000..a71851942 --- /dev/null +++ b/src/migration-scripts/quagga/5-to-6 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# * Remove parameter 'disable-network-import-check' which, as implemented, +# had no effect on boot. + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + # Check if BGP parameter disable-network-import-check exists + param = ['protocols', 'bgp', asn, 'parameters', 'disable-network-import-check'] + if config.exists(param): + # Delete parameter + config.delete(param) + else: + # Do nothing + sys.exit(0) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/salt/0-to-1 b/src/migration-scripts/salt/0-to-1 new file mode 100755 index 000000000..79053c056 --- /dev/null +++ b/src/migration-scripts/salt/0-to-1 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Delete log_file, log_level and user nodes +# rename hash_type to hash +# rename mine_interval to interval + +from sys import argv,exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'salt-minion'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # delete nodes which are now populated with sane defaults + for node in ['log_file', 'log_level', 'user']: + if config.exists(base + [node]): + config.delete(base + [node]) + + if config.exists(base + ['hash_type']): + config.rename(base + ['hash_type'], 'hash') + + if config.exists(base + ['mine_interval']): + config.rename(base + ['mine_interval'], 'interval') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/snmp/0-to-1 b/src/migration-scripts/snmp/0-to-1 new file mode 100755 index 000000000..a836f7011 --- /dev/null +++ b/src/migration-scripts/snmp/0-to-1 @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +config_base = ['service', 'snmp', 'v3'] + +if not config.exists(config_base): + # Nothing to do + sys.exit(0) +else: + # we no longer support a per trap target engine ID (https://phabricator.vyos.net/T818) + if config.exists(config_base + ['v3', 'trap-target']): + for target in config.list_nodes(config_base + ['v3', 'trap-target']): + config.delete(config_base + ['v3', 'trap-target', target, 'engineid']) + + # we no longer support a per user engine ID (https://phabricator.vyos.net/T818) + if config.exists(config_base + ['v3', 'user']): + for user in config.list_nodes(config_base + ['v3', 'user']): + config.delete(config_base + ['v3', 'user', user, 'engineid']) + + # we drop TSM support as there seem to be no users and this code is untested + # https://phabricator.vyos.net/T1769 + if config.exists(config_base + ['v3', 'tsm']): + config.delete(config_base + ['v3', 'tsm']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/snmp/1-to-2 b/src/migration-scripts/snmp/1-to-2 new file mode 100755 index 000000000..466a624e6 --- /dev/null +++ b/src/migration-scripts/snmp/1-to-2 @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import argv, exit +from vyos.configtree import ConfigTree + +def migrate_keys(config, path): + # authentication: rename node 'encrypted-key' -> 'encrypted-password' + config_path_auth = path + ['auth', 'encrypted-key'] + if config.exists(config_path_auth): + config.rename(config_path_auth, 'encrypted-password') + config_path_auth = path + ['auth', 'encrypted-password'] + + # remove leading '0x' from string if present + tmp = config.return_value(config_path_auth) + if tmp.startswith(prefix): + tmp = tmp.replace(prefix, '') + config.set(config_path_auth, value=tmp) + + # privacy: rename node 'encrypted-key' -> 'encrypted-password' + config_path_priv = path + ['privacy', 'encrypted-key'] + if config.exists(config_path_priv): + config.rename(config_path_priv, 'encrypted-password') + config_path_priv = path + ['privacy', 'encrypted-password'] + + # remove leading '0x' from string if present + tmp = config.return_value(config_path_priv) + if tmp.startswith(prefix): + tmp = tmp.replace(prefix, '') + config.set(config_path_priv, value=tmp) + +if __name__ == '__main__': + if (len(argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = argv[1] + + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + config_base = ['service', 'snmp', 'v3'] + + if not config.exists(config_base): + # Nothing to do + exit(0) + else: + # We no longer support hashed values prefixed with '0x' to unclutter + # CLI and also calculate the hases in advance instead of retrieving + # them after service startup - which was always a bad idea + prefix = '0x' + + config_engineid = config_base + ['engineid'] + if config.exists(config_engineid): + tmp = config.return_value(config_engineid) + if tmp.startswith(prefix): + tmp = tmp.replace(prefix, '') + config.set(config_engineid, value=tmp) + + config_user = config_base + ['user'] + if config.exists(config_user): + for user in config.list_nodes(config_user): + migrate_keys(config, config_user + [user]) + + config_trap = config_base + ['trap-target'] + if config.exists(config_trap): + for trap in config.list_nodes(config_trap): + migrate_keys(config, config_trap + [trap]) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/ssh/0-to-1 b/src/migration-scripts/ssh/0-to-1 new file mode 100755 index 000000000..91b832276 --- /dev/null +++ b/src/migration-scripts/ssh/0-to-1 @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# Delete "service ssh allow-root" option + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['service', 'ssh', 'allow-root']): + # Nothing to do + sys.exit(0) +else: + # Delete node with abandoned command + config.delete(['service', 'ssh', 'allow-root']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2 new file mode 100755 index 000000000..bc8815753 --- /dev/null +++ b/src/migration-scripts/ssh/1-to-2 @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# VyOS 1.2 crux allowed configuring a lower or upper case loglevel. This +# is no longer supported as the input data is validated and will lead to +# an error. If user specifies an upper case logleve, make it lowercase + +from sys import argv,exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'ssh', 'loglevel'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) +else: + # red in configured loglevel and convert it to lower case + tmp = config.return_value(base).lower() + + # VyOS 1.2 had no proper value validation on the CLI thus the + # user could use any arbitrary values - sanitize them + if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']: + tmp = 'info' + + config.set(base, value=tmp) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/sstp/0-to-1 b/src/migration-scripts/sstp/0-to-1 new file mode 100755 index 000000000..0e8dd1c4b --- /dev/null +++ b/src/migration-scripts/sstp/0-to-1 @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +# - migrate from "service sstp-server" to "vpn sstp" +# - remove primary/secondary identifier from nameserver +# - migrate RADIUS configuration to a more uniform syntax accross the system +# - authentication radius-server x.x.x.x to authentication radius server x.x.x.x +# - authentication radius-settings to authentication radius +# - do not migrate radius server req-limit, use default of unlimited +# - migrate SSL certificate path + +import os +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +old_base = ['service', 'sstp-server'] +if not config.exists(old_base): + # Nothing to do + sys.exit(0) +else: + # ensure new base path exists + if not config.exists(['vpn']): + config.set(['vpn']) + + new_base = ['vpn', 'sstp'] + # copy entire tree + config.copy(old_base, new_base) + config.delete(old_base) + + # migrate DNS servers + dns_base = new_base + ['network-settings', 'dns-server'] + if config.exists(dns_base): + if config.exists(dns_base + ['primary-dns']): + dns = config.return_value(dns_base + ['primary-dns']) + config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False) + + if config.exists(dns_base + ['secondary-dns']): + dns = config.return_value(dns_base + ['secondary-dns']) + config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False) + + config.delete(dns_base) + + + # migrate radius options - copy subtree + # thus must happen before migration of the individual RADIUS servers + old_options = new_base + ['authentication', 'radius-settings'] + if config.exists(old_options): + new_options = new_base + ['authentication', 'radius'] + config.copy(old_options, new_options) + config.delete(old_options) + + # migrate radius dynamic author / change of authorisation server + dae_old = new_base + ['authentication', 'radius', 'dae-server'] + if config.exists(dae_old): + config.rename(dae_old, 'dynamic-author') + dae_new = new_base + ['authentication', 'radius', 'dynamic-author'] + + if config.exists(dae_new + ['ip-address']): + config.rename(dae_new + ['ip-address'], 'server') + + if config.exists(dae_new + ['secret']): + config.rename(dae_new + ['secret'], 'key') + + + # migrate radius server + radius_server = new_base + ['authentication', 'radius-server'] + if config.exists(radius_server): + for server in config.list_nodes(radius_server): + base = radius_server + [server] + new = new_base + ['authentication', 'radius', 'server', server] + + # convert secret to key + if config.exists(base + ['secret']): + tmp = config.return_value(base + ['secret']) + config.set(new + ['key'], value=tmp) + + if config.exists(base + ['fail-time']): + tmp = config.return_value(base + ['fail-time']) + config.set(new + ['fail-time'], value=tmp) + + config.set_tag(new_base + ['authentication', 'radius', 'server']) + config.delete(radius_server) + + # migrate SSL certificates + old_ssl = new_base + ['sstp-settings', 'ssl-certs'] + new_ssl = new_base + ['ssl'] + config.copy(old_ssl, new_ssl) + config.delete(old_ssl) + + if config.exists(new_ssl + ['ca']): + config.rename(new_ssl + ['ca'], 'ca-cert-file') + + if config.exists(new_ssl + ['server-cert']): + config.rename(new_ssl + ['server-cert'], 'cert-file') + + if config.exists(new_ssl + ['server-key']): + config.rename(new_ssl + ['server-key'], 'key-file') + + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/sstp/1-to-2 b/src/migration-scripts/sstp/1-to-2 new file mode 100755 index 000000000..94cb04831 --- /dev/null +++ b/src/migration-scripts/sstp/1-to-2 @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# - migrate relative path SSL certificate to absolute path, as certs are only +# allowed to stored in /config/user-data/sstp/ this is pretty straight +# forward move. Delete certificates from source directory + +import os +import sys + +from shutil import copy2 +from stat import S_IRUSR, S_IWUSR, S_IRGRP, S_IROTH +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base_path = ['vpn', 'sstp', 'ssl'] +if not config.exists(base_path): + # Nothing to do + sys.exit(0) +else: + cert_path_old ='/config/user-data/sstp/' + cert_path_new ='/config/auth/sstp/' + + if not os.path.isdir(cert_path_new): + os.mkdir(cert_path_new) + + # + # migrate ca-cert-file to new path + if config.exists(base_path + ['ca-cert-file']): + tmp = config.return_value(base_path + ['ca-cert-file']) + cert_old = cert_path_old + tmp + cert_new = cert_path_new + tmp + + if os.path.isfile(cert_old): + # adjust file permissions on source file, + # permissions will be copied by copy2() + os.chmod(cert_old, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) + copy2(cert_old, cert_path_new) + # delete old certificate file + os.unlink(cert_old) + + config.set(base_path + ['ca-cert-file'], value=cert_new, replace=True) + + # + # migrate cert-file to new path + if config.exists(base_path + ['cert-file']): + tmp = config.return_value(base_path + ['cert-file']) + cert_old = cert_path_old + tmp + cert_new = cert_path_new + tmp + + if os.path.isfile(cert_old): + # adjust file permissions on source file, + # permissions will be copied by copy2() + os.chmod(cert_old, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) + copy2(cert_old, cert_path_new) + # delete old certificate file + os.unlink(cert_old) + + config.set(base_path + ['cert-file'], value=cert_new, replace=True) + + # + # migrate key-file to new path + if config.exists(base_path + ['key-file']): + tmp = config.return_value(base_path + ['key-file']) + cert_old = cert_path_old + tmp + cert_new = cert_path_new + tmp + + if os.path.isfile(cert_old): + # adjust file permissions on source file, + # permissions will be copied by copy2() + os.chmod(cert_old, S_IRUSR | S_IWUSR) + copy2(cert_old, cert_path_new) + # delete old certificate file + os.unlink(cert_old) + + config.set(base_path + ['key-file'], value=cert_new, replace=True) + + # + # check if old certificate directory exists but is empty + if os.path.isdir(cert_path_old) and not os.listdir(cert_path_old): + os.rmdir(cert_path_old) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/10-to-11 b/src/migration-scripts/system/10-to-11 new file mode 100755 index 000000000..1a0233c7d --- /dev/null +++ b/src/migration-scripts/system/10-to-11 @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +# Unclutter RADIUS configuration +# +# Move radius-server top level tag nodes to a regular node which allows us +# to specify additional general features for the RADIUS client. + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +cfg_base = ['system', 'login'] +if not (config.exists(cfg_base + ['radius-server']) or config.exists(cfg_base + ['radius-source-address'])): + # Nothing to do + sys.exit(0) +else: + # + # Migrate "system login radius-source-address" to "system login radius" + # + if config.exists(cfg_base + ['radius-source-address']): + address = config.return_value(cfg_base + ['radius-source-address']) + # delete old configuration node + config.delete(cfg_base + ['radius-source-address']) + # write new configuration node + config.set(cfg_base + ['radius', 'source-address'], value=address) + + # + # Migrate "system login radius-server" tag node to new + # "system login radius server" tag node and also rename the "secret" node to "key" + # + for server in config.list_nodes(cfg_base + ['radius-server']): + base_server = cfg_base + ['radius-server', server] + # "key" node is mandatory + key = config.return_value(base_server + ['secret']) + config.set(cfg_base + ['radius', 'server', server, 'key'], value=key) + + # "port" is optional + if config.exists(base_server + ['port']): + port = config.return_value(base_server + ['port']) + config.set(cfg_base + ['radius', 'server', server, 'port'], value=port) + + # "timeout is optional" + if config.exists(base_server + ['timeout']): + timeout = config.return_value(base_server + ['timeout']) + config.set(cfg_base + ['radius', 'server', server, 'timeout'], value=timeout) + + # format as tag node + config.set_tag(cfg_base + ['radius', 'server']) + + # delete old configuration node + config.delete(base_server) + + # delete top level tag node + if config.exists(cfg_base + ['radius-server']): + config.delete(cfg_base + ['radius-server']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/11-to-12 b/src/migration-scripts/system/11-to-12 new file mode 100755 index 000000000..0c92a0746 --- /dev/null +++ b/src/migration-scripts/system/11-to-12 @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +# converts 'set system syslog host <address>:<port>' +# to 'set system syslog host <address> port <port>' + +import sys +import re + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +cbase = ['system', 'syslog', 'host'] + +if not config.exists(cbase): + sys.exit(0) + +for host in config.list_nodes(cbase): + if re.search(':[0-9]{1,5}$',host): + h = re.search('^[a-zA-Z\-0-9\.]+', host).group(0) + p = re.sub(':', '', re.search(':[0-9]+$', host).group(0)) + config.set(cbase + [h]) + config.set(cbase + [h, 'port'], value=p) + for fac in config.list_nodes(cbase + [host, 'facility']): + config.set(cbase + [h, 'facility', fac]) + config.set_tag(cbase + [h, 'facility']) + if config.exists(cbase + [host, 'facility', fac, 'protocol']): + proto = config.return_value(cbase + [host, 'facility', fac, 'protocol']) + config.set(cbase + [h, 'facility', fac, 'protocol'], value=proto) + if config.exists(cbase + [host, 'facility', fac, 'level']): + lvl = config.return_value(cbase + [host, 'facility', fac, 'level']) + config.set(cbase + [h, 'facility', fac, 'level'], value=lvl) + config.delete(cbase + [host]) + + try: + open(file_name,'w').write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/12-to-13 b/src/migration-scripts/system/12-to-13 new file mode 100755 index 000000000..5b068f4fc --- /dev/null +++ b/src/migration-scripts/system/12-to-13 @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +# Fixup non existent time-zones. Some systems have time-zone set to: Los* +# (Los_Angeles), Den* (Denver), New* (New_York) ... but those are no real IANA +# assigned time zones. In the past they have been silently remapped. +# +# Time to clean it up! +# +# Migrate all configured timezones to real IANA assigned timezones! + +import re +import sys + +from vyos.configtree import ConfigTree +from vyos.util import cmd + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +tz_base = ['system', 'time-zone'] +if not config.exists(tz_base): + # Nothing to do + sys.exit(0) +else: + tz = config.return_value(tz_base) + + # retrieve all valid timezones + try: + tz_datas = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::') + except OSError: + tz_datas = '' + tz_data = tz_datas.split('\n') + + if re.match(r'[Ll][Oo][Ss].+', tz): + tz = 'America/Los_Angeles' + elif re.match(r'[Dd][Ee][Nn].+', tz): + tz = 'America/Denver' + elif re.match(r'[Hh][Oo][Nn][Oo].+', tz): + tz = 'Pacific/Honolulu' + elif re.match(r'[Nn][Ee][Ww].+', tz): + tz = 'America/New_York' + elif re.match(r'[Cc][Hh][Ii][Cc]*.+', tz): + tz = 'America/Chicago' + elif re.match(r'[Aa][Nn][Cc].+', tz): + tz = 'America/Anchorage' + elif re.match(r'[Pp][Hh][Oo].+', tz): + tz = 'America/Phoenix' + elif re.match(r'GMT(.+)?', tz): + tz = 'Etc/' + tz + elif tz not in tz_data: + # assign default UTC timezone + tz = 'UTC' + + # replace timezone data is required + config.set(tz_base, value=tz) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/13-to-14 b/src/migration-scripts/system/13-to-14 new file mode 100755 index 000000000..c055dad1f --- /dev/null +++ b/src/migration-scripts/system/13-to-14 @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# +# Delete 'system ipv6 blacklist' option as the IPv6 module can no longer be +# blacklisted as it is required by e.g. WireGuard and thus will always be +# loaded. + +import os +import sys + +ipv6_blacklist_file = '/etc/modprobe.d/vyatta_blacklist_ipv6.conf' + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +ip_base = ['system', 'ipv6'] +if not config.exists(ip_base): + # Nothing to do + sys.exit(0) +else: + # delete 'system ipv6 blacklist' node + if config.exists(ip_base + ['blacklist']): + config.delete(ip_base + ['blacklist']) + if os.path.isfile(ipv6_blacklist_file): + os.unlink(ipv6_blacklist_file) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/14-to-15 b/src/migration-scripts/system/14-to-15 new file mode 100755 index 000000000..2491e3d0d --- /dev/null +++ b/src/migration-scripts/system/14-to-15 @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# +# Make 'system options reboot-on-panic' valueless + +import os +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['system', 'options'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + if config.exists(base + ['reboot-on-panic']): + reboot = config.return_value(base + ['reboot-on-panic']) + config.delete(base + ['reboot-on-panic']) + # create new valueless node if action was true + if reboot == "true": + config.set(base + ['reboot-on-panic']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/15-to-16 b/src/migration-scripts/system/15-to-16 new file mode 100755 index 000000000..e70893d55 --- /dev/null +++ b/src/migration-scripts/system/15-to-16 @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# * remove "system login user <user> group" node, Why should be add a user to a +# 3rd party group when the system is fully managed by CLI? +# * remove "system login user <user> level" node +# This is the only privilege level left and also the default, what is the +# sense in keeping this orphaned node? + +import os +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['system', 'login', 'user'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + for user in config.list_nodes(base): + if config.exists(base + [user, 'group']): + config.delete(base + [user, 'group']) + + if config.exists(base + [user, 'level']): + config.delete(base + [user, 'level']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/16-to-17 b/src/migration-scripts/system/16-to-17 new file mode 100755 index 000000000..8f762c0e2 --- /dev/null +++ b/src/migration-scripts/system/16-to-17 @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# remove "system console netconsole" +# remove "system console device <device> modem" + +import os +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['system', 'console'] +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # remove "system console netconsole" (T2561) + if config.exists(base + ['netconsole']): + config.delete(base + ['netconsole']) + + if config.exists(base + ['device']): + for device in config.list_nodes(base + ['device']): + dev_path = base + ['device', device] + # remove "system console device <device> modem" (T2570) + if config.exists(dev_path + ['modem']): + config.delete(dev_path + ['modem']) + + # Only continue on USB based serial consoles + if not 'ttyUSB' in device: + continue + + # A serial console has been configured but it does no longer + # exist on the system - cleanup + if not os.path.exists(f'/dev/{device}'): + config.delete(dev_path) + continue + + # migrate from ttyUSB device to new device in /dev/serial/by-bus + for root, dirs, files in os.walk('/dev/serial/by-bus'): + for usb_device in files: + device_file = os.path.realpath(os.path.join(root, usb_device)) + # migrate to new USB device names (T2529) + if os.path.basename(device_file) == device: + config.copy(dev_path, base + ['device', usb_device]) + # Delete old USB node from config + config.delete(dev_path) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/17-to-18 b/src/migration-scripts/system/17-to-18 new file mode 100755 index 000000000..dd2abce00 --- /dev/null +++ b/src/migration-scripts/system/17-to-18 @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# migrate disable-dhcp-nameservers (boolean) to name-servers-dhcp <interface> +# if disable-dhcp-nameservers is set, just remove it +# else retrieve all interface names that have configured dhcp(v6) address and +# add them to the new name-servers-dhcp node + +from sys import argv, exit +from vyos.ifconfig import Interface +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['system'] +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['disable-dhcp-nameservers']): + config.delete(base + ['disable-dhcp-nameservers']) +else: + dhcp_interfaces = [] + + # go through all interfaces searching for 'address dhcp(v6)?' + for sect in Interface.sections(): + sect_base = ['interfaces', sect] + + if not config.exists(sect_base): + continue + + for intf in config.list_nodes(sect_base): + intf_base = sect_base + [intf] + + # try without vlans + if config.exists(intf_base + ['address']): + for addr in config.return_values(intf_base + ['address']): + if addr in ['dhcp', 'dhcpv6']: + dhcp_interfaces.append(intf) + + # try vif + if config.exists(intf_base + ['vif']): + for vif in config.list_nodes(intf_base + ['vif']): + vif_base = intf_base + ['vif', vif] + if config.exists(vif_base + ['address']): + for addr in config.return_values(vif_base + ['address']): + if addr in ['dhcp', 'dhcpv6']: + dhcp_interfaces.append(f'{intf}.{vif}') + + # try vif-s + if config.exists(intf_base + ['vif-s']): + for vif_s in config.list_nodes(intf_base + ['vif-s']): + vif_s_base = intf_base + ['vif-s', vif_s] + if config.exists(vif_s_base + ['address']): + for addr in config.return_values(vif_s_base + ['address']): + if addr in ['dhcp', 'dhcpv6']: + dhcp_interfaces.append(f'{intf}.{vif_s}') + + # try vif-c + if config.exists(intf_base + ['vif-c', vif_c]): + for vif_c in config.list_nodes(vif_s_base + ['vif-c', vif_c]): + vif_c_base = vif_s_base + ['vif-c', vif_c] + if config.exists(vif_c_base + ['address']): + for addr in config.return_values(vif_c_base + ['address']): + if addr in ['dhcp', 'dhcpv6']: + dhcp_interfaces.append(f'{intf}.{vif_s}.{vif_c}') + + # set new config nodes + for intf in dhcp_interfaces: + config.set(base + ['name-servers-dhcp'], value=intf, replace=False) + + # delete old node + config.delete(base + ['disable-dhcp-nameservers']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) + +exit(0) diff --git a/src/migration-scripts/system/6-to-7 b/src/migration-scripts/system/6-to-7 new file mode 100755 index 000000000..bf07abf3a --- /dev/null +++ b/src/migration-scripts/system/6-to-7 @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +# Change smp_affinity to smp-affinity + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +update_required = False + +intf_types = config.list_nodes(["interfaces"]) + +for intf_type in intf_types: + intf_type_path = ["interfaces", intf_type] + intfs = config.list_nodes(intf_type_path) + + for intf in intfs: + intf_path = intf_type_path + [intf] + if not config.exists(intf_path + ["smp_affinity"]): + # Nothing to do. + continue + else: + # Rename the node. + old_smp_affinity_path = intf_path + ["smp_affinity"] + config.rename(old_smp_affinity_path, "smp-affinity") + update_required = True + +if update_required: + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("failed to save the modified config: {}".format(e)) + sys.exit(1) + + + diff --git a/src/migration-scripts/system/7-to-8 b/src/migration-scripts/system/7-to-8 new file mode 100755 index 000000000..4cbb21f17 --- /dev/null +++ b/src/migration-scripts/system/7-to-8 @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +# Converts "system gateway-address" option to "protocols static route 0.0.0.0/0 next-hop $gw" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['system', 'gateway-address']): + # Nothing to do + sys.exit(0) +else: + # Save the address + gw = config.return_value(['system', 'gateway-address']) + + # Create the node for the new syntax + # Note: next-hop is a tag node, gateway address is its child, not a value + config.set(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', gw]) + + # Delete the node with the old syntax + config.delete(['system', 'gateway-address']) + + # Now, the interesting part. Both route and next-hop are supposed to be tag nodes, + # which you can verify with "cli-shell-api isTag $configPath". + # They must be formatted as such to load correctly. + config.set_tag(['protocols', 'static', 'route']) + config.set_tag(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/8-to-9 b/src/migration-scripts/system/8-to-9 new file mode 100755 index 000000000..db3fefdea --- /dev/null +++ b/src/migration-scripts/system/8-to-9 @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +# Deletes "system package" option as it is deprecated + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['system', 'package']): + # Nothing to do + sys.exit(0) +else: + # Delete the node with the old syntax + config.delete(['system', 'package']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/9-to-10 b/src/migration-scripts/system/9-to-10 new file mode 100755 index 000000000..3c49f0d95 --- /dev/null +++ b/src/migration-scripts/system/9-to-10 @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +# Operator accounts have been deprecated due to a security issue. Those accounts +# will be converted to regular admin accounts. + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base_level = ['system', 'login', 'user'] + +if not config.exists(base_level): + # Nothing to do, which shouldn't happen anyway + # only if you wipe the config and reboot. + sys.exit(0) +else: + for user in config.list_nodes(base_level): + if config.exists(base_level + [user, 'level']): + if config.return_value(base_level + [user, 'level']) == 'operator': + config.set(base_level + [user, 'level'], value="admin", replace=True) + + try: + open(file_name,'w').write(config.to_string()) + + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/vrrp/1-to-2 b/src/migration-scripts/vrrp/1-to-2 new file mode 100755 index 000000000..b2e61dd38 --- /dev/null +++ b/src/migration-scripts/vrrp/1-to-2 @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import re +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +# Convert the old VRRP syntax to the new syntax + +# The old approach was to put VRRP groups inside interfaces, +# as in "interfaces ethernet eth0 vrrp vrrp-group 10 ...". +# It was supported only under ethernet and bonding and their +# respective vif, vif-s, and vif-c subinterfaces + +def get_vrrp_group(path): + group = {"preempt": True, "rfc_compatibility": False, "disable": False} + + if config.exists(path + ["advertise-interval"]): + group["advertise_interval"] = config.return_value(path + ["advertise-interval"]) + + if config.exists(path + ["description"]): + group["description"] = config.return_value(path + ["description"]) + + if config.exists(path + ["disable"]): + group["disable"] = True + + if config.exists(path + ["hello-source-address"]): + group["hello_source"] = config.return_value(path + ["hello-source-address"]) + + # 1.1.8 didn't have it, but earlier 1.2.0 did, we don't want to break + # configs of early adopters! + if config.exists(path + ["peer-address"]): + group["peer_address"] = config.return_value(path + ["peer-address"]) + + if config.exists(path + ["preempt"]): + preempt = config.return_value(path + ["preempt"]) + if preempt == "false": + group["preempt"] = False + + if config.exists(path + ["rfc3768-compatibility"]): + group["rfc_compatibility"] = True + + if config.exists(path + ["preempt-delay"]): + group["preempt_delay"] = config.return_value(path + ["preempt-delay"]) + + if config.exists(path + ["priority"]): + group["priority"] = config.return_value(path + ["priority"]) + + if config.exists(path + ["sync-group"]): + group["sync_group"] = config.return_value(path + ["sync-group"]) + + if config.exists(path + ["authentication", "type"]): + group["auth_type"] = config.return_value(path + ["authentication", "type"]) + + if config.exists(path + ["authentication", "password"]): + group["auth_password"] = config.return_value(path + ["authentication", "password"]) + + if config.exists(path + ["virtual-address"]): + group["virtual_addresses"] = config.return_values(path + ["virtual-address"]) + + if config.exists(path + ["run-transition-scripts"]): + if config.exists(path + ["run-transition-scripts", "master"]): + group["master_script"] = config.return_value(path + ["run-transition-scripts", "master"]) + if config.exists(path + ["run-transition-scripts", "backup"]): + group["backup_script"] = config.return_value(path + ["run-transition-scripts", "backup"]) + if config.exists(path + ["run-transition-scripts", "fault"]): + group["fault_script"] = config.return_value(path + ["run-transition-scripts", "fault"]) + + # Also not present in 1.1.8, but supported by earlier 1.2.0 + if config.exists(path + ["health-check"]): + if config.exists(path + ["health-check", "interval"]): + group["health_check_interval"] = config.return_value(path + ["health-check", "interval"]) + if config.exists(path + ["health-check", "failure-count"]): + group["health_check_count"] = config.return_value(path + ["health-check", "failure-count"]) + if config.exists(path + ["health-check", "script"]): + group["health_check_script"] = config.return_value(path + ["health-check", "script"]) + + return group + +# Since VRRP is all over the place, there's no way to just check a path and exit early +# if it doesn't exist, we have to walk all interfaces and collect VRRP settings from them. +# Only if no data is collected from any interface we can conclude that VRRP is not configured +# and exit. + +groups = [] +base_paths = [] + +if config.exists(["interfaces", "ethernet"]): + base_paths.append("ethernet") +if config.exists(["interfaces", "bonding"]): + base_paths.append("bonding") + +for bp in base_paths: + parent_path = ["interfaces", bp] + + parent_intfs = config.list_nodes(parent_path) + + for pi in parent_intfs: + # Extract VRRP groups from the parent interface + vg_path =[pi, "vrrp", "vrrp-group"] + if config.exists(parent_path + vg_path): + pgroups = config.list_nodes(parent_path + vg_path) + for pg in pgroups: + g = get_vrrp_group(parent_path + vg_path + [pg]) + g["interface"] = pi + g["vrid"] = pg + groups.append(g) + + # Delete the VRRP subtree + # If left in place, configs will not load correctly + config.delete(parent_path + [pi, "vrrp"]) + + # Extract VRRP groups from 802.1q VLAN interfaces + if config.exists(parent_path + [pi, "vif"]): + vifs = config.list_nodes(parent_path + [pi, "vif"]) + for vif in vifs: + vif_vg_path = [pi, "vif", vif, "vrrp", "vrrp-group"] + if config.exists(parent_path + vif_vg_path): + vifgroups = config.list_nodes(parent_path + vif_vg_path) + for vif_group in vifgroups: + g = get_vrrp_group(parent_path + vif_vg_path + [vif_group]) + g["interface"] = "{0}.{1}".format(pi, vif) + g["vrid"] = vif_group + groups.append(g) + + config.delete(parent_path + [pi, "vif", vif, "vrrp"]) + + # Extract VRRP groups from 802.3ad QinQ service VLAN interfaces + if config.exists(parent_path + [pi, "vif-s"]): + vif_ss = config.list_nodes(parent_path + [pi, "vif-s"]) + for vif_s in vif_ss: + vifs_vg_path = [pi, "vif-s", vif_s, "vrrp", "vrrp-group"] + if config.exists(parent_path + vifs_vg_path): + vifsgroups = config.list_nodes(parent_path + vifs_vg_path) + for vifs_group in vifsgroups: + g = get_vrrp_group(parent_path + vifs_vg_path + [vifs_group]) + g["interface"] = "{0}.{1}".format(pi, vif_s) + g["vrid"] = vifs_group + groups.append(g) + + config.delete(parent_path + [pi, "vif-s", vif_s, "vrrp"]) + + # Extract VRRP groups from QinQ client VLAN interfaces nested in the vif-s + if config.exists(parent_path + [pi, "vif-s", vif_s, "vif-c"]): + vif_cs = config.list_nodes(parent_path + [pi, "vif-s", vif_s, "vif-c"]) + for vif_c in vif_cs: + vifc_vg_path = [pi, "vif-s", vif_s, "vif-c", vif_c, "vrrp", "vrrp-group"] + vifcgroups = config.list_nodes(parent_path + vifc_vg_path) + for vifc_group in vifcgroups: + g = get_vrrp_group(parent_path + vifc_vg_path + [vifc_group]) + g["interface"] = "{0}.{1}.{2}".format(pi, vif_s, vif_c) + g["vrid"] = vifc_group + groups.append(g) + + config.delete(parent_path + [pi, "vif-s", vif_s, "vif-c", vif_c, "vrrp"]) + +# If nothing was collected before this point, it means the config has no VRRP setup +if not groups: + sys.exit(0) + +# Otherwise, there is VRRP to convert + +# Now convert the collected groups to the new syntax +base_group_path = ["high-availability", "vrrp", "group"] +sync_path = ["high-availability", "vrrp", "sync-group"] + +for g in groups: + group_name = "{0}-{1}".format(g["interface"], g["vrid"]) + group_path = base_group_path + [group_name] + + config.set(group_path + ["interface"], value=g["interface"]) + config.set(group_path + ["vrid"], value=g["vrid"]) + + if "advertise_interval" in g: + config.set(group_path + ["advertise-interval"], value=g["advertise_interval"]) + + if "priority" in g: + config.set(group_path + ["priority"], value=g["priority"]) + + if not g["preempt"]: + config.set(group_path + ["no-preempt"], value=None) + + if "preempt_delay" in g: + config.set(group_path + ["preempt-delay"], value=g["preempt_delay"]) + + if g["rfc_compatibility"]: + config.set(group_path + ["rfc3768-compatibility"], value=None) + + if g["disable"]: + config.set(group_path + ["disable"], value=None) + + if "hello_source" in g: + config.set(group_path + ["hello-source-address"], value=g["hello_source"]) + + if "peer_address" in g: + config.set(group_path + ["peer-address"], value=g["peer_address"]) + + if "auth_password" in g: + config.set(group_path + ["authentication", "password"], value=g["auth_password"]) + if "auth_type" in g: + config.set(group_path + ["authentication", "type"], value=g["auth_type"]) + + if "master_script" in g: + config.set(group_path + ["transition-script", "master"], value=g["master_script"]) + if "backup_script" in g: + config.set(group_path + ["transition-script", "backup"], value=g["backup_script"]) + if "fault_script" in g: + config.set(group_path + ["transition-script", "fault"], value=g["fault_script"]) + + if "health_check_interval" in g: + config.set(group_path + ["health-check", "interval"], value=g["health_check_interval"]) + if "health_check_count" in g: + config.set(group_path + ["health-check", "failure-count"], value=g["health_check_count"]) + if "health_check_script" in g: + config.set(group_path + ["health-check", "script"], value=g["health_check_script"]) + + # Not that it should ever be absent... + if "virtual_addresses" in g: + # The new CLI disallows addresses without prefix length + # Pre-rewrite configs didn't support IPv6 VRRP, but handle it anyway + for va in g["virtual_addresses"]: + if not re.search(r'/', va): + if re.search(r':', va): + va = "{0}/128".format(va) + else: + va = "{0}/32".format(va) + config.set(group_path + ["virtual-address"], value=va, replace=False) + + # Sync group + if "sync_group" in g: + config.set(sync_path + [g["sync_group"], "member"], value=group_name, replace=False) + +# Set the tag flag +config.set_tag(base_group_path) +if config.exists(sync_path): + config.set_tag(sync_path) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/webproxy/1-to-2 b/src/migration-scripts/webproxy/1-to-2 new file mode 100755 index 000000000..070ff356d --- /dev/null +++ b/src/migration-scripts/webproxy/1-to-2 @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# migrate old style `webproxy proxy-bypass 1.2.3.4/24` +# to new style `webproxy whitelist destination-address 1.2.3.4/24` + +import sys + +from vyos.configtree import ConfigTree + +if len(sys.argv) < 1: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +cfg_webproxy_base = ['service', 'webproxy'] +if not config.exists(cfg_webproxy_base + ['proxy-bypass']): + # Nothing to do + sys.exit(0) +else: + bypass_addresses = config.return_values(cfg_webproxy_base + ['proxy-bypass']) + # delete old configuration node + config.delete(cfg_webproxy_base + ['proxy-bypass']) + for bypass_address in bypass_addresses: + # add data to new configuration node + config.set(cfg_webproxy_base + ['whitelist', 'destination-address'], value=bypass_address, replace=False) + + # save updated configuration + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/op_mode/anyconnect-control.py b/src/op_mode/anyconnect-control.py new file mode 100755 index 000000000..6382016b7 --- /dev/null +++ b/src/op_mode/anyconnect-control.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import argparse +import json + +from vyos.config import Config +from vyos.util import popen, run, DEVNULL +from tabulate import tabulate + +occtl = '/usr/bin/occtl' +occtl_socket = '/run/ocserv/occtl.socket' + +def show_sessions(): + out, code = popen("sudo {0} -j -s {1} show users".format(occtl, occtl_socket),stderr=DEVNULL) + if code: + sys.exit('Cannot get anyconnect users information') + else: + headers = ["interface", "username", "ip", "remote IP", "RX", "TX", "state", "uptime"] + sessions = json.loads(out) + ses_list = [] + for ses in sessions: + ses_list.append([ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]]) + if len(ses_list) > 0: + print(tabulate(ses_list, headers)) + else: + print("No active anyconnect sessions") + +def is_ocserv_configured(): + if not Config().exists_effective('vpn anyconnect'): + print("vpn anyconnect server is not configured") + sys.exit(1) + +def main(): + #parese args + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Control action', required=True) + parser.add_argument('--selector', help='Selector username|ifname|sid', required=False) + parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False) + args = parser.parse_args() + + + # Check is IPoE configured + is_ocserv_configured() + + if args.action == "restart": + run("systemctl restart ocserv") + sys.exit(0) + elif args.action == "show_sessions": + show_sessions() + +if __name__ == '__main__': + main() diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py new file mode 100755 index 000000000..423694187 --- /dev/null +++ b/src/op_mode/clear_conntrack.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys + +from vyos.util import ask_yes_no +from vyos.util import cmd, DEVNULL + +if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"): + sys.exit(1) +else: + cmd('/usr/sbin/conntrack -F', stderr=DEVNULL) + cmd('/usr/sbin/conntrack -F expect', stderr=DEVNULL) diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py new file mode 100755 index 000000000..a773aa28e --- /dev/null +++ b/src/op_mode/connect_disconnect.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import argparse + +from sys import exit +from psutil import process_iter +from time import strftime, localtime, time + +from vyos.util import call + +def check_interface(interface): + if not os.path.isfile(f'/etc/ppp/peers/{interface}'): + print(f'Interface {interface}: invalid!') + exit(1) + +def check_ppp_running(interface): + """ + Check if ppp process is running in the interface in question + """ + for p in process_iter(): + if "pppd" in p.name(): + if interface in p.cmdline(): + return True + + return False + +def connect(interface): + """ + Connect PPP interface + """ + check_interface(interface) + + # Check if interface is already dialed + if os.path.isdir(f'/sys/class/net/{interface}'): + print(f'Interface {interface}: already connected!') + elif check_ppp_running(interface): + print(f'Interface {interface}: connection is beeing established!') + else: + print(f'Interface {interface}: connecting...') + call(f'systemctl restart ppp@{interface}.service') + +def disconnect(interface): + """ + Disconnect PPP interface + """ + check_interface(interface) + + # Check if interface is already down + if not check_ppp_running(interface): + print(f'Interface {interface}: connection is already down') + else: + print(f'Interface {interface}: disconnecting...') + call(f'systemctl stop ppp@{interface}.service') + +def main(): + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store") + group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store") + args = parser.parse_args() + + if args.connect: + connect(args.connect) + elif args.disconnect: + disconnect(args.disconnect) + else: + parser.print_help() + + exit(0) + +if __name__ == '__main__': + main() diff --git a/src/op_mode/cpu_summary.py b/src/op_mode/cpu_summary.py new file mode 100755 index 000000000..cfd321522 --- /dev/null +++ b/src/op_mode/cpu_summary.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +from vyos.util import colon_separated_to_dict + +FILE_NAME = '/proc/cpuinfo' + +with open(FILE_NAME, 'r') as f: + data_raw = f.read() + +data = colon_separated_to_dict(data_raw) + +# Accumulate all data in a dict for future support for machine-readable output +cpu_data = {} +cpu_data['cpu_number'] = len(data['processor']) +cpu_data['models'] = list(set(data['model name'])) + +# Strip extra whitespace from CPU model names, /proc/cpuinfo is prone to that +cpu_data['models'] = map(lambda s: re.sub(r'\s+', ' ', s), cpu_data['models']) + +print("CPU(s): {0}".format(cpu_data['cpu_number'])) +print("CPU model(s): {0}".format(",".join(cpu_data['models']))) diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py new file mode 100755 index 000000000..bfc640a26 --- /dev/null +++ b/src/op_mode/dns_forwarding_reset.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# File: vyos-show-version +# Purpose: +# Displays image version and system information. +# Used by the "run show version" command. + + +import os +import argparse + +from sys import exit +from vyos.config import Config +from vyos.util import call + +PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns' + +parser = argparse.ArgumentParser() +parser.add_argument("-a", "--all", action="store_true", help="Reset all cache") +parser.add_argument("domain", type=str, nargs="?", help="Domain to reset cache entries for") + +if __name__ == '__main__': + args = parser.parse_args() + + # Do nothing if service is not configured + c = Config() + if not c.exists_effective(['service', 'dns', 'forwarding']): + print("DNS forwarding is not configured") + exit(0) + + if args.all: + call(f"{PDNS_CMD} wipe-cache \'.$\'") + exit(0) + + elif args.domain: + call(f"{PDNS_CMD} wipe-cache \'{0}$\'".format(args.domain)) + + else: + parser.print_help() + exit(1) diff --git a/src/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh new file mode 100755 index 000000000..64cc92115 --- /dev/null +++ b/src/op_mode/dns_forwarding_restart.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +if cli-shell-api existsEffective service dns forwarding; then + echo "Restarting the DNS forwarding service" + systemctl restart pdns-recursor.service +else + echo "DNS forwarding is not configured" +fi diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py new file mode 100755 index 000000000..1fb61d263 --- /dev/null +++ b/src/op_mode/dns_forwarding_statistics.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 + +import jinja2 +from sys import exit + +from vyos.config import Config +from vyos.util import cmd + +PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns' + +OUT_TMPL_SRC = """ +DNS forwarding statistics: + +Cache entries: {{ cache_entries -}} +Cache size: {{ cache_size }} kbytes + +""" + +if __name__ == '__main__': + # Do nothing if service is not configured + c = Config() + if not c.exists_effective('service dns forwarding'): + print("DNS forwarding is not configured") + exit(0) + + data = {} + + data['cache_entries'] = cmd(f'{PDNS_CMD} get cache-entries') + data['cache_size'] = "{0:.2f}".format( int(cmd(f'{PDNS_CMD} get cache-bytes')) / 1024 ) + + tmpl = jinja2.Template(OUT_TMPL_SRC) + print(tmpl.render(data)) diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py new file mode 100755 index 000000000..021acfd73 --- /dev/null +++ b/src/op_mode/dynamic_dns.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import argparse +import jinja2 +import sys +import time + +from vyos.config import Config +from vyos.util import call + +cache_file = r'/run/ddclient/ddclient.cache' + +OUT_TMPL_SRC = """ +{%- for entry in hosts -%} +ip address : {{ entry.ip }} +host-name : {{ entry.host }} +last update : {{ entry.time }} +update-status: {{ entry.status }} + +{% endfor -%} +""" + +def show_status(): + data = { + 'hosts': [] + } + + with open(cache_file, 'r') as f: + for line in f: + if line.startswith('#'): + continue + + outp = { + 'host': '', + 'ip': '', + 'time': '' + } + + if 'host=' in line: + host = line.split('host=')[1] + if host: + outp['host'] = host.split(',')[0] + + if 'ip=' in line: + ip = line.split('ip=')[1] + if ip: + outp['ip'] = ip.split(',')[0] + + if 'atime=' in line: + atime = line.split('atime=')[1] + if atime: + tmp = atime.split(',')[0] + outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(tmp, base=10))) + + if 'status=' in line: + status = line.split('status=')[1] + if status: + outp['status'] = status.split(',')[0] + + data['hosts'].append(outp) + + tmpl = jinja2.Template(OUT_TMPL_SRC) + print(tmpl.render(data)) + + +def update_ddns(): + call('systemctl stop ddclient.service') + if os.path.exists(cache_file): + os.remove(cache_file) + call('systemctl start ddclient.service') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + group.add_argument("--status", help="Show DDNS status", action="store_true") + group.add_argument("--update", help="Update DDNS on a given interface", action="store_true") + args = parser.parse_args() + + # Do nothing if service is not configured + c = Config() + if not c.exists_effective('service dns dynamic'): + print("Dynamic DNS not configured") + sys.exit(1) + + if args.status: + show_status() + elif args.update: + update_ddns() diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py new file mode 100755 index 000000000..6586cbceb --- /dev/null +++ b/src/op_mode/flow_accounting_op.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys +import argparse +import re +import ipaddress +import os.path +from tabulate import tabulate +from json import loads +from vyos.util import cmd, run +from vyos.logger import syslog + +# some default values +uacctd_pidfile = '/var/run/uacctd.pid' +uacctd_pipefile = '/tmp/uacctd.pipe' + +def parse_port(port): + try: + port_num = int(port) + if (port_num >= 0) and (port_num <= 65535): + return port_num + else: + raise ValueError("out of the 0-65535 range".format(port)) + except ValueError as e: + raise ValueError("Incorrect port number \'{0}\': {1}".format(port, e)) + +def parse_ports(arg): + if re.match(r'^\d+$', arg): + # Single port + port = parse_port(arg) + return {"type": "single", "value": port} + elif re.match(r'^\d+\-\d+$', arg): + # Port range + ports = arg.split("-") + ports = list(map(parse_port, ports)) + if ports[0] > ports[1]: + raise ValueError("Malformed port range \'{0}\': lower end is greater than the higher".format(arg)) + else: + return {"type": "range", "value": (ports[0], ports[1])} + elif re.match(r'^\d+,.*\d$', arg): + # Port list + ports = re.split(r',+', arg) # This allows duplicate commad like '1,,2,3,4' + ports = list(map(parse_port, ports)) + return {"type": "list", "value": ports} + else: + raise ValueError("Malformed port spec \'{0}\'".format(arg)) + +# check if host argument have correct format +def check_host(host): + # define regex for checking + if not ipaddress.ip_address(host): + raise ValueError("Invalid host \'{}\', must be a valid IP or IPv6 address".format(host)) + +# check if flow-accounting running +def _uacctd_running(): + command = 'systemctl status uacctd.service > /dev/null' + return run(command) == 0 + + +# get list of interfaces +def _get_ifaces_dict(): + # run command to get ifaces list + out = cmd('/bin/ip link show') + + # read output + ifaces_out = out.splitlines() + + # make a dictionary with interfaces and indexes + ifaces_dict = {} + regex_filter = re.compile(r'^(?P<iface_index>\d+):\ (?P<iface_name>[\w\d\.]+)[:@].*$') + for iface_line in ifaces_out: + if regex_filter.search(iface_line): + ifaces_dict[int(regex_filter.search(iface_line).group('iface_index'))] = regex_filter.search(iface_line).group('iface_name') + + # return dictioanry + return ifaces_dict + + +# get list of flows +def _get_flows_list(): + # run command to get flows list + out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}', + message='Failed to get flows list') + + # read output + flows_out = out.splitlines() + + # make a list with flows + flows_list = [] + for flow_line in flows_out: + try: + flows_list.append(loads(flow_line)) + except Exception as err: + syslog.error('Unable to read flow info: {}'.format(err)) + + # return list of flows + return flows_list + + +# filter and format flows +def _flows_filter(flows, ifaces): + # predefine filtered flows list + flows_filtered = [] + + # add interface names to flows + for flow in flows: + if flow['iface_in'] in ifaces: + flow['iface_in_name'] = ifaces[flow['iface_in']] + else: + flow['iface_in_name'] = 'unknown' + + # iterate through flows list + for flow in flows: + # filter by interface + if cmd_args.interface: + if flow['iface_in_name'] != cmd_args.interface: + continue + # filter by host + if cmd_args.host: + if flow['ip_src'] != cmd_args.host and flow['ip_dst'] != cmd_args.host: + continue + # filter by ports + if cmd_args.ports: + if cmd_args.ports['type'] == 'single': + if flow['port_src'] != cmd_args.ports['value'] and flow['port_dst'] != cmd_args.ports['value']: + continue + else: + if flow['port_src'] not in cmd_args.ports['value'] and flow['port_dst'] not in cmd_args.ports['value']: + continue + # add filtered flows to new list + flows_filtered.append(flow) + + # stop adding if we already reached top count + if cmd_args.top: + if len(flows_filtered) == cmd_args.top: + break + + # return filtered flows + return flows_filtered + + +# print flow table +def _flows_table_print(flows): + # define headers and body + table_headers = ['IN_IFACE', 'SRC_MAC', 'DST_MAC', 'SRC_IP', 'DST_IP', 'SRC_PORT', 'DST_PORT', 'PROTOCOL', 'TOS', 'PACKETS', 'FLOWS', 'BYTES'] + table_body = [] + # convert flows to list + for flow in flows: + table_line = [ + flow.get('iface_in_name'), + flow.get('mac_src'), + flow.get('mac_dst'), + flow.get('ip_src'), + flow.get('ip_dst'), + flow.get('port_src'), + flow.get('port_dst'), + flow.get('ip_proto'), + flow.get('tos'), + flow.get('packets'), + flow.get('flows'), + flow.get('bytes') + ] + table_body.append(table_line) + # configure and fill table + table = tabulate(table_body, table_headers, tablefmt="simple") + + # print formatted table + try: + print(table) + except IOError: + sys.exit(0) + except KeyboardInterrupt: + sys.exit(0) + + +# check if in-memory table is active +def _check_imt(): + if not os.path.exists(uacctd_pipefile): + print("In-memory table is not available") + sys.exit(1) + + +# define program arguments +cmd_args_parser = argparse.ArgumentParser(description='show flow-accounting') +cmd_args_parser.add_argument('--action', choices=['show', 'clear', 'restart'], required=True, help='command to flow-accounting daemon') +cmd_args_parser.add_argument('--filter', choices=['interface', 'host', 'ports', 'top'], required=False, nargs='*', help='filter flows to display') +cmd_args_parser.add_argument('--interface', required=False, help='interface name for output filtration') +cmd_args_parser.add_argument('--host', type=str, required=False, help='host address for output filtering') +cmd_args_parser.add_argument('--ports', type=str, required=False, help='port number, range or list for output filtering') +cmd_args_parser.add_argument('--top', type=int, required=False, help='top records for output filtering') +# parse arguments +cmd_args = cmd_args_parser.parse_args() + +try: + if cmd_args.host: + check_host(cmd_args.host) + + if cmd_args.ports: + cmd_args.ports = parse_ports(cmd_args.ports) +except ValueError as e: + print(e) + sys.exit(1) + +# main logic +# do nothing if uacctd daemon is not running +if not _uacctd_running(): + print("flow-accounting is not active") + sys.exit(1) + +# restart pmacct daemon +if cmd_args.action == 'restart': + # run command to restart flow-accounting + cmd('systemctl restart uacctd.service', + message='Failed to restart flow-accounting') + +# clear in-memory collected flows +if cmd_args.action == 'clear': + _check_imt() + # run command to clear flows + cmd(f'/usr/bin/pmacct -e -p {uacctd_pipefile}', + message='Failed to clear flows') + +# show table with flows +if cmd_args.action == 'show': + _check_imt() + # get interfaces index and names + ifaces_dict = _get_ifaces_dict() + # get flows + flows_list = _get_flows_list() + + # filter and format flows + tabledata = _flows_filter(flows_list, ifaces_dict) + + # print flows + _flows_table_print(tabledata) + +sys.exit(0) diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py new file mode 100755 index 000000000..df4486bce --- /dev/null +++ b/src/op_mode/format_disk.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import os +import re +import sys +from datetime import datetime +from time import sleep + +from vyos.util import is_admin, ask_yes_no +from vyos.util import call +from vyos.util import cmd +from vyos.util import DEVNULL + +def list_disks(): + disks = set() + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + return disks + + +def is_busy(disk: str): + """Check if given disk device is busy by re-reading it's partition table""" + return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0 + + +def backup_partitions(disk: str): + """Save sfdisk partitions output to a backup file""" + + device_path = '/dev/' + disk + backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M') + backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts) + cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}') + + +def list_partitions(disk: str): + """List partition numbers of a given disk""" + + parts = set() + part_num_expr = re.compile(disk + '([0-9]+)') + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3] != 'name' and part_num_expr.match(fields[3]): + part_idx = part_num_expr.match(fields[3]).group(1) + parts.add(int(part_idx)) + return parts + + +def delete_partition(disk: str, partition_idx: int): + cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}') + + +def format_disk_like(target: str, proto: str): + cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_argument_group() + group.add_argument('-t', '--target', type=str, required=True, help='Target device to format') + group.add_argument('-p', '--proto', type=str, required=True, help='Prototype device to use as reference') + args = parser.parse_args() + + if not is_admin(): + print('Must be admin or root to format disk') + sys.exit(1) + + target_disk = args.target + eligible_target_disks = list_disks() + + proto_disk = args.proto + eligible_proto_disks = eligible_target_disks.copy() + eligible_proto_disks.remove(target_disk) + + fmt = { + 'target_disk': target_disk, + 'proto_disk': proto_disk, + } + + if proto_disk == target_disk: + print('The two disk drives must be different.') + sys.exit(1) + + if not os.path.exists('/dev/' + proto_disk): + print('Device /dev/{proto_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if not os.path.exists('/dev/' + target_disk): + print('Device /dev/{target_disk} does not exist'.format_map(fmt)) + sys.exit(1) + + if target_disk not in eligible_target_disks: + print('Device {target_disk} can not be formatted'.format_map(fmt)) + sys.exit(1) + + if proto_disk not in eligible_proto_disks: + print('Device {proto_disk} can not be used as a prototype for {target_disk}'.format_map(fmt)) + sys.exit(1) + + if is_busy(target_disk): + print("Disk device {target_disk} is busy. Can't format it now".format_map(fmt)) + sys.exit(1) + + print('This will re-format disk {target_disk} so that it has the same disk\n' + 'partion sizes and offsets as {proto_disk}. This will not copy\n' + 'data from {proto_disk} to {target_disk}. But this will erase all\n' + 'data on {target_disk}.\n'.format_map(fmt)) + + if not ask_yes_no("Do you wish to proceed?"): + print('OK. Disk drive {target_disk} will not be re-formated'.format_map(fmt)) + sys.exit(0) + + print('OK. Re-formating disk drive {target_disk}...'.format_map(fmt)) + + print('Making backup copy of partitions...') + backup_partitions(target_disk) + sleep(1) + + print('Deleting old partitions...') + for p in list_partitions(target_disk): + delete_partition(disk=target_disk, partition_idx=p) + + print('Creating new partitions on {target_disk} based on {proto_disk}...'.format_map(fmt)) + format_disk_like(target=target_disk, proto=proto_disk) + print('Done.') diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py new file mode 100755 index 000000000..cbc9ef973 --- /dev/null +++ b/src/op_mode/generate_ssh_server_key.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import exit +from vyos.util import ask_yes_no +from vyos.util import cmd + +if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): + exit(0) + +cmd('rm -v /etc/ssh/ssh_host_*') +cmd('dpkg-reconfigure openssh-server') +cmd('systemctl restart ssh.service') diff --git a/src/op_mode/ipoe-control.py b/src/op_mode/ipoe-control.py new file mode 100755 index 000000000..7111498b2 --- /dev/null +++ b/src/op_mode/ipoe-control.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import argparse + +from vyos.config import Config +from vyos.util import popen, run + +cmd_dict = { + 'cmd_base' : '/usr/bin/accel-cmd -p 2002 ', + 'selector' : ['if', 'username', 'sid'], + 'actions' : { + 'show_sessions' : 'show sessions', + 'show_stat' : 'show stat', + 'terminate' : 'teminate' + } +} + +def is_ipoe_configured(): + if not Config().exists_effective('service ipoe-server'): + print("Service IPoE is not configured") + sys.exit(1) + +def main(): + #parese args + parser = argparse.ArgumentParser() + parser.add_argument('--action', help='Control action', required=True) + parser.add_argument('--selector', help='Selector username|ifname|sid', required=False) + parser.add_argument('--target', help='Target must contain username|ifname|sid', required=False) + args = parser.parse_args() + + + # Check is IPoE configured + is_ipoe_configured() + + if args.action == "restart": + run(cmd_dict['cmd_base'] + "restart") + sys.exit(0) + + if args.action in cmd_dict['actions']: + if args.selector in cmd_dict['selector'] and args.target: + run(cmd_dict['cmd_base'] + "{0} {1} {2}".format(args.action, args.selector, args.target)) + else: + output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action], decode='utf-8') + if not err: + print(output) + else: + print("IPoE server is not running") + +if __name__ == '__main__': + main() diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py new file mode 100755 index 000000000..06958c605 --- /dev/null +++ b/src/op_mode/lldp_op.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import jinja2 +import json + +from sys import exit +from tabulate import tabulate + +from vyos.util import cmd +from vyos.config import Config + +parser = argparse.ArgumentParser() +parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") +parser.add_argument("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces") +parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") + +# Please be careful if you edit the template. +lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station + D - Docsis, T - Telephone, O - Other + +Device ID Local Proto Cap Platform Port ID +--------- ----- ----- --- -------- ------- +{% for neighbor in neighbors %} +{% for local_if, info in neighbor.items() %} +{{ "%-25s" | format(info.chassis) }} {{ "%-9s" | format(local_if) }} {{ "%-6s" | format(info.proto) }} {{ "%-5s" | format(info.capabilities) }} {{ "%-20s" | format(info.platform[:18]) }} {{ info.remote_if }} +{% endfor %} +{% endfor %} +""" + +def get_neighbors(): + return cmd('/usr/sbin/lldpcli -f json show neighbors') + +def parse_data(data): + output = [] + for tmp in data: + for local_if, values in tmp.items(): + for chassis, c_value in values.get('chassis', {}).items(): + capabilities = c_value['capability'] + if isinstance(capabilities, dict): + capabilities = [capabilities] + + cap = '' + for capability in capabilities: + if capability['enabled']: + if capability['type'] == 'Router': + cap += 'R' + if capability['type'] == 'Bridge': + cap += 'B' + if capability['type'] == 'Wlan': + cap += 'W' + if capability['type'] == 'Station': + cap += 'S' + if capability['type'] == 'Repeater': + cap += 'r' + if capability['type'] == 'Telephone': + cap += 'T' + if capability['type'] == 'Docsis': + cap += 'D' + if capability['type'] == 'Other': + cap += 'O' + + + remote_if = 'Unknown' + if 'descr' in values.get('port', {}): + remote_if = values.get('port', {}).get('descr') + elif 'id' in values.get('port', {}): + remote_if = values.get('port', {}).get('id').get('value', 'Unknown') + + output.append({local_if: {'chassis': chassis, + 'remote_if': remote_if, + 'proto': values.get('via','Unknown'), + 'platform': c_value.get('descr', 'Unknown'), + 'capabilities': cap}}) + + + output = {'neighbors': output} + return output + +if __name__ == '__main__': + args = parser.parse_args() + tmp = { 'neighbors' : [] } + + c = Config() + if not c.exists_effective(['service', 'lldp']): + print('Service LLDP is not configured') + exit(0) + + if args.detail: + print(cmd('/usr/sbin/lldpctl -f plain')) + exit(0) + elif args.all or args.interface: + tmp = json.loads(get_neighbors()) + + if args.all: + neighbors = tmp['lldp']['interface'] + elif args.interface: + neighbors = [] + for neighbor in tmp['lldp']['interface']: + if args.interface in neighbor: + neighbors.append(neighbor) + + else: + parser.print_help() + exit(1) + + tmpl = jinja2.Template(lldp_out, trim_blocks=True) + config_text = tmpl.render(parse_data(neighbors)) + print(config_text) + + exit(0) diff --git a/src/op_mode/maya_date.py b/src/op_mode/maya_date.py new file mode 100755 index 000000000..847b543e0 --- /dev/null +++ b/src/op_mode/maya_date.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2013, 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys + +class MayaDate(object): + """ Converts number of days since UNIX epoch + to the Maya calendar date. + + Ancient Maya people used three independent calendars for + different purposes. + + The long count calendar is for recording historical events. + It represents the number of days passed + since some date in the past the Maya believed is the day + our world was created. + + Tzolkin calendar is for religious purposes, it has + two independent cycles of 13 and 20 days, where 13 day + cycle days are numbered, and 20 day cycle days are named. + + Haab calendar is for agriculture and daily life, it's a + 365 day calendar with 18 months 20 days each, and 5 + nameless days. + + The smallest unit of the long count calendar is one day (kin). + """ + + """ The long count calendar uses five different base 18 or base 20 + cycles. Modern scholars write long count calendar dates in a dot separated format + from longest to shortest cycle, + <baktun>.<katun>.<tun>.<winal>.<kin> + for example, "13.0.0.9.2". + + Classic version actually used by the ancient Maya wraps around + every 13th baktun, but modern historians often use longer cycles + such as piktun = 20 baktun. + + """ + kin = 1 + winal = 20 # 20 kin + tun = 360 # 18 winal + katun = 7200 # 20 tun + baktun = 144000 # 20 katun + + """ Tzolk'in date is composed of two independent cycles. + Dates repeat every 260 days, 13 Ajaw is considered the end + of tzolk'in. + + Every day of the 20 day cycle has unique name, we number + them from zero so it's easier to map the remainder to day: + """ + tzolkin_days = { 0: "Imix'", + 1: "Ik'", + 2: "Ak'b'al", + 3: "K'an", + 4: "Chikchan", + 5: "Kimi", + 6: "Manik'", + 7: "Lamat", + 8: "Muluk", + 9: "Ok", + 10: "Chuwen", + 11: "Eb'", + 12: "B'en", + 13: "Ix", + 14: "Men", + 15: "Kib'", + 16: "Kab'an", + 17: "Etz'nab'", + 18: "Kawak", + 19: "Ajaw" } + + """ As said above, haab (year) has 19 months. Only 18 are + true months of 20 days each, the remaining 5 days called "wayeb" + do not really belong to any month, but we think of them as a pseudo-month + for convenience. + + Also, note that days of the month are actually numbered from 0, not from 1, + it's not for technical reasons. + """ + haab_months = { 0: "Pop", + 1: "Wo'", + 2: "Sip", + 3: "Sotz'", + 4: "Sek", + 5: "Xul", + 6: "Yaxk'in'", + 7: "Mol", + 8: "Ch'en", + 9: "Yax", + 10: "Sak'", + 11: "Keh", + 12: "Mak", + 13: "K'ank'in", + 14: "Muwan'", + 15: "Pax", + 16: "K'ayab", + 17: "Kumk'u", + 18: "Wayeb'" } + + """ Now we need to map the beginning of UNIX epoch + (Jan 1 1970 00:00 UTC) to the beginning of the long count + calendar (0.0.0.0.0, 4 Ajaw, 8 Kumk'u). + + The problem with mapping the long count calendar to + any other is that its start date is not known exactly. + + The most widely accepted hypothesis suggests it was + August 11, 3114 BC gregorian date. In this case UNIX epoch + starts on 12.17.16.7.5, 13 Chikchan, 3 K'ank'in + + It's known as Goodman-Martinez-Thompson (GMT) correlation + constant. + """ + start_days = 1856305 + + """ Seconds in day, for conversion from timestamp """ + seconds_in_day = 60 * 60 * 24 + + def __init__(self, timestamp): + if timestamp is None: + self.days = self.start_days + else: + self.days = self.start_days + (int(timestamp) // self.seconds_in_day) + + def long_count_date(self): + """ Returns long count date string """ + days = self.days + + cur_baktun = days // self.baktun + days = days % self.baktun + + cur_katun = days // self.katun + days = days % self.katun + + cur_tun = days // self.tun + days = days % self.tun + + cur_winal = days // self.winal + days = days % self.winal + + cur_kin = days + + longcount_string = "{0}.{1}.{2}.{3}.{4}".format( cur_baktun, + cur_katun, + cur_tun, + cur_winal, + cur_kin ) + return(longcount_string) + + def tzolkin_date(self): + """ Returns tzolkin date string """ + days = self.days + + """ The start date is not the beginning of both cycles, + it's 4 Ajaw. So we need to add 4 to the 13 days cycle day, + and substract 1 from the 20 day cycle to get correct result. + """ + tzolkin_13 = (days + 4) % 13 + tzolkin_20 = (days - 1) % 20 + + tzolkin_string = "{0} {1}".format(tzolkin_13, self.tzolkin_days[tzolkin_20]) + + return(tzolkin_string) + + def haab_date(self): + """ Returns haab date string. + + The time start on 8 Kumk'u rather than 0 Pop, which is + 17 days before the new haab, so we need to substract 17 + from the current date to get correct result. + """ + days = self.days + + haab_day = (days - 17) % 365 + haab_month = haab_day // 20 + haab_day_of_month = haab_day % 20 + + haab_string = "{0} {1}".format(haab_day_of_month, self.haab_months[haab_month]) + + return(haab_string) + + def date(self): + return("{0}, {1}, {2}".format( self.long_count_date(), self.tzolkin_date(), self.haab_date() )) + +if __name__ == '__main__': + try: + timestamp = sys.argv[1] + except: + print("Please specify timestamp in the argument") + sys.exit(1) + + maya_date = MayaDate(timestamp) + print(maya_date.date()) diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py new file mode 100755 index 000000000..29b430d53 --- /dev/null +++ b/src/op_mode/ping.py @@ -0,0 +1,230 @@ +#! /usr/bin/env python3 + +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import socket +import ipaddress + +options = { + 'audible': { + 'ping': '{command} -a', + 'type': 'noarg', + 'help': 'Make a noise on ping' + }, + 'adaptive': { + 'ping': '{command} -A', + 'type': 'noarg', + 'help': 'Adativly set interpacket interval' + }, + 'allow-broadcast': { + 'ping': '{command} -b', + 'type': 'noarg', + 'help': 'Ping broadcast address' + }, + 'bypass-route': { + 'ping': '{command} -r', + 'type': 'noarg', + 'help': 'Bypass normal routing tables' + }, + 'count': { + 'ping': '{command} -c {value}', + 'type': '<requests>', + 'help': 'Number of requests to send' + }, + 'deadline': { + 'ping': '{command} -w {value}', + 'type': '<seconds>', + 'help': 'Number of seconds before ping exits' + }, + 'flood': { + 'ping': 'sudo {command} -f', + 'type': 'noarg', + 'help': 'Send 100 requests per second' + }, + 'interface': { + 'ping': '{command} -I {value}', + 'type': '<interface> <X.X.X.X> <h:h:h:h:h:h:h:h>', + 'help': 'Interface to use as source for ping' + }, + 'interval': { + 'ping': '{command} -i {value}', + 'type': '<seconds>', + 'help': 'Number of seconds to wait between requests' + }, + 'mark': { + 'ping': '{command} -m {value}', + 'type': '<fwmark>', + 'help': 'Mark request for special processing' + }, + 'numeric': { + 'ping': '{command} -n', + 'type': 'noarg', + 'help': 'Do not resolve DNS names' + }, + 'no-loopback': { + 'ping': '{command} -L', + 'type': 'noarg', + 'help': 'Supress loopback of multicast pings' + }, + 'pattern': { + 'ping': '{command} -p {value}', + 'type': '<pattern>', + 'help': 'Pattern to fill out the packet' + }, + 'timestamp': { + 'ping': '{command} -D', + 'type': 'noarg', + 'help': 'Print timestamp of output' + }, + 'tos': { + 'ping': '{command} -Q {value}', + 'type': '<tos>', + 'help': 'Mark packets with specified TOS' + }, + 'quiet': { + 'ping': '{command} -q', + 'type': 'noarg', + 'help': 'Only print summary lines' + }, + 'record-route': { + 'ping': '{command} -R', + 'type': 'noarg', + 'help': 'Record route the packet takes' + }, + 'size': { + 'ping': '{command} -s {value}', + 'type': '<bytes>', + 'help': 'Number of bytes to send' + }, + 'ttl': { + 'ping': '{command} -t {value}', + 'type': '<ttl>', + 'help': 'Maximum packet lifetime' + }, + 'vrf': { + 'ping': 'sudo ip vrf exec {value} {command}', + 'type': '<vrf>', + 'help': 'Use specified VRF table', + 'dflt': 'default', + }, + 'verbose': { + 'ping': '{command} -v', + 'type': 'noarg', + 'help': 'Verbose output'} +} + +ping = { + 4: '/bin/ping', + 6: '/bin/ping6', +} + + +class List (list): + def first (self): + return self.pop(0) if self else '' + + def last(self): + return self.pop() if self else '' + + def prepend(self,value): + self.insert(0,value) + + +def expension_failure(option, completions): + reason = 'Ambiguous' if completions else 'Invalid' + sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option)) + if completions: + sys.stderr.write(' Possible completions:\n ') + sys.stderr.write('\n '.join(completions)) + sys.stderr.write('\n') + sys.stdout.write('<nocomps>') + sys.exit(1) + + +def complete(prefix): + return [o for o in options if o.startswith(prefix)] + + +def convert(command, args): + while args: + shortname = args.first() + longnames = complete(shortname) + if len(longnames) != 1: + expension_failure(shortname, longnames) + longname = longnames[0] + if options[longname]['type'] == 'noarg': + command = options[longname]['ping'].format( + command=command, value='') + elif not args: + sys.exit(f'ping: missing argument for {longname} option') + else: + command = options[longname]['ping'].format( + command=command, value=args.first()) + return command + + +if __name__ == '__main__': + args = List(sys.argv[1:]) + host = args.first() + + if not host: + sys.exit("ping: Missing host") + + if host == '--get-options': + args.first() # pop ping + args.first() # pop IP + while args: + option = args.first() + + matched = complete(option) + if not args: + sys.stdout.write(' '.join(matched)) + sys.exit(0) + + if len(matched) > 1 : + sys.stdout.write(' '.join(matched)) + sys.exit(0) + + if options[matched[0]]['type'] == 'noarg': + continue + + value = args.first() + if not args: + matched = complete(option) + sys.stdout.write(options[matched[0]]['type']) + sys.exit(0) + + for name,option in options.items(): + if 'dflt' in option and name not in args: + args.append(name) + args.append(option['dflt']) + + try: + ip = socket.gethostbyname(host) + except socket.gaierror: + ip = host + + try: + version = ipaddress.ip_address(ip).version + except ValueError: + sys.exit(f'ping: Unknown host: {host}') + + command = convert(ping[version],args) + + # print(f'{command} {host}') + os.system(f'{command} {host}') + diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py new file mode 100755 index 000000000..69af427ec --- /dev/null +++ b/src/op_mode/powerctrl.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from argparse import ArgumentParser +from datetime import datetime, timedelta, time as type_time, date as type_date +from sys import exit +from time import time + +from vyos.util import ask_yes_no, cmd, call, run, STDOUT + +systemd_sched_file = "/run/systemd/shutdown/scheduled" + +def utc2local(datetime): + now = time() + offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) + return datetime + offs + +def parse_time(s): + try: + if re.match(r'^\d{1,2}$', s): + return datetime.strptime(s, "%M").time() + else: + return datetime.strptime(s, "%H:%M").time() + except ValueError: + return None + + +def parse_date(s): + for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]: + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + # If nothing matched... + return None + + +def get_shutdown_status(): + if os.path.exists(systemd_sched_file): + # Get scheduled from systemd file + with open(systemd_sched_file, 'r') as f: + data = f.read().rstrip('\n') + r_data = {} + for line in data.splitlines(): + tmp_split = line.split("=") + if tmp_split[0] == "USEC": + # Convert USEC to human readable format + r_data['DATETIME'] = datetime.utcfromtimestamp( + int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S') + else: + r_data[tmp_split[0]] = tmp_split[1] + return r_data + return None + + +def check_shutdown(): + output = get_shutdown_status() + if output and 'MODE' in output: + dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S') + if output['MODE'] == 'reboot': + print("Reboot is scheduled", utc2local(dt)) + elif output['MODE'] == 'poweroff': + print("Poweroff is scheduled", utc2local(dt)) + else: + print("Reboot or poweroff is not scheduled") + + +def cancel_shutdown(): + output = get_shutdown_status() + if output and 'MODE' in output: + timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + try: + run('/sbin/shutdown -c --no-wall') + except OSError as e: + exit("Could not cancel a reboot or poweroff: %s" % e) + + message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow) + run(f'wall {message} > /dev/null 2>&1') + else: + print("Reboot or poweroff is not scheduled") + + +def execute_shutdown(time, reboot=True, ask=True): + if not ask: + action = "reboot" if reboot else "poweroff" + if not ask_yes_no("Are you sure you want to %s this system?" % action): + exit(0) + + action = "-r" if reboot else "-P" + + if len(time) == 0: + # T870 legacy reboot job support + chk_vyatta_based_reboots() + ### + + out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) + print(out.split(",", 1)[0]) + return + elif len(time) == 1: + # Assume the argument is just time + ts = parse_time(time[0]) + if ts: + cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) + else: + exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + elif len(time) == 2: + # Assume it's date and time + ts = parse_time(time[0]) + ds = parse_date(time[1]) + if ts and ds: + t = datetime.combine(ds, ts) + td = t - datetime.now() + t2 = 1 + int(td.total_seconds())//60 # Get total minutes + cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) + else: + if not ts: + exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + else: + exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1])) + else: + exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM") + check_shutdown() + + +def chk_vyatta_based_reboots(): + # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed + # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job + # name is the node of scheduled the job, commit-confirm checks for that + + f = r'/var/run/confirm.job' + if os.path.exists(f): + jid = open(f).read().strip() + if jid != 0: + call(f'sudo atrm {jid}') + os.remove(f) + + +def main(): + parser = ArgumentParser() + parser.add_argument("--yes", "-y", + help="Do not ask for confirmation", + action="store_true", + dest="yes") + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument("--reboot", "-r", + help="Reboot the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--poweroff", "-p", + help="Poweroff the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--cancel", "-c", + help="Cancel pending shutdown", + action="store_true") + + action.add_argument("--check", + help="Check pending chutdown", + action="store_true") + args = parser.parse_args() + + try: + if args.reboot is not None: + execute_shutdown(args.reboot, reboot=True, ask=args.yes) + if args.poweroff is not None: + execute_shutdown(args.poweroff, reboot=False, ask=args.yes) + if args.cancel: + cancel_shutdown() + if args.check: + check_shutdown() + except KeyboardInterrupt: + exit("Interrupted") + +if __name__ == "__main__": + main() diff --git a/src/op_mode/ppp-server-ctrl.py b/src/op_mode/ppp-server-ctrl.py new file mode 100755 index 000000000..171107b4a --- /dev/null +++ b/src/op_mode/ppp-server-ctrl.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import argparse + +from vyos.config import Config +from vyos.util import popen, DEVNULL + +cmd_dict = { + 'cmd_base' : '/usr/bin/accel-cmd -p {} ', + 'vpn_types' : { + 'pppoe' : 2001, + 'pptp' : 2003, + 'l2tp' : 2004, + 'sstp' : 2005 + }, + 'conf_proto' : { + 'pppoe' : 'service pppoe-server', + 'pptp' : 'vpn pptp remote-access', + 'l2tp' : 'vpn l2tp remote-access', + 'sstp' : 'vpn sstp' + } +} + +def is_service_configured(proto): + if not Config().exists_effective(cmd_dict['conf_proto'][proto]): + print("Service {} is not configured".format(proto)) + sys.exit(1) + +def main(): + #parese args + parser = argparse.ArgumentParser() + parser.add_argument('--proto', help='Possible protocols pppoe|pptp|l2tp|sstp', required=True) + parser.add_argument('--action', help='Action command', required=True) + args = parser.parse_args() + + if args.proto in cmd_dict['vpn_types'] and args.action: + # Check is service configured + is_service_configured(args.proto) + + if args.action == "show sessions": + ses_pattern = " ifname,username,ip,ip6,ip6-dp,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes" + else: + ses_pattern = "" + + output, err = popen(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][args.proto]) + args.action + ses_pattern, stderr=DEVNULL, decode='utf-8') + if not err: + print(output) + else: + print("{} server is not running".format(args.proto)) + + else: + print("Param --proto and --action required") + sys.exit(1) + +if __name__ == '__main__': + main() diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py new file mode 100755 index 000000000..dbd3eb4d1 --- /dev/null +++ b/src/op_mode/reset_openvpn.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +from sys import argv, exit +from vyos.util import call + +if __name__ == '__main__': + if (len(argv) < 1): + print('Must specify OpenVPN interface name!') + exit(1) + + interface = argv[1] + if os.path.isfile(f'/run/openvpn/{interface}.conf'): + call(f'systemctl restart openvpn@{interface}.service') + else: + print(f'OpenVPN interface "{interface}" does not exist!') + exit(1) diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py new file mode 100755 index 000000000..3a0ad941c --- /dev/null +++ b/src/op_mode/reset_vpn.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import argparse + +from vyos.util import run + +cmd_dict = { + 'cmd_base' : '/usr/bin/accel-cmd -p {} terminate {} {}', + 'vpn_types' : { + 'pptp' : 2003, + 'l2tp' : 2004, + 'sstp' : 2005 + } +} + +def terminate_sessions(username='', interface='', protocol=''): + + # Reset vpn connections by username + if protocol in cmd_dict['vpn_types']: + if username == "all_users": + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'all', '')) + else: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'username', username)) + + # Reset vpn connections by ifname + elif interface: + for proto in cmd_dict['vpn_types']: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'if', interface)) + + elif username: + # Reset all vpn connections + if username == "all_users": + for proto in cmd_dict['vpn_types']: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'all', '')) + else: + for proto in cmd_dict['vpn_types']: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'username', username)) + +def main(): + #parese args + parser = argparse.ArgumentParser() + parser.add_argument('--username', help='Terminate by username (all_users used for disconnect all users)', required=False) + parser.add_argument('--interface', help='Terminate by interface', required=False) + parser.add_argument('--protocol', help='Set protocol (pptp|l2tp|sstp)', required=False) + args = parser.parse_args() + + if args.username or args.interface: + terminate_sessions(username=args.username, interface=args.interface, protocol=args.protocol) + else: + print("Param --username or --interface required") + sys.exit(1) + + terminate_sessions() + + +if __name__ == '__main__': + main() diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py new file mode 100755 index 000000000..af4fb2d15 --- /dev/null +++ b/src/op_mode/restart_dhcp_relay.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# File: restart_dhcp_relay.py +# Purpose: +# Restart IPv4 and IPv6 DHCP relay instances of dhcrelay service + +import sys +import argparse +import os + +import vyos.config +from vyos.util import call + + +parser = argparse.ArgumentParser() +parser.add_argument("--ipv4", action="store_true", help="Restart IPv4 DHCP relay") +parser.add_argument("--ipv6", action="store_true", help="Restart IPv6 DHCP relay") + +if __name__ == '__main__': + args = parser.parse_args() + c = vyos.config.Config() + + if args.ipv4: + # Do nothing if service is not configured + if not c.exists_effective('service dhcp-relay'): + print("DHCP relay service not configured") + else: + call('systemctl restart isc-dhcp-server.service') + + sys.exit(0) + elif args.ipv6: + # Do nothing if service is not configured + if not c.exists_effective('service dhcpv6-relay'): + print("DHCPv6 relay service not configured") + else: + call('systemctl restart isc-dhcp-server6.service') + + sys.exit(0) + else: + parser.print_help() + sys.exit(1) diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py new file mode 100755 index 000000000..d1b66b33f --- /dev/null +++ b/src/op_mode/restart_frr.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys +import argparse +import logging +from logging.handlers import SysLogHandler +from pathlib import Path +import psutil + +from vyos.util import call + +# some default values +watchfrr = '/usr/lib/frr/watchfrr.sh' +vtysh = '/usr/bin/vtysh' +frrconfig_tmp = '/tmp/frr_restart' + +# configure logging +logger = logging.getLogger(__name__) +logs_handler = SysLogHandler('/dev/log') +logs_handler.setFormatter(logging.Formatter('%(filename)s: %(message)s')) +logger.addHandler(logs_handler) +logger.setLevel(logging.INFO) + +# check if it is safe to restart FRR +def _check_safety(): + try: + # print warning + answer = input("WARNING: This is a potentially unsafe function! You may lose the connection to the router or active configuration after running this command. Use it at your own risk! Continue? [y/N]: ") + if not answer.lower() == "y": + logger.error("User aborted command") + return False + + # check if another restart process already running + if len([process for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']) if 'python' in process.info['name'] and 'restart_frr.py' in process.info['cmdline'][1]]) > 1: + logger.error("Another restart_frr.py already running") + answer = input("Another restart_frr.py process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ") + if not answer.lower() == "y": + return False + + # check if watchfrr.sh is running + for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']): + if 'bash' in process.info['name'] and watchfrr in process.info['cmdline']: + logger.error("Another {} already running".format(watchfrr)) + answer = input("Another {} process is already running. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(watchfrr)) + if not answer.lower() == "y": + return False + + # check if vtysh is running + for process in psutil.process_iter(attrs=['pid', 'name', 'cmdline']): + if 'vtysh' in process.info['name']: + logger.error("The vtysh is running by another task") + answer = input("The vtysh is running by another task. It is unsafe to continue. Do you want to process anyway? [y/N]: ") + if not answer.lower() == "y": + return False + + # check if temporary directory exists + if Path(frrconfig_tmp).exists(): + logger.error("The temporary directory \"{}\" already exists".format(frrconfig_tmp)) + answer = input("The temporary directory \"{}\" already exists. It is unsafe to continue. Do you want to process anyway? [y/N]: ".format(frrconfig_tmp)) + if not answer.lower() == "y": + return False + except: + logger.error("Something goes wrong in _check_safety()") + return False + + # return True if all check was passed or user confirmed to ignore they results + return True + +# write active config to file +def _write_config(): + # create temporary directory + Path(frrconfig_tmp).mkdir(parents=False, exist_ok=True) + # save frr.conf to it + command = "{} -n -w --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) + return_code = call(command) + if not return_code == 0: + logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code)) + return False + logger.info("Active config saved to {}".format(frrconfig_tmp)) + return True + +# clear and remove temporary directory +def _cleanup(): + tmpdir = Path(frrconfig_tmp) + try: + if tmpdir.exists(): + for file in tmpdir.iterdir(): + file.unlink() + tmpdir.rmdir() + except: + logger.error("Failed to remove temporary directory {}".format(frrconfig_tmp)) + print("Failed to remove temporary directory {}".format(frrconfig_tmp)) + +# check if daemon is running +def _daemon_check(daemon): + command = "{} print_status {}".format(watchfrr, daemon) + return_code = call(command) + if not return_code == 0: + logger.error("Daemon \"{}\" is not running".format(daemon)) + return False + + # return True if all checks were passed + return True + +# restart daemon +def _daemon_restart(daemon): + command = "{} restart {}".format(watchfrr, daemon) + return_code = call(command) + if not return_code == 0: + logger.error("Failed to restart daemon \"{}\"".format(daemon)) + return False + + # return True if restarted successfully + logger.info("Daemon \"{}\" restarted".format(daemon)) + return True + +# reload old config +def _reload_config(daemon): + if daemon != '': + command = "{} -n -b --config_dir {} -d {} 2> /dev/null".format(vtysh, frrconfig_tmp, daemon) + else: + command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) + + return_code = call(command) + if not return_code == 0: + logger.error("Failed to reinstall configuration") + return False + + # return True if restarted successfully + logger.info("Configuration reinstalled successfully") + return True + +# check all daemons if they are running +def _check_args_daemon(daemons): + for daemon in daemons: + if not _daemon_check(daemon): + return False + return True + +# define program arguments +cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') +cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') +cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ospfd', 'ospf6d', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons') +# parse arguments +cmd_args = cmd_args_parser.parse_args() + + +# main logic +# restart daemon +if cmd_args.action == 'restart': + # check if it is safe to restart FRR + if not _check_safety(): + print("\nOne of the safety checks was failed or user aborted command. Exiting.") + sys.exit(1) + + if not _write_config(): + print("Failed to save active config") + _cleanup() + sys.exit(1) + + # a little trick to make further commands more clear + if not cmd_args.daemon: + cmd_args.daemon = [''] + + # check all daemons if they are running + if cmd_args.daemon != ['']: + if not _check_args_daemon(cmd_args.daemon): + print("Warning: some of listed daemons are not running") + + # run command to restart daemon + for daemon in cmd_args.daemon: + if not _daemon_restart(daemon): + print("Failed to restart daemon: {}".format(daemon)) + _cleanup() + sys.exit(1) + # reinstall old configuration + _reload_config(daemon) + + # cleanup after all actions + _cleanup() + +sys.exit(0) diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py new file mode 100755 index 000000000..752db3deb --- /dev/null +++ b/src/op_mode/show_acceleration.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys +import os +import re +import argparse + +from vyos.config import Config +from vyos.util import popen +from vyos.util import call + + +def detect_qat_dev(): + output, err = popen('sudo lspci -nn', decode='utf-8') + if not err: + data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output) + #If QAT devices found + if data: + return + print("\t No QAT device found") + sys.exit(1) + +def show_qat_status(): + detect_qat_dev() + + # Check QAT service + if not os.path.exists('/etc/init.d/qat_service'): + print("\t QAT service not installed") + sys.exit(1) + + # Show QAT service + call('sudo /etc/init.d/qat_service status') + +# Return QAT devices +def get_qat_devices(): + data_st, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8') + if not err: + elm_lst = re.findall('qat_dev\d', data_st) + print('\n'.join(elm_lst)) + +# Return QAT path in sysfs +def get_qat_proc_path(qat_dev): + q_type = "" + q_bsf = "" + output, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8') + if not err: + # Parse QAT service output + data_st = output.split("\n") + for elm_str in range(len(data_st)): + if re.search(qat_dev, data_st[elm_str]): + elm_list = data_st[elm_str].split(", ") + for elm in range(len(elm_list)): + if re.search('type', elm_list[elm]): + q_list = elm_list[elm].split(": ") + q_type=q_list[1] + elif re.search('bsf', elm_list[elm]): + q_list = elm_list[elm].split(": ") + q_bsf = q_list[1] + return "/sys/kernel/debug/qat_"+q_type+"_"+q_bsf+"/" + +# Check if QAT service confgured +def check_qat_if_conf(): + if not Config().exists_effective('system acceleration qat'): + print("\t system acceleration qat is not configured") + sys.exit(1) + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("--hw", action="store_true", help="Show Intel QAT HW") +group.add_argument("--dev_list", action="store_true", help="Return Intel QAT devices") +group.add_argument("--flow", action="store_true", help="Show Intel QAT flows") +group.add_argument("--interrupts", action="store_true", help="Show Intel QAT interrupts") +group.add_argument("--status", action="store_true", help="Show Intel QAT status") +group.add_argument("--conf", action="store_true", help="Show Intel QAT configuration") + +parser.add_argument("--dev", type=str, help="Selected QAT device") + +args = parser.parse_args() + +if args.hw: + detect_qat_dev() + # Show availible Intel QAT devices + call('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'') +elif args.flow and args.dev: + check_qat_if_conf() + call('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters") +elif args.interrupts: + check_qat_if_conf() + # Delete _dev from args.dev + call('sudo cat /proc/interrupts | grep qat') +elif args.status: + check_qat_if_conf() + show_qat_status() +elif args.conf and args.dev: + check_qat_if_conf() + call('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg") +elif args.dev_list: + get_qat_devices() +else: + parser.print_help() + sys.exit(1) diff --git a/src/op_mode/show_configuration_files.sh b/src/op_mode/show_configuration_files.sh new file mode 100755 index 000000000..ad8e0747c --- /dev/null +++ b/src/op_mode/show_configuration_files.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Wrapper script for the show configuration files command +find ${vyatta_sysconfdir}/config/ \ + -type f \ + -not -name ".*" \ + -not -name "config.boot.*" \ + -printf "%f\t(%Tc)\t%T@\n" \ + | sort -r -k3 \ + | awk -F"\t" '{printf ("%-20s\t%s\n", $1,$2) ;}' diff --git a/src/op_mode/show_cpu.py b/src/op_mode/show_cpu.py new file mode 100755 index 000000000..0a540da1d --- /dev/null +++ b/src/op_mode/show_cpu.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2016-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json + +from jinja2 import Template +from sys import exit +from vyos.util import popen, DEVNULL + +OUT_TMPL_SRC = """ +{%- if cpu -%} +{% if 'vendor' in cpu %}CPU Vendor: {{cpu.vendor}}{%- endif %} +{% if 'model' in cpu %}Model: {{cpu.model}}{%- endif %} +{% if 'cpus' in cpu %}Total CPUs: {{cpu.cpus}}{%- endif %} +{% if 'sockets' in cpu %}Sockets: {{cpu.sockets}}{%- endif %} +{% if 'cores' in cpu %}Cores: {{cpu.cores}}{%- endif %} +{% if 'threads' in cpu %}Threads: {{cpu.threads}}{%- endif %} +{% if 'mhz' in cpu %}Current MHz: {{cpu.mhz}}{%- endif %} +{% if 'mhz_min' in cpu %}Minimum MHz: {{cpu.mhz_min}}{%- endif %} +{% if 'mhz_max' in cpu %}Maximum MHz: {{cpu.mhz_max}}{%- endif %} +{% endif %} +""" + +cpu = {} +cpu_json, code = popen('lscpu -J', stderr=DEVNULL) + +if code == 0: + cpu_info = json.loads(cpu_json) + if len(cpu_info) > 0 and 'lscpu' in cpu_info: + for prop in cpu_info['lscpu']: + if (prop['field'].find('Thread(s)') > -1): cpu['threads'] = prop['data'] + if (prop['field'].find('Core(s)')) > -1: cpu['cores'] = prop['data'] + if (prop['field'].find('Socket(s)')) > -1: cpu['sockets'] = prop['data'] + if (prop['field'].find('CPU(s):')) > -1: cpu['cpus'] = prop['data'] + if (prop['field'].find('CPU MHz')) > -1: cpu['mhz'] = prop['data'] + if (prop['field'].find('CPU min MHz')) > -1: cpu['mhz_min'] = prop['data'] + if (prop['field'].find('CPU max MHz')) > -1: cpu['mhz_max'] = prop['data'] + if (prop['field'].find('Vendor ID')) > -1: cpu['vendor'] = prop['data'] + if (prop['field'].find('Model name')) > -1: cpu['model'] = prop['data'] + +if len(cpu) > 0: + tmp = { 'cpu':cpu } + tmpl = Template(OUT_TMPL_SRC) + print(tmpl.render(tmp)) + exit(0) +else: + print('CPU information could not be determined\n') + exit(1) diff --git a/src/op_mode/show_current_user.sh b/src/op_mode/show_current_user.sh new file mode 100755 index 000000000..93e6efa61 --- /dev/null +++ b/src/op_mode/show_current_user.sh @@ -0,0 +1,18 @@ +#! /bin/bash + +echo -n "login : " ; who -m + +if [ -n "$VYATTA_USER_LEVEL_DIR" ] +then + echo -n "level : " + basename $VYATTA_USER_LEVEL_DIR +fi + +echo -n "user : " ; id -un +echo -n "groups : " ; id -Gn + +if id -Z >/dev/null 2>&1 +then + echo -n "context : " + id -Z +fi diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py new file mode 100755 index 000000000..ff1e3cc56 --- /dev/null +++ b/src/op_mode/show_dhcp.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# TODO: merge with show_dhcpv6.py + +from json import dumps +from argparse import ArgumentParser +from ipaddress import ip_address +from tabulate import tabulate +from sys import exit +from collections import OrderedDict +from datetime import datetime + +from isc_dhcp_leases import Lease, IscDhcpLeases + +from vyos.config import Config +from vyos.util import call + + +lease_file = "/config/dhcpd.leases" +pool_key = "shared-networkname" + +lease_display_fields = OrderedDict() +lease_display_fields['ip'] = 'IP address' +lease_display_fields['hardware_address'] = 'Hardware address' +lease_display_fields['state'] = 'State' +lease_display_fields['start'] = 'Lease start' +lease_display_fields['end'] = 'Lease expiration' +lease_display_fields['remaining'] = 'Remaining' +lease_display_fields['pool'] = 'Pool' +lease_display_fields['hostname'] = 'Hostname' + +lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] + +def in_pool(lease, pool): + if pool_key in lease.sets: + if lease.sets[pool_key] == pool: + return True + + return False + +def utc_to_local(utc_dt): + return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) + +def get_lease_data(lease): + data = {} + + # isc-dhcp lease times are in UTC so we need to convert them to local time to display + try: + data["start"] = utc_to_local(lease.start).strftime("%Y/%m/%d %H:%M:%S") + except: + data["start"] = "" + + try: + data["end"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") + except: + data["end"] = "" + + try: + data["remaining"] = lease.end - datetime.utcnow() + # negative timedelta prints wrong so bypass it + if (data["remaining"].days >= 0): + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data["remaining"] = str(data["remaining"]).split('.')[0] + else: + data["remaining"] = "" + except: + data["remaining"] = "" + + # currently not used but might come in handy + # todo: parse into datetime string + for prop in ['tstp', 'tsfp', 'atsfp', 'cltt']: + if prop in lease.data: + data[prop] = lease.data[prop] + else: + data[prop] = '' + + data["hardware_address"] = lease.ethernet + data["hostname"] = lease.hostname + + data["state"] = lease.binding_state + data["ip"] = lease.ip + + try: + data["pool"] = lease.sets[pool_key] + except: + data["pool"] = "" + + return data + +def get_leases(config, leases, state, pool=None, sort='ip'): + # get leases from file + leases = IscDhcpLeases(lease_file).get() + + # filter leases by state + if 'all' not in state: + leases = list(filter(lambda x: x.binding_state in state, leases)) + + # filter leases by pool name + if pool is not None: + if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): + leases = list(filter(lambda x: in_pool(x, pool), leases)) + else: + print("Pool {0} does not exist.".format(pool)) + exit(0) + + # should maybe filter all state=active by lease.valid here? + + # sort by start time to dedupe (newest lease overrides older) + leases = sorted(leases, key = lambda lease: lease.start) + + # dedupe by converting to dict + leases_dict = {} + for lease in leases: + # dedupe by IP + leases_dict[lease.ip] = lease + + # convert the lease data + leases = list(map(get_lease_data, leases_dict.values())) + + # apply output/display sort + if sort == 'ip': + leases = sorted(leases, key = lambda lease: int(ip_address(lease['ip']))) + else: + leases = sorted(leases, key = lambda lease: lease[sort]) + + return leases + +def show_leases(leases): + lease_list = [] + for l in leases: + lease_list_params = [] + for k in lease_display_fields.keys(): + lease_list_params.append(l[k]) + lease_list.append(lease_list_params) + + output = tabulate(lease_list, lease_display_fields.values()) + + print(output) + +def get_pool_size(config, pool): + size = 0 + subnets = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet".format(pool)) + for s in subnets: + ranges = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet {1} range".format(pool, s)) + for r in ranges: + start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) + stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) + + # Add +1 because both range boundaries are inclusive + size += int(ip_address(stop)) - int(ip_address(start)) + 1 + + return size + +def show_pool_stats(stats): + headers = ["Pool", "Size", "Leases", "Available", "Usage"] + output = tabulate(stats, headers) + + print(output) + +if __name__ == '__main__': + parser = ArgumentParser() + + group = parser.add_mutually_exclusive_group() + group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") + group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") + + parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") + parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") + + args = parser.parse_args() + + conf = Config() + + if args.allowed == 'pool': + if conf.exists_effective('service dhcp-server'): + print(' '.join(conf.list_effective_nodes("service dhcp-server shared-network-name"))) + exit(0) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + exit(0) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) + exit(0) + elif args.allowed: + parser.print_help() + exit(1) + + if args.sort not in lease_display_fields.keys(): + print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}') + exit(0) + + if not set(args.state) < set(lease_valid_states): + print(f'Invalid lease state, choose from: {lease_valid_states}') + exit(0) + + # Do nothing if service is not configured + if not conf.exists_effective('service dhcp-server'): + print("DHCP service is not configured.") + exit(0) + + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if call('systemctl -q is-active isc-dhcp-server.service') != 0: + print("WARNING: DHCP server is configured but not started. Data may be stale.") + + if args.leases: + leases = get_leases(conf, lease_file, args.state, args.pool, args.sort) + + if args.json: + print(dumps(leases, indent=4)) + else: + show_leases(leases) + + elif args.statistics: + pools = [] + + # Get relevant pools + if args.pool: + pools = [args.pool] + else: + pools = conf.list_effective_nodes("service dhcp-server shared-network-name") + + # Get pool usage stats + stats = [] + for p in pools: + size = get_pool_size(conf, p) + leases = len(get_leases(conf, lease_file, state='active', pool=p)) + + use_percentage = round(leases / size * 100) if size != 0 else 0 + + if args.json: + pool_stats = {"pool": p, "size": size, "leases": leases, + "available": (size - leases), "percentage": use_percentage} + else: + # For tabulate + pool_stats = [p, size, leases, size - leases, "{0}%".format(use_percentage)] + stats.append(pool_stats) + + # Print stats + if args.json: + print(dumps(stats, indent=4)) + else: + show_pool_stats(stats) + + else: + parser.print_help() + exit(1) diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py new file mode 100755 index 000000000..ac211fb0a --- /dev/null +++ b/src/op_mode/show_dhcpv6.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# TODO: merge with show_dhcp.py + +from json import dumps +from argparse import ArgumentParser +from ipaddress import ip_address +from tabulate import tabulate +from sys import exit +from collections import OrderedDict +from datetime import datetime + +from isc_dhcp_leases import Lease, IscDhcpLeases + +from vyos.config import Config +from vyos.util import call + +lease_file = "/config/dhcpdv6.leases" +pool_key = "shared-networkname" + +lease_display_fields = OrderedDict() +lease_display_fields['ip'] = 'IPv6 address' +lease_display_fields['state'] = 'State' +lease_display_fields['last_comm'] = 'Last communication' +lease_display_fields['expires'] = 'Lease expiration' +lease_display_fields['remaining'] = 'Remaining' +lease_display_fields['type'] = 'Type' +lease_display_fields['pool'] = 'Pool' +lease_display_fields['iaid_duid'] = 'IAID_DUID' + +lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] + +def in_pool(lease, pool): + if pool_key in lease.sets: + if lease.sets[pool_key] == pool: + return True + + return False + +def format_hex_string(in_str): + out_str = "" + + # if input is divisible by 2, add : every 2 chars + if len(in_str) > 0 and len(in_str) % 2 == 0: + out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2])) + else: + out_str = in_str + + return out_str + +def utc_to_local(utc_dt): + return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) + +def get_lease_data(lease): + data = {} + + # isc-dhcp lease times are in UTC so we need to convert them to local time to display + try: + data["expires"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") + except: + data["expires"] = "" + + try: + data["last_comm"] = utc_to_local(lease.last_communication).strftime("%Y/%m/%d %H:%M:%S") + except: + data["last_comm"] = "" + + try: + data["remaining"] = lease.end - datetime.utcnow() + # negative timedelta prints wrong so bypass it + if (data["remaining"].days >= 0): + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data["remaining"] = str(data["remaining"]).split('.')[0] + else: + data["remaining"] = "" + except: + data["remaining"] = "" + + # isc-dhcp records lease declarations as ia_{na|ta|pd} IAID_DUID {...} + # where IAID_DUID is the combined IAID and DUID + data["iaid_duid"] = format_hex_string(lease.host_identifier_string) + + lease_types_long = {"na": "non-temporary", "ta": "temporary", "pd": "prefix delegation"} + data["type"] = lease_types_long[lease.type] + + data["state"] = lease.binding_state + data["ip"] = lease.ip + + try: + data["pool"] = lease.sets[pool_key] + except: + data["pool"] = "" + + return data + +def get_leases(config, leases, state, pool=None, sort='ip'): + leases = IscDhcpLeases(lease_file).get() + + # filter leases by state + if 'all' not in state: + leases = list(filter(lambda x: x.binding_state in state, leases)) + + # filter leases by pool name + if pool is not None: + if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): + leases = list(filter(lambda x: in_pool(x, pool), leases)) + else: + print("Pool {0} does not exist.".format(pool)) + exit(0) + + # should maybe filter all state=active by lease.valid here? + + # sort by last_comm time to dedupe (newest lease overrides older) + leases = sorted(leases, key = lambda lease: lease.last_communication) + + # dedupe by converting to dict + leases_dict = {} + for lease in leases: + # dedupe by IP + leases_dict[lease.ip] = lease + + # convert the lease data + leases = list(map(get_lease_data, leases_dict.values())) + + # apply output/display sort + if sort == 'ip': + leases = sorted(leases, key = lambda k: int(ip_address(k['ip']))) + else: + leases = sorted(leases, key = lambda k: k[sort]) + + return leases + +def show_leases(leases): + lease_list = [] + for l in leases: + lease_list_params = [] + for k in lease_display_fields.keys(): + lease_list_params.append(l[k]) + lease_list.append(lease_list_params) + + output = tabulate(lease_list, lease_display_fields.values()) + + print(output) + +if __name__ == '__main__': + parser = ArgumentParser() + + group = parser.add_mutually_exclusive_group() + group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases") + group.add_argument("-s", "--statistics", action="store_true", help="Show DHCPv6 statistics") + group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") + + parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") + parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)") + parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") + + args = parser.parse_args() + + conf = Config() + + if args.allowed == 'pool': + if conf.exists_effective('service dhcpv6-server'): + print(' '.join(conf.list_effective_nodes("service dhcpv6-server shared-network-name"))) + exit(0) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + exit(0) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) + exit(0) + elif args.allowed: + parser.print_help() + exit(1) + + if args.sort not in lease_display_fields.keys(): + print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}') + exit(0) + + if not set(args.state) < set(lease_valid_states): + print(f'Invalid lease state, choose from: {lease_valid_states}') + exit(0) + + # Do nothing if service is not configured + if not conf.exists_effective('service dhcpv6-server'): + print("DHCPv6 service is not configured") + exit(0) + + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if call('systemctl -q is-active isc-dhcp-server6.service') != 0: + print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") + + if args.leases: + leases = get_leases(conf, lease_file, args.state, args.pool, args.sort) + + if args.json: + print(dumps(leases, indent=4)) + else: + show_leases(leases) + elif args.statistics: + print("DHCPv6 statistics option is not available") + else: + parser.print_help() + exit(1) diff --git a/src/op_mode/show_disk_format.sh b/src/op_mode/show_disk_format.sh new file mode 100755 index 000000000..61b15a52b --- /dev/null +++ b/src/op_mode/show_disk_format.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +disk_dev="/dev/$1" +if [ ! -b "$disk_dev" ];then + echo "$3 is not a disk device" + exit 1 +fi +sudo /sbin/fdisk -l "$disk_dev" diff --git a/src/op_mode/show_igmpproxy.py b/src/op_mode/show_igmpproxy.py new file mode 100755 index 000000000..5ccc16287 --- /dev/null +++ b/src/op_mode/show_igmpproxy.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# File: show_igmpproxy.py +# Purpose: +# Display istatistics from IPv4 IGMP proxy. +# Used by the "run show ip multicast" command tree. + +import sys +import jinja2 +import argparse +import ipaddress +import socket + +import vyos.config + +# Output Template for "show ip multicast interface" command +# +# Example: +# Interface BytesIn PktsIn BytesOut PktsOut Local +# eth0 0.0b 0 0.0b 0 xxx.xxx.xxx.65 +# eth1 0.0b 0 0.0b 0 xxx.xxx.xx.201 +# eth0.3 0.0b 0 0.0b 0 xxx.xxx.x.7 +# tun1 0.0b 0 0.0b 0 xxx.xxx.xxx.2 +vif_out_tmpl = """ +{%- for r in data %} +{{ "%-10s"|format(r.interface) }} {{ "%-12s"|format(r.bytes_in) }} {{ "%-12s"|format(r.pkts_in) }} {{ "%-12s"|format(r.bytes_out) }} {{ "%-12s"|format(r.pkts_out) }} {{ "%-15s"|format(r.loc) }} +{%- endfor %} +""" + +# Output Template for "show ip multicast mfc" command +# +# Example: +# Group Origin In Out Pkts Bytes Wrong +# xxx.xxx.xxx.250 xxx.xx.xxx.75 -- +# xxx.xxx.xx.124 xx.xxx.xxx.26 -- +mfc_out_tmpl = """ +{%- for r in data %} +{{ "%-15s"|format(r.group) }} {{ "%-15s"|format(r.origin) }} {{ "%-12s"|format(r.pkts) }} {{ "%-12s"|format(r.bytes) }} {{ "%-12s"|format(r.wrong) }} {{ "%-10s"|format(r.iif) }} {{ "%-20s"|format(r.oifs|join(', ')) }} +{%- endfor %} +""" + +parser = argparse.ArgumentParser() +parser.add_argument("--interface", action="store_true", help="Interface Statistics") +parser.add_argument("--mfc", action="store_true", help="Multicast Forwarding Cache") + +def byte_string(size): + # convert size to integer + size = int(size) + + # One Terrabyte + s_TB = 1024 * 1024 * 1024 * 1024 + # One Gigabyte + s_GB = 1024 * 1024 * 1024 + # One Megabyte + s_MB = 1024 * 1024 + # One Kilobyte + s_KB = 1024 + # One Byte + s_B = 1 + + if size > s_TB: + return str(round((size/s_TB), 2)) + 'TB' + elif size > s_GB: + return str(round((size/s_GB), 2)) + 'GB' + elif size > s_MB: + return str(round((size/s_MB), 2)) + 'MB' + elif size > s_KB: + return str(round((size/s_KB), 2)) + 'KB' + else: + return str(round((size/s_B), 2)) + 'b' + + return None + +def kernel2ip(addr): + """ + Convert any given addr from Linux Kernel to a proper, IPv4 address + using the correct host byte order. + """ + + # Convert from hex 'FE000A0A' to decimal '4261415434' + addr = int(addr, 16) + # Kernel ABI _always_ uses network byteorder + addr = socket.ntohl(addr) + + return ipaddress.IPv4Address( addr ) + +def do_mr_vif(): + """ + Read contents of file /proc/net/ip_mr_vif and print a more human + friendly version to the command line. IPv4 addresses present as + 32bit integers in hex format are converted to IPv4 notation, too. + """ + + with open('/proc/net/ip_mr_vif', 'r') as f: + lines = len(f.readlines()) + if lines < 2: + return None + + result = { + 'data': [] + } + + # Build up table format string + table_format = { + 'interface': 'Interface', + 'pkts_in' : 'PktsIn', + 'pkts_out' : 'PktsOut', + 'bytes_in' : 'BytesIn', + 'bytes_out': 'BytesOut', + 'loc' : 'Local' + } + result['data'].append(table_format) + + # read and parse information from /proc filesystema + with open('/proc/net/ip_mr_vif', 'r') as f: + header_line = next(f) + for line in f: + data = { + 'interface': line.split()[1], + 'pkts_in' : line.split()[3], + 'pkts_out' : line.split()[5], + + # convert raw byte number to something more human readable + # Note: could be replaced by Python3 hurry.filesize module + 'bytes_in' : byte_string( line.split()[2] ), + 'bytes_out': byte_string( line.split()[4] ), + + # convert IP address from hex 'FE000A0A' to decimal '4261415434' + 'loc' : kernel2ip( line.split()[7] ), + } + result['data'].append(data) + + return result + +def do_mr_mfc(): + """ + Read contents of file /proc/net/ip_mr_cache and print a more human + friendly version to the command line. IPv4 addresses present as + 32bit integers in hex format are converted to IPv4 notation, too. + """ + + with open('/proc/net/ip_mr_cache', 'r') as f: + lines = len(f.readlines()) + if lines < 2: + return None + + # We need this to convert from interface index to a real interface name + # Thus we also skip the format identifier on list index 0 + vif = do_mr_vif()['data'][1:] + + result = { + 'data': [] + } + + # Build up table format string + table_format = { + 'group' : 'Group', + 'origin': 'Origin', + 'iif' : 'In', + 'oifs' : ['Out'], + 'pkts' : 'Pkts', + 'bytes' : 'Bytes', + 'wrong' : 'Wrong' + } + result['data'].append(table_format) + + # read and parse information from /proc filesystem + with open('/proc/net/ip_mr_cache', 'r') as f: + header_line = next(f) + for line in f: + data = { + # convert IP address from hex 'FE000A0A' to decimal '4261415434' + 'group' : kernel2ip( line.split()[0] ), + 'origin': kernel2ip( line.split()[1] ), + + 'iif' : '--', + 'pkts' : '', + 'bytes' : '', + 'wrong' : '', + 'oifs' : [] + } + + iif = int( line.split()[2] ) + if not ((iif == -1) or (iif == 65535)): + data['pkts'] = line.split()[3] + data['bytes'] = byte_string( line.split()[4] ) + data['wrong'] = line.split()[5] + + # convert index to real interface name + data['iif'] = vif[iif]['interface'] + + # convert each output interface index to a real interface name + for oif in line.split()[6:]: + idx = int( oif.split(':')[0] ) + data['oifs'].append( vif[idx]['interface'] ) + + result['data'].append(data) + + return result + +if __name__ == '__main__': + args = parser.parse_args() + + # Do nothing if service is not configured + c = vyos.config.Config() + if not c.exists_effective('protocols igmp-proxy'): + print("IGMP proxy is not configured") + sys.exit(0) + + if args.interface: + data = do_mr_vif() + if data: + tmpl = jinja2.Template(vif_out_tmpl) + print(tmpl.render(data)) + + sys.exit(0) + elif args.mfc: + data = do_mr_mfc() + if data: + tmpl = jinja2.Template(mfc_out_tmpl) + print(tmpl.render(data)) + + sys.exit(0) + else: + parser.print_help() + sys.exit(1) + diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py new file mode 100755 index 000000000..d4dae3cd1 --- /dev/null +++ b/src/op_mode/show_interfaces.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 + +# Copyright 2017, 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import sys +import glob +import datetime +import argparse +import netifaces + +from vyos.ifconfig import Section +from vyos.ifconfig import Interface +from vyos.ifconfig import VRRP +from vyos.util import cmd + + +# interfaces = Sections.reserved() +interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe', 'pppoa', 'adsl'] +glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces)) + + +actions = {} +def register (name): + """ + decorator to register a function into actions with a name + it allows to use actions[name] to call the registered function + """ + def _register(function): + actions[name] = function + return function + return _register + + +def filtered_interfaces(ifnames, iftypes, vif, vrrp): + """ + get all the interfaces from the OS and returns them + ifnames can be used to filter which interfaces should be considered + + ifnames: a list of interfaces names to consider, empty do not filter + return an instance of the interface class + """ + allnames = Section.interfaces() + + vrrp_interfaces = VRRP.active_interfaces() if vrrp else [] + + for ifname in allnames: + if ifnames and ifname not in ifnames: + continue + + # return the class which can handle this interface name + klass = Section.klass(ifname) + # connect to the interface + interface = klass(ifname, create=False, debug=False) + + if iftypes and interface.definition['section'] not in iftypes: + continue + + if vif and not '.' in ifname: + continue + + if vrrp and ifname not in vrrp_interfaces: + continue + + yield interface + + +def split_text(text, used=0): + """ + take a string and attempt to split it to fit with the width of the screen + + text: the string to split + used: number of characted already used in the screen + """ + returned = cmd('stty size') + if len(returned) == 2: + rows, columns = [int(_) for _ in returned] + else: + rows, columns = (40, 80) + + desc_len = columns - used + + line = '' + for word in text.split(): + if len(line) + len(word) < desc_len: + line = f'{line} {word}' + continue + if line: + yield line[1:] + else: + line = f'{line} {word}' + + yield line[1:] + + +def get_vrrp_intf(): + return [intf for intf in Section.interfaces() if intf.is_vrrp()] + + +def get_counter_val(clear, now): + """ + attempt to correct a counter if it wrapped, copied from perl + + clear: previous counter + now: the current counter + """ + # This function has to deal with both 32 and 64 bit counters + if clear == 0: + return now + + # device is using 64 bit values assume they never wrap + value = now - clear + if (now >> 32) != 0: + return value + + # The counter has rolled. If the counter has rolled + # multiple times since the clear value, then this math + # is meaningless. + if (value < 0): + value = (4294967296 - clear) + now + + return value + + +@register('help') +def usage(*args): + print(f"Usage: {sys.argv[0]} [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION") + print(f" NAME = " + ' | '.join(Section.interfaces())) + print(f" TYPE = " + ' | '.join(Section.sections())) + print(f" ACTION = " + ' | '.join(actions)) + sys.exit(1) + + +@register('allowed') +def run_allowed(**kwarg): + sys.stdout.write(' '.join(Section.interfaces())) + + +def pppoe(ifname): + out = cmd(f'ps -C pppd -f') + if ifname in out: + return 'C' + elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]: + return 'D' + return '' + + +@register('show') +def run_show_intf(ifnames, iftypes, vif, vrrp): + handled = [] + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + handled.append(interface.ifname) + cache = interface.operational.load_counters() + + out = cmd(f'ip addr show {interface.ifname}') + out = re.sub(f'^\d+:\s+','',out) + if re.search("link/tunnel6", out): + tunnel = cmd(f'ip -6 tun show {interface.ifname}') + # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000) + tunnel = re.sub('.*encap', 'encap', tunnel) + out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{tunnel}\g<1>\g<2>', out) + + print(out) + + timestamp = int(cache.get('timestamp', 0)) + if timestamp: + when = interface.operational.strtime(timestamp) + print(f' Last clear: {when}') + + description = interface.get_alias() + if description: + print(f' Description: {description}') + + print() + print(interface.operational.formated_stats()) + + for ifname in ifnames: + if ifname not in handled and ifname.startswith('pppoe'): + state = pppoe(ifname) + if not state: + continue + string = { + 'C': 'Coming up', + 'D': 'Link down', + }[state] + print('{}: {}'.format(ifname, string)) + + +@register('show-brief') +def run_show_intf_brief(ifnames, iftypes, vif, vrrp): + format1 = '%-16s %-33s %-4s %s' + format2 = '%-16s %s' + + print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down') + print(format1 % ("Interface", "IP Address", "S/L", "Description")) + print(format1 % ("---------", "----------", "---", "-----------")) + + handled = [] + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + handled.append(interface.ifname) + + oper_state = interface.operational.get_state() + admin_state = interface.get_admin_state() + + intf = [interface.ifname,] + oper = ['u', ] if oper_state in ('up', 'unknown') else ['A', ] + admin = ['u', ] if oper_state in ('up', 'unknown') else ['D', ] + addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ] + descs = list(split_text(interface.get_alias(),0)) + + while intf or oper or admin or addrs or descs: + i = intf.pop(0) if intf else '' + a = addrs.pop(0) if addrs else '' + d = descs.pop(0) if descs else '' + s = [oper.pop(0)] if oper else [] + l = [admin.pop(0)] if admin else [] + if len(a) < 33: + print(format1 % (i, a, '/'.join(s+l), d)) + else: + print(format2 % (i, a)) + print(format1 % ('', '', '/'.join(s+l), d)) + + for ifname in ifnames: + if ifname not in handled and ifname.startswith('pppoe'): + state = pppoe(ifname) + if not state: + continue + string = { + 'C': 'u/D', + 'D': 'A/D', + }[state] + print(format1 % (ifname, '', string, '')) + + +@register('show-count') +def run_show_counters(ifnames, iftypes, vif, vrrp): + formating = '%-12s %10s %10s %10s %10s' + print(formating % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) + + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + oper = interface.operational.get_state() + + if oper not in ('up','unknown'): + continue + + stats = interface.operational.get_stats() + cache = interface.operational.load_counters() + print(formating % ( + interface.ifname, + get_counter_val(cache['rx_packets'], stats['rx_packets']), + get_counter_val(cache['rx_bytes'], stats['rx_bytes']), + get_counter_val(cache['tx_packets'], stats['tx_packets']), + get_counter_val(cache['tx_bytes'], stats['tx_bytes']), + )) + + +@register('clear') +def run_clear_intf(ifnames, iftypes, vif, vrrp): + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + print(f'Clearing {interface.ifname}') + interface.operational.clear_counters() + + +@register('reset') +def run_reset_intf(ifnames, iftypes, vif, vrrp): + for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + interface.operational.reset_counters() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(add_help=False, description='Show interface information') + parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface(s)') + parser.add_argument('--intf-type', action="store", type=str, default='', help='only show the specified interface type') + parser.add_argument('--action', action="store", type=str, default='show', help='action to perform') + parser.add_argument('--vif', action='store_true', default=False, help="only show vif interfaces") + parser.add_argument('--vrrp', action='store_true', default=False, help="only show vrrp interfaces") + parser.add_argument('--help', action='store_true', default=False, help="show help") + + args = parser.parse_args() + + def missing(*args): + print('Invalid action [{args.action}]') + usage() + + actions.get(args.action, missing)( + [_ for _ in args.intf.split(' ') if _], + [_ for _ in args.intf_type.split(' ') if _], + args.vif, + args.vrrp + ) diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py new file mode 100755 index 000000000..e319cc38d --- /dev/null +++ b/src/op_mode/show_ipsec_sa.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + +import vici +import tabulate +import hurry.filesize + +import vyos.util + + +try: + session = vici.Session() + sas = session.list_sas() +except PermissionError: + print("You do not have a permission to connect to the IPsec daemon") + sys.exit(1) +except ConnectionRefusedError: + print("IPsec is not runing") + sys.exit(1) +except Exception as e: + print("An error occured: {0}".format(e)) + sys.exit(1) + +sa_data = [] + +for sa in sas: + # list_sas() returns a list of single-item dicts + for peer in sa: + parent_sa = sa[peer] + + if parent_sa["state"] == b"ESTABLISHED": + state = "up" + else: + state = "down" + + if state == "up": + uptime = vyos.util.seconds_to_human(parent_sa["established"].decode()) + else: + uptime = "N/A" + + remote_host = parent_sa["remote-host"].decode() + remote_id = parent_sa["remote-id"].decode() + + if remote_host == remote_id: + remote_id = "N/A" + + # The counters can only be obtained from the child SAs + child_sas = parent_sa["child-sas"] + installed_sas = {k: v for k, v in child_sas.items() if v["state"] == b"INSTALLED"} + + if not installed_sas: + data = [peer, state, "N/A", "N/A", "N/A", "N/A", "N/A", "N/A"] + sa_data.append(data) + else: + for csa in installed_sas: + isa = installed_sas[csa] + + bytes_in = hurry.filesize.size(int(isa["bytes-in"].decode())) + bytes_out = hurry.filesize.size(int(isa["bytes-out"].decode())) + bytes_str = "{0}/{1}".format(bytes_in, bytes_out) + + pkts_in = hurry.filesize.size(int(isa["packets-in"].decode()), system=hurry.filesize.si) + pkts_out = hurry.filesize.size(int(isa["packets-out"].decode()), system=hurry.filesize.si) + pkts_str = "{0}/{1}".format(pkts_in, pkts_out) + # Remove B from <1K values + pkts_str = re.sub(r'B', r'', pkts_str) + + enc = isa["encr-alg"].decode() + if "encr-keysize" in isa: + key_size = isa["encr-keysize"].decode() + else: + key_size = "" + if "integ-alg" in isa: + hash = isa["integ-alg"].decode() + else: + hash = "" + if "dh-group" in isa: + dh_group = isa["dh-group"].decode() + else: + dh_group = "" + + proposal = enc + if key_size: + proposal = "{0}_{1}".format(proposal, key_size) + if hash: + proposal = "{0}/{1}".format(proposal, hash) + if dh_group: + proposal = "{0}/{1}".format(proposal, dh_group) + + data = [peer, state, uptime, bytes_str, pkts_str, remote_host, remote_id, proposal] + sa_data.append(data) + +headers = ["Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", "Remote address", "Remote ID", "Proposal"] +output = tabulate.tabulate(sa_data, headers) +print(output) diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py new file mode 100755 index 000000000..0b53112f2 --- /dev/null +++ b/src/op_mode/show_nat_statistics.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import jmespath +import json + +from argparse import ArgumentParser +from jinja2 import Template +from sys import exit +from vyos.util import cmd + +OUT_TMPL_SRC=""" +rule pkts bytes interface +---- ---- ----- --------- +{% for r in output %} +{%- if r.comment -%} +{%- set packets = r.counter.packets -%} +{%- set bytes = r.counter.bytes -%} +{%- set interface = r.interface -%} +{# remove rule comment prefix #} +{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') -%} +{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }} +{%- endif %} +{% endfor %} +""" + +parser = ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") +group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") +args = parser.parse_args() + +if args.source or args.destination: + tmp = cmd('sudo nft -j list table nat') + tmp = json.loads(tmp) + + source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" + destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" + data = { + 'output' : jmespath.search(source if args.source else destination, tmp), + 'direction' : 'source' if args.source else 'destination' + } + + tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True) + print(tmpl.render(data)) + exit(0) +else: + parser.print_help() + exit(1) + diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py new file mode 100755 index 000000000..3af33b78e --- /dev/null +++ b/src/op_mode/show_nat_translations.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +''' +show nat translations +''' + +import os +import sys +import ipaddress +import argparse +import xmltodict + +from vyos.util import popen +from vyos.util import DEVNULL + +conntrack = '/usr/sbin/conntrack' + +verbose_format = "%-20s %-18s %-20s %-18s" +normal_format = "%-20s %-20s %-4s %-8s %s" + + +def headers(verbose, pipe): + if verbose: + return verbose_format % ('Pre-NAT src', 'Pre-NAT dst', 'Post-NAT src', 'Post-NAT dst') + return normal_format % ('Pre-NAT', 'Post-NAT', 'Prot', 'Timeout', 'Type' if pipe else '') + + +def command(srcdest, proto, ipaddr): + command = f'{conntrack} -o xml -L' + + if proto: + command += f' -p {proto}' + + if srcdest == 'source': + command += ' -n' + if ipaddr: + command += f' --orig-src {ipaddr}' + if srcdest == 'destination': + command += ' -g' + + return command + + +def run(command): + xml, code = popen(command,stderr=DEVNULL) + if code: + sys.exit('conntrack failed') + return xml + + +def content(xmlfile): + xml = '' + with open(xmlfile,'r') as r: + xml += r.read() + return xml + + +def pipe(): + xml = '' + while True: + line = sys.stdin.readline() + xml += line + if '</conntrack>' in line: + break + + sys.stdin = open('/dev/tty') + return xml + + +def process(data, stats, protocol, pipe, verbose, flowtype=''): + if not data: + return + + parsed = xmltodict.parse(data) + + print(headers(verbose, pipe)) + + # to help the linter to detect typos + ORIGINAL = 'original' + REPLY = 'reply' + INDEPENDANT = 'independent' + SPORT = 'sport' + DPORT = 'dport' + SRC = 'src' + DST = 'dst' + + for rule in parsed['conntrack']['flow']: + src, dst, sport, dport, proto = {}, {}, {}, {}, {} + packet_count, byte_count = {}, {} + timeout, use = 0, 0 + + rule_type = rule.get('type', '') + + for meta in rule['meta']: + # print(meta) + direction = meta['@direction'] + + if direction in (ORIGINAL, REPLY): + if 'layer3' in meta: + l3 = meta['layer3'] + src[direction] = l3[SRC] + dst[direction] = l3[DST] + + if 'layer4' in meta: + l4 = meta['layer4'] + sp = l4.get(SPORT, '') + dp = l4.get(DPORT, '') + if sp: + sport[direction] = sp + if dp: + dport[direction] = dp + proto[direction] = l4.get('@protoname','') + + if stats and 'counters' in meta: + packet_count[direction] = meta['packets'] + byte_count[direction] = meta['bytes'] + continue + + if direction == INDEPENDANT: + timeout = meta['timeout'] + use = meta['use'] + continue + + in_src = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if ORIGINAL in sport else src[ORIGINAL] + in_dst = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if ORIGINAL in dport else dst[ORIGINAL] + + # inverted the the perl code !!? + out_dst = '%s:%s' % (dst[REPLY], dport[REPLY]) if REPLY in dport else dst[REPLY] + out_src = '%s:%s' % (src[REPLY], sport[REPLY]) if REPLY in sport else src[REPLY] + + if flowtype == 'source': + v = ORIGINAL in sport and REPLY in dport + f = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if v else src[ORIGINAL] + t = '%s:%s' % (dst[REPLY], dport[REPLY]) if v else dst[REPLY] + else: + v = ORIGINAL in dport and REPLY in sport + f = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if v else dst[ORIGINAL] + t = '%s:%s' % (src[REPLY], sport[REPLY]) if v else src[REPLY] + + # Thomas: I do not believe proto should be an option + p = proto.get('original', '') + if protocol and p != protocol: + continue + + if verbose: + msg = verbose_format % (in_src, in_dst, out_dst, out_src) + p = f'{p}: ' if p else '' + msg += f'\n {p}{f} ==> {t}' + msg += f' timeout: {timeout}' if timeout else '' + msg += f' use: {use} ' if use else '' + msg += f' type: {rule_type}' if rule_type else '' + print(msg) + else: + print(normal_format % (f, t, p, timeout, rule_type if rule_type else '')) + + if stats: + for direction in ('original', 'reply'): + if direction in packet_count: + print(' %-8s: packets %s, bytes %s' % direction, packet_count[direction], byte_count[direction]) + + +def main(): + parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__) + parser.add_argument('--verbose', help='provide more details about the flows', action='store_true') + parser.add_argument('--proto', help='filter by protocol', default='', type=str) + parser.add_argument('--file', help='read the conntrack xml from a file', type=str) + parser.add_argument('--stats', help='add usage statistics', action='store_true') + parser.add_argument('--type', help='NAT type (source, destination)', required=True, type=str) + parser.add_argument('--ipaddr', help='source ip address to filter on', type=ipaddress.ip_address) + parser.add_argument('--pipe', help='read conntrack xml data from stdin', action='store_true') + + arg = parser.parse_args() + + if arg.type not in ('source', 'destination'): + sys.exit('Unknown NAT type!') + + if arg.pipe: + process(pipe(), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) + elif arg.file: + process(content(arg.file), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) + else: + process(run(command(arg.type, arg.proto, arg.ipaddr)), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) + + +if __name__ == '__main__': + main() diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py new file mode 100755 index 000000000..32918ddce --- /dev/null +++ b/src/op_mode/show_openvpn.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import jinja2 +import argparse + +from sys import exit +from vyos.config import Config + +outp_tmpl = """ +{% if clients %} +OpenVPN status on {{ intf }} + +Client CN Remote Host Local Host TX bytes RX bytes Connected Since +--------- ----------- ---------- -------- -------- --------------- +{%- for c in clients %} +{{ "%-15s"|format(c.name) }} {{ "%-21s"|format(c.remote) }} {{ "%-21s"|format(local) }} {{ "%-9s"|format(c.tx_bytes) }} {{ "%-9s"|format(c.rx_bytes) }} {{ c.online_since }} +{%- endfor %} +{% endif %} +""" + +def bytes2HR(size): + # we need to operate in integers + size = int(size) + + suff = ['B', 'KB', 'MB', 'GB', 'TB'] + suffIdx = 0 + + while size > 1024: + # incr. suffix index + suffIdx += 1 + # divide + size = size/1024.0 + + output="{0:.1f} {1}".format(size, suff[suffIdx]) + return output + +def get_status(mode, interface): + status_file = '/opt/vyatta/etc/openvpn/status/{}.status'.format(interface) + # this is an empirical value - I assume we have no more then 999999 + # current OpenVPN connections + routing_table_line = 999999 + + data = { + 'mode': mode, + 'intf': interface, + 'local': 'N/A', + 'date': '', + 'clients': [], + } + + if not os.path.exists(status_file): + return data + + with open(status_file, 'r') as f: + lines = f.readlines() + for line_no, line in enumerate(lines): + # remove trailing newline character first + line = line.rstrip('\n') + + # check first line header + if line_no == 0: + if mode == 'server': + if not line == 'OpenVPN CLIENT LIST': + raise NameError('Expected "OpenVPN CLIENT LIST"') + else: + if not line == 'OpenVPN STATISTICS': + raise NameError('Expected "OpenVPN STATISTICS"') + + continue + + # second line informs us when the status file has been last updated + if line_no == 1: + data['date'] = line.lstrip('Updated,').rstrip('\n') + continue + + if mode == 'server': + # followed by line3 giving output information and the actual output data + # + # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since + # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019 + # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019 + # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019 + if (line_no >= 3) and (line_no < routing_table_line): + # indicator that there are no more clients and we will continue with the + # routing table + if line == 'ROUTING TABLE': + routing_table_line = line_no + continue + + client = { + 'name': line.split(',')[0], + 'remote': line.split(',')[1], + 'rx_bytes': bytes2HR(line.split(',')[2]), + 'tx_bytes': bytes2HR(line.split(',')[3]), + 'online_since': line.split(',')[4] + } + + data['clients'].append(client) + continue + else: + if line_no == 2: + client = { + 'name': 'N/A', + 'remote': 'N/A', + 'rx_bytes': bytes2HR(line.split(',')[1]), + 'tx_bytes': '', + 'online_since': 'N/A' + } + continue + + if line_no == 3: + client['tx_bytes'] = bytes2HR(line.split(',')[1]) + data['clients'].append(client) + break + + return data + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('-m', '--mode', help='OpenVPN operation mode (server, client, site-2-site)', required=True) + + args = parser.parse_args() + + # Do nothing if service is not configured + config = Config() + if len(config.list_effective_nodes('interfaces openvpn')) == 0: + print("No OpenVPN interfaces configured") + exit(0) + + # search all OpenVPN interfaces and add those with a matching mode to our + # interfaces list + interfaces = [] + for intf in config.list_effective_nodes('interfaces openvpn'): + # get interface type (server, client, site-to-site) + mode = config.return_effective_value('interfaces openvpn {} mode'.format(intf)) + if args.mode == mode: + interfaces.append(intf) + + for intf in interfaces: + data = get_status(args.mode, intf) + local_host = config.return_effective_value('interfaces openvpn {} local-host'.format(intf)) + local_port = config.return_effective_value('interfaces openvpn {} local-port'.format(intf)) + if local_host and local_port: + data['local'] = local_host + ':' + local_port + + if args.mode in ['client', 'site-to-site']: + for client in data['clients']: + if config.exists_effective('interfaces openvpn {} shared-secret-key-file'.format(intf)): + client['name'] = "None (PSK)" + + remote_host = config.return_effective_values('interfaces openvpn {} remote-host'.format(intf)) + remote_port = config.return_effective_value('interfaces openvpn {} remote-port'.format(intf)) + + if not remote_port: + remote_port = '1194' + + if len(remote_host) >= 1: + client['remote'] = str(remote_host[0]) + ':' + remote_port + + tmpl = jinja2.Template(outp_tmpl) + print(tmpl.render(data)) + diff --git a/src/op_mode/show_raid.sh b/src/op_mode/show_raid.sh new file mode 100755 index 000000000..ba4174692 --- /dev/null +++ b/src/op_mode/show_raid.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +raid_set_name=$1 +raid_sets=`cat /proc/partitions | grep md | awk '{ print $4 }'` +valid_set=`echo $raid_sets | grep $raid_set_name` +if [ -z $valid_set ]; then + echo "$raid_set_name is not a RAID set" +else + if [ -r /dev/${raid_set_name} ]; then + # This should work without sudo because we have read + # access to the dev, but for some reason mdadm must be + # run as root in order to succeed. + sudo /sbin/mdadm --detail /dev/${raid_set_name} + else + echo "Must be administrator or root to display RAID status" + fi +fi diff --git a/src/op_mode/show_ram.sh b/src/op_mode/show_ram.sh new file mode 100755 index 000000000..b013e16f8 --- /dev/null +++ b/src/op_mode/show_ram.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# +# Module: vyos-show-ram.sh +# Displays memory usage information in minimalistic format +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +MB_DIVISOR=1024 + +TOTAL=$(cat /proc/meminfo | grep -E "^MemTotal:" | awk -F ' ' '{print $2}') +FREE=$(cat /proc/meminfo | grep -E "^MemFree:" | awk -F ' ' '{print $2}') +BUFFERS=$(cat /proc/meminfo | grep -E "^Buffers:" | awk -F ' ' '{print $2}') +CACHED=$(cat /proc/meminfo | grep -E "^Cached:" | awk -F ' ' '{print $2}') + +DISPLAY_FREE=$(( ($FREE + $BUFFERS + $CACHED) / $MB_DIVISOR )) +DISPLAY_TOTAL=$(( $TOTAL / $MB_DIVISOR )) +DISPLAY_USED=$(( $DISPLAY_TOTAL - $DISPLAY_FREE )) + +echo "Total: $DISPLAY_TOTAL" +echo "Free: $DISPLAY_FREE" +echo "Used: $DISPLAY_USED" diff --git a/src/op_mode/show_sensors.py b/src/op_mode/show_sensors.py new file mode 100755 index 000000000..6ae477647 --- /dev/null +++ b/src/op_mode/show_sensors.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +import re +import sys +from vyos.util import popen +from vyos.util import DEVNULL +output,retcode = popen("sensors --no-adapter", stderr=DEVNULL) +if retcode == 0: + print (output) + sys.exit(0) +else: + output,retcode = popen("sensors-detect --auto",stderr=DEVNULL) + match = re.search(r'#----cut here----(.*)#----cut here----',output, re.DOTALL) + if match: + for module in match.group(0).split('\n'): + if not module.startswith("#"): + popen("modprobe {}".format(module.strip())) + output,retcode = popen("sensors --no-adapter", stderr=DEVNULL) + if retcode == 0: + print (output) + sys.exit(0) + + +print ("No sensors found") +sys.exit(1) + + diff --git a/src/op_mode/show_usb_serial.py b/src/op_mode/show_usb_serial.py new file mode 100755 index 000000000..776898c25 --- /dev/null +++ b/src/op_mode/show_usb_serial.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from jinja2 import Template +from pyudev import Context, Devices +from sys import exit + +OUT_TMPL_SRC = """Device Model Vendor +------ ------ ------ +{%- for d in devices %} +{{ "%-16s" | format(d.device) }} {{ "%-19s" | format(d.model)}} {{d.vendor}} +{%- endfor %} + +""" + +data = { + 'devices': [] +} + + +base_directory = '/dev/serial/by-bus' +if not os.path.isdir(base_directory): + print("No USB to serial converter connected") + exit(0) + +context = Context() +for root, dirs, files in os.walk(base_directory): + for basename in files: + os.path.join(root, basename) + device = Devices.from_device_file(context, os.path.join(root, basename)) + tmp = { + 'device': basename, + 'model': device.properties.get('ID_MODEL'), + 'vendor': device.properties.get('ID_VENDOR_FROM_DATABASE') + } + data['devices'].append(tmp) + +data['devices'] = sorted(data['devices'], key = lambda i: i['device']) +tmpl = Template(OUT_TMPL_SRC) +print(tmpl.render(data)) + +exit(0) diff --git a/src/op_mode/show_users.py b/src/op_mode/show_users.py new file mode 100755 index 000000000..8e4f12851 --- /dev/null +++ b/src/op_mode/show_users.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import argparse +import pwd +import spwd +import struct +import sys +from time import ctime + +from tabulate import tabulate +from vyos.config import Config + + +class UserInfo: + def __init__(self, uid, name, user_type, is_locked, login_time, tty, host): + self.uid = uid + self.name = name + self.user_type = user_type + self.is_locked = is_locked + self.login_time = login_time + self.tty = tty + self.host = host + + +filters = { + 'default': lambda user: not user.is_locked, # Default is everything but locked accounts + 'vyos': lambda user: user.user_type == 'vyos', + 'other': lambda user: user.user_type != 'vyos', + 'locked': lambda user: user.is_locked, + 'all': lambda user: True +} + + +def is_locked(user_name: str) -> bool: + """Check if a given user has password in shadow db""" + + try: + encrypted_password = spwd.getspnam(user_name)[1] + return encrypted_password == '*' or encrypted_password.startswith('!') + except (KeyError, PermissionError): + print('Cannot access shadow database, ensure this script is run with sufficient permissions') + sys.exit(1) + + +def decode_lastlog(lastlog_file, uid: int): + """Decode last login info of a given user uid from the lastlog file""" + + struct_fmt = '=L32s256s' + recordsize = struct.calcsize(struct_fmt) + lastlog_file.seek(recordsize * uid) + buf = lastlog_file.read(recordsize) + if len(buf) < recordsize: + return None + (time, tty, host) = struct.unpack(struct_fmt, buf) + time = 'never logged in' if time == 0 else ctime(time) + tty = tty.strip(b'\x00') + host = host.strip(b'\x00') + return time, tty, host + + +def list_users(): + cfg = Config() + vyos_users = cfg.list_effective_nodes('system login user') + users = [] + with open('/var/log/lastlog', 'rb') as lastlog_file: + for (name, _, uid, _, _, _, _) in pwd.getpwall(): + lastlog_info = decode_lastlog(lastlog_file, uid) + if lastlog_info is None: + continue + user_info = UserInfo( + uid, name, + user_type='vyos' if name in vyos_users else 'other', + is_locked=is_locked(name), + login_time=lastlog_info[0], + tty=lastlog_info[1], + host=lastlog_info[2]) + users.append(user_info) + return users + + +def main(): + parser = argparse.ArgumentParser(prog=sys.argv[0], add_help=False) + parser.add_argument('type', nargs='?', choices=['all', 'vyos', 'other', 'locked']) + args = parser.parse_args() + + filter_type = args.type if args.type is not None else 'default' + filter_expr = filters[filter_type] + + headers = ['Username', 'Type', 'Locked', 'Tty', 'From', 'Last login'] + table_data = [] + for user in list_users(): + if filter_expr(user): + table_data.append([user.name, user.user_type, user.is_locked, user.tty, user.host, user.login_time]) + print(tabulate(table_data, headers, tablefmt='simple')) + + +if __name__ == '__main__': + main() diff --git a/src/op_mode/show_version.py b/src/op_mode/show_version.py new file mode 100755 index 000000000..d0d5c6785 --- /dev/null +++ b/src/op_mode/show_version.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2016-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Purpose: +# Displays image version and system information. +# Used by the "run show version" command. + +import argparse +import vyos.version +import vyos.limericks + +from jinja2 import Template +from sys import exit +from vyos.util import call + +parser = argparse.ArgumentParser() +parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions") +parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output") +parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output") + +version_output_tmpl = """ +Version: VyOS {{version}} +Release Train: {{release_train}} + +Built by: {{built_by}} +Built on: {{built_on}} +Build UUID: {{build_uuid}} +Build Commit ID: {{build_git}} + +Architecture: {{system_arch}} +Boot via: {{boot_via}} +System type: {{system_type}} + +Hardware vendor: {{hardware_vendor}} +Hardware model: {{hardware_model}} +Hardware S/N: {{hardware_serial}} +Hardware UUID: {{hardware_uuid}} + +Copyright: VyOS maintainers and contributors +""" + +if __name__ == '__main__': + args = parser.parse_args() + + version_data = vyos.version.get_full_version_data() + + if args.json: + import json + print(json.dumps(version_data)) + exit(0) + + tmpl = Template(version_output_tmpl) + print(tmpl.render(version_data)) + + if args.all: + print("Package versions:") + call("dpkg -l") + + if args.funny: + print(vyos.limericks.get_random()) diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py new file mode 100755 index 000000000..73688c4ea --- /dev/null +++ b/src/op_mode/show_vpn_ra.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import re + +from vyos.util import popen + +# chech connection to pptp and l2tp daemon +def get_sessions(): + absent_pptp = False + absent_l2tp = False + pptp_cmd = "accel-cmd -p 2003 show sessions" + l2tp_cmd = "accel-cmd -p 2004 show sessions" + err_pattern = "^Connection.+failed$" + # This value for chack only output header without sessions. + len_def_header = 170 + + # Check pptp + output, err = popen(pptp_cmd, decode='utf-8') + if not err and len(output) > len_def_header and not re.search(err_pattern, output): + print(output) + else: + absent_pptp = True + + # Check l2tp + output, err = popen(l2tp_cmd, decode='utf-8') + if not err and len(output) > len_def_header and not re.search(err_pattern, output): + print(output) + else: + absent_l2tp = True + + if absent_l2tp and absent_pptp: + print("No active remote access VPN sessions") + + +def main(): + get_sessions() + + +if __name__ == '__main__': + main() diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py new file mode 100755 index 000000000..b6bb73d01 --- /dev/null +++ b/src/op_mode/show_vrf.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import jinja2 +from json import loads + +from vyos.util import cmd + +vrf_out_tmpl = """ +VRF name state mac address flags interfaces +-------- ----- ----------- ----- ---------- +{%- for v in vrf %} +{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}} +{%- endfor %} + +""" + +def list_vrfs(): + command = 'ip -j -br link show type vrf' + answer = loads(cmd(command)) + return [_ for _ in answer if _] + +def list_vrf_members(vrf): + command = f'ip -j -br link show master {vrf}' + answer = loads(cmd(command)) + return [_ for _ in answer if _] + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-e", "--extensive", action="store_true", + help="provide detailed vrf informatio") +parser.add_argument('interface', metavar='I', type=str, nargs='?', + help='interface to display') + +args = parser.parse_args() + +if args.extensive: + data = { 'vrf': [] } + for vrf in list_vrfs(): + name = vrf['ifname'] + if args.interface and name != args.interface: + continue + + vrf['members'] = [] + for member in list_vrf_members(name): + vrf['members'].append(member['ifname']) + data['vrf'].append(vrf) + + tmpl = jinja2.Template(vrf_out_tmpl) + print(tmpl.render(data)) + +else: + print(" ".join([vrf['ifname'] for vrf in list_vrfs()])) diff --git a/src/op_mode/show_wireless.py b/src/op_mode/show_wireless.py new file mode 100755 index 000000000..b5ee3aee1 --- /dev/null +++ b/src/op_mode/show_wireless.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import re + +from sys import exit +from copy import deepcopy + +from vyos.config import Config +from vyos.util import popen + +parser = argparse.ArgumentParser() +parser.add_argument("-s", "--scan", help="Scan for Wireless APs on given interface, e.g. 'wlan0'") +parser.add_argument("-b", "--brief", action="store_true", help="Show wireless configuration") +parser.add_argument("-c", "--stations", help="Show wireless clients connected on interface, e.g. 'wlan0'") + + +def show_brief(): + config = Config() + if len(config.list_effective_nodes('interfaces wireless')) == 0: + print("No Wireless interfaces configured") + exit(0) + + interfaces = [] + for intf in config.list_effective_nodes('interfaces wireless'): + config.set_level('interfaces wireless {}'.format(intf)) + data = { + 'name': intf, + 'type': '', + 'ssid': '', + 'channel': '' + } + data['type'] = config.return_effective_value('type') + data['ssid'] = config.return_effective_value('ssid') + data['channel'] = config.return_effective_value('channel') + + interfaces.append(data) + + return interfaces + +def ssid_scan(intf): + # XXX: This ignores errors + tmp, _ = popen(f'/sbin/iw dev {intf} scan ap-force') + networks = [] + data = { + 'ssid': '', + 'mac': '', + 'channel': '', + 'signal': '' + } + re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})') + for line in tmp.splitlines(): + if line.startswith('BSS '): + ssid = deepcopy(data) + ssid['mac'] = re.search(re_mac, line).group() + + elif line.lstrip().startswith('SSID: '): + # SSID can be " SSID: WLAN-57 6405", thus strip all leading whitespaces + ssid['ssid'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('signal: '): + # Siganl can be " signal: -67.00 dBm", thus strip all leading whitespaces + ssid['signal'] = line.lstrip().split(':')[-1].split()[0] + + elif line.lstrip().startswith('DS Parameter set: channel'): + # Channel can be " DS Parameter set: channel 6" , thus + # strip all leading whitespaces + ssid['channel'] = line.lstrip().split(':')[-1].split()[-1] + networks.append(ssid) + continue + + return networks + +def show_clients(intf): + # XXX: This ignores errors + tmp, _ = popen(f'/sbin/iw dev {intf} station dump') + clients = [] + data = { + 'mac': '', + 'signal': '', + 'rx_bytes': '', + 'rx_packets': '', + 'tx_bytes': '', + 'tx_packets': '' + } + re_mac = re.compile(r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})') + for line in tmp.splitlines(): + if line.startswith('Station'): + client = deepcopy(data) + client['mac'] = re.search(re_mac, line).group() + + elif line.lstrip().startswith('signal avg:'): + client['signal'] = line.lstrip().split(':')[-1].lstrip().split()[0] + + elif line.lstrip().startswith('rx bytes:'): + client['rx_bytes'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('rx packets:'): + client['rx_packets'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('tx bytes:'): + client['tx_bytes'] = line.lstrip().split(':')[-1].lstrip() + + elif line.lstrip().startswith('tx packets:'): + client['tx_packets'] = line.lstrip().split(':')[-1].lstrip() + clients.append(client) + continue + + return clients + +if __name__ == '__main__': + args = parser.parse_args() + + if args.scan: + print("Address SSID Channel Signal (dbm)") + for network in ssid_scan(args.scan): + print("{:<17} {:<32} {:>3} {}".format(network['mac'], + network['ssid'], + network['channel'], + network['signal'])) + exit(0) + + elif args.brief: + print("Interface Type SSID Channel") + for intf in show_brief(): + print("{:<9} {:<12} {:<32} {:>3}".format(intf['name'], + intf['type'], + intf['ssid'], + intf['channel'])) + exit(0) + + elif args.stations: + print("Station Signal RX: bytes packets TX: bytes packets") + for client in show_clients(args.stations): + print("{:<17} {:>3} {:>15} {:>9} {:>15} {:>10} ".format(client['mac'], + client['signal'], client['rx_bytes'], client['rx_packets'], client['tx_bytes'], client['tx_packets'])) + + exit(0) + + else: + parser.print_help() + exit(1) diff --git a/src/op_mode/snmp.py b/src/op_mode/snmp.py new file mode 100755 index 000000000..5fae67881 --- /dev/null +++ b/src/op_mode/snmp.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# File: snmp.py +# Purpose: +# Show SNMP community/remote hosts +# Used by the "run show snmp community" commands. + +import os +import sys +import argparse + +from vyos.config import Config +from vyos.util import call + +config_file_daemon = r'/etc/snmp/snmpd.conf' + +parser = argparse.ArgumentParser(description='Retrieve infomration from running SNMP daemon') +parser.add_argument('--allowed', action="store_true", help='Show available SNMP communities') +parser.add_argument('--community', action="store", help='Show status of given SNMP community', type=str) +parser.add_argument('--host', action="store", help='SNMP host to connect to', type=str, default='localhost') + +config = { + 'communities': [], +} + +def read_config(): + with open(config_file_daemon, 'r') as f: + for line in f: + # Only get configured SNMP communitie + if line.startswith('rocommunity') or line.startswith('rwcommunity'): + string = line.split(' ') + # append community to the output list only once + c = string[1] + if c not in config['communities']: + config['communities'].append(c) + +def show_all(): + if len(config['communities']) > 0: + print(' '.join(config['communities'])) + +def show_community(c, h): + print('Status of SNMP community {0} on {1}'.format(c, h), flush=True) + call('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h)) + +if __name__ == '__main__': + args = parser.parse_args() + + # Do nothing if service is not configured + c = Config() + if not c.exists_effective('service snmp'): + print("SNMP service is not configured") + sys.exit(0) + + read_config() + + if args.allowed: + show_all() + sys.exit(1) + elif args.community: + show_community(args.community, args.host) + sys.exit(1) + else: + parser.print_help() + sys.exit(1) diff --git a/src/op_mode/snmp_ifmib.py b/src/op_mode/snmp_ifmib.py new file mode 100755 index 000000000..2479936bd --- /dev/null +++ b/src/op_mode/snmp_ifmib.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# File: snmp_ifmib.py +# Purpose: +# Show SNMP MIB information +# Used by the "run show snmp mib" commands. + +import sys +import argparse +import netifaces + +from vyos.config import Config +from vyos.util import popen + +parser = argparse.ArgumentParser(description='Retrieve SNMP interfaces information') +parser.add_argument('--ifindex', action='store', nargs='?', const='all', help='Show interface index') +parser.add_argument('--ifalias', action='store', nargs='?', const='all', help='Show interface aliase') +parser.add_argument('--ifdescr', action='store', nargs='?', const='all', help='Show interface description') + +def show_ifindex(intf): + out, err = popen(f'/bin/ip link show {intf}', decode='utf-8') + index = 'ifIndex = ' + out.split(':')[0] + return index.replace('\n', '') + +def show_ifalias(intf): + out, err = popen(f'/bin/ip link show {intf}', decode='utf-8') + alias = out.split('alias')[1].lstrip() if 'alias' in out else intf + return 'ifAlias = ' + alias.replace('\n', '') + +def show_ifdescr(i): + ven_id = '' + dev_id = '' + + try: + with open(r'/sys/class/net/' + i + '/device/vendor', 'r') as f: + ven_id = f.read().replace('\n', '') + except FileNotFoundError: + pass + + try: + with open(r'/sys/class/net/' + i + '/device/device', 'r') as f: + dev_id = f.read().replace('\n', '') + except FileNotFoundError: + pass + + if ven_id == '' and dev_id == '': + ret = 'ifDescr = {0}'.format(i) + return ret + + device = str(ven_id) + ':' + str(dev_id) + out, err = popen(f'/usr/bin/lspci -mm -d {device}', decode='utf-8') + + vendor = "" + device = "" + + # convert output to string + string = out.split('"') + if len(string) > 3: + vendor = string[3] + + if len(string) > 5: + device = string[5] + + ret = 'ifDescr = {0} {1}'.format(vendor, device) + return ret.replace('\n', '') + +if __name__ == '__main__': + args = parser.parse_args() + + # Do nothing if service is not configured + c = Config() + if not c.exists_effective('service snmp'): + print("SNMP service is not configured") + sys.exit(0) + + if args.ifindex: + if args.ifindex == 'all': + for i in netifaces.interfaces(): + print('{0}: {1}'.format(i, show_ifindex(i))) + else: + print('{0}: {1}'.format(args.ifindex, show_ifindex(args.ifindex))) + + elif args.ifalias: + if args.ifalias == 'all': + for i in netifaces.interfaces(): + print('{0}: {1}'.format(i, show_ifalias(i))) + else: + print('{0}: {1}'.format(args.ifalias, show_ifalias(args.ifalias))) + + elif args.ifdescr: + if args.ifdescr == 'all': + for i in netifaces.interfaces(): + print('{0}: {1}'.format(i, show_ifdescr(i))) + else: + print('{0}: {1}'.format(args.ifdescr, show_ifdescr(args.ifdescr))) + + else: + #eth0: ifIndex = 2 + # ifAlias = NET-MYBLL-MUCI-BACKBONE + # ifDescr = VMware VMXNET3 Ethernet Controller + #lo: ifIndex = 1 + for i in netifaces.interfaces(): + print('{0}:\t{1}'.format(i, show_ifindex(i))) + print('\t{0}'.format(show_ifalias(i))) + print('\t{0}'.format(show_ifdescr(i))) + + sys.exit(1) diff --git a/src/op_mode/snmp_v3.py b/src/op_mode/snmp_v3.py new file mode 100755 index 000000000..92601f15e --- /dev/null +++ b/src/op_mode/snmp_v3.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# File: snmp_v3.py +# Purpose: +# Show SNMP v3 information +# Used by the "run show snmp v3" commands. + +import sys +import jinja2 +import argparse + +from vyos.config import Config + +parser = argparse.ArgumentParser(description='Retrieve SNMP v3 information') +parser.add_argument('--all', action="store_true", help='Show all available information') +parser.add_argument('--group', action="store_true", help='Show the list of configured groups') +parser.add_argument('--trap', action="store_true", help='Show the list of configured targets') +parser.add_argument('--user', action="store_true", help='Show the list of configured users') +parser.add_argument('--view', action="store_true", help='Show the list of configured views') + +GROUP_OUTP_TMPL_SRC = """ +SNMPv3 Groups: + + Group View + ----- ---- + {% if group -%}{% for g in group -%} + {{ "%-20s" | format(g.name) }}{{ g.view }}({{ g.mode }}) + {% endfor %}{% endif %} +""" + +TRAPTGT_OUTP_TMPL_SRC = """ +SNMPv3 Trap-targets: + + Tpap-target Port Protocol Auth Priv Type EngineID User + ----------- ---- -------- ---- ---- ---- -------- ---- + {% if trap -%}{% for t in trap -%} + {{ "%-20s" | format(t.name) }} {{ t.port }} {{ t.proto }} {{ t.auth }} {{ t.priv }} {{ t.type }} {{ "%-32s" | format(t.engID) }} {{ t.user }} + {% endfor %}{% endif %} +""" + +USER_OUTP_TMPL_SRC = """ +SNMPv3 Users: + + User Auth Priv Mode Group + ---- ---- ---- ---- ----- + {% if user -%}{% for u in user -%} + {{ "%-20s" | format(u.name) }}{{ u.auth }} {{ u.priv }} {{ u.mode }} {{ u.group }} + {% endfor %}{% endif %} +""" + +VIEW_OUTP_TMPL_SRC = """ +SNMPv3 Views: + {% if view -%}{% for v in view %} + View : {{ v.name }} + OIDs : .{{ v.oids | join("\n .")}} + {% endfor %}{% endif %} +""" + +if __name__ == '__main__': + args = parser.parse_args() + + # Do nothing if service is not configured + c = Config() + if not c.exists_effective('service snmp v3'): + print("SNMP v3 is not configured") + sys.exit(0) + + data = { + 'group': [], + 'trap': [], + 'user': [], + 'view': [] + } + + if c.exists_effective('service snmp v3 group'): + for g in c.list_effective_nodes('service snmp v3 group'): + group = { + 'name': g, + 'mode': '', + 'view': '' + } + group['mode'] = c.return_effective_value('service snmp v3 group {0} mode'.format(g)) + group['view'] = c.return_effective_value('service snmp v3 group {0} view'.format(g)) + + data['group'].append(group) + + if c.exists_effective('service snmp v3 user'): + for u in c.list_effective_nodes('service snmp v3 user'): + user = { + 'name' : u, + 'mode' : '', + 'auth' : '', + 'priv' : '', + 'group': '' + } + user['mode'] = c.return_effective_value('service snmp v3 user {0} mode'.format(u)) + user['auth'] = c.return_effective_value('service snmp v3 user {0} auth type'.format(u)) + user['priv'] = c.return_effective_value('service snmp v3 user {0} privacy type'.format(u)) + user['group'] = c.return_effective_value('service snmp v3 user {0} group'.format(u)) + + data['user'].append(user) + + if c.exists_effective('service snmp v3 view'): + for v in c.list_effective_nodes('service snmp v3 view'): + view = { + 'name': v, + 'oids': [] + } + view['oids'] = c.list_effective_nodes('service snmp v3 view {0} oid'.format(v)) + + data['view'].append(view) + + if c.exists_effective('service snmp v3 trap-target'): + for t in c.list_effective_nodes('service snmp v3 trap-target'): + trap = { + 'name' : t, + 'port' : '', + 'proto': '', + 'auth' : '', + 'priv' : '', + 'type' : '', + 'engID': '', + 'user' : '' + } + trap['port'] = c.return_effective_value('service snmp v3 trap-target {0} port'.format(t)) + trap['proto'] = c.return_effective_value('service snmp v3 trap-target {0} protocol'.format(t)) + trap['auth'] = c.return_effective_value('service snmp v3 trap-target {0} auth type'.format(t)) + trap['priv'] = c.return_effective_value('service snmp v3 trap-target {0} privacy type'.format(t)) + trap['type'] = c.return_effective_value('service snmp v3 trap-target {0} type'.format(t)) + trap['engID'] = c.return_effective_value('service snmp v3 trap-target {0} engineid'.format(t)) + trap['user'] = c.return_effective_value('service snmp v3 trap-target {0} user'.format(t)) + + data['trap'].append(trap) + + print(data) + if args.all: + # Special case, print all templates ! + tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC) + print(tmpl.render(data)) + tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC) + print(tmpl.render(data)) + tmpl = jinja2.Template(USER_OUTP_TMPL_SRC) + print(tmpl.render(data)) + tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC) + print(tmpl.render(data)) + + elif args.group: + tmpl = jinja2.Template(GROUP_OUTP_TMPL_SRC) + print(tmpl.render(data)) + + elif args.trap: + tmpl = jinja2.Template(TRAPTGT_OUTP_TMPL_SRC) + print(tmpl.render(data)) + + elif args.user: + tmpl = jinja2.Template(USER_OUTP_TMPL_SRC) + print(tmpl.render(data)) + + elif args.view: + tmpl = jinja2.Template(VIEW_OUTP_TMPL_SRC) + print(tmpl.render(data)) + + else: + parser.print_help() + + sys.exit(1) diff --git a/src/op_mode/snmp_v3_showcerts.sh b/src/op_mode/snmp_v3_showcerts.sh new file mode 100755 index 000000000..015b2e662 --- /dev/null +++ b/src/op_mode/snmp_v3_showcerts.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +files=`sudo ls /etc/snmp/tls/certs/ 2> /dev/null`; +if [ -n "$files" ]; then + sudo /usr/bin/net-snmp-cert showcerts --subject --fingerprint +else + echo "You don't have any certificates. Put it in '/etc/snmp/tls/certs/' folder." +fi diff --git a/src/op_mode/system_integrity.py b/src/op_mode/system_integrity.py new file mode 100755 index 000000000..c0e3d1095 --- /dev/null +++ b/src/op_mode/system_integrity.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import os +import re +import itertools +from datetime import datetime, timedelta + +from vyos.util import cmd + +verf = r'/usr/libexec/vyos/op_mode/version.py' + +def get_sys_build_version(): + if not os.path.exists(verf): + return None + + a = cmd('/usr/libexec/vyos/op_mode/version.py') + if re.search('^Built on:.+',a, re.M) == None: + return None + + dt = ( re.sub('Built on: +','', re.search('^Built on:.+',a, re.M).group(0)) ) + return datetime.strptime(dt,'%a %d %b %Y %H:%M %Z') + +def check_pkgs(dt): + pkg_diffs = { + 'buildtime' : str(dt), + 'pkg' : {} + } + + pkg_info = os.listdir('/var/lib/dpkg/info/') + for file in pkg_info: + if re.search('\.list$', file): + fts = os.stat('/var/lib/dpkg/info/' + file).st_mtime + dt_str = (datetime.utcfromtimestamp(fts).strftime('%Y-%m-%d %H:%M:%S')) + fdt = datetime.strptime(dt_str, '%Y-%m-%d %H:%M:%S') + if fdt > dt: + pkg_diffs['pkg'].update( { str(re.sub('\.list','',file)) : str(fdt)}) + + if len(pkg_diffs['pkg']) != 0: + return pkg_diffs + else: + return None + +def main(): + dt = get_sys_build_version() + pkgs = check_pkgs(dt) + if pkgs != None: + print ("The following packages don\'t fit the image creation time\nbuild time:\t" + pkgs['buildtime']) + for k, v in pkgs['pkg'].items(): + print ("installed: " + v + '\t' + k) + +if __name__ == '__main__': + sys.exit( main() ) + diff --git a/src/op_mode/toggle_help_binding.sh b/src/op_mode/toggle_help_binding.sh new file mode 100755 index 000000000..a8708f3da --- /dev/null +++ b/src/op_mode/toggle_help_binding.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Script for [un-]binding the question mark key for getting help +if [ "$1" == 'disable' ]; then + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + echo "bind '\"?\": self-insert' # vyatta key binding" >> $HOME/.bashrc + bind '"?": self-insert' +else + sed -i "/^bind '\"?\": .* # vyatta key binding$/d" $HOME/.bashrc + bind '"?": possible-completions' +fi diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py new file mode 100755 index 000000000..2c1db20bf --- /dev/null +++ b/src/op_mode/vrrp.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys +import time +import argparse +import json +import tabulate + +import vyos.util + +from vyos.ifconfig.vrrp import VRRP +from vyos.ifconfig.vrrp import VRRPError, VRRPNoData + + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-s", "--summary", action="store_true", help="Print VRRP summary") +group.add_argument("-t", "--statistics", action="store_true", help="Print VRRP statistics") +group.add_argument("-d", "--data", action="store_true", help="Print detailed VRRP data") + +args = parser.parse_args() + +# Exit early if VRRP is dead or not configured +if not VRRP.is_running(): + print('VRRP is not running') + sys.exit(0) + +try: + if args.summary: + print(VRRP.format(VRRP.collect('json'))) + elif args.statistics: + print(VRRP.collect('stats')) + elif args.data: + print(VRRP.collect('state')) + else: + parser.print_help() + sys.exit(1) +except VRRPNoData as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py new file mode 100755 index 000000000..e08bc983a --- /dev/null +++ b/src/op_mode/wireguard.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import os +import sys +import shutil +import syslog as sl +import re + +from vyos.config import Config +from vyos.ifconfig import WireGuardIf +from vyos.util import cmd +from vyos.util import run +from vyos.util import check_kmod +from vyos import ConfigError + +dir = r'/config/auth/wireguard' +psk = dir + '/preshared.key' + +k_mod = 'wireguard' + +def generate_keypair(pk, pub): + """ generates a keypair which is stored in /config/auth/wireguard """ + old_umask = os.umask(0o027) + if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0: + raise ConfigError("wireguard key-pair generation failed") + else: + sl.syslog( + sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) + os.umask(old_umask) + + +def genkey(location): + """ helper function to check, regenerate the keypair """ + pk = "{}/private.key".format(location) + pub = "{}/public.key".format(location) + old_umask = os.umask(0o027) + if os.path.exists(pk) and os.path.exists(pub): + try: + choice = input( + "You already have a wireguard key-pair, do you want to re-generate? [y/n] ") + if choice == 'y' or choice == 'Y': + generate_keypair(pk, pub) + except KeyboardInterrupt: + sys.exit(0) + else: + """ if keypair is bing executed from a running iso """ + if not os.path.exists(location): + run(f'sudo mkdir -p {location}') + run(f'sudo chgrp vyattacfg {location}') + run(f'sudo chmod 750 {location}') + generate_keypair(pk, pub) + os.umask(old_umask) + + +def showkey(key): + """ helper function to show privkey or pubkey """ + if os.path.exists(key): + print (open(key).read().strip()) + else: + print ("{} not found".format(key)) + + +def genpsk(): + """ + generates a preshared key and shows it on stdout, + it's stored only in the cli config + """ + + psk = cmd('wg genpsk') + print(psk) + +def list_key_dirs(): + """ lists all dirs under /config/auth/wireguard """ + if os.path.exists(dir): + nks = next(os.walk(dir))[1] + for nk in nks: + print (nk) + +def del_key_dir(kname): + """ deletes /config/auth/wireguard/<kname> """ + kdir = "{0}/{1}".format(dir,kname) + if not os.path.isdir(kdir): + print ("named keypair {} not found".format(kname)) + return 1 + shutil.rmtree(kdir) + + +if __name__ == '__main__': + check_kmod(k_mod) + parser = argparse.ArgumentParser(description='wireguard key management') + parser.add_argument( + '--genkey', action="store_true", help='generate key-pair') + parser.add_argument( + '--showpub', action="store_true", help='shows public key') + parser.add_argument( + '--showpriv', action="store_true", help='shows private key') + parser.add_argument( + '--genpsk', action="store_true", help='generates preshared-key') + parser.add_argument( + '--location', action="store", help='key location within {}'.format(dir)) + parser.add_argument( + '--listkdir', action="store_true", help='lists named keydirectories') + parser.add_argument( + '--delkdir', action="store_true", help='removes named keydirectories') + parser.add_argument( + '--showinterface', action="store", help='shows interface details') + args = parser.parse_args() + + try: + if args.genkey: + if args.location: + genkey("{0}/{1}".format(dir, args.location)) + else: + genkey("{}/default".format(dir)) + if args.showpub: + if args.location: + showkey("{0}/{1}/public.key".format(dir, args.location)) + else: + showkey("{}/default/public.key".format(dir)) + if args.showpriv: + if args.location: + showkey("{0}/{1}/private.key".format(dir, args.location)) + else: + showkey("{}/default/private.key".format(dir)) + if args.genpsk: + genpsk() + if args.listkdir: + list_key_dirs() + if args.showinterface: + try: + intf = WireGuardIf(args.showinterface, create=False, debug=False) + print(intf.operational.show_interface()) + # the interface does not exists + except Exception: + pass + if args.delkdir: + if args.location: + del_key_dir(args.location) + else: + del_key_dir("default") + + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/pam-configs/radius b/src/pam-configs/radius new file mode 100644 index 000000000..0e2c71e38 --- /dev/null +++ b/src/pam-configs/radius @@ -0,0 +1,20 @@ +Name: RADIUS authentication +Default: yes +Priority: 257 +Auth-Type: Primary +Auth: + [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet + [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet + [authinfo_unavail=ignore success=end default=ignore] pam_radius_auth.so + +Account-Type: Primary +Account: + [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet + [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet + [authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_radius_auth.so + +Session-Type: Additional +Session: + [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet + [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet + [authinfo_unavail=ignore success=ok default=ignore] pam_radius_auth.so diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd new file mode 100755 index 000000000..0079f7e5c --- /dev/null +++ b/src/services/vyos-hostsd @@ -0,0 +1,618 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +######### +# USAGE # +######### +# This daemon listens on its socket for JSON messages. +# The received message format is: +# +# { 'type': '<message type>', +# 'op': '<message operation>', +# 'data': <data list or dict> +# } +# +# For supported message types, see below. +# 'op' can be 'add', delete', 'get', 'set' or 'apply'. +# Different message types support different sets of operations and different +# data formats. +# +# Changes to configuration made via add or delete don't take effect immediately, +# they are remembered in a state variable and saved to disk to a state file. +# State is remembered across daemon restarts but not across system reboots +# as it's saved in a temporary filesystem (/run). +# +# 'apply' is a special operation that applies the configuration from the cached +# state, rendering all config files and reloading relevant daemons (currently +# just pdns-recursor via rec-control). +# +# note: 'add' operation also acts as 'update' as it uses dict.update, if the +# 'data' dict item value is a dict. If it is a list, it uses list.append. +# +### tags +# Tags can be arbitrary, but they are generally in this format: +# 'static', 'system', 'dhcp(v6)-<intf>' or 'dhcp-server-<client ip>' +# They are used to distinguish entries created by different scripts so they can +# be removed and recreated without having to track what needs to be changed. +# They are also used as a way to control which tags settings (e.g. nameservers) +# get added to various config files via name_server_tags_(recursor|system) +# +### name_server_tags_(recursor|system) +# A list of tags whose nameservers and search domains is used to generate +# /etc/resolv.conf and pdns-recursor config. +# system list is used to generate resolv.conf. +# recursor list is used to generate pdns-rec forward-zones. +# When generating each file, the order of nameservers is as per the order of +# name_server_tags (the order in which tags were added), then the order in +# which the name servers for each tag were added. +# +#### Message types +# +### name_servers +# +# { 'type': 'name_servers', +# 'op': 'add', +# 'data': { +# '<str tag>': ['<str nameserver>', ...], +# ... +# } +# } +# +# { 'type': 'name_servers', +# 'op': 'delete', +# 'data': ['<str tag>', ...] +# } +# +# { 'type': 'name_servers', +# 'op': 'get', +# 'tag_regex': '<str regex>' +# } +# response: +# { 'data': { +# '<str tag>': ['<str nameserver>', ...], +# ... +# } +# } +# +### name_server_tags +# +# { 'type': 'name_server_tags', +# 'op': 'add', +# 'data': ['<str tag>', ...] +# } +# +# { 'type': 'name_server_tags', +# 'op': 'delete', +# 'data': ['<str tag>', ...] +# } +# +# { 'type': 'name_server_tags', +# 'op': 'get', +# } +# response: +# { 'data': ['<str tag>', ...] } +# +### forward_zones +## Additional zones added to pdns-recursor forward-zones-file. +## If recursion-desired is true, '+' will be prepended to the zone line. +## If addNTA is true, a NTA will be added via lua-config-file. +# +# { 'type': 'forward_zones', +# 'op': 'add', +# 'data': { +# '<str zone>': { +# 'nslist': ['<str nameserver>', ...], +# 'addNTA': <bool>, +# 'recursion-desired': <bool> +# } +# ... +# } +# } +# +# { 'type': 'forward_zones', +# 'op': 'delete', +# 'data': ['<str zone>', ...] +# } +# +# { 'type': 'forward_zones', +# 'op': 'get', +# } +# response: +# { 'data': { +# '<str zone>': { ... }, +# ... +# } +# } +# +# +### search_domains +# +# { 'type': 'search_domains', +# 'op': 'add', +# 'data': { +# '<str tag>': ['<str domain>', ...], +# ... +# } +# } +# +# { 'type': 'search_domains', +# 'op': 'delete', +# 'data': ['<str tag>', ...] +# } +# +# { 'type': 'search_domains', +# 'op': 'get', +# } +# response: +# { 'data': { +# '<str tag>': ['<str domain>', ...], +# ... +# } +# } +# +### hosts +# +# { 'type': 'hosts', +# 'op': 'add', +# 'data': { +# '<str tag>': { +# '<str host>': { +# 'address': '<str address>', +# 'aliases': ['<str alias>, ...] +# }, +# ... +# }, +# ... +# } +# } +# +# { 'type': 'hosts', +# 'op': 'delete', +# 'data': ['<str tag>', ...] +# } +# +# { 'type': 'hosts', +# 'op': 'get' +# 'tag_regex': '<str regex>' +# } +# response: +# { 'data': { +# '<str tag>': { +# '<str host>': { +# 'address': '<str address>', +# 'aliases': ['<str alias>, ...] +# }, +# ... +# }, +# ... +# } +# } +### host_name +# +# { 'type': 'host_name', +# 'op': 'set', +# 'data': { +# 'host_name': '<str hostname>' +# 'domain_name': '<str domainname>' +# } +# } + +import os +import sys +import time +import json +import signal +import traceback +import re +import logging +import zmq +from voluptuous import Schema, MultipleInvalid, Required, Any +from collections import OrderedDict +from vyos.util import popen, chown, chmod_755, makedir, process_named_running +from vyos.template import render + +debug = True + +# Configure logging +logger = logging.getLogger(__name__) +# set stream as output +logs_handler = logging.StreamHandler() +logger.addHandler(logs_handler) + +if debug: + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.INFO) + +RUN_DIR = "/run/vyos-hostsd" +STATE_FILE = os.path.join(RUN_DIR, "vyos-hostsd.state") +SOCKET_PATH = "ipc://" + os.path.join(RUN_DIR, 'vyos-hostsd.sock') + +RESOLV_CONF_FILE = '/etc/resolv.conf' +HOSTS_FILE = '/etc/hosts' + +PDNS_REC_USER = PDNS_REC_GROUP = 'pdns' +PDNS_REC_RUN_DIR = '/run/powerdns' +PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua' +PDNS_REC_ZONES_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf' + +STATE = { + "name_servers": {}, + "name_server_tags_recursor": [], + "name_server_tags_system": [], + "forward_zones": {}, + "hosts": {}, + "host_name": "vyos", + "domain_name": "", + "search_domains": {}, + "changes": 0 + } + +# the base schema that every received message must be in +base_schema = Schema({ + Required('op'): Any('add', 'delete', 'set', 'get', 'apply'), + 'type': Any('name_servers', + 'name_server_tags_recursor', 'name_server_tags_system', + 'forward_zones', 'search_domains', 'hosts', 'host_name'), + 'data': Any(list, dict), + 'tag': str, + 'tag_regex': str + }) + +# more specific schemas +op_schema = Schema({ + 'op': str, + }, required=True) + +op_type_schema = op_schema.extend({ + 'type': str, + }, required=True) + +host_name_add_schema = op_type_schema.extend({ + 'data': { + 'host_name': str, + 'domain_name': Any(str, None) + } + }, required=True) + +data_dict_list_schema = op_type_schema.extend({ + 'data': { + str: [str] + } + }, required=True) + +data_list_schema = op_type_schema.extend({ + 'data': [str] + }, required=True) + +tag_regex_schema = op_type_schema.extend({ + 'tag_regex': str + }, required=True) + +forward_zone_add_schema = op_type_schema.extend({ + 'data': { + str: { + 'nslist': [str], + 'addNTA': bool, + 'recursion-desired': bool + } + } + }, required=True) + +hosts_add_schema = op_type_schema.extend({ + 'data': { + str: { + str: { + 'address': str, + 'aliases': [str] + } + } + } + }, required=True) + + +# op and type to schema mapping +msg_schema_map = { + 'name_servers': { + 'add': data_dict_list_schema, + 'delete': data_list_schema, + 'get': tag_regex_schema + }, + 'name_server_tags_recursor': { + 'add': data_list_schema, + 'delete': data_list_schema, + 'get': op_type_schema + }, + 'name_server_tags_system': { + 'add': data_list_schema, + 'delete': data_list_schema, + 'get': op_type_schema + }, + 'forward_zones': { + 'add': forward_zone_add_schema, + 'delete': data_list_schema, + 'get': op_type_schema + }, + 'search_domains': { + 'add': data_dict_list_schema, + 'delete': data_list_schema, + 'get': tag_regex_schema + }, + 'hosts': { + 'add': hosts_add_schema, + 'delete': data_list_schema, + 'get': tag_regex_schema + }, + 'host_name': { + 'set': host_name_add_schema + }, + None: { + 'apply': op_schema + } + } + +def validate_schema(data): + base_schema(data) + + try: + schema = msg_schema_map[data['type'] if 'type' in data else None][data['op']] + schema(data) + except KeyError: + raise ValueError(( + 'Invalid or unknown combination: ' + f'op: "{data["op"]}", type: "{data["type"]}"')) + + +def pdns_rec_control(command): + # pdns-r process name is NOT equal to the name shown in ps + if not process_named_running('pdns-r/worker'): + logger.info(f'pdns_recursor not running, not sending "{command}"') + return + + logger.info(f'Running "rec_control {command}"') + (ret,ret_code) = popen(( + f"rec_control --socket-dir={PDNS_REC_RUN_DIR} {command}")) + if ret_code > 0: + logger.exception(( + f'"rec_control {command}" failed with exit status {ret_code}, ' + f'output: "{ret}"')) + +def make_resolv_conf(state): + logger.info(f"Writing {RESOLV_CONF_FILE}") + render(RESOLV_CONF_FILE, 'vyos-hostsd/resolv.conf.tmpl', state, + user='root', group='root') + +def make_hosts(state): + logger.info(f"Writing {HOSTS_FILE}") + render(HOSTS_FILE, 'vyos-hostsd/hosts.tmpl', state, + user='root', group='root') + +def make_pdns_rec_conf(state): + logger.info(f"Writing {PDNS_REC_LUA_CONF_FILE}") + + # on boot, /run/powerdns does not exist, so create it + makedir(PDNS_REC_RUN_DIR, user=PDNS_REC_USER, group=PDNS_REC_GROUP) + chmod_755(PDNS_REC_RUN_DIR) + + render(PDNS_REC_LUA_CONF_FILE, + 'dns-forwarding/recursor.vyos-hostsd.conf.lua.tmpl', + state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) + + logger.info(f"Writing {PDNS_REC_ZONES_FILE}") + render(PDNS_REC_ZONES_FILE, + 'dns-forwarding/recursor.forward-zones.conf.tmpl', + state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) + +def set_host_name(state, data): + if data['host_name']: + state['host_name'] = data['host_name'] + if 'domain_name' in data: + state['domain_name'] = data['domain_name'] + +def add_items_to_dict(_dict, items): + """ + Dedupes and preserves sort order. + """ + assert isinstance(_dict, dict) + assert isinstance(items, dict) + + if not items: + return + + _dict.update(items) + +def add_items_to_dict_as_keys(_dict, items): + """ + Added item values are converted to OrderedDict with the value as keys + and null values. This is to emulate a list but with inherent deduplication. + Dedupes and preserves sort order. + """ + assert isinstance(_dict, dict) + assert isinstance(items, dict) + + if not items: + return + + for item, item_val in items.items(): + if item not in _dict: + _dict[item] = OrderedDict({}) + _dict[item].update(OrderedDict.fromkeys(item_val)) + +def add_items_to_list(_list, items): + """ + Dedupes and preserves sort order. + """ + assert isinstance(_list, list) + assert isinstance(items, list) + + if not items: + return + + for item in items: + if item not in _list: + _list.append(item) + +def delete_items_from_dict(_dict, items): + """ + items is a list of keys to delete. + Doesn't error if the key doesn't exist. + """ + assert isinstance(_dict, dict) + assert isinstance(items, list) + + for item in items: + if item in _dict: + del _dict[item] + +def delete_items_from_list(_list, items): + """ + items is a list of items to remove. + Doesn't error if the key doesn't exist. + """ + assert isinstance(_list, list) + assert isinstance(items, list) + + for item in items: + if item in _list: + _list.remove(item) + +def get_items_from_dict_regex(_dict, item_regex_string): + """ + Returns the items whose keys match item_regex_string. + """ + assert isinstance(_dict, dict) + assert isinstance(item_regex_string, str) + + tmp = {} + regex = re.compile(item_regex_string) + for item in _dict: + if regex.match(item): + tmp[item] = _dict[item] + return tmp + +def get_option(msg, key): + if key in msg: + return msg[key] + else: + raise ValueError("Missing required option \"{0}\"".format(key)) + +def handle_message(msg): + result = None + op = get_option(msg, 'op') + + if op in ['add', 'delete', 'set']: + STATE['changes'] += 1 + + if op == 'delete': + _type = get_option(msg, 'type') + data = get_option(msg, 'data') + if _type in ['name_servers', 'forward_zones', 'search_domains', 'hosts']: + delete_items_from_dict(STATE[_type], data) + elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: + delete_items_from_list(STATE[_type], data) + else: + raise ValueError(f'Operation "{op}" unknown data type "{_type}"') + elif op == 'add': + _type = get_option(msg, 'type') + data = get_option(msg, 'data') + if _type in ['name_servers', 'search_domains']: + add_items_to_dict_as_keys(STATE[_type], data) + elif _type in ['forward_zones', 'hosts']: + add_items_to_dict(STATE[_type], data) + # maybe we need to rec_control clear-nta each domain that was removed here? + elif _type in ['name_server_tags_recursor', 'name_server_tags_system']: + add_items_to_list(STATE[_type], data) + else: + raise ValueError(f'Operation "{op}" unknown data type "{_type}"') + elif op == 'set': + _type = get_option(msg, 'type') + data = get_option(msg, 'data') + if _type == 'host_name': + set_host_name(STATE, data) + else: + raise ValueError(f'Operation "{op}" unknown data type "{_type}"') + elif op == 'get': + _type = get_option(msg, 'type') + if _type in ['name_servers', 'search_domains', 'hosts']: + tag_regex = get_option(msg, 'tag_regex') + result = get_items_from_dict_regex(STATE[_type], tag_regex) + elif _type in ['name_server_tags_recursor', 'name_server_tags_system', 'forward_zones']: + result = STATE[_type] + else: + raise ValueError(f'Operation "{op}" unknown data type "{_type}"') + elif op == 'apply': + logger.info(f"Applying {STATE['changes']} changes") + make_resolv_conf(STATE) + make_hosts(STATE) + make_pdns_rec_conf(STATE) + pdns_rec_control('reload-lua-config') + pdns_rec_control('reload-zones') + logger.info("Success") + result = {'message': f'Applied {STATE["changes"]} changes'} + STATE['changes'] = 0 + + else: + raise ValueError(f"Unknown operation {op}") + + logger.debug(f"Saving state to {STATE_FILE}") + with open(STATE_FILE, 'w') as f: + json.dump(STATE, f) + + return result + +if __name__ == '__main__': + # Create a directory for state checkpoints + os.makedirs(RUN_DIR, exist_ok=True) + if os.path.exists(STATE_FILE): + with open(STATE_FILE, 'r') as f: + try: + STATE = json.load(f) + except: + logger.exception(traceback.format_exc()) + logger.exception("Failed to load the state file, using default") + + context = zmq.Context() + socket = context.socket(zmq.REP) + + # Set the right permissions on the socket, then change it back + o_mask = os.umask(0o007) + socket.bind(SOCKET_PATH) + os.umask(o_mask) + + while True: + # Wait for next request from client + msg_json = socket.recv().decode() + logger.debug(f"Request data: {msg_json}") + + try: + msg = json.loads(msg_json) + validate_schema(msg) + + resp = {} + resp['data'] = handle_message(msg) + except ValueError as e: + resp['error'] = str(e) + except MultipleInvalid as e: + # raised by schema + resp['error'] = f'Invalid message: {str(e)}' + logger.exception(resp['error']) + except: + logger.exception(traceback.format_exc()) + resp['error'] = "Internal error" + + # Send reply back to client + socket.send(json.dumps(resp).encode()) + logger.debug(f"Sent response: {resp}") diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server new file mode 100755 index 000000000..d5730d86c --- /dev/null +++ b/src/services/vyos-http-api-server @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import sys +import grp +import json +import traceback +import threading +import signal + +import vyos.config + +from flask import Flask, request +from waitress import serve + +from functools import wraps + +from vyos.configsession import ConfigSession, ConfigSessionError + + +DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' +CFG_GROUP = 'vyattacfg' + +app = Flask(__name__) + +# Giant lock! +lock = threading.Lock() + +def load_server_config(): + with open(DEFAULT_CONFIG_FILE) as f: + config = json.load(f) + return config + +def check_auth(key_list, key): + id = None + for k in key_list: + if k['key'] == key: + id = k['id'] + return id + +def error(code, msg): + resp = {"success": False, "error": msg, "data": None} + return json.dumps(resp), code + +def success(data): + resp = {"success": True, "data": data, "error": None} + return json.dumps(resp) + +def get_command(f): + @wraps(f) + def decorated_function(*args, **kwargs): + cmd = request.form.get("data") + if not cmd: + return error(400, "Non-empty data field is required") + try: + cmd = json.loads(cmd) + except Exception as e: + return error(400, "Failed to parse JSON: {0}".format(e)) + return f(cmd, *args, **kwargs) + + return decorated_function + +def auth_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + key = request.form.get("key") + api_keys = app.config['vyos_keys'] + id = check_auth(api_keys, key) + if not id: + return error(401, "Valid API key is required") + return f(*args, **kwargs) + + return decorated_function + +@app.route('/configure', methods=['POST']) +@get_command +@auth_required +def configure_op(commands): + session = app.config['vyos_session'] + env = session.get_session_env() + config = vyos.config.Config(session_env=env) + + strict_field = request.form.get("strict") + if strict_field == "true": + strict = True + else: + strict = False + + # Allow users to pass just one command + if not isinstance(commands, list): + commands = [commands] + + # We don't want multiple people/apps to be able to commit at once, + # or modify the shared session while someone else is doing the same, + # so the lock is really global + lock.acquire() + + status = 200 + error_msg = None + try: + for c in commands: + # What we've got may not even be a dict + if not isinstance(c, dict): + raise ConfigSessionError("Malformed command \"{0}\": any command must be a dict".format(json.dumps(c))) + + # Missing op or path is a show stopper + if not ('op' in c): + raise ConfigSessionError("Malformed command \"{0}\": missing \"op\" field".format(json.dumps(c))) + if not ('path' in c): + raise ConfigSessionError("Malformed command \"{0}\": missing \"path\" field".format(json.dumps(c))) + + # Missing value is fine, substitute for empty string + if 'value' in c: + value = c['value'] + else: + value = "" + + op = c['op'] + path = c['path'] + + if not path: + raise ConfigSessionError("Malformed command \"{0}\": empty path".format(json.dumps(c))) + + # Type checking + if not isinstance(path, list): + raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list".format(json.dumps(c))) + + if not isinstance(value, str): + raise ConfigSessionError("Malformed command \"{0}\": \"value\" field must be a string".format(json.dumps(c))) + + # Account for the case when value field is present and set to null + if not value: + value = "" + + # For vyos.configsessios calls that have no separate value arguments, + # and for type checking too + try: + cfg_path = " ".join(path + [value]).strip() + except TypeError: + raise ConfigSessionError("Malformed command \"{0}\": \"path\" field must be a list of strings".format(json.dumps(c))) + + if op == 'set': + # XXX: it would be nice to do a strict check for "path already exists", + # but there's probably no way to do that + session.set(path, value=value) + elif op == 'delete': + if strict and not config.exists(cfg_path): + raise ConfigSessionError("Cannot delete [{0}]: path/value does not exist".format(cfg_path)) + session.delete(path, value=value) + elif op == 'comment': + session.comment(path, value=value) + else: + raise ConfigSessionError("\"{0}\" is not a valid operation".format(op)) + # end for + session.commit() + print("Configuration modified via HTTP API using key \"{0}\"".format(id)) + except ConfigSessionError as e: + session.discard() + status = 400 + if app.config['vyos_debug']: + print(traceback.format_exc(), file=sys.stderr) + error_msg = str(e) + except Exception as e: + session.discard() + print(traceback.format_exc(), file=sys.stderr) + status = 500 + + # Don't give the details away to the outer world + error_msg = "An internal error occured. Check the logs for details." + finally: + lock.release() + + if status != 200: + return error(status, error_msg) + else: + return success(None) + +@app.route('/retrieve', methods=['POST']) +@get_command +@auth_required +def retrieve_op(command): + session = app.config['vyos_session'] + env = session.get_session_env() + config = vyos.config.Config(session_env=env) + + try: + op = command['op'] + path = " ".join(command['path']) + except KeyError: + return error(400, "Missing required field. \"op\" and \"path\" fields are required") + + try: + if op == 'returnValue': + res = config.return_value(path) + elif op == 'returnValues': + res = config.return_values(path) + elif op == 'exists': + res = config.exists(path) + elif op == 'showConfig': + config_format = 'json' + if 'configFormat' in command: + config_format = command['configFormat'] + + res = session.show_config(path=command['path']) + if config_format == 'json': + config_tree = vyos.configtree.ConfigTree(res) + res = json.loads(config_tree.to_json()) + elif config_format == 'json_ast': + config_tree = vyos.configtree.ConfigTree(res) + res = json.loads(config_tree.to_json_ast()) + elif config_format == 'raw': + pass + else: + return error(400, "\"{0}\" is not a valid config format".format(config_format)) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + print(traceback.format_exc(), file=sys.stderr) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + +@app.route('/config-file', methods=['POST']) +@get_command +@auth_required +def config_file_op(command): + session = app.config['vyos_session'] + + try: + op = command['op'] + except KeyError: + return error(400, "Missing required field \"op\"") + + try: + if op == 'save': + try: + path = command['file'] + except KeyError: + path = '/config/config.boot' + res = session.save_config(path) + elif op == 'load': + try: + path = command['file'] + except KeyError: + return error(400, "Missing required field \"file\"") + res = session.load_config(path) + res = session.commit() + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + print(traceback.format_exc(), file=sys.stderr) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + +@app.route('/image', methods=['POST']) +@get_command +@auth_required +def image_op(command): + session = app.config['vyos_session'] + + try: + op = command['op'] + except KeyError: + return error(400, "Missing required field \"op\"") + + try: + if op == 'add': + try: + url = command['url'] + except KeyError: + return error(400, "Missing required field \"url\"") + res = session.install_image(url) + elif op == 'delete': + try: + name = command['name'] + except KeyError: + return error(400, "Missing required field \"name\"") + res = session.remove_image(name) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + print(traceback.format_exc(), file=sys.stderr) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + + +@app.route('/generate', methods=['POST']) +@get_command +@auth_required +def generate_op(command): + session = app.config['vyos_session'] + + try: + op = command['op'] + path = command['path'] + except KeyError: + return error(400, "Missing required field. \"op\" and \"path\" fields are required") + + if not isinstance(path, list): + return error(400, "Malformed command: \"path\" field must be a list of strings") + + try: + if op == 'generate': + res = session.generate(path) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + print(traceback.format_exc(), file=sys.stderr) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + +@app.route('/show', methods=['POST']) +@get_command +@auth_required +def show_op(command): + session = app.config['vyos_session'] + + try: + op = command['op'] + path = command['path'] + except KeyError: + return error(400, "Missing required field. \"op\" and \"path\" fields are required") + + if not isinstance(path, list): + return error(400, "Malformed command: \"path\" field must be a list of strings") + + try: + if op == 'show': + res = session.show(path) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + print(traceback.format_exc(), file=sys.stderr) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + +def shutdown(): + raise KeyboardInterrupt + +if __name__ == '__main__': + # systemd's user and group options don't work, do it by hand here, + # else no one else will be able to commit + cfg_group = grp.getgrnam(CFG_GROUP) + os.setgid(cfg_group.gr_gid) + + # Need to set file permissions to 775 too so that every vyattacfg group member + # has write access to the running config + os.umask(0o002) + + try: + server_config = load_server_config() + except Exception as e: + print("Failed to load the HTTP API server config: {0}".format(e)) + + session = ConfigSession(os.getpid()) + + app.config['vyos_session'] = session + app.config['vyos_keys'] = server_config['api_keys'] + app.config['vyos_debug'] = server_config['debug'] + + def sig_handler(signum, frame): + shutdown() + + signal.signal(signal.SIGTERM, sig_handler) + + try: + serve(app, host=server_config["listen_address"], + port=server_config["port"]) + except OSError as e: + print(f"OSError {e}") diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py new file mode 100755 index 000000000..7e2076820 --- /dev/null +++ b/src/system/keepalived-fifo.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import os +import time +import signal +import argparse +import threading +import re +import json +from pathlib import Path +from queue import Queue +import logging +from logging.handlers import SysLogHandler + +from vyos.util import cmd + +# configure logging +logger = logging.getLogger(__name__) +logs_format = logging.Formatter('%(filename)s: %(message)s') +logs_handler_syslog = SysLogHandler('/dev/log') +logs_handler_syslog.setFormatter(logs_format) +logger.addHandler(logs_handler_syslog) +logger.setLevel(logging.DEBUG) + + +# class for all operations +class KeepalivedFifo: + # init - read command arguments + def __init__(self): + logger.info("Starting FIFO pipe for Keepalived") + # define program arguments + cmd_args_parser = argparse.ArgumentParser(description='Create FIFO pipe for keepalived and process notify events', add_help=False) + cmd_args_parser.add_argument('PIPE', help='path to the FIFO pipe') + # parse arguments + cmd_args = cmd_args_parser.parse_args() + self._config_load() + self.pipe_path = cmd_args.PIPE + + # create queue for messages and events for syncronization + self.message_queue = Queue(maxsize=100) + self.stopme = threading.Event() + self.message_event = threading.Event() + + # load configuration + def _config_load(self): + try: + # read the dictionary file with configuration + with open('/run/keepalived_config.dict', 'r') as dict_file: + vrrp_config_dict = json.load(dict_file) + self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}} + # save VRRP instances to the new dictionary + for vrrp_group in vrrp_config_dict['vrrp_groups']: + self.vrrp_config['vrrp_groups'][vrrp_group['name']] = { + 'STOP': vrrp_group.get('stop_script'), + 'FAULT': vrrp_group.get('fault_script'), + 'BACKUP': vrrp_group.get('backup_script'), + 'MASTER': vrrp_group.get('master_script') + } + # save VRRP sync groups to the new dictionary + for sync_group in vrrp_config_dict['sync_groups']: + self.vrrp_config['sync_groups'][sync_group['name']] = { + 'STOP': sync_group.get('stop_script'), + 'FAULT': sync_group.get('fault_script'), + 'BACKUP': sync_group.get('backup_script'), + 'MASTER': sync_group.get('master_script') + } + logger.debug("Loaded configuration: {}".format(self.vrrp_config)) + except Exception as err: + logger.error("Unable to load configuration: {}".format(err)) + + # run command + def _run_command(self, command): + logger.debug("Running the command: {}".format(command)) + try: + cmd(command) + except OSError as err: + logger.error(f'Unable to execute command "{command}": {err}') + + # create FIFO pipe + def pipe_create(self): + if Path(self.pipe_path).exists(): + logger.info("PIPE already exist: {}".format(self.pipe_path)) + else: + os.mkfifo(self.pipe_path) + + # process message from pipe + def pipe_process(self): + logger.debug("Message processing start") + regex_notify = re.compile(r'^(?P<type>\w+) "(?P<name>[\w-]+)" (?P<state>\w+) (?P<priority>\d+)$', re.MULTILINE) + while self.stopme.is_set() is False: + # wait for a new message event from pipe_wait + self.message_event.wait() + try: + # clear mesage event flag + self.message_event.clear() + # get all messages from queue and try to process them + while self.message_queue.empty() is not True: + message = self.message_queue.get() + logger.debug("Received message: {}".format(message)) + notify_message = regex_notify.search(message) + # try to process a message if it looks valid + if notify_message: + n_type = notify_message.group('type') + n_name = notify_message.group('name') + n_state = notify_message.group('state') + logger.info("{} {} changed state to {}".format(n_type, n_name, n_state)) + # check and run commands for VRRP instances + if n_type == 'INSTANCE': + if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]: + n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state) + if n_script: + self._run_command(n_script) + # check and run commands for VRRP sync groups + # currently, this is not available in VyOS CLI + if n_type == 'GROUP': + if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]: + n_script = self.vrrp_config['sync_groups'][n_name].get(n_state) + if n_script: + self._run_command(n_script) + # mark task in queue as done + self.message_queue.task_done() + except Exception as err: + logger.error("Error processing message: {}".format(err)) + logger.debug("Terminating messages processing thread") + + # wait for messages + def pipe_wait(self): + logger.debug("Message reading start") + self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK) + while self.stopme.is_set() is False: + # sleep a bit to not produce 100% CPU load + time.sleep(0.1) + try: + # try to read a message from PIPE + message = os.read(self.pipe_read, 500) + if message: + # split PIPE content by lines and put them into queue + for line in message.decode().strip().splitlines(): + self.message_queue.put(line) + # set new message flag to start processing + self.message_event.set() + except Exception as err: + # ignore the "Resource temporarily unavailable" error + if err.errno != 11: + logger.error("Error receiving message: {}".format(err)) + + logger.debug("Closing FIFO pipe") + os.close(self.pipe_read) + + +# handle SIGTERM signal to allow finish all messages processing +def sigterm_handle(signum, frame): + logger.info("Ending processing: Received SIGTERM signal") + fifo.stopme.set() + thread_wait_message.join() + fifo.message_event.set() + thread_process_message.join() + + +signal.signal(signal.SIGTERM, sigterm_handle) + +# init our class +fifo = KeepalivedFifo() +# try to create PIPE if it is not exist yet +# It looks like keepalived do it before the script will be running, but if we +# will decide to run this not from keepalived config, then we may get in +# trouble. So it is betteer to leave this here. +fifo.pipe_create() +# create and run dedicated threads for reading and processing messages +thread_wait_message = threading.Thread(target=fifo.pipe_wait) +thread_process_message = threading.Thread(target=fifo.pipe_process) +thread_wait_message.start() +thread_process_message.start() diff --git a/src/system/normalize-ip b/src/system/normalize-ip new file mode 100755 index 000000000..08f922a8e --- /dev/null +++ b/src/system/normalize-ip @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +# Normalizes IPv6 addresses so that they can be passed to iproute2, +# since iproute2 will not take an address with leading zeroes for an argument + +import re +import sys +import ipaddress + + +if __name__ == '__main__': + if len(sys.argv) < 2: + print("Argument required") + sys.exit(1) + + address_string, prefix_length = re.match(r'(.+)/(.+)', sys.argv[1]).groups() + + try: + address = ipaddress.IPv6Address(address_string) + normalized_address = address.compressed + except ipaddress.AddressValueError: + # It's likely an IPv4 address, do nothing + normalized_address = address_string + + print("{0}/{1}".format(normalized_address, prefix_length)) + sys.exit(0) + diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh new file mode 100755 index 000000000..a062dc810 --- /dev/null +++ b/src/system/on-dhcp-event.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# This script came from ubnt.com forum user "bradd" in the following post +# http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311 +# It has been modified by Ubiquiti to update the /etc/host file +# instead of adding to the CLI. +# Thanks to forum user "itsmarcos" for bug fix & improvements +# Thanks to forum user "ruudboon" for multiple domain fix +# Thanks to forum user "chibby85" for expire patch and static-mapping + +if [ $# -lt 5 ]; then + echo Invalid args + logger -s -t on-dhcp-event "Invalid args \"$@\"" + exit 1 +fi + +action=$1 +client_name=$2 +client_ip=$3 +client_mac=$4 +domain=$5 +hostsd_client="/usr/bin/vyos-hostsd-client" + +if [ -z "$client_name" ]; then + logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" + client_name=$(echo "client-"$client_mac | tr : -) +fi + +if [ "$domain" == "..YYZ!" ]; then + client_fqdn_name=$client_name + client_search_expr=$client_name +else + client_fqdn_name=$client_name.$domain + client_search_expr="$client_name\\.$domain" +fi + +case "$action" in + commit) # add mapping for new lease + $hostsd_client --add-hosts "$client_fqdn_name,$client_ip" --tag "dhcp-server-$client_ip" --apply + exit 0 + ;; + + release) # delete mapping for released address + $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply + exit 0 + ;; + + *) + logger -s -t on-dhcp-event "Invalid command \"$1\"" + exit 1 + ;; +esac + +exit 0 diff --git a/src/system/post-upgrade b/src/system/post-upgrade new file mode 100755 index 000000000..41b7c01ba --- /dev/null +++ b/src/system/post-upgrade @@ -0,0 +1,3 @@ +#!/bin/sh + +chown -R root:vyattacfg /config diff --git a/src/system/unpriv-ip b/src/system/unpriv-ip new file mode 100755 index 000000000..1ea0d626a --- /dev/null +++ b/src/system/unpriv-ip @@ -0,0 +1,2 @@ +#!/bin/sh +sudo /sbin/ip $* diff --git a/src/systemd/accel-ppp@.service b/src/systemd/accel-ppp@.service new file mode 100644 index 000000000..256112769 --- /dev/null +++ b/src/systemd/accel-ppp@.service @@ -0,0 +1,16 @@ +[Unit] +Description=Accel-PPP - High performance VPN server application for Linux +RequiresMountsFor=/run +ConditionPathExists=/run/accel-pppd/%i.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/accel-pppd +ExecStart=/usr/sbin/accel-pppd -d -p /run/accel-pppd/%i.pid -c /run/accel-pppd/%i.conf +ExecReload=/bin/kill -SIGUSR1 $MAINPID +PIDFile=/run/accel-pppd/%i.pid +Type=forking +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/ddclient.service b/src/systemd/ddclient.service new file mode 100644 index 000000000..a4d55827a --- /dev/null +++ b/src/systemd/ddclient.service @@ -0,0 +1,14 @@ +[Unit] +Description=Dynamic DNS Update Client +RequiresMountsFor=/run +ConditionPathExists=/run/ddclient/ddclient.conf +After=vyos-router.service + +[Service] +WorkingDirectory=/run/ddclient +Type=forking +PIDFile=/run/ddclient/ddclient.pid +ExecStart=/usr/sbin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service new file mode 100644 index 000000000..2ced1038a --- /dev/null +++ b/src/systemd/dhclient@.service @@ -0,0 +1,18 @@ +[Unit] +Description=DHCP client on %i +Documentation=man:dhclient(8) +ConditionPathExists=/var/lib/dhcp/dhclient_%i.conf +ConditionPathExists=/var/lib/dhcp/dhclient_%i.options +After=vyos-router.service + +[Service] +WorkingDirectory=/var/lib/dhcp +Type=exec +EnvironmentFile=-/var/lib/dhcp/dhclient_%i.options +PIDFile=/var/lib/dhcp/dhclient_%i.pid +ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS +ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service new file mode 100644 index 000000000..9a97ee261 --- /dev/null +++ b/src/systemd/dhcp6c@.service @@ -0,0 +1,17 @@ +[Unit] +Description=WIDE DHCPv6 client on %i +Documentation=man:dhcp6c(8) man:dhcp6c.conf(5) +ConditionPathExists=/run/dhcp6c/dhcp6c.%i.conf +After=vyos-router.service +StartLimitIntervalSec=0 + +[Service] +WorkingDirectory=/run/dhcp6c +Type=forking +PIDFile=/run/dhcp6c/dhcp6c.%i.pid +ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i +Restart=on-failure +RestartSec=20 + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/dropbear@.service b/src/systemd/dropbear@.service new file mode 100644 index 000000000..606a7ea6d --- /dev/null +++ b/src/systemd/dropbear@.service @@ -0,0 +1,14 @@ +[Unit] +Description=Dropbear SSH per-connection server +Requires=dropbearkey.service +Wants=conserver-server.service +ConditionPathExists=/run/conserver/conserver.cf +After=dropbearkey.service vyos-router.service conserver-server.service + +[Service] +Type=forking +ExecStartPre=/usr/bin/bash -c '/usr/bin/systemctl set-environment PORT=$(cli-shell-api returnActiveValue service console-server device "%I" ssh port)' +ExecStart=-/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -c "/usr/bin/console %I" -P /run/conserver/dropbear.%I.pid -p ${PORT} +PIDFile=/run/conserver/dropbear.%I.pid +KillMode=process +Restart=on-failure diff --git a/src/systemd/dropbearkey.service b/src/systemd/dropbearkey.service new file mode 100644 index 000000000..770641c8b --- /dev/null +++ b/src/systemd/dropbearkey.service @@ -0,0 +1,11 @@ +[Unit] +Description=Dropbear SSH Key Generation +ConditionPathExists=|!/etc/dropbear/dropbear_rsa_host_key + +[Service] +ExecStart=/usr/bin/dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target + diff --git a/src/systemd/isc-dhcp-relay.service b/src/systemd/isc-dhcp-relay.service new file mode 100644 index 000000000..56bcec840 --- /dev/null +++ b/src/systemd/isc-dhcp-relay.service @@ -0,0 +1,20 @@ +[Unit] +Description=ISC DHCP IPv4 relay +Documentation=man:dhcrelay(8) +Wants=network-online.target +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-relay/dhcp.conf +After=vyos-router.service + +[Service] +Type=forking +WorkingDirectory=/run/dhcp-relay +RuntimeDirectory=dhcp-relay +RuntimeDirectoryPreserve=yes +EnvironmentFile=/run/dhcp-relay/dhcp.conf +PIDFile=/run/dhcp-relay/dhcrelay.pid +ExecStart=/usr/sbin/dhcrelay -4 -pf /run/dhcp-relay/dhcrelay.pid $OPTIONS +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-relay6.service b/src/systemd/isc-dhcp-relay6.service new file mode 100644 index 000000000..85ff16e41 --- /dev/null +++ b/src/systemd/isc-dhcp-relay6.service @@ -0,0 +1,20 @@ +[Unit] +Description=ISC DHCP IPv6 relay +Documentation=man:dhcrelay(8) +Wants=network-online.target +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-relay/dhcpv6.conf +After=vyos-router.service + +[Service] +Type=forking +WorkingDirectory=/run/dhcp-relay +RuntimeDirectory=dhcp-relay +RuntimeDirectoryPreserve=yes +EnvironmentFile=/run/dhcp-relay/dhcpv6.conf +PIDFile=/run/dhcp-relay/dhcrelayv6.pid +ExecStart=/usr/sbin/dhcrelay -6 -pf /run/dhcp-relay/dhcrelayv6.pid $OPTIONS +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service new file mode 100644 index 000000000..9aa70a7cc --- /dev/null +++ b/src/systemd/isc-dhcp-server.service @@ -0,0 +1,24 @@ +[Unit] +Description=ISC DHCP IPv4 server +Documentation=man:dhcpd(8) +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-server/dhcpd.conf +After=vyos-router.service + +[Service] +Type=forking +WorkingDirectory=/run/dhcp-server +RuntimeDirectory=dhcp-server +RuntimeDirectoryPreserve=yes +Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE=/config/dhcpd.leases +PIDFile=/run/dhcp-server/dhcpd.pid +ExecStartPre=/bin/sh -ec '\ +touch ${LEASE_FILE}; \ +chown dhcpd:nogroup ${LEASE_FILE}* ; \ +chmod 664 ${LEASE_FILE}* ; \ +/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' +ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service new file mode 100644 index 000000000..1345c5fc5 --- /dev/null +++ b/src/systemd/isc-dhcp-server6.service @@ -0,0 +1,24 @@ +[Unit] +Description=ISC DHCP IPv6 server +Documentation=man:dhcpd(8) +RequiresMountsFor=/run +ConditionPathExists=/run/dhcp-server/dhcpdv6.conf +After=vyos-router.service + +[Service] +Type=forking +WorkingDirectory=/run/dhcp-server +RuntimeDirectory=dhcp-server +RuntimeDirectoryPreserve=yes +Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases +PIDFile=/run/dhcp-server/dhcpdv6.pid +ExecStartPre=/bin/sh -ec '\ +touch ${LEASE_FILE}; \ +chown nobody:nogroup ${LEASE_FILE}* ; \ +chmod 664 ${LEASE_FILE}* ; \ +/usr/sbin/dhcpd -6 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' +ExecStart=/usr/sbin/dhcpd -6 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/lcdproc.service b/src/systemd/lcdproc.service new file mode 100644 index 000000000..ef717667a --- /dev/null +++ b/src/systemd/lcdproc.service @@ -0,0 +1,13 @@ +[Unit] +Description=LCDproc system status information viewer on %I +Documentation=man:lcdproc(8) http://www.lcdproc.org/ +After=vyos-router.service LCDd.service +Requires=LCDd.service + +[Service] +User=root +ExecStart=/usr/bin/lcdproc -f -c /run/lcdproc/lcdproc.conf +PIDFile=/run/lcdproc/lcdproc.pid + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/ppp@.service b/src/systemd/ppp@.service new file mode 100644 index 000000000..bb4622034 --- /dev/null +++ b/src/systemd/ppp@.service @@ -0,0 +1,11 @@ +[Unit] +Description=Dialing PPP connection %I +After=vyos-router.service + +[Service] +ExecStart=/usr/sbin/pppd call %I nodetach nolog +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/tftpd@.service b/src/systemd/tftpd@.service new file mode 100644 index 000000000..266bc0962 --- /dev/null +++ b/src/systemd/tftpd@.service @@ -0,0 +1,14 @@ +[Unit] +Description=TFTP server +After=vyos-router.service +RequiresMountsFor=/run + +[Service] +Type=forking +#NotifyAccess=main +EnvironmentFile=-/etc/default/tftpd%I +ExecStart=/usr/sbin/in.tftpd "$DAEMON_ARGS" +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/vyos-beep.service b/src/systemd/vyos-beep.service new file mode 100644 index 000000000..78baa544c --- /dev/null +++ b/src/systemd/vyos-beep.service @@ -0,0 +1,11 @@ +[Unit] +Description=Beep after system start +DefaultDependencies=no +After=vyos.target + +[Service] +Type=oneshot +ExecStart=/usr/bin/beep -f 130 -l 100 -n -f 262 -l 100 -n -f 330 -l 100 -n -f 392 -l 100 -n -f 523 -l 100 -n -f 660 -l 100 -n -f 784 -l 300 -n -f 660 -l 300 + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/vyos-hostsd.service b/src/systemd/vyos-hostsd.service new file mode 100644 index 000000000..b77335778 --- /dev/null +++ b/src/systemd/vyos-hostsd.service @@ -0,0 +1,34 @@ +[Unit] +Description=VyOS DNS configuration keeper + +# Without this option, lots of default dependencies are added, +# among them network.target, which creates a dependency cycle +DefaultDependencies=no + +# Seemingly sensible way to say "as early as the system is ready" +# All vyos-hostsd needs is read/write mounted root +After=systemd-remount-fs.service + +[Service] +WorkingDirectory=/run/vyos-hostsd +RuntimeDirectory=vyos-hostsd +RuntimeDirectoryPreserve=yes +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-hostsd +Type=idle +KillMode=process + +SyslogIdentifier=vyos-hostsd +SyslogFacility=daemon + +Restart=on-failure + +# Does't work in Jessie but leave it here +User=root +Group=hostsd + +[Install] + +# Note: After= doesn't actually create a dependency, +# it just sets order for the case when both services are to start, +# and without RequiredBy it *does not* set vyos-hostsd to start. +RequiredBy=cloud-init-local.service vyos-router.service diff --git a/src/systemd/vyos-http-api.service b/src/systemd/vyos-http-api.service new file mode 100644 index 000000000..4fa68b4ff --- /dev/null +++ b/src/systemd/vyos-http-api.service @@ -0,0 +1,24 @@ +[Unit] +Description=VyOS HTTP API service +After=auditd.service systemd-user-sessions.service time-sync.target vyos-router.service +Requires=vyos-router.service + +[Service] +ExecStartPre=/usr/libexec/vyos/init/vyos-config +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-http-api-server +Type=idle +KillMode=process + +SyslogIdentifier=vyos-http-api +SyslogFacility=daemon + +Restart=on-failure + +# Does't work but leave it here +User=root +Group=vyattacfg + +[Install] +# Installing in a earlier target leaves ExecStartPre waiting +WantedBy=getty.target + diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service new file mode 100644 index 000000000..7e0bee8e1 --- /dev/null +++ b/src/systemd/wpa_supplicant-macsec@.service @@ -0,0 +1,17 @@ +[Unit] +Description=WPA supplicant daemon (macsec-specific version) +Requires=sys-subsystem-net-devices-%i.device +ConditionPathExists=/run/wpa_supplicant/%I.conf +After=vyos-router.service +RequiresMountsFor=/run + +# NetworkManager users will probably want the dbus version instead. + +[Service] +Type=simple +WorkingDirectory=/run/wpa_supplicant +PIDFile=/run/wpa_supplicant/%I.pid +ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I + +[Install] +WantedBy=multi-user.target diff --git a/src/tests/helper.py b/src/tests/helper.py new file mode 100644 index 000000000..a7e4f201c --- /dev/null +++ b/src/tests/helper.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import importlib.util + + +def prepare_module(file_path='', module_name=''): + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules[module_name] = module diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py new file mode 100644 index 000000000..e47770a7f --- /dev/null +++ b/src/tests/test_config_parser.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import tempfile +import unittest +from unittest import TestCase, mock + +import vyos.configtree + + +class TestConfigParser(TestCase): + def setUp(self): + with open('tests/data/config.valid', 'r') as f: + config_string = f.read() + self.config = vyos.configtree.ConfigTree(config_string) + + def test_top_level_valueless(self): + self.assertTrue(self.config.exists(["top-level-valueless-node"])) + + def test_top_level_leaf(self): + self.assertTrue(self.config.exists(["top-level-leaf-node"])) + self.assertEqual(self.config.return_value(["top-level-leaf-node"]), "foo") + + def test_top_level_tag(self): + self.assertTrue(self.config.exists(["top-level-tag-node"])) + # No sorting is intentional, child order must be preserved + self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["foo", "bar"]) + + def test_copy(self): + self.config.copy(["top-level-tag-node", "bar"], ["top-level-tag-node", "baz"]) + print(self.config.to_string()) + self.assertTrue(self.config.exists(["top-level-tag-node", "baz"])) + + def test_copy_duplicate(self): + with self.assertRaises(vyos.configtree.ConfigTreeError): + self.config.copy(["top-level-tag-node", "foo"], ["top-level-tag-node", "bar"]) + + def test_rename(self): + self.config.rename(["top-level-tag-node", "bar"], "quux") + print(self.config.to_string()) + self.assertTrue(self.config.exists(["top-level-tag-node", "quux"])) + + def test_rename_duplicate(self): + with self.assertRaises(vyos.configtree.ConfigTreeError): + self.config.rename(["top-level-tag-node", "foo"], "bar") diff --git a/src/tests/test_initial_setup.py b/src/tests/test_initial_setup.py new file mode 100644 index 000000000..1597025e8 --- /dev/null +++ b/src/tests/test_initial_setup.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import tempfile +import unittest +from unittest import TestCase, mock + +from vyos import xml +import vyos.configtree +import vyos.initialsetup as vis + + +class TestInitialSetup(TestCase): + def setUp(self): + with open('tests/data/config.boot.default', 'r') as f: + config_string = f.read() + self.config = vyos.configtree.ConfigTree(config_string) + self.xml = xml.load_configuration() + + def test_set_user_password(self): + vis.set_user_password(self.config, 'vyos', 'vyosvyos') + + # Old password hash from the default config + old_pw = '$6$QxPS.uk6mfo$9QBSo8u1FkH16gMyAVhus6fU3LOzvLR9Z9.82m3tiHFAxTtIkhaZSWssSgzt4v4dGAL8rhVQxTg0oAG9/q11h/' + new_pw = self.config.return_value(["system", "login", "user", "vyos", "authentication", "encrypted-password"]) + + # Just check it changed the hash, don't try to check if hash is good + self.assertNotEqual(old_pw, new_pw) + + def test_disable_user_password(self): + vis.disable_user_password(self.config, 'vyos') + new_pw = self.config.return_value(["system", "login", "user", "vyos", "authentication", "encrypted-password"]) + + self.assertEqual(new_pw, '!') + + def test_set_ssh_key_with_name(self): + test_ssh_key = " ssh-rsa fakedata vyos@vyos " + vis.set_user_ssh_key(self.config, 'vyos', test_ssh_key) + + key_type = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos@vyos", "type"]) + key_data = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos@vyos", "key"]) + + self.assertEqual(key_type, 'ssh-rsa') + self.assertEqual(key_data, 'fakedata') + self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + + def test_set_ssh_key_without_name(self): + # If key file doesn't include a name, the function will use user name for the key name + + test_ssh_key = " ssh-rsa fakedata " + vis.set_user_ssh_key(self.config, 'vyos', test_ssh_key) + + key_type = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos", "type"]) + key_data = self.config.return_value(["system", "login", "user", "vyos", "authentication", "public-keys", "vyos", "key"]) + + self.assertEqual(key_type, 'ssh-rsa') + self.assertEqual(key_data, 'fakedata') + self.assertTrue(self.xml.is_tag(["system", "login", "user", "vyos", "authentication", "public-keys"])) + + def test_create_user(self): + vis.create_user(self.config, 'jrandomhacker', password='qwerty', key=" ssh-rsa fakedata jrandomhacker@foovax ") + + self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker"])) + self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker", "authentication", "public-keys", "jrandomhacker@foovax"])) + self.assertTrue(self.config.exists(["system", "login", "user", "jrandomhacker", "authentication", "encrypted-password"])) + self.assertEqual(self.config.return_value(["system", "login", "user", "jrandomhacker", "level"]), "admin") + + def test_set_hostname(self): + vis.set_host_name(self.config, "vyos-test") + + self.assertEqual(self.config.return_value(["system", "host-name"]), "vyos-test") + + def test_set_name_servers(self): + vis.set_name_servers(self.config, ["192.0.2.10", "203.0.113.20"]) + servers = self.config.return_values(["system", "name-server"]) + + self.assertIn("192.0.2.10", servers) + self.assertIn("203.0.113.20", servers) + + def test_set_gateway(self): + vis.set_default_gateway(self.config, '192.0.2.1') + + self.assertTrue(self.config.exists(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '192.0.2.1'])) + self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route', '0.0.0.0/0', 'next-hop'])) + self.assertTrue(self.xml.is_tag(['protocols', 'static', 'multicast', 'route'])) + +if __name__ == "__main__": + unittest.main() + diff --git a/src/tests/test_task_scheduler.py b/src/tests/test_task_scheduler.py new file mode 100644 index 000000000..084bd868c --- /dev/null +++ b/src/tests/test_task_scheduler.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import tempfile +import unittest + +from vyos import ConfigError +try: + from src.conf_mode import task_scheduler +except ModuleNotFoundError: # for unittest.main() + import sys + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + from src.conf_mode import task_scheduler + + +class TestUpdateCrontab(unittest.TestCase): + + def test_verify(self): + tests = [ + {'name': 'one_task', + 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': None + }, + {'name': 'has_interval_and_spec', + 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '0 * * * *', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': ConfigError + }, + {'name': 'has_no_interval_and_spec', + 'tasks': [{'name': 'aaa', 'interval': '', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': ConfigError + }, + {'name': 'invalid_interval', + 'tasks': [{'name': 'aaa', 'interval': '1y', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': ConfigError + }, + {'name': 'invalid_interval_min', + 'tasks': [{'name': 'aaa', 'interval': '61m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': ConfigError + }, + {'name': 'invalid_interval_hour', + 'tasks': [{'name': 'aaa', 'interval': '25h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': ConfigError + }, + {'name': 'invalid_interval_day', + 'tasks': [{'name': 'aaa', 'interval': '32d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': ConfigError + }, + {'name': 'no_executable', + 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '', 'args': ''}], + 'expected': ConfigError + }, + {'name': 'invalid_executable', + 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/aaa', 'args': ''}], + 'expected': ConfigError + } + ] + for t in tests: + with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']): + if t['expected'] is not None: + with self.assertRaises(t['expected']): + task_scheduler.verify(t['tasks']) + else: + task_scheduler.verify(t['tasks']) + + def test_generate(self): + tests = [ + {'name': 'zero_task', + 'tasks': [], + 'expected': [] + }, + {'name': 'one_task', + 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': [ + '### Generated by vyos-update-crontab.py ###', + '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"'] + }, + {'name': 'one_task_with_hour', + 'tasks': [{'name': 'aaa', 'interval': '10h', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': [ + '### Generated by vyos-update-crontab.py ###', + '0 */10 * * * root sg vyattacfg \"/bin/ls -l\"'] + }, + {'name': 'one_task_with_day', + 'tasks': [{'name': 'aaa', 'interval': '10d', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}], + 'expected': [ + '### Generated by vyos-update-crontab.py ###', + '0 0 */10 * * root sg vyattacfg \"/bin/ls -l\"'] + }, + {'name': 'multiple_tasks', + 'tasks': [{'name': 'aaa', 'interval': '60m', 'spec': '', 'executable': '/bin/ls', 'args': '-l'}, + {'name': 'bbb', 'interval': '', 'spec': '0 0 * * *', 'executable': '/bin/ls', 'args': '-ltr'} + ], + 'expected': [ + '### Generated by vyos-update-crontab.py ###', + '*/60 * * * * root sg vyattacfg \"/bin/ls -l\"', + '0 0 * * * root sg vyattacfg \"/bin/ls -ltr\"'] + } + ] + for t in tests: + with self.subTest(msg=t['name'], tasks=t['tasks'], expected=t['expected']): + task_scheduler.crontab_file = tempfile.mkstemp()[1] + task_scheduler.generate(t['tasks']) + if len(t['expected']) > 0: + self.assertTrue(os.path.isfile(task_scheduler.crontab_file)) + with open(task_scheduler.crontab_file) as f: + actual = f.read() + self.assertEqual(t['expected'], actual.splitlines()) + os.remove(task_scheduler.crontab_file) + else: + self.assertFalse(os.path.isfile(task_scheduler.crontab_file)) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/tests/test_util.py b/src/tests/test_util.py new file mode 100644 index 000000000..0e56a67a8 --- /dev/null +++ b/src/tests/test_util.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import unittest +from unittest import TestCase + +import vyos.util + + +class TestVyOSUtil(TestCase): + def setUp(self): + pass + + def test_key_mangline(self): + data = {"foo-bar": {"baz-quux": None}} + expected_data = {"foo_bar": {"baz_quux": None}} + new_data = vyos.util.mangle_dict_keys(data, '-', '_') + self.assertEqual(new_data, expected_data) + diff --git a/src/utils/initial-setup b/src/utils/initial-setup new file mode 100644 index 000000000..37fc45751 --- /dev/null +++ b/src/utils/initial-setup @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +import argparse + +import vyos.configtree + + +parser = argparse.ArgumentParser() + +parser.add_argument("--ssh", help="Enable SSH", action="store_true") +parser.add_argument("--ssh-port", help="SSH port", type=int, action="store", default=22) + +parser.add_argument("--intf-address", help="Set interface address", type=str, action="append") + +parser.add_argument("config_file", help="Configuration file to modify", type=str) + +args = parser.parse_args() + +# Load the config file +with open(args.config_file, 'r') as f: + config_file = f.read() + +config = vyos.configtree.ConfigTree(config_file) + + +# Interface names and addresses are comma-separated, +# we need to split them +intf_addrs = list(map(lambda s: s.split(","), args.intf_address)) + +# Enable SSH, if requested +if args.ssh: + config.set(["service", "ssh", "port"], value=str(args.ssh_port)) + +# Assign addresses to interfaces +if intf_addrs: + for a in intf_addrs: + config.set(["interfaces", "ethernet", a[0], "address"], value=a[1]) + config.set_tag(["interfaces", "ethernet"]) + +print( config.to_string() ) diff --git a/src/utils/vyos-config-file-query b/src/utils/vyos-config-file-query new file mode 100755 index 000000000..a10c7e9b3 --- /dev/null +++ b/src/utils/vyos-config-file-query @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import re +import os +import sys +import json +import argparse + +import vyos.configtree + + +arg_parser = argparse.ArgumentParser() +arg_parser.add_argument('-p', '--path', type=str, + help="VyOS config node, e.g. \"system config-management commit-revisions\"", required=True) +arg_parser.add_argument('-f', '--file', type=str, help="VyOS config file, e.g. /config/config.boot", required=True) + +arg_parser.add_argument('-s', '--separator', type=str, default=' ', help="Value separator for the plain format") +arg_parser.add_argument('-j', '--json', action='store_true') + +op_group = arg_parser.add_mutually_exclusive_group(required=True) +op_group.add_argument('--return-value', action='store_true', help="Return a single node value") +op_group.add_argument('--return-values', action='store_true', help="Return all values of a multi-value node") +op_group.add_argument('--list-nodes', action='store_true', help="List children of a node") +op_group.add_argument('--exists', action='store_true', help="Check if a node exists") + +args = arg_parser.parse_args() + + +try: + with open(args.file, 'r') as f: + config_file = f.read() +except OSError as e: + print("Could not read the config file: {0}".format(e)) + sys.exit(1) + +try: + config = vyos.configtree.ConfigTree(config_file) +except Exception as e: + print(e) + sys.exit(1) + + +path = re.split(r'\s+', args.path) +values = None + +if args.exists: + if config.exists(path): + sys.exit(0) + else: + sys.exit(1) +elif args.return_value: + try: + values = [config.return_value(path)] + except vyos.configtree.ConfigTreeError as e: + print(e) + sys.exit(1) +elif args.return_values: + try: + values = config.return_values(path) + except vyos.configtree.ConfigTreeError as e: + print(e) + sys.exit(1) +elif args.list_nodes: + values = config.list_nodes(path) + if not values: + values = [] +else: + # Can't happen + print("Operation required") + sys.exit(1) + + +if values: + if args.json: + print(json.dumps(values)) + else: + if len(values) == 1: + print(values[0]) + else: + # XXX: assuming values never contain quotes + values = list(map(lambda s: "\'{0}\'".format(s), values)) + values_str = args.separator.join(values) + print(values_str) + +sys.exit(0) diff --git a/src/utils/vyos-config-to-commands b/src/utils/vyos-config-to-commands new file mode 100755 index 000000000..8b50f7c5d --- /dev/null +++ b/src/utils/vyos-config-to-commands @@ -0,0 +1,29 @@ +#!/usr/bin/python3 + +import sys + +from signal import signal, SIGPIPE, SIG_DFL +from vyos.configtree import ConfigTree + +signal(SIGPIPE,SIG_DFL) + +config_string = None +if (len(sys.argv) == 1): + # If no argument given, act as a pipe + config_string = sys.stdin.read() +else: + file_name = sys.argv[1] + try: + with open(file_name, 'r') as f: + config_string = f.read() + except OSError as e: + print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr) + +try: + config = ConfigTree(config_string) + commands = config.to_commands() +except ValueError as e: + print("Could not parse the config file: {0}".format(e), file=sys.stderr) + sys.exit(1) + +print(commands) diff --git a/src/utils/vyos-config-to-json b/src/utils/vyos-config-to-json new file mode 100755 index 000000000..e03fd6a59 --- /dev/null +++ b/src/utils/vyos-config-to-json @@ -0,0 +1,40 @@ +#!/usr/bin/python3 + +import sys +import json + +from signal import signal, SIGPIPE, SIG_DFL +from vyos.configtree import ConfigTree + +signal(SIGPIPE,SIG_DFL) + +config_string = None +if (len(sys.argv) == 1): + # If no argument given, act as a pipe + config_string = sys.stdin.read() +else: + file_name = sys.argv[1] + try: + with open(file_name, 'r') as f: + config_string = f.read() + except OSError as e: + print("Could not read config file {0}: {1}".format(file_name, e), file=sys.stderr) + +# This script is usually called with the output of "cli-shell-api showCfg", which does not +# escape backslashes. "ConfigTree()" expects escaped backslashes when parsing a config +# string (and also prints them itself). Therefore this script would fail. +# Manually escape backslashes here to handle backslashes in any configuration strings +# properly. The alternative would be to modify the output of "cli-shell-api showCfg", +# but that may be break other things who rely on that specific output. +config_string = config_string.replace("\\", "\\\\") + +try: + config = ConfigTree(config_string) + json_str = config.to_json() + # Pretty print + json_str = json.dumps(json.loads(json_str), indent=4, sort_keys=True) +except ValueError as e: + print("Could not parse the config file: {0}".format(e), file=sys.stderr) + sys.exit(1) + +print(json_str) diff --git a/src/utils/vyos-hostsd-client b/src/utils/vyos-hostsd-client new file mode 100755 index 000000000..48ebc83f7 --- /dev/null +++ b/src/utils/vyos-hostsd-client @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys +import argparse + +import vyos.hostsd_client + +parser = argparse.ArgumentParser(allow_abbrev=False) +group = parser.add_mutually_exclusive_group() + +group.add_argument('--add-name-servers', type=str, nargs='*') +group.add_argument('--delete-name-servers', action='store_true') +group.add_argument('--get-name-servers', type=str, const='.*', nargs='?') + +group.add_argument('--add-name-server-tags-recursor', type=str, nargs='*') +group.add_argument('--delete-name-server-tags-recursor', type=str, nargs='*') +group.add_argument('--get-name-server-tags-recursor', action='store_true') + +group.add_argument('--add-name-server-tags-system', type=str, nargs='*') +group.add_argument('--delete-name-server-tags-system', type=str, nargs='*') +group.add_argument('--get-name-server-tags-system', action='store_true') + +group.add_argument('--add-forward-zone', type=str, nargs='?') +group.add_argument('--delete-forward-zones', type=str, nargs='*') +group.add_argument('--get-forward-zones', action='store_true') + +group.add_argument('--add-search-domains', type=str, nargs='*') +group.add_argument('--delete-search-domains', action='store_true') +group.add_argument('--get-search-domains', type=str, const='.*', nargs='?') + +group.add_argument('--add-hosts', type=str, nargs='*') +group.add_argument('--delete-hosts', action='store_true') +group.add_argument('--get-hosts', type=str, const='.*', nargs='?') + +group.add_argument('--set-host-name', type=str) + +# for --set-host-name +parser.add_argument('--domain-name', type=str) + +# for forward zones +parser.add_argument('--nameservers', type=str, nargs='*') +parser.add_argument('--addnta', action='store_true') +parser.add_argument('--recursion-desired', action='store_true') + +parser.add_argument('--tag', type=str) + +# users must call --apply either in the same command or after they're done +parser.add_argument('--apply', action="store_true") + +args = parser.parse_args() + +try: + client = vyos.hostsd_client.Client() + ops = 1 + + if args.add_name_servers: + if not args.tag: + raise ValueError("--tag is required for this operation") + client.add_name_servers({args.tag: args.add_name_servers}) + elif args.delete_name_servers: + if not args.tag: + raise ValueError("--tag is required for this operation") + client.delete_name_servers([args.tag]) + elif args.get_name_servers: + print(client.get_name_servers(args.get_name_servers)) + + elif args.add_name_server_tags_recursor: + client.add_name_server_tags_recursor(args.add_name_server_tags_recursor) + elif args.delete_name_server_tags_recursor: + client.delete_name_server_tags_recursor(args.delete_name_server_tags_recursor) + elif args.get_name_server_tags_recursor: + print(client.get_name_server_tags_recursor()) + + elif args.add_name_server_tags_system: + client.add_name_server_tags_system(args.add_name_server_tags_system) + elif args.delete_name_server_tags_system: + client.delete_name_server_tags_system(args.delete_name_server_tags_system) + elif args.get_name_server_tags_system: + print(client.get_name_server_tags_system()) + + elif args.add_forward_zone: + if not args.nameservers: + raise ValueError("--nameservers is required for this operation") + client.add_forward_zones( + { args.add_forward_zone: { + 'nslist': args.nameservers, + 'addNTA': args.addnta, + 'recursion-desired': args.recursion_desired + } + }) + elif args.delete_forward_zones: + client.delete_forward_zones(args.delete_forward_zones) + elif args.get_forward_zones: + print(client.get_forward_zones()) + + elif args.add_search_domains: + if not args.tag: + raise ValueError("--tag is required for this operation") + client.add_search_domains({args.tag: args.add_search_domains}) + elif args.delete_search_domains: + if not args.tag: + raise ValueError("--tag is required for this operation") + client.delete_search_domains([args.tag]) + elif args.get_search_domains: + print(client.get_search_domains(args.get_search_domains)) + + elif args.add_hosts: + if not args.tag: + raise ValueError("--tag is required for this operation") + data = {} + for h in args.add_hosts: + entry = {} + params = h.split(",") + if len(params) < 2: + raise ValueError("Malformed host entry") + entry['address'] = params[1] + entry['aliases'] = params[2:] + data[params[0]] = entry + client.add_hosts({args.tag: data}) + elif args.delete_hosts: + if not args.tag: + raise ValueError("--tag is required for this operation") + client.delete_hosts([args.tag]) + elif args.get_hosts: + print(client.get_hosts(args.get_hosts)) + + elif args.set_host_name: + if not args.domain_name: + raise ValueError('--domain-name is required for this operation') + client.set_host_name({'host_name': args.set_host_name, 'domain_name': args.domain_name}) + + elif args.apply: + pass + else: + ops = 0 + + if args.apply: + client.apply() + + if ops == 0: + raise ValueError("Operation required") + +except ValueError as e: + print("Incorrect options: {0}".format(e)) + sys.exit(1) +except vyos.hostsd_client.VyOSHostsdError as e: + print("Server returned an error: {0}".format(e)) + sys.exit(1) + diff --git a/src/validators/dotted-decimal b/src/validators/dotted-decimal new file mode 100755 index 000000000..652110346 --- /dev/null +++ b/src/validators/dotted-decimal @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + +area = sys.argv[1] + +res = re.match(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$', area) +if not res: + print("\'{0}\' is not a valid dotted decimal value".format(area)) + sys.exit(1) +else: + components = res.groups() + for n in range(0, 4): + if (int(components[n]) > 255): + print("Invalid component of a dotted decimal value: {0} exceeds 255".format(components[n])) + sys.exit(1) + +sys.exit(0) diff --git a/src/validators/file-exists b/src/validators/file-exists new file mode 100755 index 000000000..5cef6b199 --- /dev/null +++ b/src/validators/file-exists @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Description: +# Check if a given file exists on the system. Used for files that +# are referenced from the CLI and need to be preserved during an image upgrade. +# Warn the user if these aren't under /config + +import os +import sys +import argparse + + +def exit(strict, message): + if strict: + sys.exit(f'ERROR: {message}') + print(f'WARNING: {message}', file=sys.stderr) + sys.exit() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.") + parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'") + parser.add_argument("file", type=str, help="Path of file to validate") + + args = parser.parse_args() + + # + # Always check if the given file exists + # + if not os.path.exists(args.file): + exit(args.error, f"File '{args.file}' not found") + + # + # Optional check if the file is under a certain directory path + # + if args.directory: + # remove directory path from path to verify + rel_filename = args.file.replace(args.directory, '').lstrip('/') + + if not os.path.exists(args.directory + '/' + rel_filename): + exit(args.error, + f"'{args.file}' lies outside of '{args.directory}' directory.\n" + "It will not get preserved during image upgrade!" + ) + + sys.exit() diff --git a/src/validators/fqdn b/src/validators/fqdn new file mode 100755 index 000000000..347ffda42 --- /dev/null +++ b/src/validators/fqdn @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + + +# pattern copied from: https://www.regextester.com/103452 +pattern = "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)" + + +if __name__ == '__main__': + if len(sys.argv) != 2: + sys.exit(1) + if not re.match(pattern, sys.argv[1]): + sys.exit(1) + sys.exit(0) diff --git a/src/validators/interface-address b/src/validators/interface-address new file mode 100755 index 000000000..4c203956b --- /dev/null +++ b/src/validators/interface-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-host $1 || ipaddrcheck --is-ipv6-host $1 diff --git a/src/validators/ip-address b/src/validators/ip-address new file mode 100755 index 000000000..51fb72c85 --- /dev/null +++ b/src/validators/ip-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-single $1 diff --git a/src/validators/ip-cidr b/src/validators/ip-cidr new file mode 100755 index 000000000..987bf84ca --- /dev/null +++ b/src/validators/ip-cidr @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-cidr $1 diff --git a/src/validators/ip-host b/src/validators/ip-host new file mode 100755 index 000000000..f2906e8cf --- /dev/null +++ b/src/validators/ip-host @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-host $1 diff --git a/src/validators/ip-prefix b/src/validators/ip-prefix new file mode 100755 index 000000000..e58aad395 --- /dev/null +++ b/src/validators/ip-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-net $1 diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol new file mode 100755 index 000000000..078f8e319 --- /dev/null +++ b/src/validators/ip-protocol @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +from sys import argv,exit + +if __name__ == '__main__': + if len(argv) != 2: + exit(1) + + input = argv[1] + try: + # IP protocol can be in the range 0 - 255, thus the range must end with 256 + if int(input) in range(0, 256): + exit(0) + except ValueError: + pass + + pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \ + "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \ + "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \ + "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \ + "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \ + "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b" + if re.match(pattern, input): + exit(0) + + exit(1) diff --git a/src/validators/ipv4-address b/src/validators/ipv4-address new file mode 100755 index 000000000..872a7645a --- /dev/null +++ b/src/validators/ipv4-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-single $1 diff --git a/src/validators/ipv4-address-exclude b/src/validators/ipv4-address-exclude new file mode 100755 index 000000000..80ad17d45 --- /dev/null +++ b/src/validators/ipv4-address-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv4-address "${arg:1}" diff --git a/src/validators/ipv4-host b/src/validators/ipv4-host new file mode 100755 index 000000000..f42feffa4 --- /dev/null +++ b/src/validators/ipv4-host @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-host $1 diff --git a/src/validators/ipv4-prefix b/src/validators/ipv4-prefix new file mode 100755 index 000000000..8ec8a2c45 --- /dev/null +++ b/src/validators/ipv4-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv4-net $1 diff --git a/src/validators/ipv4-prefix-exclude b/src/validators/ipv4-prefix-exclude new file mode 100755 index 000000000..4f7de400a --- /dev/null +++ b/src/validators/ipv4-prefix-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv4-prefix "${arg:1}" diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range new file mode 100755 index 000000000..ae3f3f163 --- /dev/null +++ b/src/validators/ipv4-range @@ -0,0 +1,33 @@ +#!/bin/bash + +# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter +ip2dec () { + local a b c d ip=$@ + IFS=. read -r a b c d <<< "$ip" + printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))" +} + +# Only run this if there is a hypen present in $1 +if [[ "$1" =~ "-" ]]; then + # This only works with real bash (<<<) - split IP addresses into array with + # hyphen as delimiter + readarray -d - -t strarr <<< $1 + + ipaddrcheck --is-ipv4-single ${strarr[0]} + if [ $? -gt 0 ]; then + exit 1 + fi + + ipaddrcheck --is-ipv4-single ${strarr[1]} + if [ $? -gt 0 ]; then + exit 1 + fi + + start=$(ip2dec ${strarr[0]}) + stop=$(ip2dec ${strarr[1]}) + if [ $start -ge $stop ]; then + exit 1 + fi +fi + +exit 0 diff --git a/src/validators/ipv4-range-exclude b/src/validators/ipv4-range-exclude new file mode 100755 index 000000000..3787b4dec --- /dev/null +++ b/src/validators/ipv4-range-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv4-range "${arg:1}" diff --git a/src/validators/ipv6 b/src/validators/ipv6 new file mode 100755 index 000000000..f18d4a63e --- /dev/null +++ b/src/validators/ipv6 @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6 $1 diff --git a/src/validators/ipv6-address b/src/validators/ipv6-address new file mode 100755 index 000000000..e5d68d756 --- /dev/null +++ b/src/validators/ipv6-address @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-single $1 diff --git a/src/validators/ipv6-host b/src/validators/ipv6-host new file mode 100755 index 000000000..f7a745077 --- /dev/null +++ b/src/validators/ipv6-host @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-host $1 diff --git a/src/validators/ipv6-prefix b/src/validators/ipv6-prefix new file mode 100755 index 000000000..e43616350 --- /dev/null +++ b/src/validators/ipv6-prefix @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-ipv6-net $1 diff --git a/src/validators/mac-address b/src/validators/mac-address new file mode 100755 index 000000000..b2d3496f4 --- /dev/null +++ b/src/validators/mac-address @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys + + +pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$" + + +if __name__ == '__main__': + if len(sys.argv) != 2: + sys.exit(1) + if not re.match(pattern, sys.argv[1]): + sys.exit(1) + sys.exit(0) diff --git a/src/validators/script b/src/validators/script new file mode 100755 index 000000000..2665ec1f6 --- /dev/null +++ b/src/validators/script @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# +# numeric value validator +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import shlex + +import vyos.util + + +if __name__ == '__main__': + if len(sys.argv) < 2: + sys.exit('Please specify script file to check') + + # if the "script" is a script+ stowaway arguments, this removes the aguements + script = shlex.split(sys.argv[1])[0] + + if not os.path.exists(script): + sys.exit(f'File {script} does not exist') + + if not (os.path.isfile(script) and os.access(script, os.X_OK)): + sys.exit('File {script} is not an executable file') + + # File outside the config dir is just a warning + if not vyos.util.file_is_persistent(script): + sys.exit( + 'Warning: file {path} is outside the / config directory\n' + 'It will not be automatically migrated to a new image on system update' + ) diff --git a/src/validators/timezone b/src/validators/timezone new file mode 100755 index 000000000..baf5abca2 --- /dev/null +++ b/src/validators/timezone @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import sys + +from vyos.util import cmd + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("--validate", action="store", required=True, help="Check if timezone is valid") + args = parser.parse_args() + + tz_data = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::') + tz_data = tz_data.split('\n') + + if args.validate not in tz_data: + sys.exit("the timezone can't be found in the timezone list") + sys.exit() diff --git a/src/validators/vrf-name b/src/validators/vrf-name new file mode 100755 index 000000000..7b6313888 --- /dev/null +++ b/src/validators/vrf-name @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +from sys import argv, exit + +if __name__ == '__main__': + if len(argv) != 2: + exit(1) + + vrf = argv[1] + length = len(vrf) + + if length not in range(1, 16): + exit(1) + + # Treat loopback interface "lo" explicitly. Adding "lo" explicitly to the + # following regex pattern would deny any VRF name starting with lo - thuse + # local-vrf would be illegal - and that we do not want. + if vrf == "lo": + exit(1) + + pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \ + "vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wlm)\d+(\.\d+(v.+)?)?$).*$" + if not re.match(pattern, vrf): + exit(1) + + exit(0) diff --git a/src/validators/wireless-phy b/src/validators/wireless-phy new file mode 100755 index 000000000..513a902de --- /dev/null +++ b/src/validators/wireless-phy @@ -0,0 +1,25 @@ +#!/bin/sh +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +if [ ! -d /sys/class/ieee80211 ]; then + echo No IEEE 802.11 physical interfaces detected + exit 1 +fi + +if [ ! -e /sys/class/ieee80211/$1 ]; then + echo Device interface "$1" does not exist + exit 1 +fi diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..9348520b5 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,5 @@ +python/ +lxml +pylint +nose +coverage diff --git a/tests/data/config.boot.default b/tests/data/config.boot.default new file mode 100644 index 000000000..0a75716b8 --- /dev/null +++ b/tests/data/config.boot.default @@ -0,0 +1,40 @@ +system { + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$QxPS.uk6mfo$9QBSo8u1FkH16gMyAVhus6fU3LOzvLR9Z9.82m3tiHFAxTtIkhaZSWssSgzt4v4dGAL8rhVQxTg0oAG9/q11h/ + plaintext-password "" + } + level admin + } + } + syslog { + global { + facility all { + level notice + } + facility protocols { + level debug + } + } + } + ntp { + server "0.pool.ntp.org" + server "1.pool.ntp.org" + server "2.pool.ntp.org" + } + console { + device ttyS0 { + speed 9600 + } + } + config-management { + commit-revisions 100 + } +} + +interfaces { + loopback lo { + } +} diff --git a/tests/data/config.valid b/tests/data/config.valid new file mode 100644 index 000000000..1fbdd1505 --- /dev/null +++ b/tests/data/config.valid @@ -0,0 +1,39 @@ +/* top level leaf node */ +top-level-leaf-node foo + +top-level-valueless-node + +top-level-tag-node foo { + top-level-tag-node-child some-value +} + +top-level-tag-node bar { + top-level-tag-node-child another-value +} + +normal-node { + normal-node-child { + valueless-node + multi-node value1 + /* valueless node comment */ + another-valueless-node + multi-node value1 + tag-node foo { + } + one-more-valueless-node + tag-node bar { + some-option some-value + } + } + option-with-quoted-value "some-value" +} + +trailing-leaf-node-option some-value + +empty-node { +} + +trailing-leaf-node-without-value + +// Trailing comment +// Another trailing comment diff --git a/tests/data/interface-definitions/test-op.xml b/tests/data/interface-definitions/test-op.xml new file mode 100644 index 000000000..50bd686ae --- /dev/null +++ b/tests/data/interface-definitions/test-op.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="foo"> + <properties> + <help>foo</help> + </properties> + <children> + <leafNode name="bar"> + <command>/usr/bin/bar</command> + <properties> + <help>bar</help> + <completionHelp> + <list>foo bar</list> + <path>interfaces tunnel</path> + <script>/usr/bin/foo</script> + </completionHelp> + </properties> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/tests/data/interface-definitions/test.xml b/tests/data/interface-definitions/test.xml new file mode 100644 index 000000000..fbb302e70 --- /dev/null +++ b/tests/data/interface-definitions/test.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="foo"> + <properties> + <help>foo</help> + </properties> + <children> + <leafNode name="bar"> + <properties> + <help>bar</help> + <valueHelp> + <format>bar</format> + <description>bar</description> + </valueHelp> + <completionHelp> + <list>foo bar</list> + <path>interfaces tunnel</path> + <script>/usr/bin/foo</script> + </completionHelp> + </properties> + </leafNode> + </children> + </node> +</interfaceDefinition> |